Create working proof of concept for Sonos TTS

This commit is contained in:
2020-05-14 16:35:32 +02:00
parent 28b9e95944
commit 886d215f6f
6 changed files with 110 additions and 7 deletions

View File

@@ -33,6 +33,7 @@ dependencies {
implementation 'org.nanohttpd:nanohttpd:2.2.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'com.github.vmichalak:sonos-controller:v.0.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View File

@@ -19,7 +19,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:name="io.bartek.ttsserver.help.HelpActivity"
android:parentActivityName=".ttsserver.MainActivity" />

View File

@@ -5,14 +5,45 @@ import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import io.bartek.ttsserver.exception.TTSException
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.util.*
data class SpeechData(val stream: InputStream, val size: Long)
class TTS(context: Context, initListener: TextToSpeech.OnInitListener) {
class TTS(private val context: Context, initListener: TextToSpeech.OnInitListener) {
private val tts = TextToSpeech(context, initListener)
private val messageDigest = MessageDigest.getInstance("SHA-256")
fun createTTSFile(text: String, language: Locale): File {
val digest = hash(text, language)
val filename = "tts_$digest.wav"
val file = File(context.cacheDir, filename)
val uuid = UUID.randomUUID().toString()
val lock = Lock()
tts.setOnUtteranceProgressListener(TTSProcessListener(uuid, lock))
synchronized(lock) {
tts.language = language
tts.synthesizeToFile(text, null, file, uuid)
lock.wait()
}
if(!lock.success) {
throw TTSException()
}
return file
}
private fun hash(text: String, language: Locale): String {
val bytes = "$text$language".toByteArray()
val digest = messageDigest.digest(bytes)
return digest.fold("", { str, it -> str + "%02x".format(it) })
}
fun fetchTTSStream(text: String, language: Locale): SpeechData {
val file = createTempFile("tmp_tts_server", ".wav")

View File

@@ -0,0 +1,29 @@
package io.bartek.ttsserver.web
import android.content.UriMatcher
import android.net.Uri
enum class Endpoint(val uri: String, val id: Int) {
UNKNOWN("/", 1),
SAY("/say", 2),
WAVE("/wave", 3),
SONOS("/sonos", 4),
SONOS_CACHE("/sonos/*", 5);
companion object {
fun of(id: Int) = values().firstOrNull { it.id == id } ?: UNKNOWN
}
}
class Endpoints {
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
Endpoint.values().forEach {
uriMatcher.addURI("", it.uri, it.id)
}
}
fun match(uri: String) =
Endpoint.of(uriMatcher.match(Uri.parse("content://$uri")))
}

View File

@@ -2,6 +2,7 @@ package io.bartek.ttsserver.web
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.speech.tts.TextToSpeech
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
@@ -12,6 +13,9 @@ import io.bartek.ttsserver.service.ForegroundService
import io.bartek.ttsserver.service.ServiceState
import io.bartek.ttsserver.tts.TTS
import org.json.JSONObject
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.util.*
private data class TTSRequestData(val text: String, val language: Locale)
@@ -21,14 +25,17 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
TextToSpeech.OnInitListener {
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private val tts = TTS(context, this)
private val endpoints = Endpoints()
override fun serve(session: IHTTPSession?): Response {
try {
session?.let {
return when (it.uri) {
"/wave" -> wave(it)
"/say" -> say(it)
else -> throw ResponseException(NOT_FOUND, "")
return when (endpoints.match(it.uri)) {
Endpoint.SAY -> say(it)
Endpoint.WAVE -> wave(it)
Endpoint.SONOS -> sonos(it)
Endpoint.SONOS_CACHE -> sonosCache(it)
Endpoint.UNKNOWN -> throw ResponseException(NOT_FOUND, "")
}
}
@@ -40,6 +47,40 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
}
}
private fun sonos(session: IHTTPSession): Response {
// SonosDiscovery.discover().firstOrNull { it.zoneGroupState.name == "Salon" }
// ?.play()
if (session.method != Method.POST) {
throw ResponseException(METHOD_NOT_ALLOWED, "")
}
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
throw ResponseException(BAD_REQUEST, "")
}
val (text, language) = getRequestData(session)
val file = tts.createTTSFile(text, language)
return newFixedLengthResponse(file.toString())
}
private fun sonosCache(session: IHTTPSession): Response {
if (session.method != Method.GET) {
throw ResponseException(METHOD_NOT_ALLOWED, "")
}
val filename = Uri.parse(session.uri).lastPathSegment ?: throw ResponseException(BAD_REQUEST, "")
val file = File(context.cacheDir, filename)
if(!file.exists()) {
throw ResponseException(NOT_FOUND, "")
}
val stream = BufferedInputStream(FileInputStream(file))
val size = file.length()
return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
}
private fun wave(session: IHTTPSession): Response {
if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) {
throw ResponseException(NOT_FOUND, "")

View File

@@ -20,7 +20,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}