Autor Thema: [Neues Modul] BOSE SoundTouch  (Gelesen 119660 mal)

Offline Prof. Dr. Peter Henning

  • Developer
  • Hero Member
  • ****
  • Beiträge: 7299
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #630 am: 03 April 2020, 18:02:14 »
OK,next step.

Define a directory for miniDLNA where the "Music" files reside, and let your BOSE box find the miniDLNA.

Then, store a file called "speech.mp3" in this directory.

Where do you get this file? Well, for the moment we try a simple approach, and fetch this from Google. Enter into your 99_myUtils.pm the code


##############################################################################
#
#  Obtain a speechlet file from Google
#
##############################################################################

sub tts_google($$){
  my ($text,$file) = @_;
  #-- obtain speechlet
  system("wget -q --user-agent='Mozilla/5.0' \'http://translate.google.de/translate_tts?ie=UTF-8&tl=de&client=tw-ob&q=$text\' -O $file.mp3");
and then from your FHEM command line excute
{tts_google("This is my first test of speech on BOSE","<directoryname>/speech")}This should write the speech.mp3 into that directory.

Note added in proof: In the code above you should replace tl=de by the language you want to have, say tl=fr or tl=en

You might have trouble in the beginning, maybe because the directory is not writable from FHEM, or maybe mniDLNA is not configured properly yet. Later on, I'll give you a code to control miniDLNA from FHEM - for the moment it should be enough to start a miniDLNA rescan by hand, simply by restarting the miniDLNA process. If then, on your server, you look at http://<ip-address>:8200 you should see the miniDLNA running and showing at least one audio file - your speech.mp3

If everything is ok, the following things should apply:

1. speech,mp3 has been obtained from google, and it contains the voice transcript of your sentence (check !)
2. speech.mp3 is found by miniDLNA
3. miniDLNA is found by the BOSe boxes
4. By issueing "set <BOSE Box> playTrack speech" to FHEM, you should hear the Google voice.

Regards

pah

Offline elmattt

  • New Member
  • *
  • Beiträge: 8
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #631 am: 03 April 2020, 18:57:48 »
ok, minidlna is running, there is one audio file in it, and i can play it with the soundtouch app !!!
everything for you list from 1 to 4 is ok ;)

Offline Prof. Dr. Peter Henning

  • Developer
  • Hero Member
  • ****
  • Beiträge: 7299
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #632 am: 03 April 2020, 20:58:13 »
Fine. Next step is to set up code that take a longer text, splits it into chunks, passes these to Google (i prefer Amazon Polly), assembles them together, puts them into miniDLNA, caches them (such that the second identical message is not passed to Google or Amazon) or rather looks up the cache if the speechlet is already present. And then play it.

Also, this allows to put a string :3-digit-number: into any text. This string is replaced automatically by one of the predefined speech messages (I call them phrases) - they may be obtained by the subroutine tts_allnew() Note: They should be obtained from Google in your case.

Right now I have other things to do, so I post the code without commenting on it. MAybe I'll be back online tomorrow afternoon


Regards

pah


use Paws; # only for Polly
use Text::Iconv;
use MP3::Info;
use DBI;


my @ttsFiles=();

##############################################################################
#
#  TTS phrases
#
##############################################################################

my %ttsPhrases = (
"001_jeanniehilfe" => "Hallo, ich bin Jeannie. Wie kann ich Dir helfen?",
"002_ok" => "Ookeh",
"003_tml" => "Es tut mir leid, das habe ich nicht verstanden",
"011_hoftuer_sichern" => "Die Hoftür wird gesichert.",
"012_hoftuer_gesichert" => "Die Hoftür ist gesichert.",
"013_hoftuer_entsichern" => "Sicherung der Hoftür aufgehoben.",
"014_hoftuer_ungesichert" => "Die Hoftür ist nicht gesichert.",
"015_haustuer_sichern" => "Die Hauseingangstür wird gesichert.",
"016_haustuer_gesichert" => "Die Hauseingangstür ist gesichert.",
"017_haustuer_entsichern" => "Sicherung der Hauseingangstür aufgehoben.",
... several more lines of this type
"121_dachfoffen" => "Mindestens ein Dachfenster ist offen."
);

##############################################################################
#
#  Obtain new speechlets for all phrases
# aws_access_key_id=xxxxxxxxxxxxxxxxxxxxxxxxxx
# aws_secret_access_key=xxxxxxxxxxxxxxxxxxxxxxxx
#
##############################################################################

sub tts_allnew(){
  my $polly = Paws->service('Polly', region=>'eu-central-1');
 
  foreach my $file (keys %ttsPhrases) {
    my $converter = Text::Iconv->new("utf-8", "iso-8859-1");
    my $text = $converter->convert($ttsPhrases{$file});
    my $ssmltext="<speak>$text</speak>";
 
    #-- obtain speechlet 
    my $res = $polly->SynthesizeSpeech(
       VoiceId => 'Marlene',
       TextType=> 'ssml',
       Text => $ssmltext,
       OutputFormat => 'mp3');
     
    #-- write stream
    my $mp3;
    open($mp3, '>', "/home/fhem/ttsNew/".$file.".mp3");
    binmode($mp3);
    print $mp3 $res->AudioStream;
    close($mp3);
  }
}
 
#my %ttsMaxChar      = ("Google"     => 100,
#                       "VoiceRSS"   => 300,
#                       "SVOX-pico"  => 1000,
#                       "Amazon-Polly" => 3000
#                       );

#############################################################################
#
#  speak as the central output routine
#
#############################################################################
                                       
sub speak($$){
  my ($name,$text) = @_;
 
  fhem("setreading SPEAK $name.out ".substr($text,0,12));
  fhem("setreading SPEAK lastoutput $name");
  fhem("setreading SPEAK length ".length($text));
  fhem("setreading SPEAK duration ".length($text)/9);
 
  #-- Telegram, therefore distant
  if($name =~ /Telegram.*/){
    fhem193Cmd("{telegramRecognition('botreply: ".$text."')}");
    fhem("setreading SPEAK Telegram.out ".substr($text,0,12));
  #-- Digital Audio Device
  }elsif($name =~ /(^RPI)|(^Rpi)|(Sound)/){
    speakDAD($name,$text,"p");
  #-- Tablet
  }elsif($name =~ /(^Tab)|(HTC)/){   
    speakTablet($name,$text);
  #-- HTC,SZ therefore reroutable ==> check here
  }elsif($name =~ /SZ/){
    if( Value("BOSE_50338B343509") eq "playing" ){
      speakDAD("SoundTouch1.OG",$text,"p");
    }else{
      speakTablet("HTC.OG",$text);
    }
  #-- WZ,MSG therefore reroutable ==> check in 193
  }elsif($name =~ /(WZ)|(MSG)|(Msg)/){
    if( Value("BOSE_C4F312DD64C7") eq "playing" ){
      speakDAD("SoundTouch.EG",$text,"p");
    }else{
      speakTablet("Tab1.EG",$text);
    }
  }else{
    Log 1,"[speak] Error, Device $name unknown (text=$text)";
  }
}


#############################################################################
#
#  speak for digital audio devices
#
#############################################################################

sub speakDAD{
  my ($name,$text,$ttsResource) = @_;
  my ($daddev,$vol,$cmd);
  my $dadtype="";
 
  #-- default voice
  $ttsResource = "p"
    if(!defined($ttsResource));
  my $ttsMaxChar = 3000;
 
  #-- output device
  if($name =~ /R(P|p)(I|i).*DG/){
    $dadtype = "RPI";
    $daddev  = "RpiAudio.DG";
    $vol     = 25;
  }elsif($name =~ /R?(P|p)(I|i).*Meson/){
    $dadtype = "RPI";
    $daddev  = "RpiAudio.Meson";
    $vol     = 25;
  }elsif($name eq "SoundTouch.EG"){
    $dadtype = "BOSE";
    $daddev  = "BOSE_C4F312DD64C7";
    $vol     = 25; 
  }elsif($name eq "SoundTouch.EZ"){
    $dadtype = "BOSE";
    $daddev  = "BOSE_2C6B7D4B53A4";
    $vol     = 25;
  }elsif($name =~ /SoundTouch.?.OG/){
    $dadtype = "BOSE";
    $daddev  = "BOSE_50338B343509";
    $vol     = 25;
  #-- unknown DAD
  }else{
    $dadtype = "unknown";
    $daddev  = "";
    $vol     = 0;
    Log 1,"[speakDAD] called with unknown device name $name (text=$text)";
    return;
  }
 
  #-- make it sound better
  $text =~ s/(\d)\.(\d)/$1,$2/g;
  $text =~ s/Hallo, ich bin Jeannie. Wie kann ich Dir helfen\?/:001:/;
  $text =~ s/^OK$/:002:/;
  $text =~ s/^Es tut mir leid, das habe ich nicht verstanden$/:003:/;
  $text =~ s/Jeannie/dschini/;
  $text =~ s/Jacqueline/dschacklihn/;
  $text =~ s/^OK/Ookeeh/;
  $text =~ s/\:P\:/<break time="1s"\/>/g;
 
  fhem("setreading SPEAK $name.out ".substr($text,0,12));
 
  #-- settings
  #my $joiner     = "sox";
  my $joiner     = "mp3wrap";
  my $ttspath    = "/home/fhem";
  my $ttsDir     = "/home/fhem/tts/";
  my $ttsCDir    = "/home/fhem/ttsCache/";
  my $ttsTarget  = "speech";
 
  #-- delete old file to outsmart miniDLNA caching
  unlink $ttsDir.$ttsTarget.".mp3";
  Log 1, "[speakDAD] File ".$ttsDir.$ttsTarget.".mp3 could not be deleted"
    if(-e $ttsDir.$ttsTarget.".mp3");
 
  #-- obtain list of prerecorded files
  opendir (DIR, $ttsDir);
  while (my $ttsFile = readdir(DIR)) {
    next if ($ttsFile !~ m/\.mp3$/);
      push(@ttsFiles,$ttsFile);
    }
  closedir(DIR);
 
  #{Dumper(Text2Speech_SplitString(["Das ist ein Test"],10," ",0,"al"))}
  #{Dumper(Text2Speech_SplitString(["Das ist ein Test, in dem ein kleiner Teil <amazon:effect name=\"whispered\">geflüstert</amazon:effect> wird"],10," ",0,"al"))}
  #-- split text at word boundaries
  my @words      = split(' ', $text);
  my @speechlets = ();
  my $speechlet;
   
  my $i=0;
  my $j=0;
  while($i<int(@words)){
    last
      if( !$words[$i] );
    #-- word does not contain prerecorded filename, simply append words
    if( $words[$i] !~ /^\:(.*)\:$/ ){
      $speechlet="";
      while( (length($speechlet) + length($words[$i])) < $ttsMaxChar && $i<int(@words) ){
        $speechlet .= $words[$i]." ";
        $i++;
      }
      push(@speechlets,$speechlet);
      $j++;
      my $ttsKey   = md5_hex($ttsResource."|".$speechlet);
      my $ttsCFile = $ttsTarget."_".$ttsKey;
      my $found    = tts_dblog($ttsKey,$speechlet);

      #-- check if already exists     
      #if( -e $ttsCDir.$ttsCFile.".mp3" ) {
      if( $found == 1 ){
        Log 1,"[speakDAD] Speechlet '".$speechlet."' already in cache as ".$ttsCDir.$ttsCFile.".mp3";
      }else{
        if( $ttsResource =~ /p.*$/ ){
          tts_polly($speechlet,$ttsCDir.$ttsCFile);
        }else{
          tts_google($speechlet,$ttsCDir.$ttsCFile);
        }
        Log 1,"[speakDAD] Speechlet '".$speechlet."' obtained as ".$ttsCDir.$ttsCFile.".mp3";
      }
      $cmd .= $ttsCDir.$ttsCFile.".mp3 ";
     
    #-- word is prerecorded filename
    }else{
      my $name   = $1;
      my $ttsPFile = "";
      foreach my $k (@ttsFiles){
        if( $k =~ /$name/ ){
          $ttsPFile = $k;   
          last;
        }
      } 
      if($ttsPFile ne ""){
        $cmd .= $ttsDir.$ttsPFile." ";
        Log 5,"[speakDAD] Filename $name resolved as ".$ttsDir.$ttsPFile;
      }else{
        Log 5,"[speakDAD] Filename $name not resolved";
      }
      $i++; 
    }
  }
 
  #-- join speechlets and auxiliary files
  if( $joiner eq "sox" ){
    $cmd = "sox ".$ttsDir."silence_48_22050_1.mp3 ".$cmd." ".$ttsDir.$ttsTarget.".mp3" ;
    system($cmd);
    #-- write MP3 tags
    $cmd ="id3tag -sfhemspeech -aFHEM -Cdesc -Aalbum ".$ttsDir.$ttsTarget.".mp3 > /dev/null";
    system($cmd);
  }elsif( $joiner eq "mp3wrap" ){
    $cmd = "mp3wrap ".$ttsDir.$ttsTarget.".mp3 ".$ttsDir."silence_48_22050_1.mp3 ".$cmd." > /dev/null";
    system($cmd);
    #-- write MP3 tags
    $cmd ="id3tag -sfhemspeech -aFHEM -Cdesc -Aalbum ".$ttsDir.$ttsTarget."_MP3WRAP.mp3 > /dev/null";
    system($cmd);
    #-- move file to final location
    $cmd = "mv ".$ttsDir.$ttsTarget."_MP3WRAP.mp3 ".$ttsDir.$ttsTarget.".mp3";
    system($cmd);
  }else{
    Log 1,"[speakDAD] No MP3 joiner defined, playing ".substr($cmd,0,index($cmd,' '));
    #-- todo: take out first file as filename and copy to speech.mp3
    $cmd = "cp ".substr($cmd,0,index($cmd,' '))." ".$ttsDir.$ttsTarget.".mp3";
    system($cmd);
  }
  #-- get duration
  Log 1,"[speakDAD] duration of final file is ".tts_duration($ttsDir.$ttsTarget.".mp3")." s";
 
  #-- play it
  if( $dadtype eq "BOSE" ){
    BOSERepsas($daddev);
    fhem("set $daddev volume $vol");
    fhem("set $daddev playTrack fhemspeech");
  }elsif( $dadtype eq "RPI" ){
    fhem("set $daddev volume $vol");
    fhem("set $daddev origin speech");
  } 
}

##############################################################################
#
#  Write into speechlet database
#
##############################################################################

sub tts_dblog($$){
  my ($key,$text) = @_;
  #-- Create database by
  #  sqlite3 tts.db "CREATE TABLE tts (key INTEGER PRIMARY KEY, count INT, time INT, content TEXT);"
  #  chown fhem:dialout tts.db
  #--
  my $path = "/home/fhem";
  my $dbargs = {AutoCommit => 0, PrintError => 1};
  my $dbh = DBI->connect("dbi:SQLite:dbname=".$path."/tts.db", "", "", $dbargs);
 
  #-- check existence
  my ($res) = $dbh->selectrow_array("SELECT EXISTS ( SELECT 1 FROM tts WHERE key='".$key."')");
  #-- exists
  if( $res == 0 ){
    #-- insert a line
    $dbh->do("INSERT INTO tts (key,count,time,content) VALUES ('".$key."','1','".time()."','".$text."');");
    if ($dbh->err()) {
      Log 1,"[tts_dblog] $DBI::errstr";
    }
  #-- does not exist
  }else{
    #-- modify
    $dbh->do("UPDATE tts SET count = count + 1 WHERE key='".$key."';");
    if ($dbh->err()) {
      Log 1,"[tts_dblog] $DBI::errstr";
    }
  }
  $dbh->commit();
  $dbh->disconnect();
  return $res;
}

##############################################################################
#
#  Display speechlet database
#
##############################################################################

sub tts_dbshow(){
  #--
  my $path   = "/home/fhem";
  my $dbargs = {AutoCommit => 0, PrintError => 1};
  my $dbh    = DBI->connect("dbi:SQLite:dbname=".$path."/tts.db", "", "", $dbargs);
  my $ret    = "";

  #-- print rows
  my $res = $dbh->selectall_arrayref("SELECT key,count,time,content FROM tts;");
  foreach my $row (@$res) {
    my ($keyr,$countr,$timer,$textr) = @$row;
    $ret .= sprintf("%s %d %d %s\n",$keyr,$countr,$timer,$textr);
  }
  $dbh->disconnect();
  return $ret;
}

##############################################################################
#
#  Purge speechlet database
#
##############################################################################

sub tts_dbpurge($$){
  my ($deltime,$mincount) = @_;
  #--
  my $path   = "/home/fhem";
  my $dbargs = {AutoCommit => 0, PrintError => 1};
  my $dbh    = DBI->connect("dbi:SQLite:dbname=".$path."/tts.db", "", "", $dbargs);

  if( defined($deltime) && $deltime > 0){
    my $earliest = time() - 86400*$deltime;
    $dbh->do("DELETE from tts where time < ".$earliest.";");
    if ($dbh->err()) {
      Log 1,"[tts_dblog] $DBI::errstr";
    }
  }elsif( defined($mincount) && $mincount > 0){
    $dbh->do("DELETE from tts where count < ".$mincount.";");
    if ($dbh->err()) {
      Log 1,"[tts_dblog] $DBI::errstr";
    }
  }
  $dbh->disconnect();
}

##############################################################################
#
#  Obtain a speechlet file from Amazon Polly
#
##############################################################################

sub tts_polly($$){
  my ($texti,$file) = @_;
    #-- we may have problems with umlaut characters
  my $converter = Text::Iconv->new("utf-8", "iso-8859-1");
  my $text = $converter->convert($texti);
 
  #<break time="1s"/>
  my $ssmltext = '<speak>'.$text.'</speak>';
 
  #-- obtain speechlet
  my $polly = Paws->service('Polly', region=>'eu-central-1');
  my $res = $polly->SynthesizeSpeech(
     VoiceId => 'Marlene',
     TextType=> 'ssml',
     Text => $ssmltext,
     OutputFormat => 'mp3');

  #-- write stream
  my $mp3;
  open($mp3, '>', $file.".mp3");
  binmode($mp3);
  print $mp3 $res->AudioStream;
  close($mp3);
}

##############################################################################
#
#  Obtain a speechlet file from Google
#
##############################################################################

sub tts_google($$){
  my ($text,$file) = @_;
  #-- obtain speechlet
  system("wget -q --user-agent='Mozilla/5.0' \'http://translate.google.de/translate_tts?ie=UTF-8&tl=de&client=tw-ob&q=$text\' -O $file.mp3");


##############################################################################
#
#  Determine duration of speech file
#
##############################################################################

sub tts_duration($) {
  my $time;
  my ($file) = @_;
  eval {
    use MP3::Info;   
    my $tag = get_mp3info($file);
    if ($tag && defined($tag->{SECS})) {
  $time = int($tag->{SECS}+0.5);
    }
  };
  if ($@) {
    return undef;
  }
  return $time;
}
« Letzte Änderung: 03 April 2020, 21:03:11 von Prof. Dr. Peter Henning »

Offline elmattt

  • New Member
  • *
  • Beiträge: 8
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #633 am: 04 April 2020, 18:05:23 »
hello and thank you for your time !
unfortunately i really don't know where and how i can use your code  ;D
i have a question before ... is this piece of code will have the same fonctionality as the module ?? for example : send speech or recorded speech, with or without a gong before or after, and will pause the actual source and play the same source after the speech ??

 ;D

Offline elmattt

  • New Member
  • *
  • Beiträge: 8
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #634 am: 30 April 2020, 20:02:21 »
Hi ! any news ??

Offline Mad

  • New Member
  • *
  • Beiträge: 21
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #635 am: 27 Mai 2020, 15:13:11 »
Hallo zusammen,

ich habe mir ein Musikplayer widget für die Bose Soundtouch erstellt.
Leider erscheint immer nur ein (Reading) Cover, wenn die Soundtouch an ist.
Ist die Soundtouch aus, gibt es kein Cover....
Gibt es die Möglichkeit einen Platzhalter (ein anderes Bild) für das Cover zu setzen, wenn sie aus ist?
So sieht es momentan aus. Es geht um das reading "art"

<div data-type="image" data-device="BOSE_08DF1FXXXXX" data-get="art" data-size="100px" data-refresh="5" class="top-space"></div>
Danke.

Offline Eisix

  • Sr. Member
  • ****
  • Beiträge: 872
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #636 am: 28 Mai 2020, 10:04:19 »
Hallo Mad,

ich habe mir ein userreading erstellt welches das reading art anzeigt oder wenn da keins ist mein default icon anzeigt.
Sobald ich wieder an min system komme kann ich dir das schicken.

Gruß
Eisix

Offline Eisix

  • Sr. Member
  • ****
  • Beiträge: 872
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #637 am: 28 Mai 2020, 11:35:37 »
ist von einer squeezebox

ftuicoverarturl:coverarturl.*|power.* {if(ReadingsVal('SB_PLAYER_b227eb45865e','power','on') eq 'on') {return ReadingsVal('SB_PLAYER_b227eb45865e'','coverarturl','')} else {return '/fhem/images/Senderlogo.jpg'}}

Offline Mad

  • New Member
  • *
  • Beiträge: 21
Antw:[Neues Modul] BOSE SoundTouch
« Antwort #638 am: Gestern um 21:52:48 »
Besten Dank.

Habe es nun so gelöst

Platzhalter {if(ReadingsVal("BOSE_68C90BXXXXX","art","n.a.") eq ""){return "../images/bose.png"}else{return ReadingsVal("BOSE_68C90BXXXXX","art","../images/bose.png")}}

 

decade-submarginal