实现实时语音转文字功能鸿蒙示例代码

本文原创发布在华为开发者社区

介绍

本示例介绍如何使用speechRecognizer实时语言转文字,并且根据光标位置插入文字,以及文本一键清空功能。

实现实时语音转文字功能源码链接

效果预览

使用说明

  1. 点击顶部按钮可切换本人或非本人模拟聊天界面发送消息,本人发送右对齐,非本人发送左对齐。
  2. 点击RichEditor组件唤起输入法,已发送的消息自动避让。
  3. 长按启动实时语音转文字,松开停止语音转文字,根据光标所在位置插入语音识别的文字,点击清空可以清除RichEditor组件中的内容。
  4. 点击发送可以将RichEditor组件中的内容发送出去,可发送文字和图片消息。

实现思路

聊天页面左右布局

通过Flex组件实现左右布局,本人发送时设置方向为FlexDirection.RowReverse,非本人发送时设置方向为FlexDirection.Row。具体实现如下:

typescript 复制代码
List({scroller: this.listScroller}) {
  ForEach(this.data, (item: MsgContent) => {
    ListItem() {
      // 通过isSelf判断是否是本人发送的消息,来决定Flex组件的direction是否需要进行反转。
      Flex({ direction: item.isSelf ? FlexDirection.RowReverse : FlexDirection.Row, space: { main: LengthMetrics.vp(8) } }) {
        Image($r('app.media.avatar'))
          .width(32)
          .aspectRatio(1)

        Text() {
          ...
        }
        ...
      }
      .width('100%')
    }
    .margin(12)
  }, (item: MsgContent, index: number) => `${JSON.stringify(item)}_${JSON.stringify(index)}`)
}

图文混排消息显示

整体使用Text去布局,文字通过内嵌Span组件显示,图片显示通过内嵌ImageSpan组件显示,具体代码如下:

typescript 复制代码
Text() {
  ForEach(item.content, (content: MsgTextImage) => {
    if (content.type === MessageType.Text) {
      Span(content.content)
    }
    if (content.type === MessageType.Image) {
      ImageSpan(content.content)
        .clip(true)
        .objectFit(ImageFit.Contain)    // 图片填充效果设置为Contain,防止图片超出范围
        .size({ width: '20vp', height: '20vp' })
        .margin(2)
        .verticalAlign(ImageSpanAlignment.CENTER)
    }
  }, (item: MsgTextImage, index: number) => `${JSON.stringify(item)}_${JSON.stringify(index)}`)
}
.padding(6)
.borderRadius(4)
.lineSpacing(LengthMetrics.vp(8))   // 设置下每行之间的空格,这样不至于看着很紧凑
.backgroundColor('#ADD8E6')
.constraintSize({ maxWidth: '75%', minHeight: 32 }) // 这里设置下最大宽度和最小高度,消息太长时不要覆盖整个屏幕宽度

点击发送消息时,需对RichEditor组件中的消息转换成其他结构,发送完毕,清理RichEditor输入区域具体代码如下:

typescript 复制代码
private sendMessage() {
  let message: MsgTextImage[] = [];
  richController.getSpans().forEach(span => {
    if ((span as RichEditorTextSpanResult).textStyle !== undefined) {
      message.push({    // 文本消息转换,type为Text,使用Span组件显示
        type: MessageType.Text,
        content: (span as RichEditorTextSpanResult).value
      });
    } else {  
      message.push({    // 图片消息转换,type为Image,使用ImageSpan组件显示
        type: MessageType.Image,
        content: (span as RichEditorImageSpanResult).valueResourceStr
      });
    }
  })
  if (message.length > 0) {
    this.data.push({
      isSelf: this.isSelf,
      content: message
    });
  }
  richController.deleteSpans();
}

实现已发送的消息自动避让

首先在aboutToAppear中设置键盘模式为上抬模式,代码如下:

typescript 复制代码
aboutToAppear(): void {
  this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
}

但仅仅只设置上抬模式还不够,消息数超过屏幕时,仍然看不到最新发送的消息。因此,在点击RichEditor组件唤起输入法时,需要将消息滚动到底部,代码如下:

typescript 复制代码
RichEditor(this.options)
  .width('100%')
  .borderRadius(4)
  .backgroundColor('#08000000')
  .constraintSize({ maxHeight: 128 })
  .placeholder(this.placeHolder, { fontColor: '#4D242E3E', font: { size: 13 } })
  .layoutWeight(1)
  .clip(true)
  .onDidChange(() => {
    this.isHaveMsg = (richController.getSpans().length !== Number(0))
  })
  .onEditingChange(this.editingChangedCb)   // 监听编辑状态是否发生改变,并执行回调

editingChanged = () => {
  this.curMenuAction = EditMenuAction.None;
  // 需要延迟一会再触发,键盘弹起之后再触发,否则无效果
  setTimeout(() => {
    this.listScroller.scrollEdge(Edge.End);
  }, 100)
}

实时语音转文字

  1. 语音转文字需使用麦克风权限,需要在module.json5文件中申明麦克风权限,使用时去请求麦克风权限
typescript 复制代码
// module.json5中申明权限
"requestPermissions": [
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:microphone_reason",
    "usedScene": {
      "abilities": [
        "EntryAbility"
      ],
      "when": "always"
    },
  }
]

// 页面即将加载时,请求麦克风权限
async aboutToAppear(): Promise<void> {
  this.speechRecognizer.intiEngine();
  await requestPermission(['ohos.permission.MICROPHONE'], getContext() as common.UIAbilityContext);
}

// 调用系统API请求所需权限
export async function requestPermission(permissions: Permissions[], context: common.UIAbilityContext): Promise<boolean> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  const result = await atManager.requestPermissionsFromUser(context, permissions);
  return !!result.authResults.length && result.authResults.every(authResults => authResults === 0);
}
  1. 调用系统speechRecognizerAPI进行实时语音识别,具体代码如下:
typescript 复制代码
import { speechRecognizer } from '@kit.CoreSpeechKit';

export class SpeechRecognizer {
  private engineParams: speechRecognizer.CreateEngineParams = {
    language: 'zh-CN',  // 目前系统API只支持设置中文
    online: 1,          // 目前系统API只支持离线模式
    extraParams: { 'locate': 'CN', 'recognizerMode': 'long' }   // 设置recognizerMode为长时模式,设置短时模式时说完一句话会自动结束识别
  };
  private asrEngine?: speechRecognizer.SpeechRecognitionEngine;
  private sessionId: string = 'SpeechRecognizer_' + Date.now();

  public async intiEngine() {
    this.asrEngine = await speechRecognizer.createEngine(this.engineParams);
  }

  // 开始语音识别,并提供回调函数,用于返回结果
  public start(callback: (srr: speechRecognizer.SpeechRecognitionResult) => void = () => {}) {
    this.setListener(callback);
    this.startListening();
  }

  // 停止语音识别
  public stop() {
    this.asrEngine?.finish(this.sessionId);
  }

  public shutdown() {
    this.asrEngine?.shutdown();
  }

  // 启动监听
  private startListening() {
    let recognizerParams: speechRecognizer.StartParams = {
      sessionId: this.sessionId,
      audioInfo: { audioType: 'pcm', sampleRate: 16000, soundChannel: 1, sampleBit: 16 },
      extraParams: { recognitionMode: 0, maxAudioDuration: 60000 }
    }
    this.asrEngine?.startListening(recognizerParams);
  }

  private setListener(callback: (srr: speechRecognizer.SpeechRecognitionResult) => void = () => {}) {
    let listener: speechRecognizer.RecognitionListener = {
      onStart(sessionId: string, eventMessage: string) {
      },
      onEvent(sessionId: string, eventCode: number, eventMessage: string) {
      },
      onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult) {
        // 语音识别到结果后,通过回调函数将识别的结果返回
        callback && callback(result);
      },
      onComplete(sessionId: string, eventMessage: string) {
        // recognizerMode设置为短时模式时,如果仍需继续识别,需要在此处再次调用startListening,启动监听。
      },
      onError(sessionId: string, errorCode: number, errorMessage: string) {
      },
    }
    this.asrEngine?.setListener(listener);
  }
}

根据光标位置插入语音识别的文字

通过getCaretOffset获取到光标所在位置。在插入文字时设置对应的偏移量来达成目的,RichEditor输入框无内容时增加正在识别...文字提示,有内容时增加...内容提示

typescript 复制代码
startSpeechRecognizer() {
  // 输入框无内容时,直接使用提示文本
  this.placeHolder = '正在识别...';
  this.fillPlaceHolder();   // 填充提示文本
  this.speechRecognizer.start((result) => {
    this.insertSpan(result.result);     // 处理语音识别到的结果
    if (result.isFinal) {   // 语音识别完,将caretOffset,speechTextLength重置
      this.caretOffset = richController.getCaretOffset();
      this.speechTextLength = 0;
    }
  })
}

stopSpeechRecognizer() {
  this.placeHolder = '';
  this.speechRecognizer.stop();
  // 结束识别,需将插入的...提示删除
  richController.deleteSpans({ start: this.caretOffset, end: this.caretOffset + 3 });
}

private insertSpan(text: string) {
    if (text.length <= 0) {     // 未识别到内容时,直接return
      return;
    }

    if (this.speechTextLength === 0) {  // 首次识别,直接根据光标位置将识别到的文字插入
      richController.addTextSpan(text, { offset: this.caretOffset });
    } else {
      // 非首次识别,需先将上次识别的文字删除,再填充新识别的文字
      richController.deleteSpans({ start: this.caretOffset, end: this.caretOffset + this.speechTextLength });
      richController.addTextSpan(text, { offset: this.caretOffset });
    }
    this.speechTextLength = text.length;    // 每次记录识别文字的长度
  }

private fillPlaceHolder() {
  this.caretOffset = richController.getCaretOffset();
  if (richController.getSpans().length > 0) {
    // 输入框有内容时,插入...提示,并更新caretOffset位置
    richController.addTextSpan('...', {
    offset: this.caretOffset,
    style: {
        fontColor: '#4D242E3E',
        fontSize: 13,
      }
    })
    richController.setCaretOffset(this.caretOffset);
  }
}
相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax