拾音企業伺服器架構:錄音交給公司的顯示卡轉錄,伺服器不留內容
拾音 4.2 的企業伺服器把會議錄音交給機房的 NVIDIA 顯示卡轉錄。這篇只寫難的地方:跟 app 逐段相同的轉錄、兩套 ggml 的衝突、多卡編號、幾小時錄音的續傳、refresh token 的競態,還有兩邊都不能信任對方送來的值。

重點
- 伺服器用 git submodule 引用拾音的 C# 轉錄核心,Mac 和 Linux 轉出的 27 段文字完全相同;Qwen3-ASR 因為兩套 ggml 符號同名,只能用子程序跑。
- 多張顯示卡要設 CUDA_DEVICE_ORDER=PCI_BUS_ID 編號才對得上 nvidia-smi,分配照正在跑的場數,不照使用率。
- 安全審查找到的都是沒檢查對方送來的值:100 bytes 的上傳讓伺服器配 2 GB,307 轉址把 refresh token 送到別台。
目錄
拾音 4.2 多了企業伺服器。員工登入公司帳號,錄音就交給機房的 NVIDIA 顯示卡轉錄,整理用公司設定的模型。這篇只寫做的時候難的地方。
架構
一台主機、一個 Docker image。ASP.NET Core 加 SQLite,佇列也放 SQLite,不用 Redis 和訊息佇列。
伺服器轉的要跟 app 逐段相同
伺服器另外用 Python 寫一套轉錄,結果會跟本機轉的不一樣,規則要維護兩份,標準答案也管不到。所以伺服器不另外寫,用 git submodule 引用拾音 Windows 版的 C# 核心,分段、接縫、過濾的規則只有一份。91 秒的合成會議,Mac(Metal)和 Linux 容器(CPU)都轉出 27 段,文字完全相同。
卡住的是 Qwen3-ASR。whisper.cpp 和 llama.cpp 各帶一套 ggml,符號同名,Linux 的動態連結會用先載入的那份,放同一個程序就壞。Qwen 只能用 llama-server 子程序跑。
多張顯示卡的編號對不上
CUDA 預設把最快的卡排第一,nvidia-smi 照 PCI 匯流排的順序,後台寫的 GPU 1 不一定是 CUDA 的 1。整個程序要設 CUDA_DEVICE_ORDER=PCI_BUS_ID,whisper 在同一個程序裡才對得上。
分配照正在跑的場數,不照使用率。轉錄剛開始幾秒使用率還沒上來,照使用率挑,同時開始的兩場會擠到同一張。
幾小時的錄音怎麼傳
八小時的 16-bit WAV 約 920 MB,一次傳完、斷一次就要重來。上傳借 tus 協定的 Upload-Offset,一次 8 MB。
refresh token 輪換的競態
refresh token 每換一次舊的就作廢;作廢的又被拿來用,代表可能被偷,整台電腦登出。問題是 app 自己會同時換:重試、幾個工作同時發現 token 過期,第二個請求拿的就是剛作廢的那張,app 把自己登出了。
改成兩邊各擋一層。伺服器:作廢後 2 分鐘內再用只回 401、不撤銷。app:同時只換一次,新的存進鑰匙圈才算換好。
兩邊都不能信任對方送來的值
上線前的安全審查,比較有意思的都是同一類問題:
| 方向 | 問題 | 改法 |
|---|---|---|
| app → 伺服器 | WAV 的 fmt 區塊照檔案寫的長度配置記憶體,100 bytes 的上傳讓伺服器配 2 GB |
先比 SHA-256 再解析,fmt 限 16~64 bytes |
| 伺服器 → app | 307、308 轉址把 refresh token、錄音、逐字稿原封不動送到別台 | 對企業伺服器的請求一律不跟轉址 |
| 伺服器 → app | 很大的時間讓 Mac 版閃退,很大的說話者編號讓 Windows 版一直建說話者 | 超過 95,040 秒的段落略過,說話者只收 1~64 |
| 伺服器 → app | 負的 Upload-Offset 讓 Mac 版閃退,比檔案大的會一直重送 |
不合理就從頭傳,位置連續 3 次沒往前算失敗 |
307、308 規定方法和 body 原樣重送,URLSession 和 HttpClient 預設都會跟,POST 的內容就直接送到轉去的那台。