HarmonyOS开发:智能语音助手全栈实现

举报
Jack20 发表于 2026/06/21 14:28:03 2026/06/21
【摘要】 HarmonyOS开发:智能语音助手全栈实现核心要点:基于HarmonyOS语音识别、语音合成、自然语言理解构建端到端智能语音助手 一、背景与动机想象一下这个场景:你正在厨房做饭,双手沾满了面粉,手机响了——你想查一下明天的天气。这时候如果还要擦手、解锁、打开天气App,那体验简直太糟糕了。但如果只需要说一句"明天天气怎么样",就能得到答案,那感觉就完全不同了。语音交互正在从"锦上添花"变...

HarmonyOS开发:智能语音助手全栈实现

核心要点:基于HarmonyOS语音识别、语音合成、自然语言理解构建端到端智能语音助手


一、背景与动机

想象一下这个场景:你正在厨房做饭,双手沾满了面粉,手机响了——你想查一下明天的天气。这时候如果还要擦手、解锁、打开天气App,那体验简直太糟糕了。但如果只需要说一句"明天天气怎么样",就能得到答案,那感觉就完全不同了。

语音交互正在从"锦上添花"变成"刚需"。开车时查导航、做饭时设定时、运动时切歌——这些场景下,语音是唯一自然的交互方式。而HarmonyOS作为全场景操作系统,天然适合语音交互——手机、手表、音箱、车机,都需要语音能力。

本文将带你从零实现一个完整的智能语音助手,涵盖语音识别(ASR)、自然语言理解(NLU)、技能执行、语音合成(TTS)四大模块。这不是一个玩具Demo,而是一个可以扩展的、工程化的全栈实现。


二、核心原理

2.1 语音助手架构

一个完整的语音助手,本质上是一个"听→懂→做→说"的闭环系统。用户说话→语音识别转文字→自然语言理解提取意图→执行对应技能→语音合成播报结果。

flowchart TD
    A[🎤 用户语音输入] --> B[语音识别 ASR]
    B --> C[原始文本]
    C --> D[自然语言理解 NLU]
    D --> E[意图识别]
    D --> F[槽位提取]
    E --> G{意图路由}
    F --> G
    
    G -->|天气查询| H1[天气技能]
    G -->|定时提醒| H2[提醒技能]
    G -->|音乐播放| H3[音乐技能]
    G -->|系统控制| H4[系统技能]
    G -->|通用问答| H5[对话技能]
    
    H1 --> I[技能执行结果]
    H2 --> I
    H3 --> I
    H4 --> I
    H5 --> I
    
    I --> J[语音合成 TTS]
    J --> K[🔊 语音播报]
    
    classDef primary fill:#4F46E5,stroke:#3730A3,color:#fff
    classDef warning fill:#F59E0B,stroke:#D97706,color:#fff
    classDef error fill:#EF4444,stroke:#DC2626,color:#fff
    classDef info fill:#06B6D4,stroke:#0891B2,color:#fff
    classDef purple fill:#8B5CF6,stroke:#7C3AED,color:#fff
    
    class A,B,C primary
    class D,E,F info
    class G warning
    class H1,H2,H3,H4,H5 purple
    class I,J,K error

2.2 语音识别(ASR)原理

HarmonyOS的语音识别支持两种模式:

  • 短语音识别:适合指令类场景,录音结束后一次性返回结果
  • 长语音识别:适合会议记录、语音输入等场景,实时流式返回

核心流程:音频采集→VAD(语音活动检测)→特征提取→声学模型解码→语言模型重排序→输出文本

2.3 自然语言理解(NLU)原理

NLU模块负责从文本中提取"意图+槽位":

  • 意图(Intent):用户想做什么,如"查天气"“设闹钟”
  • 槽位(Slot):意图的参数,如"明天"“北京”

例如:“明天北京天气怎么样” → 意图=天气查询,槽位={时间:明天, 地点:北京}

2.4 语音合成(TTS)原理

TTS将文本转为语音,核心流程:文本分析→韵律预测→声学特征生成→声码器合成→音频输出。HarmonyOS支持多种音色和语速调节。


三、代码实战

3.1 语音识别模块

// SpeechRecognizer.ets - 语音识别模块
import { speechRecognizer } from '@kit.AISpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 识别结果回调接口
export interface RecognizeCallback {
  onResult: (text: string, isFinal: boolean) => void;
  onError: (error: string) => void;
  onVolumeChange: (volume: number) => void;
}

export class SpeechRecognizer {
  private asrEngine: speechRecognizer.SpeechRecognitionEngine | null = null;
  private isListening: boolean = false;
  private callback: RecognizeCallback | null = null;
  
  // 初始化语音识别引擎
  async init(callback: RecognizeCallback): Promise<boolean> {
    this.callback = callback;
    
    try {
      // 检查语音识别能力是否可用
      const isAvailable = speechRecognizer.isAvailable();
      if (!isAvailable) {
        callback.onError('设备不支持语音识别');
        return false;
      }
      
      // 创建识别引擎
      const extraParams: Record<string, Object> = {
        "locate": "CN",           // 中国区
        "language": "zh-CN",      // 中文
        "vadBegin": 2000,         // 语音起始超时(ms)
        "vadEnd": 3000,           // 语音结束超时(ms)
        "maxAudioDuration": 30000 // 最大录音时长(ms)
      };
      
      const initParams: speechRecognizer.CreateParams = {
        language: 'zh-CN',
        extraParams: extraParams
      };
      
      this.asrEngine = speechRecognizer.createEngine(initParams);
      
      // 设置识别回调
      const listener: speechRecognizer.RecognitionListener = {
        // 识别结果回调
        onResult: (result: speechRecognizer.RecognitionResult) => {
          if (result.isFinal) {
            // 最终结果
            this.callback?.onResult(result.result, true);
            this.isListening = false;
          } else {
            // 中间结果(实时显示)
            this.callback?.onResult(result.result, false);
          }
        },
        
        // 识别完成
        onComplete: () => {
          this.isListening = false;
          console.info('[ASR] 识别完成');
        },
        
        // 错误回调
        onError: (error: BusinessError) => {
          this.isListening = false;
          this.callback?.onError(`识别错误: ${error.code} - ${error.message}`);
        },
        
        // 音量变化
        onVolumeChange: (volume: number) => {
          this.callback?.onVolumeChange(volume);
        }
      };
      
      this.asrEngine.setListener(listener);
      console.info('[ASR] 语音识别引擎初始化成功');
      return true;
      
    } catch (error) {
      const err = error as BusinessError;
      callback.onError(`初始化失败: ${err.code} - ${err.message}`);
      return false;
    }
  }
  
  // 开始监听
  startListening(): boolean {
    if (!this.asrEngine || this.isListening) {
      return false;
    }
    
    try {
      const recognizerParams: speechRecognizer.StartParams = {
        sessionId: `session_${Date.now()}`,
        audioInfo: {
          audioType: 'pcm',
          sampleRate: 16000,
          soundChannel: 1,
          sampleBit: 16
        },
        extraParams: {
          "vadBegin": 2000,
          "vadEnd": 3000,
          "maxAudioDuration": 30000
        }
      };
      
      this.asrEngine.startListening(recognizerParams);
      this.isListening = true;
      console.info('[ASR] 开始监听');
      return true;
      
    } catch (error) {
      const err = error as BusinessError;
      this.callback?.onError(`启动失败: ${err.code}`);
      return false;
    }
  }
  
  // 停止监听
  stopListening(): void {
    if (this.asrEngine && this.isListening) {
      this.asrEngine.finish(`session_stop_${Date.now()}`);
      this.isListening = false;
    }
  }
  
  // 取消识别
  cancel(): void {
    if (this.asrEngine) {
      this.asrEngine.cancel(`session_cancel_${Date.now()}`);
      this.isListening = false;
    }
  }
  
  // 释放引擎
  release(): void {
    if (this.asrEngine) {
      this.asrEngine.shutdown();
      this.asrEngine = null;
      this.isListening = false;
    }
  }
  
  // 获取监听状态
  getIsListening(): boolean {
    return this.isListening;
  }
}

3.2 自然语言理解与技能路由

// NLUEngine.ets - 自然语言理解与技能路由

// 意图定义
export enum IntentType {
  WEATHER = 'weather',           // 天气查询
  ALARM = 'alarm',               // 闹钟/定时
  MUSIC = 'music',               // 音乐播放
  SYSTEM_CONTROL = 'system',     // 系统控制
  GENERAL_QA = 'general_qa',     // 通用问答
  UNKNOWN = 'unknown'            // 未识别
}

// 意图识别结果
export interface NLUResult {
  intent: IntentType;
  slots: Record<string, string>;  // 槽位键值对
  confidence: number;             // 置信度
  rawText: string;                // 原始文本
}

// 技能执行结果
export interface SkillResult {
  success: boolean;
  responseText: string;   // 回复文本
  action?: string;        // 需要执行的动作
  actionParams?: Record<string, string>; // 动作参数
}

export class NLUEngine {
  // 意图关键词映射表
  private intentPatterns: Map<IntentType, string[]> = new Map([
    [IntentType.WATHER, ['天气', '气温', '温度', '下雨', '下雪', '刮风', '晴天', '阴天']],
    [IntentType.ALARM, ['闹钟', '定时', '提醒', '叫醒', '分钟后', '小时后']],
    [IntentType.MUSIC, ['播放', '音乐', '歌曲', '唱歌', '听歌']],
    [IntentType.SYSTEM_CONTROL, ['打开', '关闭', '调亮', '调暗', '音量', '亮度', '蓝牙', 'WiFi']],
    [IntentType.GENERAL_QA, ['什么是', '为什么', '怎么', '如何', '多少', '谁', '哪里']]
  ]);
  
  // 时间关键词映射
  private timePatterns: Map<string, string> = new Map([
    ['今天', 'today'], ['明天', 'tomorrow'], ['后天', 'dayAfterTomorrow'],
    ['现在', 'now'], ['早上', 'morning'], ['下午', 'afternoon'],
    ['晚上', 'evening'], ['今晚', 'tonight']
  ]);
  
  // 解析用户输入
  parse(text: string): NLUResult {
    const normalizedText = text.trim();
    
    // 1. 意图识别:基于关键词匹配
    let bestIntent = IntentType.UNKNOWN;
    let bestScore = 0;
    
    for (const [intent, keywords] of this.intentPatterns) {
      let score = 0;
      for (const keyword of keywords) {
        if (normalizedText.includes(keyword)) {
          score += keyword.length; // 匹配的关键词越长,分数越高
        }
      }
      if (score > bestScore) {
        bestScore = score;
        bestIntent = intent;
      }
    }
    
    // 2. 槽位提取
    const slots: Record<string, string> = {};
    
    // 提取时间槽位
    for (const [keyword, value] of this.timePatterns) {
      if (normalizedText.includes(keyword)) {
        slots['time'] = value;
        break;
      }
    }
    
    // 提取地点槽位(简化版:城市名列表匹配)
    const cities = ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '南京', '西安', '重庆'];
    for (const city of cities) {
      if (normalizedText.includes(city)) {
        slots['location'] = city;
        break;
      }
    }
    
    // 提取数字槽位(用于定时等场景)
    const numberMatch = normalizedText.match(/(\d+)\s*(分钟|小时|秒)/);
    if (numberMatch) {
      slots['duration'] = numberMatch[1];
      slots['unit'] = numberMatch[2];
    }
    
    // 提取音乐相关槽位
    if (bestIntent === IntentType.MUSIC) {
      const songMatch = normalizedText.match(/播放\s*(.+)/);
      if (songMatch) {
        slots['song'] = songMatch[1].replace(/的.*歌$/, '').trim();
      }
    }
    
    // 计算置信度
    const confidence = bestScore > 0 ? Math.min(bestScore / 5, 1.0) : 0;
    
    return {
      intent: bestIntent,
      slots,
      confidence,
      rawText: normalizedText
    };
  }
}

// 技能执行器
export class SkillExecutor {
  private nluEngine: NLUEngine = new NLUEngine();
  
  // 执行技能
  async execute(nluResult: NLUResult): Promise<SkillResult> {
    switch (nluResult.intent) {
      case IntentType.WEATHER:
        return this.executeWeather(nluResult);
      case IntentType.ALARM:
        return this.executeAlarm(nluResult);
      case IntentType.MUSIC:
        return this.executeMusic(nluResult);
      case IntentType.SYSTEM_CONTROL:
        return this.executeSystemControl(nluResult);
      case IntentType.GENERAL_QA:
        return this.executeGeneralQA(nluResult);
      default:
        return {
          success: false,
          responseText: '抱歉,我还没有学会这个技能,你可以试试问我天气、设闹钟、播放音乐哦~'
        };
    }
  }
  
  // 天气技能
  private async executeWeather(nluResult: NLUResult): Promise<SkillResult> {
    const location = nluResult.slots['location'] || '当前位置';
    const time = nluResult.slots['time'] || 'today';
    
    // 实际项目中调用天气API
    // 这里用模拟数据演示
    const weatherData: Record<string, string> = {
      '北京_today': '晴天,气温18-28°C,空气质量良好',
      '上海_today': '多云,气温20-26°C,有轻微雾霾',
      '北京_tomorrow': '阴转小雨,气温15-22°C,记得带伞'
    };
    
    const key = `${location}_${time}`;
    const weather = weatherData[key] || `${location}${time === 'tomorrow' ? '明天' : '今天'}天气信息暂未获取到`;
    
    return {
      success: true,
      responseText: weather,
      action: 'weather',
      actionParams: { location, time }
    };
  }
  
  // 闹钟技能
  private async executeAlarm(nluResult: NLUResult): Promise<SkillResult> {
    const duration = nluResult.slots['duration'];
    const unit = nluResult.slots['unit'] || '分钟';
    
    if (!duration) {
      return {
        success: false,
        responseText: '请告诉我要定多长时间的闹钟,比如"5分钟后提醒我"'
      };
    }
    
    const minutes = unit === '小时' ? parseInt(duration) * 60 : parseInt(duration);
    
    return {
      success: true,
      responseText: `好的,${duration}${unit}后我会提醒你`,
      action: 'setAlarm',
      actionParams: { duration: String(minutes * 60 * 1000) } // 毫秒
    };
  }
  
  // 音乐技能
  private async executeMusic(nluResult: NLUResult): Promise<SkillResult> {
    const song = nluResult.slots['song'] || '随机推荐';
    
    return {
      success: true,
      responseText: `正在为你播放${song}`,
      action: 'playMusic',
      actionParams: { song }
    };
  }
  
  // 系统控制技能
  private async executeSystemControl(nluResult: NLUResult): Promise<SkillResult> {
    const text = nluResult.rawText;
    
    if (text.includes('蓝牙') && text.includes('打开')) {
      return {
        success: true,
        responseText: '已为你打开蓝牙',
        action: 'enableBluetooth',
        actionParams: {}
      };
    }
    
    if (text.includes('音量')) {
      return {
        success: true,
        responseText: '已为你调整音量',
        action: 'adjustVolume',
        actionParams: {}
      };
    }
    
    return {
      success: false,
      responseText: '暂时不支持这个系统操作'
    };
  }
  
  // 通用问答技能
  private async executeGeneralQA(nluResult: NLUResult): Promise<SkillResult> {
    // 实际项目中接入大模型API
    return {
      success: true,
      responseText: `关于"${nluResult.rawText}",这是一个很好的问题。我正在学习中,稍后可以给你更好的回答。`,
      action: 'generalQA',
      actionParams: { question: nluResult.rawText }
    };
  }
}

3.3 语音合成模块

// SpeechSynthesizer.ets - 语音合成模块
import { textToSpeech } from '@kit.AISpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class SpeechSynthesizer {
  private ttsEngine: textToSpeech.TextToSpeechEngine | null = null;
  private isSpeaking: boolean = false;
  
  // 初始化TTS引擎
  async init(): Promise<boolean> {
    try {
      const isAvailable = textToSpeech.isAvailable();
      if (!isAvailable) {
        console.error('[TTS] 设备不支持语音合成');
        return false;
      }
      
      const extraParams: Record<string, Object> = {
        "style": "interaction",     // 交互风格
        "locate": "CN",             // 中国区
        "language": "zh-CN"         // 中文
      };
      
      const initParams: textToSpeech.CreateParams = {
        language: 'zh-CN',
        person: 0,                  // 音色索引(0=女声)
        online: 0,                  // 0=离线模式
        extraParams: extraParams
      };
      
      this.ttsEngine = textToSpeech.createEngine(initParams);
      
      // 设置回调
      const listener: textToSpeech.SpeakListener = {
        onStart: (requestId: string) => {
          this.isSpeaking = true;
          console.info(`[TTS] 开始播报: ${requestId}`);
        },
        onComplete: (requestId: string) => {
          this.isSpeaking = false;
          console.info(`[TTS] 播报完成: ${requestId}`);
        },
        onStop: (requestId: string) => {
          this.isSpeaking = false;
        },
        onData: (buffer: ArrayBuffer, request: textToSpeech.Request) => {
          // 音频数据回调(可用于自定义音频处理)
        },
        onError: (requestId: string, errorCode: number, errorMessage: string) => {
          this.isSpeaking = false;
          console.error(`[TTS] 播报错误: ${errorCode} - ${errorMessage}`);
        }
      };
      
      this.ttsEngine.setListener(listener);
      console.info('[TTS] 语音合成引擎初始化成功');
      return true;
      
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[TTS] 初始化失败: ${err.code} - ${err.message}`);
      return false;
    }
  }
  
  // 语音播报
  speak(text: string, speed: number = 1.0): boolean {
    if (!this.ttsEngine || this.isSpeaking) {
      return false;
    }
    
    try {
      const speakParams: textToSpeech.SpeakParams = {
        requestId: `tts_${Date.now()}`,
        extraParams: {
          "speed": speed.toString(),    // 语速
          "volume": "2",                // 音量
          "pitch": "1.0"                // 音调
        }
      };
      
      this.ttsEngine.speak(text, speakParams);
      return true;
    } catch (error) {
      console.error(`[TTS] 播报失败: ${JSON.stringify(error)}`);
      return false;
    }
  }
  
  // 停止播报
  stop(): void {
    if (this.ttsEngine && this.isSpeaking) {
      this.ttsEngine.stop();
      this.isSpeaking = false;
    }
  }
  
  // 是否正在播报
  getIsSpeaking(): boolean {
    return this.isSpeaking;
  }
  
  // 释放资源
  release(): void {
    if (this.ttsEngine) {
      this.ttsEngine.shutdown();
      this.ttsEngine = null;
      this.isSpeaking = false;
    }
  }
}

3.4 完整的语音助手UI

// VoiceAssistantPage.ets - 智能语音助手主页面
import { router } from '@kit.ArkUI';

@Entry
@Component
struct VoiceAssistantPage {
  @State isListening: boolean = false;
  @State isSpeaking: boolean = false;
  @State currentText: string = '';       // 实时识别文本
  @State chatHistory: ChatMessage[] = []; // 对话历史
  @State volumeLevel: number = 0;        // 音量级别
  @State pulseScale: number = 1.0;       // 脉冲动画缩放
  @State statusText: string = '点击麦克风开始对话';
  
  // 引擎实例
  private asr: SpeechRecognizer = new SpeechRecognizer();
  private tts: SpeechSynthesizer = new SpeechSynthesizer();
  private nlu: NLUEngine = new NLUEngine();
  private executor: SkillExecutor = new SkillExecutor();
  private scroller: Scroller = new Scroller();
  
  // 脉冲动画定时器
  private pulseTimer: number = -1;
  
  aboutToAppear() {
    this.initEngines();
  }
  
  async initEngines() {
    // 初始化ASR
    await this.asr.init({
      onResult: (text: string, isFinal: boolean) => {
        this.currentText = text;
        if (isFinal) {
          this.handleUserInput(text);
        }
      },
      onError: (error: string) => {
        this.statusText = `识别出错: ${error}`;
        this.isListening = false;
        this.stopPulseAnimation();
      },
      onVolumeChange: (volume: number) => {
        this.volumeLevel = volume;
      }
    });
    
    // 初始化TTS
    await this.tts.init();
  }
  
  // 处理用户输入
  async handleUserInput(text: string) {
    // 添加用户消息
    this.chatHistory.push({
      role: 'user',
      content: text,
      timestamp: Date.now()
    });
    
    this.isListening = false;
    this.statusText = '正在思考...';
    this.stopPulseAnimation();
    
    // NLU解析
    const nluResult = this.nlu.parse(text);
    
    // 执行技能
    const skillResult = await this.executor.execute(nluResult);
    
    // 添加助手回复
    this.chatHistory.push({
      role: 'assistant',
      content: skillResult.responseText,
      timestamp: Date.now()
    });
    
    // 语音播报
    this.isSpeaking = true;
    this.statusText = '正在回复...';
    this.tts.speak(skillResult.responseText);
    
    // 延迟重置状态(等待TTS完成)
    setTimeout(() => {
      this.isSpeaking = false;
      this.statusText = '点击麦克风继续对话';
    }, skillResult.responseText.length * 200); // 粗略估算播报时长
    
    // 滚动到底部
    setTimeout(() => {
      this.scroller.scrollEdge(Edge.Bottom);
    }, 100);
  }
  
  build() {
    Column() {
      // 标题栏
      this.TitleBar();
      
      // 对话区域
      this.ChatArea();
      
      // 底部控制区
      this.ControlArea();
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0A0A1A')
  }
  
  @Builder TitleBar() {
    Row() {
      Text('🤖 智能语音助手')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
      
      Blank()
      
      Text(this.statusText)
        .fontSize(12)
        .fontColor('#9CA3AF')
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 16, bottom: 8 })
  }
  
  @Builder ChatArea() {
    List({ space: 12, scroller: this.scroller }) {
      ForEach(this.chatHistory, (msg: ChatMessage) => {
        ListItem() {
          this.ChatBubble(msg);
        }
      })
      
      // 实时识别文本
      if (this.currentText && this.isListening) {
        ListItem() {
          Row() {
            Text(this.currentText)
              .fontSize(15)
              .fontColor('#E0E7FF')
              .maxLines(3)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .width('75%')
          .padding(12)
          .borderRadius({ topLeft: 4, topRight: 16, bottomLeft: 16, bottomRight: 16 })
          .backgroundColor('#312E81')
          .alignItems(VerticalAlign.Start)
        }
      }
    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 16, right: 16 })
  }
  
  @Builder ChatBubble(msg: ChatMessage) {
    if (msg.role === 'user') {
      // 用户消息 - 右对齐
      Row() {
        Blank()
        Text(msg.content)
          .fontSize(15)
          .fontColor('#FFFFFF')
          .padding(12)
          .borderRadius({ topLeft: 16, topRight: 4, bottomLeft: 16, bottomRight: 16 })
          .backgroundColor('#4F46E5')
          .maxLines(10)
      }
      .width('100%')
    } else {
      // 助手消息 - 左对齐
      Row() {
        Text(msg.content)
          .fontSize(15)
          .fontColor('#E0E7FF')
          .padding(12)
          .borderRadius({ topLeft: 4, topRight: 16, bottomLeft: 16, bottomRight: 16 })
          .backgroundColor('#1E1B4B')
          .maxLines(10)
      }
      .width('80%')
      .alignItems(VerticalAlign.Start)
    }
  }
  
  @Builder ControlArea() {
    Column() {
      // 音量指示条
      if (this.isListening) {
        Row() {
          ForEach([1, 2, 3, 4, 5, 6, 7, 8], (item: number) => {
            Column()
              .width(4)
              .height(`${Math.min(this.volumeLevel * 3, item * 8)}%`)
              .backgroundColor(this.volumeLevel * 3 > item * 8 ? '#4F46E5' : '#1A1A2E')
              .borderRadius(2)
              .margin({ right: 3 })
          })
        }
        .width('60%')
        .height(40)
        .justifyContent(FlexAlign.Center)
        .alignItems(VerticalAlign.Bottom)
      }
      
      // 麦克风按钮
      Stack() {
        // 外圈脉冲动画
        Circle()
          .width(this.isListening ? 100 : 80)
          .height(this.isListening ? 100 : 80)
          .fill(Color.Transparent)
          .stroke(this.isListening ? '#EF4444' : '#4F46E5')
          .strokeWidth(2)
          .opacity(this.isListening ? 0.3 : 0)
          .scale({ x: this.pulseScale, y: this.pulseScale })
        
        // 内圈按钮
        Circle()
          .width(72)
          .height(72)
          .fill(this.isListening ? '#EF4444' : '#4F46E5')
          .shadow({
            radius: this.isListening ? 20 : 10,
            color: this.isListening ? '#EF444480' : '#4F46E580',
            offsetY: 4
          })
        
        // 麦克风图标
        Text(this.isListening ? '⏹' : '🎤')
          .fontSize(28)
          .fontColor('#FFFFFF')
      }
      .width(100)
      .height(100)
      .margin({ top: 16, bottom: 16 })
      .onClick(() => this.toggleListening())
      
      // 快捷指令
      Row() {
        ForEach(['今天天气', '设5分钟闹钟', '播放音乐'], (cmd: string) => {
          Text(cmd)
            .fontSize(12)
            .fontColor('#C7D2FE')
            .backgroundColor('#1E1B4B')
            .borderRadius(14)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .margin({ right: 8 })
            .onClick(() => this.handleUserInput(cmd))
        })
      }
      .margin({ bottom: 24 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 8 })
  }
  
  // 切换监听状态
  private toggleListening() {
    if (this.isSpeaking) {
      this.tts.stop();
      this.isSpeaking = false;
    }
    
    if (this.isListening) {
      this.asr.stopListening();
      this.isListening = false;
      this.stopPulseAnimation();
      this.statusText = '已停止监听';
    } else {
      const started = this.asr.startListening();
      if (started) {
        this.isListening = true;
        this.currentText = '';
        this.statusText = '正在聆听...';
        this.startPulseAnimation();
      }
    }
  }
  
  // 启动脉冲动画
  private startPulseAnimation() {
    this.stopPulseAnimation();
    this.pulseTimer = setInterval(() => {
      this.pulseScale = this.pulseScale === 1.0 ? 1.15 : 1.0;
    }, 600);
  }
  
  // 停止脉冲动画
  private stopPulseAnimation() {
    if (this.pulseTimer !== -1) {
      clearInterval(this.pulseTimer);
      this.pulseTimer = -1;
    }
    this.pulseScale = 1.0;
  }
  
  aboutToDisappear() {
    this.stopPulseAnimation();
    this.asr.release();
    this.tts.release();
  }
}

// 对话消息数据结构
interface ChatMessage {
  role: 'user' | 'assistant';
  content: string;
  timestamp: number;
}

四、踩坑与注意事项

4.1 麦克风权限

语音识别需要麦克风权限ohos.permission.MICROPHONE,这是用户授权级别的权限。必须在module.json5中声明并运行时请求。如果用户拒绝授权,ASR引擎初始化会失败。

{
  "requestPermissions": [
    {
      "name": "ohos.permission.MICROPHONE",
      "reason": "$string:mic_reason",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    }
  ]
}

4.2 ASR引擎生命周期

这是最容易踩的坑。ASR引擎是有限资源,必须正确管理生命周期:

  • 不要重复创建引擎:一个应用只需一个ASR实例
  • 使用完毕必须调用shutdown():否则会占用系统资源,影响其他应用的语音功能
  • 不要在onComplete中立即重新startListening:需要等待至少100ms间隔

4.3 TTS与ASR的冲突

如果TTS正在播报时启动ASR,TTS的声音会被麦克风拾取,导致"回声"问题。解决方案:

  1. TTS播报时自动暂停ASR
  2. 使用AEC(声学回声消除)技术
  3. 在TTS完成后再启动ASR
// 正确的流程
tts.speak(response);
// 等TTS播报完成后再启动ASR
// 在onComplete回调中重新startListening

4.4 后台语音识别

HarmonyOS对后台运行有限制,应用进入后台后ASR可能会被系统回收。如果需要后台持续识别(如语音唤醒),需要申请长时任务:

import { backgroundTaskManager } from '@kit.BackgroundTasksKit';

// 申请长时任务
backgroundTaskManager.startBackgroundRunning(
  context,
  backgroundTaskManager.BackgroundMode.AUDIO_RECORDING,
  { title: '语音助手', text: '正在后台监听语音指令' }
);

4.5 NLU的局限性

基于关键词的NLU方案简单但粗糙,容易误判。比如"打开窗户看看天气"可能被识别为"系统控制"而非"天气查询"。实际项目中建议:

  1. 使用HarmonyOS的NLU API(如果可用)
  2. 接入云端大模型做意图理解
  3. 增加上下文管理(多轮对话)

五、HarmonyOS 6适配

5.1 API变更

变更项 HarmonyOS 5 HarmonyOS 6
ASR引擎 speechRecognizer speechRecognizer(兼容)
TTS引擎 textToSpeech textToSpeech(兼容)
语音唤醒 不支持 新增voiceWakeUp API
多音色 有限音色 新增情感化音色
离线模型 基础模型 增强版离线模型

5.2 语音唤醒

HarmonyOS 6最令人兴奋的新增功能是语音唤醒。应用可以注册唤醒词,在设备休眠状态下监听特定词语:

// HarmonyOS 6 新增
import { voiceWakeUp } from '@kit.AISpeechKit';

const wakeUpEngine = voiceWakeUp.createEngine({
  wakeupWord: '小华小华',
  sensitivity: voiceWakeUp.SensitivityLevel.MEDIUM
});

wakeUpEngine.on('wakeup', () => {
  // 检测到唤醒词,启动ASR
  asr.startListening();
});

5.3 迁移要点

  1. 语音唤醒集成:利用新的voiceWakeUp API实现"Hey 小华"唤醒功能
  2. 情感化TTS:使用新的音色参数让回复更自然
  3. 增强版离线模型:HarmonyOS 6的离线ASR准确率显著提升,建议优先使用离线模式

六、总结

本文从零实现了一个完整的HarmonyOS智能语音助手,核心知识点如下:

智能语音助手
├── 语音识别(ASR)
│   ├── speechRecognizer引擎创建与配置
│   ├── 实时识别与最终结果处理
│   ├── 音量回调与VAD参数
│   └── 生命周期管理(init→start→stop→shutdown)
├── 自然语言理解(NLU)
│   ├── 基于关键词的意图识别
│   ├── 槽位提取(时间/地点/数字/歌曲)
│   ├── 意图路由与技能分发
│   └── 置信度评估
├── 技能执行
│   ├── 天气查询技能
│   ├── 闹钟定时技能
│   ├── 音乐播放技能
│   ├── 系统控制技能
│   └── 通用问答技能
├── 语音合成(TTS)
│   ├── textToSpeech引擎创建
│   ├── 语速/音调/音量控制
│   └── 播报状态回调
├── UI交互
│   ├── 对话气泡界面
│   ├── 麦克风脉冲动画
│   ├── 音量可视化
│   └── 快捷指令
└── 工程化
    ├── 权限管理(麦克风)
    ├── ASR/TTS冲突处理
    ├── 后台长时任务
    └── HarmonyOS 6语音唤醒

关键收获

  • ASR和TTS引擎是有限资源,必须严格管理生命周期
  • TTS播报时不要同时启动ASR,否则会产生回声
  • 基于关键词的NLU是起步方案,生产环境应接入大模型
  • 后台语音识别需要申请长时任务权限
  • HarmonyOS 6的语音唤醒能力将彻底改变语音助手的交互体验

语音助手是AI落地的最佳场景之一。HarmonyOS的端侧AI能力让语音交互更快速、更隐私、更可靠。随着语音唤醒等新能力的加入,语音助手将从"按住说话"进化到"随时唤醒",这是质的飞跃。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。