跳转到主要内容
本系统支持接入 4 个国内主流语音引擎,实现语音转文字(ASR)和文字转语音(TTS)功能.每个引擎都已完整接入并测试通过.

支持的引擎

1. 百度智能云

2. 讯飞开放平台

3. 火山引擎

4. 阿里云


接入所需配置

百度智能云

ASR(语音转文字)

需要配置以下环境变数:
  • SUPERUN_BAIDU_API_KEY - API Key
  • SUPERUN_BAIDU_SECRET_KEY - Secret Key

TTS(文字转语音)

需要配置以下环境变数:
  • SUPERUN_BAIDU_API_KEY - API Key
  • SUPERUN_BAIDU_SECRET_KEY - Secret Key
音色选项:
  • 0 - 度小宇(女)
  • 1 - 度小美(男)
  • 3 - 度逍遥(女)
  • 4 - 度丫丫(男)

讯飞开放平台

ASR(语音转文字)

需要配置以下环境变数:
  • SUPERUN_XUNFEI_APP_ID - App ID
  • SUPERUN_XUNFEI_API_KEY - API Key
  • SUPERUN_XUNFEI_API_SECRET - API Secret
技术特点: 使用 WebSocket 协议进行实时语音识别.

TTS(文字转语音)

需要配置以下环境变数:
  • SUPERUN_XUNFEI_APP_ID - App ID
  • SUPERUN_XUNFEI_API_KEY - API Key
  • SUPERUN_XUNFEI_API_SECRET - API Secret
音色选项:
  • xiaoyan - 讯飞小燕(女)
  • xiaoyu - 讯飞小宇(男)
  • xiaomei - 讯飞小美(女)
  • xiaoqi - 讯飞小琪(男)
技术特点: 使用 WebSocket 协议进行语音合成.

火山引擎

ASR(语音转文字)

需要配置以下环境变数:
  • SUPERUN_VOLCANO_APP_ID - App ID
  • SUPERUN_VOLCANO_ACCESS_TOKEN - Access Token
  • SUPERUN_VOLCANO_SECRET_KEY - Secret Key(WebSocket 鉴权用)
  • SUPERUN_VOLCANO_ASR_CLUSTER - ASR Cluster(可选,预设:volcengine_input_common
技术特点: 使用 WebSocket 二进制协议,支持 Gzip 压缩,支持分片传输.

TTS(文字转语音)

需要配置以下环境变数:
  • SUPERUN_VOLCANO_APP_ID - App ID
  • SUPERUN_VOLCANO_ACCESS_TOKEN - Access Token
音色选项:
  • BV700_V2_streaming - 清新女声
  • BV001_V2_streaming - 通用男声
  • BV705_streaming - 甜美女声
  • BV701_V2_streaming - 醇厚男声

阿里云

ASR(语音转文字)

需要配置以下环境变数:
  • SUPERUN_ALIYUN_ACCESS_KEY_ID - Access Key ID
  • SUPERUN_ALIYUN_ACCESS_KEY_SECRET - Access Key Secret
  • SUPERUN_ALIYUN_APP_KEY - App Key
技术特点: 使用 REST API,支持 HMAC-SHA1 签名认证,使用 Token 机制. 限制: 单次识别音频长度 ≤ 60 秒.

TTS(文字转语音)

需要配置以下环境变数:
  • SUPERUN_ALIYUN_ACCESS_KEY_ID - Access Key ID
  • SUPERUN_ALIYUN_ACCESS_KEY_SECRET - Access Key Secret
  • SUPERUN_ALIYUN_APP_KEY - App Key
音色选项:
  • aixia - 艾夏(女)
  • aiwei - 艾偉(男)
  • aida - 艾达(女)
  • kenny - 肯尼(男)
技术特点: 使用 REST API,支持 HMAC-SHA1 签名认证.

配置方式

Supabase Edge Functions(生产环境)

在 Supabase 项目中配置环境变数:
# 百度
supabase secrets set SUPERUN_BAIDU_API_KEY=your_api_key
supabase secrets set SUPERUN_BAIDU_SECRET_KEY=your_secret_key

# 讯飞
supabase secrets set SUPERUN_XUNFEI_APP_ID=your_app_id
supabase secrets set SUPERUN_XUNFEI_API_KEY=your_api_key
supabase secrets set SUPERUN_XUNFEI_API_SECRET=your_api_secret

# 火山引擎
supabase secrets set SUPERUN_VOLCANO_APP_ID=your_app_id
supabase secrets set SUPERUN_VOLCANO_ACCESS_TOKEN=your_access_token
supabase secrets set SUPERUN_VOLCANO_SECRET_KEY=your_secret_key
supabase secrets set SUPERUN_VOLCANO_ASR_CLUSTER=volcengine_input_common

# 阿里云
supabase secrets set SUPERUN_ALIYUN_ACCESS_KEY_ID=your_access_key_id
supabase secrets set SUPERUN_ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
supabase secrets set SUPERUN_ALIYUN_APP_KEY=your_app_key

代码实现架构

前端组件

ASR 模组(语音转文字)

// src/components/mobile/ASRModule.tsx
const ASRModule = ({ engine = "baidu" }: ASRModuleProps) => {
  const callASRAPI = async (audioData: string) => {
    const { data, error } = await supabase.functions.invoke('asr-convert', {
      body: {
        engine: engine,
        audioData: audioData,
      }
    });
    
    if (data.success) {
      setResult(data.result.text);
      setMetrics({
        time: Math.round(data.result.duration || 0),
        confidence: Math.round((data.result.confidence || 0) * 100),
        rate: "16k"
      });
    }
  };
  
  // ... 录音和文件上传逻辑
};

TTS 模组(文字转语音)

// src/components/mobile/TTSModule.tsx
const TTSModule = ({ engine = "baidu" }: TTSModuleProps) => {
  const callTTSAPI = async () => {
    const { data, error } = await supabase.functions.invoke('tts-convert', {
      body: {
        engine: engine,
        text: text,
        voice: selectedVoice,
        speed: speed[0],
        volume: volume[0],
      }
    });
    
    if (data.success) {
      setAudioUrl(data.result.audioUrl);
      setStatus("complete");
    }
  };
  
  // ... 合成逻辑
};

引擎选择器

// src/components/mobile/EngineSelector.tsx
const engines = [
  { id: "baidu", name: "百度", shortName: "BD" },
  { id: "xunfei", name: "讯飞", shortName: "XF" },
  { id: "volcano", name: "火山", shortName: "HS" },
  { id: "aliyun", name: "阿里云", shortName: "ALI" },
];

后端实现(Supabase Edge Functions)

ASR 转换服务

文件位置: supabase/functions/asr-convert/index.ts 核心逻辑:
  1. 根据 engine 参数选择对应的引擎实现
  2. 从环境变数读取对应的 API 凭证
  3. 调用各引擎的 ASR API
  4. 返回标准化的识别结果
百度实现:
async function callBaiduASR(apiKey: string, secretKey: string, audioData: string) {
  // 1. 获取 Access Token
  const accessToken = await getBaiduAccessToken(apiKey, secretKey);
  
  // 2. API URL - 不要带任何参数
  const apiUrl = 'https://vop.baidu.com/server_api';
  
  // 3. 请求体 - token 必须在这里
  const requestBody = {
    format: "wav",           // 音频格式
    rate: 16000,             // 採样率(必须是 number 类型)
    channel: 1,              // 声道数
    cuid: userId,            // 用户标识
    token: accessToken,      // ← 关鍵:token 放请求体内
    speech: base64Audio,     // Base64 编码的音频
    len: audioByteLength,    // WAV 文件的实际字节数(必须是 number 类型)
    // 不要使用 dev_pid
  };
  
  // 4. 发送请求
  const response = await fetch(apiUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(requestBody),
  });
  
  return { text: result.result[0], confidence: 0.95 };
}
讯飞实现:
async function callXunfeiASR(appId: string, apiKey: string, apiSecret: string, audioData: string) {
  // 1. 构建 WebSocket 鑑权 URL(HMAC-SHA256 签名)
  const wsUrl = buildWebSocketAuthUrl(host, path, apiKey, apiSecret);
  
  // 2. 建立 WebSocket 连接
  const ws = new WebSocket(wsUrl);
  
  // 3. 发送识别请求
  ws.send(JSON.stringify({
    common: { app_id: appId },
    business: { language: "zh_cn", domain: "iat", accent: "mandarin" },
    data: { status: 2, format: "audio/L16;rate=16000", audio: base64Audio }
  }));
  
  // 4. 接收並解析结果
  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    // 解析识别结果...
  };
}
火山引擎实现:
// 使用 WebSocket 二进制协议
async function callVolcanoASR(appId: string, accessToken: string, audioData: string) {
  // 1. 构建 WebSocket URL
  const wsUrl = `wss://openspeech.bytedance.com/api/v2/asr?appid=${appId}&token=${accessToken}&cluster=${cluster}`;
  
  // 2. 建立连接(binaryType 设为 "arraybuffer")
  const ws = new WebSocket(wsUrl);
  ws.binaryType = "arraybuffer";
  
  // 3. 发送 Full Client Request(二进制协议,Gzip 压缩)
  const fullRequestMessage = await buildMessage(
    0b0001,  // message_type: full client request
    0b0000,  // flags: 非最後包
    0b0001,  // serialization: JSON
    0b0001,  // compression: Gzip
    jsonBytes
  );
  ws.send(fullRequestMessage);
  
  // 4. 分片发送音频数据
  const audioMessage = await buildMessage(
    0b0010,  // message_type: audio only
    0b0010,  // flags: 最後包
    0b0000,  // serialization: none
    0b0001,  // compression: Gzip
    audioChunk
  );
  ws.send(audioMessage);
  
  // 5. 解析二进制响应
  ws.onmessage = async (event) => {
    const result = await parseServerResponse(event.data);
    // 解析识别结果...
  };
}
阿里云实现:
async function callAliyunASR(accessKeyId: string, accessKeySecret: string, appKey: string, audioData: string) {
  // 1. 获取 Token(HMAC-SHA1 签名)
  const token = await getAliyunToken(accessKeyId, accessKeySecret);
  
  // 2. 发送 REST API 请求
  const response = await fetch('https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr?appkey=...', {
    method: 'POST',
    headers: {
      'X-NLS-Token': token,
      'Content-Type': 'application/octet-stream'
    },
    body: audioBytes  // 二进制音频数据
  });
  
  return { text: result.result, confidence: 0.94 };
}

TTS 转换服务

文件位置: supabase/functions/tts-convert/index.ts 核心逻辑:
  1. 根据 engine 参数选择对应的引擎实现
  2. 从环境变数读取对应的 API 凭证
  3. 根据 voice 参数映射到各引擎的音色代码
  4. 调用各引擎的 TTS API
  5. 返回 base64 编码的音频数据
音色映射:
const voiceMapping: Record<string, Record<string, { code: string; name: string }>> = {
  baidu: {
    female_1: { code: "0", name: "度小宇" },
    male_1: { code: "1", name: "度小美" },
    // ...
  },
  xunfei: {
    female_1: { code: "xiaoyan", name: "讯飞小燕" },
    // ...
  },
  volcano: {
    female_1: { code: "BV700_V2_streaming", name: "清新女声" },
    // ...
  },
  aliyun: {
    female_1: { code: "aixia", name: "艾夏" },
    // ...
  },
};
百度实现:
async function callBaiduTTS(apiKey: string, secretKey: string, text: string, voice: string, speed: number, volume: number) {
  const accessToken = await getBaiduAccessToken(apiKey, secretKey);
  
  const params = new URLSearchParams({
    tex: text,
    tok: accessToken,
    lan: "zh",
    spd: Math.round(speed * 5).toString(),
    vol: Math.round((volume / 100) * 15).toString(),
    per: voiceCode,
    aue: "3",  // MP3 格式
  });
  
  const response = await fetch(`https://tsn.baidu.com/text2audio?${params.toString()}`);
  const audioBuffer = await response.arrayBuffer();
  
  // 转换为 base64
  const audioBase64 = bufferToBase64(audioBuffer);
  return { audioUrl: `data:audio/mp3;base64,${audioBase64}` };
}
讯飞实现:
async function callXunfeiTTS(appId: string, apiKey: string, apiSecret: string, text: string, voice: string, speed: number, volume: number) {
  // 使用 WebSocket 协议
  const wsUrl = buildWebSocketAuthUrl(host, path, apiKey, apiSecret);
  const ws = new WebSocket(wsUrl);
  
  ws.send(JSON.stringify({
    common: { app_id: appId },
    business: {
      aue: "lame",  // MP3 格式
      vcn: voiceCode,
      speed: Math.round(speed * 50),
      volume: Math.round(volume * 100 / 80),
    },
    data: {
      status: 2,
      text: btoa(unescape(encodeURIComponent(text)))
    }
  }));
  
  // 接收音频数据块並合併
  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.data && data.data.audio) {
      audioChunks.push(data.data.audio);
    }
    if (data.data && data.data.status === 2) {
      // 合成完成
      const audioBase64 = audioChunks.join('');
      return { audioUrl: `data:audio/mp3;base64,${audioBase64}` };
    }
  };
}
火山引擎实现:
async function callVolcanoTTS(appId: string, accessToken: string, text: string, voice: string, speed: number, volume: number) {
  const response = await fetch('https://openspeech.bytedance.com/api/v1/tts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`
    },
    body: JSON.stringify({
      app: { appid: appId, token: accessToken, cluster: "volcano_tts" },
      audio: {
        voice_type: voiceCode,
        encoding: "mp3",
        speed_ratio: speed,
        volume_ratio: volume / 100,
      },
      request: { text: text, text_type: "plain" }
    })
  });
  
  const result = await response.json();
  // 返回 base64 音频
  return { audioUrl: `data:audio/mp3;base64,${result.data}` };
}
阿里云实现:
async function callAliyunTTS(accessKeyId: string, accessKeySecret: string, appKey: string, text: string, voice: string, speed: number, volume: number) {
  const token = await getAliyunToken(accessKeyId, accessKeySecret);
  
  const response = await fetch('https://nls-gateway.cn-shanghai.aliyuncs.com/stream/v1/tts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-NLS-Token': token,
    },
    body: JSON.stringify({
      appkey: appKey,
      text: text,
      voice: voiceCode,
      format: "mp3",
      sample_rate: 16000,
      volume: volume,
      speech_rate: Math.round((speed - 0.5) * 200),
    })
  });
  
  const audioBuffer = await response.arrayBuffer();
  const audioBase64 = bufferToBase64(audioBuffer);
  return { audioUrl: `data:audio/mp3;base64,${audioBase64}` };
}

百度 ASR 常见错误及解决方案

错误码 3311: param rate invalid

这是最常见的错误,原因通常是以下几点:
问题解决方案
Token 放置位置错误Token 必须放在请求体内,不要放在 URL 参数中
cuid 重复cuid 只放请求体内,不要在 URL 中重复
使用 dev_pid不要使用 dev_pid 参数,让百度自动检测语言
rate 类型错误确保 rate 是 number 类型,不是 string
len 计算错误len 必须是 WAV 文件的实际字节数

正确的 len 参数计算

从 Base64 字符串计算实际字节数:
// 从 Base64 字符串计算实际字节数
const padding = (base64Audio.match(/=/g) || []).length;
const audioByteLength = Math.floor((base64Audio.length * 3) / 4) - padding;

// 验证:audioByteLength 应該等於 WAV 文件的 blob.size

前端音频处理要点

1. 录音格式

浏览器通常是 webm/opus:
const mimeType = "audio/webm;codecs=opus";

2. 必须重采样到 16kHz(百度要求)

const offlineContext = new OfflineAudioContext(
  1,                    // 單声道
  targetLength,         
  16000                 // 目标採样率
);

3. 转换为 16bit PCM

const pcm16 = new Int16Array(samples.length);
for (let i = 0; i < samples.length; i++) {
  const s = Math.max(-1, Math.min(1, samples[i]));
  pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}

4. 添加 WAV 头(44 字节)

const wavHeader = {
  sampleRate: 16000,
  numChannels: 1,
  bitsPerSample: 16,
  byteRate: 32000,      // 16000 * 1 * 16 / 8
  blockAlign: 2,        // 1 * 16 / 8
};

环境变数配置

在 Supabase Edge Function Secrets 中配置:
# Supabase Edge Function Secrets
SUPERUN_BAIDU_API_KEY=你的百度API_Key
SUPERUN_BAIDU_SECRET_KEY=你的百度Secret_Key
获取方式:百度智能云控制台 → 语音技术 → 创建应用

调试检查清单

遇到 3311 错误时,按顺序检查:
  1. ✅ Token 是否在请求体内(不是 URL 参数)
  2. ✅ rate 是否是 number 类型(typeof rate === 'number'
  3. ✅ len 是否等于 WAV 文件实际大小
  4. ✅ 是否移除了 dev_pid 参数
  5. ✅ WAV 头中的采样率是否为 16000
  6. ✅ 音频时长是否在 0.5-60 秒范围内

完整请求示例

正确 ✓:
{
  format: "wav",
  rate: 16000,          // number 类型
  channel: 1,
  cuid: "user_001",
  token: "24.xxx...",   // 在请求体内
  speech: "UklGR...",   // Base64
  len: 63404            // number 类型,实际字节数
}
错误 ✗:
{
  format: "wav",
  rate: "16000",        // ← 错误:string 类型
  channel: 1,
  cuid: "user_001",
  dev_pid: 1737,        // ← 错误:不要使用
  speech: "UklGR...",
  len: "63404"          // ← 错误:string 类型
}
// URL: ?token=xxx      // ← 错误:token 不要放 URL

技术要点

ASR(语音转文字)

  1. 音频格式统一: 所有引擎均使用 WAV 格式,16kHz 採样率,单声道
  2. Base64 编码: 音频数据在前端转换为 base64 后传递到后端
  3. 协议差异:
    • 百度,阿里云:REST API
    • 讯飞,火山引擎:WebSocket 协议
  4. 结果标准化: 统一返回 { text, confidence, duration } 格式

TTS(文字转语音)

  1. 音色映射: 前端使用统一的音色 ID(female_1, male_1 等),后端映射到各引擎的实际音色代码
  2. 参数转换:
    • 语速:前端范圍 0.5-2.0x,各引擎转换为对应范圍
    • 音量:前端范圍 0-100%,各引擎转换为对应范圍
  3. 输出格式: 所有引擎统一返回 MP3 格式的 base64 编码音频
  4. 协议差异:
    • 百度,阿里云,火山引擎:REST API
    • 讯飞:WebSocket 协议(需要接收多个音频块)

测试建议

  1. API 凭证测试: 确保所有环境变数正确配置
  2. 音频格式测试: 测试不和格式的音频文件(WAV,MP3,M4A)
  3. 时长限制测试: 特别注意阿里云的 60 秒限制
  4. 错误处理测试: 测试网络错误,API 错误等异常情况
  5. 并发测试: 测试多个用户同时使用不和引擎的情况

注意事项

  1. 费用控制: 各引擎都有各自的计费规則,注意监控 API 调用量
  2. 速率限制: 各引擎都有调用频率限制,注意避免超限
  3. 音频大小: 建议限制上传音频文件大小(如 10MB)
  4. 超时设置: WebSocket 连接设置合理的超时时间(如 30 秒)
  5. 错误日志: 记录详细的错误信息,便于排查问题

superun 官方网站

浏览官网,了解更多功能与使用范例.