Telegram 隨身語音靈感簿
| | | 1 | |
最近寫了個小程式實現想了很久的需求。
我習慣跑步、通勤時想東想西,腦袋常會飄過各式各樣的新點子,想著程式可以怎麼改,可以寫個什麼功能,又或者有感而發想 PO 篇廢文,靈光乍現之際,便想快點記下來,生怕等下轉身就忘。

(AI 模擬使用方式示意,但跑步時這樣拿手機,應該很快就要買新的)
語音輸入技術發展了幾十年,但效果一直無法讓我滿意。神經網路(深度學習)出現後電腦語音識別能力原地起飛,Whisper 一登場便驚艷四座。前陣子試過 gpt-4o-mini-transcribe,其速度與精確率更是好到讓我讚嘆,而用 .NET 實作難度不高,成本又在可接受範圍(每分鐘語音約新台幣 0.1 元),便興起為自己寫個小工具用手機隨身記靈感的念頭。
我沒有手機 App 開發經驗又想早點拿到成品,故現在不是花時間學習與熟悉 App 開發的好時機,我挑了一條捷徑 - 直接寫成 Telegram 機器人,用 Telegram App 錄音,機器人識別後當成訊息回傳。
之前寫過 Telegram 機器人,感覺良好。Telegram 機器人不像 Line 必須開對外網站提供 Callback URL,在內網連上 Telegram 伺服器就完成訊息收發,程式架構也算簡單好寫,深得我心。因此,這次決定就用它打造我的隨身語音靈感簿。
核心邏輯不難,但需要學會接收口語錄音檔的小技巧。做法是識別訊息型別為 Telegram.Bot.Types.Enums.MessageType.Voice 後,取得 FileId,呼叫 bot.GetFile() 取得下載 URL,再用 bot.DownloadFile() 下載 .ogg 檔。.ogg 檔則需要先轉成 GPT 模型支援的 .mp3 格式再上傳 gpt-4o-mini-transcribe API 進行轉譯。
媒體檔轉檔有個小眉角是要考慮跨平台問題,影音編碼器邏輯複雜,一般不會重新造輪子,多會借用著名的 C 程式庫,一開始用 AI 推薦的 NAudio.Lame 程式庫在 Windows 測試得很開心,包成 Docker 後遇到在 Linux 平台對應不到 Lame 程式庫的錯誤,撞壁幾回合後,毅然改用 ffmpeg 程式庫過關。為順利在容器使用 ffmpeg,Dockerfile 要記得加上 apt-get install -y ffmpeg 安裝指令:
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY transcribe-bot.csproj .
RUN dotnet restore
COPY *.cs .
RUN dotnet publish -c Release -o /app/publish
# Runtime stage
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS runtime
WORKDIR /app
# 安裝 ffmpeg
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "transcribe-bot.dll"]
完成轉檔,後續流程便是將 mp3 交給 gpt-4o-mini-transcribe 轉成文字,再呼叫 gpt-5.4-mini 或更便宜的模型對文字進行修飾,這些之前做過,都是可以想像的過程,此處不多花篇幅說明。
此處還有個重點,要如何限定機器人只幫授權的對象服務?Telegram 機器人有個特性,只要知道 Bot 的 ID,任何人都可以跟機器人對話,交談對象是誰則可用 msg.Chat.Id 識別 (User Id 一串 ulong 數字)。我採取一個非常簡單但有效的方法管控權限,程式會在 data 資料夾保存使用者所有的語音檔與轉譯結果(一方面好調出上一則轉譯文字當上下文,再則可用於問題排除、研究 Prompt 與程式優化),使用者必須在環境變數宣告以 User Id 為名的變數及指定個人資料夾,並在 data 建立個人專屬資料夾才會啟用轉譯功能。對於只有幾個人用的小服務,這樣的管控簡單有效也已經足夠。
綜合以上說明,核心主流程會像這個樣子:
bot.OnMessage += async (msg, type) =>
{
Func<string, Task> sendReply = async (text) =>
{
if (string.IsNullOrEmpty(text)) text = "(無內容)";
await bot.SendMessage(chatId: msg.Chat.Id, text: text, cancellationToken: cts.Token);
};
try
{
var userId = msg.Chat.Id;
var authUserName = Environment.GetEnvironmentVariable($"USER_{userId}");
if (string.IsNullOrEmpty(authUserName)) throw new Exception($"Unauthorized User [{userId}]");
var userFolder = Path.Combine(dataFolder, authUserName);
if (!Directory.Exists(userFolder)) Directory.CreateDirectory(userFolder);
// 若為音訊檔案,先下載並轉錄,再將文字回覆給使用者
if (msg.Type == Telegram.Bot.Types.Enums.MessageType.Voice)
{
var fileId = msg.Voice?.FileId ?? throw new Exception("Voice FileId is null");
var filePath = await bot.GetFile(fileId, cts.Token);
var localPath = Path.Combine(userFolder, $"{DateTime.Now:yyyyMMddhhmmss}.ogg");
using (var memStream = new MemoryStream())
{
await bot.DownloadFile(filePath, memStream);
File.WriteAllBytes(localPath, memStream.ToArray());
var mp3Data = AudioHelper.ConvertOggToMp3(memStream.ToArray());
File.WriteAllBytes(localPath.Replace(".ogg", ".mp3"), mp3Data);
var transcription = await gpt.TranscribeAudioAsync(mp3Data, userFolder, cts.Token);
var prev = string.Empty;
if (transcription.StartsWith("繼續")) {
prev = gpt.GetLatestTranscription(userFolder);
}
var polished = await gpt.PolishTranscriptionAsync(transcription, prev, userFolder, cts.Token);
await sendReply($"{polished}");
}
}
}
catch (Exception ex)
{
await sendReply($"❌ 錯誤:{ex.Message}");
Console.WriteLine($"處理訊息時發生錯誤:{ex.ToString()}");
return;
}
};
實測效果相當不錯!! 準確率頗高,即便中英文交雜,也難不倒 GPT Transcribe 模型。如此,隨時想到好點子,開啟 Telegram 說上幾句,機器人會幫忙忠實記錄。

又為自己寫了一個 AI 應用工具,朝 AI 改善生活的道路繼續前進~
最後用這張圖解釋:在眾多 App 都有內建語音輸入的今天,為什麼仍值得自己寫一個?(實測下來 GPT 4o Transcribe 即便不是每次 100% 精準,仍是最接近我原文的一個)

Describes building a personal Telegram bot to capture voice ideas on the go. Uses OpenAI speech-to-text, ffmpeg for cross-platform audio conversion, Docker deployment, and simple user authorization, creating a lightweight alternative to a mobile app.
Comments
# by saint
Whisper 參考你的想法,我在想可以調整後,在公司內部環境用來做會議紀錄,再請AI 出 文字版本