用 CloudBase 云函数代理 Deepgram 语音转文字
一句话定义:在 CloudBase 云函数里用
@deepgram/sdk调 Deepgram 的nova-3模型,从云存储里拉一段音频做 STT,拿到带 punctuation/diarization/timestamp 的转写结果,直接写进数据库。预计耗时:30 分钟 | 难度:进阶
适用场景
- 适用:有会议录音/语音笔记/客服通话要批量转文字,自己不想维护 Whisper 推理机器
- 适用:需要"谁在什么时间说了什么"——Deepgram 的
diarize+utterances直接给说话人编号 + 句级时间戳,自建 Whisper 拼这套要写一堆后处理 - 适用:中英混说也能跑——
nova-3是 Deepgram 当前 SOTA,中英文都覆盖 - 不适用:实时低延迟场景(直播字幕/语音助手),那种走 WebSocket 流式,本文是批量接口(prerecorded);流式做法本文末尾会给最小代码,但部署在云函数里不划算,推荐走 Web 云函数或直连客户端
- 不适用:转写量极大(每天上万小时),那时候 Deepgram 单价 × 量级会很疼,值得对比一下自建 Whisper Large v3 的 GPU 成本
环境要求
| 依赖 | 版本 |
|---|---|
| Node.js(云函数运行时) | ≥ 18 |
@cloudbase/node-sdk | latest |
@deepgram/sdk | ^4.x(本文按 v4 API 写) |
| 云函数类型 | 普通事件函数即可;批量转写不需要长连接 |
| 公网出口 | 云函数默认能访问公网,Deepgram API 走 api.deepgram.com,境内可达 |
需要准备:
- Deepgram 账号 + 一个 API key(注册即送 200 美元额度,够练手)
- 一个 CloudBase 环境,里面有云存储 + 数据库
- 一个测试用音频文件,中文/英文都行,建议先用 30 秒以内的小样本
第一步:申请 Deepgram key + 在 CloudBase 配置环境变量
- 登录 Deepgram Console,进入 API Keys,点 Create a New API Key,Scope 选
Member(只需要调用 transcription) - 复制生成的 key,只显示一次,丢了只能重新建
- 打开 CloudBase 控制台 → 你的环境 → 云函数,先不创建函数,记下环境 ID(后面
tcb fn deploy -e要用) - 后面创建完函数,在「函数配置 → 环境变量」里加:
DEEPGRAM_API_KEY:刚才那串TCB_ENV:你的环境 ID(代码里tcb.init({ env })会用)
为什么 key 不直接写代码里?除了不能进 git 这条铁律,云函数的环境变量改完不需要重新打包,改 key/换环境只要在控制台改一下重启实例,比改代码快。
第二步:把音频上传到云存储
最简单的两条路:
A. 控制台手动上传:进入 CloudBase 控制台 → 云存储 → 上传文件,选一个 .mp3 或 .wav,记下生成的 fileID,形如 cloud://your-env.xxx/uploads/audio.mp3。
B. 前端走 SDK 上传:已经有上传链路的话,参考 add-file-upload-wechat-miniprogram,小程序/Web 端用 app.uploadFile 上传后把 fileID 写进数据库,云函数从数据库取 fileID 即可。
Deepgram 支持的格式很广:mp3/wav/m4a/flac/ogg/webm/mp4 等,基本上 ffmpeg 能解的都能传。单文件 batch 上限约 2GB,真有大文件再考虑分片。
第三步:写云函数(下载音频 → 调 Deepgram → 存数据库)
新建一个函数目录:
mkdir transcribe-audio && cd transcribe-audio
npm init -y
npm install --save @cloudbase/node-sdk @deepgram/sdk
index.js:
const tcb = require("@cloudbase/node-sdk");
const { createClient } = require("@deepgram/sdk");
const app = tcb.init({ env: process.env.TCB_ENV });
const db = app.database();
const deepgram = createClient(process.env.DEEPGRAM_API_KEY);
// event: { fileID: "cloud://...", language?: "zh-CN" | "en", recordId?: string }
exports.main = async (event) => {
const { fileID, language = "zh-CN", recordId } = event;
if (!fileID) {
return { ok: false, error: "missing_fileID" };
}
// 1. 从云存储下载音频到 buffer
let audioBuffer;
try {
const downloadResult = await app.downloadFile({ fileID });
audioBuffer = downloadResult.fileContent; // Buffer
} catch (err) {
console.error("download failed", err);
return { ok: false, error: "download_failed", message: err.message };
}
// 2. 调 Deepgram 批量转写
let response;
try {
response = await deepgram.listen.v1.media.transcribeFile(audioBuffer, {
model: "nova-3",
smart_format: true, // 自动加标点 + 数字格式化
punctuate: true,
diarize: true, // 说话人分离,结果在 utterances[i].speaker
utterances: true, // 句级切分 + 时间戳
language, // "zh-CN" / "en" / "auto" 等
});
} catch (err) {
// Deepgram SDK 抛 DeepgramError,带 statusCode/body
console.error("deepgram failed", err);
return {
ok: false,
error: "deepgram_failed",
statusCode: err.statusCode,
message: err.message,
};
}
const transcript =
response?.result?.results?.channels?.[0]?.alternatives?.[0]?.transcript || "";
const utterances = response?.result?.results?.utterances || [];
// 3. 写数据库;有 recordId 就更新,否则新建
const payload = {
fileID,
language,
transcript,
utterances, // [{ start, end, speaker, transcript, confidence }]
durationSec: response?.result?.metadata?.duration,
model: "nova-3",
updatedAt: new Date(),
};
if (recordId) {
await db.collection("transcripts").doc(recordId).update(payload);
return { ok: true, recordId, transcript };
} else {
const { id } = await db.collection("transcripts").add({
...payload,
createdAt: new Date(),
});
return { ok: true, recordId: id, transcript };
}
};
几个容易踩的点:
app.downloadFile返回{ fileContent: Buffer };直接把 buffer 喂给transcribeFile即可,不需要落本地磁盘。云函数/tmp也能写,但走内存更快- 中文音频必须传
language: "zh-CN",不传会按英文识别,出来一堆音译乱码(nova-3默认en) smart_format: true已经包含punctuate,但显式写一份不冲突,后期换模型万一行为变了也清楚- 真要"谁在说"的颗粒度,只看
transcript不够,得用utterances数组,里面每条带speaker(整数,从 0 开始) +start/end(秒) response.result这一层是 SDK v4 的封装,直接拿response.results会拿到 undefined,这是 v3 → v4 升级最常见的坑
package.json 里 dependencies 应当类似:
{
"name": "transcribe-audio",
"main": "index.js",
"dependencies": {
"@cloudbase/node-sdk": "^3.0.0",
"@deepgram/sdk": "^4.0.0"
}
}
第四步:部署 + 调用
部署:
tcb login
tcb fn deploy transcribe-audio -e your-env-id
部署完到控制台:
- 「函数配置 → 环境变量」:加
DEEPGRAM_API_KEY和TCB_ENV - 「函数配置 → 执行超时时间」:默认 3 秒太短,改到 60 秒(批量转写 1 分钟音频的端到端耗时大约 5-10 秒,留余量给下载 + 数据库写入)
- 「函数配置 → 内存」:512MB 够用;音频 buffer 占内存,如果文件经常超过 100MB,直接拉到 1024MB
本地用 tcb 调一次:
tcb fn invoke transcribe-audio -e your-env-id \
--params '{"fileID":"cloud://your-env.xxx/uploads/audio.mp3","language":"zh-CN"}'
预期返回:
{
"ok": true,
"recordId": "xxxx",
"transcript": "大家好,这是一段测试音频..."
}
运行验证
- 准备一段 30 秒以内、内容已知的中文音频(自己念一段绕口令最直观),
mp3即可 - 上传到云存储,记下
fileID - 用上面的
tcb fn invoke调一次,核对返回里transcript字段是不是和你念的一致 - 进数据库
transcripts集合,核对:transcript全文文本utterances是数组,每条有start/end/speaker/transcriptdurationSec大致等于音频时长(±1 秒)
- 多人对话音频再跑一次,确认
utterances[*].speaker至少出现 0 和 1 两个不同的整数
如果想前端实时看到结果,在数据库前端做一次 add-realtime-notifications-database-watch,watch 这条 record 即可,函数写完数据库前端瞬间能拿到。
常见错误
| 错误信息 | 原因 | 修复 |
|---|---|---|
401 Unauthorized | DEEPGRAM_API_KEY 没配/写错 | 控制台环境变量里重新粘一遍,确认没多空格;改完重启实例(随便重新部署一次或在控制台手动重启) |
Deepgram 返回 Audio decode failed | 上传的不是音频,或者格式损坏 | 用 ffprobe 先验下文件;webm 浏览器录音常见编码问题,转一道 ffmpeg -i in.webm out.mp3 |
| 中文音频转出全英文乱码(类似 "Da jia hao") | 没传 language: "zh-CN",模型默认 en 按音译做了 | 显式传 language: "zh-CN";混合中英文可以试 language: "auto",但准确率不如指定语言 |
| 函数 3 秒超时,日志显示 deepgram 调用刚发出 | 云函数默认超时 3 秒,1 分钟音频转写要 5-10 秒 | 控制台 → 函数配置 → 超时时间,调到 60 秒以上;长音频( >5 分钟)调到 300 秒并加大内存 |
Cannot read property 'channels' of undefined | SDK v3 → v4 写法变了,response.results 变成 response.result.results | 按本文 response?.result?.results?.channels?.[0] 取;或者升级前看一遍 Deepgram SDK CHANGELOG |
音频文件几十 MB,函数报 Request body too large | 事件型云函数请求体上限较小,直接把 buffer 当 event 传过来撑爆了 | 始终走 fileID 让函数自己去云存储下载,不要把音频字节塞进 event |
| 转出来的 transcript 准确率明显低 | 音频码率过低 / 背景噪音 / 多人重叠 | 录音建议 ≥ 16kHz 单声道;混叠场景考虑前置降噪,Deepgram 也有 redact/filler_words 等参数可以调 |
完整错误码可在 Deepgram 文档里查,云函数自身的错误码看 https://docs.cloudbase.net/error-code/。
流式实时转写(可选)
需要边说边出字(直播字幕/语音助手),把 transcribeFile 换成 WebSocket 连接。普通事件函数不能持有长连接,要用 Web 云函数(HTTP 触发) 或者干脆让客户端直连 Deepgram(用临时 token):
const connection = await deepgram.listen.v1.connect({
model: "nova-3",
interim_results: true, // 中间结果,边说边出
punctuate: true,
language: "zh-CN",
});
connection.on("open", () => {
// 客户端开始往 connection 推 PCM/Opus 音频帧
});
connection.on("message", (data) => {
if (data.type === "Results") {
const partial = data.channel.alternatives[0].transcript;
// 推给前端
}
});
connection.connect();
Deepgram 流式延迟亚 300 毫秒,但部署形态、鉴权方式都和批量不一样,有需要单独再起一篇,本文不展开。
相关文档
- add-file-upload-wechat-miniprogram — 把音频先传到云存储再交给本函数处理
- connect-openai-api-cloud-function — 同样是云函数代理境外 AI API,对比看 LLM 与 STT 两类负载的差异(SSE 流 vs 批量调用)
- add-realtime-notifications-database-watch — 转写结果落库后,前端用
watch实时刷新展示 - secure-secrets-in-cloud-function —
DEEPGRAM_API_KEY等敏感值的本地/CI/生产分层管理 - Cloudbase 错误码