From b48c93bf4f887d235023e502c48a9670f198469b Mon Sep 17 00:00:00 2001 From: "Tobias.Faust" <> Date: Sat, 3 Apr 2021 09:52:38 +0000 Subject: [PATCH] 98_Text2Speech.pm: some Improvements by Mirko git-svn-id: https://svn.fhem.de/fhem/trunk@24138 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/98_Text2Speech.pm | 460 +++++++++++++++++++++--------------- 2 files changed, 264 insertions(+), 197 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index 09a87b7a5..9331cc669 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - bugfix: 98_Text2Speech.pm: some Improvements by Mirko - change: 73_GardenaSmartBridge: Change part of code for new API - bugfix: 47_OBIS: fixed bug with 64 bit integer numbers - change: 46_TeslaPowerwall2AC: Code rewrite for new Auth API and diff --git a/fhem/FHEM/98_Text2Speech.pm b/fhem/FHEM/98_Text2Speech.pm index 81251c32a..5cfbd281b 100644 --- a/fhem/FHEM/98_Text2Speech.pm +++ b/fhem/FHEM/98_Text2Speech.pm @@ -15,8 +15,6 @@ # ALL ALL = NOPASSWD: /usr/bin/mplayer ############################################## -# VoiceRSS: http://www.voicerss.org/api/documentation.aspx - package main; use strict; use warnings; @@ -240,37 +238,37 @@ sub Text2Speech_loadmodules($$) { require IO::File; IO::File->import; 1; - } or return "IO::File Module not installed, please install"; + } or return "IO::File Module not installed. Please install."; eval { require Digest::MD5; Digest::MD5->import; 1; - } or return "Digest::MD5 Module not installed, please install"; + } or return "Digest::MD5 Module not installed. Please install."; eval { require URI::Escape; URI::Escape->import; 1; - } or return "URI::Escape Module not installed, please install"; + } or return "URI::Escape Module not installed. Please install."; eval { require Text::Iconv; Text::Iconv->import; 1; - } or return "Text::Iconv Module not installed, please install"; + } or return "Text::Iconv Module not installed. Please install."; eval { require Encode::Guess; Encode::Guess->import; 1; - } or return "Encode::Guess Module not installed, please install"; + } or return "Encode::Guess Module not installed. Please install."; eval { require MP3::Info; MP3::Info->import; 1; - } or return "MP3::Info Module not installed, please install"; + } or return "MP3::Info Module not installed. Please install."; if ($TTS_Ressource eq "Amazon-Polly") { # Module werden nur benötigt mit der Polly Engine @@ -278,7 +276,7 @@ sub Text2Speech_loadmodules($$) { require Paws::Polly; Paws::Polly->import; 1; - } or return "Paws Module not installed. Please install, goto https://metacpan.org/source/JLMARTIN/Paws-0.39"; + } or return "Paws Module not installed. Please install."; eval { require File::HomeDir; @@ -522,6 +520,8 @@ sub Text2Speech_Set($@) if($cmd ne "tts") { return "$cmd needs $sets{$cmd} parameter(s)" if(@a-$sets{$cmd} != 0); + } else { + return "$cmd needs text parameter" if(@a-$sets{$cmd} < 0); } # Abbruch falls Disabled @@ -585,6 +585,18 @@ sub Text2Speech_PrepareSpeech($$) { my $TTS_ForceSplit = 0; my $TTS_AddDelimiter; + # Cleanup string + $hash->{helper}{TTS_PlayerOptions} = ""; + while ($t =~ s/^ //isg) {}; + $t =~ s/^'(.*)'$/$1/; + $t =~ s/^"(.*)"$/$1/; + + # Check text for command string + if ($t =~ /^\[(.*?)\](.*?)$/) { + ($hash->{helper}{TTS_PlayerOptions}, $t) = ($1, $2); + while ($t =~ s/^ //isg) {}; + } + if($TTS_Delimiter && $TTS_Delimiter =~ m/^[+-]a[lfn]/i) { $TTS_ForceSplit = 1 if(substr($TTS_Delimiter,0,1) eq "+"); $TTS_ForceSplit = 0 if(substr($TTS_Delimiter,0,1) eq "-"); @@ -655,7 +667,7 @@ sub Text2Speech_PrepareSpeech($$) { for(my $j=0; $j<(@FileTplPc); $j++) { $text[$i] =~ s/:$FileTplPc[$j]:/$cutter$FileTplPc[$j]$cutter/g; } - @text = Text2Speech_SplitString(\@text, 0, $cutter, 1, ""); + @text = Text2Speech_SplitString(\@text, 0, $cutter, 1, ""); } Log3 $hash, 4, "$me: MaxChar = $ttsMaxChar{$TTS_Ressource}, Delimiter = $TTS_Delimiter, ForceSplit = $TTS_ForceSplit, AddDelimiter = $TTS_AddDelimiter"; @@ -669,23 +681,23 @@ sub Text2Speech_PrepareSpeech($$) { @text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, "\\bund\\b", 0, "af"); @text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, " ", 0, ""); - Log3 $hash, 4, "$me: Auflistung der Textbausteine nach Aufbereitung:"; + Log3 $hash, 4, "$me: Auflistung der Textbausteine nach Aufbereitung:"; for(my $i=0; $i<(@text); $i++) { # entferne führende und abschließende Leerzeichen aus jedem Textbaustein - $text[$i] =~ s/^\s+|\s+$//g; + $text[$i] =~ s/^\s+|\s+$//g; for(my $j=0; $j<(@FileTpl); $j++) { # ersetze die FileTemplates mit den echten MP3-Files @FileTplPc = split(/:/, $FileTpl[$j]); $text[$i] = $TTS_FileTemplateDir ."/". $FileTplPc[1] if($text[$i] eq $FileTplPc[0]); } - Log3 $hash, 4, "$me: $i => ".$text[$i]; + Log3 $hash, 4, "$me: $i => ".$text[$i]; } push( @{$hash->{helper}{Text2Speech}}, @text ); } ##################################### -# param1: array : Text 2 Speech +# param1: array : Text 2 Speech # param2: string: MaxChar # param3: string: Delimiter # param4: int : 1 -> es wird am Delimiter gesplittet @@ -694,7 +706,7 @@ sub Text2Speech_PrepareSpeech($$) { # # Splittet die Texte aus $hash->{helper}->{Text2Speech} anhand des # Delimiters, wenn die Stringlänge MaxChars übersteigt. -# Ist "AddDelimiter" angegeben, so wird der Delimiter an den +# Ist "AddDelimiter" angegeben, so wird der Delimiter an den # String wieder angefügt ##################################### sub Text2Speech_SplitString($$$$$){ @@ -748,7 +760,7 @@ sub Text2Speech_SplitString($$$$$){ ##################################### # param1: hash : Hash # param2: string: Datei -# +# # Erstellt den Commandstring für den Systemaufruf ##################################### sub Text2Speech_BuildMplayerCmdString($$) { @@ -760,7 +772,7 @@ sub Text2Speech_BuildMplayerCmdString($$) { my $verbose = AttrVal($hash->{NAME}, "verbose", 3); if($hash->{VOLUME}) { # per: set volume <..> - $mplayerOpts .= " -softvol -softvol-max ". $TTS_VolumeAdjust ." -volume " . $hash->{VOLUME}; + $mplayerOpts .= " -softvol -softvol-max ". $TTS_VolumeAdjust ." -volume " . $hash->{VOLUME}; } my $AlsaDevice = $hash->{ALSADEVICE}; @@ -771,13 +783,13 @@ sub Text2Speech_BuildMplayerCmdString($$) { my $NoDebug = $mplayerNoDebug; $NoDebug = "" if($verbose >= 5); - # anstatt mplayer wird ein anderer Player verwendet if ($TTS_MplayerCall !~ m/mplayer/) { $TTS_MplayerCall =~ s/{device}/$AlsaDevice/g; $TTS_MplayerCall =~ s/{volume}/$hash->{VOLUME}/g; $TTS_MplayerCall =~ s/{volumeadjust}/$TTS_VolumeAdjust/g; $TTS_MplayerCall =~ s/{file}/$file/g; + $TTS_MplayerCall =~ s/{options}/$hash->{helper}{TTS_PlayerOptions}/g; $cmd = $TTS_MplayerCall; } else { @@ -804,7 +816,7 @@ sub Text2Speech_readingsSingleUpdateByName($$$) { ##################################### # param1: string: MP3 Datei inkl. Pfad -# +# # Ermittelt die Abspieldauer einer MP3 und gibt die Zeit in Sekunden zurück. # Die Abspielzeit wird auf eine ganze Zahl gerundet ##################################### @@ -818,7 +830,7 @@ sub Text2Speech_CalcMP3Duration($$) { Log3 $hash, 4, $hash->{NAME}.": $file hat eine Länge von $time Sekunden."; } }; - + if ($@) { Log3 $hash, 2, $hash->{NAME}.": Bei der MP3-Längenermittlung ist ein Fehler aufgetreten: $@"; return undef; @@ -831,7 +843,7 @@ sub Text2Speech_CalcMP3Duration($$) { # param1: hash : Hash # param2: string: Dateiname # param2: string: Text -# +# # Holt den Text mithilfe der entsprechenden TTS_Ressource ##################################### sub Text2Speech_Download($$$) { @@ -860,7 +872,7 @@ sub Text2Speech_Download($$$) { $url .= "&" . $ttsQuality{$TTS_Ressource} . $TTS_Quality if(length($ttsQuality{$TTS_Ressource})>0); $url .= "&" . $ttsSpeed{$TTS_Ressource} . $TTS_Speed if(length($ttsSpeed{$TTS_Ressource})>0); $url .= "&" . $ttsQuery{$TTS_Ressource} . uri_escape($text); - + Log3 $hash->{NAME}, 4, $hash->{NAME}.": Hole URL: ". $url; #$HttpResponse = GetHttpFile($ttsHost, $ttsPath . $ttsLang . $TTS_Language . "&" . $ttsQuery . uri_escape($text)); my $param = { @@ -873,10 +885,10 @@ sub Text2Speech_Download($$$) { #header => "agent: Mozilla/1.22\r\nUser-Agent: Mozilla/1.22" }; ($HttpResponseErr, $HttpResponse) = HttpUtils_BlockingGet($param); - + if(length($HttpResponseErr) > 0) { Log3 $hash->{NAME}, 3, $hash->{NAME}.": Fehler beim abrufen der Daten von " .$TTS_Ressource. " Translator"; - Log3 $hash->{NAME}, 3, $hash->{NAME}.": " . $HttpResponseErr; + Log3 $hash->{NAME}, 3, $hash->{NAME}.": " . $HttpResponseErr; } $fh = new IO::File ">$file"; @@ -886,7 +898,7 @@ sub Text2Speech_Download($$$) { } $fh->print($HttpResponse); - Log3 $hash->{NAME}, 4, $hash->{NAME}.": Schreibe mp3 in die Datei $file mit ".length($HttpResponse)." Bytes"; + Log3 $hash->{NAME}, 4, $hash->{NAME}.": Schreibe mp3 in die Datei $file mit ".length($HttpResponse)." Bytes"; close($fh); } elsif ($TTS_Ressource eq "ESpeak") { @@ -895,20 +907,20 @@ sub Text2Speech_Download($$$) { $cmd = "sudo espeak -vde+f3 -k5 -s150 \"" . $text . "\" -w \"" . $FileWav . "\""; Log3 $hash, 4, $hash->{NAME}.":" .$cmd; system($cmd); - - $cmd = "lame \"" . $FileWav . "\" \"" . $file . "\""; + + $cmd = "lame \"" . $FileWav . "\" \"" . $file . "\""; Log3 $hash, 4, $hash->{NAME}.":" .$cmd; system($cmd); unlink $FileWav; } elsif ($TTS_Ressource eq "SVOX-pico") { my $FileWav = $file . ".wav"; - + $cmd = "pico2wave --lang=" . $TTS_Language . " --wave=\"" . $FileWav . "\" \"" . $text . "\""; Log3 $hash, 4, $hash->{NAME}.":" .$cmd; system($cmd); - - $cmd = "lame \"" . $FileWav . "\" \"" . $file . "\""; + + $cmd = "lame \"" . $FileWav . "\" \"" . $file . "\""; Log3 $hash, 4, $hash->{NAME}.":" .$cmd; system($cmd); unlink $FileWav; @@ -975,7 +987,7 @@ sub Text2Speech_DoIt($) { my @Mp3WrapFiles; my @Mp3WrapText; - + $TTS_SentenceAppendix = $myFileTemplateDir ."/". $TTS_SentenceAppendix if($TTS_SentenceAppendix); undef($TTS_SentenceAppendix) if($TTS_SentenceAppendix && (! -e $TTS_SentenceAppendix)); @@ -987,17 +999,17 @@ sub Text2Speech_DoIt($) { $filename = $t; $file = $filename; Log3 $hash->{NAME}, 4, $hash->{NAME}.": $filename als direkte MP3 Datei erkannt!"; - } elsif(-e $TTS_CacheFileDir."/".$t) { + } elsif(-e $TTS_CacheFileDir."/".$t) { # falls eine bestimmte mp3-Datei mit relativem Pfad gespielt werden soll $filename = $t; $file = $TTS_CacheFileDir."/".$filename; Log3 $hash->{NAME}, 4, $hash->{NAME}.": $filename als direkte MP3 Datei erkannt!"; } else { - $filename = md5_hex($language{$TTS_Ressource}{$TTS_Language} ."|". $t) . ".mp3"; + $filename = md5_hex($TTS_Ressource ."|". $t) . ".mp3"; $file = $TTS_CacheFileDir."/".$filename; Log3 $hash->{NAME}, 4, $hash->{NAME}.": Textbaustein ist keine direkte MP3 Datei, ermittle MD5 CacheNamen: $filename"; - } - + } + if(-e $file) { push(@Mp3WrapFiles, $file); push(@Mp3WrapText, $t); @@ -1021,7 +1033,7 @@ sub Text2Speech_DoIt($) { } push(@Mp3WrapFiles, $TTS_SentenceAppendix) if($TTS_SentenceAppendix); - + if (AttrVal($hash->{NAME}, "TTS_UseMP3Wrap", 0) == 1) { # benutze das Tool MP3Wrap um bereits einzelne vorhandene Sprachdateien # zusammenzuführen. Ziel: sauberer Sprachfluss @@ -1070,7 +1082,7 @@ sub Text2Speech_DoIt($) { rename($Mp3WrapFile, $TTS_OutputFile); $Mp3WrapFile = $TTS_OutputFile; } - + if ($hash->{MODE} ne "SERVER") { # im Server Mode, nicht die Datei abspielen if(-e $Mp3WrapFile) { @@ -1084,21 +1096,21 @@ sub Text2Speech_DoIt($) { } } - return $hash->{NAME} ."|". - ($TTS_SentenceAppendix ? scalar(@Mp3WrapFiles)-1: scalar(@Mp3WrapFiles)) ."|". + return $hash->{NAME} ."|". + ($TTS_SentenceAppendix ? scalar(@Mp3WrapFiles)-1: scalar(@Mp3WrapFiles)) ."|". $Mp3WrapFile; } - Log3 $hash->{NAME}, 4, $hash->{NAME}.": Bearbeite jetzt den Text: ". $hash->{helper}{Text2Speech}[0]; - + Log3 $hash->{NAME}, 4, $hash->{NAME}.": Bearbeite jetzt den Text: ". $hash->{helper}{Text2Speech}[0]; + if(! -e $file) { # Datei existiert noch nicht im Cache Text2Speech_Download($hash, $file, $hash->{helper}{Text2Speech}[0]); } else { Log3 $hash->{NAME}, 4, $hash->{NAME}.": $file gefunden, kein Download"; } - if(-e $file && $hash->{MODE} ne "SERVER") { + if(-e $file && $hash->{MODE} ne "SERVER") { # Datei existiert jetzt # im Falls Server, nicht die Datei abspielen $cmd = Text2Speech_BuildMplayerCmdString($hash, $file); @@ -1108,7 +1120,7 @@ sub Text2Speech_DoIt($) { system($cmd); } - return $hash->{NAME}. "|". + return $hash->{NAME}. "|". "1" ."|". $file; } @@ -1127,14 +1139,14 @@ sub Text2Speech_Done($) { my $hash = $defs{shift(@a)}; my $tts_done = shift(@a); my $filename = shift(@a); - + my $TTS_TimeOut = AttrVal($hash->{NAME}, "TTS_TimeOut", 60); if($filename) { my @text; - for(my $i=0; $i<$tts_done; $i++) { + for(my $i=0; $i<$tts_done; $i++) { push(@text, $hash->{helper}{Text2Speech}[$i]); - } + } Text2Speech_WriteStats($hash, 1, $filename, join(" ", @text)) if (AttrVal($hash->{NAME},"TTS_noStatisticsLog", "0")==0); readingsSingleUpdate($hash, "lastFilename", $filename, 1); @@ -1146,7 +1158,7 @@ sub Text2Speech_Done($) { # erneutes aufrufen da ev. weiterer Text in der Warteschlange steht if(@{$hash->{helper}{Text2Speech}} > 0) { # es wurde nur ein Teil abgearbeitet - Log3($hash,4, $hash->{NAME}.": Es wurde nur ein Teil ausgegeben und weitere Teile folgen!"); + Log3($hash,4, $hash->{NAME}.": Es wurde nur ein Teil ausgegeben und weitere Teile folgen!"); $hash->{helper}{RUNNING_PID} = BlockingCall("Text2Speech_DoIt", $hash, "Text2Speech_Done", $TTS_TimeOut, "Text2Speech_AbortFn", $hash); } else { @@ -1158,7 +1170,7 @@ sub Text2Speech_Done($) { } ##################################### -sub Text2Speech_AbortFn($) { +sub Text2Speech_AbortFn($) { my ($hash) = @_; delete($hash->{helper}{RUNNING_PID}); @@ -1168,7 +1180,7 @@ sub Text2Speech_AbortFn($) { ##################################### # Hiermit werden Statistken per DbLogModul gesammelt -# Wichitg zur Entscheidung welche Dateien aus dem Cache lange +# Wichitg zur Entscheidung welche Dateien aus dem Cache lange # nicht benutzt und somit gelöscht werden koennen. # # param1: hash @@ -1186,14 +1198,14 @@ sub Text2Speech_WriteStats($$$$){ if($defs{$key}{TYPE} eq "DbLog") { $DbLogDev = $key; last; - } + } } return undef if($defs{$DbLogDev}{STATE} !~ m/(active|connected)/); # muss active sein! return undef if(AttrVal($defs{$DbLogDev}, "DbLogType", "History") !~ /Current/); # muss die Tabelle Current nutzen my $logdevice = $hash->{NAME} ."|". $file; # den letzten Value von "Usage" ermitteln um dann die Statistik um 1 zu erhoehen. - my @LastValue = DbLog_Get($defs{$DbLogDev}, "", "current", "array", "-", "-", $logdevice.":Usage"); + my @LastValue = DbLog_Get($defs{$DbLogDev}, "", "current", "array", "-", "-", $logdevice.":Usage"); my $NewValue = 1; $NewValue = $LastValue[0]{value} + 1 if($LastValue[0]); @@ -1203,7 +1215,7 @@ sub Text2Speech_WriteStats($$$$){ '".TimeNow()."','".$logdevice."','".$hash->{TYPE}."','".$text."','Usage','".$NewValue."','')"; } else { $cmd = "UPDATE current SET VALUE = '".$NewValue."', TIMESTAMP = '".TimeNow()."' WHERE DEVICE ='".$logdevice."'"; - } + } DbLog_ExecSQL($defs{$DbLogDev}, $cmd); } @@ -1211,35 +1223,41 @@ sub Text2Speech_WriteStats($$$$){ =pod =item helper -=item summary speaks given text via loudspeaker -=item summary_DE wandelt Text in Sprache um zur Ausgabe auf Lautsprecher +=item summary A module that converts text to speech and also plays \ +the result on a local or remote loudspeaker + +=item summary_DE Modul, das Text in Sprache umwandelt und das Ergebnis \ +über einen lokalen oder entfernten Lautsprecher wiedergibt =begin html -

Text2Speech

+

Text2Speech