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:micro
recordChannel: isCardStyle || isDevOnline === false ? 2 : 1,
// 0:代表左耳、1:代表右耳
f2fChannel: type === 'left' ? 0 : 1,
// TTS 流编码方式,通过编码后将流写入到耳机设备,0:opus_silk、1:opus_celt
ttsEncode: isOpusCelt ? 1 : 0,
// 是否需要 TTS
needTts: 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:TTS
phase,
// 阶段状态,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);
}
};