jetzt als Hintergrund Service mit Wake-Word-Aktivierung
This commit is contained in:
179
app/src/main/java/com/example/jarvis_stts/JarviceService.kt
Normal file
179
app/src/main/java/com/example/jarvis_stts/JarviceService.kt
Normal file
@@ -0,0 +1,179 @@
|
||||
package com.example.jarvis_stts
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import org.vosk.Model
|
||||
import org.vosk.Recognizer
|
||||
import org.vosk.android.RecognitionListener
|
||||
import org.vosk.android.SpeechService
|
||||
import java.io.File
|
||||
import android.content.pm.ServiceInfo
|
||||
import org.json.JSONObject
|
||||
|
||||
class JarvisService : Service(), RecognitionListener {
|
||||
|
||||
private var voskService: SpeechService? = null
|
||||
private var voskModel: Model? = null
|
||||
private var isInteracting = false
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "JarvisServiceChannel"
|
||||
const val ACTION_START = "ACTION_START"
|
||||
const val ACTION_PAUSE = "ACTION_PAUSE"
|
||||
const val ACTION_RESUME = "ACTION_RESUME"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
|
||||
// Für Android 14 (API 34) und höher müssen wir den Typ beim Starten mitgeben
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(
|
||||
1,
|
||||
createNotification("J.A.R.V.I.S. hört zu..."),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
)
|
||||
} else {
|
||||
startForeground(1, createNotification("J.A.R.V.I.S. hört zu..."))
|
||||
}
|
||||
|
||||
initVosk()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_START -> resumeListening()
|
||||
ACTION_PAUSE -> pauseListening()
|
||||
ACTION_RESUME -> resumeListening()
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"J.A.R.V.I.S. Background Service",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(text: String): android.app.Notification {
|
||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("J.A.R.V.I.S. ist aktiv")
|
||||
.setContentText(text)
|
||||
.setSmallIcon(android.R.drawable.ic_btn_speak_now) // Standard Android Icon
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun updateNotification(text: String) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.notify(1, createNotification(text))
|
||||
}
|
||||
|
||||
private fun initVosk() {
|
||||
val baseDir = getExternalFilesDir(null)
|
||||
val modelFolder = File(baseDir, "model/model-de")
|
||||
|
||||
if (modelFolder.exists()) {
|
||||
Log.d("JARVIS", "Modell gefunden unter: ${modelFolder.absolutePath}")
|
||||
try {
|
||||
// Wir laden das Modell und speichern es in der Klassen-Variable 'voskModel'
|
||||
voskModel = Model(modelFolder.absolutePath)
|
||||
|
||||
Log.d("JARVIS", "Modell bereit, starte Hintergrund-Dienst...")
|
||||
|
||||
// Jetzt, wo das Modell da ist, können wir das Zuhören starten
|
||||
resumeListening()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("JARVIS", "Vosk Fehler beim Modell-Laden: ${e.message}")
|
||||
updateNotification("Fehler: Modell konnte nicht geladen werden")
|
||||
}
|
||||
} else {
|
||||
Log.e("JARVIS", "ORDNER NICHT GEFUNDEN! Pfad: ${modelFolder.absolutePath}")
|
||||
updateNotification("Fehler: Modell-Ordner fehlt")
|
||||
}
|
||||
}
|
||||
|
||||
private fun pauseListening() {
|
||||
Log.d("JARVIS", "Service: Pausiere Zuhören")
|
||||
voskService?.stop()
|
||||
updateNotification("Pausiert (verarbeitet Anfrage...)")
|
||||
}
|
||||
|
||||
private fun resumeListening() {
|
||||
if (voskModel == null) return
|
||||
Log.d("JARVIS", "Service: Starte Zuhören")
|
||||
voskService?.stop()
|
||||
|
||||
// Die Liste klein halten ist gut, aber wir brauchen den exakten Treffer
|
||||
val rec = Recognizer(voskModel, 16000.0f, "[\"jarvis\", \"[unk]\"]")
|
||||
voskService = SpeechService(rec, 16000.0f)
|
||||
voskService?.startListening(this)
|
||||
|
||||
isInteracting = false
|
||||
updateNotification("Warte auf 'Jarvis'...")
|
||||
}
|
||||
|
||||
// --- Vosk Listener ---
|
||||
override fun onPartialResult(hypothesis: String) {
|
||||
if (isInteracting) return
|
||||
|
||||
try {
|
||||
val json = JSONObject(hypothesis)
|
||||
// Bei onPartialResult heißt das Feld "partial"
|
||||
val partialText = json.optString("partial").lowercase().trim()
|
||||
|
||||
// Wir reagieren NUR, wenn das Wort exakt "jarvis" ist
|
||||
// Das verhindert, dass Wörter wie "Service" oder "Nachtisch" triggern
|
||||
if (partialText == "jarvis" ) {
|
||||
Log.d("JARVIS", "Service: WAKE WORD EXAKT ERKANNT: $partialText")
|
||||
|
||||
isInteracting = true
|
||||
// Ein kurzer haptischer Feedback-Vibe wäre hier cool (optional)
|
||||
|
||||
pauseListening()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra("WAKE_WORD_TRIGGERED", true)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("JARVIS", "Fehler beim Parsen des PartialResults: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResult(hypothesis: String) {}
|
||||
override fun onFinalResult(hypothesis: String) {}
|
||||
override fun onError(e: Exception) { Log.e("JARVIS", "Service Error: ${e.message}") }
|
||||
override fun onTimeout() {}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
voskService?.stop()
|
||||
voskService?.shutdown()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.example.jarvis_stts
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import org.vosk.Model
|
||||
import org.vosk.Recognizer
|
||||
import org.vosk.android.SpeechService
|
||||
import org.vosk.android.RecognitionListener
|
||||
import java.io.IOException
|
||||
import java.io.File
|
||||
|
||||
class JarvisService : Service(), RecognitionListener {
|
||||
|
||||
private var speechService: SpeechService? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
setupVosk()
|
||||
return START_STICKY // Sorgt dafür, dass der Service bei Beendung neu startet
|
||||
}
|
||||
|
||||
private fun setupVosk() {
|
||||
try {
|
||||
// MainActivity entpackt nach "model", also greifen wir hier darauf zu:
|
||||
val modelPath = File(filesDir, "model").absolutePath
|
||||
|
||||
val model = Model(modelPath)
|
||||
|
||||
// WICHTIG: Nutze hier "computer" ODER "jarvis",
|
||||
// je nachdem was du in der MainActivity definiert hast.
|
||||
val recognizer = Recognizer(model, 16000f, "[\"computer\", \"jarvis\", \"[unk]\"]")
|
||||
|
||||
speechService = SpeechService(recognizer, 16000f)
|
||||
speechService?.startListening(this)
|
||||
Log.d("JARVIS", "Service: Vosk hört jetzt zu...")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("JARVIS", "Service: Fehler beim Laden des Modells: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResult(hypothesis: String?) {
|
||||
// hypothesis ist ein JSON String, z.B.: { "text" : "jarvis" }
|
||||
if (hypothesis != null && hypothesis.contains("jarvis")) {
|
||||
println("WAKE WORD ERKANNT!")
|
||||
// Hier triggerst du deine Antwort-Logik
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPartialResult(hypothesis: String?) {
|
||||
// Wird während des Sprechens aufgerufen
|
||||
}
|
||||
|
||||
override fun onFinalResult(hypothesis: String?) {}
|
||||
|
||||
override fun onError(e: Exception?) {
|
||||
e?.printStackTrace()
|
||||
}
|
||||
|
||||
override fun onTimeout() {}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
speechService?.stop()
|
||||
speechService?.shutdown()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package com.example.jarvis_stts
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.speech.RecognizerIntent
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.speech.tts.Voice
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
@@ -16,55 +18,59 @@ import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import okhttp3.*
|
||||
import org.vosk.Model
|
||||
import org.vosk.Recognizer
|
||||
import org.vosk.android.RecognitionListener
|
||||
import org.vosk.android.SpeechService
|
||||
import org.vosk.android.StorageService
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.provider.Settings
|
||||
import android.net.Uri
|
||||
|
||||
class MainActivity : AppCompatActivity(), RecognitionListener, TextToSpeech.OnInitListener {
|
||||
class MainActivity : AppCompatActivity(), TextToSpeech.OnInitListener {
|
||||
|
||||
// UI Elemente
|
||||
private lateinit var tvStatus: TextView
|
||||
private lateinit var etUrl: EditText
|
||||
private lateinit var spinnerVoices: Spinner
|
||||
private lateinit var tts: TextToSpeech
|
||||
|
||||
// Vosk & Netzwerk
|
||||
private var voskService: SpeechService? = null
|
||||
private var voskModel: Model? = null
|
||||
private val client = OkHttpClient()
|
||||
private var webSocket: WebSocket? = null
|
||||
private var isInteracting = false
|
||||
|
||||
// TTS Stimmen
|
||||
private var availableVoices = mutableListOf<Voice>()
|
||||
private var voiceNames = mutableListOf<String>()
|
||||
|
||||
// Launcher für Google Spracherkennung
|
||||
private val speechRecognizerLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
isInteracting = false // WICHTIG: Sperre wieder aufheben!
|
||||
|
||||
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||
val spokenText = result.data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.get(0) ?: ""
|
||||
tvStatus.text = "Ich: $spokenText"
|
||||
webSocket?.send(spokenText)
|
||||
// HIER STARTEN WIR VOSK NOCH NICHT! Wir warten auf die Antwort des Servers.
|
||||
} else {
|
||||
// Nur wenn wir nichts gesagt oder abgebrochen haben, geht Vosk direkt wieder an
|
||||
startVosk()
|
||||
private val speechRecognizerLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||
val spokenText = result.data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.get(0) ?: ""
|
||||
tvStatus.text = "Ich: $spokenText"
|
||||
webSocket?.send(spokenText)
|
||||
// Hier warten wir auf den Server. Der Service bleibt pausiert.
|
||||
} else {
|
||||
// Abbruch oder Fehler -> Service soll wieder zuhören
|
||||
tellServiceTo(JarvisService.ACTION_RESUME)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOverlayPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!Settings.canDrawOverlays(this)) {
|
||||
Log.d("JARVIS", "Overlay-Berechtigung fehlt. Öffne Einstellungen...")
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:$packageName")
|
||||
)
|
||||
// Wirft den Nutzer in die Einstellungen.
|
||||
// Nach der Rückkehr muss die App meist neu gestartet/fokussiert werden.
|
||||
startActivity(intent)
|
||||
Toast.makeText(this, "Bitte erlaube J.A.R.V.I.S., über anderen Apps zu erscheinen", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// 1. UI initialisieren
|
||||
tvStatus = findViewById(R.id.tvStatus)
|
||||
etUrl = findViewById(R.id.etUrl)
|
||||
spinnerVoices = findViewById(R.id.spinnerVoices)
|
||||
@@ -73,7 +79,6 @@ class MainActivity : AppCompatActivity(), RecognitionListener, TextToSpeech.OnIn
|
||||
|
||||
tts = TextToSpeech(this, this)
|
||||
|
||||
// 2. SharedPreferences (Server URL laden)
|
||||
val prefs = getSharedPreferences("JarvisPrefs", MODE_PRIVATE)
|
||||
etUrl.setText(prefs.getString("server_url", ""))
|
||||
|
||||
@@ -85,18 +90,41 @@ class MainActivity : AppCompatActivity(), RecognitionListener, TextToSpeech.OnIn
|
||||
}
|
||||
}
|
||||
|
||||
btnSpeak.setOnClickListener {
|
||||
voskService?.stop() // Stoppe Wake-Word, wenn man manuell klickt
|
||||
startVoiceInput()
|
||||
btnSpeak.setOnClickListener {
|
||||
tellServiceTo(JarvisService.ACTION_PAUSE)
|
||||
startVoiceInput()
|
||||
}
|
||||
|
||||
// 3. Berechtigungen prüfen & Modell laden
|
||||
checkPermissionsAndInit()
|
||||
checkOverlayPermission() // <-- Hier aufrufen!
|
||||
}
|
||||
|
||||
// Wird aufgerufen, wenn die App im Hintergrund war und vom Service geweckt wird
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent?.getBooleanExtra("WAKE_WORD_TRIGGERED", false) == true) {
|
||||
Log.d("JARVIS", "MainActivity: Wake Word vom Service empfangen! Starte Google...")
|
||||
// Kleiner Delay, damit die Audio-Hardware Zeit zum Umschalten hat
|
||||
tvStatus.postDelayed({
|
||||
startVoiceInput()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermissionsAndInit() {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1)
|
||||
val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO)
|
||||
|
||||
// Notification Permission für Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
val missingPermissions = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (missingPermissions.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(this, missingPermissions.toTypedArray(), 1)
|
||||
} else {
|
||||
initVoskModel()
|
||||
}
|
||||
@@ -104,88 +132,48 @@ class MainActivity : AppCompatActivity(), RecognitionListener, TextToSpeech.OnIn
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == 1 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (requestCode == 1 && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
||||
initVoskModel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initVoskModel() {
|
||||
// "model-de" ist der Ordner in assets. "model" ist der Zielordner auf dem Handy.
|
||||
StorageService.unpack(this, "model-de", "model",
|
||||
{ model: Model ->
|
||||
voskModel = model
|
||||
Log.d("JARVIS", "Modell erfolgreich geladen!")
|
||||
startVosk()
|
||||
{ _: Model ->
|
||||
Log.d("JARVIS", "Modell bereit, starte Hintergrund-Dienst...")
|
||||
tellServiceTo(JarvisService.ACTION_START)
|
||||
tvStatus.text = "Service läuft im Hintergrund!"
|
||||
},
|
||||
{ exception: IOException ->
|
||||
Log.e("JARVIS", "Vosk Entpack-Fehler: ${exception.message}")
|
||||
runOnUiThread { tvStatus.text = "Fehler: Modell nicht gefunden" }
|
||||
{ exception: IOException ->
|
||||
Log.e("JARVIS", "Vosk Entpack-Fehler: ${exception.message}")
|
||||
tvStatus.text = "Fehler: Modell nicht gefunden"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun startVosk() {
|
||||
try {
|
||||
if (voskModel == null) return
|
||||
|
||||
// Alten Service sicherheitshalber beenden
|
||||
voskService?.stop()
|
||||
voskService?.shutdown()
|
||||
|
||||
// Wir horchen auf "computer" und "jarvis".
|
||||
val rec = Recognizer(voskModel, 16000.0f, "[\"computer\", \"jarvis\", \"[unk]\"]")
|
||||
voskService = SpeechService(rec, 16000.0f)
|
||||
voskService?.startListening(this)
|
||||
|
||||
runOnUiThread { tvStatus.text = "Bereit (Warte auf 'Jarvis' oder 'Computer')" }
|
||||
} catch (e: Exception) {
|
||||
Log.e("JARVIS", "Vosk Start Fehler: ${e.message}")
|
||||
private fun tellServiceTo(action: String) {
|
||||
val intent = Intent(this, JarvisService::class.java).apply {
|
||||
this.action = action
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Vosk RecognitionListener ---
|
||||
override fun onPartialResult(hypothesis: String) {
|
||||
if (isInteracting) return // Wenn wir schon dabei sind, ignoriere weiteres
|
||||
|
||||
val recognizedText = extractText(hypothesis)
|
||||
if (recognizedText.contains("jarvis", true)) {
|
||||
isInteracting = true // Sperre setzen
|
||||
voskService?.stop()
|
||||
|
||||
tvStatus.postDelayed({
|
||||
startVoiceInput()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractText(json: String): String {
|
||||
// Hilft, den Text aus dem JSON {"partial" : "..."} zu ziehen
|
||||
return json.substringAfter(": \"").substringBefore("\"")
|
||||
}
|
||||
|
||||
override fun onResult(hypothesis: String) {
|
||||
// Hier könnte man das finale Wort prüfen, falls Partial nicht reicht
|
||||
}
|
||||
|
||||
override fun onFinalResult(hypothesis: String) {}
|
||||
override fun onError(e: Exception) { Log.e("JARVIS", "Vosk Error: ${e.message}") }
|
||||
override fun onTimeout() {}
|
||||
|
||||
// --- Google STT & TTS ---
|
||||
private fun startVoiceInput() {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||||
putExtra(RecognizerIntent.EXTRA_LANGUAGE, "de-DE")
|
||||
putExtra(RecognizerIntent.EXTRA_PROMPT, "Ich höre dir zu...")
|
||||
|
||||
// Diese beiden sorgen dafür, dass Google geduldiger ist:
|
||||
putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 2000L)
|
||||
putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS, 2000L)
|
||||
}
|
||||
try {
|
||||
speechRecognizerLauncher.launch(intent)
|
||||
} catch (e: Exception) {
|
||||
startVosk()
|
||||
tellServiceTo(JarvisService.ACTION_RESUME)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +188,12 @@ class MainActivity : AppCompatActivity(), RecognitionListener, TextToSpeech.OnIn
|
||||
runOnUiThread { tvStatus.text = "J.A.R.V.I.S.: $text" }
|
||||
speakOut(text)
|
||||
}
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
runOnUiThread {
|
||||
tvStatus.text = "Verbindungsfehler!"
|
||||
tellServiceTo(JarvisService.ACTION_RESUME)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -208,25 +202,19 @@ class MainActivity : AppCompatActivity(), RecognitionListener, TextToSpeech.OnIn
|
||||
tts.language = Locale.GERMAN
|
||||
setupVoiceSpinner()
|
||||
|
||||
// NEU: Wir horchen darauf, wann Jarvis aufhört zu sprechen
|
||||
tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {
|
||||
// Jarvis fängt an zu sprechen
|
||||
}
|
||||
|
||||
override fun onStart(utteranceId: String?) {}
|
||||
override fun onDone(utteranceId: String?) {
|
||||
// Jarvis ist fertig! Wake-Word wieder aktivieren.
|
||||
if (utteranceId == "TTS_DONE") {
|
||||
// onDone läuft im Hintergrund, UI/Vosk Updates müssen in den Main Thread
|
||||
runOnUiThread {
|
||||
startVosk()
|
||||
// Wenn Jarvis fertig gesprochen hat, lauschen wir wieder!
|
||||
tellServiceTo(JarvisService.ACTION_RESUME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onError(utteranceId: String?) {
|
||||
runOnUiThread { startVosk() } // Bei einem Fehler auch wieder zuhören
|
||||
runOnUiThread { tellServiceTo(JarvisService.ACTION_RESUME) }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -253,18 +241,13 @@ class MainActivity : AppCompatActivity(), RecognitionListener, TextToSpeech.OnIn
|
||||
}
|
||||
|
||||
private fun speakOut(text: String) {
|
||||
// Wir können hier Vosk stoppen, damit Jarvis sich nicht selbst hört
|
||||
voskService?.stop()
|
||||
|
||||
// Die ID "TTS_DONE" triggert unseren Listener, wenn der Text fertig gesprochen wurde
|
||||
tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, "TTS_DONE")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
voskService?.stop()
|
||||
voskService?.shutdown()
|
||||
webSocket?.close(1000, "App Ende")
|
||||
tts.shutdown()
|
||||
// Wir lassen den Service ABSICHTLICH nicht stoppen, wenn die Activity zerstört wird!
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user