FHEMWEB/VoiceControl: Web-STT & Hardware-Wakeword: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung |
Keine Bearbeitungszusammenfassung |
||
| Zeile 191: | Zeile 191: | ||
Beide Wege schreiben in: | Beide Wege schreiben in: | ||
<code>global: | <code>global:STT_output</code> | ||
Die Verarbeitung erfolgt zentral über ein <code>notify</code>. Ein Device wird in der Mappingtabelle eingetragen. | Die Verarbeitung erfolgt zentral über ein <code>notify</code>. Ein Device wird in der Mappingtabelle eingetragen. | ||
| Zeile 201: | Zeile 201: | ||
<code>"hauptkeyword:Filter1|Filter3|Filter3" => { dev => "Devicename", label => "Übersichtname", cmdOn => "on", cmdOff => "off" }</code> | <code>"hauptkeyword:Filter1|Filter3|Filter3" => { dev => "Devicename", label => "Übersichtname", cmdOn => "on", cmdOff => "off" }</code> | ||
=== Beispiel: notify === | === Beispiel: notify zur Steuerung === | ||
<syntaxhighlight lang="perl"> | <syntaxhighlight lang="perl"> | ||
defmod n_VoiceControl notify global: | defmod n_VoiceControl notify global:STT_output:.* {\ | ||
my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\ | my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\ | ||
$cleanEvent //= $EVENT;;\ | $cleanEvent //= $EVENT;;\ | ||
$clientId //= "unknown";;\ | $clientId //= "unknown";;\ | ||
\ | |||
my @responses;;\ | |||
my %vacRooms = map { $_ => ucfirst($_) } qw(arbeitszimmer badezimmer esszimmer flur küche wohnzimmer);;\ | |||
my $onRegEx = qr/\b(an|ein|einschalten|starte|aktivier|aktiviere|öffne|öffnen|auf|hoch|lade)\b/;;\ | |||
my $offRegEx = qr/\b(aus|ausschalten|stop|stoppe|beende|deaktivier|deaktiviere|schließe|schließen|zu|runter)\b/;;\ | |||
\ | \ | ||
# --- | # --- 1. Geräte-Liste ---\ | ||
my % | my %devices = (\ | ||
"esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer | "esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer (an/aus)" },\ | ||
"esszimmer:aquarium" => { dev => "Aquarium_Aktor", label => "Aquarium", | "esszimmer:aquarium" => { dev => "Aquarium_Aktor", label => "Aquarium (an/aus)" },\ | ||
"küche" => { dev => " | "küche:licht|lampe|deckenlampe" => { dev => "Deckenlampe_Kue", label => "Licht Küche (an/aus)" },\ | ||
"wohnzimmer" => { dev => "Lampe06_Dek", label => "Licht Wohnzimmer | "küche:radio" => { dev => "MPD", label => "Radio Küche(an/aus)", cmdOn => "play", cmdOff => "stop" },\ | ||
"fernseher|tv" => { dev => "VuPlus", label => "Fernseher | "wohnzimmer:licht|lampe|deckenlampe" => { dev => "Lampe06_Dek", label => "Licht Wohnzimmer (an/aus)" },\ | ||
"rechner|pc" => { dev => "PC_Aktor", label => "PC | "fernseher|tv" => { dev => "VuPlus", label => "Fernseher (an/aus)" },\ | ||
"garage|tor" => { dev => "Garagentor_Aktor", label => "Garagentor öffnen/schließen", cmdOn => "open", cmdOff => "close" },\ | "rechner|pc" => { dev => "PC_Aktor", label => "PC (an/aus)" },\ | ||
"kaffee" => { dev => "Kaffeemaschine", label => "Kaffeemaschine | "garage|tor" => { dev => "Garagentor_Aktor", label => "Garagentor (öffnen/schließen)", cmdOn => "open", cmdOff => "close" },\ | ||
"roberto" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Lade Roberto", cmdOn => "charge" },\ | "kaffee" => { dev => "Kaffeemaschine (an/aus)", label => "Kaffeemaschine" },\ | ||
"ambiente" => { dev => "LampeSzeneAlle", label => " | "roberto" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Lade Roberto (an/aus)", cmdOn => "charge" },\ | ||
"sauge|reinige|putze|staubsauger|roboter" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => " | \ | ||
# Komplexe Sonderfunktionen (erkennbar am "run"-Eintrag)\ | |||
"ambiente" => { dev => "LampeSzeneAlle", label => "Ambiente", run => sub {\ | |||
my ($d, $c, $on, $off) = @_;;\ | |||
if ($c =~ /(\d+)/) {\ | |||
fhem("set $d->{dev} brightness " . int($1 * 2.55));;\ | |||
return "$d->{label} auf $1 Prozent";;\ | |||
}\ | |||
fhem("set $d->{dev} " . ($off ? "off" : "on")) if $on || $off;;\ | |||
return $on || $off ? "$d->{label} " . ($off ? "aus" : "an") : undef;;\ | |||
}},\ | |||
"sauge|reinige|putze|staubsauger|roboter" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Reinige/Sauge (Robosauger)", run => sub {\ | |||
my ($d, $c) = @_;;\ | |||
my @found = grep { $c =~ /\b$_\b/ } keys %vacRooms;;\ | |||
fhem(@found ? "set $d->{dev} clean_segment " . join(",", map { $vacRooms{$_} } @found) : "set $d->{dev} start");;\ | |||
return @found ? "Reinigung gestartet in " . join(" und ", map { $vacRooms{$_} } @found) : "Staubsauger gestartet";;\ | |||
}},\ | |||
"ambilight" => { label => "Ambilight (umschalten)", run => sub {\ | |||
system("sshpass -p '1431Fhem1982' ssh -o StrictHostKeyChecking=no root\@192.168.1.46 '/usr/share/hyperhdr/scripts/hyperhdr_toggle.sh'");;\ | |||
return "Ambilight erledigt";;\ | |||
}}\ | |||
);;\ | );;\ | ||
\ | \ | ||
my % | # --- 2. TEXTBEREINIGUNG ---\ | ||
my $text = lc($cleanEvent);;\ | |||
$text =~ s/^stt_output:\s*//;; \ | |||
$text =~ s/\[\d+\.?\d*\]//g;; \ | |||
$text =~ s/^\s+|\s+$//g;; \ | |||
\ | |||
# --- 3. SPEZIALFÄLLE (KI & HILFE) ---\ | |||
if ($text =~ /\bfrage\s+(.*)$/) {\ | |||
my $q = $1;;\ | |||
my ($sec,$min,$hour,$mday,$mon,$year) = localtime;;\ | |||
fhem(sprintf("set GeminiAI ask [Aktuelle Systemzeit: %02d.%02d.%04d %02d:%02d:%02d] %s", $mday, $mon+1, $year+1900, $hour, $min, $sec, $q));;\ | |||
fhem("sleep 2;; setreading global TTS_input erledigt");;\ | |||
return;;\ | |||
}\ | |||
\ | |||
if ($text =~ /(hilfe|kommandos|übersicht)/) {\ | |||
my $h = '<div style="text-align:left;;min-width:250px;;font-family:sans-serif;;"><b>Befehlsübersicht:</b><br><br>';;\ | |||
my %seen;;\ | |||
for my $k (sort keys %devices) {\ | |||
my $d = $devices{$k};;\ | |||
next if $seen{$d->{label}}++;;\ | |||
# Nur noch das Prozent-Suffix für das Ambiente-Licht wird dynamisch angehängt\ | |||
my $suffix = ($d->{run} && $k eq "ambiente") ? " [0-100%]" : "";;\ | |||
$h .= "• $d->{label}$suffix<br>";;\ | |||
}\ | |||
$h .= '<br><u>Staubsauger Räume</u><br>• ' . join(", ", sort values %vacRooms) . '<br></div>';;\ | |||
$h =~ s/'/\\"/g;;\ | |||
my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$clientId'){FW_okDialog('$h')}";;\ | |||
FW_directNotify("#FHEMWEB:$_", $js, "") for devspec2array("TYPE=FHEMWEB");;\ | |||
fhem("sleep 2;; setreading global TTS_input erledigt");;\ | |||
return;;\ | |||
}\ | |||
\ | |||
# --- 4. AKTIONSERKENNUNG LOOP ---\ | |||
my $global_on = $text =~ /$onRegEx/ ? 1 : 0;;\ | |||
my $global_off = $text =~ /$offRegEx/ ? 1 : 0;;\ | |||
\ | \ | ||
my | my @parts = split(/\s*(?:dann|,)\s*|(?<=\ban\b)\s*und\s*|(?<=\baus\b)\s*und\s*/, $text);;\ | ||
@parts = ($text) if @parts == 1;;\ | |||
\ | \ | ||
my @ | for my $part (@parts) {\ | ||
$part =~ s/^\s+|\s+$//g;;\ | |||
next unless $part;;\ | |||
\ | \ | ||
my $is_on = $part =~ /$onRegEx/ ? 1 : ($part =~ /$offRegEx/ ? 0 : $global_on);;\ | |||
my $is_off = $part =~ /$offRegEx/ ? 1 : ($part =~ /$onRegEx/ ? 0 : $global_off);;\ | |||
my $is_off = | |||
\ | \ | ||
# -- | # Trennung: Original-Part bleibt für "run"-Blöcke, clean_part kriegt die Füllwortbereinigung\ | ||
my $ | my $clean_part = $part;;\ | ||
$clean_part =~ s/\b(ich|brauche|mach|bitte|kannst du|würdest du|mal|doch|den|das|die|im|in der|und|sowie|,)\b/ /g;;\ | |||
\ | |||
# Schleife durch die Geräte (Sonderfunktionen mit "run" werden zuerst bewertet)\ | |||
for my $key (sort { ($devices{$b}{run} ? 1:0) <=> ($devices{$a}{run} ? 1:0) } keys %devices) {\ | |||
my ($main, $must) = split(/:/, $key);;\ | my ($main, $must) = split(/:/, $key);;\ | ||
\ | \ | ||
if ($part =~ /\b($main)\b/ || $clean_part =~ /\b($main)\b/) {\ | |||
next if $must && $part !~ /\b($must)\b/;;\ | |||
next if | |||
\ | \ | ||
my $d = $devices{$key};;\ | |||
if ($d->{run}) {\ | |||
my $res = $d->{run}->($d, $part, $is_on, $is_off);;\ | |||
push @responses, $res if $res;;\ | |||
} elsif ($is_on || $is_off) {\ | |||
if | |||
my | |||
my $fhem_cmd = $is_off ? ($d->{cmdOff} // "off") : ($d->{cmdOn} // "on");;\ | my $fhem_cmd = $is_off ? ($d->{cmdOff} // "off") : ($d->{cmdOn} // "on");;\ | ||
fhem("set $d->{dev} $fhem_cmd") | fhem("set $d->{dev} $fhem_cmd");;\ | ||
push @responses, "$d->{label} " . ($is_off ? "aus" : "an");;\ | |||
}\ | }\ | ||
}\ | }\ | ||
}\ | }\ | ||
}\ | |||
\ | \ | ||
# --- 5. FINALE SPRACHAUSGABE ---\ | |||
fhem("sleep 2;; setreading global TTS_input erledigt") if @responses;;\ | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=== Beispiel: notify für Sprachausgabe mit dem TTS-Modul === | |||
<syntaxhighlight lang="perl"> | |||
defmod n_global_TTS_output notify global:TTS_output:.* { my $text = $EVENT;;;; $text =~ s/^TTS_output:\s*//;;;; fhem("set TTS tts $text") } | |||
</syntaxhighlight> | |||
== Unterschiede der Wege == | == Unterschiede der Wege == | ||
Aktuelle Version vom 31. Mai 2026, 11:28 Uhr

VoiceControl – Sprachsteuerung via Browser und Atom Echo s3r
Diese Lösung ermöglicht eine flexible Sprachsteuerung für FHEM. Sprache wird in Text (Speech-to-Text) umgewandelt und als Reading STT im Device global bereitgestellt. Dieses Reading kann anschließend zentral (z. B. über notify oder DOIF) ausgewertet werden.
Unterstützt werden nur Chrome-basierte Browser (Chrome, Edge, Fully Browser). Firefox und Chromium haben leider kein Backend.
Es gibt zwei unterschiedliche Wege zur Spracherfassung:
- Weg 1️⃣ (Software): Browser-basierte Komplettlösung
- Weg 2️⃣ (Hybrid): Hardware-Wakeword + Browser-Spracherkennung
Hilfe
- Forenthread zum VoiceControl Sprachsteuerung
Funktionen
Grundprinzip
- Sprache → Speech-to-Text
- Ergebnis → Reading
STTim Deviceglobal - Zentrale Logik verarbeitet Befehle
Betriebsarten
- Push-to-Talk (nur Browser)
- Always-On mit Wakeword
- Hardware-Wakeword (Hybrid)
Rückmeldungen
- Sprachausgabe (TTS)
- Visuelle Bubble im Browser
- Optional gezielte Rückmeldung per Client-ID
Weg 1️⃣: Browser-Lösung (voicecontrol.js)
Das Script nutzt nur die Google Web Speech API.
Bedienung
Push-to-Talk
- Button gedrückt halten
- Wakeword erforderlich (Standard: „James“)
- Nach Aktivierung ca.6sek Zeit für Befehl
Always-On
- Kurzer Klick aktiviert Dauerbetrieb
- Wakeword erforderlich (Standard: „James“)
- Nach Aktivierung ca.6sek Zeit für Befehl
- JS wird neu gestartet udn Schleife fängt von vorne an
Wakeword
- Standard:
james - Anpassbar im Script:
const wakewords = ["james"];
Installation
Datei kopieren
{ Svn_GetFile('contrib/voicecontrol.js', 'www/pgm2/voicecontrol.js') }
Einbindung
attr WEBphone JavaScripts www/pgm2/voicecontrol.js
Hinweis (HTTP ohne HTTPS)
Chrome benötigt Freigabe für Mikrofon:
chrome://flags/#unsafely-treat-insecure-origin-as-secure- Eigene URL hinzufügen
- Auf
Enabledsetzen
Weg 2️⃣: Hybrid-Lösung mit Fully Kiosk Browser + Atom Echo (voicecontrol_echo.js)
Diese Variante kombiniert Hardware und Software. Daher ist sie besonders geeignet für ein Tablet mit Fully Kiosk Browser oder ähnlich, welches das Webinterface dauerhaft anzeigt.
- Wakeword-Erkennung erfolgt auf dem ESP (Atom Echo s3r)
- Die eigentliche Sprachverarbeitung (Speech-to-Text) erfolgt im Browser
Funktionsweise
- Wakeword wird auf dem ESP erkannt
- FHEM erzeugt Event (z. B.
james_detected) - Browser empfängt Event via WebSocket
- Speech-to-Text startet im Browser
- Ergebnis wird an FHEM übertragen
Aktivierung
- Kurzer Klick auf Button aktiviert/deaktiviert System
- Baut WebSocket-Verbindung zu FHEM auf
- Kein Push-to-Talk-Modus
Installation
Datei kopieren
Die Datei voicecontrol_echo.js ist hier zu finden: VoiceControl Sprachsteuerung und muss nach www/pgm2/voicecontrol_echo.js kopiert werden.
Die echo_s3r.yaml ist für den Atom Echo s3r. Die Konfuguration ist weiter unten beschrieben.
Einbindung in Fhem
attr WEBtablet JavaScripts www/pgm2/voicecontrol_echo.js
- Das Skript voicecontrol_echo.js muss in dem FHEMWEB-Device angelegt werden, das für den Fully Kiosk Browser zuständig ist
attr WEBtablet additionalInform atom_echos3r_9888e00f4280,global
- atom_echos3r_9********** = da kommt das Wakeword an, bzw. der Devicename vom Echo
- global = das ist zum auswerten der Sprachausgabe wichtig
Konfiguration im Javascript
const DEVICE = "atom_echos3r_9888e00f4280";
const TRIGGER = "james_detected";
const FHEM_IP = "192.168.1.76:8085";
DEVICE→ FHEM-Device des ESPTRIGGER→ Event bei WakewordFHEM_IP→ FHEM-Server
Konfiguration in der Yaml
Damit sich der ESP mit eurem MQTT-Server und WLAN verbindet, müssen folgende Stellen angepasst werden.
wifi:
ssid: "YOUR_SSID"
password: "YOUR_PW"
fast_connect: true
mqtt:
broker: 192.168.1.76
port: 1884
username: "YOUR_USERNAME"
password: "YOUR_PW"
topic_prefix: atom_echo
Wakeword (ESP / ESPHome)
Das Wakeword wird direkt auf dem ESP definiert:
- Umsetzung über ESPHome
- Eine große Auswahl an Wakewords sind hier zu finden:
https://github.com/TaterTotterson/microWakeWords
Hier ein Beispiel, wie das Wakeword definiert wird.
micro_wake_word:
id: mww
microphone: echo_mic
models:
- model: "https://github.com/TaterTotterson/microWakeWords/raw/main/microWakeWordsV2/james.json"
id: james_model
probability_cutoff: 0.45
Kommunikation
- WebSocket-Verbindung zu FHEM
- Lauscht auf Device-Events
- Automatischer Reconnect bei Abbruch
Ablauf nach Wakeword
- System sagt „Ja?“
- Browser startet SpeechRecognition
- Nutzer spricht Befehl
- Befehl wird verarbeitet
- Rückmeldung „Erledigt“
Zentrale Auswertung (Logik)
Beide Wege schreiben in:
global:STT_output
Die Verarbeitung erfolgt zentral über ein notify. Ein Device wird in der Mappingtabelle eingetragen.
Beispiel:
"esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer", cmdOn => "on", cmdOff => "off" }
Aufschlüsselung:
"hauptkeyword:Filter1|Filter3|Filter3" => { dev => "Devicename", label => "Übersichtname", cmdOn => "on", cmdOff => "off" }
Beispiel: notify zur Steuerung
defmod n_VoiceControl notify global:STT_output:.* {\
my ($cleanEvent, $clientId) = $EVENT =~ /^(.*)\s\[(.*)\]$/;;\
$cleanEvent //= $EVENT;;\
$clientId //= "unknown";;\
\
my @responses;;\
my %vacRooms = map { $_ => ucfirst($_) } qw(arbeitszimmer badezimmer esszimmer flur küche wohnzimmer);;\
my $onRegEx = qr/\b(an|ein|einschalten|starte|aktivier|aktiviere|öffne|öffnen|auf|hoch|lade)\b/;;\
my $offRegEx = qr/\b(aus|ausschalten|stop|stoppe|beende|deaktivier|deaktiviere|schließe|schließen|zu|runter)\b/;;\
\
# --- 1. Geräte-Liste ---\
my %devices = (\
"esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer (an/aus)" },\
"esszimmer:aquarium" => { dev => "Aquarium_Aktor", label => "Aquarium (an/aus)" },\
"küche:licht|lampe|deckenlampe" => { dev => "Deckenlampe_Kue", label => "Licht Küche (an/aus)" },\
"küche:radio" => { dev => "MPD", label => "Radio Küche(an/aus)", cmdOn => "play", cmdOff => "stop" },\
"wohnzimmer:licht|lampe|deckenlampe" => { dev => "Lampe06_Dek", label => "Licht Wohnzimmer (an/aus)" },\
"fernseher|tv" => { dev => "VuPlus", label => "Fernseher (an/aus)" },\
"rechner|pc" => { dev => "PC_Aktor", label => "PC (an/aus)" },\
"garage|tor" => { dev => "Garagentor_Aktor", label => "Garagentor (öffnen/schließen)", cmdOn => "open", cmdOff => "close" },\
"kaffee" => { dev => "Kaffeemaschine (an/aus)", label => "Kaffeemaschine" },\
"roberto" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Lade Roberto (an/aus)", cmdOn => "charge" },\
\
# Komplexe Sonderfunktionen (erkennbar am "run"-Eintrag)\
"ambiente" => { dev => "LampeSzeneAlle", label => "Ambiente", run => sub {\
my ($d, $c, $on, $off) = @_;;\
if ($c =~ /(\d+)/) {\
fhem("set $d->{dev} brightness " . int($1 * 2.55));;\
return "$d->{label} auf $1 Prozent";;\
}\
fhem("set $d->{dev} " . ($off ? "off" : "on")) if $on || $off;;\
return $on || $off ? "$d->{label} " . ($off ? "aus" : "an") : undef;;\
}},\
"sauge|reinige|putze|staubsauger|roboter" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Reinige/Sauge (Robosauger)", run => sub {\
my ($d, $c) = @_;;\
my @found = grep { $c =~ /\b$_\b/ } keys %vacRooms;;\
fhem(@found ? "set $d->{dev} clean_segment " . join(",", map { $vacRooms{$_} } @found) : "set $d->{dev} start");;\
return @found ? "Reinigung gestartet in " . join(" und ", map { $vacRooms{$_} } @found) : "Staubsauger gestartet";;\
}},\
"ambilight" => { label => "Ambilight (umschalten)", run => sub {\
system("sshpass -p '1431Fhem1982' ssh -o StrictHostKeyChecking=no root\@192.168.1.46 '/usr/share/hyperhdr/scripts/hyperhdr_toggle.sh'");;\
return "Ambilight erledigt";;\
}}\
);;\
\
# --- 2. TEXTBEREINIGUNG ---\
my $text = lc($cleanEvent);;\
$text =~ s/^stt_output:\s*//;; \
$text =~ s/\[\d+\.?\d*\]//g;; \
$text =~ s/^\s+|\s+$//g;; \
\
# --- 3. SPEZIALFÄLLE (KI & HILFE) ---\
if ($text =~ /\bfrage\s+(.*)$/) {\
my $q = $1;;\
my ($sec,$min,$hour,$mday,$mon,$year) = localtime;;\
fhem(sprintf("set GeminiAI ask [Aktuelle Systemzeit: %02d.%02d.%04d %02d:%02d:%02d] %s", $mday, $mon+1, $year+1900, $hour, $min, $sec, $q));;\
fhem("sleep 2;; setreading global TTS_input erledigt");;\
return;;\
}\
\
if ($text =~ /(hilfe|kommandos|übersicht)/) {\
my $h = '<div style="text-align:left;;min-width:250px;;font-family:sans-serif;;"><b>Befehlsübersicht:</b><br><br>';;\
my %seen;;\
for my $k (sort keys %devices) {\
my $d = $devices{$k};;\
next if $seen{$d->{label}}++;;\
# Nur noch das Prozent-Suffix für das Ambiente-Licht wird dynamisch angehängt\
my $suffix = ($d->{run} && $k eq "ambiente") ? " [0-100%]" : "";;\
$h .= "• $d->{label}$suffix<br>";;\
}\
$h .= '<br><u>Staubsauger Räume</u><br>• ' . join(", ", sort values %vacRooms) . '<br></div>';;\
$h =~ s/'/\\"/g;;\
my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$clientId'){FW_okDialog('$h')}";;\
FW_directNotify("#FHEMWEB:$_", $js, "") for devspec2array("TYPE=FHEMWEB");;\
fhem("sleep 2;; setreading global TTS_input erledigt");;\
return;;\
}\
\
# --- 4. AKTIONSERKENNUNG LOOP ---\
my $global_on = $text =~ /$onRegEx/ ? 1 : 0;;\
my $global_off = $text =~ /$offRegEx/ ? 1 : 0;;\
\
my @parts = split(/\s*(?:dann|,)\s*|(?<=\ban\b)\s*und\s*|(?<=\baus\b)\s*und\s*/, $text);;\
@parts = ($text) if @parts == 1;;\
\
for my $part (@parts) {\
$part =~ s/^\s+|\s+$//g;;\
next unless $part;;\
\
my $is_on = $part =~ /$onRegEx/ ? 1 : ($part =~ /$offRegEx/ ? 0 : $global_on);;\
my $is_off = $part =~ /$offRegEx/ ? 1 : ($part =~ /$onRegEx/ ? 0 : $global_off);;\
\
# Trennung: Original-Part bleibt für "run"-Blöcke, clean_part kriegt die Füllwortbereinigung\
my $clean_part = $part;;\
$clean_part =~ s/\b(ich|brauche|mach|bitte|kannst du|würdest du|mal|doch|den|das|die|im|in der|und|sowie|,)\b/ /g;;\
\
# Schleife durch die Geräte (Sonderfunktionen mit "run" werden zuerst bewertet)\
for my $key (sort { ($devices{$b}{run} ? 1:0) <=> ($devices{$a}{run} ? 1:0) } keys %devices) {\
my ($main, $must) = split(/:/, $key);;\
\
if ($part =~ /\b($main)\b/ || $clean_part =~ /\b($main)\b/) {\
next if $must && $part !~ /\b($must)\b/;;\
\
my $d = $devices{$key};;\
if ($d->{run}) {\
my $res = $d->{run}->($d, $part, $is_on, $is_off);;\
push @responses, $res if $res;;\
} elsif ($is_on || $is_off) {\
my $fhem_cmd = $is_off ? ($d->{cmdOff} // "off") : ($d->{cmdOn} // "on");;\
fhem("set $d->{dev} $fhem_cmd");;\
push @responses, "$d->{label} " . ($is_off ? "aus" : "an");;\
}\
}\
}\
}\
\
# --- 5. FINALE SPRACHAUSGABE ---\
fhem("sleep 2;; setreading global TTS_input erledigt") if @responses;;\
}
Beispiel: notify für Sprachausgabe mit dem TTS-Modul
defmod n_global_TTS_output notify global:TTS_output:.* { my $text = $EVENT;;;; $text =~ s/^TTS_output:\s*//;;;; fhem("set TTS tts $text") }
Unterschiede der Wege
| Feature | Browser | Hybrid (Atom Echo S3) |
|---|---|---|
| Wakeword | Browser | ESP (Hardware) |
| Push-to-Talk | Ja | Nein |
| Always-On | Ja (bedingt) | Ja |
| Architektur | Software | Software + Hardware |
| Stabilität | Sehr stabil | Sehr stabil (Wakeword extern) |
FireOS mit Fully Browser (Plus)
Damit die Spracherkennung funktioniert:
- Apps installieren:
Google Speech Recognition & Synthesis Activity Launcher (Für bessere Sprachausgabe)
- FireOS:
„Tastatur und Sprache“ → „Text-to-Speech“
- Fully Browser:
Enable JavaScriptInterface (PLUS) aktivieren
Bessere Stimme für Sprachausgabe (4 Googlestimmen):
Damit konnte ich die roboterhafte Stimme von Hans gegen eine bessere Stimme tauschen.
- App Activity Launcher aus Playstore installieren
- Die App starten
- Suche nach Google
- Öffne --> Spracherkennung und -Synthese von Google
- Öffne --> Sprache hinzufügen
- Starte Aktivität
- Deutsch downloaden
- Deutsch anklicken. Nun stehen 4 Stimmen zur Auswahl. 2 weibliche und 2 männliche Stimmen.
- Zum Abschluss noch per adb shell auf das Device connecten und folgendes eingeben:
settings put secure tts_default_synth com.google.android.tts
reboot
Wer zurück zu Amazon möchte:
settings put secure tts_default_synth com.ivona.tts.oem
reboot