1
1
This commit is contained in:
2026-01-24 19:04:45 +01:00
parent 3661e33a16
commit 1ec116397c
9 changed files with 101 additions and 56 deletions

View File

@@ -1,21 +1,23 @@
package dev.zerozipp
import dev.zerozipp.assistant.*
import dev.zerozipp.assistant.languages.*
import javax.sound.sampled.*
import kotlinx.coroutines.*
import org.vosk.*
fun main() = runBlocking {
fun main() {
LibVosk.setLogLevel(LogLevel.WARNINGS)
val format = AudioFormat(16000f, 16, 1, true, false)
val assistant = System.getenv("HA_URL") ?: error("Set HA_URL env var")
val token = System.getenv("HA_TOKEN") ?: error("Set HA_TOKEN env var")
val name = System.getenv("COMMAND_NAME") ?: "computer"
val name = System.getenv("ASSISTANT_NAME") ?: "computer"
val lang = System.getenv("ASSISTANT_LANG") ?: "english"
val vosk = System.getenv("VOSK_MODEL") ?: "vosk"
Home(assistant, token).use { home ->
val formatted: String = name.trim().lowercase()
Assistant(vosk, formatted, home, format).use {
val formatted = name.trim().lowercase()
val dict = if(lang == "german") German() else English()
Assistant(vosk, home, dict, formatted, format).use {
assistant -> assistant.listen()
}
}

View File

@@ -0,0 +1,12 @@
package dev.zerozipp.assistant
abstract class Action {
abstract val triggers: Set<String>
abstract val options: Map<String, Runnable>
fun run(command: String) = triggers.forEach {
if(!command.startsWith(it)) return@forEach
val option = command.replaceFirst(it, "")
options[option.trim()]?.run()
}
}

View File

@@ -2,21 +2,26 @@ package dev.zerozipp.assistant
import org.vosk.Model
import org.vosk.Recognizer
import dev.zerozipp.assistant.actions.*
import javax.sound.sampled.*
import com.google.gson.*
class Assistant(path: String,
class Assistant(
path: String, home: Home,
private val dict: Dictionary,
private val name: String,
private val home: Home,
format: AudioFormat
) : AutoCloseable {
private val model = Model(path)
private val microphone = line(format)
private val buffer = ByteArray(4096)
private val action = "mach das licht"
private val microphone = line(format)
private val actions = listOf<Action>(
Light(home)
)
private val recognizer = Recognizer(
model, format.sampleRate, Gson().toJson(
listOf(name, action) + Light.modes.keys
listOf(name) + dict.words
)
)
@@ -26,19 +31,19 @@ class Assistant(path: String,
AudioSystem.getLine(info) as TargetDataLine
}.also { it.open(format) }
private suspend fun process() {
private fun process() {
val bytes = microphone.read(buffer, 0, buffer.size)
if(bytes <= 0 || !recognizer.acceptWaveForm(buffer, bytes)) return
val json = Gson().fromJson(recognizer.result, JsonObject::class.java)
val text = json.let { it["text"].asString.orEmpty().trim() }.split(action)
val text = json.let { it["text"].asString.orEmpty().trim() }
text.takeIf { it.size == 2 }?.also { (name, key) ->
Light.modes[key.trim()]?.takeIf { name.trim() == this.name }
?.let { home.light("all", it.state, it.level, it.color) }
}
if(!text.startsWith(name)) return
val command = text.replaceFirst("name", text)
val translated = dict.translate(command.trim())
actions.forEach { it.run(translated) }
}
suspend fun listen() {
fun listen() {
microphone.start()
while(true) process()
}

View File

@@ -0,0 +1,8 @@
package dev.zerozipp.assistant
abstract class Dictionary {
abstract val mappings: Map<String, String>
val words: Set<String> get() = mappings.keys
fun translate(text: String) = text.split(" ")
.mapNotNull { mappings[it] }.joinToString(" ")
}

View File

@@ -1,6 +1,7 @@
package dev.zerozipp.assistant
import com.google.gson.*
import kotlinx.coroutines.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.*
@@ -13,8 +14,8 @@ class Home(
override fun close() = http.close()
private val http = HttpClient(CIO)
suspend fun light(entity: String,
state: Boolean, level: Int?, color: String?) {
fun light(entity: String, state: Boolean,
level: Int?, color: String?) = runBlocking {
val service = if(state) "turn_on" else "turn_off"
val url = "$host/api/services/light/$service"
val type = ContentType.Application.Json

View File

@@ -1,37 +0,0 @@
package dev.zerozipp.assistant
object Light {
val on = Mode(true, null, null)
val off = Mode(false, null, null)
fun color(name: String) = Mode(true, name, null)
fun level(level: Int) = Mode(true, null, level)
data class Mode(
val state: Boolean,
val color: String?,
val level: Int?
)
val modes = mapOf(
"rot" to color("red"),
"grün" to color("green"),
"blau" to color("blue"),
"weiß" to color("white"),
"gelb" to color("yellow"),
"orange" to color("orange"),
"lila" to color("purple"),
"rosa" to color("pink"),
"türkis" to color("cyan"),
"magenta" to color("magenta"),
"braun" to color("brown"),
"grau" to color("gray"),
"schwarz" to color("black"),
"maximal" to level(255),
"minimal" to level(0),
"dunkel" to level(64),
"mittel" to level(128),
"hell" to level(191),
"aus" to off,
"an" to on
)
}

View File

@@ -0,0 +1,22 @@
package dev.zerozipp.assistant.actions
import dev.zerozipp.assistant.Action
import dev.zerozipp.assistant.Home
class Light(private val home: Home) : Action() {
private val on = Runnable { home.light("all", true, null, null) }
private val off = Runnable { home.light("all", false, null, null) }
private fun color(name: String) = Runnable { home.light("all", true, null, name) }
private fun level(level: Int) = Runnable { home.light("all", true, level, null) }
override val triggers = setOf("turn the light", "light")
override val options = mapOf(
"maximum" to level(255), "minimum" to level(0),
"dark" to level(64), "mid" to level(128), "bright" to level(191),
"red" to color("red"), "green" to color("green"), "blue" to color("blue"),
"white" to color("white"), "yellow" to color("yellow"), "orange" to color("orange"),
"purple" to color("purple"), "pink" to color("pink"), "magenta" to color("magenta"),
"cyan" to color("cyan"), "brown" to color("brown"),
"on" to on, "off" to off
)
}

View File

@@ -0,0 +1,16 @@
package dev.zerozipp.assistant.languages
import dev.zerozipp.assistant.Dictionary
class English : Dictionary() {
override val mappings = mapOf(
"turn" to "turn", "the" to "the", "light" to "light",
"maximum" to "maximum", "minimum" to "minimum",
"dark" to "dark", "mid" to "mid", "bright" to "bright",
"red" to "red", "green" to "green", "blue" to "blue",
"white" to "white", "yellow" to "yellow", "orange" to "orange",
"purple" to "purple", "pink" to "pink", "magenta" to "magenta",
"cyan" to "cyan", "brown" to "brown",
"on" to "on", "off" to "off"
)
}

View File

@@ -0,0 +1,16 @@
package dev.zerozipp.assistant.languages
import dev.zerozipp.assistant.Dictionary
class German : Dictionary() {
override val mappings = mapOf(
"mach" to "turn", "the" to "das", "licht" to "light",
"maximum" to "maximum", "minimum" to "minimum",
"dunkel" to "dark", "mitte" to "mid", "hell" to "bright",
"rot" to "red", "grün" to "green", "blau" to "blue",
"weiß" to "white", "gelb" to "yellow", "orange" to "orange",
"lila" to "purple", "rosa" to "pink", "magenta" to "magenta",
"türkis" to "cyan", "braun" to "brown",
"an" to "on", "aus" to "off"
)
}