Say() can not say two or more phrases

Yes really

More OH. It has no way of knowing how long the phrase will last and there is no trigger back from the TTS to say “I am finished”

2 Likes

I have seen the same behaviour.

I’ve thought about waiting for the player state (my case a Chromecast Audio) to change back to Stop before issuing the next phrase.

I haven’t tried it yet, as I just compiled the phrases into 1 command (text string) instead.

For example,

Say(“first phrase”+". "+“second phrase"+". "+“third phrase”)

Or as an example

say("The outdoor temperature is "+outdoortemp.state+" degrees C and the wind speed is "+windspeed.state+" miles per hour")

Hi,

I have the same problem with say and sending TTS text to chromecast devices. Since I have different “sources” sending TTS text, combining text into one phrase was not an option for me.

I am using the following rule as a “workaround” which works very well for my purposes. I have shortened the rule (I have more in my rule), hopefully I have not introduced any errors…

I have two items:

  1. Item TTSText: Send all Text for Text-to-speech to this item

The rule will introduce the delay based on the length of the text and send it to the second Item:

  1. Item TTSText_Speaker: Item for say command

I had to introduce bQueueSpeakerError as failsafe. Sometimes there seems to be an error in the timer (I do not know why).
bQueueSpeakerError makes sure that the queue will be processed even if there is a timer exception. Probably not the most elegant way to do this but it works for me :wink:

Maybe you can try this. Hope this helps.

import java.util.concurrent.ConcurrentLinkedQueue

val ConcurrentLinkedQueue<String> queueSpeaker = new ConcurrentLinkedQueue()
var Timer timerSpeaker = null
var boolean bQueueSpeakerError = false

val int iTTSCharacterDelay = 120  // delay in ms per character
var boolean debug = false

rule "tts"
when
	Item TTSText received command 
then
	queueSpeaker.add(receivedCommand.toString)
	if (debug) {
		logInfo("TTS", "Add to queue: " + receivedCommand.toString)
	}
	// Check if timer exists
	if (timerSpeaker === null || bQueueSpeakerError) {
		if (bQueueSpeakerError) {
			bQueueSpeakerError = false 
			logError("ERROR", "Queue error in rule tts.rules: method entered because bQueueSpeakerError is true")
		}
		if (debug) {
			logInfo("TTS", "No timer existing - create")
		}
		timerSpeaker = createTimer(now, [ |
			try {
				// if queue is not empty
				if (queueSpeaker.peek !== null) {
					var cmd = queueSpeaker.poll
					// get next command from queue
					if (debug) {
						logInfo("TTS", "queue not empty - get command: " + cmd.toString)
					}
					TTSText_Speaker.sendCommand(cmd)

					// If string is too short, put in 10 characters 
					if (cmd.length() < 10) {
						cmd = "1234567890"
					}
					// reschedule timer - time is defined in iTTSCharacterDelay
					if (debug) {
						logInfo("TTS", "Timer reschedule: " + (cmd.length() * iTTSCharacterDelay).toString)
					}
					timerSpeaker.reschedule(now.plusMillis(cmd.length() * iTTSCharacterDelay))
				} else {
					if (debug) {
						logInfo("TTS", "Queue empty - timer end: null")
					}
					timerSpeaker = null
				}
			} catch(Exception e) {
				logError("ERROR", "Queue error in rule tts.rules: " + e.toString)
				bQueueSpeakerError = true 
				if (timerSpeaker !== null) {
					logError("ERROR", "Queue error in rule tts.rules: timerSpeaker.cancel")
					timerSpeaker.cancel
				}
				logError("ERROR", "Queue error in rule tts.rules: timerSpeaker = null")
				timerSpeaker = null
			}
		])
	}
end

rule "tts speaker"
when
	Item TTSText_Speaker received command 
then
	say(receivedCommand.toString, "googletts:nlNLWavenetB", "chromecast:chromecast:<ID>")
end

Best regards,
Matt

1 Like

Thanks, Matt! Will try it.

But what if to use this trigger of idle state of speaker http://prntscr.com/prybb3
Has anyone tried?

Have a go and let us know

I would, but not not at home and can not(((

Say commands make use of a TTS service, the return of such should be handled asynchronically (IMHO) by the binding that handles the audiosink in question (you never told which ok you use). In order the get any information about when the stream is finished playing that binding would need to raise an event, If it does would be described in the documentation The “idling” channel you showed MIGHT be useable, be we can’t tell!

I use Chromecast.

Your approach appears to be similar to Design Pattern: Gate Keeper. For those using Scripted Automation, I’ve submitted a version of this DP as a library module in the Helper Library.

I’m very happy to see you are using a ConcurrelyLinkedQueue instead of a ReentrantLock or some other less safe (for Rules DSL) approach.

Hi Rich,

it is indeed based on your excellent gate keeper design pattern, thank you very much for that :slight_smile: I did re-write pretty much everything to get rid of ReentrantLocks.

1 Like

Im trying to use this rule, but it is again problems with timers in it.

Please look the rules.
Timer Rule:

import java.util.concurrent.ConcurrentLinkedQueue
val ConcurrentLinkedQueue<String> queueSpeaker = new ConcurrentLinkedQueue()
var Timer timerSpeaker = null
val int iTTSCharacterDelay = 150  // delay in ms per character

rule "TTS Queue TextToSay"

when
	Item TTSTextToSay received command 
then
	queueSpeaker.add(receivedCommand.toString)
	logInfo("TTS", "Add to queue: " + receivedCommand.toString)
	// Check if timer exists
	if (timerSpeaker === null) {
		logInfo("TTS", "No timer existing - create")

		timerSpeaker = createTimer(now, [ |
			// if queue is not empty
			if (queueSpeaker.peek !== null) {
				var cmd = queueSpeaker.poll
				// get next command from queue
				logInfo("TTS", "queue not empty - get command: " + cmd.toString)

				TTSOutputToSpeaker.sendCommand(cmd)

				// If string is too short, put in 10 characters 
				if (cmd.length() < 10) {
					cmd = "1234567890"
				}
				// reschedule timer - time is defined in iTTSCharacterDelay
				logInfo("TTS", "Timer reschedule: " + (cmd.length() * iTTSCharacterDelay).toString)
				timerSpeaker.reschedule(now.plusMillis(cmd.length() * iTTSCharacterDelay))
			} else {
				logInfo("TTS", "Queue empty - timer end: null")
				timerSpeaker = null
				timerSpeaker?.cancel()
			}
		])
	}
end

then Output to speaker

rule "TTS OutputToSpeaker"
when
	Item TTSOutputToSpeaker received command 
then
	say(receivedCommand.toString, "googletts:ruRUStandardB", "chromecast:chromecast:e338f8cdb77fb98a6dc66361aca9cfe7")
	logInfo("TTS", "Am saying now: " + receivedCommand.toString)

end

and last one - commant to send the texts

rule "Send text to say"
when
     Item Test changed
then
    TTSTextToSay.sendCommand("One. Tell the test Text")
    TTSTextToSay.sendCommand("Two. Tell the test Text") 
    TTSTextToSay.sendCommand("Тhree. Tell the test Text")   
	TTSTextToSay.sendCommand("Four. Tell the test Text") 
end 

I can not get all 4 textes said correctly. I thing becuase i got one one, but many timers created at once.
Here is the sample of logs

2019-11-09 20:15:29.200 [ome.event.ItemCommandEvent] - Item 'TTSTextToSay' received command One. Tell the test Text

2019-11-09 20:15:29.213 [ome.event.ItemCommandEvent] - Item 'TTSTextToSay' received command Two. Tell the test Text

2019-11-09 20:15:29.217 [vent.ItemStateChangedEvent] - TTSTextToSay changed from Four. Tell the test Text to One. Tell the test Text

2019-11-09 20:15:29.234 [vent.ItemStateChangedEvent] - TTSTextToSay changed from One. Tell the test Text to Two. Tell the test Text

2019-11-09 20:15:29.247 [ome.event.ItemCommandEvent] - Item 'TTSTextToSay' received command Тhree. Tell the test Text

2019-11-09 20:15:29.250 [ome.event.ItemCommandEvent] - Item 'TTSTextToSay' received command Four. Tell the test Text

2019-11-09 20:15:29.270 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Add to queue: One. Tell the test Text

2019-11-09 20:15:29.239 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Add to queue: Two. Tell the test Text

2019-11-09 20:15:29.276 [vent.ItemStateChangedEvent] - TTSTextToSay changed from Two. Tell the test Text to Тhree. Tell the test Text

2019-11-09 20:15:29.291 [vent.ItemStateChangedEvent] - TTSTextToSay changed from Тhree. Tell the test Text to Four. Tell the test Text

2019-11-09 20:15:29.290 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Add to queue: Тhree. Tell the test Text

2019-11-09 20:15:29.287 [INFO ] [g.eclipse.smarthome.model.script.TTS] - No timer existing - create

2019-11-09 20:15:29.302 [INFO ] [g.eclipse.smarthome.model.script.TTS] - No timer existing - create

2019-11-09 20:15:29.307 [INFO ] [g.eclipse.smarthome.model.script.TTS] - No timer existing - create

2019-11-09 20:15:29.316 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Add to queue: Four. Tell the test Text

2019-11-09 20:15:29.316 [INFO ] [g.eclipse.smarthome.model.script.TTS] - queue not empty - get command: Two. Tell the test Text

2019-11-09 20:15:29.324 [INFO ] [g.eclipse.smarthome.model.script.TTS] - queue not empty - get command: One. Tell the test Text

2019-11-09 20:15:29.329 [ome.event.ItemCommandEvent] - Item 'TTSOutputToSpeaker' received command Two. Tell the test Text

2019-11-09 20:15:29.342 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Timer reschedule: 3450

2019-11-09 20:15:29.345 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Timer reschedule: 3450

2019-11-09 20:15:29.351 [vent.ItemStateChangedEvent] - TTSOutputToSpeaker changed from Тhree. Tell the test Text to Two. Tell the test Text

2019-11-09 20:15:29.363 [ome.event.ItemCommandEvent] - Item 'TTSOutputToSpeaker' received command One. Tell the test Text

2019-11-09 20:15:29.387 [vent.ItemStateChangedEvent] - TTSOutputToSpeaker changed from Two. Tell the test Text to One. Tell the test Text

2019-11-09 20:15:32.809 [INFO ] [g.eclipse.smarthome.model.script.TTS] - queue not empty - get command: Тhree. Tell the test Text

2019-11-09 20:15:32.816 [ome.event.ItemCommandEvent] - Item 'TTSOutputToSpeaker' received command Тhree. Tell the test Text

2019-11-09 20:15:32.821 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Timer reschedule: 3750

2019-11-09 20:15:32.835 [vent.ItemStateChangedEvent] - TTSOutputToSpeaker changed from One. Tell the test Text to Тhree. Tell the test Text

2019-11-09 20:15:33.938 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Am saying now: Тhree. Tell the test Text

2019-11-09 20:15:36.584 [INFO ] [g.eclipse.smarthome.model.script.TTS] - queue not empty - get command: Four. Tell the test Text

2019-11-09 20:15:36.610 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Timer reschedule: 3600

2019-11-09 20:15:36.596 [ome.event.ItemCommandEvent] - Item 'TTSOutputToSpeaker' received command Four. Tell the test Text

2019-11-09 20:15:36.618 [vent.ItemStateChangedEvent] - TTSOutputToSpeaker changed from Тhree. Tell the test Text to Four. Tell the test Text

2019-11-09 20:15:37.185 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Am saying now: Four. Tell the test Text

2019-11-09 20:15:40.230 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Queue empty - timer end: null

2019-11-09 20:15:59.433 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Am saying now: One. Tell the test Text

2019-11-09 20:15:59.432 [INFO ] [g.eclipse.smarthome.model.script.TTS] - Am saying now: Two. Tell the test Text

In this case http://prntscr.com/puq4v6
Three timers war created at once and only two phrases was said.

Where is my problem that i am looking for 4 days(((?

There was an issue where timers wouldn’t be created if they were created within quick succession:

It should be fixed on recent milestones and you can probably workaround it by adding some sleeps.

I tought also this way!

I added a sleep (just to test, because as i know - speel is bad idea for rules), after this line

queueSpeaker.add(receivedCommand.toString)
 Thread::sleep(1000)

but it do not help(((

Adding the sleep there won’t help. They will still all be created close together, just one second later. You need to add a sleep between your sendCommands to TTSTextToSay.

This rule isn’t really based on my code but @mstehle’s so he might be better able to help debug it. The concept is the same as the Gatekeeper DP but it’s his code.

Rich, it was just test command.
I can not add sleep there, because commands comes from diffenent rules and i do not know when it will come (((

Hi, Matt

Could i ask u? I have problem with double and tripple created timers if command comes at the same time.
How did you solved this problem?
Few posts up there is a rule and logs with this problem from me
https://community.openhab.org/t/say-can-not-say-two-or-more-phrases/84805/16?u=thisisio
Thanks!

Hi Michael,

I do not have the situation that TTS text is created at excatly the same time (same millisecond). I have just tested your “scenario” here and get the same issue - multiple timers are created. However, if text is send to the TTSTextToSay item with just 1 ms difference everything seems to be working ok here.
BTW, I am on 2.5 M4 so the issue @wborn mentioned above should not be the problem here. The issue seems to be that if the TTSTextToSay item receives text within the same millisecond, the variable holding the timer handle (timerSpeaker) is still null, hence a second/third timer is created.

If you really need to make sure that text that is receive in the same millisecond is queued properly, maybe you need to look into Rich’s DP for the gatekeeper and use a continuously looping timer instead of creating the timer only when the queue receives the first entry and stopping it when the queue is empty. I have opted not to do this for my TTS queue usecase.
Maybe someone else also has a different approach or idea :wink:

Cheers,
Matt

2 Likes

Thanks a lot, Matt

I have just got pi4 and put there 2.5m4.
Will make some tests top see 8f it still makes problems.

But you can add the sleep there to test whether the problem Wouter mentioned is the same problem you are having. The whole point of adding the sleepes to your test rule is to figure out the source of the problem.

I am using

var Number cmd_wait = ((cmd_Text.state.toString.length() * 70) + 1000)
Thread::sleep(cmd_wait)

Thats working fine for me, for short and for long texts.