因為工作的關係,經常會與客戶進行開會,討論軟體或者系統該如何開發或者修正的需求,又或者在內部進行開會,因此,無論是會議錄音、訪談內容、醫療紀錄,或是客服通話內容,只要能夠轉換為可搜尋、可分析的文字資料,如此,就可以將這些錄音檔案轉換出來文字稿,使用 ChatGPT 這個 LLM 大語言模型,整理出來一份會議紀錄,可以把該會議的內容分類歸納出來,做為日後執行與追蹤之用。這時候,Azure Speech to Text 便成為一項極具價值的雲端服務。它隸屬於 Microsoft Azure,透過深度學習模型將語音轉換為高品質文字,支援即時串流與離線批次轉錄。
當我們手上有錄音筆或手機錄製的語音檔案,例如 MP3、WAV 或 M4A 檔案,其實整個轉換流程並不複雜。第一步通常是在 Azure 入口網站中建立 Speech 資源。建立完成後,系統會提供金鑰(Key)與服務端點(Endpoint),這兩個資訊就是後續呼叫 API 的核心憑證。
接著,使用者可以選擇透過 SDK 或 REST API 進行語音轉換。如果是單一檔案測試,開發者可以透過簡單的程式碼將音訊檔上傳至服務端點,系統會回傳轉換後的文字內容。這種方式適合小型專案或即時處理需求,例如在 Web 應用程式中即時顯示逐字稿。
在技術實作層面上,開發流程大致包含三個核心動作。首先是音訊格式的確認與優化,例如建議使用單聲道、16kHz 或 44.1kHz 的標準音訊格式,以提升辨識準確率。接著是透過 SDK 初始化 SpeechConfig,設定語言(例如 zh-TW 或 en-US)與金鑰資訊。最後將音訊串流傳入 SpeechRecognizer 物件,等待服務回傳辨識結果。整個過程可以在幾十行程式碼內完成。
從架構角度來看,實務上常見的做法是建立一條完整的語音處理流程。音訊由前端裝置上傳至雲端儲存空間,再由後端服務呼叫 Speech API 進行轉錄,轉換完成後將結果寫入資料庫,最後透過搜尋或 AI 分析系統進行後續應用。這樣的設計能讓語音資料真正成為企業可再利用的數位資產,而不是一次性的錄音檔案。
整體而言,Azure Speech to Text 的導入並不困難,但其價值並不只在於「轉成文字」本身,而是在於它打開了語音資料數位化、結構化與智慧化應用的大門。當語音可以被搜尋、索引、分析與整合時,它就不再只是聲音,而是企業知識的一部分。
開啟 Visual Studio 2026 選擇「建立新專案」 在 [建立新專案] 視窗中,在右方清單內,找到並選擇 [主控台應用程式] 項目 然後點擊右下方「下一步」按鈕 此時將會看到 [設定新的專案] 對話窗 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 "csSpeechToTextBatch" 然後點擊右下方「下一步」按鈕 接著會看到 [其他資訊] 對話窗 在這個對話窗內,確認使用底下的選項架構:.NET 10.0 (或更新版本) 勾選 不要使用最上層陳述式 (這是我的個人習慣) 然後點擊右下方「建立」按鈕 現在,已經完成了這個 主控台應用程式 專案的建立 套件 [Newtonsoft.Json] 是一個流行的 JSON 處理庫,提供了方便的序列化和反序列化功能。透過這個套件,我們可以輕鬆地在 C# 程式碼中處理 JSON 資料,例如將物件轉換為 JSON 字串,或將 JSON 字串轉換為物件。這裡將會列出這個套件提供的功能清單:
將物件序列化為 JSON 字串
將 JSON 字串反序列化為物件
支援自訂序列化設定
支援 LINQ to JSON 操作
在 Visual Studio 的「方案總管」視窗中,右鍵點擊專案名稱
從右鍵選單中,選擇「管理 NuGet 套件」
在 NuGet 套件管理器視窗中,切換到「瀏覽」標籤頁
在搜尋框中,輸入 "Newtonsoft.Json" 並按下 Enter 鍵
從搜尋結果中,找到 "Newtonsoft.Json" 套件 並點擊它
在這裡的範例中,使用該套件的版本為 13.0.3
在右側的詳細資訊面板中,點擊「安裝」按鈕
打開 Program.cs 檔案,並將其內容替換為以下程式碼: using Newtonsoft . Json ;
using System . Net . Http . Headers ;
namespace csSpeechToTextBatch ;
class TranscriptionJson
{
[ JsonProperty ( "combinedRecognizedPhrases" ) ]
public CombinedPhrase [ ] CombinedRecognizedPhrases { get ; set ; }
}
class CombinedPhrase
{
[ JsonProperty ( "display" ) ]
public string Display { get ; set ; }
}
internal class Program
{
static async Task Main ( string [ ] args )
{
string SubscriptionKey = Environment . GetEnvironmentVariable ( "AzureSpeechServiceSubscriptionKey" ) ;
string ServiceRegion = Environment . GetEnvironmentVariable ( "AzureSpeechServiceRegion" ) ;
string AudioFileSasUri = "https://blogstoragekh.blob.core.windows.net/audio-files/250501_0814.mp3?sv=2025-05-05&se=2025-05-01T10%3A53%3A53Z&sr=b&sp=r&sig=HVG%2Bs3hxD5cmv%2FrVOs5HZbekqmIBJujOGJWnsRLTjUQ%3D" ;
var client = new HttpClient ( ) ;
client . DefaultRequestHeaders . Add ( "Ocp-Apim-Subscription-Key" , SubscriptionKey ) ;
var createUrl = $ "https://{ ServiceRegion } .api.cognitive.microsoft.com/speechtotext/v3.2/transcriptions";
var createBody = new
{
contentUrls = new [ ] { AudioFileSasUri } ,
locale = "zh-TW" ,
displayName = "My Batch Transcription" ,
properties = new
{
diarizationEnabled = false ,
wordLevelTimestampsEnabled = true ,
punctuationMode = "DictatedAndAutomatic"
}
} ;
var jsonContent = new StringContent ( JsonConvert . SerializeObject ( createBody ) ) ;
jsonContent . Headers . ContentType = new MediaTypeHeaderValue ( "application/json" ) ;
var createResponse = await client . PostAsync ( createUrl , jsonContent ) ;
createResponse . EnsureSuccessStatusCode ( ) ;
var createResult = await createResponse . Content . ReadAsStringAsync ( ) ;
Console . WriteLine ( "已建立批次轉錄工作:" ) ;
Console . WriteLine ( createResult ) ;
dynamic createJson = JsonConvert . DeserializeObject ( createResult ) ;
string transcriptionUrl = createJson . self ;
Console . WriteLine ( "開始輪詢轉錄狀態…" ) ;
TimeSpan elapsedTime ;
DateTime startTime = DateTime . Now ;
while ( true )
{
elapsedTime = DateTime . Now - startTime ;
Console . WriteLine ( $ "已經花費時間:{ elapsedTime . Hours : D2} :{ elapsedTime . Minutes : D2} :{ elapsedTime . Seconds : D2} ") ;
var statusResponse = await client . GetAsync ( transcriptionUrl ) ;
statusResponse . EnsureSuccessStatusCode ( ) ;
var statusJson = await statusResponse . Content . ReadAsStringAsync ( ) ;
dynamic statusObj = JsonConvert . DeserializeObject ( statusJson ) ;
string status = statusObj . status ;
Console . WriteLine ( $ "目前狀態:{ status } ") ;
if ( status == "Succeeded" )
{
string filesUrl = statusObj . links . files ;
var filesResponse = await client . GetAsync ( filesUrl ) ;
filesResponse . EnsureSuccessStatusCode ( ) ;
var filesJson = await filesResponse . Content . ReadAsStringAsync ( ) ;
dynamic filesObj = JsonConvert . DeserializeObject ( filesJson ) ;
foreach ( var file in filesObj . values )
{
if ( ( string ) file . kind == "Transcription" )
{
var fileUrl = ( string ) file . links . contentUrl ;
var transcriptionResult = await client . GetStringAsync ( fileUrl ) ;
var resultObj = JsonConvert . DeserializeObject < TranscriptionJson > ( transcriptionResult ) ;
string fullText = string . Join ( " " ,
resultObj . CombinedRecognizedPhrases
. Select ( p => p . Display ? . Trim ( ) )
. Where ( s => ! string . IsNullOrEmpty ( s ) )
) ;
Console . WriteLine ( "---- 完整轉錄文字 ----" ) ;
Console . WriteLine ( fullText ) ;
}
}
break ;
}
else if ( status == "Failed" )
{
Console . WriteLine ( "轉錄失敗" ) ;
break ;
}
await Task . Delay ( TimeSpan . FromSeconds ( 5 ) ) ;
}
}
}
在這個檔案內,宣告了 [TranscriptionJson] 與 [CombinedPhrase] 兩個類別,這些類別用來對應從 Azure Speech to Text API 回傳的 JSON 結構。
在程式進入點,宣告了三個字串 [SubscriptionKey]、[ServiceRegion] 與 [AudioFileSasUri],分別用來存放 Azure Speech 服務的金鑰、區域與要轉錄的音訊檔案的 SAS URI。其中,關於 [SAS URI] 部分,想要取得這個服務端點,可以參考 如何使用與存取 Azure Blob Storage Service 這篇文章內的說明。
接著,程式碼中使用了 [HttpClient] 類別來與 Azure Speech API 進行 HTTP 通訊。首先,建立了一個新的 HttpClient 實例,並在其標頭中加入了 [Ocp-Apim-Subscription-Key] 的訂閱金鑰。宣告了 [createUrl] 這個服務端點字串,作為呼叫 Speech To Text 的 API 端點,其中,[ServiceRegion] 是前面宣告的區域字串,這個值會根據你在 Azure 入口網站中建立 Speech 資源時所選擇的區域而有所不同。接著要產生一個 [createBody] 的物件,這個物件包含了轉錄工作所需的參數,例如使用 [contentUrls] 做為要轉錄的音訊檔案 URL、[locale] 設定語言,這裡指定了 [zh-tw] 也就是要產生出繁體中文的文稿、[displayName] 是這個轉錄工作的顯示名稱,還有一些屬性設定,例如 [diarizationEnabled] 是否啟用說話人分離、[wordLevelTimestampsEnabled] 是否啟用逐字時間戳記、[punctuationMode] 標點符號的處理方式等等。然後,將這個物件序列化為 JSON 字串,並包裝成一個 [StringContent] 物件,最後呼叫 [PostAsync] 方法來發送 HTTP POST 請求以建立轉錄工作。
一旦這次的 API 呼叫成功,便可以透過 [Content.ReadAsStringAsync()] 方法來取得 API 回傳的結果,這個結果會包含一些關於轉錄工作的資訊,例如轉錄工作的 ID、狀態、以及一個 [self] URL,這個 URL 就是用來輪詢轉錄狀態的端點。透過 [Json.NET] 將取得的回應 JSON 內容,反序列化成為 [createJson] 這個型別為 [dynamic] 的物件,然後從中取出 [self] URL,並將它存放在 [transcriptionUrl] 這個字串變數中。這個 [transcriptionUrl] 就是後續用來輪詢轉錄狀態的 API 端點,透過不斷的呼叫這個 API,確認此次將語音文字轉換成為文字稿的過程是否已經完成。
這裡同樣的是使用 [HttpClient] 來發送 HTTP GET 請求,並在每次呼叫後,解析回傳的 JSON 內容,經過反序列化之後,取得這個 JSON 物件 [statusObj],透過該物件從中取出 [status] 欄位的值,來確認目前轉錄工作的狀態。如果狀態是 "Succeeded",就代表轉錄已經完成,可以進一步取得轉錄結果;如果狀態是 "Failed",則代表轉錄過程中發生了錯誤,需要進行錯誤處理。為了避免過於頻繁地呼叫 API,這裡使用了 [Task.Delay] 方法來讓程式在每次輪詢之間暫停幾秒鐘。
一旦 [status] 的值變成 "Succeeded",就表示轉換過程已經完成了,接著就可以透過 [filesUrl] 這個 API 端點來取得轉錄結果的相關資訊。這裡同樣是使用 [HttpClient] 發送 HTTP GET 請求,並解析回傳的 JSON 內容,從中找到轉錄結果的 URL,然後再發送一次 HTTP GET 請求來下載轉錄結果的 JSON 內容。最後,將這個轉錄結果的 JSON 字串反序列化成為 [TranscriptionJson] 這個物件,從中取出所有的 [display] 欄位內容,並將它們串接起來形成完整的轉錄文字,最後印出來。