1.背景:我司AI项目聊天对话框实现语音录入
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配置
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/, ''),
},
},
},