AISpeech API ASSDK 教程 2:一个更完整的版本
在本文中,我们将构建一个更完整的版本。除了查看 ASSDK 返回和响应的内容外,我们还将探索 ASSDK 提供的几乎所有主要方法。
引言
在上一篇文章中,我们快速回顾了使用 AISpeech API 云服务及其 ASSDK (ActionScript 3.0) 构建语音英语测评应用的过程。
在本文中,我们将构建一个更完整的版本。除了查看 ASSDK 返回和响应的内容外,我们还将探索 ASSDK 提供的几乎所有主要方法。
本教程的源代码可在 github 上找到。
目标
我们将构建一款语音英语测评应用。这是一个 Flex 应用。用户界面如图 1 所示。
图 1 应用线框图
分数显示
这次,我们将参考文本的每个单词着色,以指示发音分数。绿色 (#00FF00
) 表示非常好。黄色 (#FFD800
) 表示良好。橙色 (#FF6A00
) 表示差。红色 (#FF0000
) 表示非常差。我们给每个单词一个 <span>
HTML 标签,并为其分配颜色属性。然后,我们使用 Flex TextArea
(spark) 组件显示 HTML 内容。
我们从一个 Flex TextArea
(spark) 组件开始:
<s:TextArea id="txtScores" fontSize="30" editable="false"/>
然后我们添加以下脚本:
import spark.utils.TextFlowUtil;
private var test:String =
"<span color='#00ff00'>I </span> " +
"<span color='#ffd800'>like </span> " +
"<span color='#ffd800'>to </span> " +
"<span color='#ff6a00'>play </span> " +
"<span color='#ff0000'>piano </span> ";
private function init():void
{
txtScores.textFlow = TextFlowUtil.importFromString(test);
}
我们需要将 init()
方法分配给应用程序的 creationComplete
回调方法。还要注意我们附加到每个单词的空格。它显示为
图 2 彩色分数输出
其他控件
如图 1 所示,我们还需要其他几个控件。脚本如下:
<s:VGroup>
<s:HGroup>
<s:Label text="Reference Text"/>
<s:TextInput id="txtRefText" text="I like to play piano" fontSize="30" width="627"/>
</s:HGroup>
<s:HGroup>
<s:Button label="Start Record" fontSize="20" />
<s:Button label="Stop Record" fontSize="20" />
<s:CheckBox label="Play 'ding'" id="chboxPlayDing" selected="true" />
<s:Button label="Replay the latest" fontSize="20" />
</s:HGroup>
<s:HGroup>
<s:Label text="Scores" />
<s:TextArea id="txtScores" width="666" height="50" editable="false" fontSize="30" />
</s:HGroup>
<s:HGroup>
<s:Label text="RecorderLib returns and events"/>
<s:TextArea id="txtReturns" width="531" height="100"/>
</s:HGroup>
<s:Label text="RecordLib components states"/>
<s:Label text="Microphone: " id="txtMicrophoneState" fontSize="20"/>
<s:Label text="Connection: " id="txtConnectionState" fontSize="20"/>
<s:Label text="Core requester: " id="txtCoreRequesterState" fontSize="20"/>
<s:Label text="Recorder: " id="txtRecorderState" fontSize="20"/>
</s:VGroup>
完成的布局如下所示。
图 3 完成的布局
使用 AISpeech API ASSDK 录制用户语音
如前一篇文章所述,AISpeech API ASSDK 以 AISpeechLib.swc 的形式提供。将 Flex 应用链接到此库。以下代码准备并初始化一个 RecorderLib
实例。
import com.aispeech.RecorderLib;
private static const RECORDERLIB_PARAMS:Object = {
appKey:"your application ID",
secretKey:"your secret Key",
serverList:["rtmp://demo.aispeech.com:80/v2.0/aistream","rtmpt://demo.aispeech.com:433/v2.0/aistream"]
};
private var _coreRequesterParams:Object = {
refText:"past", // this to be updated for each core request (record)
rank:4, // we use four-level-scores this time
coreType:"en.sent.score", // request the English-Senetence core
userId:"xxxxxx",
applicationId:"your application ID" // application ID again
};
private var _recorder:RecorderLib;
private function init():void
{
_recorder = new RecorderLib();
_recorder.init(RECORDERLIB_PARAMS);
}
为了使本教程能够独立完成,有几点需要注意,尽管这些内容已在上一教程中介绍过。
RECORDERLIB_PARAMS
用于初始化RecorderLib
实例。appKey
和secretKey
由 AISpeech 提供,需要向其申请。serverLists
包含 AISpeech API 云访问点的列表。_coreRequesterParams
用于向语音核心发出请求。每次录音都对应一个核心请求。refText
可能会针对每个请求/录音进行更新。
运行应用程序,如图所示,您应该会看到一个请求用户使用麦克风设备的权限的对话框。我们说“这个提示是一个很好的指标,表明 RecorderLib
已成功加载”,但这并不完全准确。稍后我们将找出原因。
图 4 请求用户权限的对话框
然后我们添加 startRecord()
和 stopRecord()
方法,如下所示:
private function startRecord():void
{
_coreRequesterParams.refText = txtRefText.text;
var recordLength:int = 2500 + txtRefText.text.split(" ").length * 450;
var recorderParams:Object =
{
serverParam:_coreRequesterParams,
recordLength:recordLength // ms
};
_recorder.startRecord(recorderParams);
}
private function stopRecord():void
{
_recorder.stopRecord();
}
有几点需要注意:
- 在将
_coreRequesterParams.refText
发送到_recorder
的startRecord()
方法之前,我们会根据用户的输入 (txtRefText.txt
) 更新它。 - 我们根据参考文本中的单词数量计算录音时长(毫秒)。
- 然后,我们将
startRecord()
方法分配给“开始录音”按钮的点击回调,并将stopRecord()
方法分配给“停止录音”按钮的点击回调。
运行项目。单击“开始”按钮。如果听到“叮”的一声,则表示录音现在工作正常。
显示分数
在本节中,我们将分三个步骤进行:获取 API 响应、构建 HTML 字符串和显示分数。
以下脚本提供了一个事件处理程序来解析分数结果:
private function coreRequesterEventHandler(event:CoreRequesterEvent):void
{
if (event.type == CoreRequesterEvent.RESULT)
{
// parse results
var details:Array = event.data.result.details;
trace("length of details: " + details.length);
}
}
以下脚本(位于 init()
方法内)让 RecorderLib
实例监听 CoreRequesterEvent.RESULT
事件。请注意,addEventListener()
方法在 RecorderLib
实例被创建后、初始化之前调用。
_recorder = new RecorderLib();
_recorder.addEventListener(CoreRequesterEvent.RESULT, coreRequesterEventHandler);
_recorder.init(RECORDERLIB_PARAMS);
在 coreResultEventHandler()
方法中,我们输出了 details
的长度。调试项目,检查控制台,可以看到 details
的长度为 5,给定参考文本为“I like to play piano”,这是正确的。
在调用 trace()
方法的那一行设置一个断点。再次调试项目。对着麦克风说话,程序会停止。如图所示,details
数组元素的 score
属性为“4”。由于我们将 _coreRequesterParams
对象的 rank
属性设置为 4,因此语音核心现在返回 4 级分数。4 是最高分,1 是最低分。
图 5 结果
以下代码提供了一个辅助函数,用于构建 HTML 字符串,该字符串根据单词的分数对其进行彩色标记。请记住规则:红色表示非常差,绿色表示非常好。介于两者之间的是橙色和黄色。
private function colourfulScoreHelper(refText:String, details:Array):String
{
// red, orange, yellow, green
var colourArray:Array = ["#FF0000", "#FF6A00", "#FFD800", "#00FF00"];
var refTextArray:Array = refText.split(" ");
var htmlString:String = "";
var temp:String = "";
for (var i:int = 0; i < refTextArray.length; i ++)
{
// recall the example: "<span color='#00ff00'>I </span>"
temp = "<span color='" +
colourArray[details[i].score - 1] +
"'>" +
refTextArray[i] +
" </span>";
htmlString += temp;
}
return htmlString;
}
思路是:我们使用单词的分数作为索引,从 colourArray
中选择颜色值。其余的只是简单的字符串拼接。我们可以进一步将 colourArray
设为常量成员变量。
回到 coreRequesterEventHandler()
方法。以下代码调用 HTML 字符串辅助函数并更新 txtScores
TextArea 组件。
private function coreRequesterEventHandler(event:CoreRequesterEvent):void
{
if (event.type == CoreRequesterEvent.RESULT)
{
// parse results
var details:Array = event.data.result.details;
var htmlString:String = colourfulScoreHelper(_coreRequesterParams.refText, details);
txtScores.textFlow = TextFlowUtil.importFromString(htmlString);
}
}
项目应该可以工作,并显示彩色且有意义的语音评估结果。
到目前为止,我们几乎重复了上一教程中的内容。但这次,我们实现了一个新的分数视图,并尝试了 4 级分数。在下一节中,我们将更深入地研究 RecorderLib
。
RecorderLib 组件
尽管 RecorderLib
只暴露了少量方法和属性,但其内部相当复杂。简单来说,RecorderLib
包含四个主要组件:麦克风、录音器、连接和核心请求器。我们不会深入研究它们的实现。(实际上,AI Speech Ltd 及其工程师计划开源 ASSDK,可能在下一次主要更新之后。)在演示中,我们将看到 RecorderLib
除了我们处理过的核心结果之外,还会返回哪些其他信息。
我们从方法返回值开始。
根据 ASSDK 文档,init()
和 startRecord()
等方法都返回一个 StatusCode
。我们现在将它们显示在 txtReturns
中,这是一个我们在布局中定义的 TextArea 实例。
我们首先提供一个将字符串附加到 txtReturns
的函数:
private function appendReturns(message:String):void
{
txtReturns.text += (message + "\n");
}
然后我们将 appendReturns()
方法应用于调用 RecorderLib
的 init()
、startRecord()
和 stopRecord()
方法(到目前为止我们只知道这三个方法)。例如:
appendReturns(_recorder.init(RECORDERLIB_PARAMS));
运行应用程序,并进行一些操作(例如,单击“开始录音”和“停止录音”按钮)。这次,“RecorderLib 返回和事件”显示了一些“日志”。其中大部分是 StatusCode
或数字。根据 ASSDK 文档,我列出了一些 StatusCode
及其含义如下:
- 50004:成功
- 50005:参数错误
- 50003:麦克风设备不可用
- ……
现在我们知道 RecorderLib
方法返回 StatusCode
。让我们看看 RecordeLib
分派了哪些事件。
我们已经知道了 CoreRequesterEvent.RESULT
。此事件对于捕获 API 响应和解析结果(分数)至关重要。RecorderLib
分派许多其他事件。以下是完整列表:
- FactoryEvent
- READY
- EXCEPTION_TIMEOUT
- NetEvent
- EXCEPTION_CLOSED
- CoreRequesterEvent
- RESULT
- EXCEPTION_TIMEOUT
- EXCEPTION_PARAMETERS_ERROR
- EXCEPTION_RESPONSE_ERROR
- MicrophoneDeviceEvent
- MIC_ALLOWED
- EXCEPTION_MIC_NOT_ALLOWED
- EXCEPTION_MIC_NOT_FOUND
- RecorderEvent
- RECORD_STARTED
- RECORD_STOPPED
- REPLAY_STARTED
- REPLAY_STOPPED
- RECORDID_GOT
- EXCEPTION_NO_RECORD
请注意,RecorderLib
有一个工厂组件,它初始化 RecorderLib
实例,连接到服务器,并自动连接上述四个主要组件。在工厂完成工作之前,RecorderLib
实例不应进行录音或请求语音核心。因此,出现了 FactoryEvent.READY
事件。这就是为什么我们指出,看到请求用户权限的对话框并不一定意味着 RecorderLib
实例已成功初始化。事件不会撒谎。
在以下脚本中,我们添加了几个事件监听器来监听所有定义的事件。请注意,上面列出的所有事件都扩展自 AIEvent
类。除了 CoreRequesterEvent.RESULT
,我们只使用一个事件处理程序来处理其他事件:将它们打印出来。这仅用于演示目的。在实际应用程序中,每个事件都应仔细处理。例如,RecorderEvent.RECORD_STARTED
和 RecorderEvent.RECORD_STOPPED
可以组合起来控制录音进度条。作为另一个例子,RecorderEvent.RECORDID_GOT
可用于检索本地缓存录音的 recordId
。
_recorder = new RecorderLib();
_recorder.addEventListener(CoreRequesterEvent.RESULT, coreRequesterEventHandler);
// add the other event listeners
_recorder.addEventListener(FactoryEvent.READY, eventHandler);
_recorder.addEventListener(FactoryEvent.EXCEPTION_TIMEOUT, eventHandler);
_recorder.addEventListener(NetEvent.EXCEPTION_CLOSED, eventHandler);
_recorder.addEventListener(CoreRequesterEvent.EXCEPTION_PARAMETERS_ERROR, eventHandler);
_recorder.addEventListener(CoreRequesterEvent.EXCEPTION_RESPONSE_ERROR, eventHandler);
_recorder.addEventListener(CoreRequesterEvent.EXCEPTION_TIMEOUT, eventHandler);
_recorder.addEventListener(MicrophoneDeviceEvent.MIC_ALLOWED, eventHandler);
_recorder.addEventListener(MicrophoneDeviceEvent.EXCEPTION_MIC_NOT_ALLOWED, eventHandler);
_recorder.addEventListener(MicrophoneDeviceEvent.EXCEPTION_MIC_NOT_FOUND, eventHandler);
_recorder.addEventListener(RecorderEvent.EXCEPTION_NO_RECORD, eventHandler);
_recorder.addEventListener(RecorderEvent.RECORD_STARTED, eventHandler);
_recorder.addEventListener(RecorderEvent.RECORD_STOPPED, eventHandler);
_recorder.addEventListener(RecorderEvent.RECORDID_GOT, eventHandler);
_recorder.addEventListener(RecorderEvent.REPLAY_STARTED, eventHandler);
_recorder.addEventListener(RecorderEvent.REPLAY_STOPPED, eventHandler);
appendReturns(_recorder.init(RECORDERLIB_PARAMS));
请注意,我们在初始化 RecorderLib
实例之前添加了事件监听器。
eventHandler()
方法简单地将 event.type
附加到 txtReturns
TextArea:
private function eventHandler(event:AIEvent):void
{
appendReturns(event.type);
}
现在,我们可以看到更多的日志输出。例如:
microphone.device.exception.mic.not.allowed [{"message":"","statusCode":"50004"}] factory.ready microphone.device.mic.allowed recorder.recordID.got recorder.record.started [{"message":"start recording","statusCode":"50004"}] recorder.record.stopped [{"message":"stop record","statusCode":"50004"}]
除了方法返回值和事件之外,RecorderLib
还允许应用程序查看其组件的状态。
在以下代码中,我们设置了一个计时器,自动检查 RecorderLib
实例的状态,并将它们打印到屏幕上。首先是 checkStates()
方法:
private function checkStates(event:TimerEvent):void
{
txtMicrophoneState.text = "Microphone: " + _recorder.microphoneDeviceState;
txtRecorderState.text = "Recorder: " + _recorder.recorderState;
txtConnectionState.text = "Connection: " + _recorder.connectionState;
txtCoreRequesterState.text = "Core requester: " + _recorder.coreRequesterState;
}
然后在 init()
方法中,我们设置了一个计时器:
_timer = new Timer(2000);
_timer.addEventListener(TimerEvent.TIMER, checkStates);
_timer.start()
请注意,_timer
是一个 Timer
实例,定义为私有成员变量。
现在,应用程序每 2 秒检查一次 RecorderLib
实例的组件状态,并显示它们(作为 String
),如下所示:
图 6 组件状态
另外两个 RecorderLib 功能。
我们想收听最新的录音。将以下代码分配给“重播最新录音”按钮的点击回调:
appendReturns(_recorder.startReplay({}));
几点说明:
- 我们调用
startReplay()
方法进行重播。我们给这个方法一个空对象“{}
”,以便播放最新的本地缓存录音。 - 我们将
startReplay()
方法用appendReturns()
包装起来,以便我们能在屏幕上看到它的返回值。
回想图 1。在该线框图中,我们设计了一个复选框“播放叮声”。我们注意到 RecorderLib
会播放一声“叮”来通知用户开始说话。这是一个很好的语音用户界面设计,系统应该响应用户的操作(单击“开始录音”按钮)。这也有助于确保用户在实际录音功能开始后才说话。但是,如果您不想播放“叮”声,请随意这样做。然而,响应用户的操作是一个最佳实践,因此录音进度条可能是一个很好的补充。
回想 startRecord()
方法,我们在 recorderParam
变量中设置了两个属性(serverParam
和 recordLength
)。RecorderLib.startRecord()
方法接受另一个属性:playDing
。请看以下代码:
private function startRecord():void
{
_coreRequesterParams.refText = txtRefText.text;
var recordLength:int = 2500 + txtRefText.text.split(" ").length * 450;
_playDing = chboxPlayDing.selected;
var recorderParams:Object =
{
serverParam:_coreRequesterParams,
recordLength:recordLength, // ms
playDing:_playDing
};
appendReturns(_recorder.startRecord(recorderParams));
}
结论
好了,这篇文章很长。但我们现在都完成了。
在本教程中,在“RecorderLib 组件”部分之前,与上一教程相比,有两件新事物:
- 我们使用 Flex TextArea 渲染 HTML 字符串,以彩色显示分数。
- 我们要求语音核心返回 4 级分数。
在“RecorderLib 组件”及后续部分,我们捕获了 RecorderLib
的方法返回值、事件和组件状态。我们只是将它们打印在屏幕上。但这些信息对于构建一个防错且用户友好的应用程序非常重要。
如我所承诺的,我已向 AI Speech Ltd 索取了 ASSDK 文档的英文版本。据我所知,在撰写本文时,他们正在为源代码添加 ASDoc 注释。我可能会在某个地方上传 PDF 版本。
另外,本教程的源代码可以在 github 上找到。
感谢阅读。
历史
这是本文的第一个版本。