############################################## # $Id$ # # 98_Text2Speech.pm # # written by Tobias Faust 2013-10-23 # e-mail: tobias dot faust at gmx dot net # ############################################## ############################################## # EDITOR=nano # visudo # ALL ALL = NOPASSWD: /usr/bin/mplayer ############################################## # VoiceRSS: http://www.voicerss.org/api/documentation.aspx package main; use strict; use warnings; use Blocking; use HttpUtils; # use Data::Dumper; use lib ('./FHEM/lib', './lib'); sub Text2Speech_OpenDev($); sub Text2Speech_CloseDev($); # SetParamName -> Anzahl Paramter my %sets = ( "tts" => "1", "volume" => "1" ); # path to mplayer my $mplayer = 'sudo /usr/bin/mplayer'; my $mplayerOpts = '-nolirc -noconsolecontrols'; my $mplayerNoDebug = '-really-quiet'; my $mplayerAudioOpts = '-ao alsa:device='; my %ttsHost = ("Google" => "translate.google.com", "VoiceRSS" => "api.voicerss.org" ); my %ttsLang = ("Google" => "tl=", "VoiceRSS" => "hl=" ); my %ttsQuery = ("Google" => "q=", "VoiceRSS" => "src=" ); my %ttsPath = ("Google" => "/translate_tts?", "VoiceRSS" => "/?" ); my %ttsAddon = ("Google" => "client=tw-ob&ie=UTF-8", "VoiceRSS" => "" ); my %ttsAPIKey = ("Google" => "", # kein APIKey nötig "VoiceRSS" => "key=" ); my %ttsUser = ("Google" => "", # kein Username nötig "VoiceRSS" => "" # kein Username nötig ); my %ttsSpeed = ("Google" => "", "VoiceRSS" => "r=" ); my %ttsQuality = ("Google" => "", "VoiceRSS" => "f=" ); my %ttsMaxChar = ("Google" => 100, "VoiceRSS" => 300, "SVOX-pico" => 1000, "Amazon-Polly" => 3000 ); my %language = ("Google" => { "Deutsch" => "de", "English-US" => "en-us", "Schwedisch" => "sv", "France" => "fr", "Spain" => "es", "Italian" => "it", "Chinese" => "cn", "Dutch" => "nl" }, "VoiceRSS" => { "Deutsch" => "de-de", "English-US" => "en-us", "Schwedisch" => "sv-se", "France" => "fr-fr", "Spain" => "es-es", "Italian" => "it-it", "Chinese" => "zh-cn", "Dutch" => "nl-nl" }, "SVOX-pico" => { "Deutsch" => "de-DE", "English-US" => "en-US", "France" => "fr-FR", "Spain" => "es-ES", "Italian" => "it-IT" }, "Amazon-Polly"=> {"Deutsch" => "Marlene", "English-US" => "Joanna", "Schwedisch" => "Astrid", "France" => "Celine", "Spain" => "Conchita", "Italian" => "Carla", "Chinese" => "Zhiyu", "Dutch" => "Lotte" } ); ########################## sub Text2Speech_Initialize($) { my ($hash) = @_; $hash->{WriteFn} = "Text2Speech_Write"; $hash->{ReadyFn} = "Text2Speech_Ready"; $hash->{DefFn} = "Text2Speech_Define"; $hash->{SetFn} = "Text2Speech_Set"; $hash->{UndefFn} = "Text2Speech_Undefine"; $hash->{AttrFn} = "Text2Speech_Attr"; $hash->{AttrList} = "disable:0,1". " TTS_Delimiter". " TTS_Ressource:ESpeak,SVOX-pico,Amazon-Polly,". join(",", sort keys %ttsHost). " TTS_APIKey". " TTS_User". " TTS_Quality:". "48khz_16bit_stereo,". "48khz_16bit_mono,". "48khz_8bit_stereo,". "48khz_8bit_mono". "44khz_16bit_stereo,". "44khz_16bit_mono,". "44khz_8bit_stereo,". "44khz_8bit_mono". "32khz_16bit_stereo,". "32khz_16bit_mono,". "32khz_8bit_stereo,". "32khz_8bit_mono". "24khz_16bit_stereo,". "24khz_16bit_mono,". "24khz_8bit_stereo,". "24khz_8bit_mono". "22khz_16bit_stereo,". "22khz_16bit_mono,". "22khz_8bit_stereo,". "22khz_8bit_mono". "16khz_16bit_stereo,". "16khz_16bit_mono,". "16khz_8bit_stereo,". "16khz_8bit_mono". "8khz_16bit_stereo,". "8khz_16bit_mono,". "8khz_8bit_stereo,". "8khz_8bit_mono". " TTS_Speed:-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10". " TTS_TimeOut". " TTS_CacheFileDir". " TTS_UseMP3Wrap:0,1". " TTS_MplayerCall". " TTS_SentenceAppendix". " TTS_FileMapping". " TTS_FileTemplateDir". " TTS_VolumeAdjust". " TTS_noStatisticsLog:1,0". " TTS_Language:".join(",", sort keys %{$language{"Google"}}). " TTS_Language_Custom". " TTS_SpeakAsFastAsPossible:1,0". " TTS_OutputFile". " ".$readingFnAttributes; } ########################## # Define Text2Speech # Define Text2Speech host[:port][:SSL] [portpassword] ########################## sub Text2Speech_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t]+", $def); #$a[0]: Name #$a[1]: Type/Alias -> Text2Speech #$a[2]: definition #$a[3]: optional: portpasswd if(int(@a) < 3) { my $msg = "wrong syntax: define Text2Speech \n". "see at /etc/asound.conf\n". "or remote syntax: define Text2Speech host[:port][:SSL] [portpassword]"; Log3 $hash, 2, $msg; return $msg; } my $dev = $a[2]; if($dev =~ m/^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*/ ) { # Ein RemoteDevice ist angegeben # zb: 192.168.10.24:7272:SSL mypasswd if($dev =~ m/^(.*):SSL$/) { $dev = $1; $hash->{SSL} = 1; } if($dev !~ m/^.+:[0-9]+$/) { # host:port $dev = "$dev:7072"; } $hash->{Host} = $dev; $hash->{portpassword} = $a[3] if(@a == 4); $hash->{MODE} = "REMOTE"; } elsif (lc($dev) eq "none") { # Ein DummyDevice, Serverdevice. Nur Generierung der mp3 TTS Dateien $hash->{MODE} = "SERVER"; undef $hash->{ALSADEVICE}; } else { # Ein Alsadevice ist angegeben # pruefen, ob Alsa-Device in /etc/asound.conf definiert ist $hash->{MODE} = "DIRECT"; $hash->{ALSADEVICE} = $a[2]; } BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID})); delete($hash->{helper}{RUNNING_PID}); $hash->{STATE} = "Initialized"; my $ret = Text2Speech_loadmodules($hash, ""); if ($ret) { Log3 $hash->{NAME}, 3, $ret; } return undef; } ########################## # Überprüfung und Einladen der notwendigen Module ########################## sub Text2Speech_loadmodules($$) { my ($hash, $TTS_Ressource) = @_; eval { require IO::File; IO::File->import; 1; } 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"; eval { require URI::Escape; URI::Escape->import; 1; } 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"; eval { require Encode::Guess; Encode::Guess->import; 1; } 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"; if ($TTS_Ressource eq "Amazon-Polly") { # Module werden nur benötigt mit der Polly Engine eval { require Paws::Polly; Paws::Polly->import; 1; } or return "Paws Module not installed. Please install, goto https://metacpan.org/source/JLMARTIN/Paws-0.39"; eval { require File::HomeDir; File::HomeDir->import; 1; } or return "File::HomeDir Module not installed. Please install"; } return undef; } ##################################### sub Text2Speech_Undefine($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash); BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID})); Text2Speech_CloseDev($hash); return undef; } sub Text2Speech_Attr(@) { my @a = @_; my $do = 0; my $hash = $defs{$a[1]}; my $value = $a[3]; my $TTS_FileTemplateDir = AttrVal($hash->{NAME}, "TTS_FileTemplateDir", "templates"); my $TTS_CacheFileDir = AttrVal($hash->{NAME}, "TTS_CacheFileDir", "cache"); my $TTS_FileMapping = AttrVal($hash->{NAME}, "TTS_FileMapping", ""); # zb, silence:silence.mp3 ring:myringtone.mp3; if($a[2] eq "TTS_Delimiter" && $a[0] ne "del") { return "wrong Delimiter syntax: [+-]a[lfn]. Please see CommandRef for Notation. \n". " Example 1: +an~\n". " Example 2: +al." if($value !~ m/^([+-]a[lfn]){0,1}(.){1}$/i); return "This Attribute is only available in direct or server mode" if($hash->{MODE} !~ m/(DIRECT|SERVER)/ ); } elsif ($a[2] eq "TTS_Ressource" && $value eq "Amazon-Polly") { Log3 $hash->{NAME}, 4, $hash->{NAME}. ": Wechsele auf Amazon Polly, Lade Librarys nach."; my $ret = Text2Speech_loadmodules($hash, $a[2]); if ($ret) {return $ret;} # breche ab wenn Module fehlen if (! -e File::HomeDir->my_home."/.aws/credentials"){ return "No AWS credentials in FHEM Homedir found, please check ".File::HomeDir->my_home."/.aws/credentials
please refer https://metacpan.org/pod/Paws#AUTHENTICATION"; } } elsif ($a[2] eq "TTS_Ressource") { return "This Attribute is only available in direct or server mode" if($hash->{MODE} !~ m/(DIRECT|SERVER)/ ); } elsif ($a[2] eq "TTS_CacheFileDir") { return "This Attribute is only available in direct or server mode" if($hash->{MODE} !~ m/(DIRECT|SERVER)/ ); } elsif ($a[2] eq "TTS_SpeakAsFastAsPossible") { return "This Attribute is only available in direct or server mode" if($hash->{MODE} !~ m/(DIRECT|SERVER)/ ); } elsif ($a[2] eq "TTS_UseMP3Wrap") { return "This Attribute is only available in direct or server mode" if($hash->{MODE} !~ m/(DIRECT|SERVER)/ ); return "Attribute TTS_UseMP3Wrap is required by Attribute TTS_SentenceAppendix! Please delete it first." if(($a[0] eq "del") && (AttrVal($hash->{NAME}, "TTS_SentenceAppendix", undef))); } elsif ($a[2] eq "TTS_SentenceAppendix") { return "This Attribute is only available in direct or server mode" if($hash->{MODE} !~ m/(DIRECT|SERVER)/ ); return "Attribute TTS_UseMP3Wrap is required!" unless(AttrVal($hash->{NAME}, "TTS_UseMP3Wrap", undef)); my $file = $TTS_CacheFileDir ."/". $value; return "File <".$file."> does not exists in CacheFileDir" if(! -e $file); } elsif ($a[2] eq "TTS_FileTemplateDir") { # Verzeichnis beginnt mit /, dann absoluter Pfad, sonst Unterpfad von $TTS_CacheFileDir my $newDir; if($value =~ m/^\/.*/) { $newDir = $value; } else { $newDir = $TTS_CacheFileDir ."/". $value;} unless(-e ($newDir) or mkdir ($newDir)) { #Verzeichnis anlegen gescheitert return "Could not create directory: <$value>"; } } elsif ($a[0] eq "set" && $a[2] eq "TTS_TimeOut") { return "Only Numbers allowed" if ($value !~ m/[0-9]+/); } elsif ($a[2] eq "TTS_FileMapping") { #Bsp: silence:silence.mp3 pling:mypling,mp3 #ueberpruefen, ob mp3 Template existiert my @FileTpl = split(" ", $TTS_FileMapping); my $newDir; for(my $j=0; $j<(@FileTpl); $j++) { my @FileTplPc = split(/:/, $FileTpl[$j]); if($TTS_FileTemplateDir =~ m/^\/.*/) { $newDir = $TTS_FileTemplateDir; } else { $newDir = $TTS_CacheFileDir ."/". $TTS_FileTemplateDir;} return "file does not exist: <".$newDir ."/". $FileTplPc[1] .">" unless (-e $newDir ."/". $FileTplPc[1]); } } if($a[0] eq "set" && $a[2] eq "disable") { $do = (!defined($a[3]) || $a[3]) ? 1 : 2; } $do = 2 if($a[0] eq "del" && (!$a[2] || $a[2] eq "disable")); return if(!$do); $hash->{STATE} = ($do == 1 ? "disabled" : "Initialized"); return undef; } ##################################### sub Text2Speech_Ready($) { my ($hash) = @_; return Text2speech_OpenDev($hash, 1); } ######################## sub Text2Speech_OpenDev($) { my ($hash) = @_; my $dev = $hash->{Host}; my $name = $hash->{NAME}; Log3 $name, 4, "Text2Speech opening $name at $dev"; my $conn; if($hash->{SSL}) { eval "use IO::Socket::SSL"; Log3 $name, 1, $@ if($@); $conn = IO::Socket::SSL->new(PeerAddr => "$dev", MultiHomed => 1) if(!$@); } else { $conn = IO::Socket::INET->new(PeerAddr => $dev, MultiHomed => 1); } if(!$conn) { Log3($name, 3, $hash->{NAME}.": Can't connect to $dev: $!"); $hash->{STATE} = "disconnected"; return ""; } else { $hash->{STATE} = "Initialized"; } $hash->{TCPDev} = $conn; $hash->{FD} = $conn->fileno(); Log3 $name, 4, "Text2Speech device opened ($name)"; syswrite($hash->{TCPDev}, $hash->{portpassword} . "\n") if($hash->{portpassword}); return undef; } ######################## sub Text2Speech_CloseDev($) { my ($hash) = @_; my $name = $hash->{NAME}; my $dev = $hash->{Host}; return if(!$dev); if($hash->{TCPDev}) { $hash->{TCPDev}->close(); Log3 $hash, 4, "Text2speech Device closed ($name)"; } delete($hash->{TCPDev}); delete($hash->{FD}); } ######################## sub Text2Speech_Write($$) { my ($hash,$msg) = @_; my $name = $hash->{NAME}; my $dev = $hash->{Host}; #my $call = "set tts tts Das ist ein Test."; my $call = "set $name $msg"; #Prüfen ob PRESENCE vorhanden und present my $isPresent = 0; my $hasPRESENCE = 0; my $devname=""; if ($hash->{MODE} eq "REMOTE") { foreach $devname (devspec2array("TYPE=PRESENCE")) { if (defined $defs{$devname}->{ADDRESS} && $dev) { if ($dev =~ $defs{$devname}->{ADDRESS}) { $hasPRESENCE = 1; $isPresent = 1 if (ReadingsVal($devname,"presence","unknown") eq "present"); last; } } } } if ($hasPRESENCE) { Log3 $hash, 4, $name.": found PRESENCE Device $devname for host: $dev, it\'s state is: ".($isPresent ? "present" : "absent"); Text2Speech_OpenDev($hash) if(!$hash->{TCPDev} && $isPresent); #lets try again Text2Speech_OpenDev($hash) if(!$hash->{TCPDev} && $isPresent); } else { Log3 $hash, 4, $name.": no proper PRESENCE Device for host: $dev"; Text2Speech_OpenDev($hash) if(!$hash->{TCPDev}); #lets try again Text2Speech_OpenDev($hash) if(!$hash->{TCPDev}); } if($hash->{TCPDev}) { Log3 $hash, 4, $name.": Write remote message to $dev: $call"; Log3 $hash, 3, $name.": Could not write remote message ($call) at " .$hash->{Host} if(!defined(syswrite($hash->{TCPDev}, "$call\n"))); Text2Speech_CloseDev($hash); } } ########################################################################### sub Text2Speech_Set($@) { my ($hash, @a) = @_; my $me = $hash->{NAME}; my $TTS_APIKey = AttrVal($hash->{NAME}, "TTS_APIKey", undef); my $TTS_User = AttrVal($hash->{NAME}, "TTS_User", undef); my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google"); my $TTS_TimeOut = AttrVal($hash->{NAME}, "TTS_TimeOut", 60); 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)); my $ret = Text2Speech_loadmodules($hash, $TTS_Ressource); if ($ret) { # breche ab wenn Module fehlen Log3 $me, 3, $ret; return $ret; } my $cmd = shift(@a); # Dummy $cmd = shift(@a); # DevName if(!defined($sets{$cmd})) { my $r = "Unknown argument $cmd, choose one of ".join(" ",sort keys %sets); return $r; } if($cmd ne "tts") { return "$cmd needs $sets{$cmd} parameter(s)" if(@a-$sets{$cmd} != 0); } # Abbruch falls Disabled return "no set cmd on a disabled device !" if(IsDisabled($me)); if($cmd eq "tts") { if($hash->{MODE} eq "DIRECT" || $hash->{MODE} eq "SERVER") { $hash->{VOLUME} = ReadingsNum($me, "volume", 100); readingsSingleUpdate($hash, "playing", "1", 1); Text2Speech_PrepareSpeech($hash, join(" ", @a)); $hash->{helper}{RUNNING_PID} = BlockingCall("Text2Speech_DoIt", $hash, "Text2Speech_Done", $TTS_TimeOut, "Text2Speech_AbortFn", $hash) unless(exists($hash->{helper}{RUNNING_PID})); } elsif ($hash->{MODE} eq "REMOTE") { Text2Speech_Write($hash, "tts " . join(" ", @a)); } else {return undef;} } elsif($cmd eq "volume") { my $vol = join(" ", @a); return "volume level expects 0..100 percent" if($vol !~ m/^([0-9]{1,3})$/ or $vol > 100); if($hash->{MODE} eq "DIRECT") { $hash->{VOLUME} = $vol if($vol <= 100); delete($hash->{VOLUME}) if($vol > 100); } elsif ($hash->{MODE} eq "REMOTE") { Text2Speech_Write($hash, "volume $vol"); } else {return undef;} readingsSingleUpdate($hash, "volume", (($vol>100)?0:$vol), 1); } return undef; } ##################################### # Bereitet den gesamten String vor. # Bei Nutzung Google wird dieser in ein Array # zerlegt mit jeweils einer maximalen # Stringlänge von 100Chars # # param1: $hash # param2: string to speech # ##################################### ################################### # Angabe des Delimiters: zb.: +af~ # + -> erzwinge das Trennen, auch wenn Textbaustein < 100Zeichen # - -> Trenne nur wenn Textbaustein > 100Zeichen # af -> add first -> füge den Delimiter am Satzanfang wieder hinzu # al -> add last -> füge den Delimiter am Satzende wieder hinzu # an -> add nothing -> Delimiter nicht wieder hinzufügen # ~ -> der Delimiter ################################### sub Text2Speech_PrepareSpeech($$) { my ($hash, $t) = @_; my $me = $hash->{NAME}; my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google"); my $TTS_Delimiter = AttrVal($hash->{NAME}, "TTS_Delimiter", undef); my $TTS_FileTpl = AttrVal($hash->{NAME}, "TTS_FileMapping", ""); # zb, silence:silence.mp3 ring:myringtone.mp3; im Text: mein Klingelton :ring: ist laut. my $TTS_FileTemplateDir = AttrVal($hash->{NAME}, "TTS_FileTemplateDir", "templates"); my $TTS_ForceSplit = 0; my $TTS_AddDelimiter; 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 "-"); $TTS_AddDelimiter = substr($TTS_Delimiter,1,2); # af, al oder an $TTS_Delimiter = substr($TTS_Delimiter,3); } elsif (!$TTS_Delimiter) { # Default wenn Attr nicht gesetzt $TTS_Delimiter = "(?<=[\\.!?])\\s*"; $TTS_ForceSplit = 0; $TTS_AddDelimiter = ""; } #-- we may have problems with umlaut characters # ersetze Sonderzeichen die Google nicht auflösen kann my $converter; # wandle per standard alles nach UTF8 # check only ascii, utf8 and UTF-(16|32) with BOM, if not enough use function set_suspects # Encode::Guess->set_suspects(qw/euc-jp shiftjis 7bit-jis/); # for japanese codepages my $enc = guess_encoding($t); if ($enc->name ne "utf8") { Log3 $hash, 4, "$me: ermittelte CodePage: " .$enc->name. " , konvertiere nach UTF-8"; $converter = Text::Iconv->new($enc->name, "utf-8"); $t = $converter->convert($t); } #if($TTS_Ressource eq "Google") { # Google benötigt UTF-8 # $t =~ s/ä/ae/g; # $t =~ s/ö/oe/g; # $t =~ s/ü/ue/g; # $t =~ s/Ä/Ae/g; # $t =~ s/Ö/Oe/g; # $t =~ s/Ü/Ue/g; # $t =~ s/ß/ss/g; #} if ($TTS_Ressource eq "Amazon-Polly") { # Amazon benötigt ISO-8859-1 bei Nutzung Region eu-central-1 $converter = Text::Iconv->new("utf-8", "iso-8859-1"); $t = $converter->convert($t); } my @text; push(@text, $t); # hole alle Filetemplates my @FileTpl = split(" ", $TTS_FileTpl); my @FileTplPc; # bei Angabe direkter MP3-Files wird hier ein temporäres Template vergeben for(my $i=0; $i<(@text); $i++) { @FileTplPc = ($text[$i] =~ /:(\w+?\.(?:mp3|ogg|wav)):/g); for(my $j=0; $j<(@FileTplPc); $j++) { my $tpl = "FileTpl_#".$i."_".$j; #eindeutige Templatedefinition schaffen Log3 $hash, 4, "$me: Angabe einer direkten MP3-Datei gefunden: $FileTplPc[$j] => $tpl"; push(@FileTpl, $tpl.":".$FileTplPc[$j]); #zb: FileTpl_123645875_#0:/ring.mp3 $text[$i] =~ s/$FileTplPc[$j]/$tpl/g; # Ersetze die DateiDefinition gegen ein Template } } #iteriere durch die Sprachbausteine und splitte den Text bei den Filetemplates auf for(my $i=0; $i<(@text); $i++) { my $cutter = '#!#'; #eindeutigen Cutter als Delimiter bei den Filetemplates vergeben @FileTplPc = ($text[$i] =~ /:([^:]+):/g); for(my $j=0; $j<(@FileTplPc); $j++) { $text[$i] =~ s/:$FileTplPc[$j]:/$cutter$FileTplPc[$j]$cutter/g; } @text = Text2Speech_SplitString(\@text, 0, $cutter, 1, ""); } Log3 $hash, 4, "$me: MaxChar = $ttsMaxChar{$TTS_Ressource}, Delimiter = $TTS_Delimiter, ForceSplit = $TTS_ForceSplit, AddDelimiter = $TTS_AddDelimiter"; @text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, $TTS_Delimiter, $TTS_ForceSplit, $TTS_AddDelimiter); @text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, "(?<=[.!?])\\s*", 0, ""); @text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, ",", 0, "al"); @text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, ";", 0, "al"); @text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, "und", 0, "af"); @text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, ":", 0, "al"); @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:"; for(my $i=0; $i<(@text); $i++) { # entferne führende und abschließende Leerzeichen aus jedem Textbaustein $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]; } push( @{$hash->{helper}{Text2Speech}}, @text ); } ##################################### # param1: array : Text 2 Speech # param2: string: MaxChar # param3: string: Delimiter # param4: int : 1 -> es wird am Delimiter gesplittet # 0 -> es wird nur gesplittet, wenn Stringlänge länger als MaxChar # param5: string: Add Delimiter to String? [al|af|] (AddLast/AddFirst) # # 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 # String wieder angefügt ##################################### sub Text2Speech_SplitString($$$$$){ my @text = @{shift()}; my $MaxChar = shift; my $Delimiter = shift; my $ForceSplit = shift; my $AddDelimiter = shift; my @newText; for(my $i=0; $i<(@text); $i++) { if((length($text[$i]) <= $MaxChar) && (!$ForceSplit)) { #Google kann nur 100zeichen push(@newText, $text[$i]); next; } my @b; if($Delimiter =~/^ $/) { @b = split(' ', $text[$i]); } else { @b = split(/$Delimiter/, $text[$i]); } if((@b)>1) { # setze zu kleine Textbausteine wieder zusammen bis MaxChar erreicht ist if(length($Delimiter)==1) { for(my $k=0; $k<(@b); ) { if($k+1<(@b) && length($b[$k])+length($b[$k+1]) <= $MaxChar) { $b[$k] = join($Delimiter, $b[$k], $b[$k+1]); splice(@b, $k+1, 1); } else { $k++; } } } for(my $j=0; $j<(@b); $j++) { (my $boundaryDelimiter = $Delimiter) =~ s/^\\b(.+)\\b$/$1/g; $b[$j] = $b[$j] . $boundaryDelimiter if($AddDelimiter eq "al"); # Am Satzende wieder hinzufügen. $b[$j+1] = $boundaryDelimiter . $b[$j+1] if(($AddDelimiter eq "af") && ($b[$j+1])); # Am Satzanfang des nächsten Satzes wieder hinzufügen. push(@newText, $b[$j]); } } elsif((@b)==1) { push(@newText, $text[$i]); } } return @newText; } ##################################### # param1: hash : Hash # param2: string: Datei # # Erstellt den Commandstring für den Systemaufruf ##################################### sub Text2Speech_BuildMplayerCmdString($$) { my ($hash, $file) = @_; my $cmd; my $TTS_MplayerCall = AttrVal($hash->{NAME}, "TTS_MplayerCall", $mplayer); my $TTS_VolumeAdjust = AttrVal($hash->{NAME}, "TTS_VolumeAdjust", 110); my $verbose = AttrVal($hash->{NAME}, "verbose", 3); if($hash->{VOLUME}) { # per: set volume <..> $mplayerOpts .= " -softvol -softvol-max ". $TTS_VolumeAdjust ." -volume " . $hash->{VOLUME}; } my $AlsaDevice = $hash->{ALSADEVICE}; if($AlsaDevice eq "default") { $AlsaDevice = ""; $mplayerAudioOpts = ""; } 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; $cmd = $TTS_MplayerCall; } else { $cmd = $TTS_MplayerCall . " " . $mplayerAudioOpts . $AlsaDevice . " " .$NoDebug. " " . $mplayerOpts . " " . $file; } 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; } ##################################### # Benutzt um Infos aus dem Blockingprozess # in die Readings zu schreiben ##################################### sub Text2Speech_readingsSingleUpdateByName($$$) { my ($devName, $readingName, $readingVal) = @_; my $hash = $defs{$devName}; Log3 $hash, 5, $hash->{NAME}.": readingsSingleUpdateByName: Dev:$devName Reading:$readingName Val:$readingVal"; readingsSingleUpdate($hash, $readingName, $readingVal, 1); } ##################################### # 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 ##################################### sub Text2Speech_CalcMP3Duration($$) { my $time; my ($hash, $file) = @_; eval { my $tag = get_mp3info($file); if ($tag && defined($tag->{SECS})) { $time = int($tag->{SECS}+0.5); 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; } return $time; } ##################################### # param1: hash : Hash # param2: string: Dateiname # param2: string: Text # # Holt den Text mithilfe der entsprechenden TTS_Ressource ##################################### sub Text2Speech_Download($$$) { my ($hash, $file, $text) = @_; my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google"); my $TTS_User = AttrVal($hash->{NAME}, "TTS_User", ""); my $TTS_APIKey = AttrVal($hash->{NAME}, "TTS_APIKey", ""); my $TTS_Language = AttrVal($hash->{NAME}, "TTS_Language_Custom", $language{$TTS_Ressource}{AttrVal($hash->{NAME}, "TTS_Language", "Deutsch")}); my $TTS_Quality = AttrVal($hash->{NAME}, "TTS_Quality", ""); my $TTS_Speed = AttrVal($hash->{NAME}, "TTS_Speed", ""); my $cmd; Log3 $hash->{NAME}, 4, $hash->{NAME}.": Verwende ".$TTS_Ressource." Resource zur TTS-Generierung"; if($TTS_Ressource =~ m/(Google|VoiceRSS)/) { my $HttpResponse; my $HttpResponseErr; my $fh; my $url = "http://" . $ttsHost{$TTS_Ressource} . $ttsPath{$TTS_Ressource}; $url .= $ttsLang{$TTS_Ressource} . $TTS_Language; $url .= "&" . $ttsAddon{$TTS_Ressource} if(length($ttsAddon{$TTS_Ressource})>0); $url .= "&" . $ttsUser{$TTS_Ressource} . $TTS_User if(length($ttsUser{$TTS_Ressource})>0); $url .= "&" . $ttsAPIKey{$TTS_Ressource} . $TTS_APIKey if(length($ttsAPIKey{$TTS_Ressource})>0); $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 = { url => $url, timeout => 5, hash => $hash, # Muss gesetzt werden, damit die Callback funktion wieder $hash hat method => "GET" # Lesen von Inhalten #httpversion => "1.1", #header => "User-Agent:Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.172 Safari/537.22m" # Den Header gemäss abzufragender Daten ändern #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; } $fh = new IO::File ">$file"; if(!defined($fh)) { Log3 $hash->{NAME}, 2, $hash->{NAME}.": mp3 Datei <$file> konnte nicht angelegt werden."; return undef; } $fh->print($HttpResponse); Log3 $hash->{NAME}, 4, $hash->{NAME}.": Schreibe mp3 in die Datei $file mit ".length($HttpResponse)." Bytes"; close($fh); } elsif ($TTS_Ressource eq "ESpeak") { my $FileWav = $file . ".wav"; $cmd = "sudo espeak -vde+f3 -k5 -s150 \"" . $text . "\" -w \"" . $FileWav . "\""; Log3 $hash, 4, $hash->{NAME}.":" .$cmd; system($cmd); $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 . "\""; Log3 $hash, 4, $hash->{NAME}.":" .$cmd; system($cmd); unlink $FileWav; } elsif ($TTS_Ressource eq "Amazon-Polly") { # with awscli # aws polly synthesize-speech --output-format mp3 --voice-id Marlene --text '%text%' abc.mp3 #$cmd = "aws polly synthesize-speech --output-format json --speech-mark-types='[\"viseme\"]' --voice-id " . $TTS_Language . " --text '" . $text . "' " . $file; #Log3 $hash, 4, $hash->{NAME}.":" .$cmd; #system($cmd); my $fh; my $texttype = "text"; $texttype = "ssml" if($text =~ m/^.*<\/speak>$/); Log3 $hash->{NAME}, 4, $hash->{NAME}.": Folgender TextTyp wurde für ".$TTS_Ressource." erkannt: ".$texttype; my $polly = Paws->service('Polly', region => 'eu-central-1'); my $res = $polly->SynthesizeSpeech( VoiceId => $TTS_Language, Text => $text, TextType => $texttype, OutputFormat => 'mp3', ); $fh = new IO::File ">$file"; if(!defined($fh)) { Log3 $hash->{NAME}, 2, $hash->{NAME}.": mp3 Datei <$file> konnte nicht angelegt werden."; return undef; } $fh->print($res->AudioStream); Log3 $hash->{NAME}, 4, $hash->{NAME}.": Schreibe mp3 in die Datei $file mit ". $res->RequestCharacters ." Chars"; close($fh); } } ##################################### sub Text2Speech_DoIt($) { my ($hash) = @_; my $TTS_CacheFileDir = AttrVal($hash->{NAME}, "TTS_CacheFileDir", "cache"); my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google"); my $TTS_Language = AttrVal($hash->{NAME}, "TTS_Language", "Deutsch"); my $TTS_SentenceAppendix = AttrVal($hash->{NAME}, "TTS_SentenceAppendix", undef); #muss eine mp3-Datei sein, ohne Pfadangabe my $TTS_FileTemplateDir = AttrVal($hash->{NAME}, "TTS_FileTemplateDir", "templates"); my $TTS_OutputFile = AttrVal($hash->{NAME}, "TTS_OutputFile", undef); my $myFileTemplateDir; if($TTS_FileTemplateDir =~ m/^\/.*/) { $myFileTemplateDir = $TTS_FileTemplateDir; } else { $myFileTemplateDir = $TTS_CacheFileDir ."/". $TTS_FileTemplateDir;} my $verbose = AttrVal($hash->{NAME}, "verbose", 3); my $cmd; Log3 $hash->{NAME}, 4, $hash->{NAME}.": Verwende TTS Spracheinstellung: ".$TTS_Language; my $filename; my $file; unless(-e $TTS_CacheFileDir or mkdir $TTS_CacheFileDir) { #Verzeichnis anlegen gescheitert Log3 $hash->{NAME}, 2, $hash->{NAME}.": Angegebenes Verzeichnis $TTS_CacheFileDir konnte erstmalig nicht angelegt werden."; return undef; } my @Mp3WrapFiles; my @Mp3WrapText; $TTS_SentenceAppendix = $myFileTemplateDir ."/". $TTS_SentenceAppendix if($TTS_SentenceAppendix); undef($TTS_SentenceAppendix) if($TTS_SentenceAppendix && (! -e $TTS_SentenceAppendix)); #Abspielliste erstellen my $AnzahlDownloads = 0; foreach my $t (@{$hash->{helper}{Text2Speech}}) { if(-e $t) { # falls eine bestimmte mp3-Datei mit absolutem Pfad gespielt werden soll $filename = $t; $file = $filename; Log3 $hash->{NAME}, 4, $hash->{NAME}.": $filename als direkte MP3 Datei erkannt!"; } 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"; $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); } else { # es befindet sich noch Text zum Download in der Queue if (AttrVal($hash->{NAME}, "TTS_SpeakAsFastAsPossible", 0) == 0 || (AttrVal($hash->{NAME}, "TTS_SpeakAsFastAsPossible", 0) == 1 && $AnzahlDownloads == 0)) { # nur Download wenn kein TTS_SpeakAsFastAsPossible gesetzt ist oder der erste Download erfolgen soll Text2Speech_Download($hash, $file, $t); $AnzahlDownloads ++; if(-e $file) { push(@Mp3WrapFiles, $file); push(@Mp3WrapText, $t); } } else { last; } } last if (AttrVal($hash->{NAME}, "TTS_UseMP3Wrap", 0) == 0); # ohne mp3wrap darf nur ein Textbaustein verarbeitet werden } 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 Log3 $hash->{NAME}, 4, $hash->{NAME}.": Bearbeite per MP3Wrap jetzt den Text: ". join(" ", @Mp3WrapText); my $Mp3WrapFile; my $Mp3WrapPrefix; $Mp3WrapPrefix = md5_hex(join("|", @Mp3WrapFiles)); if ($TTS_OutputFile) { if ($TTS_OutputFile !~ m/^\//) { $TTS_OutputFile = $TTS_CacheFileDir ."/".$TTS_OutputFile; } Log3 $hash->{NAME}, 4, $hash->{NAME}.": Verwende fixen Dateinamen: $TTS_OutputFile"; $Mp3WrapFile = $TTS_OutputFile; unlink($Mp3WrapFile); } else { $Mp3WrapFile = $TTS_CacheFileDir ."/". $Mp3WrapPrefix . ".mp3"; } if (scalar(@Mp3WrapFiles) == 1) { # wenn nur eine Datei, dann wird diese genutzt $Mp3WrapFile = $Mp3WrapFiles[0]; } elsif(! -e $Mp3WrapFile) { $cmd = "mp3wrap " .$Mp3WrapFile. " " .join(" ", @Mp3WrapFiles); $cmd .= " >/dev/null" if($verbose < 5); Log3 $hash->{NAME}, 4, $hash->{NAME}.": " .$cmd; system($cmd); 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.";} } if ($TTS_OutputFile && $TTS_OutputFile ne $Mp3WrapFile) { Log3 $hash->{NAME}, 4, $hash->{NAME}.": Benenne Datei um von <".$Mp3WrapFile."> nach <".$TTS_OutputFile.">"; rename($Mp3WrapFile, $TTS_OutputFile); $Mp3WrapFile = $TTS_OutputFile; } if ($hash->{MODE} ne "SERVER") { # im Server Mode, nicht die Datei abspielen if(-e $Mp3WrapFile) { $cmd = Text2Speech_BuildMplayerCmdString($hash, $Mp3WrapFile); $cmd .= " >/dev/null" if($verbose < 5); Log3 $hash->{NAME}, 4, $hash->{NAME}.": " .$cmd; system($cmd); } else { Log3 $hash->{NAME}, 2, $hash->{NAME}.": Mp3Wrap Datei konnte nicht gefunden werden."; } } 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]; 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") { # Datei existiert jetzt # im Falls Server, nicht die Datei abspielen $cmd = Text2Speech_BuildMplayerCmdString($hash, $file); $cmd .= " >/dev/null" if($verbose < 5); Log3 $hash->{NAME}, 4, $hash->{NAME}.":" .$cmd; system($cmd); } return $hash->{NAME}. "|". "1" ."|". $file; } #################################################### # Rückgabe der Blockingfunktion # param1: HashName # param2: Anzahl der abgearbeiteten Textbausteine # param3: Dateiname der abgespielt wurde #################################################### sub Text2Speech_Done($) { my ($string) = @_; return unless(defined($string)); my @a = split("\\|",$string); 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++) { 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); } delete($hash->{helper}{RUNNING_PID}); splice(@{$hash->{helper}{Text2Speech}}, 0, $tts_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!"); $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); } } ##################################### sub Text2Speech_AbortFn($) { my ($hash) = @_; delete($hash->{helper}{RUNNING_PID}); Log3 $hash->{NAME}, 2, $hash->{NAME}.": BlockingCall for ".$hash->{NAME}." was aborted"; readingsSingleUpdate($hash, "playing", "0", 1); } ##################################### # Hiermit werden Statistken per DbLogModul gesammelt # Wichitg zur Entscheidung welche Dateien aus dem Cache lange # nicht benutzt und somit gelöscht werden koennen. # # param1: hash # param2: int: 0=indirekt (über mp3wrap); 1=direkt abgespielt # param3: string: Datei # param4: string: Text der als mp3 abgespielt wird ##################################### sub Text2Speech_WriteStats($$$$){ my($hash, $typ, $file, $text) = @_; my $DbLogDev; #suche ein DbLogDevice return undef unless($modules{"DbLog"} && $modules{"DbLog"}{"LOADED"}); foreach my $key (keys(%defs)) { 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 $NewValue = 1; $NewValue = $LastValue[0]{value} + 1 if($LastValue[0]); my $cmd; if ($NewValue == 1) { $cmd = "INSERT INTO current (TIMESTAMP, DEVICE, TYPE, EVENT, READING, VALUE, UNIT) VALUES (\ '".TimeNow()."','".$logdevice."','".$hash->{TYPE}."','".$text."','Usage','".$NewValue."','')"; } else { $cmd = "UPDATE current SET VALUE = '".$NewValue."', TIMESTAMP = '".TimeNow()."' WHERE DEVICE ='".$logdevice."'"; } DbLog_ExecSQL($defs{$DbLogDev}, $cmd); } 1; =pod =item helper =item summary speaks given text via loudspeaker =item summary_DE wandelt Text in Sprache um zur Ausgabe auf Lautsprecher =begin html

Text2Speech


    Define
      Local : define <name> Text2Speech <alsadevice>
      Remote: define <name> Text2Speech <host>[:<portnr>][:SSL] [portpassword]

      This module converts any text into speech with serveral possible providers. The Device can be defined as locally or remote device.

    • Local Device
        The output will be send to any connected audiodevice. For example external speakers connected per jack or with bluetooth speakers - connected per bluetooth dongle. Its important to install mplayer.
        apt-get install mplayer
        The given alsadevice has to be configured in /etc/asound.conf

        Special AlsaDevice: default
        The internal mplayer command will be without any audio directive if the given alsadevice is default. In this case mplayer is using the standard audiodevice.

        Example:
        define MyTTS Text2Speech hw=0.0
        define MyTTS Text2Speech default

    • Remote Device
        This module can configured as remote-device for client-server Environments. The Client has to be configured as local device.
        Notice: the Name of the locally instance has to be the same!
        • Host: setting up IP-adress
        • PortNr: setting up TelnetPort of FHEM; default: 7072
        • SSL: setting up if connect over SSL; default: no SSL
        • PortPassword: setting up the configured target telnet passwort

        Example:
        define MyTTS Text2Speech 192.168.178.10:7072 fhempasswd define MyTTS Text2Speech 192.168.178.10

        If a PRESENCE Device is avilable for the host IP-address, than this will be used to detect the reachability instead of the blocking internal method.
    • Server Device
        In case of an usage of an Server, only the mp3 file will be generated.It makes no sence to use the attribute TTS_speakAsFastAsPossible. Its recommend, to use the attribute TTS_useMP3Wrap. Otherwise only the last audiobrick will be shown is reading lastFilename.
Set
  • tts:
    Giving a text to translate into audio. You play set mp3-files directly. In this case you have to enclosure them with a single colon before and after the declaration. The files must save under the directory of given TTS_FileTemplateDir. Please note: The text doesn´t have any colons itself.
  • volume:
    Setting up the volume audio response.
    Notice: Only available in locally instances!

Get
    N/A

Attributes
  • TTS_Delimiter
    optional: By using the google engine, its not possible to convert more than 100 characters in a single audio brick. With a Delimiter the audio brick will be split at this character. A Delimiter must be a single character.!
    By default, ech audio brick will be split at sentence end. Is a single sentence longer than 100 characters, the sentence will be split additionally at comma, semicolon and the word and.
    Notice: Only available in locally instances with Google engine!
  • TTS_Ressource
    optional: Selection of the Translator Engine
    Notice: Only available in locally instances!
    • Google
      Using the Google Engine. It´s nessessary to have internet access. This engine is the recommend engine because the quality is fantastic. This engine is using by default.
    • VoiceRSS
      Using the VoiceRSS Engine. Its a free engine till 350 requests per day. If you need more, you have to pay. It´s nessessary to have internet access. This engine is the 2nd recommend engine because the quality is also fantastic. To use this engine you need an APIKey (see TTS_APIKey)
    • ESpeak
      Using the ESpeak Engine. Installation Espeak and lame is required.
      apt-get install espeak lame
    • SVOX-pico
      Using the SVOX-Pico TTS-Engine (from the AOSP).
      Installation of the engine and lame is required:
      sudo apt-get install libttspico-utils lame

      On ARM/Raspbian the package libttspico-utils,
      so you may have to compile it yourself or use the precompiled package from this guide, in short:
      sudo apt-get install libpopt-dev lame
      cd /tmp
      wget http://www.dr-bischoff.de/raspi/pico2wave.deb
      sudo dpkg --install pico2wave.deb
    • Amazon-Polly
      Using the Amazon Polly engine, the same as Amazon Alexa.
      The perl package Paws is required. An AWS Access and Polly Aws User is required too
      cpan paws
      The credentials to your AWS Polly are expected at ~/.aws/credentials
      [default] aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxx aws_access_key_id = xxxxxxxxxxxxxxx
  • TTS_Language
    Selection of different languages
  • TTS_Language_Custom
    if you want another engine and speech of default languages, you can insert this here.
    The definition is dependent of used engine. This attribute overrides an TTS_Language attribute.
    Please refer the specific API reference.
  • TTS_APIKey
    An APIKey its needed if you want to use VoiceRSS. You have to register at the following page:
    http://www.voicerss.org/registration.aspx
    After this, you will get your personal APIKey.
  • TTS_User
    Actual without any usage. Needed in case if a TTS Engine need an username and an apikey for each request.
  • TTS_CacheFileDir
    optional: The downloaded Goole audio bricks are saved in this folder for reusing. No automatically implemented deleting are available.
    Default: cache/
    Notice: Only available in locally instances!
  • TTS_UseMP3Wrap
    optional: To become a liquid audio response its recommend to use the tool mp3wrap. Each downloaded audio bricks are concatinated to a single audio file to play with mplayer.
    Installtion of the mp3wrap source is required.
    apt-get install mp3wrap
    Notice: Only available in locally instances!
  • TTS_MplayerCall
    optional: Setting up the Mplayer system call. The following example is default.
    Example: sudo /usr/bin/mplayer
  • TTS_SentenceAppendix
    Optional: Definition of one mp3-file to append each time of audio response.
    Using of Mp3Wrap is required. The audio bricks has to be downloaded before into CacheFileDir. Example: silence.mp3
  • TTS_FileMapping
    Definition of mp3files with a custom templatedefinition. Separated by space. All templatedefinitions can used in audiobricks by tts. The definition must begin and end with e colon. The mp3files must saved in the given directory by TTS_FIleTemplateDir.
    attr myTTS TTS_FileMapping ring:ringtone.mp3 beep:MyBeep.mp3
    set MyTTS tts Attention: This is my ringtone :ring: Its loud?
  • TTS_FileTemplateDir
    Directory to save all mp3-files are defined in TTS_FileMapping und TTS_SentenceAppendix
    Optional, Default: cache/templates
  • TTS_noStatisticsLog
    If set to 1, it prevents logging statistics to DbLog Devices, default is 0
    But please notice: this looging is important to able to delete longer unused cachefiles. If you disable this please take care to cleanup your cachedirectory by yourself.
  • TTS_speakAsFastAsPossible
    Trying to get an speach as fast as possible. In case of not present audiobricks, you can hear a short break. The audiobrick will be download at this time. In case of an presentation of all audiobricks at local cache, this attribute has no impact.
    Attribute only valid in case of an local or server instance.
  • readingFnAttributes

  • disable
    If this attribute is activated, the soundoutput will be disabled.
    Possible values: 0 => not disabled , 1 => disabled
    Default Value is 0 (not disabled)

  • verbose
    4: each step will be logged
    5: Additionally the individual debug informations from mplayer and mp3wrap will be logged

Beispiele
    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:
=end html =begin html_DE

Text2Speech


    Define
      Local : define <name> Text2Speech <alsadevice>
      Remote: define <name> Text2Speech <host>[:<portnr>][:SSL] [portpassword] Server : define <name> Text2Speech none

      Das Modul wandelt Text mittels verschiedener Provider/Ressourcen in Sprache um. Dabei kann das Device als Remote, Lokales Device oder als Server konfiguriert werden.

    • Local Device
        Die Ausgabe erfolgt auf angeschlossenen Audiodevices, zb. Lautsprecher direkt am Gerät oder per Bluetooth-Lautsprecher per Mplayer. Dazu ist Mplayer zu installieren.
        apt-get install mplayer
        Das angegebene Alsadevice ist in der /etc/asound.conf zu konfigurieren.

        Special AlsaDevice: default
        Ist als Alsa-Device default angegeben, so wird mplayer ohne eine Audiodevice Angabe aufgerufen. Dementsprechend verwendet mplayer das Standard Audio Ausgabedevice.

        Beispiel:
        define MyTTS Text2Speech hw=0.0
        define MyTTS Text2Speech default

    • Remote Device
        Das Modul ist Client-Server fäas bedeutet, das auf der Haupt-FHEM Installation eine Text2Speech-Instanz als Remote definiert wird. Auf dem Client wird Text2Speech als Local definiert. Die Sprachausgabe erfolgt auf der lokalen Instanz.
        Zu beachten ist, das die Text2Speech Instanz (Definition als local Device) auf dem Zieldevice identisch benannt ist.
        • Host: Angabe der IP-Adresse
        • PortNr: Angabe des TelnetPorts von FHEM; default: 7072
        • SSL: Angabe ob der der Zugriff per SSL erfolgen soll oder nicht; default: kein SSL
        • PortPassword: Angabe des in der Ziel-FHEM-Installtion angegebene Telnet Portpasswort

        Beispiel:
        define MyTTS Text2Speech 192.168.178.10:7072 fhempasswd define MyTTS Text2Speech 192.168.178.10

        Wenn ein PRESENCE Gerät die Host IP-Adresse abfragt, wird die blockierende interne Prüfung auf Erreichbarkeit umgangen und das PRESENCE Gerät genutzt.
    • Server Device
        Im Falle der Verwendung als Server, wird nur die MP3 Datei erstellt und als Reading lastFilename dargestellt. Es macht keinen Sinn hier das Attribut TTS_speakAsFastAsPossible zu verwenden. Die Verwendung des Attributes TTS_useMP3Wrap wird dringend empfohlen. Ansonsten wird hier nur der letzte Teiltext als mp3 Datei im Reading dargestellt.

      Beispiel:
      define MyTTS Text2Speech none

Set
  • tts:
    Setzen eines Textes zur Sprachausgabe. Um mp3-Dateien direkt auszugeben, müssen diese mit führenden und schließenden Doppelpunkten angegebenen sein. Die MP3-Dateien müssen unterhalb des Verzeichnisses TTS_FileTemplateDir gespeichert sein.
    Der Text selbst darf deshalb selbst keine Doppelpunte beinhalten.
    Für die SpracheEngine Amazon Polly kann auch SSML verwendet werden, Siehe Beispiele.
  • volume:
    Setzen der Ausgabe Lautstärke.
    Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich!

Get
    N/A

Attribute
  • TTS_Delimiter
    Optional: Wird ein Delimiter angegeben, so wird der Sprachbaustein an dieser Stelle geteilt. Als Delimiter ist nur ein einzelnes Zeichen zulässig. Hintergrund ist die Tatsache, das die einige Sprachengines nur eine bestimmt Anzahl an Zeichen (zb. Google nur 100Zeichen) zulässt.
    Im Standard wird nach jedem Satzende geteilt. Ist ein einzelner Satz länger als 100 Zeichen, so wird zusätzlich nach Kommata, Semikolon und dem Verbindungswort und geteilt.
    Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich!
    Notation
    + -> erzwinge das Trennen, auch wenn Textbaustein < x Zeichen
    - -> Trenne nur wenn Textbaustein > x Zeichen af -> add first -> füge den Delimiter am Satzanfang wieder hinzu
    al -> add last -> füge den Delimiter am Satzende wieder hinzu
    an -> add nothing -> Delimiter nicht wieder hinzufügen
    ~ -> der Delimiter
    Beispiel
    attr myTTS TTS_Delimiter -al.
  • TTS_Ressource
    Optional: Auswahl der Sprachengine
    Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich!
    • Google
      Nutzung der Google Sprachengine. Ein Internetzugriff ist notwendig! Aufgrund der Qualität ist der Einsatz diese Engine zu empfehlen und der Standard.
    • VoiceRSS
      Nutzung der VoiceRSS Sprachengine. Die Nutzung ist frei bis zu 350 Anfragen pro Tag. Wenn mehr benötigt werden ist ein Bezahlmodell wählbar. Ein Internetzugriff ist notwendig! Aufgrund der Qualität ist der Einsatz diese Engine ebenfalls zu empfehlen. Wenn diese Engine benutzt wird, ist ein APIKey notwendig (siehe TTS_APIKey)
    • ESpeak
      Nutzung der ESpeak Offline Sprachengine. Die Qualitä ist schlechter als die Google Engine. ESpeak und lame sind vor der Nutzung zu installieren.
      apt-get install espeak lame
    • SVOX-pico
      Nutzung der SVOX-Pico TTS-Engine (aus dem AOSP).
      Die Sprachengine sowie lame müssen installiert sein:
      sudo apt-get install libttspico-utils lame

      Für ARM/Raspbian sind die libttspico-utils leider nicht verfügbar,
      deswegen müsste man diese selbst kompilieren oder das vorkompilierte Paket aus dieser Anleitung verwenden, in aller Kürze:
      sudo apt-get install libpopt-dev lame
      cd /tmp
      wget http://www.dr-bischoff.de/raspi/pico2wave.deb
      sudo dpkg --install pico2wave.deb
    • Amazon-Polly
      Nutzung der Amazon Polly Sprachengine, dieselbe Engine wie für Amazon Alexa verwendet wird.
      Es muss das Perl Package Paws installiert sowie ein AWS Konto und ein Polly AWS User verfügbar sein
      cpan paws
      Die Zugangsdaten zum eigenen AWS Konto müssen unter ~/.aws/credentials liegen.
      [default] aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxx aws_access_key_id = xxxxxxxxxxxxxxx
  • TTS_Language
    Auswahl verschiendener Standardsprachen
  • TTS_Language_Custom
    Möchte man eine Spreche und Stimme abweichend der Standardspreachen verwenden, so kann man diese hier eintragen.
    Die Definition ist abhängig der verwendeten Sprachengine. Diesea Attribut überschreibt ein ev. vorhandenes TTS_Langugae Attribut.
    Siehe in die jeweilige API Referenz
  • TTS_APIKey
    Wenn VoiceRSS genutzt wird, ist ein APIKey notwendig. Um diesen zu erhalten ist eine vorherige Registrierung notwendig. Anschließend erhält man den APIKey
    http://www.voicerss.org/registration.aspx
  • TTS_User
    Bisher ohne Benutzung. Falls eine Sprachengine zusätzlich zum APIKey einen Usernamen im Request verlangt.
  • TTS_CacheFileDir
    Optional: Die per Google geladenen Sprachbausteine werden in diesem Verzeichnis zur Wiedeverwendung abgelegt. Es findet zurZeit keine automatisierte Löschung statt.
    Default: cache/
    Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich!
  • TTS_UseMP3Wrap
    Optional: Für eine flüssige Sprachausgabe ist es zu empfehlen, die einzelnen vorher geladenen Sprachbausteine zu einem einzelnen Sprachbaustein zusammenfassen zu lassen bevor dieses per Mplayer ausgegeben werden. Dazu muss Mp3Wrap installiert werden.
    apt-get install mp3wrap
    Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich!
  • TTS_MplayerCall
    Optional: Angabe des Systemaufrufes zu Mplayer oder einem anderem Tool. Wird ein anderes Tool als mplayer
    dort verwendet gelten folgende Templates:
    • {device}
    • {volume}
    • {volumeadjust}
    • {file}
    Beispiele:
    attr myTTS TTS_MplayerCall sudo /usr/bin/mplayer attr myTTS TTS_MplayerCall AUDIODEV={device} play -q -v {volume} {file}
  • TTS_SentenceAppendix
    Optional: Angabe einer mp3-Datei die mit jeder Sprachausgabe am Ende ausgegeben wird.
    Voraussetzung ist die Nutzung von MP3Wrap. Die Sprachbausteine müssen bereits als mp3 im CacheFileDir vorliegen. Beispiel: silence.mp3
  • TTS_FileMapping
    Angabe von möglichen MP3-Dateien mit deren Templatedefinition. Getrennt duch Leerzeichen. Die Templatedefinitionen können in den per tts übergebenen Sprachbausteinen verwendet werden und müssen mit einem beginnenden und endenden Doppelpunkt angegeben werden. Die Dateien müssen im Verzeichnis TTS_FileTemplateDir gespeichert sein.
    attr myTTS TTS_FileMapping ring:ringtone.mp3 beep:MyBeep.mp3
    set MyTTS tts Achtung: hier kommt mein Klingelton :ring: War der laut?
  • TTS_FileTemplateDir
    Verzeichnis, in dem die per TTS_FileMapping und TTS_SentenceAppendix definierten MP3-Dateien gespeichert sind.
    Optional, Default: cache/templates
  • TTS_VolumeAdjust
    Anhebung der Grundlautstärke zur Anpassung an die angeschlossenen Lautsprecher.
    Default: 110
    attr myTTS TTS_VolumeAdjust 400
  • TTS_noStatisticsLog
    1, verhindert das Loggen von Statistikdaten in DbLog Geräten, default ist 0
    Bitte zur Beachtung: Das Logging ist wichtig um alte, lang nicht genutzte Cachedateien automatisiert zu loeschen. Wenn dieses hier dektiviert wird muss sich der User selbst darum kuemmern.
  • TTS_speakAsFastAsPossible
    Es wird versucht, so schnell als möglich eine Sprachausgabe zu erzielen. Bei Sprachbausteinen die nicht bereits lokal vorliegen, ist eine kurze Pause wahrnehmbar. Dann wird der benötigte Sprachbaustein nachgeladen. Liegen alle Sprachbausteine im Cache vor, so hat dieses Attribut keine Auswirkung.
    Attribut nur verfügbar bei einer lokalen oder Server Instanz
  • TTS_OutputFile
    Angabe eines fixen Dateinamens als mp3 Output. Das Attribut ist nur relevant in Verbindung mit TTS_UseMP3Wrap.
    Wenn ein Dateinamen angegeben wird, so wird zusätzlich TTS_CacheFileDir beachtet. Bei einer absoluten Pfadangabe
    muss der Dateipfad durch FHEM schreibbar sein.
    attr myTTS TTS_OutputFile output.mp3
    attr myTTS TTS_OutputFile /media/miniDLNA/output.mp3
  • readingFnAttributes

  • disable
    If this attribute is activated, the soundoutput will be disabled.
    Possible values: 0 => not disabled , 1 => disabled
    Default Value is 0 (not disabled)

  • verbose
    4: Alle Zwischenschritte der Verarbeitung werden ausgegeben
    5: Zusätzlich werden auch die Meldungen von Mplayer und Mp3Wrap ausgegeben

Beispiele
    define TTS_EG_WZ Text2Speech hw=/dev/snd/controlC3
    attr TTS_EG_WZ TTS_Language Deutsch
    attr TTS_EG_WZ TTS_MplayerCall /usr/bin/mplayer
    attr TTS_EG_WZ TTS_Ressource Amazon-Polly
    attr TTS_EG_WZ TTS_UseMP3Wrap 1

    set MyTTS tts <speak>Mary had a little lamb.</speak>
    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:
=end html_DE =cut