Back to all posts
Tutorial

用 Cloudflare Whisper 為 Chatbot 添加 $0 成本多語言語音識別

如果你正在構建一個 Chatbot,想要添加語音輸入,又不想為 Google Speech-to-Text、AWS Transcribe 或 OpenAI Whisper API 付費,那麼你已經擁有了一切所需——Cloudflare 免費套餐裡就有。

這篇文章將詳細介紹我如何把 Cloudflare Workers AI Whisper large-v3-turbo 整合到我的作品集網站 Chatbot 中,以完全零成本支援 99 種語言的語音識別。同時,我也會分享遇到的兩個生產 Bug,以及精確的修復方法。


為什麼選擇 Cloudflare Workers AI?

Cloudflare 透過 Workers AI 提供語音識別能力,運行在 Cloudflare 全球邊緣網路上。免費套餐包含:

  • Workers AI 推理:免費,額度參見官方限制文件,足夠個人專案和小型應用使用
  • 零冷啟動:運行在邊緣節點,距離使用者最近
  • 無需管理 API 金鑰:透過 env.AI 直接綁定到 Worker
  • 99 種語言支援:包括中文、日文、韓文、法文、西班牙文、阿拉伯文等

最大的優勢:你不需要託管模型、管理 GPU 實例,也不用處理第三方 API 的速率限制。開箱即用。


Whisper Base vs Whisper Large v3 Turbo:深度對比

Cloudflare Workers AI 提供兩個 Whisper 模型。選對模型至關重要。

@cf/openai/whisper(基礎版)

這是 Whisper base 原版模型(約 3900 萬參數)。

優點:

  • 推理速度快——適合對延遲敏感的場景
  • 每次請求資源消耗低
  • 在 Cloudflare 上經過充分驗證,非常穩定

缺點:

  • 詞錯率(WER)較高,在非英語語言上表現一般
  • 對口音、專業詞彙、混合語言輸入識別較弱
  • audio 參數格式:number[](將 Uint8Array 展開成普通 JS 陣列)

適合場景: 高並發、速度優先的應用,或純英語使用場景。


@cf/openai/whisper-large-v3-turbo(Turbo 版)

這是 Whisper large-v3 的精簡蒸餾版(約 8.09 億參數),在保留大部分精度的同時,推理速度比 large-v3 快約 2 倍。

優點:

  • 與基礎版相比詞錯率降低約 50%——中日韓等語言效果顯著提升
  • 多語言精度優秀,對各種口音表現良好
  • 支援可選參數:language、task(transcribe/translate)、vad_filter、beam_size
  • 返回豐富輸出:text、word_count、帶時間碼的 segments(WebVTT 格式)

缺點:

  • 比基礎版重——單次請求延遲略高
  • audio 參數格式:Base64 字串(不是數字陣列——這導致了我的第一個 Bug)
  • 無法穩定解碼 webm/opus 容器(這導致了我的第二個 Bug)

適合場景: 多語言 Chatbot、語音助理、無障礙工具、對精度要求高的轉錄流水線。


對比總結

| | @cf/openai/whisper | @cf/openai/whisper-large-v3-turbo | |---|---|---| | 參數量 | 約 3900 萬 | 約 8.09 億 | | 支援語言 | 99 種 | 99 種 | | 識別精度 | 良好 | 優秀 | | 推理速度 | 最快 | 快(比 large-v3 快約 2×) | | audio 參數格式 | number[] | Base64 字串 | | 支援容器格式 | webm、wav、mp3 | wav、mp3(webm 不穩定) | | 額外輸出欄位 | 僅 text | text、word_count、segments | | Cloudflare 費用 | 免費套餐 | 免費套餐 |


架構設計

完整的資料處理流程:

瀏覽器麥克風
    ↓
MediaRecorder(錄製 webm/opus——瀏覽器預設格式)
    ↓ 【客戶端轉碼】
AudioContext.decodeAudioData() → 解碼 webm/opus
    ↓
encodeWAV() → 16-bit PCM WAV(單聲道,原始取樣率)
    ↓ HTTP POST multipart/form-data
/api/transcribe(Next.js Edge Route,運行在 Cloudflare Pages)
    ↓
getRequestContext().env.AI(Cloudflare AI 綁定)
    ↓
btoa() → 分塊 Base64 編碼(每批 8192 位元組)
    ↓
env.AI.run('@cf/openai/whisper-large-v3-turbo', { audio: base64 })
    ↓
{ text: "識別結果" } → 回填 Chatbot 輸入框

第一步:在 Wrangler 中綁定 AI

在 wrangler.jsonc(或 wrangler.toml)中添加 AI 綁定:

{
  "ai": {
    "binding": "AI"
  }
}

在 env.d.ts 中宣告類型:

interface CloudflareEnv {
  AI: any;
}

執行 npx wrangler types 可從即時綁定產生精準類型定義。


第二步:伺服器端轉錄路由

建立 src/app/api/transcribe/route.ts。兩個關鍵要求:

  1. export const runtime = 'edge'——必須宣告,否則 getRequestContext() 在 Cloudflare Pages 上無法正常運作
  2. 將音訊 Base64 編碼——turbo 模型要求此格式;Edge Runtime 沒有 Node.js 的 Buffer
import { NextRequest, NextResponse } from 'next/server';
import { getRequestContext } from '@cloudflare/next-on-pages';

export const runtime = 'edge';

export async function POST(req: NextRequest) {
  try {
    const formData = await req.formData();
    const file = formData.get('file') as File;
    if (!file) return NextResponse.json({ error: 'No audio file provided' }, { status: 400 });

    const { env } = getRequestContext();
    if (!env.AI) return NextResponse.json({ error: 'Cloudflare AI binding is missing' }, { status: 500 });

    const arrayBuffer = await file.arrayBuffer();
    const audioBytes = new Uint8Array(arrayBuffer);

    // Edge Runtime 沒有 Buffer——分塊 Base64 編碼,避免大陣列堆疊溢位
    let binary = '';
    for (let i = 0; i < audioBytes.length; i += 8192) {
      binary += String.fromCharCode(...audioBytes.subarray(i, i + 8192));
    }
    const audioBase64 = btoa(binary);

    const response = await env.AI.run('@cf/openai/whisper-large-v3-turbo', { audio: audioBase64 });

    if (!response || !response.text) throw new Error('No transcription returned from AI model');

    return NextResponse.json({ text: response.text });
  } catch (err: unknown) {
    console.error('Transcription API Error:', err);
    return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
  }
}

我在生產中遇到的兩個 Bug

Bug 1:錯誤碼 5006 — 類型不匹配

從 @cf/openai/whisper 切換到 whisper-large-v3-turbo 後,API 返回錯誤 5006。

根本原因: 我沿用了舊格式 audio: [...audioBytes](數字陣列),這是基礎模型的格式。turbo 模型要求的是 Base64 字串。兩者的輸入 schema 完全不同,但文件沒有明確說明。

解決方案: 在伺服器端用 btoa() 分塊轉換,將 Uint8Array 編碼為 Base64 字串後傳入。


Bug 2:錯誤碼 3030 — 音訊檔案解碼失敗

修復 Base64 格式問題後,又遇到錯誤 3030。

根本原因: 瀏覽器以 webm/opus 格式錄製。雖然 Base64 編碼已正確,但 turbo 模型無法解碼 webm/opus 容器。Cloudflare Whisper turbo 只穩定支援 WAV 和 MP3。

解決方案: 在瀏覽器端用 AudioContext.decodeAudioData() 解碼 webm/opus,再用自訂 encodeWAV() 重新編碼為 16-bit PCM WAV 上傳。WAV 是 Whisper 最通用的輸入格式。


注意事項與技巧

  1. export const runtime = 'edge' 是必須的。 沒有它,getRequestContext() 在 Cloudflare Pages 上不會返回任何內容。

  2. Edge Runtime 沒有 Buffer。 Base64 編碼不能用 Buffer.from(bytes).toString('base64'),要用 btoa() 分塊處理,避免大檔案時呼叫堆疊溢位。

  3. turbo 和 base 模型的 audio 欄位類型不同。 文件中沒有明確說明這一差異——我是透過報錯和實驗發現的。

  4. 必須關閉 AudioContext。 瀏覽器對同時存在的 AudioContext 實例數量有上限,解碼後必須 await audioCtx.close()。

  5. 防範空錄音。 如果使用者誤觸麥克風即觸即鬆,decodeAudioData 會對空 blob 拋出例外。檢查 blob.size < 1000 並提前返回。

  6. Base64 會增加約 33% 體積。 10 秒語音錄音轉 Base64 後約 215KB,遠在 Cloudflare 限制內。

  7. iOS 需要使用者手勢才能建立 AudioContext。 使用 onPointerDown(而非計時器或 useEffect)滿足此要求。


最終效果

實現這套流程後,我的 Chatbot 支援:

  • 長按說話:按住麥克風按鈕說話,放手後開始識別
  • 點擊切換:點一下開始錄音,說完再點一下停止識別
  • 99 種語言:中文、日文、韓文、英文等識別效果優秀
  • 零成本:完全在 Cloudflare 免費套餐內
  • 無第三方 API 金鑰:一切都在 Cloudflare 邊緣運行

完整實現已上線 elvisli.ca。