Zum Inhalt springen

FHEMWEB/VoiceControl: Web-STT & Hardware-Wakeword: Unterschied zwischen den Versionen

Aus FHEMWiki
Schwatter (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
Schwatter (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
 
(19 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
[[Datei:VoiceControl_Logo.png|Mini|rechts]]
[[Datei:Voicecontrol.png|mini]]


== VoiceControl – Sprachsteuerung via Browser und Atom Echo s3r ==
== VoiceControl – Sprachsteuerung via Browser und Atom Echo s3r ==
Zeile 5: Zeile 5:
Diese Lösung ermöglicht eine flexible Sprachsteuerung für [[FHEM]]. Sprache wird in Text (Speech-to-Text) umgewandelt und als Reading <code>STT</code> im Device <code>global</code> bereitgestellt. Dieses Reading kann anschließend zentral (z. B. über <code>notify</code> oder <code>DOIF</code>) ausgewertet werden.
Diese Lösung ermöglicht eine flexible Sprachsteuerung für [[FHEM]]. Sprache wird in Text (Speech-to-Text) umgewandelt und als Reading <code>STT</code> im Device <code>global</code> bereitgestellt. Dieses Reading kann anschließend zentral (z. B. über <code>notify</code> oder <code>DOIF</code>) ausgewertet werden.


Das System unterstützt zwei unterschiedliche Wege zur Spracherfassung:
'''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 1️⃣ (Software): Browser-basierte Komplettlösung
* Weg 2️⃣ (Hybrid): Hardware-Wakeword + Browser-Spracherkennung
* Weg 2️⃣ (Hybrid): Hardware-Wakeword + Browser-Spracherkennung


=== Hilfe ===
=== Hilfe ===
* Forenthread zum {{Link2Forum|Topic=144147|LinkText=VoiceControl Sprachsteuerung}}
* Forenthread zum {{Link2Forum|Topic=144147|LinkText=VoiceControl Sprachsteuerung}}
Dort befinden sich in Post #1 auch alle benötigten Dateien.




Zeile 35: Zeile 38:
== Weg 1️⃣: Browser-Lösung (voicecontrol.js) ==
== Weg 1️⃣: Browser-Lösung (voicecontrol.js) ==


Das Script nutzt die Google Web Speech API. Unterstützt werden Chromium-basierte Browser (Chrome, Edge, Fully Browser). Firefox wird aktuell nicht unterstützt.
Das Script nutzt nur die Google Web Speech API.


=== Bedienung ===
=== Bedienung ===


==== Push-to-Talk ====
==== Push-to-Talk ====
* Button gedrückt halten (~450 ms)
* Button gedrückt halten
* Direkt sprechen (kein Wakeword nötig)
* Wakeword erforderlich (Standard: „James“)
* Befehl wird sofort verarbeitet
* Nach Aktivierung ca.6sek Zeit für Befehl


==== Always-On ====
==== Always-On ====
* Kurzer Klick aktiviert Dauerbetrieb
* Kurzer Klick aktiviert Dauerbetrieb
* Wakeword erforderlich (Standard: „James“)
* Wakeword erforderlich (Standard: „James“)
* Nach Aktivierung permanentes Mithören
* Nach Aktivierung ca.6sek Zeit für Befehl
* JS wird neu gestartet udn Schleife fängt von vorne an


==== Wakeword ====
==== Wakeword ====
Zeile 54: Zeile 58:
<syntaxhighlight lang="javascript">
<syntaxhighlight lang="javascript">
const wakewords = ["james"];</syntaxhighlight>
const wakewords = ["james"];</syntaxhighlight>
Ablauf:
# „James“ sagen
# System antwortet „Ja?“
# Zeitfenster (~6 Sekunden) für Befehl
# Danach automatische Verarbeitung oder Abbruch


=== Installation ===
=== Installation ===


==== Datei kopieren ====
==== Datei kopieren ====
<code>voicecontrol.js</code> nach:
<syntaxhighlight lang="Perl">
<code>/opt/fhem/www/voicecontrol/</code>
{ Svn_GetFile('contrib/voicecontrol.js', 'www/pgm2/voicecontrol.js') }
</syntaxhighlight>


==== Einbindung ====
==== Einbindung ====
<syntaxhighlight lang="perl">
<syntaxhighlight lang="perl">
attr WEBphone JavaScripts voicecontrol/voicecontrol.js
attr WEBphone JavaScripts www/pgm2/voicecontrol.js
</syntaxhighlight>
</syntaxhighlight>


Zeile 81: Zeile 79:




== Weg 2️⃣: Hybrid-Lösung mit Browser + Atom Echo (voicecontrol_echo.js) ==
== Weg 2️⃣: Hybrid-Lösung mit Fully Kiosk Browser + Atom Echo (voicecontrol_echo.js) ==


Diese Variante kombiniert Hardware und Software:
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)
* Wakeword-Erkennung erfolgt auf dem ESP (Atom Echo s3r)
Zeile 101: Zeile 100:
* Baut WebSocket-Verbindung zu FHEM auf
* Baut WebSocket-Verbindung zu FHEM auf
* Kein Push-to-Talk-Modus
* Kein Push-to-Talk-Modus
=== Installation ===
==== Datei kopieren ====
Die Datei voicecontrol_echo.js ist hier zu finden:
{{Link2Forum|Topic=144147|LinkText=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 ====
<syntaxhighlight lang="perl">
attr WEBtablet JavaScripts www/pgm2/voicecontrol_echo.js
</syntaxhighlight>
* Das Skript voicecontrol_echo.js muss in dem FHEMWEB-Device angelegt werden, das für den Fully Kiosk Browser zuständig ist
<syntaxhighlight lang="perl">
attr WEBtablet additionalInform atom_echos3r_9888e00f4280,global
</syntaxhighlight>
* atom_echos3r_9********** = da kommt das Wakeword an, bzw. der Devicename vom Echo
* global = das ist zum auswerten der Sprachausgabe wichtig


=== Konfiguration im Javascript ===
=== Konfiguration im Javascript ===
Zeile 147: Zeile 171:
       - model: "https://github.com/TaterTotterson/microWakeWords/raw/main/microWakeWordsV2/james.json"
       - model: "https://github.com/TaterTotterson/microWakeWords/raw/main/microWakeWordsV2/james.json"
         id: james_model
         id: james_model
         probability_cutoff: 0.6    
         probability_cutoff: 0.45    


=== Kommunikation ===
=== Kommunikation ===
Zeile 163: Zeile 187:
# Rückmeldung „Erledigt“
# Rückmeldung „Erledigt“


=== Installation ===
== Zentrale Auswertung (Logik) ==


siehe oben wie unter Punkt 1.
Beide Wege schreiben in:


== Zentrale Auswertung (Logik) ==
<code>global:STT_output</code>


Beide Wege schreiben in:
Die Verarbeitung erfolgt zentral über ein <code>notify</code>. Ein Device wird in der Mappingtabelle eingetragen.


<code>global:STT</code>
Beispiel:
<code>"esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer",  cmdOn => "on", cmdOff => "off" }</code>


Die Verarbeitung erfolgt zentral über ein <code>notify</code>.
Aufschlüsselung:
<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:STT:.* {\
defmod n_VoiceControl notify global:STT_output:.* {\
    # 1. VORBEREITUNG\
     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/;;\
\
\
     my %lightRooms = (\
    # --- 1. Geräte-Liste ---\
         "esszimmer" => { dev => "Lampe01_Ez",       label => "Licht Esszimmer" },\
     my %devices = (\
         "küche"     => { dev => "Deckenlampe_Kue", label => "Licht Küche" },\
         "esszimmer:licht|lampe|deckenlampe" => { dev => "Lampe01_Ez", label => "Licht Esszimmer (an/aus)" },\
         "wohnzimmer" => { dev => "Lampe06_Dek",         label => "Licht Wohnzimmer" }\
        "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" },\
    my %vacRooms = (\
         "wohnzimmer:licht|lampe|deckenlampe" => { dev => "Lampe06_Dek", label => "Licht Wohnzimmer (an/aus)" },\
         "arbeitszimmer" => "Arbeitszimmer",\
        "fernseher|tv" => { dev => "VuPlus", label => "Fernseher (an/aus)" },\
         "badezimmer" => "Badezimmer",\
        "rechner|pc" => { dev => "PC_Aktor", label => "PC (an/aus)" },\
         "esszimmer" => "Esszimmer",\
        "garage|tor" => { dev => "Garagentor_Aktor", label => "Garagentor (öffnen/schließen)", cmdOn => "open", cmdOff => "close" },\
         "flur" => "Flur",\
         "kaffee" => { dev => "Kaffeemaschine (an/aus)", label => "Kaffeemaschine" },\
        "küche" => "Küche",\
         "roberto" => { dev => "MQTT2_valetudo_FlusteredUnequaledFish", label => "Lade Roberto (an/aus)", cmdOn => "charge" },\
         "wohnzimmer" => "Wohnzimmer"\
         \
        # 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 $onRegEx = '\b(an|ein|einschalten|starte|aktivier|aktiviere)\b';;\
    # --- 2. TEXTBEREINIGUNG ---\
     my $offRegEx = '\b(aus|ausschalten|stop|stoppe|beende|deaktivier|deaktiviere)\b';;\
     my $text = lc($cleanEvent);;\
    $text =~ s/^stt_output:\s*//;; \
    $text =~ s/\[\d+\.?\d*\]//g;;   \
     $text =~ s/^\s+|\s+$//g;;       \
\
\
     my @commands = split(/\s*(?:und|dann|,)\s*/, lc($cleanEvent));;\
     # --- 3. SPEZIALFÄLLE (KI & HILFE) ---\
\
     if ($text =~ /\bfrage\s+(.*)$/) {\
    # 2. DER BEFEHLS-LOOP\
         my $q = $1;;\
     foreach my $cmd_part (@commands) {\
         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));;\
        $cmd_part =~ s/^\s+|\s+$//g;;\
         fhem("sleep 2;; setreading global TTS_input erledigt");;\
         \
        return;;\
         my $is_on  = ($cmd_part =~ /$onRegEx/) ? 1 : 0;;\
    }\
         my $is_off = ($cmd_part =~ /$offRegEx/) ? 1 : 0;;\
\
         $cmd_part =~ s/\b(ich|brauche|mach|bitte|kannst du|würdest du|mal|doch|den|das|die|im|in der)\b//g;;\
\
\
        # --- INTENT: STAUBSAUGER ---\
    if ($text =~ /(hilfe|kommandos|übersicht)/) {\
        if ($cmd_part =~ /(reinige|sauge|putze|staubsauger|roboter)/) {\
        my $h = '<div style="text-align:left;;min-width:250px;;font-family:sans-serif;;"><b>Befehlsübersicht:</b><br><br>';;\
            my @found = grep { $cmd_part =~ /\b$_\b/ } keys %vacRooms;;\
        my %seen;;\
            if (@found) {\
        for my $k (sort keys %devices) {\
                fhem("set MQTT2_valetudo_FlusteredUnequaledFish clean_segment " . join(",", map { $vacRooms{$_} } @found));;\
            my $d = $devices{$k};;\
             } else {\
             next if $seen{$d->{label}}++;;\
                fhem("set MQTT2_valetudo_FlusteredUnequaledFish start");;\
             # Nur noch das Prozent-Suffix für das Ambiente-Licht wird dynamisch angehängt\
             }\
             my $suffix = ($d->{run} && $k eq "ambiente") ? " [0-100%]" : "";;\
             next;;\
             $h .= "• $d->{label}$suffix<br>";;\
        } elsif ($cmd_part =~ /(lade|aufladen|dock|station|home)/) {\
            fhem("set MQTT2_valetudo_FlusteredUnequaledFish charge");;\
             next;;\
         }\
         }\
        $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;;\
    }\
\
\
        # --- INTENT: FERNSEHER ---\
    # --- 4. AKTIONSERKENNUNG LOOP ---\
        if ($cmd_part =~ /(fernseher|tv|vuplus)/) {\
    my $global_on  = $text =~ /$onRegEx/ ? 1 : 0;;\
            fhem("set VuPlus " . ($is_off ? "off" : "on"));;\
    my $global_off = $text =~ /$offRegEx/ ? 1 : 0;;\
            next;;\
        }\
\
\
        # --- INTENT: AMBIENTE ---\
    my @parts = split(/\s*(?:dann|,)\s*|(?<=\ban\b)\s*und\s*|(?<=\baus\b)\s*und\s*/, $text);;\
        if ($cmd_part =~ /ambiente/) {\
    @parts = ($text) if @parts == 1;;\
            if ($cmd_part =~ /(\d+)/) {\
                my $b = ($1 > 255 ? 255 : ($1 < 1 ? 1 : $1));;\
                fhem("set LampeSzeneAlle brightness $b");;\
            } else {\
                fhem("set LampeSzeneAlle " . ($is_off ? "off" : "on"));;\
            }\
            next;;\
        }\
\
\
        # --- INTENT: AMBILIGHT ---\
    for my $part (@parts) {\
         if ($cmd_part =~ /ambilight/) {\
         $part =~ s/^\s+|\s+$//g;;\
            system("sshpass -p 'GEHEIMESPASSWORT' ssh -o StrictHostKeyChecking=no root\@192.168.1.46 '/usr/share/hyperhdr/scripts/hyperhdr_toggle.sh'");;\
        next unless $part;;\
            next;;\
        }\
\
\
        # --- INTENT: LICHT ---\
         my $is_on  = $part =~ /$onRegEx/ ? 1 : ($part =~ /$offRegEx/ ? 0 : $global_on);;\
         my ($lightRoom) = grep { $cmd_part =~ /\b$_\b/ } keys %lightRooms;;\
        my $is_off = $part =~ /$offRegEx/ ? 1 : ($part =~ /$onRegEx/ ? 0 : $global_off);;\
        if ($lightRoom || $cmd_part =~ /(licht|lampe)/) {\
            my $dev = $lightRooms{$lightRoom}{dev} // "LampeSzeneAlle";;\
            fhem("set $dev " . ($is_off ? "off" : "on")) if ($is_on || $is_off);;\
            next;;\
        }\
\
\
         # --- HILFE (DYNAMISCH) ---\
         # Trennung: Original-Part bleibt für "run"-Blöcke, clean_part kriegt die Füllwortbereinigung\
         if ($cmd_part =~ /(hilfe|kommandos|übersicht)/) {\
        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;;\
\
\
            my $h = '<div style="text-align:left;;min-width:250px;;font-family:sans-serif;;">';;\
        # Schleife durch die Geräte (Sonderfunktionen mit "run" werden zuerst bewertet)\
             $h .= '<b>Befehlsübersicht:</b><br><br>';;\
        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/;;\
\
\
            # Licht\
                my $d = $devices{$key};;\
            $h .= '<u>Licht</u><br>';;\
                if ($d->{run}) {\
            for my $k (sort keys %lightRooms) {\
                    my $res = $d->{run}->($d, $part, $is_on, $is_off);;\
                $h .= "".$lightRooms{$k}{label}." an/aus<br>";;\
                    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");;\
                }\
             }\
             }\
\
            # Staubsauger\
            $h .= '<br><u>Staubsauger</u><br>';;\
            $h .= "• Sauge [Raum]<br>";;\
            $h .= "  Räume: ".join(", ", map { ucfirst($_) } sort keys %vacRooms)."<br>";;\
            $h .= "• Lade Roberto<br>";;\
\
            # Sonstiges\
            $h .= '<br><u>Sonstiges</u><br>';;\
            $h .= "• Fernseher an/aus<br>";;\
            $h .= "• Ambiente [an|aus|1-255]<br>";;\
            $h .= "• Ambilight<br>";;\
\
            $h .= '</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");;\
\
            next;;\
         }\
         }\
     }\
     }\
\
    # --- 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 ==
Zeile 335: Zeile 367:
   Google
   Google
   Speech Recognition & Synthesis
   Speech Recognition & Synthesis
  Activity Launcher (Für bessere Sprachausgabe)
* FireOS:
* FireOS:
   „Tastatur und Sprache“ → „Text-to-Speech“
   „Tastatur und Sprache“ → „Text-to-Speech“
* Fully Browser:
* Fully Browser:
   <code>Enable JavaScriptInterface (PLUS)</code> aktivieren
   <code>Enable JavaScriptInterface (PLUS)</code> 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:
<code>
settings put secure tts_default_synth com.google.android.tts
reboot
</code>
Wer zurück zu Amazon möchte:
<code>
settings put secure tts_default_synth com.ivona.tts.oem
reboot
</code>




[[Kategorie:Code Snippets]]
[[Kategorie:Code Snippets]]
[[Kategorie:Schnittstellen]]
[[Kategorie:FHEM Frontends]]
[[Kategorie:Sprachsteuerung]]

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


Funktionen

Grundprinzip

  • Sprache → Speech-to-Text
  • Ergebnis → Reading STT im Device global
  • 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 Enabled setzen


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

  1. Wakeword wird auf dem ESP erkannt
  2. FHEM erzeugt Event (z. B. james_detected)
  3. Browser empfängt Event via WebSocket
  4. Speech-to-Text startet im Browser
  5. 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 ESP
  • TRIGGER → Event bei Wakeword
  • FHEM_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

  1. System sagt „Ja?“
  2. Browser startet SpeechRecognition
  3. Nutzer spricht Befehl
  4. Befehl wird verarbeitet
  5. 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