AI 音频产品开发模板及流程(一)
1. 准备工作
前提条件
- 已阅读 Ray 新手村任务,了解 Ray 框架的基础知识。
- 已阅读 使用 Ray 开发万能面板,了解 Ray 面板开发的基础知识。
构建内容
您可以利用面板小程序开发构建出一个基于 Ray 框架的 AI 音频设备面板,并实现以下功能:
- 面对面翻译:选择好需要互转的语言,使用不同语言的双方对话时,App 会自动断句并转写、翻译,设备自动播放翻译结果。
- 同声传译:设置输入语言和翻译语言,点击开始后 App 会自动进行断句,转写文字并进行翻译。
- 现场录音:将耳机等拾音设备放置在桌面上,点击开始录音,就会自动录下现场声音,结束后可以进行转写和 AI 总结。
- 转录和 AI 总结:将音频内容转换为文字,AI 根据模板总结内容,并生成 Markdown 格式文本。
所需条件
- 智能生活 App
- Tuya MiniApp IDE
- NVM 及 Node 开发环境(建议使用 18.x 系列版本)
- Yarn 依赖管理工具
详见 面板小程序 > 搭建环境。
2. 创建产品
首先需要创建一个产品,定义产品有哪些功能点,然后再在面板中一一实现这些功能点。
注册登录 涂鸦开发者平台,并在平台创建产品:
- 单击页面左侧 产品 > 产品开发,在 产品开发 页面单击 创建产品。
- 在 标准类目 下选择 影音穿戴 > AI 耳机
- 选择 智能化方式 和 产品方案,完善产品信息,单击 创建产品。
- 在 添加标准功能 页面,根据实际需求选择对应的功能点,单击 确定。
🎉 完成以上步骤后,您已成功创建了一个支持 AI 音频实时、离线转录的设备产品。
3. 创建项目
开发者平台创建面板小程序
面板小程序的开发在 小程序开发者 平台上进行操作,首先请前往 小程序开发者平台 完成平台的注册登录。
IDE 基于模板创建项目工程
打开 IDE 创建一个基于 AI 耳机模版 的 AI 音频面板小程序项目,需要在 Tuya MiniApp IDE 上进行操作。
4. 关键能力依赖
- App 版本
- 智能生活 v6.3.0 及以上版本
- Kit 依赖
- BaseKit:v3.0.6
- MiniKit:v3.0.1
- DeviceKit:v4.14.0
- BizKit:v4.2.0
- WearKit:v1.1.6
- baseversion:v2.27.0
- 组件依赖
- @ray-js/ray^1.7.14
5. 面对面翻译
- 实现现场跨语言交流,打破语言壁垒,让不同语言用户可以顺畅沟通。
- 自动断句、转写和翻译,提高交流效率。
- 翻译结果可由设备自动播放,沟通体验更自然。
- 支持多语言互转,适用场景广泛,提升产品智能化和国际化能力。
功能展示
代码片段
// 开始录音const handleStartRecord = useCallback((type: 'left' | 'right') => {const startRecordFn = async (type: 'left' | 'right') => {// 需要设备在线if (!isOnline) return;try {showLoading({ title: '' });const config: any = {// 录音类型,0:呼叫、1:会议、2:同声传译、3:面对面翻译recordType: 3,// DP 控制超时时间,单位秒controlTimeout: 5,// 灌流超时时间,单位秒dataTimeout: 10,// 0:文件转写、1:实时转写transferType: 1,// 是否需要翻译needTranslate: true,// 输入语言originalLanguage: type === 'left' ? leftLanguage : rightLanguage,// 输出语言targetLanguage: type === 'left' ? rightLanguage : leftLanguage,// 智能体 ID,后面具体根据提供的 SDK 获取 agentId。agentId: '',// 录音通道,0:BLE、1:Bt、2:microrecordChannel: isCardStyle || isDevOnline === false ? 2 : 1,// 0:代表左耳、1:代表右耳f2fChannel: type === 'left' ? 0 : 1,// TTS 流编码方式,通过编码后将流写入到耳机设备,0:opus_silk、1:opus_celtttsEncode: isOpusCelt ? 1 : 0,// 是否需要 TTSneedTts: true,};await tttStartRecord({deviceId,config,},true);setActiveType(type);hideLoading();setIntervals(1000);lastTimeRef.current = Date.now();} catch (error) {ty.showToast({title: Strings.getLang('error_simultaneous_recording_start'),icon: 'error',});hideLoading();}};ty.authorize({scope: 'scope.record',success: () => {startRecordFn(type);},fail: e => {ty.showToast({ title: Strings.getLang('no_record_permisson'), icon: 'error' });},});},[deviceId, isOnline, rightLanguage, leftLanguage]);
// 暂停const handlePauseRecord = async () => {try {const d = await tttPauseRecord(deviceId);setActiveType('');console.log('pauseRecord', d);setIntervals(undefined);} catch (error) {console.log('handlePauseRecord fail', error);}};
// 继续录音const handleResumeRecord = async () => {if (!isOnline) return;try {const d = await tttResumeRecord(deviceId);setIntervals(1000);lastTimeRef.current = Date.now();} catch (error) {console.log('handleResumeRecord fail', error);}};
// 停止录音const handleStopRecord = async () => {try {showLoading({ title: '' });const d = await tttStopRecord(deviceId);hideLoading();console.log('stopRecord', d);backToHome(fromType);} catch (error) {hideLoading();}};
// 监听 ASR 和翻译返回onRecordTransferRealTimeRecognizeStatusUpdateEvent(handleRecrodChange);// 处理 ASR 和翻译const handleRecrodChange = d => {try {const {// 阶段,0:任务、4:ASR、5:翻译、6:skill、7:TTSphase,// 阶段状态,0:未开启、1:进行中、2:结束、3:取消status,requestId,// 转写的文本text,// 错误码errorCode,} = d;// ASR 阶段接收并实时更新对应 requestId 文本if (phase === 4) {const currTextItemIdx = currTextListRef.current.findIndex(item => item.id === requestId);if (currTextItemIdx > -1) {const newList = currTextListRef.current.map(item =>item.id === requestId ? { ...item, text } : item);currTextListRef.current = newList;setTextList(newList);} else {if (!text) return;const newList = [...currTextListRef.current,{id: requestId,text,},];currTextListRef.current = newList;setTextList(newList);}// 翻译返回阶段,接收并展示 status=2 即已完成翻译的} else if (phase === 5 && status === 2) {let resText = '';if (text && text !== 'null') {if (isJsonString(text)) {const textArr = JSON.parse(text);const isArr = Array.isArray(textArr);// 数字的 string 类型如 111,isJsonString 判断为 json 字符串,会导致 .join 失败resText = isArr ? textArr?.join('\n') : textArr;} else {resText = text;}}if (!resText) {return;}const newList = currTextListRef.current.map(item => {return item.id === requestId ? { ...item, text: `${item.text}\n${resText}` } : item;});currTextListRef.current = newList;setTextList(newList);}} catch (error) {console.warn(error);}};
AI 音频产品开发模板及流程(二)