- feature: new module 98_Text2Speech.pm added (Tobias Faust)

Google Translator Engine or ESpeak can be used


git-svn-id: svn://svn.code.sf.net/p/fhem/code/trunk@4655 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
tobiasfaust
2014-01-15 10:24:29 +00:00
parent d75c6a5426
commit 41ef277d7c
3 changed files with 824 additions and 0 deletions

View File

@@ -1,6 +1,8 @@
# 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.
- SVN - SVN
- feature: new module 98_Text2Speech.pm added (Tobias Faust)
Google Translator Engine or ESpeak can be used
- feature: YAMAHA_AVR: define separate on and off status intervals for - feature: YAMAHA_AVR: define separate on and off status intervals for
cyclic status updates cyclic status updates
- feature: Visualizations (Plots) for SYSMON added - feature: Visualizations (Plots) for SYSMON added

821
fhem/FHEM/98_Text2Speech.pm Normal file
View File

@@ -0,0 +1,821 @@
##############################################
# $Id$
#
# 98_Text2Speech.pm
#
# written by Tobias Faust 2013-10-23
# e-mail: tobias dot faust at online dot de
#
##############################################
##############################################
# EDITOR=nano
# visudo
# ALL ALL = NOPASSWD: /usr/bin/mplayer
##############################################
package main;
use strict;
use warnings;
use Blocking;
use IO::File;
use HttpUtils;
use Digest::MD5 qw(md5_hex);
use URI::Escape;
use Data::Dumper;
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 -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 $mplayerNoDebug = '-really-quiet';
my $mplayerAudioOpts = '-ao alsa:device=';
#my $ttsAddr = 'http://translate.google.com/translate_tts?tl=de&q=';
my $ttsHost = 'translate.google.com';
my $ttsPath = '/translate_tts?tl=de&q=';
##########################
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_Delemiter".
" TTS_Ressource:Google,ESpeak".
" TTS_CacheFileDir".
" TTS_UseMP3Wrap:0,1".
" TTS_MplayerCall".
" ".$readingFnAttributes;
}
##########################
# Define <tts> Text2Speech <alsa-device>
# Define <tts> 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 <name> Text2Speech <alsa-device>\n".
"see at /etc/asound.conf\n".
"or remote syntax: define <name> 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";
} 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";
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;
}
###################################
# 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(@) {
my @a = @_;
my $do = 0;
my $hash = $defs{$a[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");
if($a[2] eq "TTS_Delemiter") {
my $TTS_Delemiter = $a[3];
return "wrong delemiter syntax: [+-]a[lfn]. \n".
" Example 1: +an~\n".
" Example 2: +al." if($TTS_Delemiter =~ m/^([+-]a[lfn]){0,1}(.){1}$/i);
return "This Attribute is only available in direct mode" if($hash->{MODE} ne "DIRECT");
} elsif ($a[2] eq "TTS_Ressource") {
return "This Attribute is only available in direct mode" if($hash->{MODE} ne "DIRECT");
} elsif ($a[2] eq "TTS_CacheFileDir") {
return "This Attribute is only available in direct mode" if($hash->{MODE} ne "DIRECT");
} elsif ($a[2] eq "TTS_UseMP3Wrap") {
return "This Attribute is only available in direct mode" if($hash->{MODE} ne "DIRECT");
}
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") if(!$@);
} else {
$conn = IO::Socket::INET->new(PeerAddr => $dev);
}
if(!$conn) {
Log3($name, 3, "Text2Speech: 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 tts $msg";
Text2Speech_OpenDev($hash) if(!$hash->{TCPDev});
#lets try again
Text2Speech_OpenDev($hash) if(!$hash->{TCPDev});
if($hash->{TCPDev}) {
Log3 $hash, 4, "Text2Speech: Write remote message to $dev: $call";
Log3 $hash, 3, "Text2Speech: 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};
return "no set argument specified" if(int(@a) < 2);
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 undef if(AttrVal($hash->{NAME}, "disable", "0") eq "1");
if($cmd eq "tts") {
if($hash->{MODE} eq "DIRECT") {
Text2Speech_PrepareSpeech($hash, join(" ", @a));
$hash->{helper}{RUNNING_PID} = BlockingCall("Text2Speech_DoIt", $hash, "Text2Speech_Done", 60, "Text2Speech_AbortFn", $hash) unless(exists($hash->{helper}{RUNNING_PID}));
} elsif ($hash->{MODE} eq "REMOTE") {
Text2Speech_Write($hash, join(" ", @a));
} else {return undef;}
} elsif($cmd eq "volume") {
my $vol = join(" ", @a);
return "volume adjusting only available in direct mode" if($hash->{MODE} ne "DIRECT");
return "volume level expects 0..100 percent" if($vol !~ m/^([0-9]{1,2})$/);
$hash->{VOLUME} = $vol if($vol ne 100);
delete($hash->{VOLUME}) if($vol eq 100);
}
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
#
#####################################
sub Text2Speech_PrepareSpeech($$) {
my ($hash, $t) = @_;
my $TTS_Ressource = AttrVal($hash->{NAME}, "TTS_Ressource", "Google");
my $TTS_Delemiter = AttrVal($hash->{NAME}, "TTS_Delemiter", undef);
my $TTS_ForceSplit = 0;
my $TTS_AddDelemiter;
if($TTS_Delemiter && $TTS_Delemiter =~ m/^[+-]a[lfn]/i) {
$TTS_ForceSplit = 1 if(substr($TTS_Delemiter,0,1) eq "+");
$TTS_ForceSplit = 0 if(substr($TTS_Delemiter,0,1) eq "-");
$TTS_AddDelemiter = substr($TTS_Delemiter,1,2); # af, al oder an
$TTS_Delemiter = substr($TTS_Delemiter,3);
} elsif (!$TTS_Delemiter) { # Default wenn Attr nicht gesetzt
$TTS_Delemiter = "(?<=[\\.!?])\\s*";
$TTS_ForceSplit = 1;
$TTS_AddDelemiter = "";
}
if($TTS_Ressource eq "Google") {
my @text;
$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/ß/sz/g;
@text = $hash->{helper}{Text2Speech} if($hash->{helper}{Text2Speech}[0]);
push(@text, $t);
@text = Text2Speech_SplitString(\@text, 100, $TTS_Delemiter, $TTS_ForceSplit, $TTS_AddDelemiter);
@text = Text2Speech_SplitString(\@text, 100, "(?<=[\\.!?])\\s*", 0, "");
@text = Text2Speech_SplitString(\@text, 100, ",", 0, "al");
@text = Text2Speech_SplitString(\@text, 100, ";", 0, "al");
@text = Text2Speech_SplitString(\@text, 100, "und", 0, "af");
@{$hash->{helper}{Text2Speech}} = @text;
} else {
push(@{$hash->{helper}{Text2Speech}}, $t);
}
}
#####################################
# param1: array : Text 2 Speech
# param2: string: MaxChar
# param3: string: Delemiter
# param4: int : 1 -> es wird am Delemiter gesplittet
# 0 -> es wird nur gesplittet, wenn Stringlänge länger als MaxChar
# param5: string: Add Delemiter to String? [pre|past|<empty>]
#
# Splittet die Texte aus $hash->{helper}->{Text2Speech} anhand des
# Delemiters, wenn die Stringlänge MaxChars übersteigt.
# Ist "AddDelemiter" angegeben, so wird der Delemiter an den
# String wieder angefügt
#####################################
sub Text2Speech_SplitString(@$$$$){
my @text = @{$_[0]};
my $MaxChar = $_[1];
my $Delemiter = $_[2];
my $ForceSplit = $_[3];
my $AddDelemiter = $_[4];
my @newText;
for(my $i=0; $i<(@text); $i++) {
if((length($text[$i]) <= 100) && (!$ForceSplit)) { #Google kann nur 100zeichen
push(@newText, $text[$i]);
next;
}
my @b = split(/$Delemiter/, $text[$i]);
for(my $j=0; $j<(@b); $j++) {
$b[$j] = $b[$j] . $Delemiter if($AddDelemiter eq "al"); # Am Satzende wieder hinzufügen.
$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]);
}
}
return @newText;
}
#####################################
# param1: hash : Hash
# param2: string: Typ (mplayer oder mp3wrap oder ....)
# param3: 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 $verbose = AttrVal($hash->{NAME}, "verbose", 3);
if($hash->{VOLUME}) { # per: set <name> volume <..>
$mplayerOpts .= " -softvol -softvol-max 110 -volume " . $hash->{VOLUME};
}
my $AlsaDevice = $hash->{ALSADEVICE};
if($AlsaDevice eq "none") {
$AlsaDevice = "";
$mplayerAudioOpts = "";

View File

@@ -173,6 +173,7 @@ FHEM/95_Dashboard.pm svenson08 http://forum.fhem.de Frontends
FHEM/95_PachLog.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges FHEM/95_PachLog.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges
FHEM/95_holiday.pm rudolfkoenig http://forum.fhem.de Sonstiges FHEM/95_holiday.pm rudolfkoenig http://forum.fhem.de Sonstiges
FHEM/95_remotecontrol.pm ulimaass http://forum.fhem.de Frontends FHEM/95_remotecontrol.pm ulimaass http://forum.fhem.de Frontends
FHEM/98_Text2Speech.pm tobiasfaust http://forum.fhem.de Unterstuetzende Dienste
FHEM/98_apptime.pm martinp876 http://forum.fhem.de Sonstiges FHEM/98_apptime.pm martinp876 http://forum.fhem.de Sonstiges
FHEM/98_CULflash.pm rudolfkoenig http://forum.fhem.de Sonstiges FHEM/98_CULflash.pm rudolfkoenig http://forum.fhem.de Sonstiges
FHEM/98_GEOFANCY.pm loredo http://forum.fhem.de Unterstuetzende Dienste FHEM/98_GEOFANCY.pm loredo http://forum.fhem.de Unterstuetzende Dienste