mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-03-10 03:06:37 +00:00
98_Text2Speech.pm: add Amazon Polly as new suggested TTS-Engine due best quality
git-svn-id: https://svn.fhem.de/fhem/trunk@19307 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
parent
7f974e3057
commit
293ac1c944
@ -1,5 +1,7 @@
|
|||||||
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide.
|
# 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.
|
# Do not insert empty lines here, update check depends on it.
|
||||||
|
- feature: 98_Text2Speech: add Amazon Polly as new suggested TTS-Engine
|
||||||
|
due best quality
|
||||||
- bugfix: 73_AutoShuttersControl: fix shading absent and coming home, fix
|
- bugfix: 73_AutoShuttersControl: fix shading absent and coming home, fix
|
||||||
Reading ASC_Time_PrivacyDriveDown, fix blocking shutter then
|
Reading ASC_Time_PrivacyDriveDown, fix blocking shutter then
|
||||||
shading drive and terrace door open
|
shading drive and terrace door open
|
||||||
|
@ -25,9 +25,14 @@ use IO::File;
|
|||||||
use HttpUtils;
|
use HttpUtils;
|
||||||
use Digest::MD5 qw(md5_hex);
|
use Digest::MD5 qw(md5_hex);
|
||||||
use URI::Escape;
|
use URI::Escape;
|
||||||
|
use Text::Iconv;
|
||||||
|
#use Encode::Detect::Detector;
|
||||||
use Data::Dumper;
|
use Data::Dumper;
|
||||||
use lib ('./FHEM/lib', './lib');
|
use lib ('./FHEM/lib', './lib');
|
||||||
|
|
||||||
|
# loading in attr function of TTS_RESSOURCE if Paws is really needed
|
||||||
|
# require Paws::Polly
|
||||||
|
|
||||||
sub Text2Speech_OpenDev($);
|
sub Text2Speech_OpenDev($);
|
||||||
sub Text2Speech_CloseDev($);
|
sub Text2Speech_CloseDev($);
|
||||||
|
|
||||||
@ -39,12 +44,11 @@ my %sets = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
# path to mplayer
|
# path to mplayer
|
||||||
my $mplayer = 'sudo /usr/bin/mplayer';
|
my $mplayer = 'sudo /usr/bin/mplayer';
|
||||||
#my $mplayerOpts = '-nolirc -noconsolecontrols -http-header-fields "User-Agent:Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.172 Safari/537.22m"';
|
my $mplayerOpts = '-nolirc -noconsolecontrols';
|
||||||
my $mplayerOpts = '-nolirc -noconsolecontrols';
|
my $mplayerNoDebug = '-really-quiet';
|
||||||
my $mplayerNoDebug = '-really-quiet';
|
my $mplayerAudioOpts = '-ao alsa:device=';
|
||||||
my $mplayerAudioOpts = '-ao alsa:device=';
|
|
||||||
#my $ttsAddr = 'http://translate.google.com/translate_tts?tl=de&q=';
|
|
||||||
my %ttsHost = ("Google" => "translate.google.com",
|
my %ttsHost = ("Google" => "translate.google.com",
|
||||||
"VoiceRSS" => "api.voicerss.org"
|
"VoiceRSS" => "api.voicerss.org"
|
||||||
);
|
);
|
||||||
@ -57,7 +61,7 @@ my %ttsQuery = ("Google" => "q=",
|
|||||||
my %ttsPath = ("Google" => "/translate_tts?",
|
my %ttsPath = ("Google" => "/translate_tts?",
|
||||||
"VoiceRSS" => "/?"
|
"VoiceRSS" => "/?"
|
||||||
);
|
);
|
||||||
my %ttsAddon = ("Google" => "client=tw-ob",
|
my %ttsAddon = ("Google" => "client=tw-ob&ie=UTF-8",
|
||||||
"VoiceRSS" => ""
|
"VoiceRSS" => ""
|
||||||
);
|
);
|
||||||
my %ttsAPIKey = ("Google" => "", # kein APIKey nötig
|
my %ttsAPIKey = ("Google" => "", # kein APIKey nötig
|
||||||
@ -74,38 +78,41 @@ my %ttsQuality = ("Google" => "",
|
|||||||
);
|
);
|
||||||
my %ttsMaxChar = ("Google" => 100,
|
my %ttsMaxChar = ("Google" => 100,
|
||||||
"VoiceRSS" => 300,
|
"VoiceRSS" => 300,
|
||||||
"SVOX-pico" => 1000
|
"SVOX-pico" => 1000,
|
||||||
|
"Amazon-Polly" => 3000
|
||||||
);
|
);
|
||||||
my %language = ("Google" => {"Deutsch" => "de",
|
my %language = ("Google" => { "Deutsch" => "de",
|
||||||
"English-US" => "en-us",
|
"English-US" => "en-us",
|
||||||
"Schwedisch" => "sv",
|
"Schwedisch" => "sv",
|
||||||
"Indian-Hindi" => "hi",
|
"France" => "fr",
|
||||||
"Arabic" => "ar",
|
"Spain" => "es",
|
||||||
"France" => "fr",
|
"Italian" => "it",
|
||||||
"Spain" => "es",
|
"Chinese" => "cn",
|
||||||
"Italian" => "it",
|
"Dutch" => "nl"
|
||||||
"Chinese" => "cn",
|
|
||||||
"Dutch" => "nl"
|
|
||||||
},
|
},
|
||||||
"VoiceRSS" => {"Deutsch" => "de-de",
|
"VoiceRSS" => { "Deutsch" => "de-de",
|
||||||
"English-US" => "en-us",
|
"English-US" => "en-us",
|
||||||
"Schwedisch" => "sv-se",
|
"Schwedisch" => "sv-se",
|
||||||
"Indian-Hindi" => "en-in", # gibts nicht
|
"France" => "fr-fr",
|
||||||
"Arabic" => "en-us", # gibts nicht
|
"Spain" => "es-es",
|
||||||
"France" => "fr-fr",
|
"Italian" => "it-it",
|
||||||
"Spain" => "es-es",
|
"Chinese" => "zh-cn",
|
||||||
"Italian" => "it-it",
|
"Dutch" => "nl-nl"
|
||||||
"Chinese" => "zh-cn"
|
|
||||||
},
|
},
|
||||||
"SVOX-pico" => {"Deutsch" => "de-DE",
|
"SVOX-pico" => { "Deutsch" => "de-DE",
|
||||||
"English-US" => "en-US",
|
"English-US" => "en-US",
|
||||||
"Schwedisch" => "en-US", # gibts nicht
|
"France" => "fr-FR",
|
||||||
"Indian-Hindi" => "en-US", # gibts nicht
|
"Spain" => "es-ES",
|
||||||
"Arabic" => "en-US", # gibts nicht
|
"Italian" => "it-IT"
|
||||||
"France" => "fr-FR",
|
},
|
||||||
"Spain" => "es-ES",
|
"Amazon-Polly"=> {"Deutsch" => "Marlene",
|
||||||
"Italian" => "it-IT",
|
"English-US" => "Joanna",
|
||||||
"Chinese" => "en-US" # gibts nicht
|
"Schwedisch" => "Astrid",
|
||||||
|
"France" => "Celine",
|
||||||
|
"Spain" => "Conchita",
|
||||||
|
"Italian" => "Carla",
|
||||||
|
"Chinese" => "Zhiyu",
|
||||||
|
"Dutch" => "Lotte"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -120,8 +127,8 @@ sub Text2Speech_Initialize($)
|
|||||||
$hash->{UndefFn} = "Text2Speech_Undefine";
|
$hash->{UndefFn} = "Text2Speech_Undefine";
|
||||||
$hash->{AttrFn} = "Text2Speech_Attr";
|
$hash->{AttrFn} = "Text2Speech_Attr";
|
||||||
$hash->{AttrList} = "disable:0,1".
|
$hash->{AttrList} = "disable:0,1".
|
||||||
" TTS_Delemiter".
|
" TTS_Delimiter".
|
||||||
" TTS_Ressource:ESpeak,SVOX-pico,". join(",", sort keys %ttsHost).
|
" TTS_Ressource:ESpeak,SVOX-pico,Amazon-Polly,". join(",", sort keys %ttsHost).
|
||||||
" TTS_APIKey".
|
" TTS_APIKey".
|
||||||
" TTS_User".
|
" TTS_User".
|
||||||
" TTS_Quality:".
|
" TTS_Quality:".
|
||||||
@ -164,7 +171,9 @@ sub Text2Speech_Initialize($)
|
|||||||
" TTS_VolumeAdjust".
|
" TTS_VolumeAdjust".
|
||||||
" TTS_noStatisticsLog:1,0".
|
" TTS_noStatisticsLog:1,0".
|
||||||
" TTS_Language:".join(",", sort keys %{$language{"Google"}}).
|
" TTS_Language:".join(",", sort keys %{$language{"Google"}}).
|
||||||
|
" TTS_Language_Custom".
|
||||||
" TTS_SpeakAsFastAsPossible:1,0".
|
" TTS_SpeakAsFastAsPossible:1,0".
|
||||||
|
" TTS_OutputFile".
|
||||||
" ".$readingFnAttributes;
|
" ".$readingFnAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,15 +246,6 @@ sub Text2Speech_Undefine($$)
|
|||||||
return undef;
|
return undef;
|
||||||
}
|
}
|
||||||
|
|
||||||
###################################
|
|
||||||
# Angabe des Delemiters: zb.: +af~
|
|
||||||
# + -> erzwinge das Trennen, auch wenn Textbaustein < 100Zeichen
|
|
||||||
# - -> Trenne nur wenn Textbaustein > 100Zeichen
|
|
||||||
# af -> add first -> füge den Delemiter am Satzanfang wieder hinzu
|
|
||||||
# al -> add last -> füge den Delemiter am Satzende wieder hinzu
|
|
||||||
# an -> add nothing -> Delemiter nicht wieder hinzufügen
|
|
||||||
# ~ -> der Delemiter
|
|
||||||
###################################
|
|
||||||
sub Text2Speech_Attr(@) {
|
sub Text2Speech_Attr(@) {
|
||||||
my @a = @_;
|
my @a = @_;
|
||||||
my $do = 0;
|
my $do = 0;
|
||||||
@ -256,12 +256,30 @@ sub Text2Speech_Attr(@) {
|
|||||||
my $TTS_CacheFileDir = AttrVal($hash->{NAME}, "TTS_CacheFileDir", "cache");
|
my $TTS_CacheFileDir = AttrVal($hash->{NAME}, "TTS_CacheFileDir", "cache");
|
||||||
my $TTS_FileMapping = AttrVal($hash->{NAME}, "TTS_FileMapping", ""); # zb, silence:silence.mp3 ring:myringtone.mp3;
|
my $TTS_FileMapping = AttrVal($hash->{NAME}, "TTS_FileMapping", ""); # zb, silence:silence.mp3 ring:myringtone.mp3;
|
||||||
|
|
||||||
if($a[2] eq "TTS_Delemiter" && $a[0] ne "del") {
|
if($a[2] eq "TTS_Delimiter" && $a[0] ne "del") {
|
||||||
return "wrong delemiter syntax: [+-]a[lfn]. \n".
|
return "wrong Delimiter syntax: [+-]a[lfn]. Please see CommandRef for Notation. \n".
|
||||||
" Example 1: +an~\n".
|
" Example 1: +an~\n".
|
||||||
" Example 2: +al." if($value !~ m/^([+-]a[lfn]){0,1}(.){1}$/i);
|
" 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)/ );
|
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, "Wechsele auf Amazon Polly, Lade Librarys nach.";
|
||||||
|
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";
|
||||||
|
|
||||||
|
if (! -e File::HomeDir->my_home."/.aws/credentials"){
|
||||||
|
return "No AWS credentials in FHEM Homedir found, please check ".File::HomeDir->my_home."/.aws/credentials <br> please refer https://metacpan.org/pod/Paws#AUTHENTICATION";
|
||||||
|
}
|
||||||
|
|
||||||
} elsif ($a[2] eq "TTS_Ressource") {
|
} elsif ($a[2] eq "TTS_Ressource") {
|
||||||
return "This Attribute is only available in direct or server mode" if($hash->{MODE} !~ m/(DIRECT|SERVER)/ );
|
return "This Attribute is only available in direct or server mode" if($hash->{MODE} !~ m/(DIRECT|SERVER)/ );
|
||||||
|
|
||||||
@ -292,7 +310,7 @@ sub Text2Speech_Attr(@) {
|
|||||||
return "Could not create directory: <$value>";
|
return "Could not create directory: <$value>";
|
||||||
}
|
}
|
||||||
|
|
||||||
} elsif ($a[2] eq "TTS_TimeOut") {
|
} elsif ($a[0] eq "set" && $a[2] eq "TTS_TimeOut") {
|
||||||
return "Only Numbers allowed" if ($value !~ m/[0-9]+/);
|
return "Only Numbers allowed" if ($value !~ m/[0-9]+/);
|
||||||
|
|
||||||
} elsif ($a[2] eq "TTS_FileMapping") {
|
} elsif ($a[2] eq "TTS_FileMapping") {
|
||||||
@ -437,8 +455,8 @@ sub Text2Speech_Set($@)
|
|||||||
|
|
||||||
return "no set argument specified" if(int(@a) < 2);
|
return "no set argument specified" if(int(@a) < 2);
|
||||||
|
|
||||||
return "No APIKey specified" if (length($ttsAPIKey{$TTS_Ressource})>0 && !defined($TTS_APIKey));
|
return "No APIKey specified" if (!defined($TTS_APIKey) && ($ttsAPIKey{$TTS_Ressource} || length($ttsAPIKey{$TTS_Ressource})>0));
|
||||||
return "No Username for TTS Access specified" if (length($ttsUser{$TTS_Ressource})>0 && !defined($TTS_User));
|
return "No Username for TTS Access specified" if (!defined($TTS_User) && ($ttsUser{$TTS_Ressource} || length($ttsUser{$TTS_Ressource})>0));
|
||||||
|
|
||||||
my $cmd = shift(@a); # Dummy
|
my $cmd = shift(@a); # Dummy
|
||||||
$cmd = shift(@a); # DevName
|
$cmd = shift(@a); # DevName
|
||||||
@ -458,6 +476,7 @@ sub Text2Speech_Set($@)
|
|||||||
if($cmd eq "tts") {
|
if($cmd eq "tts") {
|
||||||
|
|
||||||
if($hash->{MODE} eq "DIRECT" || $hash->{MODE} eq "SERVER") {
|
if($hash->{MODE} eq "DIRECT" || $hash->{MODE} eq "SERVER") {
|
||||||
|
$hash->{VOLUME} = ReadingsNum($me, "volume", 100);
|
||||||
readingsSingleUpdate($hash, "playing", "1", 1);
|
readingsSingleUpdate($hash, "playing", "1", 1);
|
||||||
Text2Speech_PrepareSpeech($hash, join(" ", @a));
|
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}));
|
$hash->{helper}{RUNNING_PID} = BlockingCall("Text2Speech_DoIt", $hash, "Text2Speech_Done", $TTS_TimeOut, "Text2Speech_AbortFn", $hash) unless(exists($hash->{helper}{RUNNING_PID}));
|
||||||
@ -474,6 +493,7 @@ sub Text2Speech_Set($@)
|
|||||||
} elsif ($hash->{MODE} eq "REMOTE") {
|
} elsif ($hash->{MODE} eq "REMOTE") {
|
||||||
Text2Speech_Write($hash, "volume $vol");
|
Text2Speech_Write($hash, "volume $vol");
|
||||||
} else {return undef;}
|
} else {return undef;}
|
||||||
|
|
||||||
readingsSingleUpdate($hash, "volume", (($vol>100)?0:$vol), 1);
|
readingsSingleUpdate($hash, "volume", (($vol>100)?0:$vol), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,41 +510,62 @@ sub Text2Speech_Set($@)
|
|||||||
# param2: string to speech
|
# 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($$) {
|
sub Text2Speech_PrepareSpeech($$) {
|
||||||
my ($hash, $t) = @_;
|
my ($hash, $t) = @_;
|
||||||
my $me = $hash->{NAME};
|
my $me = $hash->{NAME};
|
||||||
|
|
||||||
my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google");
|
my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google");
|
||||||
my $TTS_Delemiter = AttrVal($hash->{NAME}, "TTS_Delemiter", undef);
|
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_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_FileTemplateDir = AttrVal($hash->{NAME}, "TTS_FileTemplateDir", "templates");
|
||||||
|
|
||||||
my $TTS_ForceSplit = 0;
|
my $TTS_ForceSplit = 0;
|
||||||
my $TTS_AddDelemiter;
|
my $TTS_AddDelimiter;
|
||||||
|
|
||||||
if($TTS_Delemiter && $TTS_Delemiter =~ m/^[+-]a[lfn]/i) {
|
if($TTS_Delimiter && $TTS_Delimiter =~ m/^[+-]a[lfn]/i) {
|
||||||
$TTS_ForceSplit = 1 if(substr($TTS_Delemiter,0,1) eq "+");
|
$TTS_ForceSplit = 1 if(substr($TTS_Delimiter,0,1) eq "+");
|
||||||
$TTS_ForceSplit = 0 if(substr($TTS_Delemiter,0,1) eq "-");
|
$TTS_ForceSplit = 0 if(substr($TTS_Delimiter,0,1) eq "-");
|
||||||
|
|
||||||
$TTS_AddDelemiter = substr($TTS_Delemiter,1,2); # af, al oder an
|
$TTS_AddDelimiter = substr($TTS_Delimiter,1,2); # af, al oder an
|
||||||
|
|
||||||
$TTS_Delemiter = substr($TTS_Delemiter,3);
|
$TTS_Delimiter = substr($TTS_Delimiter,3);
|
||||||
|
|
||||||
} elsif (!$TTS_Delemiter) { # Default wenn Attr nicht gesetzt
|
} elsif (!$TTS_Delimiter) { # Default wenn Attr nicht gesetzt
|
||||||
$TTS_Delemiter = "(?<=[\\.!?])\\s*";
|
$TTS_Delimiter = "(?<=[\\.!?])\\s*";
|
||||||
$TTS_ForceSplit = 1;
|
$TTS_ForceSplit = 0;
|
||||||
$TTS_AddDelemiter = "";
|
$TTS_AddDelimiter = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#-- we may have problems with umlaut characters
|
||||||
# ersetze Sonderzeichen die Google nicht auflösen kann
|
# ersetze Sonderzeichen die Google nicht auflösen kann
|
||||||
|
my $converter;
|
||||||
if($TTS_Ressource eq "Google") {
|
if($TTS_Ressource eq "Google") {
|
||||||
$t =~ s/ä/ae/g;
|
# Google benötigt UTF-8
|
||||||
$t =~ s/ö/oe/g;
|
# $t =~ s/ä/ae/g;
|
||||||
$t =~ s/ü/ue/g;
|
# $t =~ s/ö/oe/g;
|
||||||
$t =~ s/Ä/Ae/g;
|
# $t =~ s/ü/ue/g;
|
||||||
$t =~ s/Ö/Oe/g;
|
# $t =~ s/Ä/Ae/g;
|
||||||
$t =~ s/Ü/Ue/g;
|
# $t =~ s/Ö/Oe/g;
|
||||||
$t =~ s/ß/ss/g;
|
# $t =~ s/Ü/Ue/g;
|
||||||
|
# $t =~ s/ß/ss/g;
|
||||||
|
|
||||||
|
# -> use Encode::Detect::Detector;
|
||||||
|
#Log3 $hash, 4, "$me: ermittelte CodePage: " .detect($t). " , konvertiere nach UTF-8";
|
||||||
|
#$converter = Text::Iconv->new(detect($t), "utf-8");
|
||||||
|
#$t = $converter->convert($t);
|
||||||
|
} elsif ($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;
|
my @text;
|
||||||
@ -536,7 +577,7 @@ sub Text2Speech_PrepareSpeech($$) {
|
|||||||
|
|
||||||
# bei Angabe direkter MP3-Files wird hier ein temporäres Template vergeben
|
# bei Angabe direkter MP3-Files wird hier ein temporäres Template vergeben
|
||||||
for(my $i=0; $i<(@text); $i++) {
|
for(my $i=0; $i<(@text); $i++) {
|
||||||
@FileTplPc = ($text[$i] =~ /\:([^:]+\.(?|mp3|ogg|wav))\:/g);
|
@FileTplPc = ($text[$i] =~ /:(\w+?\.(?:mp3|ogg|wav)):/g);
|
||||||
for(my $j=0; $j<(@FileTplPc); $j++) {
|
for(my $j=0; $j<(@FileTplPc); $j++) {
|
||||||
my $tpl = "FileTpl_#".$i."_".$j; #eindeutige Templatedefinition schaffen
|
my $tpl = "FileTpl_#".$i."_".$j; #eindeutige Templatedefinition schaffen
|
||||||
Log3 $hash, 4, "$me: Angabe einer direkten MP3-Datei gefunden: $FileTplPc[$j] => $tpl";
|
Log3 $hash, 4, "$me: Angabe einer direkten MP3-Datei gefunden: $FileTplPc[$j] => $tpl";
|
||||||
@ -547,7 +588,7 @@ sub Text2Speech_PrepareSpeech($$) {
|
|||||||
|
|
||||||
#iteriere durch die Sprachbausteine und splitte den Text bei den Filetemplates auf
|
#iteriere durch die Sprachbausteine und splitte den Text bei den Filetemplates auf
|
||||||
for(my $i=0; $i<(@text); $i++) {
|
for(my $i=0; $i<(@text); $i++) {
|
||||||
my $cutter = '#!#'; #eindeutigen Cutter als Delemiter bei den Filetemplates vergeben
|
my $cutter = '#!#'; #eindeutigen Cutter als Delimiter bei den Filetemplates vergeben
|
||||||
@FileTplPc = ($text[$i] =~ /:([^:]+):/g);
|
@FileTplPc = ($text[$i] =~ /:([^:]+):/g);
|
||||||
for(my $j=0; $j<(@FileTplPc); $j++) {
|
for(my $j=0; $j<(@FileTplPc); $j++) {
|
||||||
$text[$i] =~ s/:$FileTplPc[$j]:/$cutter$FileTplPc[$j]$cutter/g;
|
$text[$i] =~ s/:$FileTplPc[$j]:/$cutter$FileTplPc[$j]$cutter/g;
|
||||||
@ -555,11 +596,16 @@ sub Text2Speech_PrepareSpeech($$) {
|
|||||||
@text = Text2Speech_SplitString(\@text, 0, $cutter, 1, "");
|
@text = Text2Speech_SplitString(\@text, 0, $cutter, 1, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, $TTS_Delemiter, $TTS_ForceSplit, $TTS_AddDelemiter);
|
Log3 $hash, 4, "$me: MaxChar = $ttsMaxChar{$TTS_Ressource}, Delimiter = $TTS_Delimiter, ForceSplit = $TTS_ForceSplit, AddDelimiter = $TTS_AddDelimiter";
|
||||||
@text = Text2Speech_SplitString(\@text, $ttsMaxChar{$TTS_Ressource}, "(?<=[\\.!?])\\s*", 0, "");
|
|
||||||
|
@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}, ";", 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}, "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:";
|
Log3 $hash, 4, "$me: Auflistung der Textbausteine nach Aufbereitung:";
|
||||||
for(my $i=0; $i<(@text); $i++) {
|
for(my $i=0; $i<(@text); $i++) {
|
||||||
@ -579,22 +625,22 @@ sub Text2Speech_PrepareSpeech($$) {
|
|||||||
#####################################
|
#####################################
|
||||||
# param1: array : Text 2 Speech
|
# param1: array : Text 2 Speech
|
||||||
# param2: string: MaxChar
|
# param2: string: MaxChar
|
||||||
# param3: string: Delemiter
|
# param3: string: Delimiter
|
||||||
# param4: int : 1 -> es wird am Delemiter gesplittet
|
# param4: int : 1 -> es wird am Delimiter gesplittet
|
||||||
# 0 -> es wird nur gesplittet, wenn Stringlänge länger als MaxChar
|
# 0 -> es wird nur gesplittet, wenn Stringlänge länger als MaxChar
|
||||||
# param5: string: Add Delemiter to String? [al|af|<empty>] (AddLast/AddFirst)
|
# param5: string: Add Delimiter to String? [al|af|<empty>] (AddLast/AddFirst)
|
||||||
#
|
#
|
||||||
# Splittet die Texte aus $hash->{helper}->{Text2Speech} anhand des
|
# Splittet die Texte aus $hash->{helper}->{Text2Speech} anhand des
|
||||||
# Delemiters, wenn die Stringlänge MaxChars übersteigt.
|
# Delimiters, wenn die Stringlänge MaxChars übersteigt.
|
||||||
# Ist "AddDelemiter" angegeben, so wird der Delemiter an den
|
# Ist "AddDelimiter" angegeben, so wird der Delimiter an den
|
||||||
# String wieder angefügt
|
# String wieder angefügt
|
||||||
#####################################
|
#####################################
|
||||||
sub Text2Speech_SplitString($$$$$){
|
sub Text2Speech_SplitString($$$$$){
|
||||||
my @text = @{shift()};
|
my @text = @{shift()};
|
||||||
my $MaxChar = shift;
|
my $MaxChar = shift;
|
||||||
my $Delemiter = shift;
|
my $Delimiter = shift;
|
||||||
my $ForceSplit = shift;
|
my $ForceSplit = shift;
|
||||||
my $AddDelemiter = shift;
|
my $AddDelimiter = shift;
|
||||||
my @newText;
|
my @newText;
|
||||||
|
|
||||||
for(my $i=0; $i<(@text); $i++) {
|
for(my $i=0; $i<(@text); $i++) {
|
||||||
@ -603,20 +649,43 @@ sub Text2Speech_SplitString($$$$$){
|
|||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
|
|
||||||
my @b = split(/$Delemiter/, $text[$i]);
|
my @b;
|
||||||
for(my $j=0; $j<(@b); $j++) {
|
if($Delimiter =~/^ $/) {
|
||||||
$b[$j] = $b[$j] . $Delemiter if($AddDelemiter eq "al"); # Am Satzende wieder hinzufügen.
|
@b = split(' ', $text[$i]);
|
||||||
$b[$j+1] = $Delemiter . $b[$j+1] if(($AddDelemiter eq "af") && ($b[$j+1])); # Am Satzanfang des nächsten Satzes wieder hinzufügen.
|
|
||||||
push(@newText, $b[$j]);
|
|
||||||
}
|
}
|
||||||
|
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;
|
return @newText;
|
||||||
}
|
}
|
||||||
|
|
||||||
#####################################
|
#####################################
|
||||||
# param1: hash : Hash
|
# param1: hash : Hash
|
||||||
# param2: string: Typ (mplayer oder mp3wrap oder ....)
|
# param2: string: Datei
|
||||||
# param3: string: Datei
|
|
||||||
#
|
#
|
||||||
# Erstellt den Commandstring für den Systemaufruf
|
# Erstellt den Commandstring für den Systemaufruf
|
||||||
#####################################
|
#####################################
|
||||||
@ -638,18 +707,21 @@ sub Text2Speech_BuildMplayerCmdString($$) {
|
|||||||
$mplayerAudioOpts = "";
|
$mplayerAudioOpts = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
# anstatt mplayer wird ein anderer Player verwendet
|
|
||||||
if ($TTS_MplayerCall !~ m/mplayer/) {
|
|
||||||
$AlsaDevice = "";
|
|
||||||
$mplayerAudioOpts = "";
|
|
||||||
$mplayerNoDebug = "";
|
|
||||||
$mplayerOpts = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
my $NoDebug = $mplayerNoDebug;
|
my $NoDebug = $mplayerNoDebug;
|
||||||
$NoDebug = "" if($verbose >= 5);
|
$NoDebug = "" if($verbose >= 5);
|
||||||
|
|
||||||
$cmd = $TTS_MplayerCall . " " . $mplayerAudioOpts . $AlsaDevice . " " .$NoDebug. " " . $mplayerOpts . " " . $file;
|
# 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);
|
my $mp3Duration = Text2Speech_CalcMP3Duration($hash, $file);
|
||||||
BlockingInformParent("Text2Speech_readingsSingleUpdateByName", [$hash->{NAME}, "duration", "$mp3Duration"], 0);
|
BlockingInformParent("Text2Speech_readingsSingleUpdateByName", [$hash->{NAME}, "duration", "$mp3Duration"], 0);
|
||||||
@ -664,8 +736,8 @@ sub Text2Speech_BuildMplayerCmdString($$) {
|
|||||||
sub Text2Speech_readingsSingleUpdateByName($$$) {
|
sub Text2Speech_readingsSingleUpdateByName($$$) {
|
||||||
my ($devName, $readingName, $readingVal) = @_;
|
my ($devName, $readingName, $readingVal) = @_;
|
||||||
my $hash = $defs{$devName};
|
my $hash = $defs{$devName};
|
||||||
#Log3 $hash, 4, "Text2Speech_readingsSingleUpdateByName: Dev:$devName Reading:$readingName Val:$readingVal";
|
Log3 $hash, 5, $hash->{NAME}.": readingsSingleUpdateByName: Dev:$devName Reading:$readingName Val:$readingVal";
|
||||||
readingsSingleUpdate($defs{$devName}, $readingName, $readingVal, 1);
|
readingsSingleUpdate($hash, $readingName, $readingVal, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#####################################
|
#####################################
|
||||||
@ -707,7 +779,7 @@ sub Text2Speech_Download($$$) {
|
|||||||
my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google");
|
my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google");
|
||||||
my $TTS_User = AttrVal($hash->{NAME}, "TTS_User", "");
|
my $TTS_User = AttrVal($hash->{NAME}, "TTS_User", "");
|
||||||
my $TTS_APIKey = AttrVal($hash->{NAME}, "TTS_APIKey", "");
|
my $TTS_APIKey = AttrVal($hash->{NAME}, "TTS_APIKey", "");
|
||||||
my $TTS_Language = AttrVal($hash->{NAME}, "TTS_Language", "Deutsch");
|
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_Quality = AttrVal($hash->{NAME}, "TTS_Quality", "");
|
||||||
my $TTS_Speed = AttrVal($hash->{NAME}, "TTS_Speed", "");
|
my $TTS_Speed = AttrVal($hash->{NAME}, "TTS_Speed", "");
|
||||||
my $cmd;
|
my $cmd;
|
||||||
@ -718,8 +790,7 @@ sub Text2Speech_Download($$$) {
|
|||||||
my $fh;
|
my $fh;
|
||||||
|
|
||||||
my $url = "http://" . $ttsHost{$TTS_Ressource} . $ttsPath{$TTS_Ressource};
|
my $url = "http://" . $ttsHost{$TTS_Ressource} . $ttsPath{$TTS_Ressource};
|
||||||
$url .= $ttsLang{$TTS_Ressource};
|
$url .= $ttsLang{$TTS_Ressource} . $TTS_Language;
|
||||||
$url .= $language{$TTS_Ressource}{$TTS_Language};
|
|
||||||
$url .= "&" . $ttsAddon{$TTS_Ressource} if(length($ttsAddon{$TTS_Ressource})>0);
|
$url .= "&" . $ttsAddon{$TTS_Ressource} if(length($ttsAddon{$TTS_Ressource})>0);
|
||||||
$url .= "&" . $ttsUser{$TTS_Ressource} . $TTS_User if(length($ttsUser{$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 .= "&" . $ttsAPIKey{$TTS_Ressource} . $TTS_APIKey if(length($ttsAPIKey{$TTS_Ressource})>0);
|
||||||
@ -729,7 +800,7 @@ sub Text2Speech_Download($$$) {
|
|||||||
|
|
||||||
Log3 $hash->{NAME}, 4, $hash->{NAME}.": Verwende ".$TTS_Ressource." OnlineResource zum Download";
|
Log3 $hash->{NAME}, 4, $hash->{NAME}.": Verwende ".$TTS_Ressource." OnlineResource zum Download";
|
||||||
Log3 $hash->{NAME}, 4, $hash->{NAME}.": Hole URL: ". $url;
|
Log3 $hash->{NAME}, 4, $hash->{NAME}.": Hole URL: ". $url;
|
||||||
#$HttpResponse = GetHttpFile($ttsHost, $ttsPath . $ttsLang . $language{$TTS_Ressource}{$TTS_Language} . "&" . $ttsQuery . uri_escape($text));
|
#$HttpResponse = GetHttpFile($ttsHost, $ttsPath . $ttsLang . $TTS_Language . "&" . $ttsQuery . uri_escape($text));
|
||||||
my $param = {
|
my $param = {
|
||||||
url => $url,
|
url => $url,
|
||||||
timeout => 5,
|
timeout => 5,
|
||||||
@ -755,21 +826,23 @@ sub Text2Speech_Download($$$) {
|
|||||||
$fh->print($HttpResponse);
|
$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);
|
close($fh);
|
||||||
} elsif ($TTS_Ressource eq "ESpeak") {
|
}
|
||||||
|
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";
|
my $FileWav = $file . ".wav";
|
||||||
|
|
||||||
$cmd = "sudo espeak -vde+f3 -k5 -s150 \"" . $text . "\">\"" . $FileWav . "\"";
|
$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 "SVOX-pico") {
|
|
||||||
my $FileWav = $file . ".wav";
|
|
||||||
|
|
||||||
$cmd = "pico2wave --lang=" . $language{$TTS_Ressource}{$TTS_Language} . " --wave=\"" . $FileWav . "\" \"" . $text . "\"";
|
|
||||||
Log3 $hash, 4, $hash->{NAME}.":" .$cmd;
|
Log3 $hash, 4, $hash->{NAME}.":" .$cmd;
|
||||||
system($cmd);
|
system($cmd);
|
||||||
|
|
||||||
@ -778,6 +851,30 @@ sub Text2Speech_Download($$$) {
|
|||||||
system($cmd);
|
system($cmd);
|
||||||
unlink $FileWav;
|
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 $polly = Paws->service('Polly', region => 'eu-central-1');
|
||||||
|
my $res = $polly->SynthesizeSpeech(
|
||||||
|
VoiceId => $TTS_Language,
|
||||||
|
Text => $text,
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#####################################
|
#####################################
|
||||||
@ -789,6 +886,7 @@ sub Text2Speech_DoIt($) {
|
|||||||
my $TTS_Language = AttrVal($hash->{NAME}, "TTS_Language", "Deutsch");
|
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_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_FileTemplateDir = AttrVal($hash->{NAME}, "TTS_FileTemplateDir", "templates");
|
||||||
|
my $TTS_OutputFile = AttrVal($hash->{NAME}, "TTS_OutputFile", undef);
|
||||||
|
|
||||||
my $myFileTemplateDir;
|
my $myFileTemplateDir;
|
||||||
if($TTS_FileTemplateDir =~ m/^\/.*/) { $myFileTemplateDir = $TTS_FileTemplateDir; } else { $myFileTemplateDir = $TTS_CacheFileDir ."/". $TTS_FileTemplateDir;}
|
if($TTS_FileTemplateDir =~ m/^\/.*/) { $myFileTemplateDir = $TTS_FileTemplateDir; } else { $myFileTemplateDir = $TTS_CacheFileDir ."/". $TTS_FileTemplateDir;}
|
||||||
@ -814,6 +912,7 @@ sub Text2Speech_DoIt($) {
|
|||||||
undef($TTS_SentenceAppendix) if($TTS_SentenceAppendix && (! -e $TTS_SentenceAppendix));
|
undef($TTS_SentenceAppendix) if($TTS_SentenceAppendix && (! -e $TTS_SentenceAppendix));
|
||||||
|
|
||||||
#Abspielliste erstellen
|
#Abspielliste erstellen
|
||||||
|
my $AnzahlDownloads = 0;
|
||||||
foreach my $t (@{$hash->{helper}{Text2Speech}}) {
|
foreach my $t (@{$hash->{helper}{Text2Speech}}) {
|
||||||
if(-e $t) {
|
if(-e $t) {
|
||||||
# falls eine bestimmte mp3-Datei mit absolutem Pfad gespielt werden soll
|
# falls eine bestimmte mp3-Datei mit absolutem Pfad gespielt werden soll
|
||||||
@ -836,8 +935,10 @@ sub Text2Speech_DoIt($) {
|
|||||||
push(@Mp3WrapText, $t);
|
push(@Mp3WrapText, $t);
|
||||||
} else {
|
} else {
|
||||||
# es befindet sich noch Text zum Download in der Queue
|
# es befindet sich noch Text zum Download in der Queue
|
||||||
if (AttrVal($hash->{NAME}, "TTS_SpeakAsFastAsPossible", 0) == 0) {
|
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);
|
Text2Speech_Download($hash, $file, $t);
|
||||||
|
$AnzahlDownloads ++;
|
||||||
if(-e $file) {
|
if(-e $file) {
|
||||||
push(@Mp3WrapFiles, $file);
|
push(@Mp3WrapFiles, $file);
|
||||||
push(@Mp3WrapText, $t);
|
push(@Mp3WrapText, $t);
|
||||||
@ -853,32 +954,57 @@ sub Text2Speech_DoIt($) {
|
|||||||
|
|
||||||
push(@Mp3WrapFiles, $TTS_SentenceAppendix) if($TTS_SentenceAppendix);
|
push(@Mp3WrapFiles, $TTS_SentenceAppendix) if($TTS_SentenceAppendix);
|
||||||
|
|
||||||
if(scalar(@Mp3WrapFiles) >= 2 && AttrVal($hash->{NAME}, "TTS_UseMP3Wrap", 0) == 1) {
|
if (AttrVal($hash->{NAME}, "TTS_UseMP3Wrap", 0) == 1) {
|
||||||
# benutze das Tool MP3Wrap um bereits einzelne vorhandene Sprachdateien
|
# benutze das Tool MP3Wrap um bereits einzelne vorhandene Sprachdateien
|
||||||
# zusammenzuführen. Ziel: sauberer Sprachfluss
|
# zusammenzuführen. Ziel: sauberer Sprachfluss
|
||||||
Log3 $hash->{NAME}, 4, $hash->{NAME}.": Bearbeite per MP3Wrap jetzt den Text: ". join(" ", @Mp3WrapText);
|
Log3 $hash->{NAME}, 4, $hash->{NAME}.": Bearbeite per MP3Wrap jetzt den Text: ". join(" ", @Mp3WrapText);
|
||||||
|
|
||||||
my $Mp3WrapPrefix = md5_hex(join("|", @Mp3WrapFiles));
|
my $Mp3WrapFile;
|
||||||
my $Mp3WrapFile = $TTS_CacheFileDir ."/". $Mp3WrapPrefix . "_MP3WRAP.mp3";
|
my $Mp3WrapPrefix;
|
||||||
|
$Mp3WrapPrefix = md5_hex(join("|", @Mp3WrapFiles));
|
||||||
|
|
||||||
if(! -e $Mp3WrapFile) {
|
if ($TTS_OutputFile) {
|
||||||
$cmd = "mp3wrap " .$TTS_CacheFileDir. "/" .$Mp3WrapPrefix. ".mp3 " .join(" ", @Mp3WrapFiles);
|
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);
|
$cmd .= " >/dev/null" if($verbose < 5);
|
||||||
|
|
||||||
Log3 $hash->{NAME}, 4, $hash->{NAME}.": " .$cmd;
|
Log3 $hash->{NAME}, 4, $hash->{NAME}.": " .$cmd;
|
||||||
system($cmd);
|
system($cmd);
|
||||||
|
|
||||||
|
my $t = substr($Mp3WrapFile, 0, length($Mp3WrapFile)-4)."_MP3WRAP.mp3";
|
||||||
|
Log3 $hash->{NAME}, 4, $hash->{NAME}.": Benenne Datei um von <".$t."> nach <".$Mp3WrapFile.">";
|
||||||
|
rename($t, $Mp3WrapFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
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") {
|
if ($hash->{MODE} ne "SERVER") {
|
||||||
# im Falls Server, nicht die Datei abspielen
|
# im Server Mode, nicht die Datei abspielen
|
||||||
if(-e $Mp3WrapFile) {
|
if(-e $Mp3WrapFile) {
|
||||||
$cmd = Text2Speech_BuildMplayerCmdString($hash, $Mp3WrapFile);
|
$cmd = Text2Speech_BuildMplayerCmdString($hash, $Mp3WrapFile);
|
||||||
$cmd .= " >/dev/null" if($verbose < 5);
|
$cmd .= " >/dev/null" if($verbose < 5);
|
||||||
|
|
||||||
Log3 $hash->{NAME}, 4, $hash->{NAME}.":" .$cmd;
|
Log3 $hash->{NAME}, 4, $hash->{NAME}.": " .$cmd;
|
||||||
system($cmd);
|
system($cmd);
|
||||||
} else {
|
} else {
|
||||||
Log3 $hash->{NAME}, 2, $hash->{NAME}.": Mp3Wrap Datei konnte nicht angelegt werden.";
|
Log3 $hash->{NAME}, 2, $hash->{NAME}.": Mp3Wrap Datei konnte nicht gefunden werden.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1010,7 +1136,7 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
=pod
|
=pod
|
||||||
=item helper
|
=item helper
|
||||||
=item summary speaks given text via loudspeaker
|
=item summary speaks given text via loudspeaker
|
||||||
=item summary_DE wandelt uebergebenen Text um fuer Ausgabe auf Lautsprecher
|
=item summary_DE wandelt Text in Sprache um zur Ausgabe auf Lautsprecher
|
||||||
=begin html
|
=begin html
|
||||||
|
|
||||||
<a name="Text2Speech"></a>
|
<a name="Text2Speech"></a>
|
||||||
@ -1100,9 +1226,9 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
<a name="Text2Speechattr"></a>
|
<a name="Text2Speechattr"></a>
|
||||||
<b>Attributes</b>
|
<b>Attributes</b>
|
||||||
<ul>
|
<ul>
|
||||||
<li>TTS_Delemiter<br>
|
<li>TTS_Delimiter<br>
|
||||||
optional: By using the google engine, its not possible to convert more than 100 characters in a single audio brick.
|
optional: By using the google engine, its not possible to convert more than 100 characters in a single audio brick.
|
||||||
With a delemiter the audio brick will be split at this character. A delemiter must be a single character.!<br>
|
With a Delimiter the audio brick will be split at this character. A Delimiter must be a single character.!<br>
|
||||||
By default, ech audio brick will be split at sentence end. Is a single sentence longer than 100 characters,
|
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 <i>and</i>.<br>
|
the sentence will be split additionally at comma, semicolon and the word <i>and</i>.<br>
|
||||||
Notice: Only available in locally instances with Google engine!
|
Notice: Only available in locally instances with Google engine!
|
||||||
@ -1136,9 +1262,29 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
<code>wget http://www.dr-bischoff.de/raspi/pico2wave.deb</code><br>
|
<code>wget http://www.dr-bischoff.de/raspi/pico2wave.deb</code><br>
|
||||||
<code>sudo dpkg --install pico2wave.deb</code>
|
<code>sudo dpkg --install pico2wave.deb</code>
|
||||||
</li>
|
</li>
|
||||||
|
<li>Amazon-Polly<br>
|
||||||
|
Using the Amazon Polly engine, the same as Amazon Alexa.<br>
|
||||||
|
The perl package Paws is required. An AWS Access and Polly Aws User is required too<br>
|
||||||
|
<code>cpan paws</code><br>
|
||||||
|
The credentials to your AWS Polly are expected at ~/.aws/credentials<br>
|
||||||
|
<code>[default]
|
||||||
|
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
aws_access_key_id = xxxxxxxxxxxxxxx
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>TTS_Language<br>
|
||||||
|
Selection of different languages
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>TTS_Language_Custom<br>
|
||||||
|
if you want another engine and speech of default languages, you can insert this here.<br>
|
||||||
|
The definition is dependent of used engine. This attribute overrides an TTS_Language attribute.<br>
|
||||||
|
Please refer the specific API reference.
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>TTS_APIKey<br>
|
<li>TTS_APIKey<br>
|
||||||
An APIKey its needed if you want to use VoiceRSS. You have to register at the following page:<br>
|
An APIKey its needed if you want to use VoiceRSS. You have to register at the following page:<br>
|
||||||
http://www.voicerss.org/registration.aspx <br>
|
http://www.voicerss.org/registration.aspx <br>
|
||||||
@ -1293,6 +1439,10 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
hier das Attribut <i>TTS_speakAsFastAsPossible</i> zu verwenden. Die Verwendung des Attributes <i>TTS_useMP3Wrap</i> wird dringend empfohlen.
|
hier das Attribut <i>TTS_speakAsFastAsPossible</i> zu verwenden. Die Verwendung des Attributes <i>TTS_useMP3Wrap</i> wird dringend empfohlen.
|
||||||
Ansonsten wird hier nur der letzte Teiltext als mp3 Datei im Reading dargestellt.
|
Ansonsten wird hier nur der letzte Teiltext als mp3 Datei im Reading dargestellt.
|
||||||
</ul>
|
</ul>
|
||||||
|
<p>
|
||||||
|
<b>Beispiel:</b><br>
|
||||||
|
<code>define MyTTS Text2Speech none</code>
|
||||||
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
@ -1319,13 +1469,22 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
<a name="Text2Speechattr"></a>
|
<a name="Text2Speechattr"></a>
|
||||||
<b>Attribute</b>
|
<b>Attribute</b>
|
||||||
<ul>
|
<ul>
|
||||||
<li>TTS_Delemiter<br>
|
<li>TTS_Delimiter<br>
|
||||||
Optional: Wird ein Delemiter angegeben, so wird der Sprachbaustein an dieser Stelle geteilt.
|
Optional: Wird ein Delimiter angegeben, so wird der Sprachbaustein an dieser Stelle geteilt.
|
||||||
Als Delemiter ist nur ein einzelnes Zeichen zulässig.
|
Als Delimiter ist nur ein einzelnes Zeichen zulässig.
|
||||||
Hintergrund ist die Tatsache, das die Google Sprachengine nur 100Zeichen zulässt.<br>
|
Hintergrund ist die Tatsache, das die einige Sprachengines nur eine bestimmt Anzahl an Zeichen (zb. Google nur 100Zeichen) zulässt.<br>
|
||||||
Im Standard wird nach jedem Satzende geteilt. Ist ein einzelner Satz länger als 100 Zeichen,
|
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 <i>und</i> geteilt.<br>
|
so wird zusätzlich nach Kommata, Semikolon und dem Verbindungswort <i>und</i> geteilt.<br>
|
||||||
Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich und nur bei Nutzung der Google Sprachengine relevant!
|
Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich!<br>
|
||||||
|
<b>Notation</b><br>
|
||||||
|
+ -> erzwinge das Trennen, auch wenn Textbaustein < x Zeichen<br>
|
||||||
|
- -> Trenne nur wenn Textbaustein > x Zeichen
|
||||||
|
af -> add first -> füge den Delimiter am Satzanfang wieder hinzu<br>
|
||||||
|
al -> add last -> füge den Delimiter am Satzende wieder hinzu<br>
|
||||||
|
an -> add nothing -> Delimiter nicht wieder hinzufügen<br>
|
||||||
|
~ -> der Delimiter<br>
|
||||||
|
<b>Beispiel</b><br>
|
||||||
|
<code>attr myTTS TTS_Delimiter -al.</code>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>TTS_Ressource<br>
|
<li>TTS_Ressource<br>
|
||||||
@ -1340,7 +1499,7 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
Nutzung der VoiceRSS Sprachengine. Die Nutzung ist frei bis zu 350 Anfragen pro Tag.
|
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!
|
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.
|
Aufgrund der Qualität ist der Einsatz diese Engine ebenfalls zu empfehlen.
|
||||||
Wenn diese Engine benutzt wird, ist ein APIKey notwendig (siehe TTXS_APIKey)
|
Wenn diese Engine benutzt wird, ist ein APIKey notwendig (siehe TTS_APIKey)
|
||||||
</li>
|
</li>
|
||||||
<li>ESpeak<br>
|
<li>ESpeak<br>
|
||||||
Nutzung der ESpeak Offline Sprachengine. Die Qualitä ist schlechter als die Google Engine.
|
Nutzung der ESpeak Offline Sprachengine. Die Qualitä ist schlechter als die Google Engine.
|
||||||
@ -1358,9 +1517,29 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
<code>wget http://www.dr-bischoff.de/raspi/pico2wave.deb</code><br>
|
<code>wget http://www.dr-bischoff.de/raspi/pico2wave.deb</code><br>
|
||||||
<code>sudo dpkg --install pico2wave.deb</code>
|
<code>sudo dpkg --install pico2wave.deb</code>
|
||||||
</li>
|
</li>
|
||||||
|
<li>Amazon-Polly<br>
|
||||||
|
Nutzung der Amazon Polly Sprachengine, dieselbe Engine wie für Amazon Alexa verwendet wird.<br>
|
||||||
|
Es muss das Perl Package Paws installiert sowie ein AWS Konto und ein Polly AWS User verfügbar sein<br>
|
||||||
|
<code>cpan paws</code><br>
|
||||||
|
Die Zugangsdaten zum eigenen AWS Konto müssen unter ~/.aws/credentials liegen. <br>
|
||||||
|
<code>[default]
|
||||||
|
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
aws_access_key_id = xxxxxxxxxxxxxxx
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>TTS_Language<br>
|
||||||
|
Auswahl verschiendener Standardsprachen
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>TTS_Language_Custom<br>
|
||||||
|
Möchte man eine Spreche und Stimme abweichend der Standardspreachen verwenden, so kann man diese hier eintragen. <br>
|
||||||
|
Die Definition ist abhängig der verwendeten Sprachengine. Diesea Attribut überschreibt ein ev. vorhandenes TTS_Langugae Attribut.<br>
|
||||||
|
Siehe in die jeweilige API Referenz
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>TTS_APIKey<br>
|
<li>TTS_APIKey<br>
|
||||||
Wenn VoiceRSS genutzt wird, ist ein APIKey notwendig. Um diesen zu erhalten ist eine vorherige
|
Wenn VoiceRSS genutzt wird, ist ein APIKey notwendig. Um diesen zu erhalten ist eine vorherige
|
||||||
Registrierung notwendig. Anschließend erhält man den APIKey <br>
|
Registrierung notwendig. Anschließend erhält man den APIKey <br>
|
||||||
@ -1373,13 +1552,13 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
|
|
||||||
<li>TTS_CacheFileDir<br>
|
<li>TTS_CacheFileDir<br>
|
||||||
Optional: Die per Google geladenen Sprachbausteine werden in diesem Verzeichnis zur Wiedeverwendung abgelegt.
|
Optional: Die per Google geladenen Sprachbausteine werden in diesem Verzeichnis zur Wiedeverwendung abgelegt.
|
||||||
Es findet zurZEit keine automatisierte Löschung statt.<br>
|
Es findet zurZeit keine automatisierte Löschung statt.<br>
|
||||||
Default: <i>cache/</i><br>
|
Default: <i>cache/</i><br>
|
||||||
Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich!
|
Achtung: Nur bei einem lokal definierter Text2Speech Instanz möglich!
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>TTS_UseMP3Wrap<br>
|
<li>TTS_UseMP3Wrap<br>
|
||||||
Optional: Für eine flüssige Sprachausgabe ist es zu empfehlen, die einzelnen vorher per Google
|
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
|
geladenen Sprachbausteine zu einem einzelnen Sprachbaustein zusammenfassen zu lassen bevor dieses per
|
||||||
Mplayer ausgegeben werden. Dazu muss Mp3Wrap installiert werden.<br>
|
Mplayer ausgegeben werden. Dazu muss Mp3Wrap installiert werden.<br>
|
||||||
<code>apt-get install mp3wrap</code><br>
|
<code>apt-get install mp3wrap</code><br>
|
||||||
@ -1387,8 +1566,17 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>TTS_MplayerCall<br>
|
<li>TTS_MplayerCall<br>
|
||||||
Optional: Angabe der Systemaufrufes zu Mplayer. Das folgende Beispiel ist der Standardaufruf.<br>
|
Optional: Angabe des Systemaufrufes zu Mplayer oder einem anderem Tool. Wird ein anderes Tool als mplayer<br>
|
||||||
Beispiel: <code>sudo /usr/bin/mplayer</code>
|
dort verwendet gelten folgende Templates: <br>
|
||||||
|
<ul>
|
||||||
|
<li>{device}</li>
|
||||||
|
<li>{volume}</li>
|
||||||
|
<li>{volumeadjust}</li>
|
||||||
|
<li>{file}</li>
|
||||||
|
</ul>
|
||||||
|
Beispiele:<br>
|
||||||
|
<code>attr myTTS TTS_MplayerCall sudo /usr/bin/mplayer</code>
|
||||||
|
<code>attr myTTS TTS_MplayerCall AUDIODEV={device} play -q -v {volume} {file}</code>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>TTS_SentenceAppendix<br>
|
<li>TTS_SentenceAppendix<br>
|
||||||
@ -1432,6 +1620,15 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
Attribut nur verfügbar bei einer lokalen oder Server Instanz
|
Attribut nur verfügbar bei einer lokalen oder Server Instanz
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>TTS_OutputFile<br>
|
||||||
|
Angabe eines fixen Dateinamens als mp3 Output. Das Attribut ist nur relevant in Verbindung mit TTS_UseMP3Wrap.<br>
|
||||||
|
Wenn ein Dateinamen angegeben wird, so wird zusätzlich TTS_CacheFileDir beachtet. Bei einer absoluten Pfadangabe <br>
|
||||||
|
muss der Dateipfad durch FHEM schreibbar sein.<br>
|
||||||
|
<code>attr myTTS TTS_OutputFile output.mp3</code><br>
|
||||||
|
<code>attr myTTS TTS_OutputFile /media/miniDLNA/output.mp3</code><br>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li><a href="#readingFnAttributes">readingFnAttributes</a>
|
<li><a href="#readingFnAttributes">readingFnAttributes</a>
|
||||||
</li><br>
|
</li><br>
|
||||||
|
|
||||||
@ -1451,6 +1648,12 @@ sub Text2Speech_WriteStats($$$$){
|
|||||||
<a name="Text2SpeechExamples"></a>
|
<a name="Text2SpeechExamples"></a>
|
||||||
<b>Beispiele</b>
|
<b>Beispiele</b>
|
||||||
<ul>
|
<ul>
|
||||||
|
<code>define TTS_EG_WZ Text2Speech hw=/dev/snd/controlC3</code><br>
|
||||||
|
<code>attr TTS_EG_WZ TTS_Language Deutsch</code><br>
|
||||||
|
<code>attr TTS_EG_WZ TTS_MplayerCall /usr/bin/mplayer</code><br>
|
||||||
|
<code>attr TTS_EG_WZ TTS_Ressource Amazon-Polly</code><br>
|
||||||
|
<code>attr TTS_EG_WZ TTS_UseMP3Wrap 1</code><br>
|
||||||
|
<br>
|
||||||
<code>define MyTTS Text2Speech hw=0.0</code><br>
|
<code>define MyTTS Text2Speech hw=0.0</code><br>
|
||||||
<code>set MyTTS tts Die Alarmanlage ist bereit.</code><br>
|
<code>set MyTTS tts Die Alarmanlage ist bereit.</code><br>
|
||||||
<code>set MyTTS tts :beep.mp3:</code><br>
|
<code>set MyTTS tts :beep.mp3:</code><br>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user