语音转文字录入,ffmpeg 实现音频格式处理

323 阅读2分钟

1.背景:我司AI项目聊天对话框实现语音录入

image.png

2.实现方案: 通过浏览器api实现对语音录入(浏览器默认格式一般为audio/webm)-> 将语音文件上传至cos或取文件链接 -> 将文件链接转给后端(去调用腾讯云的语音识别API)-> 返回给前端识别后的文字

3.遇到的坑:1.腾讯云语音识别文件限制(一般只支持wav,pcm,mp3),即使在生成音频文件时指定文件格式{ type: 'audio/wav' },但是给到腾讯云还是会提示文件格式不支持,文件本质还是webm, 2.文件格式转换 ffmpeg 使用问题

4.实现代码


 "@ffmpeg/ffmpeg": "^0.12.15",    
 "@ffmpeg/util": "^0.12.2",

useCosUploader的实现 可以看上一篇文件上传的二次封装


import { getAsrString } from '@/apis/user'
import { useCosUploader } from '@/utils/cos'
import { convertAudioToWav } from '@/utils/ffmpeg'

interface UseRecorderOptions {
  onEnd?: (text: string) => void
}

export function useRecorder(options: UseRecorderOptions = {}) {
  const { onEnd } = options

  let recorder: MediaRecorder | null = null
  let chunks: Blob[] = []

  const isRecording = ref(false)
  const isLoading = ref(false) // 是否正在加载(录音过程中)
  const recordingText = ref('')

  // 创建试听链接并添加到页面
  // const createPreview = (file: File) => {
  //   const url = URL.createObjectURL(file)
  //   console.log('🚀 ~ createPreview ~ url:', url)
  //   // 清理之前的试听播放器(可选)
  //   const existingAudio = document.getElementById('audio-preview')
  //   // eslint-disable-next-line style/max-statements-per-line
  //   if (existingAudio) { existingAudio.remove() }
  //   const audio = new Audio(url)
  //   audio.controls = true
  //   audio.id = 'audio-preview'
  //   document.body.appendChild(audio)
  // }

  const startRecording = async () => {
    // eslint-disable-next-line style/max-statements-per-line
    if (isRecording.value) { return }

    isLoading.value = true

    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
      recorder = new MediaRecorder(stream, { mimeType: 'audio/webm' })
      chunks = []

      recorder.ondataavailable = (event) => {
        if (event.data.size > 0) {
          chunks.push(event.data)
        }
      }

      recorder.onstop = async () => {
        try {
          const audioBlob = new Blob(chunks, { type: 'audio/webm' })
          // 停止录音时恢复状态
          isRecording.value = false
          isLoading.value = false

          const wavBlob = await convertAudioToWav(audioBlob)
          const wavFile = new File([wavBlob], `recording.wav`, { type: 'audio/wav' })
          // createPreview(audioFile)
          // 上传文件转文字
          const url = await useCosUploader({ file: wavFile })
          console.log('🚀 ~ recorder.onstop= ~ url:', url)
          const encodedUrl = encodeURIComponent(url)
          const text = await getAsrString(encodedUrl)
          recordingText.value = text
          console.log('🚀 ~ recorder.onstop= ~ text:', text)
          if (text && onEnd) {
            onEnd(text)
          }
        } catch (err) {
          console.log('err', err)
          ElMessage.error('识别失败')
        } finally {
          // 停止录音时恢复状态
          isRecording.value = false
          isLoading.value = false
        }
      }

      recorder.start()
      isRecording.value = true
      console.log('🎙️ 录音开始')
    } catch (err) {
      console.error('🚫 无法获取麦克风权限', err)
    }
  }

  const stopRecording = () => {
    if (recorder && recorder.state === 'recording') {
      recorder.stop()
    }
  }

  const toggleRecording = () => {
    isRecording.value ? stopRecording() : startRecording()
  }

  return {
    isRecording,
    recordingText,
    startRecording,
    stopRecording,
    toggleRecording,
  }
}

文件格式转换


import { FFmpeg } from '@ffmpeg/ffmpeg'
import { fetchFile, toBlobURL } from '@ffmpeg/util'

// const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core-mt@0.12.9/dist/esm'
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm'

/**
 * 将 WebM/Opus 格式音频 Blob 转换为 WAV 格式
 * @param blob 录音生成的音频 Blob(audio/webm)
 * @returns 转换后的 audio/wav 格式 Blob
 */
// 初始化 ffmpeg 实例
const ffmpeg = new FFmpeg()

export async function convertAudioToWav(blob: Blob): Promise<Blob> {
//   // 加载 ffmpeg 核心代码
  if (!ffmpeg.loaded) {
    await ffmpeg.load({
      coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
      wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
    })
  }
  // 写入 webm 文件
  await ffmpeg.writeFile('input.webm', await fetchFile(blob))
  // 转换为 16bit PCM、16kHz、单声道 wav
  await ffmpeg.exec([
    '-i',
    'input.webm', // 输入文件
    '-ar',
    '16000', // 设置采样率为 16kHz
    '-ac',
    '1', // 设置为单声道
    '-sample_fmt',
    's16', // 设置采样格式为 16bit PCM
    'output.wav', // 输出文件
  ])
  // 读取输出文件内容
  const data = await ffmpeg.readFile('output.wav')
  // 返回新的 Blob,供上传等操作使用
  return new Blob([data], { type: 'audio/wav' })
}


vite配置

image.png


    optimizeDeps: {
      exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util'],
    },
    server: {
      proxy: {
        '/ffmpeg': {
          target: 'https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/esm',
          changeOrigin: true,
          rewrite: path => path.replace(/^\/ffmpeg/, ''),
        },

      },
    },