/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.androidthings.imageclassifier;
import android.speech.tts.TextToSpeech;
import com.example.androidthings.imageclassifier.classifier.Classifier.Recognition;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.NavigableMap;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
public class TtsSpeaker {
private static final String UTTERANCE_ID
= "com.example.androidthings.imageclassifier.UTTERANCE_ID";
private static final float HUMOR_THRESHOLD = 0.2f;
private static final Random RANDOM = new Random();
private static final List<Utterance> SHUTTER_SOUNDS = new ArrayList<>();
private static final List<Utterance> JOKES = new ArrayList<>();
static {
SHUTTER_SOUNDS.add(new ShutterUtterance("Click!"));
SHUTTER_SOUNDS.add(new ShutterUtterance("Cheeeeese!"));
SHUTTER_SOUNDS.add(new ShutterUtterance("Smile!"));
JOKES.add(new SimpleUtterance("It's a bird! It's a plane! It's... it's..."));
JOKES.add(new SimpleUtterance("Oops, someone left the lens cap on! Just kidding..."));
JOKES.add(new SimpleUtterance("Hey, that looks like me! Just kidding..."));
JOKES.add(new ISeeDeadPeopleUtterance());
}
/**
* Don't play the same joke within this span of time
*/
private static final long JOKE_COOLDOWN_MILLIS = TimeUnit.MINUTES.toMillis(2);
/**
* For multiple results, speak only the first if it has at least this much confidence
*/
private static final float SINGLE_ANSWER_CONFIDENCE_THRESHOLD = 0.4f;
/**
* Stores joke utterances keyed by time last spoken.
*/
private NavigableMap<Long, Utterance> mJokes;
/**
* Controls where to use jokes or not. If true, jokes will be applied randomly. If false, no
* joke will ever be played. Use {@link #setHasSenseOfHumor(boolean)} to change the mood.
*/
private boolean mHasSenseOfHumor = true;
public TtsSpeaker() {
mJokes = new TreeMap<>();
long key = 0L;
for (Utterance joke : JOKES) {
// can't insert them with same key
mJokes.put(key++, joke);
}
}
public void speakReady(TextToSpeech tts) {
tts.speak("I'm ready!", TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID);
}
public void speakShutterSound(TextToSpeech tts) {
getRandomElement(SHUTTER_SOUNDS).speak(tts);
}
public void speakResults(TextToSpeech tts, List<Recognition> results) {
if (results.isEmpty()) {
tts.speak("I don't understand what I see.", TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID);
if (isFeelingFunnyNow()) {
tts.speak("Please don't unplug me, I'll do better next time.",
TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID);
}
} else {
if (isFeelingFunnyNow()) {
playJoke(tts);
}
if (results.size() == 1
|| results.get(0).getConfidence() > SINGLE_ANSWER_CONFIDENCE_THRESHOLD) {
tts.speak(String.format(Locale.getDefault(),
"I see a %s", results.get(0).getTitle()),
TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID);
} else {
tts.speak(String.format(Locale.getDefault(), "This is a %s, or maybe a %s",
results.get(0).getTitle(), results.get(1).getTitle()),
TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID);
}
}
}
private boolean playJoke(TextToSpeech tts) {
long now = System.currentTimeMillis();
// choose a random joke whose last occurrence was far enough in the past
SortedMap<Long, Utterance> availableJokes = mJokes.headMap(now - JOKE_COOLDOWN_MILLIS);
Utterance joke = null;
if (!availableJokes.isEmpty()) {
int r = RANDOM.nextInt(availableJokes.size());
int i = 0;
for (Long key : availableJokes.keySet()) {
if (i++ == r) {
joke = availableJokes.remove(key); // also removes from mJokes
break;
}
}
}
if (joke != null) {
joke.speak(tts);
// add it back with the current time
mJokes.put(now, joke);
return true;
}
return false;
}
private static <T> T getRandomElement(List<T> list) {
return list.get(RANDOM.nextInt(list.size()));
}
private boolean isFeelingFunnyNow() {
return mHasSenseOfHumor && RANDOM.nextFloat() < HUMOR_THRESHOLD;
}
public void setHasSenseOfHumor(boolean hasSenseOfHumor) {
this.mHasSenseOfHumor = hasSenseOfHumor;
}
public boolean hasSenseOfHumor() {
return mHasSenseOfHumor;
}
interface Utterance {
void speak(TextToSpeech tts);
}
private static class SimpleUtterance implements Utterance {
private final String mMessage;
SimpleUtterance(String message) {
mMessage = message;
}
@Override
public void speak(TextToSpeech tts) {
tts.speak(mMessage, TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID);
}
}
private static class ShutterUtterance extends SimpleUtterance {
ShutterUtterance(String message) {
super(message);
}
@Override
public void speak(TextToSpeech tts) {
tts.setPitch(1.5f);
tts.setSpeechRate(1.5f);
super.speak(tts);
tts.setPitch(1f);
tts.setSpeechRate(1f);
}
}
private static class ISeeDeadPeopleUtterance implements Utterance {
@Override
public void speak(TextToSpeech tts) {
tts.setPitch(0.2f);
tts.speak("I see dead people...", TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID);
tts.setPitch(1);
tts.speak("Just kidding...", TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID);
}
}
}