跟随我们学习如何修改使用 IBM Watson Speech to Text 服务的 speech-to-text-nodejs 示例应用程序。您将创建一个游戏,其中的主要角色 Melvin 将根据您的语音命令而走进一座房子、销毁它并进入下一关。您还将使用 Alchemy API Relation Extraction 服务 从 Speech to Text 服务创建的文本中提取关系。了解这种认知计算的力量之后,就可以尽情地玩这个游戏了!
点击查看大图
关闭 [x]
全新的 developerWorks Premium 会员计划支持会员全权访问强大的开发工具和资源,包括 500 篇通过 Safari Books Online 提供的针对应用程序开发人员的顶级技术文章,参加重要开发人员活动可享大幅折扣,最新的 O'Reilly 大会的视频回放,等等。观看 Govind Baliga 的这个揭秘视频 ,他揭示了 developerWorks Premium 会员能获得的好处。立即注册。
运行应用程序
获取代码
首先,我们下载并修改 Speech to Text 示例应用程序。然后,我们需要设置我们的 Bluemix 实例并为它配备访问 Watson Speech to Text 服务和 AlchemyAPI 服务的能力。
使用下面这个命令克隆 Git 存储库:
$ git clone https://github.com/watson-developer-cloud/speech-to-text-nodejs.git
manifest.yml
文件,使用清单 1清单 1中所示的代码更新它。 清单 1. 编辑 manifest.yml 文件
--- declared-services: alchemy-api-service-free: label: alchemy_api plan: Free speech-to-text-service-standard: label: speech_to_text plan: standard applications: - name: speech-to-text-game path: . command: npm start memory: 512M services: - speech-to-text-service-standard - alchemy-api-service-free env: NODE_ENV: production
cf
命令行工具连接到 Bluemix:
$ cf api https://api.ng.bluemix.net
$ cf login -u <<em>your_user_ID</em>>
在 Bluemix 中创建 Speech to Text 服务。
$ cf create-service speech_to_text standard speech-to-text-service-standard
在 Bluemix 中创建 Alchemy API 服务。
$ cf create-service alchemy_api free alchemy-api-service-free
$ cf push
。 对于本地开发,您可将您的凭据存储在一个单独的环境文件(一个 .env
文件)中,让它们在存储库之外。然后,您可以灵活地在您的生产环境中使用不同的凭据。所幸,一个名为 node-env-file 的 Node 库执行了大部分工作。下面设置我们的应用程序来使用这个库并将我们的凭据存储在一个 .env
文件中。
将 node-env-file 库依赖项添加到应用程序中:
npm install --save node-env-file
.env
文件时使用。 .env
文件并插入凭据,如清单 2清单 2中所示。 清单 2. .env
文件中的凭据
USERNAME=[your-speech-to-text-username-goes-here] PASSWORD=[your-speech-to-text-password-goes-here] ALCHEMY_API_KEY=[your-alchemy-api-key-goes-here]
.env
文件中的凭据,如清单 3清单 3中所示。 清单 3. 编辑 app.js 以使用 .env
文件
var express = require('express'); var app = express(); var vcapServices = require('vcap_services'); var extend = require('util')._extend; var watson = require('watson-developer-cloud'); var env = require('node-env-file'); // Bootstrap application settings require('./config/express')(app); // Load .env environmental variables env(__dirname + '/.env', { raise: false }); var alchemyApiCredentials = vcapServices.getCredentials('alchemy_api'); // For local development, replace username and password var speechToTextConfig = extend({ version: 'v1', url: 'https://stream.watsonplatform.net/speech-to-text/api', username: process.env.USERNAME || '<username>', password: process.env.PASSWORD || '<password>' }, vcapServices.getCredentials('speech_to_text')); var alchemyConfig = extend({ api_key: process.env.ALCHEMY_API_KEY || '<api_key>' }, vcapServices.getCredentials('alchemy_api')); var authService = watson.authorization(speechToTextConfig); var alchemyLanguage = watson.alchemy_language(alchemyConfig);
请注意,我们将 config
对象重命名为 speechToTextConfig
。
/api/token
(它公开一个端点以允许 JavaScript 应用程序请求一个临时 Speech To Text 令牌),以便使用更新的名称,如清单 4清单 4 中所示。
清单 4. 编辑 app.js 以使用 express API
// Get token using your speech-to-text credentials app.post('/api/token', function(req, res, next) { authService.getToken({url: speechToTextConfig.url}, function(err, token) { if (err) next(err); else res.send(token); }); });
您还需要创建一个 API 接口来代为处理从 JavaScript 应用程序到 Alchemy API Relations API 的 Alchemy API 请求,这样一来凭据就不可见,进而更加安全。
清单 5. 编辑 app.js 文件以创建新端点
// User our secret API key to proxy request to the alchemy-keywords API app.post('/api/alchemy-relations', function(req, res, next) { alchemyLanguage.relations(req.body, function(err, response) { if (err) { next(err); } else { res.send(response); } }); });
现在您已有一个 API 端点来代为处理 Alchemy Relations API 请求,下面更新我们的应用程序来使用它们。
清单 6. 创建新端点
exports.createAlchemyProxy = function() { return { getAlchemyRelations: function(text, callback) { var url = '/api/alchemy-relations'; var relationsRequest = new XMLHttpRequest(); relationsRequest.open('POST', url, true); relationsRequest.setRequestHeader('csrf-token', $('meta[name="ct"]').attr('content')); relationsRequest.setRequestHeader('Content-type', 'application/json'); relationsRequest.onreadystatechange = function() { if (relationsRequest.readyState !== 4) { return; } if (relationsRequest.status === 200) { var resp = JSON.parse(relationsRequest.responseText); callback(null, resp); } else { var error = 'Cannot reach server'; if (relationsRequest.responseText) { try { error = JSON.parse(relationsRequest.responseText); } catch (e) { error = relationsRequest.responseText; } } callback(error); } }; relationsRequest.send(JSON.stringify({ text: text })); } }; };
清单 7. 使用 Alchemy API 代理
var utils = require('./utils'); var alchemyProxy = utils.createAlchemyProxy(); alchemyProxy.getAlchemyRelations( "Move Marvin to the left side of the screen.", function(err, json) { // Do something with relation results } );
$(document).ready
内初始化该代理,如清单 8清单 8中所示。 清单 8. 初始化代理
$(document).ready(function() { var tokenGenerator = utils.createTokenGenerator(); var alchemyProxy = utils.createAlchemyProxy(); ...
viewContext
添加一个对 alchemyProxy
的引用,如清单 9清单 9中所示。 清单 9. 添加 viewContext 引用
tokenGenerator.getSpeechToTextToken(function(err, token) { ... var viewContext = { currentModel: 'en-US_BroadbandModel', models: models, token: token, alchemyProxy: alchemyProxy, bufferSize: BUFFERSIZE }; });
要创建游戏,我们将利用前人的成果,使用 Phaser 游戏引擎来让我们的工作更容易。
观看: 应用程序实际运行和您将在创建这个简单游戏时添加的游戏交互的快速演示
Phaser 框架的使用实质上非常简单,而且我们的语音到文本转换游戏可以仅包含这些函数:
preload
用于加载资源,比如游戏中使用的图像和子画面。 create
用于设置游戏 update
用于检测按钮按下和碰撞 清单 10. 添加 Phaser
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <!-- Place js/phaser.min.js here at the end of the document just before we load our app code --> <script src="js/phaser.min.js"></script> <script src="js/index.js"></script>
清单 11. 核心游戏代码,包含 create 函数
/* global Phaser */ var game; function initGame(ctx) { var updateLength = 1; var updateCount = 0; var command = ''; var speechButtonRate = 1000; var nextSpeechButtonPressTime = 0; var isFairyNextToHouse = false; var toggleRecordSpeechCommandButton; var player; var houses; var house; var rightHousePosition = 720; var leftHousePosition = 0; var currentHousePosition = 1; var cursors; var level = 1; var levelString; var levelText; game = new Phaser.Game( 750, 300, Phaser.AUTO, 'example-game', { preload: preload, create: create, update: update, render: render } ); function preload() { var fairyData = [ '....F...........F...', '...F1F.........F1F..', '..F111F.......F111F..', '.F11111F.333.F1111F..', '.F11111534333511111F', 'F111113323333331111F', 'F111133333333333111F', 'F111137773337773111F', '.F1113858777858311F.', '.F1113858888858311F..', '.F111378888888731F..', '..F11133333333311F..', '...FF....65556......', '.......6.757.6......', '.......5.666.5......', '.......5.777.5......', '........6..7........', '........7..7........' ]; var houseData = [ '.....6.....', '....676....', '...67676...', '..6767676..', '.676767676.', '67676767676', '55BBB555555', '55BBB55EE55', '552BB55EE55', '55BBB556655', '55BBB555555', '55555555555', '55555555555' ]; game.create.texture('fairy', fairyData, 3, 3, 0); house = game.create.texture('house', houseData, 3, 3, 0); } function create() { game.physics.startSystem(Phaser.Physics.ARCADE); player = game.add.sprite(100, 100, 'fairy'); game.physics.arcade.enable(player); player.body.collideWorldBounds = true; player.body.gravity.y = 500; houses = game.add.physicsGroup(); house = houses.create(rightHousePosition, 260, 'house'); // The current level levelString = 'Level : '; levelText = game.add.text(10, 10, levelString + level, { font: '34px Arial', fill: '#fff' }); houses.setAll('body.immovable', true); cursors = game.input.keyboard.createCursorKeys(); toggleRecordSpeechCommandButton = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR); } function update() { } function render() { } return game; } exports.initGame = initGame;
在包含 game = new Phaser.Game(750, 300, Phaser.AUTO, 'example-game', { preload: preload, create: create, update: update, render: render });
的行中,记下字符串 'example-game'
。此名称是将作为游戏的容器的 HTML 元素 ID 的名称。
再次更新 views/index.ejs 文件,并将以下元素添加到 HTML 中:
<div id="example-game"></div>
initGame
函数初始化该游戏,该函数已从 src/views/game.js 中导出,如清单 12清单 12中所示。 清单 12. 初始化游戏
var initSessionPermissions = require('./sessionpermissions').initSessionPermissions; var initAnimatePanel = require('./animatepanel').initAnimatePanel; var initShowTab = require('./showtab').initShowTab; var initDragDrop = require('./dragdrop').initDragDrop; var initPlaySample = require('./playsample').initPlaySample; var initRecordButton = require('./recordbutton').initRecordButton; var initFileUpload = require('./fileupload').initFileUpload; var initDisplayMetadata = require('./displaymetadata').initDisplayMetadata; var initGame = require('./game').initGame; exports.initViews = function(ctx) { console.log('Initializing views...'); initPlaySample(ctx); initDragDrop(ctx); initRecordButton(ctx); initFileUpload(ctx); initSessionPermissions(); initShowTab(); initAnimatePanel(); initShowTab(); initDisplayMetadata(); initGame(ctx); };
$ npm build
和 $ npm start
来构建并运行它。 #example-game
元素内绘制的房子。 现在我们需要更新代码,以允许游戏触发和处理语音输入,处理空格按钮按下事件,以及将解析的关系与命令相关联。
清单 13. 触发并处理语音
var Microphone = require('../Microphone'); var handleMicrophone = require('../handlemicrophone').handleMicrophone; var showError = require('./showerror').showError; exports.initRecordButton = function(ctx) { var running = false; var micOptions = { bufferSize: ctx.buffersize }; var mic = new Microphone(micOptions); ctx.toggleRecordSpeechCommand = function() { var currentModel = localStorage.getItem('currentModel'); if (!running) { $('#resultsText').val(''); // clear hypotheses from previous runs console.log('Not running, handleMicrophone()'); handleMicrophone(ctx.token, currentModel, mic, function(err) { if (err) { var msg = 'Error: ' + err.message; console.log(msg); showError(msg); running = false; } else { console.log('starting mic'); mic.record(); running = true; } }); } else { console.log('Stopping microphone, sending stop action message'); $.publish('hardsocketstop'); mic.stop(); running = false; ctx.alchemyProxy.getAlchemyRelations( $('#resultsText').val(), function(err, response) { if (!err) { ctx.relations = response.relations; } } ); } }; };
清单 13清单 13 中的代码与之前类似,但按钮事件被删除,并使用 toggleRecordSpeechCommand
属性替换为一个分配给视图上下文的回调。 toggleRecordSpeechCommand
函数触发麦克风并使用之前公开的 alchemyProxy
对象将语音到文本转换结果提交到 Alchemy API。Alchemy API 成功处理关系后,会通过视图上下文的 relations
属性与其他视图共享。
清单 14. 添加空格按钮特性
var LEFT_COMMAND = 'left'; var RIGHT_COMMAND = 'right'; var subject = 'melvin'; var relationActions = [ { action: 'move', parseGameCommand: parseMoveActionCommand } ] function parseMoveActionCommand(text) { var directionCommands = [LEFT_COMMAND, RIGHT_COMMAND]; return directionCommands.find(function(directionCommand) { return text.toLowerCase().split(' ').indexOf(directionCommand) !== -1; }); } function parseRelationsForCommand(relations) { try { var relationWithSubject = relations.find(function(relation) { return relation.subject.text.toLowerCase() === subject; }); if (!relationWithSubject) { return; } var relationAction = relationActions.find(function(relationAction) { return relationAction.action === relationWithSubject.action.lemmatized.toLowerCase(); }); if (!!relationAction) { return relationAction.parseGameCommand(relationWithSubject.object.text); } } catch (e) { console.log('An error occurred while parsing speech commands', e); } }
update
函数以检测空格按下事件,并在视图上下文中检测到新关系时寻找命令,如清单 15清单 15中所示。 清单 15. 使用 update 函数
function update() { if (updateCount > updateLength) { command = ''; } if (ctx.relations) { console.log(ctx.relations); command = parseRelationsForCommand(ctx.relations); updateCount = 0; ctx.relations = null; } if (toggleRecordSpeechCommandButton.isDown) { if (game.time.time >= nextSpeechButtonPressTime) { nextSpeechButtonPressTime = game.time.time + speechButtonRate; console.log('toggling recording'); ctx.toggleRecordSpeechCommand(); } } switch (command) { case LEFT_COMMAND: updateCount++; player.body.velocity.x = -250; break; case RIGHT_COMMAND: updateCount++; player.body.velocity.x = 250; break; } }
使用这些命令构建并运行应用程序:
$ npm build
$ npm start
最后,让我们更新游戏来允许精灵 Melvin 销毁游戏中的房子并进入下一关。
relationActions
数组中,如清单 16清单 16中所示。 清单 16. 使用 relationActions 数组
function parseDestroyActionCommand(text) { if (isFairyNextToHouse amp;amp; text.toLowerCase().split(' ').indexOf('house') !== -1) { return DESTROY_HOUSE_COMMAND; } } var relationActions = [ { action: 'move', parseGameCommand: parseMoveActionCommand }, { action: 'destroy', parseGameCommand: parseDestroyActionCommand } ];
update
函数来检测精灵与房子之间的碰撞,然后响应销毁命令,如清单 17清单 17中所示。 清单 17. 创建碰撞处理函数
var DESTROY_HOUSE_COMMAND = 'destroy_house'; function collisionHandler() { isFairyNextToHouse = true; } function update() { game.physics.arcade.collide(player, houses, collisionHandler); ... switch (command) { case LEFT_COMMAND: updateCount++; player.body.velocity.x = -250; break; case RIGHT_COMMAND: updateCount++; player.body.velocity.x = 250; break; case DESTROY_HOUSE_COMMAND: house.kill(); isFairyNextToHouse = false; level++; levelText.text = levelString + level; command = ''; if (currentHousePosition == 1) { currentHousePosition = 2; house = houses.create(leftHousePosition, 260, 'house'); houses.setAll('body.immovable', true); } else { currentHousePosition = 1; house = houses.create(rightHousePosition, 260, 'house'); houses.setAll('body.immovable', true); } break; } }
使用这些命令构建并运行应用程序:
$ npm build
$ npm start
可在 这里 播放最终游戏的演示,可在 这里 查看最终的源代码。
运行应用程序
获取代码
在本文中,您学习了如何使用 Watson Speech to Text 服务和 Alchemy API 服务来创建一个语音控制游戏。您直接体验到了这些 API 的强大功能和潜力。希望您已受到启发,会试验并创建应用程序来扩展这个游戏。
BLUEMIX SERVICE USED IN THIS TUTORIAL: IBM Watson Speech to Text 此服务将人类语音转换为书面文字。使用它填补口语与书面语之间的空白,包括语音控制嵌入式系统,听译会议和电话会议,听写电子邮件和笔记。