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

鸿蒙场景化示例代码技术工程师
发布于 2025-4-17 09:28
8969浏览
0收藏

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

介绍

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

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

效果预览

实现实时语音转文字功能鸿蒙示例代码-鸿蒙开发者社区

使用说明

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

实现思路

聊天页面左右布局

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

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)}`)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

图文混排消息显示

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

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 }) // 这里设置下最大宽度和最小高度,消息太长时不要覆盖整个屏幕宽度
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

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

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();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

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

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

aboutToAppear(): void {
  this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
}
  • 1.
  • 2.
  • 3.

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

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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

实时语音转文字

  1. 语音转文字需使用麦克风权限,需要在module.json5文件中申明麦克风权限,使用时去请求麦克风权限
// 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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  1. 调用系统speechRecognizerAPI进行实时语音识别,具体代码如下:
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);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.

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

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

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);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
收藏
回复
举报


回复
    相关推荐