Multiple TTS one after another

What is the correct way to do multiple TTS one after another? I am trying to create a morning routine but the following doesnt wait for the first message to complete

        self.call_service("tts/google_say", entity_id = self.args["player"], message = joke)
        self.call_service("tts/google_say", entity_id = self.args["player"], message = "Test extra words")
1 Like

you will need to calculate the time it takes and make a pause between the calls.
i have seen people calculating based on words.

and then you do it like:

    after_some_sconds = #calculate the time
    self.run_in(self.say_something,1,message = joke)
    self.run_in(self.say_something,after_some_seconds,message = "test extra words")
def self.say_something(self,kwargs):
    self.call_service("tts/google_say", entity_id = self.args["player"], message = kwargs["message"])

You could just concatenate the messages into a single (larger) message.

1 Like

Hi Rene,

Thats perfect, just what I was looking for. I should have been able to to figure that out myself :slight_smile: Thanks again for your help.

1 Like

Hi jedi,

Thanks for your other work around. I did think of this but I think I perfer to break up the notifications as I am gong to have a whole lot.

If you can’t concatenate, use wait_template to check when it’s not busy.

I like that idea, have you got an example? I suppose you poll the status of the media player and wait for it to be stopped?

I use this across multiple units in a script (and the media_player entity is fed into the script), but ‘source’ can be replaced with your media_player entity.

    - wait_template: "{%- if states(source) == 'off' or states(source) == 'idle' or states(source) == 'stopped' or states(source) == 'paused' -%}true{%- else -%}false{%- endif -%}"
      timeout: 00:01:00 

Obviously if you are using the media_player for music and watching a video on it (e.g. Google Home Hub playing a yout tube video), that will not work.

I tried doing somthing similar with a call back but it just caused a loop :rofl:

self.handle = self.listen_state(self.say_something_lots, self.args["player"], new = "idle", message = weather)

The message plays and my google home goes idle so it plays again, over and over :slight_smile:

off course.

that callback is fired as soon as the player goes to idle.
but you could use a run_in loop that uses get_state to see if the player is idle.

Do you have any examples? Would I use 2 callbacks? Then cancel the the first one when all the messages were played? When I think about this my brain gets into a loop :joy:

something like:

    self.run_in(self.say_something,after_some_seconds,message = "test extra words")
def self.say_something(self,kwargs):
    if self.get_state(self.args["player"]) == "idle":
        self.call_service("tts/google_say", entity_id = self.args["player"], message = kwargs["message"])
    else:
        self.run_in(self.say_something,1,message = "test extra words")

this will start the callback, and it will try to call the service every second untill the player is idle, and only then it plays the message.
allthough that can cause problems when you start several at almost the same time, because you cant predict which one will be picked up first.

but before you start to reinvent the wheel, you better look at the examples :wink:
this one:


probably is exactly what you want.
1 Like

Hi Rene,

Thats pretty nice. I added a stream method to play streaming media and changed it to use google tts. I am trying to add the possibility to restore the previous media and position after it finishes the last interruption. My issue is that would work for one interuption but because I am sending it 5 one after another, the last previous state is either stopped or the last tts command (if that makes sense) .

import appdaemon.plugins.hass.hassapi as hass
from queue import Queue
import sys
from threading import Thread
from threading import Event
import time
#import globals

#
# App to manage announcements via TTS and stream sound files to Sonos
#
# Provides methods to enqueue TTS and Media file requests and make sure that only one is executed at a time
# Volume of the media player is set to a specified level and then restored afterwards
#
# Args:
#
# player - media player to use for announcements
# base = base directory for media - this will be a subdirectory under <home assistant config dir>/www
# ip = IP address of machine running this app
# port = HASS port
#
# To use from another APP:
# TTS:
# sound = self.get_app("Sound")
# sound.tts(text, volume, duration)
# duration should be set to longer than the expected duration of the speech
#
# e.g.:
#
# sound = self.get_app("Sound")
# sound.tts("Warning: Intuder alert", 0.5, 10)#
#
# SOUND:
# sound = self.get_app("Sound")
# sound.play(file, volume, content_type, duration)
# file is the path of the file to play relative to "base"
# Content type is the mime type of the media e.g. "audio/mp3" or "audio/wav"
# duration should be set to longer than the expected duration of the media file
#
# e.g.:
# sound = self.get_app("Sound")
# sound.play("warning.wav", "audio/wav", 0.5, 10)
#
# Release Notes
#
# Version 1.0:
#   Initial Version

class Sound(hass.Hass):

  def initialize(self):
    
    # Create Queue
    self.queue = Queue(maxsize=0)

    # Create worker thread
    t = Thread(target=self.worker)
    t.daemon = True
    t.start()
    
    self.event = Event()
    
  def worker(self):
    active = True
    while active:
      try:
        # Get data from queue
        data = self.queue.get()
        if data["type"] == "terminate":
          active = False
        else:
          # Save current volume
          mediaplayer_state = self.get_state(self.args["player"])
          self.log('media player state before insteruption was:{}'.format(mediaplayer_state))
          volume = self.get_state(self.args["player"], attribute="volume_level")
          media_position = self.get_state(self.args["player"], attribute="media_position")
          media_playing = self.get_state(self.args["player"], attribute="media_content_id")
            
          # Set to the desired volume
          self.call_service("media_player/volume_set", entity_id = self.args["player"], volume_level = data["volume"])
          if data["type"] == "tts":
            # Call TTS service
            self.call_service("tts/google_say", entity_id = self.args["player"], message = data["text"])
          if data["type"] == "play":
            netpath = netpath = 'http://{}:{}/local/{}/{}'.format(self.args["ip"], self.args["port"], self.args["base"], data["path"])
            self.call_service("media_player/play_media", entity_id = self.args["player"], media_content_id = netpath, media_content_type = data["content"])
          if data["type"] == "stream":
            self.call_service("media_player/play_media", entity_id = self.args["player"], media_content_id = data["path"], media_content_type = data["content"])

          # Sleep to allow message to complete before restoring volume
          self.log('Sleeping for:{} seconds'.format(data["length"]))
          time.sleep(int(data["length"]))
          # Restore volume
          self.call_service("media_player/volume_set", entity_id = self.args["player"], volume_level = volume)
          # Restore previous media if it was playing
         # if mediaplayer_state == 'playing':
          #  self.log('Restoring previous media player state')
           # self.call_service("media_player/play_media", entity_id = self.args["player"], media_content_id = media_playing, media_content_type = "music")
            #self.call_service("media_player/media_seek", entity_id = self.args["player"], seek_position = media_position)
        
            
          # Set state locally as well to avoid race condition
          self.set_state(self.args["player"], attributes = {"volume_level": volume})
      except:
        self.log("Error")
        self.log(sys.exc_info())

      # Rinse and repeat
      self.queue.task_done()
      
    self.log("Worker thread exiting")
    self.event.set()
       
  def tts(self, text, volume, length):
    self.queue.put({"type": "tts", "text": text, "volume": volume, "length": length})
    
  def play(self, path, content, volume, length):
    self.queue.put({"type": "play", "path": path, "content": content, "volume": volume, "length": length})
  
  def stream(self, path, content, volume, length):
    self.queue.put({"type": "stream", "path": path, "content": content, "volume": volume, "length": length})

  def terminate(self):
    self.event.clear()
    self.queue.put({"type": "terminate"})
    self.event.wait()

with that app you would normally do this in another app:

sound = self.get_app("the name_yo_gave_it")
sound.tts("text 1")
sound.tts("text 2")
sound.tts("text 3")

the result would be that you get this:
save volume
set volume
speak text 1
restore volume
save volume
set volume
speak text 2
restore volume
save volume
set volume
speak text 3
restore volume

that means that the volume always gets back to the previous state.

you can use streamin in that, but dont forget that the app is on hold for the time you set it to stream.
and the point of the app is that you can automate tts at any point in time, and none of the messages you send gets lost.

Thanks again, understood :slight_smile: