From 7540d27ff370d379b5f1faf7b605e053ae6f4e70 Mon Sep 17 00:00:00 2001 From: "Tobias.Faust" <> Date: Sun, 21 Jul 2024 14:17:28 +0000 Subject: [PATCH] 98_Text2Speech: add maryTTS git-svn-id: https://svn.fhem.de/fhem/trunk@29035 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/98_Text2Speech.pm | 356 ++++++++++++++++++++++++++++-------- 1 file changed, 278 insertions(+), 78 deletions(-) diff --git a/fhem/FHEM/98_Text2Speech.pm b/fhem/FHEM/98_Text2Speech.pm index 9bfd8419b..9bae9ead9 100644 --- a/fhem/FHEM/98_Text2Speech.pm +++ b/fhem/FHEM/98_Text2Speech.pm @@ -56,7 +56,8 @@ my %ttsAddon = ("Google" => "client=tw-ob&ie=UTF-8", "VoiceRSS" => "" ); my %ttsAPIKey = ("Google" => "", # kein APIKey nötig - "VoiceRSS" => "key=" + "VoiceRSS" => "key=", + "maryTTS" => '' ); my %ttsUser = ("Google" => "", # kein Username nötig "VoiceRSS" => "" # kein Username nötig @@ -70,7 +71,8 @@ my %ttsQuality = ("Google" => "", my %ttsMaxChar = ("Google" => 200, "VoiceRSS" => 300, "SVOX-pico" => 1000, - "Amazon-Polly" => 3000 + "Amazon-Polly" => 3000, + "maryTTS" => 3000 ); my %language = ("Google" => { "Deutsch" => "de", "English-US" => "en-us", @@ -116,10 +118,11 @@ sub Text2Speech_Initialize($) $hash->{DefFn} = "Text2Speech_Define"; $hash->{SetFn} = "Text2Speech_Set"; $hash->{UndefFn} = "Text2Speech_Undefine"; + $hash->{RenameFn} = "Text2Speech_Rename"; $hash->{AttrFn} = "Text2Speech_Attr"; $hash->{AttrList} = "disable:0,1". " TTS_Delimiter". - " TTS_Ressource:ESpeak,SVOX-pico,Amazon-Polly,". join(",", sort keys %ttsHost). + " TTS_Ressource:ESpeak,SVOX-pico,Amazon-Polly,maryTTS,". join(",", sort keys %ttsHost). " TTS_APIKey". " TTS_User". " TTS_Quality:". @@ -166,6 +169,7 @@ sub Text2Speech_Initialize($) " TTS_SpeakAsFastAsPossible:1,0". " TTS_OutputFile". " TTS_AWS_HomeDir". + " TTS_RemotePlayerCall". " ".$readingFnAttributes; } @@ -227,6 +231,7 @@ sub Text2Speech_Define($$) if ($ret) { Log3 $hash->{NAME}, 3, $ret; } + Text2Speech_AddExtension( $hash->{NAME}, \&Text2Speech_getLastMp3, "$hash->{TYPE}/$hash->{NAME}/last.mp3" ); return undef; } @@ -296,11 +301,20 @@ sub Text2Speech_Undefine($$) RemoveInternalTimer($hash); BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID})); + Text2Speech_RemoveExtension( "$hash->{TYPE}/$hash->{NAME}/last.mp3" ); Text2Speech_CloseDev($hash); return undef; } +sub Text2Speech_Rename(@) { + my ( $newname, $oldname ) = @_; + my $hash = $defs{$newname}; + my $type = $hash->{TYPE}; + Text2Speech_RemoveExtension( "$type/$oldname/last.mp3" ); + Text2Speech_AddExtension( $newname, \&Text2Speech_getLastMp3, "$type/$newname/last.mp3" ); +} + sub Text2Speech_Attr(@) { my @a = @_; my $do = 0; @@ -374,6 +388,12 @@ sub Text2Speech_Attr(@) { return "file does not exist: <".$newDir ."/". $FileTplPc[1] .">" unless (-e $newDir ."/". $FileTplPc[1]); } + } elsif ($a[0] eq "set" && $a[2] eq "TTS_RemotePlayerCall") { + if( $init_done ) { + eval $value ; + return "Text2Speach_Attr evaluating TTS_RemotePlayerCall error: $@" if ( $@ ); + + } } if($a[0] eq "set" && $a[2] eq "disable") { @@ -506,7 +526,7 @@ sub Text2Speech_Set($@) return "no set argument specified" if(int(@a) < 2); return "No APIKey specified" if (!defined($TTS_APIKey) && ($ttsAPIKey{$TTS_Ressource} || length($ttsAPIKey{$TTS_Ressource})>0)); - return "No Username for TTS Access specified" if (!defined($TTS_User) && ($ttsUser{$TTS_Ressource} || length($ttsUser{$TTS_Ressource})>0)); + return "No Username for TTS Access specified" if ( $TTS_Ressource ne 'maryTTS' && !defined($TTS_User) && ($ttsUser{$TTS_Ressource} || length($ttsUser{$TTS_Ressource})>0)); my $ret = Text2Speech_loadmodules($hash, $TTS_Ressource); if ($ret) { @@ -804,7 +824,6 @@ sub Text2Speech_BuildMplayerCmdString($$) { my $mp3Duration = Text2Speech_CalcMP3Duration($hash, $file); BlockingInformParent("Text2Speech_readingsSingleUpdateByName", [$hash->{NAME}, "duration", "$mp3Duration"], 0); - BlockingInformParent("Text2Speech_readingsSingleUpdateByName", [$hash->{NAME}, "endTime", "00:00:00"], 0); return $cmd; } @@ -959,6 +978,45 @@ sub Text2Speech_Download($$$) { $fh->print($res->AudioStream); Log3 $hash->{NAME}, 4, $hash->{NAME}.": Schreibe mp3 in die Datei $file mit ". $res->RequestCharacters ." Chars"; close($fh); + } 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'; + + $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 => 'GET' # POST can be found in https://github.com/marytts/marytts-txt2wav/blob/python/txt2wav.py#L33 + }; + 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 mit ".length $maryTTSResponse . ' Bytes'); + close $fh2; + $cmd = qq(lame "$FileWav2" "$file"); + Log3($hash, 4, "$hash->{NAME}:$cmd"); + system $cmd; + return unlink $FileWav2; } } @@ -1071,15 +1129,21 @@ sub Text2Speech_DoIt($) { my $t = substr($Mp3WrapFile, 0, length($Mp3WrapFile)-4)."_MP3WRAP.mp3"; if(-e $t){ + Log3 $hash->{NAME}, 4, $hash->{NAME}.": Benenne Datei um von <".$t."> nach <".$Mp3WrapFile.">"; rename($t, $Mp3WrapFile); #falls die Datei existiert den ID3V1 und ID3V2 Tag entfernen - eval{ - remove_mp3tag($Mp3WrapFile, 2); - remove_mp3tag($Mp3WrapFile, 1); - Log3 $hash, 4, $hash->{NAME}.": Die ID3 Tags von $Mp3WrapFile wurden geloescht"; - } or Log3 $hash->{NAME}, 3, "MP3::Info Modul fehlt, konnte MP3 Tags nicht entfernen"; - } else {Log3 $hash->{NAME}, 3, $hash->{NAME}.": MP3WRAP Fehler!, Datei wurde nicht generiert.";} + my $ret = eval{ remove_mp3tag( $Mp3WrapFile, 'ALL' ) }; + Log3 $hash, 1, $hash->{NAME}.": Fehle beim entfernen der ID3 Tags: $@" if ( $@ ); + Log3 $hash, 4, $hash->{NAME}.": Die ID3 Tags ( $ret Bytes ) von $Mp3WrapFile wurden geloescht." if ( $ret > 0 ); + Log3 $hash, 4, $hash->{NAME}.": Die ID3 Tags ( 0 Bytes ) von $Mp3WrapFile wurden geloescht." if ( $ret == -1 ); + Log3 $hash->{NAME}, 3, "MP3::Info Modul fehlt, konnte MP3 Tags nicht entfernen!" if ( !$ret ); + + } else { + + Log3 $hash->{NAME}, 3, $hash->{NAME}.": MP3WRAP Fehler!, Datei wurde nicht generiert."; + + } } if ($TTS_OutputFile && $TTS_OutputFile ne $Mp3WrapFile) { @@ -1124,7 +1188,6 @@ sub Text2Speech_DoIt($) { Log3 $hash->{NAME}, 4, $hash->{NAME}.":" .$cmd; system($cmd); } - return $hash->{NAME}. "|". "1" ."|". $file; @@ -1154,7 +1217,16 @@ sub Text2Speech_Done($) { } Text2Speech_WriteStats($hash, 1, $filename, join(" ", @text)) if (AttrVal($hash->{NAME},"TTS_noStatisticsLog", "0")==0); - readingsSingleUpdate($hash, "lastFilename", $filename, 1); + readingsBeginUpdate( $hash ); + readingsBulkUpdate($hash, 'lastFilename', $filename ); + # Update der Dauer im Servermode + readingsBulkUpdate( $hash, 'duration', Text2Speech_CalcMP3Duration( $hash, $filename ) ) if( $hash->{MODE} eq "SERVER" ); + readingsEndUpdate( $hash, 1 ); + + # Aufruf eine eines Abspielgerätes wenn das Attibut gesetzt ist + my $playercall = AttrVal( $hash->{NAME}, 'TTS_RemotePlayerCall', '' ); + eval $playercall if( $playercall ); + Log3( $hash, 1, $hash->{NAME}." TTS_RemotePlayerCall: eval error $@.") if( $@ ); } delete($hash->{helper}{RUNNING_PID}); @@ -1167,9 +1239,9 @@ sub Text2Speech_Done($) { $hash->{helper}{RUNNING_PID} = BlockingCall("Text2Speech_DoIt", $hash, "Text2Speech_Done", $TTS_TimeOut, "Text2Speech_AbortFn", $hash); } else { + # alles wurde bearbeitet Log3($hash,4, $hash->{NAME}.": Es wurden alle Teile ausgegeben und der Befehl ist abgearbeitet."); - readingsSingleUpdate($hash, "playing", "0", 1); } } @@ -1224,6 +1296,91 @@ sub Text2Speech_WriteStats($$$$){ DbLog_ExecSQL($defs{$DbLogDev}, $cmd); } +######################### +sub Text2Speech_readMp3(@) { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name Text2Speech_readMp3:"; + my $filename = ReadingsVal( $name, 'lastFilename', '' ); + + if ( $filename and -e $filename ) { + + if( open my $fh, '<:raw', $filename ) { + + my $content = ''; + + while (1) { + my $success = read $fh, $content, 1024, length( $content ); + if( not defined $success ) { + close $fh; + Log3 $name, 1, "$iam read file \"$filename\" error: $!."; + return undef; + } + + last if not $success; + } + + close $fh; + Log3 $name, 4, "$iam file \"$filename\" content length: ".length( $content ); + return \$content; + + } else { + Log3 $name, 1, "$iam open file \"$filename\" error: $!."; + } + + } else { + Log3 $name, 2, "$iam file \"$filename\" does not exist."; + } + return undef; +} + +######################### +sub Text2Speech_getLastMp3 { + my ($request) = @_; + + if ( $request =~ /^\/(Text2Speech)\/(\w+)\/last.mp3/ ) { + + my $type = $1; + my $name = $2; + my $hash = $defs{$name}; + my $audioData = Text2Speech_readMp3( $hash ); + return ( "text/plain; charset=utf-8","${type} ${name}: No MP3 file for webhook $request" ) if ( !$audioData ); + my $audioMime = 'audio/mpeg'; + + return ( $audioMime, $$audioData ); + } + return ( "text/plain; charset=utf-8", "No Text2Speech device for webhook $request" ); +} + +######################### +sub Text2Speech_AddExtension(@) { + my ( $name, $func, $link ) = @_; + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + + my $url = "/$link"; + Log3( $name, 2, "Registering $type $name for URL $url..." ); + $::data{FWEXT}{$url}{deviceName} = $name; + $::data{FWEXT}{$url}{FUNC} = $func; + $::data{FWEXT}{$url}{LINK} = $link; + + return; +} + +######################### +sub Text2SpeechRemoveExtension(@) { + my ($link) = @_; + my $url = "/$link"; + my $name = $::data{FWEXT}{$url}{deviceName}; + + Log3( $name, 2, "Unregistering URL $url..." ); + delete $::data{FWEXT}{$url}; + + return; +} + + 1; =pod @@ -1235,12 +1392,12 @@ the result on a local or remote loudspeaker über einen lokalen oder entfernten Lautsprecher wiedergibt =begin html - +
define <name> Text2Speech <alsadevice>
define <name> Text2Speech <host>[:<portnr>][:SSL] [portpassword]
@@ -1308,30 +1465,29 @@ the result on a local or remote loudspeaker
sudo apt-get install libttspico-utils lame
libttspico-utils
,(Full) example for maryTTS (values are defaults and may be left out):
+attr t2s TTS_User host=127.0.0.1 port=59125 lang=de_DE voice=de_DE/thorsten_low
silence.mp3
set MyTTS tts Attention: This is my ringtone :ring: Its loud?
cache/templates
attr myTTS TTS_VolumeAdjust 400
attr myTTS TTS_OutputFile output.mp3
attr myTTS TTS_OutputFile /media/miniDLNA/output.mp3
<protocol>://<fhem server ip or name>:<fhem port>/fhem/Text2Speech/<device name>/last.mp3.
attr <device name> TTS_RemotePlayerCall GetFileFromURL('<protocol>://<remote player name or ip>:<remote player port>/?cmd=playSound&url=<protocol>://<fhem server name orip>:<fhem port>/fhem/Text2Speech/<device name>/last.mp3&loop=false&password=<password>')
define TTS_EG_WZ Text2Speech hw=/dev/snd/controlC3
attr TTS_EG_WZ TTS_Language English
define MyTTS Text2Speech hw=0.0
set MyTTS tts The alarm system is ready.
set MyTTS tts :beep.mp3:
set MyTTS tts :mytemplates/alarm.mp3:The alarm system is ready.:ring.mp3:
set MyTTS tts :mytemplates/alarm.mp3:The alarm system is ready.:ring.mp3:
+
+ define T2S Text2Speech default
+ attr T2S TTS_MplayerCall /usr/bin/mplayer
+ attr T2S TTS_Ressource maryTTS
+ attr T2S TTS_User host=192.168.100.1 port=59125 lang=de_DE voice=de_DE/thorsten_low ssml=1
+ set T2S tts '<voice name="de_DE/m-ailabs_low#rebecca_braunert_plunkett">Das ist ein Test in deutsch </voice><voice name="en_US/vctk_low#p236">and this is an test in english.</voice>'
+
define <name> Text2Speech <alsadevice>
define <name> Text2Speech <host>[:<portnr>][:SSL] [portpassword]
Server : define <name> Text2Speech none
attr myTTS TTS_Delimiter -al.
(Vollständiges) Beispiel für maryTTS (die angegebenen Werte entsprechen den defaults):
+attr t2s TTS_User host=127.0.0.1 port=59125 lang=de_DE voice=de_DE/thorsten_low
attr myTTS TTS_MplayerCall player {file} {options}
silence.mp3
set MyTTS tts Achtung: hier kommt mein Klingelton :ring: War der laut?
cache/templates
attr myTTS TTS_VolumeAdjust 400
attr myTTS TTS_OutputFile /media/miniDLNA/output.mp3
<protocol>://<fhem server name or ip>:<fhem port>/fhem/Text2Speech/<device name>/last.mp3
attr <device name> TTS_RemotePlayerCall GetFileFromURL('<protocol>://<remote player name or ip>:2323/?cmd=playSound&url=<protocol>://<fhem server name or ip>:<fhem port>/fhem/Text2Speech/<device name>/last.mp3&loop=false&password=<password>')
define TTS_EG_WZ Text2Speech hw=/dev/snd/controlC3
attr TTS_EG_WZ TTS_Language Deutsch
define MyTTS Text2Speech hw=0.0
set MyTTS tts Die Alarmanlage ist bereit.
set MyTTS tts :beep.mp3:
set MyTTS tts :mytemplates/alarm.mp3:Die Alarmanlage ist bereit.:ring.mp3:
set MyTTS tts :mytemplates/alarm.mp3:Die Alarmanlage ist bereit.:ring.mp3:
+
+ define T2S Text2Speech default
+ attr T2S TTS_MplayerCall /usr/bin/mplayer
+ attr T2S TTS_Ressource maryTTS
+ attr T2S TTS_User host=192.168.100.1 port=59125 lang=de_DE voice=de_DE/thorsten_low ssml=1
+ set T2S tts '<voice name="de_DE/m-ailabs_low#rebecca_braunert_plunkett">Das ist ein Test in deutsch </voice><voice name="en_US/vctk_low#p236">and this is an test in english.</voice>'
+