Create working proof of concept for Sonos TTS
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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")
|
||||
|
||||
29
app/src/main/java/io/bartek/ttsserver/web/Endpoints.kt
Normal file
29
app/src/main/java/io/bartek/ttsserver/web/Endpoints.kt
Normal 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")))
|
||||
}
|
||||
@@ -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, "")
|
||||
|
||||
@@ -20,7 +20,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user