一、实验目的
本实验通过开发一个完整的实时语音翻译应用,使学生掌握端云协同语音翻译系统的设计与实现。具体目标如下:
1.1 知识目标
- 理解实时语音翻译系统的整体架构,掌握ASR、MT、TTS三大模块的协作流程。
- 掌握鸿蒙AudioCapturer API的使用方法,理解PCM音频数据格式与流式采集机制。
- 理解语音活动检测(VAD)的原理,掌握基于能量阈值的端侧VAD实现。
- 掌握云端流式ASR的WebSocket接入方法,理解中间结果与最终结果的区别。
- 掌握大模型翻译API的调用方法,理解上下文感知翻译与temperature参数的作用。
- 掌握TTS语音合成API的调用方法,理解AudioRenderer音频播放控制。
- 理解流式流水线并行化设计,掌握低延迟优化的关键策略。
1.2 能力目标
- 能够独立设计端云协同的语音翻译系统架构,合理划分端侧与云端职责。
- 能够使用鸿蒙音频API实现实时音频采集、VAD检测和音频播放。
- 能够集成云端ASR、MT、TTS服务,构建完整的语音翻译流水线。
- 能够实现流式并行处理,将端到端延迟控制在2秒以内。
- 能够设计翻译历史记录、语言对切换、统计等交互功能。
- 能够进行功能测试与延迟测量,定位并解决性能瓶颈。
1.3 素养目标
- 关注无障碍通信技术的社会价值,理解语音翻译对跨语言交流的意义。
- 培养端云协同的系统思维,在延迟、精度、成本之间做出合理权衡。
- 关注用户语音数据的隐私保护,理解端侧处理与数据脱敏的重要性。
二、实验环境与工具
2.1 开发环境

2.2 云端服务

【注意】使用云端服务前,需在阿里云DashScope平台注册账号并获取API Key。API Key的安全管理参见第14章"性能优化与安全"的相关内容。实验中请使用您自己的API Key,不要使用他人的密钥。
2.3 硬件要求

【提示】如果使用模拟器测试,需确认模拟器支持麦克风输入。部分模拟器可能无法采集音频,建议使用真机测试语音采集功能。
三、实验前准备
3.1 知识准备
完成本实验前,学生应具备以下知识:
1.掌握ArkTS语言基础语法(变量、函数、类、接口、泛型)。
2.掌握ArkUI声明式UI开发(组件、状态管理、列表、路由跳转)。
3.理解HTTP请求与WebSocket通信的基本原理(参见第5章)。
4.了解音频信号的基本概念(采样率、位深、声道、PCM格式)。
5.理解ASR、MT、TTS的基本概念和端云协同架构(参见第9章样章)。
3.2 环境准备
6.安装DevEco Studio 5.0及以上版本,配置HarmonyOS NEXT SDK。
7.在阿里云DashScope平台注册账号,开通语音识别、文本生成、语音合成服务,获取API Key。
8.准备一台支持HarmonyOS NEXT的真机或配置好模拟器。
9.确保开发机与测试设备网络连通,可访问dashscope.aliyuncs.com。
3.3 实验前思考题
请学生在实验前完成以下思考题,并在实验报告中作答:
- 思考题1:在实时语音翻译场景中,为什么选择16kHz采样率而不是44.1kHz?请从奈奎斯特采样定理和数据量两个角度分析。
- 思考题2:端侧VAD(语音活动检测)在实时语音翻译中起什么作用?基于能量阈值的VAD有什么局限性?
- 思考题3:翻译流水线中ASR、MT、TTS三个阶段如何实现并行化?为什么流式ASR是实时翻译的必要条件?
四、实验任务
本实验分为6个任务,按顺序完成。

任务1:项目创建与结构搭建
任务目标
创建鸿蒙项目,搭建项目目录结构,配置所需权限,定义核心类型。
步骤1.1:创建项目
1.打开DevEco Studio,选择Create Project。
2.选择Application -> Empty Ability模板。
3.项目名称填写SpeechTranslator,Bundle Name填写com.example.speechtranslator。
4.Compile SDK选择API 12,语言选择ArkTS。
5.点击Finish创建项目。
步骤1.2:创建目录结构
在entry/src/main/ets目录下创建以下子目录:
entry/src/main/ets/
├── model/ # 类型定义
├── utils/ # 工具模块(音频采集、VAD、播放)
├── services/ # 服务模块(ASR、MT、TTS、流水线、历史)
├── pages/ # 页面
└── entryability/ # 入口Ability
步骤1.3:配置权限
在entry/src/main/resources/module.json5中声明所需权限:
{
"module": {
"name": "entry",
"type": "entry",
"abilities": [{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "语音翻译应用入口",
"icon": "$media:app_icon",
"label": "语音翻译",
"startWindowIcon": "$media:app_icon",
"startWindowBackground": "$color:start_window_background"
}],
"requestPermissions": [
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:reason_microphone",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.INTERNET",
"reason": "$string:reason_internet",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
在entry/src/main/resources/base/element/string.json中添加权限说明:
{
"string": [
{ "name": "reason_microphone", "value": "需要使用麦克风采集您的语音进行翻译" },
{ "name": "reason_internet", "value": "需要网络连接访问云端语音翻译服务" }
]
}
步骤1.4:定义核心类型
在model/Types.ets中定义核心类型:
arkts
// model/Types.ets
export enum Language {
ZH = 'zh',
EN = 'en'
}
export interface LanguagePair {
source: Language;
target: Language;
}
export interface ASRResult {
text: string;
isFinal: boolean;
sentenceId: number;
}
export interface TranslationRecord {
id: string;
sourceText: string;
translatedText: string;
sourceLang: Language;
targetLang: Language;
timestamp: number;
duration: number;
}
export enum TranslationState {
IDLE = 'idle',
LISTENING = 'listening',
RECOGNIZING = 'recognizing',
TRANSLATING = 'translating',
SPEAKING = 'speaking',
ERROR = 'error'
}
export type VADStateChange = 'start' | 'end' | 'none';
export interface VADResult {
isSpeech: boolean;
stateChange: VADStateChange;
rms: number;
}
步骤1.5:配置路由
在entry/src/main/resources/base/profile/main_pages.json中配置页面路由:
arkts
{
"src": [
"pages/IndexPage",
"pages/TranslatePage",
"pages/HistoryPage"
]
}
验收标准
- 项目能成功编译,无报错。
- 目录结构完整,包含model、utils、services、pages、entryability五个子目录。
- module.json5中已声明MICROPHONE和INTERNET权限。
- Types.ets中已定义所有核心类型。
任务2:音频采集与VAD模块
任务目标
实现音频采集管理器和语音活动检测模块,能够实时采集麦克风PCM音频数据,并检测语音活动的起止。
步骤2.1:实现语音活动检测(utils/SimpleVAD.ets)
创建SimpleVAD类,基于RMS能量阈值检测语音活动:
arkts
// utils/SimpleVAD.ets
import { VADResult, VADStateChange } from '../model/Types';
export class SimpleVAD {
private energyThreshold: number = 300;
private silenceFrames: number = 0;
private maxSilenceFrames: number = 30; // 约3秒静音判定结束
private isSpeaking: boolean = false;
// 计算PCM数据的RMS能量
calculateRMS(buffer: ArrayBuffer): number {
const view = new Int16Array(buffer);
let sum = 0;
for (let i = 0; i < view.length; i++) {
sum += view[i] * view[i];
}
return Math.sqrt(sum / view.length);
}
// 检测当前帧是否包含语音
detect(buffer: ArrayBuffer): VADResult {
const rms = this.calculateRMS(buffer);
const isSpeech = rms > this.energyThreshold;
let stateChange: VADStateChange = 'none';
if (isSpeech && !this.isSpeaking) {
this.isSpeaking = true;
this.silenceFrames = 0;
stateChange = 'start';
} else if (!isSpeech && this.isSpeaking) {
this.silenceFrames++;
if (this.silenceFrames >= this.maxSilenceFrames) {
this.isSpeaking = false;
stateChange = 'end';
}
}
return { isSpeech, stateChange, rms };
}
setThreshold(threshold: number): void {
this.energyThreshold = threshold;
}
getIsSpeaking(): boolean {
return this.isSpeaking;
}
reset(): void {
this.silenceFrames = 0;
this.isSpeaking = false;
}
}
【提示】能量阈值默认300适用于安静环境。如果测试环境噪声较大,可适当提高阈值(如500-800)。可在应用启动时录制1秒环境噪声,自动计算阈值 = 噪声RMS × 3。
步骤2.2:实现音频采集管理器(utils/AudioCaptureManager.ets)
创建AudioCaptureManager类,使用AudioCapturer API实现流式音频采集:
arkts
// utils/AudioCaptureManager.ets
import { audio } from '@kit.AudioKit';
import { SimpleVAD } from './SimpleVAD';
import { VADResult } from '../model/Types';
export class AudioCaptureManager {
private capturer: audio.AudioCapturer | null = null;
private vad: SimpleVAD = new SimpleVAD();
private onDataCallback: ((buffer: ArrayBuffer, vadResult: VADResult) => void) | null = null;
private isCapturing: boolean = false;
async start(onData: (buffer: ArrayBuffer, vadResult: VADResult) => void): Promise<void> {
if (this.isCapturing) return;
this.onDataCallback = onData;
const audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleSize: audio.AudioSampleSize.SAMPLE_SIZE_16,
encodingType: audio.AudioEncodingType.ENCODING_PCM
};
const capturerInfo: audio.AudioCapturerInfo = {
sourceType: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0
};
const options: audio.AudioCapturerOptions = {
streamInfo: audioStreamInfo,
capturerInfo: capturerInfo
};
this.capturer = await audio.createAudioCapturer(options);
// 注册音频数据回调
this.capturer.on('readData', (buffer: ArrayBuffer) => {
const vadResult = this.vad.detect(buffer);
if (this.onDataCallback) {
this.onDataCallback(buffer, vadResult);
}
});
await this.capturer.start();
this.isCapturing = true;
}
async stop(): Promise<void> {
if (!this.isCapturing || !this.capturer) return;
await this.capturer.stop();
await this.capturer.release();
this.capturer = null;
this.isCapturing = false;
}
getIsCapturing(): boolean {
return this.isCapturing;
}
setVADThreshold(threshold: number): void {
this.vad.setThreshold(threshold);
}
}
步骤2.3:实现音频播放器(utils/AudioPlayer.ets)
创建AudioPlayer类,使用AudioRenderer API播放TTS合成的音频:
arkts
// utils/AudioPlayer.ets
import { audio } from '@kit.AudioKit';
export class AudioPlayer {
private renderer: audio.AudioRenderer | null = null;
private isPlaying: boolean = false;
async play(audioData: ArrayBuffer): Promise<void> {
if (this.isPlaying) await this.stop();
const audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleSize: audio.AudioSampleSize.SAMPLE_SIZE_16,
encodingType: audio.AudioEncodingType.ENCODING_PCM
};
const rendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
rendererFlags: 0
};
const options: audio.AudioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: rendererInfo
};
this.renderer = await audio.createAudioRenderer(options);
await this.renderer.start();
this.isPlaying = true;
await this.renderer.writeBuffer(audioData);
await this.waitForFinish();
await this.renderer.stop();
await this.renderer.release();
this.renderer = null;
this.isPlaying = false;
}
async stop(): Promise<void> {
if (this.renderer && this.isPlaying) {
await this.renderer.stop();
await this.renderer.release();
this.renderer = null;
this.isPlaying = false;
}
}
getIsPlaying(): boolean {
return this.isPlaying;
}
private waitForFinish(): Promise<void> {
return new Promise(resolve => {
const check = setInterval(() => {
if (this.renderer) {
const state = this.renderer.state;
if (state === audio.AudioState.STATE_STOPPED) {
clearInterval(check);
resolve();
}
}
}, 100);
});
}
}
步骤2.4:测试音频采集
在TranslatePage中临时添加测试代码,验证音频采集和VAD是否正常工作:
arkts
// 临时测试代码(后续任务中会替换)
private captureManager: AudioCaptureManager = new AudioCaptureManager();
async testAudioCapture(): Promise<void> {
await this.captureManager.start((buffer: ArrayBuffer, vadResult: VADResult) => {
console.info(`RMS: ${vadResult.rms.toFixed(0)}, ` +
`isSpeech: ${vadResult.isSpeech}, ` +
`stateChange: ${vadResult.stateChange}`);
});
}
async stopTest(): Promise<void> {
await this.captureManager.stop();
}
运行应用,点击测试按钮,对着麦克风说话,观察控制台日志:
- 说话时RMS值应明显升高(>300),isSpeech为true,stateChange为start。
- 停止说话后约3秒,stateChange应为end。
- 静默时RMS值应低于阈值,isSpeech为false。
验收标准
- SimpleVAD能正确计算RMS能量,检测语音起止。
- AudioCaptureManager能成功启动音频采集,回调中收到PCM数据。
- AudioPlayer能播放音频数据。
- 控制台日志显示VAD检测结果合理。
任务3:云端流式ASR客户端
任务目标
实现云端流式ASR客户端,通过WebSocket连接云端ASR服务,实时发送音频帧并接收识别结果。
步骤3.1:创建ASR客户端(services/ASRClient.ets)
使用NetworkKit的webSocket模块建立与云端ASR服务的WebSocket长连接:
arkts
// services/ASRClient.ets
import { webSocket } from '@kit.NetworkKit';
import { ASRResult, Language } from '../model/Types';
const ASR_CONFIG = {
url: 'wss://dashscope.aliyuncs.com/api-ws/v1/inference/',
model: 'paraformer-realtime-v2',
apiKey: 'your_api_key' // 替换为您的API Key
};
export class ASRClient {
private ws: webSocket.WebSocket | null = null;
private onResultCallback: ((result: ASRResult) => void) | null = null;
private isConnected: boolean = false;
async connect(lang: Language, onResult: (result: ASRResult) => void): Promise<void> {
if (this.isConnected) await this.disconnect();
this.onResultCallback = onResult;
this.ws = webSocket.createWebSocket();
await this.ws.connect(ASR_CONFIG.url, {
header: {
'Authorization': `Bearer ${ASR_CONFIG.apiKey}`,
'X-DashScope-DataInspection': 'enable'
}
});
// 发送开始任务指令
const startTask = {
action: 'start-task',
task: {
model: ASR_CONFIG.model,
task_id: `task_${Date.now()}`,
input: {
format: 'pcm',
sample_rate: 16000,
language_hints: lang === Language.ZH ? ['zh', 'en'] : ['en', 'zh']
}
}
};
this.ws.send(JSON.stringify(startTask));
// 注册消息回调
this.ws.on('message', (err, data) => {
if (err || !data) return;
const result = this.parseResult(data.toString());
if (result && this.onResultCallback) {
this.onResultCallback(result);
}
});
this.isConnected = true;
}
// 发送音频帧
sendAudio(buffer: ArrayBuffer): void {
if (this.ws && this.isConnected) {
this.ws.send(buffer);
}
}
// 结束识别
async finish(): Promise<void> {
if (this.ws && this.isConnected) {
this.ws.send(JSON.stringify({ action: 'finish-task' }));
}
}
async disconnect(): Promise<void> {
if (this.ws) {
await this.ws.close();
this.ws = null;
}
this.isConnected = false;
}
getIsConnected(): boolean {
return this.isConnected;
}
private parseResult(data: string): ASRResult | null {
try {
const obj = JSON.parse(data);
const sentence = obj.output?.sentence;
if (!sentence) return null;
return {
text: sentence.text || '',
isFinal: sentence.end_time !== undefined && sentence.end_time > 0,
sentenceId: sentence.sentence_id || 0
};
} catch {
return null;
}
}
}
【注意】代码中的apiKey为占位符,请替换为您在阿里云DashScope平台获取的真实API Key。切勿将API Key提交到版本控制系统,密钥管理参见第14章。
步骤3.2:测试ASR连接
在TranslatePage中临时添加测试代码,验证ASR是否能正确识别语音:
arkts
private asrClient: ASRClient = new ASRClient();
private captureManager: AudioCaptureManager = new AudioCaptureManager();
async testASR(): Promise<void> {
// 1. 连接ASR
await this.asrClient.connect(Language.ZH, (result: ASRResult) => {
console.info(`ASR: text=${result.text}, isFinal=${result.isFinal}`);
});
// 2. 启动音频采集,将音频发送给ASR
await this.captureManager.start((buffer: ArrayBuffer, vadResult: VADResult) => {
this.asrClient.sendAudio(buffer);
});
}
async stopTest(): Promise<void> {
await this.captureManager.stop();
await this.asrClient.finish();
await this.asrClient.disconnect();
}
运行测试,对着麦克风说"你好世界",观察控制台日志:
- 应看到中间识别结果(isFinal=false),文本逐步增长。
- 说完后应看到最终结果(isFinal=true),文本为"你好世界"。
验收标准
- ASRClient能成功连接云端ASR服务。
- 发送音频帧后能收到中间识别结果和最终识别结果。
- 中文语音识别结果基本准确。
任务4:机器翻译与TTS服务
任务目标
实现机器翻译服务和TTS语音合成服务,将ASR识别的文本翻译为目标语言,并合成为语音。
步骤4.1:实现机器翻译服务(services/TranslationService.ets)
使用大模型API实现上下文感知翻译:
arkts
// services/TranslationService.ets
import { Language, LanguagePair } from '../model/Types';
const MT_CONFIG = {
url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
model: 'qwen-plus',
apiKey: 'your_api_key'
};
export class TranslationService {
private contextWindow: string[] = [];
private maxContextSize: number = 5;
async translate(text: string, langPair: LanguagePair): Promise<string> {
const sourceLangName = langPair.source === Language.ZH ? 'Chinese' : 'English';
const targetLangName = langPair.target === Language.ZH ? 'Chinese' : 'English';
const contextStr = this.contextWindow.length > 0
? 'Previous conversation:\n' + this.contextWindow.join('\n') + '\n'
: '';
const prompt = `${contextStr}` +
`Translate the following ${sourceLangName} text to ${targetLangName}. ` +
`This is a spoken conversation, keep it natural and colloquial. ` +
`Only output the translation, no explanation.\n\nText: ${text}`;
const response = await fetch(MT_CONFIG.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${MT_CONFIG.apiKey}`
},
body: JSON.stringify({
model: MT_CONFIG.model,
input: { prompt: prompt },
parameters: { temperature: 0.1, result_format: 'text' }
})
});
const data = await response.json();
const translated = data.output?.text?.trim() || '';
this.addToContext(text, translated);
return translated;
}
private addToContext(source: string, translation: string): void {
this.contextWindow.push(`[${source} -> ${translation}]`);
if (this.contextWindow.length > this.maxContextSize) {
this.contextWindow.shift();
}
}
getContext(): string[] {
return [...this.contextWindow];
}
clearContext(): void {
this.contextWindow = [];
}
}
步骤4.2:实现TTS语音合成服务(services/TTSService.ets)
调用云端TTS API将翻译文本合成为语音:
arkts
// services/TTSService.ets
import { Language } from '../model/Types';
const TTS_CONFIG = {
url: 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts',
model: 'cosyvoice-v1',
apiKey: 'your_api_key'
};
export class TTSService {
async synthesize(text: string, lang: Language): Promise<ArrayBuffer> {
const voice = lang === Language.EN ? 'cosyvoice-en-v1' : 'cosyvoice-zh-v1';
const response = await fetch(TTS_CONFIG.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TTS_CONFIG.apiKey}`,
'X-DashScope-DataInspection': 'enable'
},
body: JSON.stringify({
model: TTS_CONFIG.model,
input: { text: text, voice: voice },
parameters: {
format: 'mp3',
sample_rate: 16000,
speed: 1.0,
volume: 50,
pitch: 50
}
})
});
const blob = await response.blob();
return await blob.arrayBuffer();
}
}
步骤4.3:测试翻译与TTS
在TranslatePage中临时添加测试代码:
arkts
private translationService: TranslationService = new TranslationService();
private ttsService: TTSService = new TTSService();
private audioPlayer: AudioPlayer = new AudioPlayer();
async testMTandTTS(): Promise<void> {
// 测试翻译
const translated = await this.translationService.translate(
'你好世界',
{ source: Language.ZH, target: Language.EN }
);
console.info(`翻译结果: ${translated}`);
// 测试TTS
const audio = await this.ttsService.synthesize(translated, Language.EN);
await this.audioPlayer.play(audio);
console.info('TTS播放完成');
}
运行测试,验证:
- "你好世界"翻译为"Hello world"或类似英文。
- 能听到英文朗读。
验收标准
- TranslationService能正确翻译中英文文本。
- TTSService能合成语音并返回音频数据。
- AudioPlayer能播放合成的语音。
- 翻译结果自然流畅,TTS语音清晰。
任务5:翻译流水线管理器
任务目标
实现翻译流水线管理器,协调音频采集、VAD、ASR、MT、TTS和音频播放各模块,实现流式并行处理。
步骤5.1:实现历史记录存储(services/HistoryStore.ets)
先实现历史记录存储模块,用于保存翻译记录:
arkts
// services/HistoryStore.ets
import { TranslationRecord, Language } from '../model/Types';
import { preferences } from '@kit.ArkData';
const STORE_NAME = 'translation_history';
const KEY_RECORDS = 'records';
export class HistoryStore {
private records: TranslationRecord[] = [];
private prefs: preferences.Preferences | null = null;
async init(context: Context): Promise<void> {
this.prefs = await preferences.getPreferences(context, STORE_NAME);
const json = await this.prefs.get(KEY_RECORDS, '[]');
this.records = JSON.parse(json as string);
}
addRecord(record: TranslationRecord): void {
this.records.unshift(record);
if (this.records.length > 200) {
this.records = this.records.slice(0, 200);
}
this.save();
}
deleteRecord(id: string): void {
this.records = this.records.filter(r => r.id !== id);
this.save();
}
clearAll(): void {
this.records = [];
this.save();
}
getAllRecords(): TranslationRecord[] {
return [...this.records];
}
getRecentRecords(count: number): TranslationRecord[] {
return this.records.slice(0, count);
}
searchRecords(keyword: string): TranslationRecord[] {
const lower = keyword.toLowerCase();
return this.records.filter(r =>
r.sourceText.toLowerCase().includes(lower) ||
r.translatedText.toLowerCase().includes(lower)
);
}
getStatistics(): { total: number; zhToEn: number; enToZh: number; avgDuration: number } {
const total = this.records.length;
const zhToEn = this.records.filter(r => r.sourceLang === Language.ZH).length;
const enToZh = this.records.filter(r => r.sourceLang === Language.EN).length;
const avgDuration = total > 0
? Math.round(this.records.reduce((sum, r) => sum + r.duration, 0) / total)
: 0;
return { total, zhToEn, enToZh, avgDuration };
}
private async save(): Promise<void> {
if (this.prefs) {
await this.prefs.put(KEY_RECORDS, JSON.stringify(this.records));
await this.prefs.flush();
}
}
}
步骤5.2:实现翻译流水线管理器(services/TranslationPipeline.ets)
TranslationPipeline是整个应用的核心,协调所有模块实现流式并行处理:
arkts
// services/TranslationPipeline.ets
import { AudioCaptureManager } from '../utils/AudioCaptureManager';
import { AudioPlayer } from '../utils/AudioPlayer';
import { ASRClient } from './ASRClient';
import { TranslationService } from './TranslationService';
import { TTSService } from './TTSService';
import { HistoryStore } from './HistoryStore';
import { ASRResult, Language, LanguagePair, TranslationState,
TranslationRecord, VADResult } from '../model/Types';
export class TranslationPipeline {
private captureManager: AudioCaptureManager = new AudioCaptureManager();
private audioPlayer: AudioPlayer = new AudioPlayer();
private asrClient: ASRClient = new ASRClient();
private translationService: TranslationService = new TranslationService();
private ttsService: TTSService = new TTSService();
private historyStore: HistoryStore = new HistoryStore();
private langPair: LanguagePair = { source: Language.ZH, target: Language.EN };
private state: TranslationState = TranslationState.IDLE;
private onASRTextCallback: ((text: string, isFinal: boolean) => void) | null = null;
private onTranslationCallback: ((text: string) => void) | null = null;
private onStateChangeCallback: ((state: TranslationState) => void) | null = null;
private onVADStateChangeCallback: ((speaking: boolean) => void) | null = null;
private sentenceStartTime: number = 0;
setLanguagePair(pair: LanguagePair): void {
this.langPair = pair;
this.translationService.clearContext();
}
getLanguagePair(): LanguagePair {
return this.langPair;
}
getState(): TranslationState {
return this.state;
}
getHistoryStore(): HistoryStore {
return this.historyStore;
}
setCallbacks(callbacks: {
onASRText?: (text: string, isFinal: boolean) => void;
onTranslation?: (text: string) => void;
onStateChange?: (state: TranslationState) => void;
onVADStateChange?: (speaking: boolean) => void;
}): void {
this.onASRTextCallback = callbacks.onASRText || null;
this.onTranslationCallback = callbacks.onTranslation || null;
this.onStateChangeCallback = callbacks.onStateChange || null;
this.onVADStateChangeCallback = callbacks.onVADStateChange || null;
}
async start(): Promise<void> {
if (this.state !== TranslationState.IDLE) return;
this.setState(TranslationState.LISTENING);
this.sentenceStartTime = Date.now();
await this.asrClient.connect(this.langPair.source, (result: ASRResult) => {
this.handleASRResult(result);
});
await this.captureManager.start((buffer: ArrayBuffer, vadResult: VADResult) => {
this.handleAudioData(buffer, vadResult);
});
}
async stop(): Promise<void> {
this.setState(TranslationState.IDLE);
await this.captureManager.stop();
await this.asrClient.finish();
await this.asrClient.disconnect();
await this.audioPlayer.stop();
}
private handleAudioData(buffer: ArrayBuffer, vadResult: VADResult): void {
this.asrClient.sendAudio(buffer);
if (vadResult.stateChange === 'start') {
this.setState(TranslationState.RECOGNIZING);
this.onVADStateChangeCallback?.(true);
} else if (vadResult.stateChange === 'end') {
this.onVADStateChangeCallback?.(false);
}
}
private handleASRResult(result: ASRResult): void {
this.onASRTextCallback?.(result.text, result.isFinal);
if (result.isFinal) {
this.processSentence(result.text);
}
}
private async processSentence(text: string): Promise<void> {
if (!text.trim()) return;
this.setState(TranslationState.TRANSLATING);
try {
const translated = await this.translationService.translate(text, this.langPair);
this.onTranslationCallback?.(translated);
const record: TranslationRecord = {
id: `rec_${Date.now()}`,
sourceText: text,
translatedText: translated,
sourceLang: this.langPair.source,
targetLang: this.langPair.target,
timestamp: Date.now(),
duration: Date.now() - this.sentenceStartTime
};
this.historyStore.addRecord(record);
this.setState(TranslationState.SPEAKING);
const audio = await this.ttsService.synthesize(translated, this.langPair.target);
await this.audioPlayer.play(audio);
} catch (error) {
console.error('Translation error:', error);
this.setState(TranslationState.ERROR);
}
if (this.captureManager.getIsCapturing()) {
this.setState(TranslationState.LISTENING);
} else {
this.setState(TranslationState.IDLE);
}
}
private setState(state: TranslationState): void {
this.state = state;
this.onStateChangeCallback?.(state);
}
}
【提示】processSentence方法是异步调用的,不会阻塞handleASRResult回调。这意味着ASR可以继续处理后续音频帧,同时前一句的翻译和TTS在后台执行,实现流水线并行化。
验收标准
- TranslationPipeline能协调所有模块正常工作。
- 说一句话后,能自动完成识别、翻译、合成、播放全流程。
- 状态指示正确反映当前处理阶段。
- 翻译记录能正确保存到HistoryStore。
任务6:UI页面与集成测试
任务目标
实现首页、翻译主页面和历史记录页面,完成端到端集成测试,验证整个应用功能正常。
步骤6.1:实现首页(pages/IndexPage.ets)
首页展示应用标题、语言对选择卡片和功能入口:
arkts
// pages/IndexPage.ets
import { Language, LanguagePair } from '../model/Types';
import { router } from '@kit.ArkUI';
@Entry
@Component
struct IndexPage {
@State langPair: LanguagePair = { source: Language.ZH, target: Language.EN };
build() {
Column() {
Row() {
Text('实时语音翻译')
.fontSize(24).fontWeight(FontWeight.Bold)
.fontColor('#1A3C6E')
Blank()
Text('鸿蒙AI应用开发')
.fontSize(12).fontColor('#999')
}
.width('100%').height(56).padding({ left: 20, right: 20 })
.backgroundColor('#F5F7FA')
Column() {
Text('选择翻译方向')
.fontSize(16).fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Row() {
this.langCard('中文', 'English', Language.ZH, Language.EN)
Image($r('app.media.ic_swap'))
.width(32).height(32).margin({ left: 12, right: 12 })
.onClick(() => {
const tmp = this.langPair.source;
this.langPair.source = this.langPair.target;
this.langPair.target = tmp;
})
this.langCard('English', '中文', Language.EN, Language.ZH)
}
.justifyContent(FlexAlign.Center)
}
.width('90%').padding(24)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ top: 20 })
Column() {
this.featureCard('开始翻译', '点击开始实时语音翻译', () => {
AppStorage.setOrCreate('langPair', JSON.stringify(this.langPair));
router.pushUrl({ url: 'pages/TranslatePage' });
})
this.featureCard('翻译历史', '查看历史翻译记录', () => {
router.pushUrl({ url: 'pages/HistoryPage' });
})
}
.width('90%').margin({ top: 20 })
Blank()
Text('请确保网络连接正常,并已授予麦克风权限')
.fontSize(12).fontColor('#999').margin({ bottom: 20 })
}
.width('100%').height('100%')
.backgroundColor('#F5F7FA')
}
@Builder
langCard(from: string, to: string, src: Language, tgt: Language) {
Column() {
Text(from).fontSize(18).fontWeight(FontWeight.Bold)
Image($r('app.media.ic_arrow_down')).width(20).height(20)
.margin({ top: 4, bottom: 4 })
Text(to).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#0070C0')
}
.width(100).padding(16)
.backgroundColor(this.langPair.source === src ? '#E8F4FD' : '#F5F5F5')
.borderRadius(12)
.onClick(() => { this.langPair = { source: src, target: tgt }; })
}
@Builder
featureCard(title: string, desc: string, action: () => void) {
Row() {
Image($r('app.media.ic_translate')).width(40).height(40)
Column() {
Text(title).fontSize(16).fontWeight(FontWeight.Bold)
Text(desc).fontSize(12).fontColor('#999').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).margin({ left: 12 })
Blank()
Image($r('app.media.ic_arrow_right')).width(20).height(20)
}
.width('100%').padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ bottom: 12 })
.onClick(() => action())
}
}
【注意】上述代码中使用了app.media.ic_swap、ic_arrow_down、ic_arrow_right、ic_translate等图片资源。请在entry/src/main/resources/base/media目录下放置对应的SVG或PNG图片,或使用系统图标替代。
步骤6.2:实现翻译主页面(pages/TranslatePage.ets)
翻译主页面是核心交互页面,显示识别文本、翻译文本、状态指示和控制按钮:
arkts
// pages/TranslatePage.ets
import { TranslationPipeline } from '../services/TranslationPipeline';
import { HistoryStore } from '../services/HistoryStore';
import { Language, LanguagePair, TranslationState } from '../model/Types';
import { router, pasteboard, promptAction } from '@kit.ArkUI';
@Entry
@Component
struct TranslatePage {
private pipeline: TranslationPipeline = new TranslationPipeline();
@State langPair: LanguagePair = { source: Language.ZH, target: Language.EN };
@State state: TranslationState = TranslationState.IDLE;
@State asrText: string = '';
@State translatedText: string = '';
@State partialText: string = '';
aboutToAppear(): void {
const langStr = AppStorage.get<string>('langPair');
if (langStr) this.langPair = JSON.parse(langStr);
this.pipeline.getHistoryStore().getContext(this);
this.pipeline.setCallbacks({
onASRText: (text: string, isFinal: boolean) => {
if (isFinal) {
this.asrText += text;
this.partialText = '';
} else {
this.partialText = text;
}
},
onTranslation: (text: string) => { this.translatedText = text; },
onStateChange: (state: TranslationState) => { this.state = state; },
onVADStateChange: (speaking: boolean) => {}
});
this.pipeline.setLanguagePair(this.langPair);
}
aboutToDisappear(): void { this.pipeline.stop(); }
build() {
Column() {
Row() {
Image($r('app.media.ic_back')).width(24).height(24)
.onClick(() => router.back())
Text(this.langPair.source === Language.ZH ? '中译英' : '英译中')
.fontSize(18).fontWeight(FontWeight.Bold).margin({ left: 12 })
Blank()
Text('切换').fontSize(14).fontColor('#0070C0')
.onClick(() => {
const tmp = this.langPair.source;
this.langPair.source = this.langPair.target;
this.langPair.target = tmp;
this.pipeline.setLanguagePair(this.langPair);
this.asrText = '';
this.translatedText = '';
})
}
.width('100%').height(56).padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
Row() {
Circle({ width: 10, height: 10 })
.fill(this.getStateColor()).margin({ right: 8 })
Text(this.getStateText()).fontSize(14).fontColor('#666')
}
.width('100%').padding({ left: 20, top: 12, bottom: 12 })
Column() {
Text('识别文本').fontSize(12).fontColor('#999')
Text(this.asrText + this.partialText)
.fontSize(18).margin({ top: 8 }).width('100%')
if (this.partialText) {
Text('...').fontSize(14).fontColor('#0070C0')
}
}
.width('90%').padding(16)
.backgroundColor('#F8FBFF')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
Column() {
Row() {
Text('翻译结果').fontSize(12).fontColor('#999')
Blank()
if (this.translatedText) {
Text('复制').fontSize(12).fontColor('#0070C0')
.onClick(() => this.copyText(this.translatedText))
}
}.width('100%')
Text(this.translatedText || '等待翻译...')
.fontSize(20).fontWeight(FontWeight.Medium).margin({ top: 8 })
.width('100%')
}
.width('90%').padding(16).margin({ top: 12 })
.backgroundColor('#E8F4FD')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
Blank()
Row() {
Button(this.state === TranslationState.IDLE ? '开始翻译' : '停止')
.width(200).height(56)
.fontSize(18).fontColor(Color.White)
.backgroundColor(this.state === TranslationState.IDLE ? '#0070C0' : '#CC0000')
.borderRadius(28)
.onClick(() => {
if (this.state === TranslationState.IDLE) {
this.pipeline.start();
} else {
this.pipeline.stop();
}
})
}
.width('100%').justifyContent(FlexAlign.Center)
.margin({ bottom: 40 })
}
.width('100%').height('100%')
.backgroundColor('#F5F7FA')
}
getStateColor(): string {
switch (this.state) {
case TranslationState.IDLE: return '#CCCCCC';
case TranslationState.LISTENING: return '#00C853';
case TranslationState.RECOGNIZING: return '#FFB300';
case TranslationState.TRANSLATING: return '#FF6F00';
case TranslationState.SPEAKING: return '#6200EA';
case TranslationState.ERROR: return '#CC0000';
default: return '#CCCCCC';
}
}
getStateText(): string {
switch (this.state) {
case TranslationState.IDLE: return '空闲';
case TranslationState.LISTENING: return '正在聆听...';
case TranslationState.RECOGNIZING: return '正在识别...';
case TranslationState.TRANSLATING: return '正在翻译...';
case TranslationState.SPEAKING: return '正在朗读...';
case TranslationState.ERROR: return '出错了';
default: return '';
}
}
copyText(text: string): void {
const clipboard = pasteboard.getSystemPasteboard();
clipboard.setData(text);
promptAction.showToast({ message: '已复制' });
}
}
步骤6.3:实现历史记录页面(pages/HistoryPage.ets)
历史记录页面展示翻译记录列表、搜索和统计功能:
arkts
// pages/HistoryPage.ets
import { TranslationRecord, Language } from '../model/Types';
import { HistoryStore } from '../services/HistoryStore';
import { router } from '@kit.ArkUI';
@Entry
@Component
struct HistoryPage {
@State records: TranslationRecord[] = [];
@State searchKeyword: string = '';
@State showStats: boolean = false;
private historyStore: HistoryStore = new HistoryStore();
aboutToAppear(): void {
this.historyStore.getContext(this);
setTimeout(() => {
this.records = this.historyStore.getAllRecords();
}, 100);
}
build() {
Column() {
Row() {
Image($r('app.media.ic_back')).width(24).height(24)
.onClick(() => router.back())
Text('翻译历史').fontSize(18).fontWeight(FontWeight.Bold).margin({ left: 12 })
Blank()
Text('统计').fontSize(14).fontColor('#0070C0')
.onClick(() => { this.showStats = !this.showStats; })
}
.width('100%').height(56).padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
if (this.showStats) { this.statsPanel() }
Row() {
TextInput({ placeholder: '搜索翻译记录', text: this.searchKeyword })
.width('80%').height(40)
.onChange((value: string) => {
this.searchKeyword = value;
this.records = value
? this.historyStore.searchRecords(value)
: this.historyStore.getAllRecords();
})
if (this.searchKeyword) {
Text('清除').fontSize(14).fontColor('#999').margin({ left: 8 })
.onClick(() => {
this.searchKeyword = '';
this.records = this.historyStore.getAllRecords();
})
}
}
.width('90%').margin({ top: 12 })
List() {
ForEach(this.records, (record: TranslationRecord) => {
ListItem() { this.recordCard(record) }
.margin({ bottom: 8 })
})
}
.width('90%').layoutWeight(1)
.margin({ top: 12 })
if (this.records.length > 0) {
Button('清空全部记录')
.width('90%').height(44)
.fontSize(14).fontColor('#CC0000')
.backgroundColor(Color.White)
.borderRadius(8).margin({ bottom: 20 })
.onClick(() => {
this.historyStore.clearAll();
this.records = [];
})
}
}
.width('100%').height('100%')
.backgroundColor('#F5F7FA')
}
@Builder
statsPanel() {
Row() {
Column() {
Text(this.records.length.toString()).fontSize(24).fontWeight(FontWeight.Bold)
Text('总记录').fontSize(12).fontColor('#999')
}.layoutWeight(1)
Column() {
Text(this.records.filter(r => r.sourceLang === Language.ZH).length.toString())
.fontSize(24).fontWeight(FontWeight.Bold).fontColor('#0070C0')
Text('中译英').fontSize(12).fontColor('#999')
}.layoutWeight(1)
Column() {
Text(this.records.filter(r => r.sourceLang === Language.EN).length.toString())
.fontSize(24).fontWeight(FontWeight.Bold).fontColor('#00C853')
Text('英译中').fontSize(12).fontColor('#999')
}.layoutWeight(1)
}
.width('90%').padding(16).margin({ top: 12 })
.backgroundColor(Color.White).borderRadius(12)
}
@Builder
recordCard(record: TranslationRecord) {
Column() {
Row() {
Text(record.sourceLang === Language.ZH ? '中' : 'EN')
.fontSize(10).fontColor('#999')
.width(24).height(24).textAlign(TextAlign.Center)
.backgroundColor('#F0F0F0').borderRadius(12)
Text(record.sourceText).fontSize(14).margin({ left: 8 }).layoutWeight(1)
}.width('100%')
Row() {
Text(record.targetLang === Language.ZH ? '中' : 'EN')
.fontSize(10).fontColor('#0070C0')
.width(24).height(24).textAlign(TextAlign.Center)
.backgroundColor('#E8F4FD').borderRadius(12)
Text(record.translatedText).fontSize(14).fontWeight(FontWeight.Medium)
.fontColor('#333').margin({ left: 8 }).layoutWeight(1)
}.width('100%').margin({ top: 8 })
Text(this.formatTime(record.timestamp))
.fontSize(11).fontColor('#999').margin({ top: 8 })
}
.width('100%').padding(12)
.backgroundColor(Color.White).borderRadius(8)
}
formatTime(ts: number): string {
const d = new Date(ts);
return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
}
}
步骤6.4:实现入口Ability(entryability/EntryAbility.ets)
arkts
// entryability/EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
await this.requestPermissions();
}
private async requestPermissions(): Promise<void> {
const atm = abilityAccessCtrl.createAtManager();
const permissions: Permissions[] = [
'ohos.permission.MICROPHONE',
'ohos.permission.INTERNET'
];
try {
await atm.requestPermissionsFromUser(this.context, permissions);
} catch (error) {
console.error('权限申请失败:', error);
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/IndexPage', (err) => {
if (err) console.error('加载内容失败:', err);
});
}
}
步骤6.5:集成测试
完成所有模块后,进行端到端集成测试:

验收标准
- 所有15项集成测试通过。
- 中译英和英译中双向翻译功能正常。
- 流式识别和翻译延迟可接受(说完后2秒内开始翻译)。
- 历史记录、搜索、统计功能正常。
- 网络异常时应用不崩溃,有友好提示。
五、常见问题与解决方案
问题1:AudioCapturer启动失败
现象:调用capturer.start()时报错,或on("readData")回调不触发。
可能原因与解决方案:
1.未授予麦克风权限:检查module.json5是否声明ohos.permission.MICROPHONE,并确认EntryAbility中调用了requestPermissionsFromUser。
2.模拟器不支持麦克风:使用真机测试,或确认模拟器配置中已启用麦克风。
3.AudioCapturer实例重复创建:确保每次start前检查isCapturing状态,stop时正确release。
4.采样率不匹配:确认AudioStreamInfo中的samplingRate与ASR服务要求的16000一致。
问题2:WebSocket连接ASR服务失败
现象:ws.connect()报错,或连接后收不到识别结果。
可能原因与解决方案:
1.API Key错误:检查API Key是否正确,是否已开通Paraformer实时语音识别服务。
2.网络不通:检查设备是否能访问dashscope.aliyuncs.com,尝试在浏览器中打开。
3.start-task指令格式错误:检查JSON结构是否符合DashScope WebSocket API规范。
4.音频格式不匹配:确认发送的PCM数据为16kHz、16bit、单声道,与start-task中声明的format一致。
问题3:翻译结果为空或质量差
现象:translate()返回空字符串,或翻译结果不正确。
可能原因与解决方案:
1.API Key未开通文本生成服务:在DashScope控制台确认已开通qwen-plus模型。
2.prompt格式问题:检查prompt中语言名称是否正确(Chinese/English)。
3.temperature过高:确认temperature设为0.1,过高的temperature会导致输出不稳定。
4.网络超时:翻译请求可能超时,增加fetch的超时处理和重试机制。
问题4:TTS合成音频无法播放
现象:ttsService.synthesize()返回数据,但AudioPlayer播放无声。
可能原因与解决方案:
1.音频格式不匹配:TTS返回MP3格式,但AudioRenderer配置为PCM。需要将MP3解码为PCM,或让TTS返回PCM格式。
2.采样率不一致:确认TTS的sample_rate与AudioRenderer的samplingRate一致(均为16000)。
3.播放器未正确释放:上一次播放未正确stop和release,导致新的renderer创建失败。
4.音量设置过低:检查TTS参数中的volume是否为50(默认值)。
问题5:VAD检测不灵敏或误触发
现象:说话时VAD不触发,或环境噪声被误判为语音。
可能原因与解决方案:
1.阈值过高:降低energyThreshold(如从300降到200)。
2.阈值过低:提高energyThreshold(如从300升到500)。
3.静音帧数过多:减少maxSilenceFrames(如从30降到15,即1.5秒静音判定结束)。
4.环境噪声大:实现自适应阈值,启动时录制1秒噪声,阈值 = 噪声RMS × 3。
问题6:端到端延迟过高
现象:说完话后超过3秒才开始播放翻译。
可能原因与解决方案:
1.ASR延迟高:检查网络连接质量,考虑使用更近的服务节点。
2.翻译模型过慢:将qwen-plus替换为更快的qwen-turbo。
3.TTS未流式播放:实现流式TTS,按句子分割,逐句合成播放。
4.流水线未并行化:确认processSentence是异步调用,不阻塞ASR回调。
5.音频未压缩:PCM数据量大,考虑转为Opus压缩格式传输。
六、API参考表
6.1 鸿蒙API

6.2 云端服务API
