Hallo zusammen,
mal wieder ein "work in Progress"-Thread...
Was bisher geschah:
Rudi hat f18 etwas aufgebohrt, so dass man per Klick gesprochene Sprache an FHEM übermitteln kann, die Vorarbeiten (nochmals ein FETTES DANKE an @schwatter) sind in [Voicecontrol] Button für Fhemweb (https://forum.fhem.de/index.php?topic=144147.0) zu finden.
Mein Ausgangspunkt war eigentlich, das FULLY-Modul so zu erweitern, das es mit RHASSPY in Punkto Sprachein- und -ausgabe so zusammenarbeitet, wie das vorher mit AMAD möglich gewesen war, insbesondere also interaktive Dialoge möglich sind. Wie es im Moment aussieht, braucht es dafür aber auch einiges an javascript, so dass es letztlich (fast) völlig egal ist, welchen Browser man verwendet...
Wer hier also mit testen und entwickeln will: Am unkompliziertesten scheint Chrome (unter Android) zu sein, den man zwischenzeitlich auch mit FHEM als "WebApp" fullscreen starten kann (super Anleitung hier https://forum.fhem.de/index.php?msg=1360621).
Mein erster Stolpersteil war die Frage, wie man das Mikro wieder aktivieren kann. Dazu hier erst mal ein snipplet (Danke an schwatter für die Vorlage!), wie das mit dem in f18 eingebauten stt-Code geht:
my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$hash->{FW_ID}'){f18_stt()}";
FW_directNotify("#FHEMWEB:$_", $js, "")
for devspec2array("TYPE=FHEMWEB");
Was aus mir bisher nicht bekannten Gründen nicht funktioniert:
FW_directNotify("#FHEMWEB:$_", "f18_stt()", "")
for devspec2array("TYPE=FHEMWEB:FILTER=FW_ID=$hash->{FW_ID}");
Wo soll das hinführen?
1. Spracheingabe wird aktiviert
2. Man sagt was (z.B. "wie spät ist es")
3. Es gibt ein Event am FHEMWEB-Device, so dass identifizierbar ist, wo genau der Text eingesprochen wurde. Stand jetzt wäre es auch an der FULLY-Instanz sichtbar, und FULLY leitet das auch schon zur Beantwortung an RHASSPY weiter.
4. Das Event wird verarbeitet in einer NotifyFn() (bzw. in FULLY direkt per spezieller set-Anweisung), also ein Gerät wird geschaltet, oder hier einfach: Die Antwort auf die Frage wird ermittelt.
5. (optional): Der Antworttext wird in Audio umgewandelt
6. An der Stelle, von der die Frage (bzw. Anweisung) kam, erfolgt eine Ausgabe, entweder als TTS-Anweisung, oder als "play audio"-Befehl (mit FULLY+RHASSPY bereits als TTS-Anweisung funktional)
=> der Einsprechende erhält seine Rückmeldung.
7. (optional) Er kann eine weitere Anweisung einsprechen, oder ggf. eine Rückfrage beantworten. Hier kommt dann das obige snipplet zum Tragen, und das Spiel kann von vorne (bei 2.) beginnen. Das (nächste zu lösende) Problem ist demnach: Wann ist der richtige Zeitpunkt dafür? Tendenziell muss unser javascript noch ein paar Events erzeugen, aus dem man ablesen kann, was der Status der Audio-Geräte (Mikro/Sounausgabe) gerade ist.
Wie man sieht: Das hat im Kern wenig mit RHASSPY zu tun, das funktioniert auch z.B. mit Babble vermutlich kaum anders...
Das soll es für den Moment erst mal gewesen sein :) .
Wer mitexperimentieren will, kann sich einfach hier melden.
Als Basis erst mal ein JavaScript auf Basis der Vorarbeit von schwatter. Liegt bei mir in www/pgm2 und ist via Javascripts-Attribut in FHEMWEB aktiviert.
Da ist im Moment noch nicht viel anders wie vorher, außer dass es unter Chromium@Linux nicht läuft, unter Chrome@Android aber schon...+
Anders ist:
- Der eingesprochene Text wird kurz angezeigt ("eigentlich" sollte das "on-the-fly" auch mit Teilen erfolgen...) und dann automatisch an FHEM übermittelt
- Die Aktivierung erfolgt im Moment nur durch Klicken, es sollte auch gehen, das von FHEM aus zu aktivieren.
- Da man dazu die FW_ID benötigt, erzeugt diese Version die STT-Events/Readings (auch*) an der temporären FHEMWEB-Instanz, die zur betreffenden Verbindung gehört.
*Zusätzlich kommt ein Event an der FULLY-Instanz, falls man fully als Browser benutzt und die deviceid paßt.
Braucht alles noch Feinschliff, geht erst mal v.a. drum, auch die Zwischenschritte zu dokumentieren...
Nabend,
zu dem Codesnippet oben. Wo probiert du das gerade aus? In Fully, FHEMWEB oder mit f18. Muss fragen,
da ich den Überblick verloren habe.
Vielleicht noch ein paar Details zu deinen Devices, um sich ein Bild zu machen.
Edit:
Ich sehe du hast schon geantwortet. Ich gehe mal dem Problem mit Chromium nach.
Ich muss gestehen, das ich hier und da geschrieben habe Chrome/Chromium funktioniert,
ich habe es aber nie selber ausprobiert. Bin einfach davon ausgegangen, datt geht.
Melde mich wegen Chromium.
Gruß schwatter
Der JavaScript-Code wird via FHEMWEB-Instanz geladen, vermutlich werde ich dazu künftig zu weiteren Tests eine eigene Instanz aufmachen.
Das Mikro wird dann (außer bei Chromium@Linux) in FF@Linux/Android, Chrome@Android und fully unten angezeigt.
Getestet habe ich dann unter Android sowohl mit fully wie mit Chrome.
fully spricht den set-Command an der betreffenden FULLY-Instanz an, der dann (gesteuert über ein Attribut) den Text an RHASSPY weiterleitet. Das wertet (analog zu deinem notify, aber sehr viel differenzierter...) den Text aus, schaltet ggf. entsprechend der Anweisungen ("Schalte das Radio im Esszimmer und Wohnzimmer an und das Licht am Esstisch aus"...) und gibt Rückmeldung (als Text) an FULLY, der das dann an fully aussprechen läßt (derzeit...).
Chrome erzeugt im Moment nur Events. Der Plan ist, RHASSPY per NotifyFn() darauf anzusetzen und dann direkt in FHEMWEB (via FW_directNotify()) die Antwort aussprechen zu lassen. (Der Code ist im Prinzip derselbe wir für AMAD, also zu 90% schon da...)
Letzteres ist dann mein nächster Step, wobei dazu optimalerweise ein "speak" mit nachgelagertem "schalte das Mikro wieder scharf, wenn du fertig mit sprechen bist" in den Code kommt...
Raw-Definitionen von den Devices dürften im Moment nicht viel weiterhelfen. Doku zu RHASSPY gibt es ausführlich im Wiki (https://wiki.fhem.de/wiki/RHASSPY).
Auszugsweise, was man (außer, das in RHASSPY per devspec zu erfassen) konfigurieren muss, um das vernünftig (an/aus, Helligkeit, colortemp, Farbe...) aus RHASSPY heraus schalten zu können:
defmod Licht_Essen MQTT2_DEVICE zigbee_Esstisch
attr Licht_Essen genericDeviceType light
attr Licht_Essen rhasspyName licht am esstisch
Oder der Receiver:
defmod Yamaha_Main YAMAHA_AVR <IP-Adresse>
attr Yamaha_Main genericDeviceType media
attr Yamaha_Main rhasspyMapping GetNumeric:currentVal=volume,type=volume\
SetNumeric:currentVal=volume,cmd=volume,minVal=0,maxVal=99,step=2,type=volume\
SetOnOff:cmdOn=on,cmdOff=off\
GetOnOff:currentVal=state,valueOff=off\
GetState:response=Verstärker ist [Yamaha_Main:state] die Lautstärke ist [Yamaha_Main:volume]
attr Yamaha_Main rhasspyName verstärker,receiver,radio
attr Yamaha_Main rhasspyRoom wohnzimmer
attr Yamaha_Main rhasspySpecials scenes:scene1="Musik hören" scene2="Film ansehen" scene3=none scene4="vorderen Eingang auswählen"\
priority: inRoom=volume outsideRoom=volume,scene\
confirm: SetOnOff="wirklich $target $Value schalten?" SetScene
Ist das jetzt etwas klarer?
Ok,
danke. Dein JS habe ich kurz reingeschaut. Du hast einfach den Freischaltprocess ausgeschaltet um direkt Google SpeechToText zu starten.
Ja Chromium, da werde ich nochmal in Ruhe schauen warum der Button nicht auftaucht.
5 Minuten Suche im Internet hat mir aber offenbart, genau so schlecht wie bei Firefox.
Quasi wie ein AOSP Rom für ein Androidhandy. Der is blank. Keys kann man selber einbauen aber....ist mist. Fully z.B greift da auf Android Systemdienste zu.
Gruß schwatter
Moin,
hier mal ein Codeschnipsel mit dem du TextToSpeach machen kannst. 2ter Informchannel auf ein reading deiner
Wahl. Mein notify macht jetzt am Ende ein setreading voiceSpeak in global.
function startExternalTunnel() {
if (!isJamesActive) return;
if (jamesSocket) jamesSocket.close();
if (globalSocket) globalSocket.close();
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
const combinedFilter = `(${DEVICE}|global)`;
jamesSocket = new WebSocket(`${protocol}//${FHEM_IP}/fhem?XHR=1&inform=type=status;filter=${combinedFilter}`);
jamesSocket.onmessage = (e) => {
if (!isJamesActive) return;
if (!isWaitingForCommand && !isSpeaking && e.data.includes(DEVICE) && e.data.includes(TRIGGER)) {
startJamesSTT();
}
if (e.data.includes("global-voiceSpeak")) {
let raw = e.data.split('global-voiceSpeak')[1];
let txt = raw.replace(/^[^\w\däöüÄÖÜß]+/, '').split(/[",\]<]/)[0];
txt = txt.replace(/_/g, ' ').trim();
if (txt && txt !== "no" && txt !== "definition") {
showBubble("🤖 " + txt);
speak(txt);
}
}
};
jamesSocket.onopen = () => console.log("DEBUG: James-Kombi-Socket verbunden");
}
edit:
Nochmal angepasst auf eine Websocketverbindung mit Filter.
Gruß schwatter
Zitat von: schwatter am 03 April 2026, 21:46:11Ja Chromium, da werde ich nochmal in Ruhe schauen warum der Button nicht auftaucht.
Bei meinem Code dachte ich, es läge nach der JS-Konsole an der Syntax. Jetzt ist das Mikro aber auch an Chromium@Linux da. Es tut nur nix in Richtung STT-Reading :o .
Zitat von: schwatter am 04 April 2026, 08:27:512ter Informchannel auf ein reading deiner Wahl
:)
Der Ansatz, dafür einen socket von der JS-Seite her aufzumachen, will zumindest im Moment nicht zu meinen Design-Vorstellungen passen:
Der js-Code soll am Ende ganz ohne irgendwelche Vorab-Parametrierungen (const DEVICE = "atom_echos3r_9888e00f4280"; und so) auskommen, der diesbezügliche Block am Anfang ist im Moment nur noch deswegen drin, weil der Code erst mal nicht zu sehr verändert werden sollte, um die Lauffähigkeit zu erhalten ;D .
Die Idee wäre, aus dem Endgerät (und eventuell dem verwendeten wakeword oä.) abzulesen, wer da gerade sprechen will, um dann die Parametrierung des aktuell im Browser laufenden Codes und/oder von FHEM so anzupassen, dass z.B. "er" auf seinem Endgerät deutsche Ein- und Ausgaben erhält, und "sie" auf ihrem spanische...
(Oder die Kinder bestimmte Dinge nicht dürfen?)
Vielleicht wäre eine Funktion hilfreich, die einen JSON-Blob entgegennimmt, aus dem sich alle jeweils aktuell erforderlichen Infos ableiten lassen. Falls du da eine Idee hast: Gerne!!!
Ich konnte die Websocketverbindung auf eine reduzieren. Siehe oben. Aber so wie bei
dir, das passt auch nicht zu meinen Designvorstellungen. Problem, fhemweb.js
Lebt ihm hier und jetzt, bzw abonniert nur Devices bei Raum- oder Deviceübersicht.
Direkt ans Backendfiltern klappt nicht? Wenn doch, vielleicht hat wer ein Beispiel?
Oder ich bin gerade zu doof :-[
Gruß schwatter
Zitat von: schwatter am 04 April 2026, 10:00:35Lebt ihm hier und jetzt, bzw abonniert nur Devices bei Raum- oder Deviceübersicht.
Das ist in der Tat ein Problem.
Zitat von: schwatter am 04 April 2026, 10:00:35Wenn doch, vielleicht hat wer ein Beispiel?
Hmm, ich _glaube_, du hattest ein passendes Beispiel geliefert!!!
Meine Mikro-Aktivierung
Zitat von: Beta-User am 03 April 2026, 11:24:46 my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$hash->{FW_ID}'){f18_stt()}";
FW_directNotify("#FHEMWEB:$_", $js, "")
for devspec2array("TYPE=FHEMWEB");
basiert auf deinem notify-Code, jetzt zu finden unter https://wiki.fhem.de/wiki/FHEMWEB/VoiceControl:_Web-STT_%26_Hardware-Wakeword#Beispiel:_notify, dort der Abschnitt #Hilfe.
Damit machst du was genau? Du sendest an eine mehr oder weniger unbekannte Stelle formatierten Text hin, um den in genau einem FHEMWEB-Client anzuzeigen... Das müßte doch eigentlich genauso für TTS-Infos gehen ;) , oder stehe ich auf dem Schlauch?
Thx to Rudi gibt es jetzt auch eine speak-Funktion in f18 :) . (=> https://forum.fhem.de/index.php?msg=1361205).
Damit kann man RHASSPY jetzt aus Chrome@Android wie folgt befragen, zum Testen habe ich eine separate FHEMWEB-Instanz aufgemacht, und die mit zwei allowed-Devices abgesichert, und beim einen einen ROOMMATE als username und damit erlaubtem Partner abgesichert:
defmod WEBVOICE FHEMWEB 8084 global
attr WEBVOICE HTTPS 1
attr WEBVOICE styleData {\
"f18": {\
"Pinned.menu": false,\
[...]
"showMicro": true\
}\
}
attr WEBVOICE stylesheetPrefix f18
attr WEBVOICE title { if($FW_room) { "FHEM: $FW_room" } elsif($FW_detail) { "FHEM: $FW_detail" } else { "Home, Sweet Home" } }
defmod allowed_rr_Frau allowed WEBVOICE
attr allowed_rr_Frau basicAuth <user sollte dem ROOMATE-Namen entsprechen>
attr allowed_rr_Frau validFor WEBVOICE
defmod allowed_rr_Mann allowed WEBVOICE
attr allowed_rr_Mann basicAuth <user muss dem ROOMATE-Namen entsprechen>
attr allowed_rr_Mann validFor WEBVOICE
Dann den ROOMMATE (rr_Mann) neben dem (nicht notwendigen, aber noch dort stehenden AMADDevice) eingetragen.
attr rhasspy rhasspySpeechDialog allowed=TabletWohnzimmer,rr_Mann
Fertig*...
(update ab morgen, oder aus dem svn holen)
*noch lange nicht, aber es funktioniert prinzipiell, dass man eine Antwort genau da erhält, wo man die Frage bzw. Anweisung eingesprochen hatte 8)
Zitat von: Beta-User am 04 April 2026, 11:48:18Zitat von: schwatter am 04 April 2026, 10:00:35Lebt ihm hier und jetzt, bzw abonniert nur Devices bei Raum- oder Deviceübersicht.
Das ist in der Tat ein Problem.
Zitat von: schwatter am 04 April 2026, 10:00:35Wenn doch, vielleicht hat wer ein Beispiel?
Hmm, ich _glaube_, du hattest ein passendes Beispiel geliefert!!!
Meine Mikro-Aktivierung
Zitat von: Beta-User am 03 April 2026, 11:24:46 my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$hash->{FW_ID}'){f18_stt()}";
FW_directNotify("#FHEMWEB:$_", $js, "")
for devspec2array("TYPE=FHEMWEB");
basiert auf deinem notify-Code, jetzt zu finden unter https://wiki.fhem.de/wiki/FHEMWEB/VoiceControl:_Web-STT_%26_Hardware-Wakeword#Beispiel:_notify, dort der Abschnitt #Hilfe.
Damit machst du was genau? Du sendest an eine mehr oder weniger unbekannte Stelle formatierten Text hin, um den in genau einem FHEMWEB-Client anzuzeigen... Das müßte doch eigentlich genauso für TTS-Infos gehen ;) , oder stehe ich auf dem Schlauch?
Hey, ne. Da reden wir aneinander vorbei. Mir ging es darum, im Webinterface direkt per inform Updates zu Devices
zu bekommen, welche nicht in einem Raum/DeviceOverview sind. Per FW_directNotify wird ja nur ein JS-Befehl abgeschickt.
Gruß schwatter
Zitat von: schwatter am 05 April 2026, 20:49:19Hey, ne. Da reden wir aneinander vorbei. Mir ging es darum, im Webinterface direkt per inform Updates zu Devices zu bekommen, welche nicht in einem Raum/DeviceOverview sind.
Hmm, ok.
Vermutlich fehlen mir da noch ein paar weitere Verständnis-Brocken. Mein Ansatz scheint etwas anders zu sein, nämlich, dass das js "nichts" zu "wissen" braucht, also z.B. die Verbindung zwischen einem wakeword-Event an einem ESP (das wird dauern...) und einem bestimmten Endgerät dann eher über ein notify hergestellt werden soll. Dazu braucht es verlässliche Info auf der FHEM-Seite, was im Moment nur über fully/FULLY 1:1 geht, oder wohl auch z.B. über die IP des Endgeräts, falls man die fest einstellt.
Für den nächsten Schritt kämpfe ich mit piper-tts...
Der Server an sich läuft, was auch die empfohlene Variante für schnelle Reaktion wäre, aber die Anfrage-Syntax ist anders als bei z.B. maryTTS, https://github.com/OHF-Voice/piper1-gpl/blob/main/docs/API_HTTP.md:
curl -X POST -H 'Content-Type: application/json' -d '{ "text": "This is a test." }' -o test.wav localhost:5000Das Text2Speak-Modul "kann" zwar maryTTS, aber für diesen Call müßte man das mal wieder aufbohren, und außerdem gefällt mir das "Zwischenlagern" von files nicht...
Via https://curlconverter.com/javascript/ bin ich dann auf diesen Testcode gekommen:
function
f18_speak2(txt)
{
//curl -X POST -H 'Content-Type: application/json' -d '{ "text": "This is a test." }' -o test.wav
var url = fetch('http://<server-ip>:5000', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': 'http://192.168.2.70'
},
// body: '{ "text": "This is a test." }',
body: JSON.stringify({
'text': 'Hier spricht Thorsten!'
})
});
// console.log('PLAY:', url);
var a = new Audio(url);
a.addEventListener("ended", function(){
a.currentTime = 0;
var fw_id = $("body").attr("fw_id");
FW_cmd(`${FW_root}?cmd=setreading `+
`TYPE=FHEMWEB:FILTER=FW_ID=${fw_id}:FILTER=inform=.%2B `+
`TTS_state audio finished&XHR=1`);
});
a.play().catch(function(e){
console.log('audio blocked:', e);
});
}
Nun ja, über die js-console gibt es jetzt entweder die Rückmeldung, dass die Quelle HTTPS sein sollte, und irgendwas mit CORS. Mal wieder Neuland...
Zitat von: Beta-User am 14 April 2026, 07:42:32Zitat von: schwatter am 05 April 2026, 20:49:19Hey, ne. Da reden wir aneinander vorbei. Mir ging es darum, im Webinterface direkt per inform Updates zu Devices zu bekommen, welche nicht in einem Raum/DeviceOverview sind.
Hmm, ok.
Vermutlich fehlen mir da noch ein paar weitere Verständnis-Brocken. Mein Ansatz scheint etwas anders zu sein, nämlich, dass das js "nichts" zu "wissen" braucht, also z.B. die Verbindung zwischen einem wakeword-Event an einem ESP (das wird dauern...) und einem bestimmten Endgerät dann eher über ein notify hergestellt werden soll. Dazu braucht es verlässliche Info auf der FHEM-Seite, was im Moment nur über fully/FULLY 1:1 geht, oder wohl auch z.B. über die IP des Endgeräts, falls man die fest einstellt.
Bei dem JS im contrib verfolge ich mehr oder weniger auch den Weg, das es fast nichts kennen muss. Außer
Wakeword und wo das Readings STT ist. Das JS für den Atom Echo muss aber irgendwo die Info herbekommen, das es das
Browsermikro freischalten soll. Das passiert durch das neue Inform. Da wird einfach "wakeword_detected" weitergereicht.
Jetzt auch, ohne das das MQTT-Device im JS bekannt ist. Glaub mir, das ist super, ohne das man auf die Perlebene muss, oder
einen Websocket, Jsonlist2, whatever im Hintergrund zur Abfrage braucht. Wie ein NOTIFYDEV.
So, zu Piper. Ich habe es auch mal installiert. Mit pip und pipx wollte es nicht. Daher mein Manual-Way.
1. System-Installation (Standalone Engine)
Diese Befehle installieren die Engine unabhängig von Python direkt im System.
Bash
# In temporäres Verzeichnis wechseln
cd /tmp
# Piper Standalone für x86_64 laden
wget https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz
# Entpacken und nach /opt verschieben
tar -xvf piper_amd64.tar.gz
sudo mv piper /opt/piper
# Rechte setzen (Besitz root, ausführbar für alle)
sudo chown -R root:root /opt/piper
sudo chmod -R 755 /opt/piper
# Symlink erstellen, damit 'piper' als Befehl überall funktioniert
sudo ln -sf /opt/piper/piper /usr/local/bin/piper
# Bibliotheken im System registrieren (wichtig!)
echo "/opt/piper" | sudo tee /etc/ld.so.conf.d/piper.conf
sudo ldconfig
2. Stimmen-Modell (Thorsten Medium)
Hier laden wir die Sprachdateien herunter und setzen die korrekten FHEM-Berechtigungen.
Bash
# Verzeichnis für Models erstellen
sudo mkdir -p /opt/fhem/piper_models
cd /opt/fhem/piper_models
# Thorsten Medium Modell und Konfiguration laden
sudo wget -O de_DE-thorsten-medium.onnx https://huggingface.co/Thorsten-Voice/Piper/resolve/main/de_DE-thorsten-medium.onnx
sudo wget -O de_DE-thorsten-medium.onnx.json https://huggingface.co/Thorsten-Voice/Piper/resolve/main/de_DE-thorsten-medium.onnx.json
# Rechte für den User fhem anpassen
sudo chown -R fhem:dialout /opt/fhem/piper_models
sudo chmod 644 /opt/fhem/piper_models/de_DE-thorsten-medium.onnx*
3. FHEM Audio-Verzeichnis vorbereiten
Sicherstellen, dass Piper die Sprachdatei in das Web-Verzeichnis schreiben darf.
Bash
sudo mkdir -p /opt/fhem/www/audio
sudo chown fhem:dialout /opt/fhem/www/audio
sudo chmod 775 /opt/fhem/www/audio
4. Test-Befehl (Funktionsprüfung)
Diesen Befehl nutzen, um zu prüfen, ob der User fhem fehlerfrei sprechen kann.
Bash
sudo -u fhem piper --model /opt/fhem/piper_models/de_DE-thorsten-medium.onnx --output_file /opt/fhem/www/audio/voice.wav <<< "Test erfolgreich."
Immer wenn ich Zeit habe, schau ich mal weiter bei Piper mit rein.
Gruß schwatter
Zitat von: schwatter am 14 April 2026, 21:52:10So, zu Piper. Ich habe es auch mal installiert. Mit pip und pipx wollte es nicht. Daher mein Manual-Way.
Seltsam.
V.a.: du hast mit 1.2 eine alte Version erwischt, und soweit ich verstanden habe, gab es beim Wechsel auf 1.3 grundlegende architektonische Änderungen. Z.B. klinkt sich die neue Version wohl auch direkt in espeak ein...
Aktuell ist
Zitathttps://github.com/OHF-Voice/piper1-gpl/releases/tag/v1.4.2
Ich habe dazu ein neues Verzeichnis in /opt erstellt, darin ein venv eingerichtet und dann - soweit ich mich entsinne - mit pip installiert.
Hier mein /etc/systemd/system/piper.service:
[Unit]
Description=piper TTS server
After=network.target
[Service]
type=exec
Environment=EXAMPLE_ENV=bla
WorkingDirectory=/opt/piper
ExecStart=/opt/piper/venv/bin/python -m piper.http_server -m de_DE-thorsten-medium
Restart=always
User=<secret>
Group=<secret>
[Install]
WantedBy=multi-user.targetDas sollte (auszugweise) der Abschnitt aus der bash history sein:
sudo mkdir piper
sudo chown <user> piper
cd piper
mkdir venv
python3 -m venv /opt/piper/venv
source /opt/piper/venv/bin/activate
pip install piper-tts
python3 -m pip install piper-tts[http]
python3 -m piper.download_voices de_DE-thorsten-medium
python3 -m piper.http_server -m de_DE-thorsten-medium
curl localhost:5000/voices
Zitat von: schwatter am 14 April 2026, 21:52:10Bei dem JS im contrib verfolge ich mehr oder weniger auch den Weg, das es fast nichts kennen muss. Außer Wakeword und wo das Readings STT ist. Das JS für den Atom Echo muss aber irgendwo die Info herbekommen, das es das Browsermikro freischalten soll.
Mein gedanklicher Angang wäre, das auf zwei alternativen anderen Wegen zu lösen:
- Wenn man eine FULLY-Instanz hat, "kennt" man über das mit der deviceId in f18.js die aktuelle Verbindung (FW_ID), und kann darüber das Mikro aufmachen. Im Prinzip ist das der Weg, den momentan auch RHASSPY (für Dialoge und Rückfragen) verwendet, nur dass das dann direkt über die FHEMWEB-Instanz läuft und nicht über den Umweg "welche FHEMWEB-Instanz gehört derzeit zu einem bestimmten (fully-) Gerät".
- Der andere (noch nicht getestete) Weg ginge über die allowed-Info. Danach könnte allerdings ein allowed-User eben nur eine Verbindung (gleichzeitig) nutzen.
Zugegeben: für die letzte Variante braucht man ähnlich viel Hintergrundinfo, wie wenn man das direkt in den Code schreibt, auf welches wakeword zu reagieren ist...
(Fast) OT noch eine Frage zu der derzeit in der Entwicklung befindlichen Alternative zu RHASSPY, Gemini (https://forum.fhem.de/index.php?topic=144423.0):
Wie klappt das da mit "speziellen" Funktionalitäten?
"Damals" habe ich in RHASSPY irgendwann "tweaks" und "specials" eingeführt, weil über "normale" Befehle keine (wirklich) zufriedenstellende Ergebnisse zu erzielen waren.
Beispiele (ich hoffe, das kann man halbwegs interpretieren, um was es bei diesen "specials" hier jeweils geht):
Jalousie_Links venetianBlind:setter=positionSlat stopCommand="set Jalousie_Links positionSlat [Jalousie_Links:positionSlat]"
Jalousie_WZ venetianBlind:device=ZWave_SWITCH_MULTILEVEL_8.02 stopCommand="set ZWave_SWITCH_MULTILEVEL_8.02 dim [ZWave_SWITCH_MULTILEVEL_8.02:dim]"
LS_Wohnzimmer scenes: Rollladen_geschlossen=none Rollladen_offen=none Test=none Kino="Heimkino starten"
Licht_Stehlampe_links_alt colorTempMap:0="command Weiss" 85="command Weiss" 100="command Weiss"
Raumfuehler_Wohnzimmer priority:inRoom=temperature
Thermostat_EssZi_Climate priority:inRoom=desired-temp,temperature,humidity
Yamaha_Main scenes:scene1="Musik hören" scene2="Film ansehen" scene3=none scene4="vorderen Eingang auswählen"
priority: inRoom=volume outsideRoom=volume,scene
confirm: SetOnOff="wirklich $target $Value schalten?" SetScene
Yamaha_Zone2 priority:inRoom=volume
Nabend,
ok, es ist ganz einfach wenn die richtige Repo benutzt wird ;D Danke für den Link.
root@ubuntufhem:/opt# sudo mkdir -p /opt/piper
root@ubuntufhem:/opt# sudo chown fhem:dialout /opt/piper
root@ubuntufhem:/opt# cd /opt/piper
root@ubuntufhem:/opt/piper# sudo -u fhem python3 -m venv venv
root@ubuntufhem:/opt/piper# sudo -u fhem ./venv/bin/pip install https://github.com/OHF-Voice/piper1-gpl/releases/download/v1.4.2/piper_tts-1.4.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl
Collecting piper-tts==1.4.2
Downloading https://github.com/OHF-Voice/piper1-gpl/releases/download/v1.4.2/piper_tts-1.4.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl (13.8 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 13.8/13.8 MB 10.2 MB/s eta 0:00:00
***Viele Packte die instalierte wurden***
root@ubuntufhem:/opt/piper# sudo ln -s /opt/piper/venv/bin/piper /usr/local/bin/piper
root@ubuntufhem:/opt/piper# piper --version
usage: piper [-h] -m MODEL [-c CONFIG] [-i INPUT_FILE] [-f OUTPUT_FILE] [-d OUTPUT_DIR]
[--output-dir-naming {timestamp,text}] [--output-raw] [-s SPEAKER] [--length-scale LENGTH_SCALE]
[--noise-scale NOISE_SCALE] [--noise-w-scale NOISE_W_SCALE] [--cuda]
[--sentence-silence SENTENCE_SILENCE] [--volume VOLUME] [--no-normalize] [--data-dir DATA_DIR] [--debug]
piper: error: the following arguments are required: -m/--model
root@ubuntufhem:/opt/piper# sudo -u fhem piper --model /opt/fhem/piper_models/de_DE-thorsten-medium.onnx --output_file /opt/fhem/www/audio/voice.wav <<< "Version eins Punkt vier Punkt zwei ist nun aktiv."
root@ubuntufhem:/opt/piper#Zitat von: Beta-User am 15 April 2026, 07:36:35Zitat von: schwatter am 14 April 2026, 21:52:10Bei dem JS im contrib verfolge ich mehr oder weniger auch den Weg, das es fast nichts kennen muss. Außer Wakeword und wo das Readings STT ist. Das JS für den Atom Echo muss aber irgendwo die Info herbekommen, das es das Browsermikro freischalten soll.
Mein gedanklicher Angang wäre, das auf zwei alternativen anderen Wegen zu lösen:
- Wenn man eine FULLY-Instanz hat, "kennt" man über das mit der deviceId in f18.js die aktuelle Verbindung (FW_ID), und kann darüber das Mikro aufmachen. Im Prinzip ist das der Weg, den momentan auch RHASSPY (für Dialoge und Rückfragen) verwendet, nur dass das dann direkt über die FHEMWEB-Instanz läuft und nicht über den Umweg "welche FHEMWEB-Instanz gehört derzeit zu einem bestimmten (fully-) Gerät".
- Der andere (noch nicht getestete) Weg ginge über die allowed-Info. Danach könnte allerdings ein allowed-User eben nur eine Verbindung (gleichzeitig) nutzen.
Zugegeben: für die letzte Variante braucht man ähnlich viel Hintergrundinfo, wie wenn man das direkt in den Code schreibt, auf welches wakeword zu reagieren ist...
Aber was ist wenn der User mal nicht Fully nutzt?
Und zu Gemini. Bis jetzt habe ich es nur als Backup für Fragen benutzt, die nichts mit Fhem zu tun haben.
Der @ahlermi hatte mir seinem systemPrompt geschickt.
Beispiel:
Du bist ein KI-Assistent und Teil meiner FHEM Haussteuerung. Deine Aufgaben sind:
## 1. Geräte steuern und Rückmeldung geben
- Für die Geräteidentifizierung hat der Alias priorität, danach guck über dem Comment.
- Führe Steuerbefehle für Geräte aus, wenn du dazu aufgefordert wirst.
- Gib immer eine kurze Bestätigung, was geschaltet wurde (maximal 1 Satz).
- Beispiele:
- "Rolllade Wohnzimmer wurde auf 0% geschlossen."
- "Heizung Bad auf 22°C gestellt."
### Gerätespezifische Regeln:
- **Rollladen:** Wenn das Gerät als Rolllade erkannt wird und den set-Parameter "pct" hat:
- 0 = ganz schließen, 100 = ganz öffnen.
- **Heizung:** Wenn das Gerät eine Heizung ist und "desiredTemperature" unterstützt, setze die Temperatur entsprechend.
- **Andere Befehle:** Bei Befehlen wie `motion_detection`:
- Syntax: `set <GERÄT> motion_detection true` (ohne Doppelpunkt).Das werde ich jetzt nach und nach testen.
Gruß schwatter
Zitat von: schwatter am 15 April 2026, 19:10:35ok, es ist ganz einfach wenn die richtige Repo benutzt wird ;D Danke für den Link.
Danke zurück für die bereinigte Fassung der Anleitung!
(Irgendwann sollten wir ggf. mal hergehen, und das ganze für "Nachbauer" strukturiert (im Wiki) zusammenfassen).
Zitat von: schwatter am 15 April 2026, 19:10:35Aber was ist wenn der User mal nicht Fully nutzt?
Na ja, es gibt für mich geanklich zwei völlig unterschiedliche Wege, die nicht wirklich was miteinander zu tun haben:
- Entweder es kommt fully/FULLY zum Einsatz. Dann ist es (eher*) ein gerätebezogener Kontext
- ODER die Sache läuft über FHEMWEB+allowed. Das wäre User-bezogen, so dass das ganze weder von einem bestimmten Browser noch von einem bestimmten Endgerät abhängt.
*Als "Krone" stelle ich mir eine Mischung von beidem vor. z.B. ein wakeword-ESP, der zwei unterschiedliche wakewords erkennt, eines für "rr_Frau", eines für "rr_Mann". Das wakeword-notify könnte dann via FULLY auf dem Endgerät entweder "fhem:8090" (user:"rr_Frau") oder "fhem:8091" (für den user "rr_Mann") laden und das Mikro öffnen. Das ganze dann noch mit passender Spracheinstellung, wenn rr_Frau z.B. gerne spanisch reden möchte?
Das neue Attribut sttProcessor für FULLY könnte man dann aufbohren, so dass für rr_Mann eine bestimmte RHASSPY-Instanz angesprochen wird, und für rr_Frau eine andere (oder Babble...)?
Zitat von: schwatter am 15 April 2026, 19:10:35@ahlermi hatte mir seinem systemPrompt geschickt.
Vermutlich sollte ich ihn mal per pm anpingen. Nach meinem Eindruck hat das ganze Potential, und es wäre schade, wenn man den Aufwand treibt, das Rad an manchen Stellen neu zu erfinden...
Einfaches Beispiel: Das mit "comment" finde ich sehr schwierig, weil ein allgemeines Attribut "mißbraucht" wird. Da ist es m.E. mittelfristig einfacher, spezielle Attribute einzuführen.
RHASSPY enthält ("geklaut" aus MQTT_GENERIC_BRIDGE) eine Grundstruktur, die man dafür m.E. mit ziemlich wenig Aufwand "recyclen" könnte.
Btw: es ist gepackaged, man muss also eigentlich nur an einigen wenigen Stellen (würde ohne commandref auf unter 20 Zeilen tippen!) was anpassen, um z.B: "gemini2" daraus zu machen ;) .
Nabend,
ich habe Piper in Text2Speech mit meinem Bluetooth-Lautsprecher funktionsfähig.
1. Lame
sudo apt-get install lame
2. Die angehängte 98_Text2Speech.pm nach Fhem
Folgende Attribute funktionieren oder wurden hinzugefügt.
attr TTS TTS_MplayerCall /usr/bin/mplayer -ao alsa:device=bluealsa -af resample=44100
attr TTS TTS_PiperModel /opt/piper/models/de_DE-thorsten-high.onnx
attr TTS TTS_Ressource Piper
attr TTS TTS_Speed -5
Schau mal rein. Für die Sprachausgabe ist Piper in der sub Text2Speech_Download($$$).
Damit nicht für jedes Wort eine MP3 erstellt wird, musste in der sub Text2Speech_PrepareSpeech($$)
der Text am Anfang noch vorbereitet werden. Bzw es werden Unterstriche hinzugefügt.
Gruß schwatter
Zitat von: schwatter am 16 April 2026, 22:12:44ich habe Piper in Text2Speech mit meinem Bluetooth-Lautsprecher funktionsfähig.
:) Cool!
Den Code schaue ich mir bei Gelegenheit an. Was mir nicht so richtig gefallen will: Man braucht nochmal eine Ladung Attribute...
Hat den Vorteil, dass man explizit erklären kann, wie es geht, im Prinzip sollten aber auch kleinere Änderungen bei https://svn.fhem.de/trac/changeset/29035/trunk/fhem/FHEM/98_Text2Speech.pm ausreichen. Man muss halt den piper-http-server spezifischen Call anpassen, das ist (leider) anders als bei maryTTS bzw. mimic3.
Hier der betreffende Änderungssatz:
https://svn.fhem.de/trac/changeset/29035/trunk/fhem/FHEM/98_Text2Speech.pm
Moin,
bei den Attributen habe ich nur TTS_PiperModel neu eingeführt. Das Model direkt per
Folder zu setzen finde ich sehr smart. Wenn man sich da aber auf kurz oder lang
festlegt, welches das Standardfolder ist, dann kann eine automatische Liste erzeugt
werden.
Die anderen Attribute sind schon im Modul vorhanden.
Gruß schwatter
Soweit ich das verstanden habe, sollte man Piper nicht - wie jetzt in deiner Modulfassung vorgesehen - per Kommandozeile aufrufen, sondern für schnelle Antworten als http-server laufen lassen.
(OT: der Weg über Text2speech kommt mir trotzdem wie ein unnötiger und komplizierter Umweg vor).
Ok,
jetzt verstehe ich, auf was du hinaus willst.
Gruß schwatter
Zitat von: schwatter am 17 April 2026, 12:52:59jetzt verstehe ich, auf was du hinaus willst.
:)
So langsam ist auch mir klar, wie die Teile in etwa zusammenspielen sollten, sorry, wenn das hier noch nicht so deutlich rüberkam:
Zitat von: Beta-User am 14 April 2026, 07:42:32Via https://curlconverter.com/javascript/ (https://curlconverter.com/javascript/) bin ich dann auf diesen Testcode gekommen:
Es geht also letztlich darum, auf der FHEM-Seite js-Code zu generieren, den man dann in einer bestimmten Instanz (derzeit) einer FHEMWEB-Verbindung browserseitig ausführen läßt. Der Code an sich soll sich
- selbständig bei einem (mehr oder weniger beliebigen) Server die Audio-File "on the fly" generieren lassen,
- diese dann abspielen, und
- am Ende ein "bin fertig mit deiner TTS-Anweisung" an FHEM zurückmelden...
Der Testcode war erst mal dazu gedacht rauszufinden, wie der Code überhaupt grundsätzlich aussehen muss :) .
piper-tts (imo: vorrangig als http-Aufruf) in Text2Speech.pm einzubauen ist trotzdem kein Fehler. Ich würde das eben nur als "generischen" http-request coden wollen, um künftig einfacher Variablen ändern zu können, falls jemand wieder einen neuen (nicht maryTTS-kompatiblen) Servertyp ins Spiel bringt... (Mal sehen, das scheint ja doch grundsätzlich schon einigermaßen flexibel gewesen zu sein, was ich mir damals ausgedacht hatte).
Moin,
nachdem ich erst bei der alten Project gelandet war und du mir dann den Link zur
neuen Repo gezeigt hättest, muss ich nochmal von vorne anfangen. ::)
Problem, ich hatte einfach die Wheeldatei installiert. Auf dieser Basis dann die
Text2Speech angepasst. Aber in der Wheeldatei war nur die Bin. Kein Httpserver
auf Basis von Python. Männer halt, wenn sie ohne Bedienungsanleitung arbeiten.
;D
edit:
Und schon wieder liege ich halb falsch. Daher ignoriere mich einfach... Wenn ich per Wheel installiere
ist der HTTP_server da. Aber z.B flask fehlt. Daher gilt auch wieterhin. Um alle Pakete zu bekommen,
installieren wie in der Repo beschrieben.
root@ubuntufhem:/opt/piper/venv# find /opt/piper/venv -name "http_server.py"
/opt/piper/venv/lib/python3.12/site-packages/piper/http_server.py
root@ubuntufhem:/opt/piper/venv#
edit2:
Neuer sauberer Weg
# --- PIPER INSTALLATION ---
# 1. Verzeichnis vorbereiten
mkdir -p /opt/piper
chown fhem:dialout /opt/piper
# 2. Venv erstellen und installieren (als user fhem)
sudo -u fhem python3 -m venv /opt/piper/venv
sudo -u fhem /opt/piper/venv/bin/pip install --upgrade pip
sudo -u fhem /opt/piper/venv/bin/pip install piper-tts
sudo -u fhem /opt/piper/venv/bin/python3 -m piper.download_voices -m de_DE-pavoque-low
# 3. Service Datei erstellen (als root)
cat <<EOF > /etc/systemd/system/piper.service
[Unit]
Description=Piper TTS Server
After=network.target
[Service]
Type=simple
User=fhem
WorkingDirectory=/opt/piper
ExecStart=/opt/piper/venv/bin/python3 -m piper.http_server -m de_DE-pavoque-low --port 5000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# 4. Service aktivieren und starten
systemctl daemon-reload
systemctl enable piper
systemctl start piper
systemctl status piper
Gruß schwatter
Zitat von: schwatter am 18 April 2026, 07:15:12Daher ignoriere mich einfach...
Ungern ;D .
Brauchst du denn jetzt noch eine Anleitung oder nicht? Ich hatte neulich auf Basis meiner hier geposteten Notizen auf einem Testsystem die Installation nochmal gemacht, und das hat da ohne größere Umstände funktioniert. Dabei habe ich die Installation erst mit meinem Standarduser gemacht, und dann danach die Rechte auf /opt/piper auf fhem:dialout geändert und in der systemd-unit-file fhem als User angegeben.
Zwischenzeitlich habe ich etwas mit dem Code von Text2Speech gespielt, leider nur mit Teilerfolgen:
define t2s Text2Speech none
attr t2s TTS_Ressource maryTTS
attr t2s TTS_User host='http://127.0.0.1:5000/' method=POST header='{ "Content-Type" => "application/json" }' body='{ "text": "$text" }'
attr t2s verbose 5
Der Code-Abschnitt im Modul (komplette Fassung anbei):
} elsif ( $TTS_Ressource eq 'maryTTS' ) {
my $mTTSurl = $TTS_User;
my($unnamed, $named) = parseParams($mTTSurl);
$named->{host} //= shift @{$unnamed} // '127.0.0.1';
$named->{port} //= shift @{$unnamed} // '59125';
$named->{lang} //= shift @{$unnamed} // !$TTS_Language || $TTS_Language eq 'Deutsch' ? 'de_DE' : $TTS_Language;
$named->{voice} //= shift @{$unnamed} // 'de_DE/thorsten_low';
$named->{endpoint} //= shift @{$unnamed} // 'process';
#$named->{method} //= shift @{$unnamed} // 'GET';
if (defined $named->{method} && $named->{method} eq 'POST') {
$mTTSurl = $named->{host};
} else {
$mTTSurl = "http://$named->{host}:$named->{port}/$named->{endpoint}?INPUT_TYPE=TEXT&OUTPUT_TYPE=AUDIO&AUDIO=WAVE_FILE&LOCALE=$named->{lang}&VOICE=$named->{voice}&INPUT_TEXT="; # https://github.com/marytts/marytts-txt2wav/blob/python/txt2wav.py#L21
$mTTSurl .= uri_escape($text);
}
Log3( $hash->{NAME}, 4, "$hash->{NAME}: Hole URL: $mTTSurl" );
my $param = { url => $mTTSurl,
timeout => 5,
hash => $hash, # Muss gesetzt werden, damit die Callback funktion wieder $hash hat
method => $named->{method} // 'GET' # POST can be found in https://github.com/marytts/marytts-txt2wav/blob/python/txt2wav.py#L33
};
$param->{header} = $named->{header} if defined $named->{header};
if (defined $named->{body}) {
$named->{body} =~ s{(\$\w+)}{$1}eegx;
$param->{body} = $named->{body};
Log3($hash->{NAME}, 3, "$hash->{NAME} body: $param->{body}, header: $param->{header}");
}
my ($maryTTSResponseErr, $maryTTSResponse) = HttpUtils_BlockingGet($param);
if(length($maryTTSResponseErr) > 0) {
Log3($hash->{NAME}, 3, "$hash->{NAME}: Fehler beim Abrufen der Daten von $TTS_Ressource: $maryTTSResponseErr");
return;
}
my $FileWav2 = $file . '.wav';
my $fh2 = new IO::File ">$FileWav2";
if ( !defined $FileWav2 ) {
Log3($hash->{NAME}, 2, "$hash->{NAME}: wav Datei <$FileWav2> konnte nicht angelegt werden.");
return;
}
$fh2->print($maryTTSResponse);
Log3($hash->{NAME}, 4, "$hash->{NAME}: Schreibe wav in die Datei $FileWav2, Bytes: ".length $maryTTSResponse);
close $fh2;
$cmd = qq(lame "$FileWav2" "$file");
Log3($hash, 4, "$hash->{NAME}:$cmd");
system $cmd;
return unlink $FileWav2;
}Irgendeine Kleinigkeit paßt noch nicht, vermutlich im Header (ich habe einige Versionen durch, muss jetzt aber auch mal was anderes machen)? Jedenfalls will HttpUtils_BlockingGet() eigentlich einen Endpunkt haben, und via curl bekomme ich auch mit dem ersten slash eine wav:
curl -X POST -H 'Content-Type: application/json' -d '{ "text": "thorsten sagt jetzt mal wieder etwas an" }' -o test.wav localhost:5000/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 84577 100 84524 100 53 228k 146 --:--:-- --:--:-- --:--:-- 228k
oder
curl -X POST -H 'Content-Type: application/json' -d '{ "text": "thorsten sagt wieder etwas an" }' -o test.wav http://localhost:5000/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 69719 100 69676 100 43 317k 200 --:--:-- --:--:-- --:--:-- 318k
Jetzt habt Ihr mich irgendwo abgehängt.
Was ist denn jetzt das Ziel - STT oder TTS?
Für STT benutze ich immer noch meine Android-Tablets mit Automagic - das kann ich dank Google Backup auch auf neuen Geräten installieren. Auf den Tablets läuft aber auch überall Fully.
Für TTS baue ich entweder in FHEM mit Hilfe von Amazon Polly neue MP3s zusammen, sende diese per Automagic an die Tablets (oder andere Geräte) und spiele sie ab. Oder nutze die TTS-Fähigkeit der Tablets selbst, weil ich mir auch hier die inzwischen > 10 Jahre alte Sprachbibliothek gesichert habe, die Amazon Polly verwendet. Also sprechen alle meine Devices mit derselben Stimme, das hat sich als wichtig herausgestellt.
Einzig unzufrieden bin ich mit der Wakeword detection, die über RHASSPY läuft.
Vielleicht könnte einer von Euch mal (gerne auf Papier) eine Skizze produzieren, welche Komponenten und Abläufe jetzt hier diskutiert werden.
LG
pah
Zitat von: Prof. Dr. Peter Henning am 18 April 2026, 11:57:17Was ist denn jetzt das Ziel - STT oder TTS?
Ich komme von:
Zitat von: Beta-User am 03 April 2026, 11:24:46Mein Ausgangspunkt war eigentlich, das FULLY-Modul so zu erweitern, das es mit RHASSPY in Punkto Sprachein- und -ausgabe so zusammenarbeitet, wie das vorher mit AMAD möglich gewesen war, insbesondere also interaktive Dialoge möglich sind. Wie es im Moment aussieht, braucht es dafür aber auch einiges an javascript, so dass es letztlich (fast) völlig egal ist, welchen Browser man verwendet...
Stand JETZT funktioniert das insoweit, dass die TTS- und STT-Fähigkeiten des Handys wie beschrieben genutzt werden können, wenn man als Browser Chrome verwendet.
Letztlich ist meine Vision, (aus User-Sicht betrachtet) funktional (im Moment: bis auf die "lokale" STT-Fähigkeit) einen vollwertigen Rhasspy-Satelliten zu basteln, wobei technisch am Ende* vom Rhasspy-Framework letztlich nur die intent-Erkennung und Teile des session-Managements weiter genutzt werden, den Rest erledigt RHASSPY.
Was ich letztlich loswerden will: Sämtliche Abhängigkeiten von veralteten "Bauteilen" (wie automagic oder Polly-TTS-Engine), bei denen man nie weiß, wie lange es funktioniert. Ganz am Ende* ist auch das Rhasspy-Framework in diesem Setup ohne weiteres gegen eine andere Intent-Erkennung austauschbar, mir schwebt was auf rivescript-Basis vor, ohne dass das irgendwie schon konkretere Formen angenommen hätte.
Was dazu als nächstes ansteht, wäre die Option, einen mehr oder weniger beliebigen (eigenen und lokal laufenden) TTS-Server mit der Generierung der Audios zu beaufschlagen, das ganze möglichst per direktem js-Aufruf. Da mir das aber im Moment zu viele Zwischenschritte sind, versuche ich das mit piper+Text2Speech (notgedrungen halt incl. dem Umweg über lame, damit es "modulkonform" ist).
Ist das Bild jetzt auch ohne Zeichnung klarer?
OT:
Zitat von: Prof. Dr. Peter Henning am 18 April 2026, 11:57:17Einzig unzufrieden bin ich mit der Wakeword detection, die über RHASSPY läuft.
RHASSPY ist bzgl. Wakeword nur ein "Makler-Device", die Unzufriendenheit müßte sich demnach auf einen Rhasspy-Satelliten beziehen?
Was das Thema angeht, bin ich nach wie vor eher Nutzer klassischer Buttons am Handy, von daher habe ich eher noch Wünsche in Richtung der Bedienbarkeit/Konfigurierbarkeit der neuen f18.js-Funktionen.
Zitat von: Beta-User am 18 April 2026, 12:19:12Ist das Bild jetzt auch ohne Zeichnung klarer?
So halb. Meine Rivescript-Dateien steuere ich gerne bei, Rivescript ist bei mir ja dem Babble-Modul nachgeschaltet.
Natürlich wird das Wakeword nicht von Rhasspy selbst erkannt, sondern ich nutze darauf MycroftAI Precise, selbst trainiert. Das ist allerdings jetzt auch schon ein paar Jahre alt und wird nicht weiter entwickelt. Darum habe ich ja auch nach dem Atom Echo gefragt.
Ich habe gerade ein Buchmanuskript fertig, und darum hoffentlich etwas mehr Zeit, damit kann ich dann also mal spielen.
LG
pah
Zitat von: Prof. Dr. Peter Henning am 18 April 2026, 14:06:22hoffentlich etwas mehr Zeit
Dann mal willkommen an Bord!
ZitatSo halb.
Nun ja, das hier ist "work in progress", bei mir ist es auch so, dass da auch zunächst eine eher vage Vorstellung davon da war, dass das mit dem "voicebutton" als Erbe von "FHEM-WebViewControl" (wie im Wiki noch dargestellt) doch heutzutage keine große Sache sein sollte und man damit einen einfachen RHASSPY-Satelliten generieren kann.
Alles andere kristiallisiert sich erst mir der Zeit raus, leider teils mit Irrwegen und Missverständnissen. Jetzt ist vieles zum Glück schon funktional, aber vermutlich wird es weiter so sein, dass sich die Vorgehensweise an die Kenntnis dessen anpassen wird, was denn möglich ist...
ZitatMeine Rivescript-Dateien steuere ich gerne bei, Rivescript ist bei mir ja dem Babble-Modul nachgeschaltet.
Habe vor einiger Zeit mal ein paar der Threads zu rivescript durchgesehen, aber im Moment nicht viel mehr wie die vage Vorstellung, dass man RHASSPY damit eigentlich relativ leicht dahin umbauen können sollte, dass es mit einer "sentences.ini" (mehr oder weniger mit der von Rhasspy her bekannten Syntax) rivescript aufrufen kann, um den "intent-json" zu generieren, dabei intents zu (de-) aktivieren, "slots" füllen usw..
Babble habe ich mir auch kurz angeschaut, aber irgendwie kam mir das nicht wie ein adäquater Ersatz für RHASSPY vor. Mag sein, dass ich mich da täusche. Jedenfalls: FULLY kann man ohne großes Aufhebens so konfigurieren, dass es Babble_DoIt() mit dem erkannten Text aufruft (wenn fully als Browser verwendet wird). Um den Rückweg für die response muss man sich dann natürlich selber kümmern :) .
Bei mir sieht das so aus
A: Nur Sprache
- Wakeword-Engine detektiert Wakeword
- Liefert Event an FHEM
- FHEM fordert beim AMAD-Device "activateVoiceInput" an
- Tablet zeigt Sprachaufforderung
- Sprache wird vom Tablet an Google geschickt, kommt als Text zurück
- Text wird vom AMAD-Device zur Auswertung an Babble gesendet
B: Spracheingabebutton in FTUI3
- Button-Event wird vom Tablet an FHEM gesendet
- FHEM fordert beim AMAD-Device "activateVoiceInput" an (6 verschiedene Tablets)
- Tablet zeigt Sprachaufforderung
- Sprache wird vom Tablet an Google geschickt, kommt als Text zurück
- Text wird vom AMAD-Device zur Auswertung an Babble gesendet
Das Problem ist jetzt erstens, dass je nach Auslastung der Tablets durch FTUI3-Aufgaben durchaus 1-2 Sekunden vergehen können, bis die Spracheingabe aufgeht.
Und zweitens, dass das dumme Automagic zusammen mit Google nicht in der Lage ist, nach einem Timeout (fehlende Spracheingabe ...) sauber abzubrechen. Sondern dann ab und zu ein Fehlerfenster auf dem Bildschirm des Tablets stehen bleibt, das man auch durch ein AMAD-Skript nicht wegbekommt.
LG
pah
Zitat von: Prof. Dr. Peter Henning am 18 April 2026, 17:07:45- FHEM fordert beim AMAD-Device "activateVoiceInput" an
[...]
- Text wird vom AMAD-Device zur Auswertung an Babble gesendet
Für den ersten Schritt "activateVoiceInput" kennen wir - immer vorausgesetzt, die Verbindung zum Browser auf dem Endgerät (nicht beschränkt auf Android!) steht - zwischenzeitlich drei Alternativen, RHASSPY verwendet (allerdings derzeit nur zur Fortsetzung der Dialoge) den js-Aufruf von f18_stt() via devspec.
my $aVICommand = $hash->{helper}->{SpeechDialog}->{config}->{$device}->{activateVoiceInput};
3053 $aVICommand //= 'set $DEVICE activateVoiceInput' if InternalVal($device, 'TYPE', '') eq 'AMADDevice';
3054 $aVICommand //= q({ my $js = "if((document.querySelector('input[name=\"fw_id\"]')||{}).value==='$hash->{FW_ID}'){f18_stt()}"; FW_directNotify("#FHEMWEB:$_", $js, "")
3055 for devspec2array("TYPE=FHEMWEB"); }) if InternalVal($device, 'TYPE', '') eq 'FULLY';
3056 $aVICommand //= q({ FW_AsyncOutput($defs{$DEVICE},'',qq/["#FHEMWEB:","f18_stt()",""]/)}) if InternalVal($device, 'TYPE', '') eq 'FHEMWEB';
Den Text greift dann entweder das FULLY-Device ab, oder RHASSPY per NotifyFn() auf FHEMWEB-Events; da muss ich noch eventuelle Doppelungen rausoperieren...
Zitat von: Beta-User am 18 April 2026, 11:32:41Zitat von: schwatter am 18 April 2026, 07:15:12Daher ignoriere mich einfach...
Ungern ;D .
Brauchst du denn jetzt noch eine Anleitung oder nicht? Ich hatte neulich auf Basis meiner hier geposteten Notizen auf einem Testsystem die Installation nochmal gemacht, und das hat da ohne größere Umstände funktioniert. Dabei habe ich die Installation erst mit meinem Standarduser gemacht, und dann danach die Rechte auf /opt/piper auf fhem:dialout geändert und in der systemd-unit-file fhem als User angegeben.
Danke erstmal nicht. Komme zu einem späteren Zeitpunkt darauf zurück. Mir schwirrt der Kopf. Gerade weil soviel
Nachholbedarf besteht und viele Sachen sich gerade vermischen.
Da kommt schon wieder die nächste Idee. Ich habe gerade neben Rudi seinem STT Button noch einen Audiobutton eingebaut.
Damit konnte ich erfolgreich eine MP3 abspielen. Im Modul TTS kann auch der Pfad in einem Adressraum geändert werden,
den der Browser versteht.
Ziel: Button 1 x drücken, JS im f18 lauscht dauerhaft nach neuen TTS-Events und spielt direkt immer die aktuelle MP3 ab.
Hier kommt dann das neue attr additionalInform TTS ins Spiel.
Gruß schwatter
Welche STT-Engine wird denn in f18_stt() benutzt? Versteh ich noch nicht so ganz.
LG
pah
Zitat von: Prof. Dr. Peter Henning am 20 April 2026, 05:37:44Welche STT-Engine wird denn in f18_stt() benutzt? Versteh ich noch nicht so ganz.
Nach meinem Verständnis: der auf dem Endgerät eingestellte Default.
Vermutlich also in der Regel bei Android Google.
Nachtrag: schwatter hat das für Firefox auch mal auf einen lokalen, eigenen Server konfiguriert.
Nabend,
ja genau, die Google STT Engine. Der Platzhirsch leider. Sonst sieht es mau aus. Aber super für
kleine Leistungsschwache Server wie mein Intel(R) Celeron(R) J4105 CPU.
Ich hatte mal mit Whisper experimentiert. Da war damals eine dauerhafte Verbindung notwendig.
Problem war A) das fehlende Wakeword und B) mein Celeron zu schwach. Reagiert hat es zischen 10 bis 20sek.
Aber funktional war es.
Gruß schwatter
Für mich ist die entscheidende Frage, wie f18_stt bei einer fehlenden Spracheingabe reagiert - kommt man da wieder raus?. Ich habe es noch nicht ausprobieren können.
Der Witz bei Rivescript ist ja, dass man da komplexe Dialoge führen kann. Etwa
-> spiele Musik
<- wo und von wem soll ich musik spielen
-> im wohnzimmer
<- von wem soll ich im wohnzimmer musik spielen
-> von mozart
<- ok, ich werde im wohnzimmer musik von mozart spielen aus dem album die zauberflöte
Das habe ich an verschiedenen Stellen in meiner Rivescript-Umgebung realisiert. Schwachpunkt ist immer, dass man bei einem activateVoiceInput über AMAD innerhalb sehr kurzer Zeit etwas sagen muss, sonst bleibt die Spracheingabe mit einem Fehlerfenster hängen.
LG
pah
Zitat von: Prof. Dr. Peter Henning am 21 April 2026, 05:16:13Für mich ist die entscheidende Frage, wie f18_stt bei einer fehlenden Spracheingabe reagiert - kommt man da wieder raus?
Derzeit (!) zeigt f18_stt() "einfach" ein Dialogfeld, das auch eine "abbrechen"-Schalftfläche hat. Nicht schön anzusehen, keine angenehme user-experience, wenn man einfach so was sagen will und erwartet, dass das dann auch passiert, ohne nochmal einen Klick zu machen, ABER: Mir wäre nicht bekannt, dass es irgendeine zeitliche Grenze gäbe, innerhalb derer man was gesagt haben müßte, und noch weniger, dass der js-Code irgendwann nicht mehr auf die "abbrechen"-Schaltfläche reagieren würde.
Zugegebenermaßen: Ausgetestet habe ich das nicht...
Der Punkt ist:
Wir haben es bei dem Browser/js-basierten Ansatz in der Hand, WAS genau passiert, und wenn es nicht funktioniert, können wir das ändern, für f18_stt() hat Rudi nach meinem Verständnis ausdrücklich offen gelassen, wie wir das weiterentwickeln wollen. Bei AMAD läuft im Hintergrund irgendeine App (automagic oder eben tasker, falls man das ans Laufen bekommt...), von der man eben nicht genau weiß, was sie wie tut, und welche Funktionen aus dem OS sie benötigt. Daher auch meine Aussage an anderer Stelle, dass "automagic" als Option für AMAD letztlich "tot" ist.
Zitat von: Prof. Dr. Peter Henning am 21 April 2026, 05:16:13Der Witz bei Rivescript ist ja, dass man da komplexe Dialoge führen kann. Etwa [...]
Der Witz bei RHASSPY ist, dass man genau diese Art Dialog ebenfalls führen kann (das wäre ein CustomIntent mit Dialog (https://forum.fhem.de/index.php?topic=139337.0)), und genau dafür steht das "interaktiv" im Thread-Titel.
Und ja: Das funktioniert derzeit bereits mit f18_stt() (i.V.m. dem "mini-patch", der das Ende der jeweiligen Sprachausgabe als Event wirft) - nur ist die user-Experience noch nicht so, wie ich das gerne hätte ;) .
@Rudi - falls du hier mitliest: Scheinbar hatte ich bisher noch nicht meine modifizierte f18_speak() mit dem "bin fertig"-Event gezeigt.
Hier die Roh-Fassung:
function
f18_speak(txt)
{
let synth = window.speechSynthesis;
if(!synth)
return FW_okDialog("No speechSynthesis available");
const utterance = new SpeechSynthesisUtterance(txt);
// Good practice: Set listeners even if they're unreliable on all voices
utterance.onend = () => {
var fw_id = $("body").attr("fw_id");
FW_cmd(`${FW_root}?cmd=setreading `+
`TYPE=FHEMWEB:FILTER=FW_ID=${fw_id}:FILTER=inform=.%2B `+
`TTS_state finished&XHR=1`);
};
synth.speak(utterance);
//speechSynthesis.speak(new SpeechSynthesisUtterance(txt));
//see https://iifx.dev/en/articles/457363230/chrome-tts-workarounds-solving-the-speechsynthesisutterance-event-and-initial-speak-failure for more info
}@schwatter: Ich würde mir sowas auch für die "Text2Speech-Automatik" aus https://forum.fhem.de/index.php?msg=1362354 wünschen. Den zugehörigen Code sehe ich mir bei Gelegenheit an (ein angehängtes diff wäre evtl. hilfreich, um sich direkt in die Materie reinzudenken), und habe auch noch nicht verstanden, warum man das nicht per notify aus FHEM heraus macht.
Klingt schon mal halb gut. Es müsste auch je eine Möglichkeit bestehen, den Abbruch der Spracheingabe a.)von FHEM aus durchzuführen und b.)ein Timeout mitzugeben.
Ich würde außerdem präferieren, wenn man den gesamten Sprachkram in eine eigene Javascript-Datei auslagert. Das liegt daran, dass ich (ebenso wie viele andere) eigene Styles verwende. In meinem Fall sind die hochgradig parametriert - Farben und Abstände beispielsweise sind nur an genau einer Stelle definiert und werden dann an anderen Stellen verwendet.
LG
pah
Zitat von: Prof. Dr. Peter Henning am 21 April 2026, 09:11:24Klingt schon mal halb gut.
Imo ist es bis auf Kleinigkeiten schon derzeit besser als die bisherige AMAD-Option.
ZitatEs müsste auch je eine Möglichkeit bestehen, den Abbruch der Spracheingabe a.)von FHEM aus durchzuführen und b.)ein Timeout mitzugeben.
Ich würde das ergänzen mit: Die Sprache zu bestimmen, die erkannt werden soll. Wobei, hmm, s.u..
Zitat von: Prof. Dr. Peter Henning am 21 April 2026, 09:11:24Ich würde außerdem präferieren, wenn man den gesamten Sprachkram in eine eigene Javascript-Datei auslagert.
Das war auch mein erster Gedanke, ABER:
1. Für Einsteiger in die Materie finde ich einen einfach zugänglichen, standardisierten Weg hilfreich. Das Feld ist insgesamt so oder so ziemlich unübersichtlich, und Dinge nach und nach anzupassen, wenn man die Grundfunktionalität mal verstanden hat, finde ich persönlich den leichter vermittelbaren Weg. Kann natürlich sein, dass der erfahrene Didaktiker das anders sieht.
2. Die eigentlichen Funktionen, die man braucht, kann man ja auch parallel in seperaten .js anbieten bzw. das kann der fortgeschrittene User dann ggf. auch selbst nach Gusto zusammenstellen. Er kann dann z.B. auch die Funktion doppeln, und für die französischsprachige "Sie" beim passenden wakeword auch einfach die "doppelte" stt-Funktion in der passenden Sprache aufrufen (ich unterstelle mal, dass sowas realisierbar sein müßte), und dann das (andere) Event eben selbst abgreifen.
(OT-Nachtrag noch: Meine eigene "Ziel-UI" ist eigentlich auch nicht f18 pur, sondern FHEMapp... Da muss auch noch ein Weg her, ein passendes Overlay zu generieren und das Ganze funktional mit FHEM zu verbinden...)
Dementsprechend plädiere ich jedenfalls für den Moment dafür, die Funktionen weiter in f18 zu belassen und lieber über die Standards (oder besser vielleicht: Empfehlungen) zu sprechen, die man als "Kopierer" einhalten sollte, also wann man wo man welche Events generiert usw..
Zitat von: Beta-User am 21 April 2026, 07:23:50@schwatter: Ich würde mir sowas auch für die "Text2Speech-Automatik" aus https://forum.fhem.de/index.php?msg=1362354 wünschen. Den zugehörigen Code sehe ich mir bei Gelegenheit an (ein angehängtes diff wäre evtl. hilfreich, um sich direkt in die Materie reinzudenken), und habe auch noch nicht verstanden, warum man das nicht per notify aus FHEM heraus macht.
Nabend,
mein Server steht im Keller und an dem ist mit bluealsa ein Bluetoothspeaker gepairt. Was ist aber, wenn nur das Tablet mit
Browser da ist. Damit kann Audio abgespielt werden, ohne das mir das Audiodevice bekannt ist.
Oder ist dir ein Weg mit notify bekannt, mit dem die direkt in Fhem Audio abspielen kannst, ohne das du ein Audiodevice
definiert hast?
--- /tmp/f18.js 2026-04-25 18:34:22.000000000 +0200
+++ /opt/fhem/www/pgm2/f18.js 2026-04-23 12:57:49.000000000 +0200
@@ -1,5 +1,5 @@
"use strict";
-FW_version["f18.js"] = "$Id$";
+FW_version["f18.js"] = "$Id: f18.js 31043 2026-03-28 19:41:39Z rudolfkoenig $";
// TODO: hierMenu+Pin,SVGcolors,floorplan
// Known bugs: AbsSize is wrong for ColorSlider
@@ -18,7 +18,9 @@
menu: "111111", sel: "333333", inpBack:"444444" }
};
var f18_isday;
+window.f18_playMedia_active = false;
+$(window).on('load', function() {$('body').css('opacity', 1);});
$(window).resize(f18_resize);
$(document).ready(function(){
f18_room = $("div#content").attr("room");
@@ -115,11 +117,16 @@
$("<div id='micro'></div>").prependTo("div#menuScrollArea")
.css( {"background-image":"url('"+f18_icon.micro+"')", "cursor":"pointer" })
.click(f18_stt);
+
+ // Innerhalb von f18_menu()
+ $("<div id='speaker'></div>").prependTo("div#menuScrollArea")
+ .css( {"background-image":"url('"+f18_icon.speaker+"')", "cursor":"pointer" })
+ .click(f18_playMedia);
$("div#menu").prepend("<div></div>");
f18_addPin("div#menu > div:first", "menu", true, fixMenu, f18_small);
setTimeout(function(){
- $("#menu,#content,#logo,#hdr,#menuBtn,#textInput,#micro")
+ $("#menu,#content,#logo,#hdr,#menuBtn,#textInput,#micro,#speaker")
.addClass("animated"); }, 10);
function
fixMenu()
@@ -130,6 +137,8 @@
f18_getAttr("hideMenu") ? "none" : "block");
$("#micro").css("display",
f18_getAttr("showMicro") ? "block" : "none");
+ $("#speaker").css("display",
+ f18_getAttr("showSpeaker") ? "block" : "none");
if(f18_getAttr("Pinned.menu")) {
$("body").addClass("pinnedMenu");
$("#menu").removeClass("hidden");
@@ -152,25 +161,17 @@
var stt = new SpeechRecognition();
stt.continuous = true;
- stt.interimResults = true;
stt.lang = $("body").attr("data-language") == "EN" ? "en-US":"de-DE";
var doSend = false;
- var txt='';
+ var txt;
stt.onresult = function(e){
- var interim_txt='';
- for(let r of e.results) {
- if(r.isFinal) {
- txt = r[0].transcript;
- $("#f18_stt").html(txt);
- } else {
- interim_txt += r[0].transcript;
- $("#f18_stt").html(interim_txt);
- }
- }
+ txt='';
+ for(var i1=0; i1<event.results.length; i1++)
+ txt += event.results[i1][0].transcript;
+ $("#f18_stt").html(txt);
};
-
stt.onaudiostart = function(e){ $("#stt_state").html("Audio started") };
stt.onaudioend = function(e){ $("#stt_state").html("Audio stopped") };
stt.onspeechstart= function(e){ $("#stt_state").html("Speech started") };
@@ -181,10 +182,8 @@
$("#FW_okDialog").remove();
var div = $("<div id='FW_okDialog'>");
- $(div).html(`<div id="stt_avl"></div>
- <div id="stt_state"></div>
- <div id="f18_stt" style="min-height:200px;min-width:200px">
- </div>`);
+ $(div).html('<div id="stt_state"></div><div id="f18_stt" '+
+ 'style="min-height:200px;min-width:200px"></div>');
$("body").append(div);
var oldPos = $("body").scrollTop();
$(div).dialog({
@@ -196,31 +195,58 @@
],
close:function(){
if(doSend && txt) {
- var fw_id = $("body").attr("fw_id");
- if(typeof fully !== 'undefined')
+ if(typeof fully !== 'undefined')
FW_cmd(FW_root + "?cmd=set TYPE=FULLY:FILTER=deviceid=" +
fully.getDeviceId()+" STTinput "+encodeURIComponent(txt)+
- " ["+fw_id+"]&XHR=1");
+ " ["+$("body").attr("fw_id")+"]&XHR=1");
- FW_cmd(`${FW_root}?cmd=setreading `+
- `TYPE=FHEMWEB:FILTER=FW_ID=${fw_id}:FILTER=inform=.%2B `+
- `STT ${encodeURIComponent(txt)}&XHR=1`);
+ FW_cmd(FW_root+"?cmd=setreading "+f18_webName+
+ " STT "+encodeURIComponent(txt)+"&XHR=1");
}
stt.stop();
$(div).remove();
}
});
-}
-function
-f18_speak(txt)
-{
- let synth = window.speechSynthesis;
- if(!synth)
- return FW_okDialog("No speechSynthesis available");
- speechSynthesis.speak(new SpeechSynthesisUtterance(txt));
}
+(function() {
+ const _old = FW_doUpdate;
+ FW_doUpdate = (evt) => {
+ _old(evt);
+
+ if (evt?.data && evt.data.includes("-lastFilename")) {
+ try {
+ const match = evt.data.match(/[^"]+-lastFilename","([^"]+)"/);
+ const path = match ? match[1] : null;
+
+ if (window.f18_playMedia_active && path) {
+ _f18_play(path);
+ }
+ } catch (e) {
+ console.error("f18-Media-Fehler:", e);
+ }
+ }
+ };
+})();
+
+const _f18_play = (path) => {
+ const url = window.location.origin + path.replace("/opt/fhem/www", "/fhem").replace(/^\.\//, "/fhem/");
+ new Audio(url).play().catch(console.error);
+};
+
+function f18_playMedia() {
+ window.f18_playMedia_active = !window.f18_playMedia_active;
+ const s = document.getElementById("speaker");
+
+ if (s) {
+ s.style.filter = window.f18_playMedia_active
+ ? "invert(36%) sepia(85%) saturate(760%) hue-rotate(85deg) brightness(90%) contrast(105%)"
+ : "none";
+ }
+
+ if (window.f18_playMedia_active) new Audio().play().catch(() => {});
+}
function
f18_serviceWorkerRegister(verbose)
@@ -566,6 +592,7 @@
$("div.pinHeader div.pin").css("display", c ? "none":"block");
});
addHider("showMicro", true, "STT", f18_menu);
+ addHider("showSpeaker", true, "Speaker", f18_menu);
addHider("fixedInput", false, "Fixed input and menu", f18_setFixedInput);
addHider("wrapcolumns",false,"Wrap columns<br>on small screen",
f18_setWrapColumns);
@@ -633,18 +660,20 @@
pm = f18_getAttr("Pinned.menu") || hm,
rm = (f18_getAttr("rightMenu") && f18_small),
sm = f18_getAttr("showMicro"),
+ sp = f18_getAttr("showSpeaker"),
hti = f18_getAttr("hideTextInput");
var left = 0;
left += hl ? 0 : 40;
- left += sm ? 28 : 0;
+ if (sm && sp) left += 56;
+ else if (sm || sp) left += 28;
left += pm ? 0 : 44;
left += hti ? 0 : 40;
var lleft = (pm || hl ? 10 : 52);
$("input.maininput").css({ width:(w-left-(FW_isiOS ? 36 : 24))+'px',
"margin-left":(rm ? "0px" : "10px"),
display: hi ? "none":"block"});
- $("#menu,#content").css("top", (hi && pm && hl && hti && !sm) ? "10px" : "50px");
+ $("#menu,#content").css("top", (hi && pm && hl && hti && !sm && !sp) ? "10px" : "50px");
$("#hdr").css({ left:(rm ? 10 : left)+'px' });
$("#textInput").css({ left: (rm ? "auto":(left-32)+"px"),
right:(rm ? (lleft+32)+"px":"auto"),
@@ -652,8 +681,8 @@
$("#menuBtn").toggle(!pm || f18_small);
$("#menuBtn").css({ left:rm ? "auto":"10px", right:rm ? "10px":"auto" });
$("#logo") .css({ left:rm ? "auto":lleft , right:rm ? "48px":"auto" });
- $("#micro") .css({ left:rm ? "auto":(lleft+32)+"px",
- right:rm ? (lleft+64)+"px":"auto" });
+ $("#micro").css({left:rm?"auto":(lleft+32)+"px",right:rm?(lleft+64)+"px":"auto",display:sm?"block":"none"});
+ $("#speaker").css({left:rm?"auto":(sm?(lleft+64):(lleft+32))+"px",right:rm?(sm?(lleft+128):(lleft+64))+"px":"auto",display:sp?"block":"none"});
$("#menu").css({ display: (hm ? "none":"block") });
if(FW_isiOS)
$("#logo,#menuBtn").css({ top:'12px'});
@@ -1057,12 +1086,13 @@
// font-awesome: txInp:plus-square
var prf='data:image/svg+xml;utf8,<svg viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path fill="gray" d="';
var sfx='"/></svg>';
- f18_icon.pinIn = prf+'M 896 1088q66 0 128-15v655q0 26-19 45t-45 19h-128q-26 0-45-19t-19-45v-655q62 15 128 15zm0-1088q212 0 362 150t150 362-150 362-362 150-362-150-150-362 150-362 362-150zm0 224q14 0 23-9t9-23-9-23-23-9q-146 0-249 103t-103 249q0 14 9 23t23 9 23-9 9-23q0-119 84.5-203.5t203.5-84.5z'+sfx;
- f18_icon.burger = prf+'M 1664 1344v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45z'+sfx;
- f18_icon.arrows = prf+'M 1792 896q0 26-19 45l-256 256q-19 19-45 19t-45-19-19-45v-128h-384v384h128q26 0 45 19t19 45-19 45l-256 256q-19 19-45 19t-45-19l-256-256q-19-19-19-45t19-45 45-19h128v-384h-384v128q0 26-19 45t-45 19-45-19l-256-256q-19-19-19-45t19-45l256-256q19-19 45-19t45 19 19 45v128h384v-384h-128q-26 0-45-19t-19-45 19-45l256-256q19-19 45-19t45 19l256 256q19 19 19 45t-19 45-45 19h-128v384h384v-128q0-26 19-45t45-19 45 19l256 256q19 19 19 45z'+sfx;
- f18_icon.ban = prf+'M 1440 893q0-161-87-295l-754 753q137 89 297 89 111 0 211.5-43.5t173.5-116.5 116-174.5 43-212.5zm-999 299l755-754q-135-91-300-91-148 0-273 73t-198 199-73 274q0 162 89 299zm1223-299q0 157-61 300t-163.5 246-245 164-298.5 61-298.5-61-245-164-163.5-246-61-300 61-299.5 163.5-245.5 245-164 298.5-61 298.5 61 245 164 163.5 245.5 61 299.5z'+sfx;
- f18_icon.txInp = prf+'M 1302,839 V 939 c 0,19 -15,37 -36,37 H 993 v 277 c 0,19 -15,37 -36,37 H 856 c -20,0 -36,-15 -36,-37 V 977 H 546 c -20,0 -36,-15 -36,-37 V 839 c 0,-19 15,-37 36,-37 H 818 V 521 c 0,-19 15,-37 36,-37 h 97 c 20,0 36,15 36,37 V 798 H 1261 c 20,0 36,15 36,37 z M 1600,331 V 1447 c 0,83 -65,151 -148,151 H 360 C 277,1600 212,1532 212,1448 V 331 C 212,246 277,180 360,180 H 1450 c 81,0 147,66 147,151 z M 1450,1428 V 350 c 0,-8 -7,-17 -17,-17 H 379 c -9,0 -17,7 -17,17 V 1428 c 0,8 7,17 17,17 H 1431 c 9,0 17,-7 17,-17 z'+sfx;
- f18_icon.micro = prf+'M 840 1324 c -298 0 -542 -243 -542 -542 c 0 -31 25 -57 57 -57 c 31 0 57 25 57 57 C 413 1017 604 1209 840 1209 c 235 0 426 -191 426 -426 c 0 -31 25 -57 57 -57 s 57 25 57 57 C 1382 1081 1138 1324 840 1324 z M 840 1133 L 840 1133 c -191 0 -349 -157 -349 -349 V 349 C 490 157 648 0 840 0 h 0 c 191 0 349 157 349 349 v 435 C 1189 976 1031 1133 840 1133 z M 840 1665 c -31 0 -57 -25 -57 -57 V 1281 c 0 -31 25 -57 57 -57 c 31 0 57 25 57 57 v 325 C 897 1639 871 1665 840 1665 z M 1035 1680 H 644 c -31 0 -57 -25 -57 -57 s 25 -57 57 -57 h 390 c 31 0 57 25 57 57 S 1067 1680 1035 1680 z'+sfx;
+ f18_icon.pinIn = prf+'M 896 1088q66 0 128-15v655q0 26-19 45t-45 19h-128q-26 0-45-19t-19-45v-655q62 15 128 15zm0-1088q212 0 362 150t150 362-150 362-362 150-362-150-150-362 150-362 362-150zm0 224q14 0 23-9t9-23-9-23-23-9q-146 0-249 103t-103 249q0 14 9 23t23 9 23-9 9-23q0-119 84.5-203.5t203.5-84.5z'+sfx;
+ f18_icon.burger = prf+'M 1664 1344v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45z'+sfx;
+ f18_icon.arrows = prf+'M 1792 896q0 26-19 45l-256 256q-19 19-45 19t-45-19-19-45v-128h-384v384h128q26 0 45 19t19 45-19 45l-256 256q-19 19-45 19t-45-19l-256-256q-19-19-19-45t19-45 45-19h128v-384h-384v128q0 26-19 45t-45 19-45-19l-256-256q-19-19-19-45t19-45l256-256q19-19 45-19t45 19 19 45v128h384v-384h-128q-26 0-45-19t-19-45 19-45l256-256q19-19 45-19t45 19l256 256q19 19 19 45t-19 45-45 19h-128v384h384v-128q0-26 19-45t45-19 45 19l256 256q19 19 19 45z'+sfx;
+ f18_icon.ban = prf+'M 1440 893q0-161-87-295l-754 753q137 89 297 89 111 0 211.5-43.5t173.5-116.5 116-174.5 43-212.5zm-999 299l755-754q-135-91-300-91-148 0-273 73t-198 199-73 274q0 162 89 299zm1223-299q0 157-61 300t-163.5 246-245 164-298.5 61-298.5-61-245-164-163.5-246-61-300 61-299.5 163.5-245.5 245-164 298.5-61 298.5 61 245 164 163.5 245.5 61 299.5z'+sfx;
+ f18_icon.txInp = prf+'M 1302,839 V 939 c 0,19 -15,37 -36,37 H 993 v 277 c 0,19 -15,37 -36,37 H 856 c -20,0 -36,-15 -36,-37 V 977 H 546 c -20,0 -36,-15 -36,-37 V 839 c 0,-19 15,-37 36,-37 H 818 V 521 c 0,-19 15,-37 36,-37 h 97 c 20,0 36,15 36,37 V 798 H 1261 c 20,0 36,15 36,37 z M 1600,331 V 1447 c 0,83 -65,151 -148,151 H 360 C 277,1600 212,1532 212,1448 V 331 C 212,246 277,180 360,180 H 1450 c 81,0 147,66 147,151 z M 1450,1428 V 350 c 0,-8 -7,-17 -17,-17 H 379 c -9,0 -17,7 -17,17 V 1428 c 0,8 7,17 17,17 H 1431 c 9,0 17,-7 17,-17 z'+sfx;
+ f18_icon.micro = prf+'M 840 1324 c -298 0 -542 -243 -542 -542 c 0 -31 25 -57 57 -57 c 31 0 57 25 57 57 C 413 1017 604 1209 840 1209 c 235 0 426 -191 426 -426 c 0 -31 25 -57 57 -57 s 57 25 57 57 C 1382 1081 1138 1324 840 1324 z M 840 1133 L 840 1133 c -191 0 -349 -157 -349 -349 V 349 C 490 157 648 0 840 0 h 0 c 191 0 349 157 349 349 v 435 C 1189 976 1031 1133 840 1133 z M 840 1665 c -31 0 -57 -25 -57 -57 V 1281 c 0 -31 25 -57 57 -57 c 31 0 57 25 57 57 v 325 C 897 1639 871 1665 840 1665 z M 1035 1680 H 644 c -31 0 -57 -25 -57 -57 s 25 -57 57 -57 h 390 c 31 0 57 25 57 57 S 1067 1680 1035 1680 z'+sfx;
+ f18_icon.speaker = prf +'M 640 640 L 640 1152 L 960 1152 L 1280 1472 L 1280 320 L 960 640 Z M 1408 512 C 1536 640 1536 1152 1408 1280' +sfx;
}
function
Gruß schwatter
Nabend,
Piper per Url-Aufruf funktioniert jetzt.
defmod TTS Text2Speech default
attr TTS TTS_CacheFileDir /opt/fhem/www/audio
attr TTS TTS_MplayerCall /usr/bin/mplayer -ao alsa:device=bluealsa -af resample=44100
attr TTS TTS_Ressource Piper
attr TTS TTS_TimeOut 60
attr TTS TTS_User host=127.0.0.1 port=5000 length_scale=1.0 noise_scale=1.5
attr TTS TTS_VolumeAdjust 90
attr TTS room 12.System
attr TTS verbose 0
Edit: Ich will noch auf wav umstellen. Aber ich habe gerade kein Ton aus Fhem. Daher schaue ich gerade, woran es liegt.
Gruß schwatter
Zitat von: schwatter am 26 April 2026, 20:41:31Piper per Url-Aufruf funktioniert jetzt.
Thx, ein Pfad!
Der Code will mir jedenfalls beim Überfliegen noch nicht recht gefallen (ich hätte gerne "generischen POST"-Code, falls jemand eine neue Idee bzw. einen "besseren Server(dienst)" hat oder verwenden will), und das Testen muss noch etwas warten....
Nabend,
ok verstehe. Also komplett leer. Damit einfach der Post-Code gesetzt werden kann.
Mittlerweile funktioniert auch Wave bei mir. Damit erstmal nichts umgebaut werden
muss, maskiere ich die Wave einfach als Mp3. Damit gehts nu.
root@ubuntufhem:/opt/fhem/www/audio# file 5aeaad16ab211cb0a6bdbae7a57d1538.mp3
5aeaad16ab211cb0a6bdbae7a57d1538.mp3: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 16000 Hz
root@ubuntufhem:/opt/fhem/www/audio#
Gruß schwatter
So,
ich habe mir dein maryTTS Beispiel eine Seite vorher nochmal angeschaut. Das hatte ich vergessen.
Der TTS_User wird jetzt so gesetzt:
attr TTS TTS_User host='http://127.0.0.1:5000/' method=POST header='Content-Type: application/json' body='{"text": "$text", "length_scale": 1.0, "noise_scale": 1.5}'
Außerdem bin ich erstmal wieder zu MP3 zurück. Das maskieren hat solala funktioniert. Wahrscheinlich weil der Header Wave ist und
die Datei Mp3. Piper hat dann nach ein paar Wörtern das Buchstabieren angefangen....
Gruß schwatter
:o - kaum macht man es richtig, funktioniert es auch ;D ...
DANKE! für den Schubs!
Mein aktueller Code:
defmod t2s Text2Speech none
attr t2s TTS_Ressource maryTTS
attr t2s TTS_User url='http://127.0.0.1:5000/' method=POST header='Content-Type: application/json' data={"text": "$text"}
Idee hinter dieser Konfig: Alle "eigenen Serverdienste" können über die "Großmutter" solcher Dienste, maryTTS angesteuert werden, man muss "nur" die korrekten Settings in TTS_User übergeben.
Kann sein, dass das noch nicht vollständig alle Optionen für fhem-httputils abdeckt, aber für den Moment würde ich das so lassen?
Nabend,
ich bestätige, so läuft es auch bei mir unter marryTTS. Und ja, ich auch denke auch, lass es erstmal so. Wenn noch was ist,
meldet sich wer. Oder wir sind schneller.
Gruß schwatter
Dann mache ich mal einen Patch für @Tobias fertig :) .
Ja bitte, meine Liste von exclude_from_update wird immer größer ;D
Gruß schwatter
Zitat von: schwatter am 28 April 2026, 21:19:42Ja bitte, meine Liste von exclude_from_update wird immer größer ;D
Gruß schwatter
here you are: https://forum.fhem.de/index.php?topic=128346.0 (muss vermutlich Tobias auch direkt anpingen).
Btw: updates für t2s sind so selten, da lohnt es sich kaum, die "Hosenträger" anzuziehen...
Zitat von: Beta-User am 29 April 2026, 07:56:57muss vermutlich Tobias auch direkt anpingen
done. Dann warten wir mal ab, wann er Zeit findet, sich das anzusehen.
Nun ja, zumindest vorläufig können wir also diesen Exkurs zu Text2Speech für "erledigt" erklären, stellt sich die Frage, wie es sinnvoll weitergehen soll (wenn ich mal wieder Zeit finde, überhaupt weiterzubasteln - die nächsten Tage vermutlich nicht...).
Meine Idealvorstellung wäre weiter, piper per fetch() von js aus abzufragen und dann das js einfach wieder den "bin fertig"-Event absetzen zu lassen. Den ganzen Code dazu würde man auf der FHEM-Seite doch zusammenbasteln können, schwierig zu umschiffen kommt mir allerdings das CORS-Ding vor (Neuland, das alles)...
T2S-Events abzugreifen ginge (vielleicht) auch, aber das per js zu machen, kommt mir nach wie vor nicht wie eine "einfache" Lösung vor, und wechselseitige Event-Abhängigkeiten z.B. von RHASSPY und T2S zu basteln, gefällt mir letztlich genausowenig, wie z.B. die wav (direkt) aus RHASSPY heraus generieren zu lassen und dann zur Wiedergabe per js freizugeben....
Na ja, das darf jetzt jedenfalls erst mal vor sich hin gedacht werden ??? .
Nabend,
den Event bekommen wir durch das attr additionalInform auf das TTS-Device gesetzt. Hier in f18 umgesetzt.
https://forum.fhem.de/index.php?msg=1362632
Damit wird auf jedem Device die letzte MP3 von TTS abgespielt. Daher werde ich Voicecontrol wohl auch umbauen,
damit ich Piper als Stimme habe. Die Sprachausgabe auf meinem FireHD ist roboterhaft schlecht.
Gruß schwatter
Zitat von: schwatter am 30 April 2026, 18:56:18den Event bekommen wir durch das attr additionalInform auf das TTS-Device gesetzt.
Vermutlich reden wir in Teilen aneinander vorbei, es geht um zwei Zeitpunkte:
1. Wann kann die Ausgabe starten, und
2. wann ist sie beendet?
Das mit additionalInform ist zunächst mal eine Option für den ersten Zeitpunkt. Ich werde mir das ansehen (Danke noch für das diff hier weiter oben), wie schon weiter oben mal angekündigt.
Für die "Interaktivität" im Sinne dieses Threads ist aber das
2., das "bin fertig"-Event genauso wichtig...
In diesem Code "verborgen" in dem "utterance.onend = ..."-Handler.
Zitat von: Beta-User am 21 April 2026, 07:23:50@Rudi - falls du hier mitliest: Scheinbar hatte ich bisher noch nicht meine modifizierte f18_speak() mit dem "bin fertig"-Event gezeigt.
Hier die Roh-Fassung:
Code Auswählen Erweitern
function
f18_speak(txt)
{
let synth = window.speechSynthesis;
if(!synth)
return FW_okDialog("No speechSynthesis available");
const utterance = new SpeechSynthesisUtterance(txt);
// Good practice: Set listeners even if they're unreliable on all voices
utterance.onend = () => {
var fw_id = $("body").attr("fw_id");
FW_cmd(`${FW_root}?cmd=setreading `+
`TYPE=FHEMWEB:FILTER=FW_ID=${fw_id}:FILTER=inform=.%2B `+
`TTS_state finished&XHR=1`);
};
synth.speak(utterance);
//speechSynthesis.speak(new SpeechSynthesisUtterance(txt));
//see https://iifx.dev/en/articles/457363230/chrome-tts-workarounds-solving-the-speechsynthesisutterance-event-and-initial-speak-failure for more info
}
Zitatdamit ich Piper als Stimme habe.
:) freut mich, dass dir diese Lösung (besser) gefällt. Ist auch kein Zufall, dass pah seine "Polly"-Apps als backup vermutlich im Jahr 2050 noch vorhalten wird ;D .
Was gefällt mir nun eigentlich an dem "additionalInform"-Weg im Moment nicht so recht?
- Man braucht überhaupt ein weiteres FHEM-Device. Wenn man das Abholen von piper per js erledigen könnte, braucht man den ganzen anderen Kram gar nicht :P . Leider funktionierte mein Testcode (https://forum.fhem.de/index.php?msg=1361964) nicht.
- T2S fragt blockierend ab und wandelt nach mp3 um. Ist historisch bedingt halt so, aber beides mag ich nun mal einfach nicht, zumal
- mir das "Zwischenlagern" von files nicht gefällt, und
- mir noch nicht klar ist, ob T2S überhaupt einen Event generiert, wenn es diese Ausgabe schon als file gibt;
- Zu guter Letzt: Wenn es mehrere User gibt, braucht man auch mehrere Instanzen von T2S, oder?
Insgesamt: Es ist frickelig, mir sind insgesamt bei dem T2S-Weg zu viele Stolpersteine verbaut, die mir im Moment unnötig vorkommen :P .
Wenn es nun keine Lösung für das direkte Abfragen bei piper aus js heraus gibt, werde ich daher für RHASSPY vermutlich dann separaten, non-blocking code basteln, der dann eine Datei pro offenem Dialog abholt und per "play"-Funktion aktiv an die betreffende Gegenstelle zum Abspielen anweist. Die js-Funktionalität fehlt in f18 noch, und wäre bitte ebenfalls mit "bin fertig-Event" am Ende zu versehen.
Hoffe, das ist jetzt etwas klarer?
Nabend,
Rudi hat die erste Version von Mp3 abspielen eingebaut. Vielleicht kann er das noch um einen "ich bin fertig" Event ergänzen.
Ich habe auch mal ein bisschen am direktholen der Audiodatei von Piper getüddelt. Aber ja, bei mir hat auch das CORS-Problem
dazwischen gehauen. Von einem Port auf den anderen zugreifen mag der Browser nicht. Vielleicht können wir Piper anders starten.
Mal sehen.
Gruß schwatter
Es ist eigentlich problemlos möglich, CORS zu erlauben. Die Doku von FHEMWEB ist da ein wenig dünn.
Hier kann man schon weiterkommen: https://stackoverflow.com/questions/39045421/how-to-enable-cors-sever-javascript-where
LG
pah
Zitat von: Prof. Dr. Peter Henning am 03 Mai 2026, 03:24:45Hier kann man schon weiterkommen: https://stackoverflow.com/questions/39045421/how-to-enable-cors-sever-javascript-where (https://stackoverflow.com/questions/39045421/how-to-enable-cors-sever-javascript-where)
Danke für den Link, leider blicke ich immer noch nicht durch:
Wenn ich die diversen Quellen zu CORS richtig verstanden habe, müßte eigentlich der piper-server per preflight-Anfrage zunächst angepingt werden, da application/json nicht zu den "an sich erlaubten" Content-Type gehört. Aha....?
Vielleicht beschäftige ich mich ein andermal wieder mit https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#preflighted_requests, für den Moment habe ich erst mal wieder fertig mit rumprobieren...