HarmonyOS 智能工具箱(三):语音交互工具

系列: HarmonyOS 智能工具箱 App 全栈开发 · 第 3 篇

上一篇 : OCR 文字识别工具


引言

语音是最自然的人机交互方式。本文将使用 HarmonyOS 的 Core Speech Kit 实现语音输入(ASR)和语音朗读(TTS)功能,并将其集成到智能工具箱中。用户可以用语音输入文字、用语音控制 App 操作、让 App 朗读 OCR 识别结果。

通过本文你将学到:

  • 使用 Core Speech Kit 的 speechRecognizer 进行端侧语音识别
  • 使用 textToSpeech 实现文字转语音朗读
  • 实现麦克风权限的动态申请
  • 构建实时字幕和语音指令系统

环境准备

项目 版本
DevEco Studio 6.1.1 Release (6.1.1.280)
API Level API 24 (HarmonyOS 6.1.1 Release)
设备 真机(需要麦克风)

依赖配置

json5 复制代码
// oh-package.json5
{
  "dependencies": {
    "@kit.CoreSpeechKit": "^1.0.0",
    "@kit.AbilityKit": "^1.0.0",
    "@kit.PerformanceAnalysisKit": "^1.0.0"
  }
}

权限配置

json5 复制代码
// entry/src/main/module.json5
{
  "requestPermissions": [
    { "name": "ohos.permission.MICROPHONE", "reason": "语音识别需要使用麦克风采集音频" }
  ]
}

注意:语音识别(ASR)必须声明麦克风权限并运行时动态申请;语音合成(TTS)播放无需麦克风权限。


核心实现

Step 1: 封装语音服务层

目标: 将 Core Speech Kit 的 ASR 和 TTS 能力封装为统一服务。

typescript 复制代码
// service/SpeechService.ets
import { speechRecognizer, textToSpeech } from '@kit.CoreSpeechKit';
import { abilityAccessCtrl } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'SpeechService';
const DOMAIN = 0xFF00;

// ASR 识别结果
export class AsrResult {
  text: string = '';
  isFinal: boolean = false;
  confidence: number = 0;
}

// TTS 状态
export enum TtsState {
  IDLE = 'idle',
  SPEAKING = 'speaking',
  PAUSED = 'paused'
}

// 语音服务类
export class SpeechService {
  private static asrListener: speechRecognizer.SpeechRecognitionListener | null = null;
  private static ttsSynthesizer: textToSpeech.TextToSpeechSynthesizer | null = null;
  private static ttsState: TtsState = TtsState.IDLE;

  // ========== ASR 语音识别 ==========

  /**
   * 检查并申请麦克风权限
   */
  static async checkPermission(context: Context): Promise<boolean> {
    try {
      const atManager = abilityAccessCtrl.createAtManager();
      const result = await atManager.checkAccessToken(
        context.tokenID as number,
        'ohos.permission.MICROPHONE'
      );
      if (result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
        return true;
      }

      // 申请权限
      const grantResults = await atManager.requestPermissionsFromUser(
        context,
        ['ohos.permission.MICROPHONE']
      );
      return grantResults.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `权限检查失败: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 开始语音识别
   * @param onResult 识别结果回调
   * @param onError 错误回调
   * @param onVolume 音量变化回调
   */
  static startListening(
    onResult: (result: AsrResult) => void,
    onError: (code: number, message: string) => void,
    onVolume?: (volume: number) => void
  ): void {
    // 创建监听器
    this.asrListener = speechRecognizer.createSpeechRecognizerListener({
      onResult: (result) => {
        const asrResult = new AsrResult();
        asrResult.text = result.text;
        asrResult.isFinal = result.isFinal;
        onResult(asrResult);
      },
      onError: (error) => {
        hilog.error(DOMAIN, TAG, `ASR 错误: ${error.code} - ${error.message}`);
        onError(error.code, error.message);
      },
      onVolumeChanged: (volume) => {
        if (onVolume) {
          onVolume(volume);
        }
      }
    });

    // 开始识别 --- 推荐音频格式:16kHz, 16-bit, 单声道
    speechRecognizer.startListening(this.asrListener, {
      language: 'zh',
      audioFormat: {
        sampleRate: 16000,
        channelCount: 1,
        sampleBit: 16
      }
    });

    hilog.info(DOMAIN, TAG, 'ASR 开始识别');
  }

  /**
   * 停止语音识别
   */
  static stopListening(): void {
    if (this.asrListener) {
      speechRecognizer.stopListening(this.asrListener);
      this.asrListener = null;
      hilog.info(DOMAIN, TAG, 'ASR 停止识别');
    }
  }

  /**
   * 释放 ASR 资源
   */
  static releaseAsr(): void {
    this.stopListening();
    speechRecognizer.release();
    hilog.info(DOMAIN, TAG, 'ASR 资源已释放');
  }

  // ========== TTS 语音合成 ==========

  /**
   * 初始化 TTS 合成器
   */
  static initTts(): void {
    if (this.ttsSynthesizer) return;

    this.ttsSynthesizer = textToSpeech.createSynthesizer();

    // 监听合成状态
    this.ttsSynthesizer.on('state', (state: string) => {
      hilog.info(DOMAIN, TAG, `TTS 状态: ${state}`);
      if (state === 'playing') {
        this.ttsState = TtsState.SPEAKING;
      } else if (state === 'paused') {
        this.ttsState = TtsState.PAUSED;
      } else {
        this.ttsState = TtsState.IDLE;
      }
    });

    hilog.info(DOMAIN, TAG, 'TTS 初始化完成');
  }

  /**
   * 朗读文本
   * @param text 要朗读的文本
   * @param options 朗读选项
   */
  static async speak(text: string, options?: TtsOptions): Promise<void> {
    this.initTts();

    if (!this.ttsSynthesizer) {
      hilog.error(DOMAIN, TAG, 'TTS 合成器未初始化');
      return;
    }

    try {
      await this.ttsSynthesizer.speak(text, {
        language: options?.language ?? 'zh',
        voiceId: options?.voiceId ?? 'zh_female',
        speed: options?.speed ?? 1.0,
        volume: options?.volume ?? 1.0
      });
      hilog.info(DOMAIN, TAG, `TTS 开始朗读: ${text.substring(0, 20)}...`);
    } catch (err) {
      hilog.error(DOMAIN, TAG, `TTS 朗读失败: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 停止朗读
   */
  static stopSpeaking(): void {
    if (this.ttsSynthesizer) {
      this.ttsSynthesizer.stop();
      this.ttsState = TtsState.IDLE;
    }
  }

  /**
   * 暂停朗读
   */
  static pauseSpeaking(): void {
    if (this.ttsSynthesizer) {
      this.ttsSynthesizer.pause();
    }
  }

  /**
   * 恢复朗读
   */
  static resumeSpeaking(): void {
    if (this.ttsSynthesizer) {
      this.ttsSynthesizer.resume();
    }
  }

  /**
   * 获取当前 TTS 状态
   */
  static getTtsState(): TtsState {
    return this.ttsState;
  }
}

// TTS 选项
export class TtsOptions {
  language: string = 'zh';
  voiceId: string = 'zh_female';
  speed: number = 1.0;
  volume: number = 1.0;
}

Step 2: 创建语音输入页面

目标: 实现语音输入主界面,支持实时字幕显示。

typescript 复制代码
// features/speech/SpeechPage.ets
import { SpeechService, AsrResult } from '../../service/SpeechService';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'SpeechPage';

@Entry
@Component
struct SpeechPage {
  @Local recognizedText: string = '';
  @Local intermediateText: string = '';
  @Local isListening: boolean = false;
  @Local volumeLevel: number = 0;
  @Local speechHistory: string[] = [];

  async aboutToAppear(): Promise<void> {
    // 检查麦克风权限
    const context = getContext(this);
    const hasPermission = await SpeechService.checkPermission(context);
    if (!hasPermission) {
      this.recognizedText = '请授予麦克风权限后使用语音功能';
    }
  }

  aboutToDisappear(): void {
    // 释放资源
    SpeechService.releaseAsr();
  }

  // 切换语音识别状态
  toggleListening(): void {
    if (this.isListening) {
      this.stopListening();
    } else {
      this.startListening();
    }
  }

  // 开始语音识别
  startListening(): void {
    this.intermediateText = '';
    this.recognizedText = '';

    SpeechService.startListening(
      // onResult
      (result: AsrResult) => {
        if (result.isFinal) {
          // 最终结果 --- 确认识别
          this.recognizedText = result.text;
          this.intermediateText = '';
          this.isListening = false;
          // 添加到历史
          if (result.text.trim()) {
            this.speechHistory.unshift(result.text);
            if (this.speechHistory.length > 20) {
              this.speechHistory.pop();
            }
          }
        } else {
          // 中间结果 --- 实时显示
          this.intermediateText = result.text;
        }
      },
      // onError
      (code: number, message: string) => {
        hilog.error(DOMAIN, TAG, `识别错误: ${code} - ${message}`);
        this.isListening = false;
        this.intermediateText = '';
      },
      // onVolume
      (volume: number) => {
        this.volumeLevel = volume;
      }
    );

    this.isListening = true;
  }

  // 停止语音识别
  stopListening(): void {
    SpeechService.stopListening();
    this.isListening = false;
    this.intermediateText = '';
  }

  build() {
    Column({ space: 16 }) {
      // 顶部标题
      Row() {
        Text('语音助手')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
        Blank()
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      // 识别结果展示区
      Column({ space: 12 }) {
        // 中间结果(实时字幕)
        if (this.intermediateText) {
          Text(this.intermediateText)
            .fontSize(16)
            .fontColor('#666')
            .fontStyle(FontStyle.Italic)
            .width('100%')
            .padding(12)
            .backgroundColor('#F8F8F8')
            .borderRadius(8)
        }

        // 最终结果
        if (this.recognizedText) {
          Column({ space: 8 }) {
            Row() {
              Text('识别结果')
                .fontSize(14)
                .fontColor('#999')
              Blank()
              Button('朗读')
                .type(ButtonType.Normal)
                .height(28)
                .fontSize(12)
                .onClick(() => {
                  SpeechService.speak(this.recognizedText);
                })
              Button('复制')
                .type(ButtonType.Normal)
                .height(28)
                .fontSize(12)
                .margin({ left: 8 })
                .onClick(() => {
                  this.getUIContext().getPromptAction().showToast({
                    message: '已复制',
                    duration: 1000
                  });
                })
            }
            .width('100%')

            Text(this.recognizedText)
              .fontSize(18)
              .fontColor('#333')
              .width('100%')
              .padding(12)
              .backgroundColor('#FFFFFF')
              .borderRadius(8)
          }
        }

        // 提示文字
        if (!this.recognizedText && !this.intermediateText) {
          Text(this.isListening ? '正在聆听...' : '点击麦克风开始语音输入')
            .fontSize(15)
            .fontColor('#999')
            .margin({ top: 40 })
        }
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
      .layoutWeight(1)

      // 麦克风按钮
      Column({ space: 8 }) {
        // 音量指示器
        if (this.isListening) {
          Row({ space: 4 }) {
            ForEach([0, 1, 2, 3, 4], (index: number) => {
              Column()
                .width(4)
                .height(10 + this.volumeLevel * 2)
                .backgroundColor(this.volumeLevel > index ? '#007DFF' : '#E0E0E0')
                .borderRadius(2)
            })
          }
          .height(30)
          .alignItems(VerticalAlign.Bottom)
        }

        // 麦克风按钮
        SymbolGlyph(this.isListening ? $r('sys.symbol.mic_fill') : $r('sys.symbol.mic'))
          .fontSize(40)
          .fontColor([this.isListening ? '#FF4444' : '#007DFF'])
          .onClick(() => this.toggleListening())
          .padding(20)
          .borderRadius(40)
          .backgroundColor(this.isListening ? '#FFF0F0' : '#F0F8FF')
      }
      .width('100%')
      .padding({ bottom: 24 })

      // 语音历史
      if (this.speechHistory.length > 0) {
        Column({ space: 8 }) {
          Text('识别历史')
            .fontSize(14)
            .fontColor('#999')
            .width('100%')

          List({ space: 4 }) {
            ForEach(this.speechHistory, (text: string, index: number) => {
              ListItem() {
                Row() {
                  Text(text)
                    .fontSize(13)
                    .fontColor('#666')
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                    .layoutWeight(1)
                  SymbolGlyph($r('sys.symbol.speaker'))
                    .fontSize(16)
                    .fontColor(['#007DFF'])
                    .onClick(() => SpeechService.speak(text))
                }
                .padding({ left: 12, right: 12, top: 8, bottom: 8 })
                .backgroundColor('#FFFFFF')
                .borderRadius(6)
              }
            })
          }
          .height(150)
        }
        .padding({ left: 16, right: 16, bottom: 16 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

Step 3: 创建语音指令解析器

目标: 支持用语音控制 App 操作(如"打开设置"、"朗读结果")。

typescript 复制代码
// features/speech/VoiceCommand.ets
import { SpeechService } from '../../service/SpeechService';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'VoiceCommand';

// 指令定义
interface VoiceCommandItem {
  keywords: string[];
  action: () => void;
  description: string;
}

export class VoiceCommandParser {
  private commands: VoiceCommandItem[] = [];

  // 注册指令
  register(keywords: string[], action: () => void, description: string): void {
    this.commands.push({ keywords, action, description });
  }

  // 解析并执行指令
  parse(text: string): boolean {
    const lower = text.toLowerCase().trim();

    for (const cmd of this.commands) {
      for (const keyword of cmd.keywords) {
        if (lower.includes(keyword)) {
          hilog.info(DOMAIN, TAG, `匹配指令: ${cmd.description}`);
          cmd.action();
          return true;
        }
      }
    }

    return false;
  }

  // 获取所有指令描述
  getCommandList(): string[] {
    return this.commands.map((cmd: VoiceCommandItem) => {
      const keywords = cmd.keywords.map((k: string) => `"${k}"`).join(' / ');
      return `${keywords} --- ${cmd.description}`;
    });
  }
}

在页面中集成语音指令:

typescript 复制代码
// 在 SpeechPage 中集成
private commandParser: VoiceCommandParser = new VoiceCommandParser();

aboutToAppear() {
  // 注册语音指令
  this.commandParser.register(
    ['朗读', '读出来', '播放'],
    () => {
      if (this.recognizedText) {
        SpeechService.speak(this.recognizedText);
      }
    },
    '朗读识别结果'
  );

  this.commandParser.register(
    ['停止', '暂停', '别读了'],
    () => { SpeechService.stopSpeaking(); },
    '停止朗读'
  );

  this.commandParser.register(
    ['清空', '清除', '重来'],
    () => {
      this.recognizedText = '';
      this.intermediateText = '';
    },
    '清空识别结果'
  );

  this.commandParser.register(
    ['复制', '拷贝'],
    () => {
      // 复制到剪贴板
    },
    '复制识别文本'
  );
}

// 在 onResult 回调中尝试匹配指令
onResult(result: AsrResult) {
  if (result.isFinal) {
    // 先尝试匹配指令
    const isCommand = this.commandParser.parse(result.text);
    if (!isCommand) {
      // 非指令,作为普通文本处理
      this.recognizedText = result.text;
    }
  }
}

Step 4: 创建 TTS 设置页面

目标: 提供语音朗读的语速、音量和音色设置。

typescript 复制代码
// features/speech/TtsSettings.ets
import { SpeechService } from '../../service/SpeechService';
import { PersistenceV2, Type } from '@kit.ArkUI';

@ObservedV2
class TtsSettings {
  @Trace @Type(Number) speed: number = 1.0
  @Trace @Type(Number) volume: number = 1.0
  @Trace @Type(String) voiceId: string = 'zh_female'
}

const ttsSettings = PersistenceV2.connect(
  TtsSettings, 'tts_settings', () => new TtsSettings()
)!

@Component
export struct TtsSettingsPage {
  @Local testText: string = '你好,我是 HarmonyOS 智能工具箱的语音助手。'

  build() {
    Column({ space: 16 }) {
      Text('语音朗读设置')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16 })

      // 语速设置
      Column({ space: 8 }) {
        Row() {
          Text('语速').fontSize(15)
          Blank()
          Text(`${ttsSettings!.speed.toFixed(1)}x`).fontSize(14).fontColor('#999')
        }.width('100%')

        Slider({ value: $$ttsSettings!.speed, min: 0.5, max: 2.0, step: 0.1 })
          .width('100%')
      }
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      // 音量设置
      Column({ space: 8 }) {
        Row() {
          Text('音量').fontSize(15)
          Blank()
          Text(`${Math.round(ttsSettings!.volume * 100)}%`).fontSize(14).fontColor('#999')
        }.width('100%')

        Slider({ value: $$ttsSettings!.volume, min: 0.0, max: 1.0, step: 0.1 })
          .width('100%')
      }
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      // 音色选择
      Column({ space: 8 }) {
        Text('音色').fontSize(15).width('100%')

        Row({ space: 12 }) {
          Button('女声')
            .type(ButtonType.Normal)
            .backgroundColor(ttsSettings!.voiceId === 'zh_female' ? '#007DFF' : '#F0F0F0')
            .fontColor(ttsSettings!.voiceId === 'zh_female' ? '#FFF' : '#333')
            .onClick(() => { ttsSettings!.voiceId = 'zh_female' })

          Button('男声')
            .type(ButtonType.Normal)
            .backgroundColor(ttsSettings!.voiceId === 'zh_male' ? '#007DFF' : '#F0F0F0')
            .fontColor(ttsSettings!.voiceId === 'zh_male' ? '#FFF' : '#333')
            .onClick(() => { ttsSettings!.voiceId = 'zh_male' })
        }
      }
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)

      // 试听按钮
      Button('试听语音')
        .type(ButtonType.Capsule)
        .width('80%')
        .height(44)
        .onClick(() => {
          SpeechService.speak(this.testText, {
            speed: ttsSettings!.speed,
            volume: ttsSettings!.volume,
            voiceId: ttsSettings!.voiceId
          });
        })

      // 试听文本
      TextArea({ text: this.testText, placeholder: '输入试听文本' })
        .width('90%')
        .height(100)
        .onChange((value: string) => { this.testText = value })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .padding({ left: 16, right: 16 })
  }
}

Step 5: 注册路由

目标: 在首页 Navigation 中注册语音页面路由。

typescript 复制代码
// pages/Index.ets 中的 navDestination 部分
.navDestination((name: string, param: Object) => {
  if (name === 'SpeechPage') {
    SpeechPage()
  } else if (name === 'TtsSettings') {
    TtsSettingsPage()
  }
})

效果展示

语音页面包含 ASR 语音输入和 TTS 文字朗读两个区域。首次使用时请求麦克风权限,引擎初始化完成后才允许开始识别。

当前监听器在 isFinaltrue 时更新最终文本;示例没有实现中间字幕、语速/音量/音色设置页,也没有语音指令解析器,因此这些能力不作为本篇完成项。


关键代码解读

ASR 识别流程

typescript 复制代码
// 1. 创建监听器
const listener = speechRecognizer.createSpeechRecognizerListener({
  onResult: (result) => {
    if (result.isFinal) {
      // 最终结果 --- 可信度高,用于确认
    } else {
      // 中间结果 --- 可能被修正,用于实时显示
    }
  }
});

// 2. 开始识别
speechRecognizer.startListening(listener, {
  language: 'zh',
  audioFormat: { sampleRate: 16000, channelCount: 1, sampleBit: 16 }
});

// 3. 停止识别
speechRecognizer.stopListening(listener);

// 4. 释放资源
speechRecognizer.release();

关键点:

  • 中间结果 vs 最终结果isFinal: false 的中间结果可能被后续回调修正,UI 应区分显示
  • 音频格式:推荐 16kHz/16-bit/单声道,不匹配可能导致识别率下降
  • 并发限制 :同一时间只能有一个 ASR 实例,重复调用 startListening() 会抛错

TTS 合成流程

typescript 复制代码
// 1. 创建合成器
const synthesizer = textToSpeech.createSynthesizer();

// 2. 监听状态
synthesizer.on('state', (state) => {
  // state: 'playing' | 'paused' | 'idle'
});

// 3. 朗读
await synthesizer.speak(text, {
  language: 'zh',
  voiceId: 'zh_female',
  speed: 1.0,
  volume: 1.0
});

// 4. 控制
synthesizer.pause();   // 暂停
synthesizer.resume();  // 恢复
synthesizer.stop();    // 停止

总结

本文实现了智能工具箱的语音交互工具,主要完成了:

  1. SpeechService 服务层:封装 Core Speech Kit 的 ASR 和 TTS 能力
  2. 语音输入页面:实时字幕显示、识别历史、一键朗读
  3. 语音指令系统:自然语言匹配控制 App 操作
  4. TTS 设置页面:语速、音量、音色可调,自动持久化

语音能力可与 OCR 结合------识别出的文字可以直接朗读出来,形成完整的"看→听"体验。


下篇预告

下一篇: HarmonyOS 智能工具箱(四):AI 图像处理工具

将使用 Core Vision Kit 和 MindSpore Lite 实现图像分类、目标检测和图像超分辨率功能。


参考资料

相关推荐
国服第二切图仔1 小时前
HarmonyOS7新特性之沉浸光感:新一代材质渲染技术解析
harmonyos
●VON1 小时前
鸿蒙 PC Markdown 编辑器链接补全与离线校验:中文路径、标题锚点与授权边界
华为·编辑器·harmonyos·鸿蒙
kiros_wang2 小时前
鸿蒙性能优化全维度实战(启动速度 + 内存治理 + 帧率稳定 + 包体积瘦身)
华为·性能优化·harmonyos
●VON2 小时前
鸿蒙 PC Markdown 编辑器快速打开:模糊排序、最近权重与纯键盘路径
华为·编辑器·计算机外设·harmonyos·鸿蒙
●VON2 小时前
鸿蒙 PC Markdown 编辑器命令面板:把桌面高频操作收束到一个入口
服务器·华为·编辑器·harmonyos·鸿蒙
爱写代码的森2 小时前
鸿蒙三方库 | harmony-utils之FileUtil文件拷贝与移动详解
华为·harmonyos·鸿蒙·huawei
xd1855785552 小时前
落枕缓解指南 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙
世人万千丶10 小时前
鸿蒙Flutter Flex多子组件权重分配
学习·flutter·华为·harmonyos·鸿蒙
拥抱太阳061614 小时前
HarmonyOS 应用开发《掌上英语》第23篇-图标与颜色系统构建可主题化视觉体系
华为·harmonyos