【Dueros技能开发】人人可做开发者——问答技能添加图片模版
蓝****1 · 蓝****1 发布于2019-08-16 20:57 浏览:1496 回复:1

摘要

       在创建一个问答类技能之后,系统会自动创建意图也会生成初始化的模版代码。针对有屏类的设备,技能展示的是黑色的背景上显示问题的文字,看起来会很单调,本文主要讲解如何为问答类技能添加背景图片模版。

创建技能

       首先在控制台中申请一个问答类的技能模版,为技能起一个朗朗上口,突出主题的名字。应用场景当然要选择有屏场景,无屏场景有无都可。一切就绪,点击“确定”按钮,就创建类一个问答类的技能。

       

初始技能

       创建好技能之后,系统会自动生成意图,用于答案的识别和捕获。打开服务配置,就可以看到如下的初始化代码。

const Bot = require('bot-sdk');
const privateKey = require("./rsaKeys.js").privateKey;
const questions = {
  QUESTIONS: [
    {
      '远上还山石径斜,白云深处有人家': [
        '正确答案',
        '杜甫',
        '白居易',
        '李白',
      ],
    },
    {
      '离离原上草,一岁一枯荣': [
        '正确答案',
        '杜甫',
        '白居易',
        '李白',
      ],
    },
    {
      '举头望明月,低头思故乡': [
        '正确答案',
        '杜甫',
        '白居易',
        '李白',
      ],
    },    {
      '锄禾日当午 汗滴禾下土': [
        '正确答案',
        '杜甫',
        '白居易',
        '李白',
      ],
    },
    {
      '白日依山尽,黄河入海流': [
        '正确答案',
        '杜甫',
        '白居易',
        '李白',
      ],
    },
    {
      '李白乘舟将欲行,忽闻岸上踏歌声': [
        '正确答案',
        '杜甫',
        '白居易',
        '李白',
      ],
    },
    {
      '横看成岭侧成峰,远近高低各不同': [
        '正确答案',
        '杜甫',
        '白居易',
        '李白',
      ],
    },
    {
      '人生自古谁无死,留取丹心照汗青': [
        '文天祥',
        '杜甫',
        '白居易',
        '李白',
      ],
    },
  ],
};


//定义一轮问答中的问题数量
const GAME_LENGTH = 5;
//定义每个问题的答案数量
const ANSWER_COUNT = 3;

class InquiryBot extends Bot {
    constructor(postData) {
        super(postData);
        this.addLaunchHandler(() => {
        	this.waitAnswer();
			let speechOutput = '欢迎来到古诗问答。我将念两句古诗并给你四个诗人的名字。需要你告诉我哪一个是正确的作者。';
			//初始化一轮中的问题列表和第一题的话术
			let repromptText = this.startNewGame();
			let card = new Bot.Card.TextCard(repromptText);
			return {
				card: card,
				outputSpeech: speechOutput + repromptText
			};
        });

        this.addSessionEndedHandler(() => {
            this.endSession();
            return {
                outputSpeech: '谢谢使用!'
            };
        });

        this.addIntentHandler('answer_intent', () => {
            this.waitAnswer();
            //确保获取到了用户的回答
            let theAnswer = this.getSlot('theAnswer');
            if (!theAnswer) {
                this.nlu.ask('theAnswer');
                return {
                    outputSpeech: '您的答案是哪个?'
                };
            }
            //获取session中相关信息
            let questionsList = this.getSessionAttribute('questionsList');
            let score = this.getSessionAttribute('score');
            let currentQuestionIndex = this.getSessionAttribute('currentQuestionIndex');
            let correctAnswerIndex = this.getSessionAttribute('correctAnswerIndex');
            let gameQuestions = this.getSessionAttribute('gameQuestions');
            let correctAnswerText = this.getSessionAttribute('correctAnswerText');
            let speechOutput = '';
            if (theAnswer == correctAnswerIndex){
            	score += 1;
            	speechOutput = '回答正确,得一分。目前得分:' + score + '分。';
            }else{
            	speechOutput = '很遗憾,回答错误。正确答案是' + correctAnswerText + '.目前得分:' + score + '分。';
            }
            //到达最后一题,用户选择重新开始一轮或者退出技能
            if (currentQuestionIndex == GAME_LENGTH - 1){
            	speechOutput += '已经是最后一题了。您可以说重新开始来继续答题,或者说退出来退出技能。'
            	return {
                	outputSpeech: speechOutput
            	};
            }
            //获取下一题信息
			currentQuestionIndex += 1;
			correctAnswerIndex = Math.floor(Math.random() * (ANSWER_COUNT));
			let spokenQuestion = Object.keys(questionsList[gameQuestions[currentQuestionIndex]])[0];
			let roundAnswers = this.populateRoundAnswers(gameQuestions, currentQuestionIndex,correctAnswerIndex,questionsList);
			let questionIndexForSpeech = currentQuestionIndex + 1;
			let repromptText = '第' + questionIndexForSpeech + '题:\n' + spokenQuestion + '\n';
			for (let i = 0; i < ANSWER_COUNT; i += 1) {
				repromptText += `${i + 1}. ${roundAnswers[i]}. `;
			}
			speechOutput += repromptText;
			let currentQuestion = questionsList[gameQuestions[currentQuestionIndex]];
			this.setSessionAttribute('speechOutput',speechOutput);
			this.setSessionAttribute('currentQuestionIndex',currentQuestionIndex);
			this.setSessionAttribute('correctAnswerIndex',correctAnswerIndex + 1);
			this.setSessionAttribute('gameQuestions',gameQuestions);
			this.setSessionAttribute('questionsList',questionsList);
			this.setSessionAttribute('score',score);
			this.setSessionAttribute('correctAnswerText',currentQuestion[Object.keys(currentQuestion)[0]][0]);
			let card = new Bot.Card.TextCard(repromptText);
			return {
				card: card,
				outputSpeech: speechOutput
			};
        });
        
        //重新开始答题,得分清零
        this.addIntentHandler('newGame_intent', () => {
        	this.waitAnswer();
        	//初始化一轮中的问题列表和第一题的话术
			let repromptText =  this.startNewGame();
			let card = new Bot.Card.TextCard(repromptText);
			return {
				card: card,
				outputSpeech: '好的,重新开始。' + repromptText
			};
        });
    }
    
    
    /**
     *  获取新一轮问题列表和相应的信息,并将信息存入session中
     *
     *  @return 新一轮答题话术
     */
    startNewGame() {
		let questionsList = questions.QUESTIONS;
		let gameQuestions = this.populateGameQuestions(questionsList);
		let correctAnswerIndex = Math.floor(Math.random() * (ANSWER_COUNT));
		console.log(correctAnswerIndex);
		let roundAnswers = this.populateRoundAnswers(gameQuestions, 0,correctAnswerIndex,questionsList);
		let currentQuestionIndex = 0;
		let spokenQuestion = Object.keys(questionsList[gameQuestions[currentQuestionIndex]])[0];
		let repromptText = '第1题:\n' + spokenQuestion + '\n';
		for (let i = 0; i < ANSWER_COUNT; i += 1) {
			repromptText += `${i + 1}. ${roundAnswers[i]}. `;
		}

		let currentQuestion = questionsList[gameQuestions[currentQuestionIndex]];

		this.setSessionAttribute('currentQuestionIndex',currentQuestionIndex);
		this.setSessionAttribute('correctAnswerIndex',correctAnswerIndex + 1);
		this.setSessionAttribute('gameQuestions',gameQuestions);
		this.setSessionAttribute('questionsList',questionsList);
		this.setSessionAttribute('score',0);
		this.setSessionAttribute('correctAnswerText',currentQuestion[Object.keys(currentQuestion)[0]][0]);
		return repromptText;
    }
    
    /**
     *  从问题列表中随机抽取问题。问题个数由变量GAME_LENGTH定义
     *  @param {list} translatedQuestions 所有问题列表
     *  @return 问题id列表
     */
	populateGameQuestions(translatedQuestions) {
	  let gameQuestions = [];
	  let indexList = [];
	  let index = translatedQuestions.length;
	  if (GAME_LENGTH > index) {
		throw new Error('Invalid Game Length.');
	  }

	  for (let i = 0; i < translatedQuestions.length; i += 1) {
		indexList.push(i);
	  }

	  for (let j = 0; j < GAME_LENGTH; j += 1) {
		let rand = Math.floor(Math.random() * index);
		index -= 1;

		let temp = indexList[index];
		indexList[index] = indexList[rand];
		indexList[rand] = temp;
		gameQuestions.push(indexList[index]);
	  }
	  return gameQuestions;
	}
	
    /**
     *  从问题列表中随机抽取问题。问题个数由变量GAME_LENGTH定义
     *  @param {list} gameQuestionIndexes 一轮问答中问题id列表
     *  @param {int} currentQuestionIndex 当前问题Index
     *  @param {int} correctAnswerTargetLocation 当前问题答案Index
     *  @param {list} translatedQuestions 所有问题列表
     *  @return 当前问题答案选项列表
     */
	populateRoundAnswers(gameQuestionIndexes,currentQuestionIndex,correctAnswerTargetLocation,translatedQuestions) {
	  const answers = [];
	  const translatedQuestion = translatedQuestions[gameQuestionIndexes[currentQuestionIndex]];
	  const answersCopy = translatedQuestion[Object.keys(translatedQuestion)[0]].slice();
	  let index = answersCopy.length;

	  if (index < ANSWER_COUNT) {
		throw new Error('Not enough answers for question.');
	  }

	  // 打乱当前问题答案列表顺序
	  for (let j = 1; j < answersCopy.length; j += 1) {
		const rand = Math.floor(Math.random() * (index - 1)) + 1;
		index -= 1;

		const swapTemp1 = answersCopy[index];
		answersCopy[index] = answersCopy[rand];
		answersCopy[rand] = swapTemp1;
	  }

	  // 将正确答案放置到correctAnswerTargetLocation的位置
	  for (let i = 0; i < ANSWER_COUNT; i += 1) {
		answers[i] = answersCopy[i];
	  }
	  const swapTemp2 = answers[0];
	  answers[0] = answers[correctAnswerTargetLocation];
	  answers[correctAnswerTargetLocation] = swapTemp2;
	  return answers;
	}
}

exports.handler = function(event, context, callback) {
    try {
        let b = new InquiryBot(event);
        // 0: debug  1: online
        b.botMonitor.setEnvironmentInfo(privateKey, 0);
        b.run().then(function(result) {
            callback(null, result);
        }).catch(callback);
    } catch (e) {
        callback(e);
    }
}

       开发自己的技能需要对上面的代码做修改,在其中添加和修改成自己技能的代码。在有屏模拟器上运行上面的代码会发现,运行的结果只有黑色的屏幕和文字,看起来更单调。作为具有一定审美能力的开发者,这个不能忍,那就给它美化一下。

       

添加模版

       DuerOS提供了多种展现模板供开发者使用。展现模板分body template和list template两种类型。其中body template由图片和文字组成,list template由一系列list item组成,每个list item由图片和文字组成。不同的展现模板适合不同的场景,开发者可以根据技能展现的需求选择合适的模板。有如下几种模版:

       BodyTemplate1模板

       BodyTemplate1模板适用于同时展现图片和文字的场景,其中图片展现在上方,文字展现在下方,模板包含以下内容。

       

       BodyTemplate2模板

       BodyTemplate2适用于仅展现文本信息的场景。

       

       BodyTemplate3模板

       BodyTemplate3模板适用于同时展现图片和文字的场景,其中图片展现在屏幕左侧,文字展现在屏幕右侧。

       

       BodyTemplate4模板

       BodyTemplate4模板适用于同时展现图片和文字的场景,其中图片展现在屏幕右侧,文字展现在屏幕左侧。

       

       BodyTemplate5模板

       BodyTemplate5模板适用于展现图片的场景,可以展现多张图片。

       

       BodyTemplate6模板

       BodyTemplate6模板适用于同时展现图片和文字的场景,其中图片展现在上方,文字展现在下方,图片可以在区域内进行自适应展示。

       

       可以针对技能的主题选择不同的模版使用,本文主要针对BadyTemplate2模版的使用

	/**
	* 获取图文模版
	*/
	
	getImgTem(title,content,imgsrc){
		let bodyTemplate = new Bot.Directive.Display.Template.BodyTemplate2();
		bodyTemplate.setToken('890989');
		bodyTemplate.setBackGroundImage(imgsrc);
		bodyTemplate.setTitle(title);
		bodyTemplate.setPlainContent(content);
		let renderTemplate = new Bot.Directive.Display.RenderTemplate(bodyTemplate);
		return renderTemplate;
	}

       加上模版之后,在技能输出中使用就可以了。

let renderTemplate = this.getImgTem('理想伴侣',repromptText, ANSWER_BACKIMG);
return {
    directives: [renderTemplate],
    outputSpeech: speechOutput + repromptText
};

查看效果

       通过加上有屏模版就可以更好的展现内容,这样就能让问答类的有屏技能更酷炫。

       

测试上线

       功能开发之后要认真测试,测试通过之后就能申请上线啦!

点赞  ( 1 )
收藏
评论(1)
共1条回复 最后由乐****小回复于2019-09-03 23:06
#2乐****小回复于2019-09-03 23:06:22
该评论已删除
TOP