HarmonyOS ArkTS 与智能融合案例之实时语音翻译应用 实验指导书和代码

一、实验目的

本实验通过开发一个完整的实时语音翻译应用,使学生掌握端云协同语音翻译系统的设计与实现。具体目标如下:

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. 思考题1:在实时语音翻译场景中,为什么选择16kHz采样率而不是44.1kHz?请从奈奎斯特采样定理和数据量两个角度分析。
  2. 思考题2:端侧VAD(语音活动检测)在实时语音翻译中起什么作用?基于能量阈值的VAD有什么局限性?
  3. 思考题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

相关推荐
木子雨廷1 小时前
第 16 天|网络四态:loading、error、empty、content
harmonyos
用户593096009781 小时前
Flutter 鸿蒙化实战:open_app_settings 适配 OpenHarmony,一键跳转系统设置
harmonyos
贾伟康1 小时前
【HarmonyOS 7新能力|047】ModularObjectExtensionAbility工程封装:把接入逻辑放进可维护的分层结构
harmonyos·arkts·模块化架构·跨应用调用·abilitykit
李游Leo2 小时前
HarmonyOS 7 ArkTS 并发实战:Sendable、共享模块与跨线程对象传递机制
harmonyos
骑着蜗牛撵大象3273 小时前
从跨平台Linux到鸿蒙PC:基于Qt C++的Flameshot截图工具源码级适配
harmonyos·鸿蒙·es
zhangfeng11335 小时前
Ubuntu 版的 CANN 9.2.0-beta.1 的下载方式 三大云厂商的服务器系统
人工智能·华为·ai编程·npu·cann
威哥爱编程5 小时前
HarmonyOS 7 空间音频实战:降噪、美化、变声、空间渲染的节点编排
华为·harmonyos·arkts
威哥爱编程5 小时前
HarmonyOS 7 视觉 AI 实战:系统级场景化控件,低门槛接入端侧视觉能力
华为·harmonyos·arkts
贾伟康5 小时前
【HarmonyOS 7新能力|046】智慧手势工程封装:把接入逻辑放进可维护的分层结构
人机交互·harmonyos·arkts·arkui·手势识别