185 lines
6.3 KiB
Kotlin
185 lines
6.3 KiB
Kotlin
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 (Volles Wörterbuch)")
|
|
voskService?.stop()
|
|
|
|
// GANZ WICHTIG: Keine Grammar (Wortliste) mehr übergeben!
|
|
// Wir nutzen das volle deutsche Vokabular, um Fehlinterpretationen zu vermeiden.
|
|
val rec = Recognizer(voskModel, 16000.0f)
|
|
|
|
voskService = SpeechService(rec, 16000.0f)
|
|
voskService?.startListening(this)
|
|
|
|
isInteracting = false
|
|
updateNotification("Warte auf 'Jarvis'")
|
|
}
|
|
|
|
override fun onPartialResult(hypothesis: String) {
|
|
if (isInteracting) return
|
|
|
|
try {
|
|
val json = JSONObject(hypothesis)
|
|
val partialText = json.optString("partial").lowercase().trim()
|
|
|
|
// HIER: Lass dir mal ausgeben, was er eigentlich versteht!
|
|
if (partialText.isNotEmpty()) {
|
|
Log.d("JARVIS_DEBUG", "Vosk hört: $partialText")
|
|
}
|
|
|
|
// Unser Regex-Check (den passen wir gleich an)
|
|
if (Regex("\\b(hey joe avis)\\b").containsMatchIn(partialText)) {
|
|
Log.d("JARVIS", "WAKE WORD ERKANNT: $partialText")
|
|
triggerJarvis()
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e("JARVIS", "Fehler beim Parsen: ${e.message}")
|
|
}
|
|
}
|
|
|
|
// onResult bleibt leer oder loggt nur zur Kontrolle
|
|
override fun onResult(hypothesis: String) {}
|
|
|
|
private fun triggerJarvis() {
|
|
isInteracting = true
|
|
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)
|
|
}
|
|
|
|
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
|
|
} |