您可以利用面板小程序开发构建出一个基于 Ray 框架的 AI 音频设备面板,并实现以下功能:
taskType 分别触发转写、总结、翻译,上传完成后由事件驱动拉取结构化结果。详见 面板小程序 > 搭建环境。
首先需要创建一个产品,定义产品有哪些功能点,然后再在面板中一一实现这些功能点。
注册登录 涂鸦开发者平台,并在平台创建产品:




面板小程序的开发在 小程序开发者 平台上进行操作,首先请前往 小程序开发者平台 完成平台的注册登录。
详细操作步骤,可以参考 面板小程序 > 创建面板小程序。
打开 IDE 创建一个基于 AI 耳机模板 的 AI 音频面板小程序项目,需要在 Tuya MiniApp IDE 上进行操作。
详细操作步骤,可以参考 面板小程序 > 初始化项目工程。
tttStartAudioRecording 发起,使用 audioSourceList + ttsConfigList 适配多设备音源与 TTS 输出。useAsrTransferQueue 入队展示,沟通体验更自然。 // 开始对话翻译录音(Pro:recordType=2;非 Pro:recordType=3)
const startAudioRecording = async (type: 'left' | 'right' | 'auto') => {
if (!isOnline) {
ty.showToast({ title: Strings.getLang('device_offline'), icon: 'error' });
return;
}
const audioSourceList = getAudioSourceList(customDevType, recordType);
const config: any = {
// 发起源,0:AI Note、1:AI Translate
businessType: BusinessType.Note,
// 多音源列表(替代旧版单一 audioSource/recordChannel)
audioSourceList,
saveDataWhenError: true,
// 录音类型,2:对话翻译(Pro)、3:对话翻译
recordType,
controlTimeout: 5,
dataTimeout: 10,
// 0:文件转写、1:实时转写
transferType: 1,
needTranslate: true,
// 起始/目标语言(左右耳互转)
originalLanguage: isPro ? leftLanguage : type === 'left' ? leftLanguage : rightLanguage,
targetLanguage: isPro ? rightLanguage : type === 'left' ? rightLanguage : leftLanguage,
// 智能体 ID(由 atopGetAgentInfo/store 获取)
agentId,
ttsEncode: isOpusCelt ? 1 : 0,
needTts: true,
needAsr: true,
// 是否开启自动断句识别
needAutoRecognize: autoSpeek,
...(!isPro && {
// 非 Pro:录音通道,0:蓝牙 LE、1:BT、2:Micro;f2fChannel,0:左耳、1:右耳
recordChannel: isPhone ? 2 : 1,
f2fChannel: type === 'left' ? 0 : 1,
}),
// TTS 输出配置列表(按音源生成)
ttsConfigList: getTtsConfigList(audioSourceList, recordType),
};
await tttStartAudioRecording({ deviceId, config }, true);
setActiveType(type);
setInterval(1000);
lastTimeRef.current = Date.now();
};
const handleStartRecord = async (type?: 'left' | 'right' | 'auto') => {
// 先校验权益余额
const balance = await checkBalance(homeId, deviceId);
if (balance === 0 || balance === 2) return;
ty.authorize({
scope: 'scope.record',
success: () => startAudioRecording(type),
fail: () => {
ty.showToast({ title: Strings.getLang('no_record_permisson'), icon: 'error' });
},
});
};
// 暂停/继续/停止(状态由全局录音任务事件同步)
await tttPauseRecord(deviceId);
await tttResumeRecord(deviceId);
await tttStopRecord(deviceId);
// 双通道 ASR:事件入队,由 useAsrTransferQueue 统一处理展示
// phase,0:任务、4:ASR、5:翻译、6:Skill、7:TTS
ty.wear.onRecordTransferRealTimeRecognizeStatusUpdateEvent(d => {
if (!handleTttError(d, true)) return;
pushEvent(d); // DualChannelASRList / useAsrTransferQueue
});
// 重新进入页面时可按 recordId 拉取已有实时结果
const realTimeResult = await tttGetRecordTransferRealTimeResult({ recordId: task.recordId });
resetTextList(parseRealTimerResult(realTimeResult));
RealTimeRecording,通过 needTranslate/needTts 区分能力。tttStartAudioRecording + audioSource/ttsConfig;语言变更可 tttUpdateParams 热更新。useRealTimeTextQueue 队列渲染,支持历史回填与滚动加载。 // 同声传译/实时转写共用 RealTimeRecording,配置后交给页面层 tttStartAudioRecording
const recordingConfig = useMemo(() => {
const audioSource = getAudioSource(customDevType, currRecordType, isCall);
const config: any = {
deviceId,
config: {
// 发起源,0: AI Note、1:AI Translate
businessType: BusinessType.Note,
// 音频输入源(手机/入门版/Pro 等)
audioSource,
// 录音类型,0:呼叫、1:会议(同声传译走会议)
recordType: isCall ? 0 : 1,
needAsr: true,
// 同声传译默认开启翻译
needTranslate,
needTts,
needAmplitude: false,
originalLanguage: originLanguage,
agentId,
ttsEncode: isOpusCelt ? 1 : 0,
},
};
if (needTts) {
config.config.ttsConfig = {
// 输出源为设备时传 deviceId;BT/MIC 传空
devId:
customDevType === DEVTYPES.entry ||
customDevType === DEVTYPES.phone ||
customDevType === DEVTYPES.card
? ''
: deviceId,
ttsOutput: getTtsOutput(audioSource),
ttsEncode: isOpusCelt ? 2 : customDevType === DEVTYPES.pro ? 1 : 0,
ttsOutputChannel: 0,
};
}
if (needTranslate) {
config.config.targetLanguage = translationLanguage;
}
return config;
}, [deviceId, customDevType, needTranslate, originLanguage, translationLanguage, needTts, agentId]);
// 语言变更时热更新参数(无需停录)
tttUpdateParams(recordingConfig);
// 页面层统一发起(含权益校验、权限、离线降级到手机音源)
await tttStartAudioRecording(recordingConfig);
// 暂停/继续/停止
await tttPauseRecord(deviceId);
await tttResumeRecord(deviceId);
await tttStopRecord(deviceId);
// 实时 ASR/翻译事件:只入队,展示由 useRealTimeTextQueue 处理
// phase,0:任务、4:ASR、5:翻译、6:Skill、7:TTS
ty.wear.onRecordTransferRealTimeRecognizeStatusUpdateEvent(d => {
if (d?.errorCode === 10002 || d?.errorCode === '10002') {
// 设备不支持,提示后返回
return;
}
if (!handleTttError(d, true)) return;
pushEvent(d);
});
// 已有任务时回填实时结果
const realTimeResult = await tttGetRecordTransferRealTimeResult({ recordId: task.recordId });
resetTextList(parseRealTimerResult(realTimeResult));
RecordingModes 组装配置,由 recordingMode 页统一调用 tttStartAudioRecording。needAmplitude)用于波形展示;结束后可进入转写和 AI 总结。onRecordTransferFinishEvent 处理。
// 现场录音配置(RecordingModes),由 recordingMode 页统一调用 tttStartAudioRecording
const recordingConfig = {
deviceId,
config: {
// 发起源,0: AI Note、1: AI Translate
businessType: 0,
// 音频输入源:按设备类型映射(phone、entry、proLive 等)
audioSource: getAudioSource(customDevType, currRecordType, isCall),
// 录音类型,0:呼叫、1:会议(现场录音为会议)
recordType: currRecordType,
needAsr: false,
needTranslate: false,
needTts: false,
// 现场录音需要波形振幅
needAmplitude: true,
},
};
// 开始:校验在线/权限后发起(设备离线时可降级到手机音源)
const handleStartRecord = async (recordingConfig: any) => {
if (checkAndShowOfflineDialog()) return; // 电话模式才强校验 BT;现场可降级
const needPermission =
customDevType === DEVTYPES.phone ||
customDevType === DEVTYPES.os ||
customDevType === DEVTYPES.entry;
if (needPermission) {
ty.authorize({
scope: 'scope.record',
success: () => startRecordingProcess(recordingConfig),
fail: () => {
ty.showToast({ title: Strings.getLang('no_record_permisson'), icon: 'error' });
},
});
return;
}
await startRecordingProcess(recordingConfig);
};
const startRecordingProcess = async (recordingConfig: any) => {
await tttStartAudioRecording(recordingConfig);
setInterval(1000);
lastTimeRef.current = Date.now();
};
// 暂停/继续/停止
await tttPauseRecord(deviceId);
await tttResumeRecord(deviceId);
await tttStopRecord(deviceId);
// 结束后由全局 taskState === FINISH 回首页
// 监听录音结束事件(异常码非 0 时提示)
ty.wear.onRecordTransferFinishEvent(d => {
if (d.deviceId !== deviceId) return;
if (d.code !== 0) {
handleTttError(d, true);
}
setInterval(undefined);
});
transfer/summary)。taskType 区分任务类型(0:转写、1:总结、2:翻译),未转写时触发总结会自动先走转写。onRecordTransferFileUploadEvent 推送,完成后进入转写/总结中。// 任务类型,0:转写、1:总结、2:翻译
enum GenerateType {
Transcribe,
Summarize,
Translate,
}
// 转写状态,0:未转写、1:转写中、2:成功、3:失败
// 总结状态,0:兼容老数据、1:未总结、2:总结中、3:成功、4:失败
// 1. 进入详情:按 recordId 拉取文件详情,再按状态加载结果
getFileDetail({
recordId,
amplitudeMaxCount: 100,
}).then(async res => {
// transfer === 2:转写成功,拉取转写结果
if (res?.transfer === TransferStatus.Success) {
await getTransferResult();
}
// summary === 3:总结成功,拉取总结结果
if (res?.summary === SummaryStatus.Success) {
await getSummaryResult();
}
});
// 2. 确认模板后发起转写/总结(tttTransfer)
const fetchTransferOrSummary = async () => {
// 未转写时,无论入口是总结还是转写,都先走转写任务
let taskType = generateType;
if (fileDetail?.transfer === TransferStatus.Initial) {
taskType = GenerateType.Transcribe;
}
const params: any = {
recordTransferId: fileDetail?.recordTransferId,
// 模板 ID;auto 时传空字符串
template: selectedTemplate === 'auto' ? '' : selectedTemplate,
// 转写源语言
language: fileDetail?.originalLanguage || language,
// 总结语言
summaryLang,
// 0:转写、1:总结、2:翻译
taskType,
// 是否区分说话人
enableSpeaker,
};
// 翻译任务额外传入目标语言
if (taskType === GenerateType.Translate) {
params.transLang = translateLang;
}
return tttTransfer(params);
};
// 3. 监听上传进度:上传完成后,本地标记为转写中/总结中
ty.wear.onRecordTransferFileUploadEvent(res => {
const { fileId, progress, status } = res || {};
// status:等待/上传中/完成 ...
if (status === OfflineFileUploadStatus.Completed) {
updateFileDetailLocal(prev =>
prev
? { ...prev, transfer: TransferStatus.Process, summary: SummaryStatus.Process }
: prev
);
}
});
// 4. 监听局部刷新事件:转写/总结完成后拉取结果(替代旧的本地 + 云端双拉)
// 事件字段 transferStatus/summaryStatus 对应详情字段 transfer/summary
const handlePartialRecordUpdate = (res: any) => {
const currentFile = res?.updateList?.find(
item => item?.recordId === fileDetailRef.current?.recordId
);
if (!currentFile) return;
const { transferStatus: tStatus, summaryStatus: sStatus } = currentFile;
if (tStatus === TransferStatus.Success) {
updateFileDetailLocal(prev =>
prev ? { ...prev, transfer: tStatus, cloudTranscription: true } : prev
);
getTransferResult();
}
if (sStatus === SummaryStatus.Success) {
updateFileDetailLocal(prev => (prev ? { ...prev, summary: sStatus } : prev));
getSummaryResult();
refreshFileDetail();
}
};
// 5. 获取转写结果
const fetchRecordTransferResult = async () => {
const {
transfer,
recordId,
transferType,
cloudTranscription,
recordTransferId,
isFromCloud,
duration,
} = fileDetailRef.current;
// 实时转写且尚未云端转录:走实时结果接口
if (transferType === TransferType.Realtime && !cloudTranscription && !isFromCloud) {
const realTimeResult = await tttGetRecordTransferRealTimeResult({ recordId });
return processRealtimeData(realTimeResult);
}
// 转写成功/已云端转录:读本地缓存的转写 JSON
if (transfer === TransferStatus.Success || cloudTranscription || isFromCloud) {
const cloudResult = await tttGetRecordTransferRecognizeResult({
recordTransferId,
from: 0, // 本地
});
const list = cloudResult?.text ? JSON.parse(cloudResult.text) : [];
return processCloudTransferData(list, duration);
}
return [];
};
// 6. 获取总结结果(结构化 JSON)
const fetchRecordSummaryResult = async () => {
const { summary, recordTransferId } = fileDetailRef.current;
if (summary !== SummaryStatus.Success) return;
const result = await tttGetRecordTransferSummaryResult({
recordTransferId,
from: 0, // 本地
});
if (!result?.text) return;
const data = JSON.parse(result.text);
// data.summary:总结正文
// data.title:总结标题(可回写文件名)
// data.outline:章节大纲 JSON 字符串
// data.question:预设问题 JSON 字符串
// data.imageUrl:信息图地址(summaryImageStatus === Complete 时展示)
setSummaryData(data?.summary || '');
};
{/* 7. 详情 Tab:转写/总结/思维导图 */}
{activeTab === DetailTabType.Transcribe && (
<TranscriptContent outlineList={outlineList} updateSummary={updateSummary} />
)}
{activeTab === DetailTabType.Summarize && (
<SummaryContent
summary={summaryData}
updateSummary={updateSummary}
summaryImageUrl={summaryImageUrl}
/>
)}
{activeTab === DetailTabType.MindMap && <MindMapContent summary={summaryData} />}
{/* 模板选择:推荐模板 + 总结语言 + 区分说话人 */}
<ChooseTransferTemplate
show={selectTemplateShow}
enableSpeaker={enableSpeaker}
onSpeakerChange={setEnableSpeaker}
onBottomBtnClick={handleConfirmTemplate}
/>
recordType: 0(呼叫),音频输入源使用专业版电话音源(audioSource: 21)。 // 模式入口:仅专业版设备展示电话录音
const modes = useMemo(() => {
const data = [
// ... 其他模式
{
name: Strings.getLang('mode_title_call_recording'),
key: RecordMode.phoneRecording,
icon: 'modeCallIcon',
},
];
if (deviceList.find(item => item.customDevType === 'pro_version')) {
return data;
}
return data.filter(item => item.key !== RecordMode.phoneRecording);
}, [deviceList]);
// 电话录音配置,并调用 App 能力开始录音
const recordingConfig = {
deviceId,
config: {
// 发起源,0: AI Note、1: AI Translate
businessType: 0,
// 专业版电话音源
audioSource: 21,
// 录音类型,0:呼叫、1:会议
recordType: 0,
needAsr: false,
needTranslate: false,
needTts: false,
needAmplitude: true,
},
};
// 开始前检查设备在线与 BT 连接
const checkAndShowOfflineDialog = (btStatus?: number) => {
if (!isOnline) {
// 提示连接耳机
return true;
}
if (btStatus === 0) {
// 提示蓝牙未连接
return true;
}
return false;
};
await tttStartAudioRecording(recordingConfig, true);
// 监听通话挂断事件(errorCode = 10022),自动结束录音
const handleRealTimeRecognizeStatusUpdateEvent = (d: any) => {
const { errorCode } = d || {};
if (errorCode !== 10022) return;
ty.showModal({
title: Strings.getLang('tip'),
content: Strings.getLang('phone_call_end_tip'),
showCancel: false,
confirmText: Strings.getLang('confirm'),
success: ({ confirm }) => {
if (!confirm) return;
handleStopRecord();
},
});
};
ty.wear.onRecordTransferRealTimeRecognizeStatusUpdateEvent(
handleRealTimeRecognizeStatusUpdateEvent
);

// 点击 "导入音频" 模式,唤起系统文件选择并开始导入
case RecordMode.importAudio: {
if (isImporting) {
showToast({ title: Strings.getLang('audio_importing_warn'), icon: 'none' });
break;
}
await tttImportAudio();
break;
}
// 监听导入状态事件
// status,0:未导入、1:正在导入、2:导入成功、3:导入失败、4:分享导入异常
const handleFileImportStatusEvent = (res: FileImportStatusResult) => {
if (res.status === FileImportStatus.Imported) {
// 刷新文件列表;若无失败文件则关闭导入弹窗
dispatch(updateRecordTransferResultList({ source: 'history_file_imported' }));
if (!res?.failedFiles?.length) {
setIsShowImportModal(false);
}
setImportInfo(res);
return;
}
setImportInfo(res);
};
ty.wear.onFileImportStatusEvent(handleFileImportStatusEvent);
// 取消导入/失败重试
await tttCancelImport();
await tttRetryImport();
// 查询当前导入状态(页面初始化时调用)
const status = await tttGetAudioImportStatus();

// 查询卡片设备离线文件状态,若有待传文件则启动蓝牙 LE 传输
const status = await getDeviceOfflineFileStatus(deviceId, true);
if (status?.response?.total > 0) {
await tttLoadOfflineFile({
deviceId,
sessionId: status.sessionId || 0,
// 1:蓝牙 LE、2:Wi-Fi 快传
channel: 1,
});
}
// App 级监听离线文件传输进度(建议全局注册一次,避免页面切换互相覆盖)
// status,0:未开始、1:下载中、2:结束
const handleOfflineFilesProgress = (data: DeviceOfflineFileResult) => {
const { status, errorCode, response } = data || {};
// 转发进度到首页/传输页,更新同步中卡片信息
if (status === 1) {
// 传输中
} else if (status === 2) {
// 传输结束,可继续探测其他设备或关闭提示
}
};
ty.wear.onOfflineFilesProgressEvent(handleOfflineFilesProgress);
// 切换传输通道:Wi-Fi 快传 <-> 蓝牙
await tttSwitchModeLoadOfflineFile({
channel: 2, // 2:Wi-Fi 快传、1:蓝牙 LE
deviceId,
});

// 获取云同步开关状态,并初始化同步状态
const data = await tttGetCloudSyncStatus();
// data.enabled:是否开启
// data.syncType:1:仅 Wi-Fi、其他为全部网络
// data.modifyTime:上次同步时间
// 开启/关闭云同步(Highway:m.wearable.sync.switch.save)
await toggleCloudSync({
enabled: true,
syncType: 'wifi', // 'wifi':仅 Wi-Fi;'all':全部网络
});
// 手动同步 Note 记录(上传文件 + 音频,并下载云端 Note)
await tttSyncNoteRecord();
// 监听云同步开关变更
ty.wear.onCloudSyncSwitchStatusEvent(res => {
// res.enabled / res.syncType / res.modifyTime
});
// 监听文件列表刷新(含云同步上传进度)
// syncBusinessType,0:其他业务(刷新列表)、1:上传业务(仅更新云同步卡片状态)
ty.wear.onFileListUpdateStatusEvent(res => {
if (res?.syncBusinessType === 1) {
// 更新云同步卡片状态
} else {
// 刷新录音列表
}
});
// 清理本地音频缓存(云端副本保留)
await tttRemoveFiles({ fileIds: [], isDeleteAll: false });
// 详情页按需下载云端音频
await tttDownloadNoteAudio({ fileId, recordId });