01MVP 标识01MVP
包文档AI 与语音asr 语音识别

asr 语音识别

自动语音识别(ASR)集成,支持 DashScope 和火山引擎

概述

@mono/asr 提供统一的语音识别接口,支持多个服务提供商。无论是离线音频文件识别还是实时流式识别,都能轻松实现。

核心功能

OpenAI 兼容 API

支持 DashScope(阿里云通义千问)等 OpenAI 兼容的 ASR 服务。

基本使用

import { createOpenAICompatibleAsrClient } from "@mono/asr";

const client = createOpenAICompatibleAsrClient({
  apiKey: process.env.DASHSCOPE_API_KEY,
  model: "qwen3-asr-flash",
  language: "zh",
  enableItn: true,
});

const result = await client.transcribeAudioUrl(
  "https://example.com/audio.mp3"
);

console.log(result.transcript); // "你好世界"

自定义配置

const client = createOpenAICompatibleAsrClient({
  baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  model: "qwen3-asr-flash",
  language: "zh",
  enableItn: true,
  timeoutMs: 30000,
});

火山引擎流式识别

适用于实时语音识别场景。

import { transcribeAudioPcmByVolcengine } from "@mono/asr";

// 音频要求:PCM 格式,16kHz, 16bit, 单声道
const audioPcm = Buffer.from(/* PCM 数据 */);

const transcript = await transcribeAudioPcmByVolcengine(audioPcm);
console.log(transcript);

实战示例

示例 1:音频文件转文字

构建一个音频转文字服务。

import { createOpenAICompatibleAsrClient } from "@mono/asr";

const client = createOpenAICompatibleAsrClient();

export async function transcribeAudioFile(audioUrl: string) {
  try {
    const result = await client.transcribeAudioUrl(audioUrl);

    return {
      success: true,
      text: result.transcript,
      raw: result.raw,
    };
  } catch (error) {
    console.error("识别失败:", error);
    return {
      success: false,
      error: error instanceof Error ? error.message : "未知错误",
    };
  }
}

// 使用
const result = await transcribeAudioFile(
  "https://example.com/meeting.mp3"
);

if (result.success) {
  console.log("会议记录:", result.text);
}

示例 2:多语言识别

支持中英文等多种语言。

import { createOpenAICompatibleAsrClient } from "@mono/asr";

const client = createOpenAICompatibleAsrClient();

export async function transcribeMultiLanguage(
  audioUrl: string,
  language: "zh" | "en" | "ja"
) {
  const result = await client.transcribeAudioUrl(audioUrl, {
    language,
    enableItn: true,
  });

  return {
    language,
    text: result.transcript,
    timestamp: new Date().toISOString(),
  };
}

// 识别中文音频
const zhResult = await transcribeMultiLanguage(audioUrl, "zh");

// 识别英文音频
const enResult = await transcribeMultiLanguage(audioUrl, "en");

示例 3:批量音频处理

并发处理多个音频文件。

import { createOpenAICompatibleAsrClient } from "@mono/asr";

const client = createOpenAICompatibleAsrClient();

export async function batchTranscribe(audioUrls: string[]) {
  console.log(`开始批量识别 ${audioUrls.length} 个音频文件`);

  const results = await Promise.allSettled(
    audioUrls.map(async (url, index) => {
      console.log(`[${index + 1}/${audioUrls.length}] 识别: ${url}`);
      const result = await client.transcribeAudioUrl(url);
      return {
        url,
        transcript: result.transcript,
      };
    })
  );

  return results.map((result, index) => ({
    url: audioUrls[index],
    success: result.status === "fulfilled",
    transcript: result.status === "fulfilled" ? result.value.transcript : null,
    error: result.status === "rejected" ? result.reason.message : null,
  }));
}

// 使用
const audioUrls = [
  "https://example.com/audio1.mp3",
  "https://example.com/audio2.mp3",
  "https://example.com/audio3.mp3",
];

const results = await batchTranscribe(audioUrls);

results.forEach((result, index) => {
  if (result.success) {
    console.log(`✓ [${index + 1}] ${result.transcript}`);
  } else {
    console.error(`✗ [${index + 1}] ${result.error}`);
  }
});

示例 4:实时语音识别

使用火山引擎进行实时识别。

import { transcribeAudioPcmByVolcengine } from "@mono/asr";
import { readFileSync } from "fs";

export async function realtimeTranscribe(pcmFilePath: string) {
  try {
    // 读取 PCM 音频文件
    const audioPcm = readFileSync(pcmFilePath);

    console.log(`音频大小: ${audioPcm.length} 字节`);
    console.log("开始实时识别...");

    const startTime = Date.now();
    const transcript = await transcribeAudioPcmByVolcengine(audioPcm);
    const duration = Date.now() - startTime;

    console.log(`识别完成 (${duration}ms)`);

    return {
      transcript,
      duration,
      audioSize: audioPcm.length,
    };
  } catch (error) {
    console.error("实时识别失败:", error);
    throw error;
  }
}

// 使用
const result = await realtimeTranscribe("./audio.pcm");
console.log("识别结果:", result.transcript);

示例 5:带重试的识别

增加错误重试机制。

import { createOpenAICompatibleAsrClient } from "@mono/asr";

const client = createOpenAICompatibleAsrClient();

export async function transcribeWithRetry(
  audioUrl: string,
  maxRetries = 3,
  retryDelay = 1000
) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`尝试 ${attempt}/${maxRetries}: ${audioUrl}`);

      const result = await client.transcribeAudioUrl(audioUrl);

      console.log(`✓ 识别成功 (尝试 ${attempt})`);
      return result.transcript;
    } catch (error) {
      console.error(`✗ 尝试 ${attempt} 失败:`, error);

      if (attempt === maxRetries) {
        throw new Error(`识别失败,已重试 ${maxRetries} 次`);
      }

      // 指数退避
      const delay = retryDelay * attempt;
      console.log(`等待 ${delay}ms 后重试...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error("识别失败");
}

// 使用
try {
  const transcript = await transcribeWithRetry(audioUrl);
  console.log("最终结果:", transcript);
} catch (error) {
  console.error("所有重试均失败:", error);
}

示例 6:音频转字幕

将音频转换为字幕格式。

import { createOpenAICompatibleAsrClient } from "@mono/asr";

const client = createOpenAICompatibleAsrClient();

interface SubtitleSegment {
  index: number;
  startTime: string;
  endTime: string;
  text: string;
}

export async function generateSubtitles(
  audioUrl: string,
  segmentDuration = 5
): Promise<SubtitleSegment[]> {
  const result = await client.transcribeAudioUrl(audioUrl);
  const text = result.transcript;

  // 简单分段(实际应用中可能需要更复杂的逻辑)
  const words = text.split(/\s+/);
  const wordsPerSegment = Math.ceil(words.length / segmentDuration);
  const segments: SubtitleSegment[] = [];

  for (let i = 0; i < words.length; i += wordsPerSegment) {
    const segmentWords = words.slice(i, i + wordsPerSegment);
    const index = Math.floor(i / wordsPerSegment) + 1;
    const startSeconds = (index - 1) * segmentDuration;
    const endSeconds = index * segmentDuration;

    segments.push({
      index,
      startTime: formatTime(startSeconds),
      endTime: formatTime(endSeconds),
      text: segmentWords.join(" "),
    });
  }

  return segments;
}

function formatTime(seconds: number): string {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = seconds % 60;
  return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
}

function pad(num: number): string {
  return num.toString().padStart(2, "0");
}

// 使用
const subtitles = await generateSubtitles(audioUrl);

// 输出 SRT 格式
subtitles.forEach(segment => {
  console.log(segment.index);
  console.log(`${segment.startTime} --> ${segment.endTime}`);
  console.log(segment.text);
  console.log();
});

配置指南

环境变量配置

创建 .env.local 文件:

# DashScope(阿里云通义千问)
DASHSCOPE_API_KEY=your-dashscope-api-key
OPENAI_ASR_MODEL=qwen3-asr-flash
OPENAI_ASR_LANGUAGE=zh
OPENAI_ASR_ENABLE_ITN=true

# 火山引擎
VOLCENGINE_APP_ID=your-app-id
VOLCENGINE_ACCESS_TOKEN=your-access-token
VOLCENGINE_ENABLE_ITN=true
VOLCENGINE_ENABLE_PUNC=true

提供商选择

DashScope(推荐)

优势:

  • 支持多种音频格式
  • 高准确率
  • 简单易用
  • OpenAI 兼容接口

适用场景:

  • 离线音频文件识别
  • 批量处理
  • 需要高准确率
const client = createOpenAICompatibleAsrClient({
  apiKey: process.env.DASHSCOPE_API_KEY,
  model: "qwen3-asr-flash", // 快速模型
  // model: "qwen3-asr-pro", // 高精度模型
});

火山引擎

优势:

  • 实时流式识别
  • 低延迟
  • 支持长音频

适用场景:

  • 实时语音识别
  • 语音对话系统
  • 需要低延迟
// 通过环境变量配置
const transcript = await transcribeAudioPcmByVolcengine(audioPcm);

错误处理

常见错误

import { createOpenAICompatibleAsrClient } from "@mono/asr";

const client = createOpenAICompatibleAsrClient();

try {
  const result = await client.transcribeAudioUrl(audioUrl);
  console.log(result.transcript);
} catch (error) {
  if (error instanceof Error) {
    // API 密钥错误
    if (error.message.includes("缺少")) {
      console.error("请配置 DASHSCOPE_API_KEY 环境变量");
    }
    // 请求失败
    else if (error.message.includes("请求失败")) {
      console.error("API 请求失败,请检查网络连接");
    }
    // 识别结果为空
    else if (error.message.includes("结果为空")) {
      console.error("音频无法识别,请检查音频质量");
    }
    // 其他错误
    else {
      console.error("未知错误:", error.message);
    }
  }
}

火山引擎错误处理

import { transcribeAudioPcmByVolcengine } from "@mono/asr";

try {
  const transcript = await transcribeAudioPcmByVolcengine(audioPcm);
  console.log(transcript);
} catch (error) {
  if (error instanceof Error) {
    // 凭证未配置
    if (error.message.includes("未配置")) {
      console.error("请配置 VOLCENGINE_APP_ID 和 VOLCENGINE_ACCESS_TOKEN");
    }
    // 音频数据无效
    else if (error.message.includes("音频数据为空")) {
      console.error("音频数据无效,请检查 PCM 格式");
    }
    // 连接失败
    else if (error.message.includes("连接失败")) {
      console.error("WebSocket 连接失败,请检查网络");
    }
    // 其他错误
    else {
      console.error("识别错误:", error.message);
    }
  }
}

性能优化

1. 选择合适的模型

// 快速模型(低延迟,适合实时场景)
const fastClient = createOpenAICompatibleAsrClient({
  model: "qwen3-asr-flash",
});

// 高精度模型(高准确率,适合离线处理)
const accurateClient = createOpenAICompatibleAsrClient({
  model: "qwen3-asr-pro",
});

2. 并发控制

import pLimit from "p-limit";

const limit = pLimit(5); // 最多 5 个并发请求

const results = await Promise.all(
  audioUrls.map(url =>
    limit(() => client.transcribeAudioUrl(url))
  )
);

3. 超时控制

const client = createOpenAICompatibleAsrClient({
  timeoutMs: 30000, // 30 秒超时
});

4. 缓存结果

import { createMemoryCache } from "@mono/cache";

const cache = createMemoryCache();

async function transcribeWithCache(audioUrl: string) {
  const cached = cache.get<string>(audioUrl);
  if (cached) {
    console.log("使用缓存结果");
    return cached;
  }

  const result = await client.transcribeAudioUrl(audioUrl);
  cache.set(audioUrl, result.transcript, 3600); // 缓存 1 小时

  return result.transcript;
}

最佳实践

1. 音频预处理

确保音频格式正确:

# 使用 FFmpeg 转换音频
ffmpeg -i input.mp3 -ar 16000 -ac 1 -f s16le output.pcm

2. 结果验证

function validateTranscript(transcript: string): boolean {
  // 检查是否为空
  if (!transcript || transcript.trim().length === 0) {
    return false;
  }

  // 检查最小长度
  if (transcript.length < 2) {
    return false;
  }

  return true;
}

const result = await client.transcribeAudioUrl(audioUrl);
if (!validateTranscript(result.transcript)) {
  throw new Error("识别结果无效");
}

3. 日志记录

async function transcribeWithLogging(audioUrl: string) {
  const startTime = Date.now();
  console.log(`[ASR] 开始识别: ${audioUrl}`);

  try {
    const result = await client.transcribeAudioUrl(audioUrl);
    const duration = Date.now() - startTime;

    console.log(`[ASR] 识别成功 (${duration}ms)`);
    console.log(`[ASR] 文本长度: ${result.transcript.length}`);

    return result;
  } catch (error) {
    const duration = Date.now() - startTime;
    console.error(`[ASR] 识别失败 (${duration}ms):`, error);
    throw error;
  }
}

4. 错误监控

import { createOpenAICompatibleAsrClient } from "@mono/asr";

const client = createOpenAICompatibleAsrClient();

let successCount = 0;
let errorCount = 0;

export async function transcribeWithMetrics(audioUrl: string) {
  try {
    const result = await client.transcribeAudioUrl(audioUrl);
    successCount++;
    return result;
  } catch (error) {
    errorCount++;
    console.error(`错误率: ${(errorCount / (successCount + errorCount) * 100).toFixed(2)}%`);
    throw error;
  }
}

故障排除

问题 1:API 密钥错误

症状: 缺少 OPENAI_API_KEY / OPENAI_ASR_API_KEY(或 DASHSCOPE_API_KEY)配置

解决方案:

  1. 检查 .env.local 文件是否存在
  2. 确认环境变量名称正确
  3. 重启开发服务器
# 检查环境变量
echo $DASHSCOPE_API_KEY

问题 2:识别结果为空

症状: 语音识别结果为空

解决方案:

  1. 检查音频文件是否有效
  2. 确认音频格式是否支持
  3. 尝试使用不同的模型
  4. 检查音频质量

问题 3:火山引擎连接失败

症状: 火山引擎连接失败

解决方案:

  1. 检查 VOLCENGINE_APP_IDVOLCENGINE_ACCESS_TOKEN
  2. 确认网络连接正常
  3. 检查防火墙设置
  4. 验证凭证是否过期

问题 4:音频格式不支持

症状: 识别失败或结果不准确

解决方案:

使用 FFmpeg 转换音频格式:

# 转换为 PCM(火山引擎)
ffmpeg -i input.mp3 -ar 16000 -ac 1 -f s16le output.pcm

# 转换为 MP3(DashScope)
ffmpeg -i input.wav -ar 16000 -ac 1 output.mp3

问题 5:超时错误

症状: 请求超时

解决方案:

增加超时时间:

const client = createOpenAICompatibleAsrClient({
  timeoutMs: 120000, // 增加到 120 秒
});

相关资源

下一步