Is it possible to play audio file or stream using actions-on-google-nodejs library?
Using SSML you can return an audio clip up to 120s.
<speak>
<audio src="https://actions.google.com/sounds/v1/animals/cat_purr_close.ogg">
<desc>a cat purring</desc>
PURR (sound didn't load)
</audio>
</speak>
Edit
If you want to play audio the mp3 file (over 120s), you need to use Media Responses
if (!conv.surface.capabilities.has('actions.capability.MEDIA_RESPONSE_AUDIO')) {
conv.ask('Sorry, this device does not support audio playback.');
return;
}
conv.ask(new MediaObject({
name: 'Jazz in Paris',
url: 'https://storage.googleapis.com/automotive-media/Jazz_In_Paris.mp3',
description: 'A funky Jazz tune',
icon: new Image({
url: 'https://storage.googleapis.com/automotive-media/album_art.jpg',
alt: 'Ocean view',
}),
}));
To add one more point to Nick's answer, you can also build a Media Response which will allow you to play a long audio file (I'm currently playing 50 mins album in my app).
You can find it on Google's doc here.
A short example in Node.js could be:
const richResponse = app.buildRichResponse()
.addSimpleResponse("Here's song one.")
.addMediaResponse(app.buildMediaResponse()
.addMediaObjects([
app.buildMediaObject("Song One", "https://....mp3")
.setDescription("Song One with description and large image.") // Optional
.setImage("https://....jpg", app.Media.ImageType.LARGE)
// Optional. Use app.Media.ImageType.ICON if displaying icon.
])
)
.addSuggestions(["other songs"]);
And then you can just do
app.ask(richResponse)
UPDATE:
As per a comment request, here is the JSON response sent by my app for a mediaResponse:
{
"conversationToken": "[\"_actions_on_google\"]",
"expectUserResponse": true,
"expectedInputs": [
{
"inputPrompt": {
"richInitialPrompt": {
"items": [
{
"simpleResponse": {
"textToSpeech": "Here is my favorite album."
}
},
{
"mediaResponse": {
"mediaType": "AUDIO",
"mediaObjects": [
{
"name": my_name,
"description": my_descr,
"largeImage": {
"url": my_url
},
"contentUrl": my_contentURL
}
]
}
}
],
"suggestions": [
{
"title": my_suggestion
}
]
}
},
"possibleIntents": [
{
"intent": "assistant.intent.action.TEXT"
}
]
}
],
"responseMetadata": {
"status": {
"message": "Success (200)"
},
"queryMatchInfo": {
"queryMatched": true,
"intent": "0a3c14f8-87ca-47e7-a211-4e0a8968e3c5",
"parameterNames": [
my_param_name
]
}
},
"userStorage": "{\"data\":{}}"
}
Related
I'm trying to use the Linkedin API to share a post with an image. I've followed all the steps in the documentation:
https://learn.microsoft.com/es-es/linkedin/consumer/integrations/self-serve/share-on-linkedin?context=linkedin/consumer/context#share-media
First, I register the upload with a POST to https://api.linkedin.com/v2/assets?action=registerUpload and this body:
{
"registerUploadRequest": {
"recipes": [
"urn:li:digitalmediaRecipe:feedshare-image"
],
"owner": "urn:li:person:{{owner_ID}}",
"serviceRelationships": [
{
"relationshipType": "OWNER",
"identifier": "urn:li:userGeneratedContent"
}
]
}
}
I get this response:
{
"value": {
"uploadMechanism": {
"com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest": {
"uploadUrl": "https://api.linkedin.com/mediaUpload/C4E22AQF6AS1WhrD1lw/feedshare-uploadedImage/0?ca=vector_feedshare&cn=uploads&m=AQKLg3lSJnswAgAAAXSgLwWp6iAPzjh6E_5XQh8QuP1Aucf_j9bgW3m5vQ&app=74851466&sync=0&v=beta&ut=2h9vtCSeuP0ps1",
"headers": {
"media-type-family": "STILLIMAGE"
}
}
},
"asset": "urn:li:digitalmediaAsset:C4E22AQF6AS1WhrD1lw",
"mediaArtifact": "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C4E22AQF6AS1WhrD1lw,urn:li:digitalmediaMediaArtifactClass:feedshare-uploadedImage)"
}
}
Then I use the uploadURL to make another post with the image as body, and get a 201 Created response.
I've checked the upload with a GET https://api.linkedin.com/v2/assets/C4E22AQF6AS1WhrD1lw and it has been uploaded correctly:
{
"recipes": [
{
"recipe": "urn:li:digitalmediaRecipe:feedshare-image",
"status": "AVAILABLE"
}
],
"serviceRelationships": [
{
"relationshipType": "OWNER",
"identifier": "urn:li:userGeneratedContent"
}
],
"mediaTypeFamily": "STILLIMAGE",
"created": 1600415270252,
"id": "C4E22AQF6AS1WhrD1lw",
"lastModified": 1600415345915,
"status": "ALLOWED"
}
When I try to share a post referencing this image I get this error:
{"message":"com.linkedin.restli.server.RestLiServiceException [HTTP Status:401]: com.linkedin.content.common.ResponseException: Writers of type person are not authorized to modify UserGeneratedContent.","status":401}
I'm sending a POST to this URL https://api.linkedin.com/v2/ugcPosts with this body:
{
"author": "urn:li:person:{{person_id}}",
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": {
"text": "Feeling inspired after meeting so many talented individuals at this year's conference. #talentconnect"
},
"shareMediaCategory": "IMAGE",
"media": [
{
"status": "READY",
"description": {
"text": ""
},
"media": "urn:li:digitalmediaAsset:C4E22AQF6AS1WhrD1lw",
"title": {
"text": ""
}
}
]
}
},
"visibility": {
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
}
}
I can share an only text post without trouble, look at my person ID, even like a post, but I'm stuck with this.
Can anyone tell me what am I doing wrong?
Solved. I had an error in the person_id variable, so I was trying to create a post with the wrong id.
When we have a simple response and basic card with link out button. See below code, if user clicks the link to open the web view page before Google assistant finishes reading. Once Google assistant finishes reading, the web view page is forced to close. User has to click the button again to open the web view. It looks like the the response gets reset when Google assistant activate the listening mode.
{
"expectUserResponse": true,
"expectedInputs": [
{
"possibleIntents": [
{
"intent": "actions.intent.TEXT"
}
],
"inputPrompt": {
"richInitialPrompt": {
"items": [
{
"simpleResponse": {
"ssml": "<speak>\r\n <audio src=\"…audio file location url…">some long text message\ r\n</speak>",
"displayText": " some long text message”
}
},
{
"basicCard": {
"title": "",
"subtitle": "",
"formattedText": "",
"buttons": [
{
"title": "title",
"openUrlAction": {
"url": "… url…",
"urlTypeHint": 0
}
}
]
}
}
]
}
}
}
],
"isInSandbox": false
}
I am trying to integrate camera to my smarthome with google assistant. I followed CameraStream in actions on google. i have synced my camera and tried execute and responding with an URL(cameraStreamAccessUrl).
Command: "show camera"
google home reply: "Sure, streaming camera" <-- but it is not streaming the video.
Command: "show camera on my phone"
google home reply: "Sorry, i don't know where to play the video. Please tell me the exact name of the screen"
My question is how can i stream it on my phone or on the app?
execution request google json:
{
"inputs": [
"intent": "action.devices.EXECUTE",
"payload": {
"commands": [
{
"devices": [
{
"customData": {
"barValue": true,
"bazValue": "lambtwirl",
"fooValue": 74
},
"id": "id"
}
],
"execution": [
{
"command": "action.devices.commands.GetCameraStream",
"params": {
"StreamToChromecast": false,
"SupportedStreamProtocols": [
"hls"
]
}
}
]
}
]
}
}], "requestId": "requestId"
}
my json response:
{
"requestId": "requestId",
"payload": {
"commands": [
{
"ids": [
"requestId"
],
"status": "SUCCESS",
"states": {
"cameraStreamAccessUrl": "https://url.url"
}
}
]
}
}
If you have a Chromecast in your home, the Google Home will forward the video stream to the Chromecast. I don't know where the stream would go if you don't have a display.
Saying "on my phone" is not a valid target from a Google Home. To get it on your phone, you'd need to use the Assistant on your phone or the Google Home App.
I am connecting to Dialogflow REST API v2beta1 to the method: projects.agent.sessions.detectIntent. In the first request I send a text and the response is returning the expected result which contains outputContexts; when I made the 2nd request I send the context and it should find the intent which is linked to that context, but instead of that it is returning the Default Fallback Intent.
What may be the problem on the 2nd request?
Here are the URL and requests with their respective responses, and below I added the screenshots of the intents expected to match.
URL
https://dialogflow.googleapis.com/v2beta1/projects/project-name/agent/sessions/12343:detectIntent
1st request
{
"queryInput":{
"text":{
"text":"play a video about love",
"languageCode":"en"
}
}
}
1st response
{
"responseId": "15a3b767-52fe-4fc2-8ffd-9d7bb9c6961a",
"queryResult": {
"queryText": "play a video about love",
"action": "video.play",
"parameters": {
"organization": "",
"tag": "Love",
"item": ""
},
"allRequiredParamsPresent": true,
"fulfillmentText": "Here is a video about Love!",
"fulfillmentMessages": [
{
"platform": "ACTIONS_ON_GOOGLE",
"simpleResponses": {
"simpleResponses": [
{
"textToSpeech": "Here is a video about Love!"
}
]
}
},
{
"text": {
"text": [
"Here is a video about Love!"
]
}
}
],
"outputContexts": [
{
"name": "projects/project-name/agent/sessions/12343/contexts/play-video",
"lifespanCount": 5,
"parameters": {
"tag": "Love",
"organization": "",
"tag.original": "love",
"item": "",
"organization.original": "",
"item.original": ""
}
}
],
"intent": {
"name": "projects/project-name/agent/intents/9e5d2bbc-81f3-4700-8740-01504b05443f",
"displayName": "video-play"
},
"intentDetectionConfidence": 1,
"languageCode": "en"
}
}
2nd request (where the problem should be)
{
"queryParams":{
"contexts":[
{
"name":"projects/project-name/agent/sessions/12342/contexts/play-video"
}
]
},
"queryInput":{
"text":{
"text":"that video matters a lot for me",
"languageCode":"en"
}
}
}
2nd response
{
"responseId": "40d1f94f-4673-4644-aa53-99c854ff2596",
"queryResult": {
"queryText": "that video matters a lot for me",
"action": "input.unknown",
"parameters": {},
"allRequiredParamsPresent": true,
"fulfillmentText": "Can you say that again?",
"fulfillmentMessages": [
{
"text": {
"text": [
"Sorry, what was that?"
]
}
}
],
"intent": {
"name": "projects/project-name/agent/intents/10c88e8d-f16a-4905-b829-f596d3b3c588",
"displayName": "Default Fallback Intent",
"isFallback": true
},
"intentDetectionConfidence": 1,
"languageCode": "en"
}
}
Screenshots of the intents expected to match
1st intent
2nd intent
Useful documentation
Doc of the method: https://dialogflow.com/docs/reference/api-v2/rest/v2beta1/projects.agent.sessions/detectIntent
Doc of the Context object: https://dialogflow.com/docs/reference/api-v2/rest/Shared.Types/Context
Doc of the Params object to be sent: https://dialogflow.com/docs/reference/api-v2/rest/v2beta1/QueryParameters
It looks like your second request has an incomplete context. Although you're specifying the name, you're not including the lifespanCount parameter. Since you're not providing a parameter, it defaults to 0, which means that it has timed out.
You should send back exactly what you received from the outputContext attribute in the previous reply.
{
"queryParams":{
"contexts":[
{
"name": "projects/project-name/agent/sessions/12343/contexts/play-video",
"lifespanCount": 5,
"parameters": {
"tag": "Love",
"organization": "",
"tag.original": "love",
"item": "",
"organization.original": "",
"item.original": ""
}
}
]
},
"queryInput":{
"text":{
"text":"that video matters a lot for me",
"languageCode":"en"
}
}
}
I'm a bit confused by the Google Actions documentation about storing data and hoped someone can help clarify...
The docs state that data in the conv.user.storage object will be saved "across conversations". I took this to mean that if the user exited the conversation these values would be persisted and available the next time they interact with my action. Is that understanding correct?
The reason I ask is that I can't get this behaviour to work in my action.
I have built a simple action fulfilment service (using Actions on Google NodeJS library v2.4.0 and Koa v2.5.3). The fulfilment is triggered from an intent defined in Dialogflow (after an account has been linked with Google Sign In) and stores a value in conversation storage. The code is as follows:
server.js (base server - loads actions dynamically from the local ./actions/ dir)
/* Load the environment */
const dotenv = require('dotenv');
const path = require('path');
const packageJson = require('./package.json');
dotenv.config({
silent: true,
path: process.env.ENV_FILE!=undefined && process.env.ENV_FILE.trim()!='' ? path.normalize(process.env.ENV_FILE) : path.join(__dirname, './.env')
});
const SERVER_NAME = process.env.NAME || packageJson.name;
const SERVER_PORT = process.env.PORT||'8080';
const SERVER_HOST = process.env.HOST||'0.0.0.0';
const HANDLERS_PATH = './actions/';
/* Load the dependencies */
const logger = require('utils-general').logger('google-server');
const Koa = require('koa');
const KoaBody = require('koa-body');
const KoaActionsOnGoogle = require('koa-aog');
const fs = require('fs');
const { dialogflow } = require('actions-on-google');
/* Load and initialise the Google Assistant actions */
//Initialise DialogFlow
const action = dialogflow({ debug: process.env.ACTIONS_DEBUG==='true', clientId: process.env.GOOGLE_CLIENT_ID });
//Load the action intent handlers
const handlers = [];
let handlerFiles = fs.readdirSync(HANDLERS_PATH);
handlerFiles.forEach(function loadHandlers(file) {
let handlerImpl = require(HANDLERS_PATH+file);
let handler = {};
handler[handlerImpl.intent] = handlerImpl.action;
handlers.push(handler);
});
//Add the actions intent handlers to DialogFlow
handlers.forEach(item => {
let key = Object.keys(item)[0];
logger.info(`Adding handler for action intent ${key}`);
action.intent(key, item[key]);
});
/* Create the application server to handle fulfilment requests */
logger.info(`Initialising the ${SERVER_NAME} server (port: ${SERVER_PORT}, host: ${SERVER_HOST})`);
//Create the server
const app = new Koa();
//Add default error handler middleware
app.on('error', function handleAppError(err) {
logger.error(`Unhandled ${err.name||'Error'}: ${err.message || JSON.stringify(err)}`);
});
//Add body parsing middleware
app.use(KoaBody({ jsonLimit: '50kb' }));
//Log the request/ response
app.use(async (ctx, next) => {
logger.trace(`REQUEST ${ctx.method} ${ctx.path} ${JSON.stringify(ctx.request.body)}`);
await next();
logger.trace(`RESPONSE (${ctx.response.status}) ${ctx.response.body ? JSON.stringify(ctx.response.body) : ''}`);
});
//Make the action fulfilment endpoint available on the server
app.use(KoaActionsOnGoogle({ action: action }));
/* Start server on the specified port */
app.listen(SERVER_PORT, SERVER_HOST, function () {
logger.info(`${SERVER_NAME} server started at ${new Date().toISOString()} and listening for requests on port ${SERVER_PORT}`);
});
module.exports = app;
storage-read.js (fulfilment for the "STORAGE_READ" intent - reads stored uuid from conversation storage):
const logger = require('utils-general').logger('google-action-storage-read');
const { SimpleResponse } = require('actions-on-google');
const { getUserId } = require('../utils/assistant-util');
const _get = require('lodash.get');
module.exports = {
intent: 'STORAGE_READ',
action: async function (conv, input) {
logger.debug(`Processing STORAGE_READ intent request: ${JSON.stringify(conv)}`, { traceid: getUserId(conv) });
let storedId = _get(conv, 'user.storage.uuid', undefined);
logger.debug(`User storage UUID is ${storedId}`);
conv.close(new SimpleResponse((storedId!=undefined ? `This conversation contains stored data` : `There is no stored data for this conversation`)));
}
}
storage-write.js (fulfils the "STORAGE_WRITE" intent - writes a UUID to conversation storage):
const logger = require('utils-general').logger('google-action-storage-read');
const { SimpleResponse } = require('actions-on-google');
const { getUserId } = require('../utils/assistant-util');
const _set = require('lodash.set');
const uuid = require('uuid/v4');
module.exports = {
intent: 'STORAGE_WRITE',
action: async function (conv, input) {
logger.debug(`Processing STORAGE_WRITE intent request`, { traceid: getUserId(conv) });
let newId = uuid();
logger.debug(`Writing new UUID to conversation storage: ${newId}`);
_set(conv, 'user.storage.uuid', newId);
conv.close(new SimpleResponse(`OK, I've written a new UUID to conversation storage`));
}
}
This "STORAGE_WRITE" fulfilment stores the data and makes it available between turns in the same conversation (i.e. another intent triggered in the same conversation can read the stored data). However, when the conversation is closed, subsequent (new) conversations with the same user are unable to read the data (i.e. when the "STORAGE_READ" intent is fulfilled) - the conv.user.storage object is always empty.
I have voice match set up on the Google account/ Home Mini I'm using, but I can't see how I determine in the action if the voice is matched (although it seems to be as when I start a new conversation my linked account is used). I'm also getting the same behaviour on the simulator.
Sample request/ responses (when using the simulator) are as follows:
STORAGE_WRITE request:
{
"user": {
"userId": "AB_Hidden_EWVzx3q",
"locale": "en-US",
"lastSeen": "2018-10-18T12:52:01Z",
"idToken": "eyMyHiddenTokenId"
},
"conversation": {
"conversationId": "ABwppHFrP5DIKzykGIfK5mNS42yVzuunzOfFUhyPctG0h0xM8p6u0E9suX8OIvaaGdlYydTl60ih-WJ5kkqV4acS5Zd1OkRJ5pnE",
"type": "NEW"
},
"inputs": [
{
"intent": "actions.intent.MAIN",
"rawInputs": [
{
"inputType": "KEYBOARD",
"query": "ask my pathfinder to write something to conversation storage"
}
],
"arguments": [
{
"name": "trigger_query",
"rawText": "write something to conversation storage",
"textValue": "write something to conversation storage"
}
]
}
],
"surface": {
"capabilities": [
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
},
{
"name": "actions.capability.MEDIA_RESPONSE_AUDIO"
}
]
},
"isInSandbox": true,
"availableSurfaces": [
{
"capabilities": [
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
}
]
}
],
"requestType": "SIMULATOR"
}
STORAGE_WRITE response:
{
"conversationToken": "[]",
"finalResponse": {
"richResponse": {
"items": [
{
"simpleResponse": {
"textToSpeech": "OK, I've written a new UUID to conversation storage"
}
}
]
}
},
"responseMetadata": {
"status": {
"message": "Success (200)"
},
"queryMatchInfo": {
"queryMatched": true,
"intent": "a7e54fcf-8ff1-4690-a311-e4c6a8d1bfd7"
}
},
"userStorage": "{\"data\":{\"uuid\":\"7dc835fa-0470-4028-b8ed-3374ed65ac7c\"}}"
}
Subsequent STORAGE_READ request:
{
"user": {
"userId": "AB_Hidden_EWVzx3q",
"locale": "en-US",
"lastSeen": "2018-10-18T12:52:47Z",
"idToken": "eyMyHiddenTokenId"
},
"conversation": {
"conversationId": "ABwppHHVvp810VEfa4BhBJPf1NIfKUGzyvw9JCw7kKq9YBd_F8w0VYjJiSuzGLrHcXHGc9pC6ukuMB62XVkzkZOaC24pEbXWLQX5",
"type": "NEW"
},
"inputs": [
{
"intent": "STORAGE_READ",
"rawInputs": [
{
"inputType": "KEYBOARD",
"query": "ask my pathfinder what is in conversation storage"
}
],
"arguments": [
{
"name": "trigger_query",
"rawText": "what is in conversation storage",
"textValue": "what is in conversation storage"
}
]
}
],
"surface": {
"capabilities": [
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
},
{
"name": "actions.capability.MEDIA_RESPONSE_AUDIO"
}
]
},
"isInSandbox": true,
"availableSurfaces": [
{
"capabilities": [
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
}
]
}
],
"requestType": "SIMULATOR"
}
STORAGE_READ response:
{
"conversationToken": "[]",
"finalResponse": {
"richResponse": {
"items": [
{
"simpleResponse": {
"textToSpeech": "There is no stored data for this conversation"
}
}
]
}
},
"responseMetadata": {
"status": {
"message": "Success (200)"
},
"queryMatchInfo": {
"queryMatched": true,
"intent": "368d08d3-fe0c-4481-aa8e-b0bdfa659eeb"
}
}
}
Can someone set me straighten me out on whether I'm misinterpreting the docs or maybe I have a bug somewhere?
Thanks!
my suspicion is that personal results are turned off in your case.
You mentioned you're testing on Home Mini and Prisoner was able reproduce on device (in the comments).
Shared devices like Smart Speakers (Home, Mini) and Smart Displays have personal results disabled by default. Check this documentation to enable it.
Open Settings on your Android phone
Under "Assistant devices," select your device (e.g. Mini)
Turn Personal results on
Beware that this means personal results like Calendar entries can be accessed through the device.
To check if userStorage will persist, you can use the GUEST/VERIFIED flag, see documentation here.