Need for a Virtual Speaker TTS Queue App/Device?

So, you have to create a separate custom command for every message/volume combo you would want to use? or are you passing the paramaters to that command from standard hubitat capabilities that are accessible from Rule Machine, like Dimmer level and Device Notification?

You pass the parameters via the playTextAndRestore (and other commands).

The manual strings/volume in the Device Handler were developed for TESTING the app and app behavior. I will keep it there until (and if) I formalize this tool pair.

Ok...so, this wouldn't be usable from RM then?

I do not use RM. If RM allows you to enter playTextAndRestore for a device based on a trigger from another device, then it should. It should work with ANY audio notification available in the RM if the target speaker is audio notification compliant (i.e., it executes the six audio notification commands).

UPDATE. Just ran Rule Machine. It does not use audio notification as a potential device. It does use Speech Synthesis with the Speak command. I will update code.

I believe that RM only supports the speak and the deviceNotification command from RM. But the driver could grab from those to whatever command it wants to actually execute.

If you use the Notification capability in your device you can use deviceNotificaiton. I used that for Cast-Web-Api so that I didn't have to use "speak". Works just like Push bullet.

Thanks. The driver code has been updated and tested using rule machine as a test-bed. It works using playTextAndResume in the speakers. I could (in future) update the preferences in the driver to allow playTextAndRestore. It sill only works with speakers with capability "audioNotification".

I can update this to be "speech synthesis" in the application. I chose audio notification since it has been around for a long time and more speakers support that capability.

I am thinking this implementation carefully as far as whether I support audio notification or speak. The intent is to keep it simple and THEN expand.

UPDATE WILL BE COMPLETE TODAY. But remember, this is a test app for me and others to play with. I am NOT publishing as a finished product.

Updated driver and app are at bottom. Notes:

  • Speaker must have capability "Audio Notification" or "Speech Synthesis"
  • Commands supported are limited by the speaker faithful implementation of the capabilities. Common commands available are: speak, playText, playTextAndRestore, playTestAndResume, and setVolume.
  • You must select only one device from either the Audio Notification or Speech Synthesis list.
  • This is a TEST implementation. There is no capability for multiple instances at this time. That will be added later.

Application Code:

/*

*/
//	def debugLog() { return false }
	def debugLog() { return true }
definition(
	name: "TTS Queueing",
	namespace: "davegut",
	author: "Dave Gutheinz",
	description: "Queue text-to-speech messages for sequential playback.",
	iconUrl: "",
	iconX2Url: ""
)

preferences {
	page(name: "mainPage", title: "Queue message for speaker playback", install: true, uninstall: true)
}

def mainPage() {
	dynamicPage(name: "mainPage") {
		section {
			paragraph "You may only select a single real device in this application"
		}
		section {
			input "speaker", "capability.audioNotification", title: "On this Audio Notification capableSpeaker player", required: true
			input "speaker", "capability.speechSynthesis", title: "On this real Speech Synthesis capable Speaker player", required: true
        }
	}
}

def setLevel(level) {
	speaker.setLevel(level)
}

def inputTTS(playItem, volume, method) {
	logDebug("inputTTS: playItem = ${playItem}, volume = ${volume}, method = ${method}")
	def TTSQueue = state.TTSQueue
	TTSQueue << [playItem, volume, method]
	if (state.playingTTS == false) { processQueue() }
}

def processQueue() {
	logDebug("processQueue: TTSQueue = ${state.TTSQueue}")
	state.playingTTS = true
	def TTSQueue = state.TTSQueue
	if (TTSQueue.size() == 0) {
		state.playingTTS = false
		return
	}
	def nextTTS = TTSQueue[0]
	TTSQueue.remove(0)
	playTTS(nextTTS[0], nextTTS[1], nextTTS[2])
}

def playTTS(playItem, volume, method) {
	logDebug("playTTS: playint: ${playItem}, volume = ${volume}, method = ${method}")
	def duration
	switch(method) {
		case "speak":
			duration = Math.max(Math.round(playItem.length()/12),2)+3
			speaker.speak(playItem)
			break
		case "playText":
			duration = Math.max(Math.round(playItem.length()/12),2)+3
			speaker.playText(playItem, volume)
			break
		case "playTextAndRestore":
			duration = Math.max(Math.round(playItem.length()/12),2)+3
			speaker.playTextAndRestore(playItem, volume)
			break
		case "playTextAndResume":
			duration = Math.max(Math.round(playItem.length()/12),2)+3
			speaker.playTextAndResume(playItem, volume)
			break
		case "playTrack":
			try {
				duration = playItem.duration.toInteger()+3
			} catch (e) {
				duration = 20
			}
			speaker.playText(playItem, volume)
			break
		case "playTrackAndRestore":
			try {
				duration = playItem.duration.toInteger()+3
			} catch (e) {
				duration = 20
			}
			speaker.playTextAndRestore(playItem, volume)
			break
		case "playTrackAndResume":
			try {
				duration = playItem.duration.toInteger()+3
			} catch (e) {
				duration = 20
			}
			speaker.playTextAndResume(playItem, volume)
			break
		default:
			return
	}
	runIn(duration, processQueue)
}

def clearQueue() {
	state.TTSQueue = []
	state.playingTTS = false
	logDebug("clearQueue:  TTSQueue = ${state.TTSQueue}")
}

def setInitialStates() {
}

def installed() {
	state.playingTTS = false
	state.TTSQueue = []
	initialize()
}

def updated() { initialize() }

def initialize() {
	logDebug("initialize: speaker = ${speaker}")
	unsubscribe()
	unschedule()
	if (speaker) { addDevices() }
}

def addDevices() {
	logDebug("addDevices: speaker = ${speaker}")
	try { 
		hub = location.hubs[0] 
	} catch (error) { 
		log.error "Hub not detected.  You must have a hub to install this app."
		return
	}
	def hubId = hub.id
	def virtualDni = "${speaker.getDeviceNetworkId}_TTS"
	def isChild = getChildDevice(virtualDni)
	if (!isChild) {
		logDebug("addDevices: ${virtualDni} / ${hubId} / speaker = ${speaker.label}")
		addChildDevice(
			"davegut",
			"Virtual TTS Speaker",
			virtualDni,
			hubId, [
				"label" : "${speaker.label} TTS Queue",
				"name" : "Virtual TTS Speaker"]
		)
			log.info "Installed Button Driver named ${speaker.label} TTS Queue"
	}
}

def logDebug(msg){
	if(debugLog() == true) { log.debug msg }
}

//	end-of-file

Driver Code:

/*===== HUBITAT INTEGRATION VERSION ===========================
===== HUBITAT INTEGRATION VERSION ===========================*/
metadata {
	definition (name: "Virtual TTS Speaker", namespace: "davegut", author: "David Gutheinz") {
		capability "Audio Notification"
		capability "Speech Synthesis"
		command "setLevel", ["NUMBER"]
		command "clearQueue"
		command "testQueue"
	}
}

preferences {
//	===== Set response modes (to display log & debug messages) =====
	input name: "debugMode", type: "bool", title: "Display debug messages?", required: false
}

def setLevel(level) {
	logDebug("setlevel: level = ${level}")
	parent.setLevel(level.toInteger())
}

def speak(text) {
	logDebug("speak: text = ${text}")
	parent.inputTTS(text, null, "speak")
}

def playText(text, volume=null) {
	logDebug("playText: text = ${text}, volume = ${volume}")
	parent.inputTTS(text, volume, "playText")
}

def playTextAndRestore(text, volume=null) {
	logDebug("playTextAndRestore: text = ${text}, volume = ${volume}")
	parent.inputTTS(text, volume, "playTextAndRestore")
}

def playTextAndResume(text, volume=null) {
	logDebug("playText: text = ${text}, volume = ${volume}")
	parent.inputTTS(text, volume, "playTextAndResume")
}

def playTrack(trackUri, volume=null) {
	logDebug("playTrack: text = ${text}, volume = ${volume}")
	parent.inputTTS(track, volume, "playTrack")
}

def playTrackAndRestore(track, volume=null) {
	logDebug("playTrackAndRestore: text = ${text}, volume = ${volume}")
	parent.inputTTS(track, volume, "playTrack")
}

def playTrackAndResume(track, volume=null) {
	logDebug("playTrackAndResume: text = ${text}, volume = ${volume}")
	parent.inputTTS(track, volume, "playTrack")
}

def clearQueue() { parent.clearQueue() }

def testQueue() {
	playTextAndResume("Hello David.  First Try", 30)
	pauseExecution(3000)
	playTextAndResume("Hello David.  Second Try", 50)
	pauseExecution(3000)
	playTextAndRestore("Hello David.  Third Try", 20)
	pauseExecution(3000)
	playTextAndRestore("Hello David.  Forth Try")
}
	

def logDebug(msg) {
	if (debugMode == true) {
		log.debug msg
	}
}
	
//	End-of-File

Hey Dave,

The app doesn't do anything now after you select an item. No option to really move forward in the app. Modified code and changed the required to false which now lets you move forward. :slight_smile:

1 Like

I'm a little confused. When you send a TTS notification to a device, Hubitat is only "busy" for a short period of time, the time it takes to send the notification to the device. But the device might be busy for 5 or 6 seconds for the notification to play. So, does this take into account how long it will take for the notification to actually play on the device? And what about a startup delay (like a Chromecast device would have)?

For now. go to the app and remove line 25 or line 26 (25 is you use speech synthesis, 26 otherwise). Then it will run. I will update tomorrow to allow either.

Dave

Yes. If you look at the application code, you will see a "runIn(duration, processQueue)". That delays the next queue playing until the duration has been completed. (I tried a pauseExecution, but it caused other problems). The runIn is not an efficient command; however, it works.

How are you calculating that duration though? Sorry if that's in the code but I'm not a programmer. If you're just taking the duration of the MP3 that will play, that's not really the time it will take for the device to play the message in all cases.

TTS is text to speedch. Duration is calculated roughly by taking the string length and divide by 12. Then a 3 second buffer is added. For reasonable length (and English) this seems to work.

So, 120 characters would take approximately 10 seconds to say? Plus the buffer of 3 seconds. Okay, that seems like it should be plenty (if not a little too much) in the couple quick tests I ran here not using any inflection command or the like. I got 122 characters out in 9 seconds from click on speak to phrase complete. I was thinking you would go by the length of the mp3 created and I was trying to figure out how you were calculating that. Character length is a lot easier. Thanks!

@Ryan780 ,

I want to apologize publicly about a response I made to a reply you made to me about this topic. I was having a rough morning and COMPLETELY took your message wrong and became defensive due to me being a crabby ass that morning. I don't know if you read it prior to it being flagged; which I then realized how much of a jerk I was being and then promptly deleted. Weather you did or did not read that reply I want to apologize to you either way. You have been a great contributor to the community and I had no right to respond like I did in such a rude way.

Again I apologize for my abrupt crabbiness.

Aaron

3 Likes

Thank you. Happens to all of us.

1 Like

@djgutheinz is this app still under active development? I haven't noticed any updates recently, is it ready for primetime?

It is good for a single speaker implementation. I may update this week to allow selection of multiple speakers (creating a virtual device for each).

Dave

2 Likes

Update of the single speaker implementation coming today to GitHub. Will post here when done. Will then expand to multiple speaker this week. I want to keep it simple until I reverify all of the functionality.

2 Likes

So the current implementation only allows one instance of the app/driver? I have to sonos "groups" I want to use it with, one upstairs and one downstairs. I can't currently have multiple instances of the app setup to allow two "virtual" devices to be addressed separately from RM?