AI產的Code還是要人工不斷code review校正, 直接使用的話太危險
Azure佈署在地端可到Microsoft Artifact Registry拉取最新Image:
mcr.microsoft.com/azure-cognitive-services/speechservices/speech-to-text:5.1.0-amd64-zh-twpod的yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: azure-stt
namespace: independent
spec:
replicas: 1
selector:
matchLabels:
app: azure-stt
template:
metadata:
labels:
app: azure-stt
spec:
containers:
- name: azure-stt
image: mcr.microsoft.com/azure-cognitive-services/speechservices/speech-to-text:5.1.0-amd64-zh-tw
ports:
- containerPort: 5000
env:
- name: eula
value: "accept"
- name: apikey
valueFrom:
secretKeyRef:
name: azure-tts-secret #建一個secret存azure key
key: KEY
- name: billing
valueFrom:
secretKeyRef:
name: azure-tts-secret #建一個secret存azure url,同下hostnames
key: URI
resources:
requests:
cpu: "0.05"
memory: "1Gi"
limits:
cpu: "8"
memory: "8Gi"
restartPolicy: Always
hostAliases:
- ip: "你azure的資源ip"
hostnames:
- "你azure的資源名.cognitiveservices.azure.com"
除了中英文以外, 可以理解少量常用台語、區分發言者、發言時間
呼叫方式可參考官網: 使用快速轉錄 API - 語音服務 - Foundry Tools | Microsoft Learn
由於只能上傳wav格式, 所以我寫了一支程式:
核心功能
1. 錄音功能
• 透過麥克風即時錄音
• 儲存為 WAV 格式
• 支援手動停止錄音
2. 語音轉文字 (STT)
• 使用 Azure Cognitive Services Speech SDK
• 支援多格式檔案:
• 音訊: WAV, MP3
• 影片: MP4, AVI, MOV, WMV, MKV, FLV, WebM
• 自動轉換非 WAV 格式為 WAV
3. 對話辨識與說話者識別
• 使用 ConversationTranscriber 進行多人對話辨識
• 標記不同說話者 (某1, 某2...)
• 輸出時間戳記 (開始~結束時間)
4. 智能重試機制
• 最多重試 10 次
• 發生錯誤時自動續傳
• 從中斷點切割音訊繼續處理
5. 使用者介面
• 即時顯示辨識結果
• 狀態訊息追蹤
• 支援取消分析
• 靜音超時設定UI 畫面:

UI程式 Form1.Designer.cs:
namespace STT
{
partial class Form1
{
/// <summary>
/// 設計工具所需的變數。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清除任何使用中的資源。
/// </summary>
/// <param name="disposing">如果應該處置受控資源則為 true,否則為 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form 設計工具產生的程式碼
/// <summary>
/// 此為設計工具支援所需的方法 - 請勿使用程式碼編輯器修改
/// 這個方法的內容。
/// </summary>
private void InitializeComponent()
{
this.btnStart = new System.Windows.Forms.Button();
this.txtStatus = new System.Windows.Forms.TextBox();
this.txtPath = new System.Windows.Forms.TextBox();
this.btnPath = new System.Windows.Forms.Button();
this.btnSTT = new System.Windows.Forms.Button();
this.txtResult = new System.Windows.Forms.TextBox();
this.panel1 = new System.Windows.Forms.Panel();
this.lblEndSilenceTimeout = new System.Windows.Forms.Label();
this.numSilenceTimeout = new System.Windows.Forms.NumericUpDown();
this.lblInitialSilenceTimeout = new System.Windows.Forms.Label();
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.btnCancel = new System.Windows.Forms.Button();
this.panel1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.numSilenceTimeout)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
this.splitContainer1.Panel1.SuspendLayout();
this.splitContainer1.Panel2.SuspendLayout();
this.splitContainer1.SuspendLayout();
this.SuspendLayout();
//
// btnStart
//
this.btnStart.Location = new System.Drawing.Point(12, 12);
this.btnStart.Name = "btnStart";
this.btnStart.Size = new System.Drawing.Size(105, 32);
this.btnStart.TabIndex = 0;
this.btnStart.Text = "進行錄音";
this.btnStart.UseVisualStyleBackColor = true;
this.btnStart.Click += new System.EventHandler(this.btnStart_Click);
//
// txtStatus
//
this.txtStatus.Dock = System.Windows.Forms.DockStyle.Fill;
this.txtStatus.Location = new System.Drawing.Point(0, 0);
this.txtStatus.Multiline = true;
this.txtStatus.Name = "txtStatus";
this.txtStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtStatus.Size = new System.Drawing.Size(477, 673);
this.txtStatus.TabIndex = 0;
//
// txtPath
//
this.txtPath.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.txtPath.Location = new System.Drawing.Point(235, 17);
this.txtPath.Name = "txtPath";
this.txtPath.Size = new System.Drawing.Size(899, 29);
this.txtPath.TabIndex = 2;
//
// btnPath
//
this.btnPath.Location = new System.Drawing.Point(123, 13);
this.btnPath.Name = "btnPath";
this.btnPath.Size = new System.Drawing.Size(106, 32);
this.btnPath.TabIndex = 1;
this.btnPath.Text = "音檔路徑";
this.btnPath.UseVisualStyleBackColor = true;
this.btnPath.Click += new System.EventHandler(this.btnPath_Click);
//
// btnSTT
//
this.btnSTT.BackColor = System.Drawing.Color.SpringGreen;
this.btnSTT.Location = new System.Drawing.Point(12, 50);
this.btnSTT.Name = "btnSTT";
this.btnSTT.Size = new System.Drawing.Size(217, 32);
this.btnSTT.TabIndex = 3;
this.btnSTT.Text = "轉文字";
this.btnSTT.UseVisualStyleBackColor = false;
this.btnSTT.Click += new System.EventHandler(this.btnSTT_Click);
//
// txtResult
//
this.txtResult.Dock = System.Windows.Forms.DockStyle.Fill;
this.txtResult.Location = new System.Drawing.Point(0, 0);
this.txtResult.Multiline = true;
this.txtResult.Name = "txtResult";
this.txtResult.ReadOnly = true;
this.txtResult.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtResult.Size = new System.Drawing.Size(665, 673);
this.txtResult.TabIndex = 0;
//
// panel1
//
this.panel1.Controls.Add(this.lblEndSilenceTimeout);
this.panel1.Controls.Add(this.numSilenceTimeout);
this.panel1.Controls.Add(this.btnStart);
this.panel1.Controls.Add(this.lblInitialSilenceTimeout);
this.panel1.Controls.Add(this.btnSTT);
this.panel1.Controls.Add(this.txtPath);
this.panel1.Controls.Add(this.btnPath);
this.panel1.Dock = System.Windows.Forms.DockStyle.Top;
this.panel1.Location = new System.Drawing.Point(0, 0);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(1146, 92);
this.panel1.TabIndex = 1;
//
// lblEndSilenceTimeout
//
this.lblEndSilenceTimeout.AutoSize = true;
this.lblEndSilenceTimeout.Location = new System.Drawing.Point(391, 58);
this.lblEndSilenceTimeout.Name = "lblEndSilenceTimeout";
this.lblEndSilenceTimeout.Size = new System.Drawing.Size(121, 20);
this.lblEndSilenceTimeout.TabIndex = 2;
this.lblEndSilenceTimeout.Text = "秒則終止轉文字";
//
// numSilenceTimeout
//
this.numSilenceTimeout.Location = new System.Drawing.Point(328, 55);
this.numSilenceTimeout.Maximum = new decimal(new int[] {
1200,
0,
0,
0});
this.numSilenceTimeout.Minimum = new decimal(new int[] {
5,
0,
0,
0});
this.numSilenceTimeout.Name = "numSilenceTimeout";
this.numSilenceTimeout.Size = new System.Drawing.Size(57, 29);
this.numSilenceTimeout.TabIndex = 4;
this.numSilenceTimeout.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
this.numSilenceTimeout.Value = new decimal(new int[] {
60,
0,
0,
0});
//
// lblInitialSilenceTimeout
//
this.lblInitialSilenceTimeout.AutoSize = true;
this.lblInitialSilenceTimeout.Location = new System.Drawing.Point(235, 58);
this.lblInitialSilenceTimeout.Name = "lblInitialSilenceTimeout";
this.lblInitialSilenceTimeout.Size = new System.Drawing.Size(89, 20);
this.lblInitialSilenceTimeout.TabIndex = 0;
this.lblInitialSilenceTimeout.Text = "未發言超過";
//
// splitContainer1
//
this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer1.Location = new System.Drawing.Point(0, 92);
this.splitContainer1.Name = "splitContainer1";
//
// splitContainer1.Panel1
//
this.splitContainer1.Panel1.Controls.Add(this.txtResult);
//
// splitContainer1.Panel2
//
this.splitContainer1.Panel2.Controls.Add(this.txtStatus);
this.splitContainer1.Size = new System.Drawing.Size(1146, 673);
this.splitContainer1.SplitterDistance = 665;
this.splitContainer1.TabIndex = 5;
//
// btnCancel
//
this.btnCancel.BackColor = System.Drawing.Color.LightPink;
this.btnCancel.Location = new System.Drawing.Point(12, 50);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(217, 32);
this.btnCancel.TabIndex = 0;
this.btnCancel.Text = "取消轉文字";
this.btnCancel.UseVisualStyleBackColor = false;
this.btnCancel.Visible = false;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(10F, 20F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1146, 765);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.splitContainer1);
this.Controls.Add(this.panel1);
this.Font = new System.Drawing.Font("微軟正黑體", 12F);
this.Name = "Form1";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "語音轉文字";
this.Load += new System.EventHandler(this.Form1_Load);
this.panel1.ResumeLayout(false);
this.panel1.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.numSilenceTimeout)).EndInit();
this.splitContainer1.Panel1.ResumeLayout(false);
this.splitContainer1.Panel1.PerformLayout();
this.splitContainer1.Panel2.ResumeLayout(false);
this.splitContainer1.Panel2.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
this.splitContainer1.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Button btnStart;
private System.Windows.Forms.TextBox txtStatus;
private System.Windows.Forms.TextBox txtPath;
private System.Windows.Forms.Button btnPath;
private System.Windows.Forms.Button btnSTT;
private System.Windows.Forms.TextBox txtResult;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.NumericUpDown numSilenceTimeout;
private System.Windows.Forms.Label lblInitialSilenceTimeout;
private System.Windows.Forms.Label lblEndSilenceTimeout;
private System.Windows.Forms.SplitContainer splitContainer1;
private System.Windows.Forms.Button btnCancel;
}
}
Form1.cs:
using Microsoft.CognitiveServices.Speech;
using Microsoft.CognitiveServices.Speech.Audio;
using NAudio.Wave;
using System;
using System.IO;
using System.Threading;
using System.Windows.Forms;
namespace STT
{
public partial class Form1 : Form
{
private const string url = @"www.自己url.com";
private WaveInEvent waveIn;
private WaveFileWriter writer;
private string outputPath = string.Empty;
private string lastStatusMessage = string.Empty;
private CancellationTokenSource cancellationTokenSource;
private double offset = 0.0;
// 重試設定
private const int MAX_RETRY_COUNT = 10;
private const int RETRY_DELAY_MS = 3000;
public Form1()
{
InitializeComponent();
}
private void btnCancel_Click(object sender, EventArgs e)
{
if (cancellationTokenSource != null && !cancellationTokenSource.IsCancellationRequested)
{
if (MessageBox.Show("是否要終止分析?", "確認終止", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
AppendStatusText("使用者取消分析");
cancellationTokenSource?.Cancel();
}
}
}
private void Form1_Load(object sender, EventArgs e)
{
txtStatus.Text = "";
lastStatusMessage = "";
}
private void btnStart_Click(object sender, EventArgs e)
{
try
{
using (var sfd = new SaveFileDialog()
{
Filter = "WAV 檔 (*.wav)|*.wav",
Title = "選擇錄音輸出檔案",
FileName = $"record_{DateTime.Now:yyyyMMdd_HHmmss}.wav",
AddExtension = true
})
{
if (sfd.ShowDialog() != DialogResult.OK)
return;
outputPath = sfd.FileName;
txtPath.Text = outputPath;
}
// 設定錄音參數
waveIn = new WaveInEvent
{
DeviceNumber = 0, // 預設裝置,可根據需要改成其他索引
WaveFormat = new WaveFormat(16000, 16, 1) // 常見設定
};
waveIn.DataAvailable += OnDataAvailable;
waveIn.RecordingStopped += OnRecordingStopped;
writer = new WaveFileWriter(outputPath, waveIn.WaveFormat);
waveIn.StartRecording();
if (MessageBox.Show("錄音中,按確定停止錄音", "錄音中", MessageBoxButtons.OK, MessageBoxIcon.Information) == DialogResult.OK)
{
waveIn?.StopRecording();
Cleanup();
}
}
catch (Exception ex)
{
AppendStatusText($"啟動錄音失敗: {ex.ToString()}");
MessageBox.Show(ex.Message, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
Cleanup();
}
}
private void OnDataAvailable(object sender, WaveInEventArgs e)
{
try
{
// 將 buffer 寫入 WAV
writer?.Write(e.Buffer, 0, e.BytesRecorded);
writer?.Flush();
}
catch (Exception ex)
{
AppendStatusText($"寫入音訊失敗,停止錄音");
AppendStatusText(ex.ToString());
MessageBox.Show(ex.Message, "寫檔錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
waveIn?.StopRecording();
}
}
private void OnRecordingStopped(object sender, StoppedEventArgs e)
{
Cleanup();
if (e.Exception != null)
{
AppendStatusText("錄音停止(有例外)");
AppendStatusText(e.Exception.ToString());
MessageBox.Show(e.Exception.Message, "錄音例外", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else
{
AppendStatusText("錄音完成");
}
}
private void Cleanup()
{
if (waveIn != null)
{
waveIn.DataAvailable -= OnDataAvailable;
waveIn.RecordingStopped -= OnRecordingStopped;
waveIn.Dispose();
waveIn = null;
}
if (writer != null)
{
writer.Dispose();
writer = null;
}
}
private void btnPath_Click(object sender, EventArgs e)
{
using (var ofd = new OpenFileDialog())
{
ofd.Filter = "所有支援的檔案|*.wav;*.mp3;*.mp4;*.avi;*.mov;*.wmv;*.mkv;*.flv;*.webm|音訊檔|*.wav;*.mp3|影片檔|*.mp4;*.avi;*.mov;*.wmv;*.mkv;*.flv;*.webm|WAV 檔|*.wav|MP3 檔|*.mp3|MP4 檔|*.mp4|所有檔案|*.*";
if (ofd.ShowDialog() == DialogResult.OK)
{
txtPath.Text = ofd.FileName;
}
}
}
private async void btnSTT_Click(object sender, EventArgs e)
{
txtResult.Text = string.Empty;
if (txtPath.Text.Length == 0)
{
btnPath_Click(sender, e);
if (txtPath.Text.Length == 0) return;
}
if (!File.Exists(txtPath.Text))
{
MessageBox.Show("找不到指定的檔案", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
string wavPath = txtPath.Text;
string tempFile = null;
string fileExt = Path.GetExtension(txtPath.Text).ToLower();
// 判斷是否需要轉換
if (fileExt != ".wav")
{
try
{
tempFile = Path.Combine(Path.GetTempPath(), $"temp_{Guid.NewGuid()}.wav");
if (fileExt == ".mp3")
{
ConvertMp3ToWav(txtPath.Text, tempFile);
}
else if (IsVideoFile(fileExt))
{
ConvertVideoToWav(txtPath.Text, tempFile);
}
else
{
MessageBox.Show($"不支援的檔案格式: {fileExt}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
wavPath = tempFile;
}
catch (Exception ex)
{
MessageBox.Show($"轉換失敗: {ex.Message}\r\n\r\n請確認系統是否支援該格式的解碼。", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
AppendStatusText(ex.ToString());
if (tempFile != null && File.Exists(tempFile))
{
try { File.Delete(tempFile); } catch { }
}
return;
}
}
// 檢查音訊長度
try
{
using (var reader = new WaveFileReader(wavPath))
{
double durationSeconds = reader.TotalTime.TotalSeconds;
AppendStatusText($"音訊長度 {FormatTime(durationSeconds)}");
}
}
catch (Exception ex)
{
MessageBox.Show($"讀取音訊失敗: {ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
AppendStatusText(ex.ToString());
if (tempFile != null && File.Exists(tempFile))
{
try { File.Delete(tempFile); } catch { }
}
return;
}
try
{
panel1.Enabled = false;
btnCancel.Visible = true;
this.Cursor = Cursors.WaitCursor;
AppendStatusText($"**如果連續 {numSilenceTimeout.Value} 秒沒有發言, 將出現錯誤訊息並停止分析");
cancellationTokenSource = new CancellationTokenSource();
await ProcessAudioWithResume(wavPath, cancellationTokenSource.Token);
if (!cancellationTokenSource.Token.IsCancellationRequested)
AppendStatusText("辨識完成");
}
catch (OperationCanceledException)
{
AppendStatusText("分析已被取消");
}
catch (Exception ex)
{
AppendStatusText(ex.ToString().Replace(url, "端點網址"));
MessageBox.Show($"解析失敗:\n{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
AppendStatusText($"==========================={Environment.NewLine}");
cancellationTokenSource?.Dispose();
cancellationTokenSource = null;
// 清理臨時檔案
if (tempFile != null && File.Exists(tempFile))
{
try { File.Delete(tempFile); } catch { }
}
panel1.Enabled = true;
btnCancel.Visible = false;
this.Cursor = Cursors.Default;
}
}
/// <summary>
/// 處理音訊辨識(支援智能續傳)
/// </summary>
private async System.Threading.Tasks.Task ProcessAudioWithResume(string wavPath, CancellationToken cancellationToken)
{
int retryCount = 0;
double startOffsetForCut = 0;
offset = 0;
while (retryCount <= MAX_RETRY_COUNT)
{
if (cancellationToken.IsCancellationRequested)
throw new OperationCanceledException(cancellationToken);
try
{
string processWavPath = wavPath;
string tempResumeFile = null;
// 如果是續傳模式,切割音訊從 startOffsetForCut 開始
if (startOffsetForCut > 0)
{
tempResumeFile = Path.Combine(Path.GetTempPath(), $"resume_{Guid.NewGuid()}.wav");
CutWavFile(wavPath, tempResumeFile, startOffsetForCut);
processWavPath = tempResumeFile;
AppendStatusText($"從 {FormatTime(startOffsetForCut)} 處續傳");
}
await ProcessAudioInternal(processWavPath, startOffsetForCut, cancellationToken);
// 清理臨時切割檔案
if (tempResumeFile != null && File.Exists(tempResumeFile))
try { File.Delete(tempResumeFile); } catch { }
return;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
retryCount++;
if (retryCount <= MAX_RETRY_COUNT)
{
AppendStatusText($"發生錯誤: {ex.Message.Replace(url, "端點網址")}");
// 將當前的 offset 設為下次的切割起點
startOffsetForCut = offset;
AppendStatusText($"等待 {RETRY_DELAY_MS / 1000} 秒後進行第 {retryCount} 次重試");
await System.Threading.Tasks.Task.Delay(RETRY_DELAY_MS, cancellationToken);
}
else
{
AppendStatusText($"重試 {MAX_RETRY_COUNT} 次後仍失敗");
throw;
}
}
}
}
/// <summary>
/// 從指定時間點切割 WAV 檔案
/// </summary>
private void CutWavFile(string inputWavPath, string outputWavPath, double startSeconds)
{
using (var reader = new WaveFileReader(inputWavPath))
{
var waveFormat = reader.WaveFormat;
int bytesPerSecond = waveFormat.AverageBytesPerSecond;
long startByte = (long)(bytesPerSecond * startSeconds);
// 確保對齊到 block
long blockAlign = waveFormat.BlockAlign;
startByte = (startByte / blockAlign) * blockAlign;
if (startByte >= reader.Length)
throw new ArgumentException($"起始時間 {startSeconds} 秒超過音訊長度");
reader.Position = startByte;
using (var writer = new WaveFileWriter(outputWavPath, waveFormat))
{
byte[] buffer = new byte[bytesPerSecond]; // 1秒的緩衝區
int bytesRead;
while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0)
writer.Write(buffer, 0, bytesRead);
}
}
}
/// <summary>
/// 實際處理音訊的內部方法
/// </summary>
private async System.Threading.Tasks.Task ProcessAudioInternal(string wavPath, double startOffsetForCut, CancellationToken cancellationToken)
{
var speechConfig = SpeechConfig.FromHost(new Uri("http://" + url));
speechConfig.SpeechRecognitionLanguage = "zh-TW";
speechConfig.OutputFormat = OutputFormat.Detailed;
//speechConfig.SetProperty(PropertyId.Speech_LogFilename, "../speech-sdk.log");
speechConfig.SetProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous");
var timeout = numSilenceTimeout.Value.ToString() + "000";
speechConfig.SetProperty(PropertyId.SpeechServiceConnection_InitialSilenceTimeoutMs, timeout);
speechConfig.SetProperty(PropertyId.SpeechServiceConnection_EndSilenceTimeoutMs, timeout);
speechConfig.SetProperty(PropertyId.Speech_SegmentationStrategy, "Semantic");
using (var audioConfig = AudioConfig.FromWavFileInput(wavPath))
{
var conversationTranscriber = new Microsoft.CognitiveServices.Speech.Transcription.ConversationTranscriber(speechConfig, audioConfig);
var stopTranscription = new System.Threading.Tasks.TaskCompletionSource<int>();
conversationTranscriber.Transcribed += (s, e1) =>
{
if (cancellationToken.IsCancellationRequested)
{
stopTranscription.TrySetResult(0);
return;
}
if (e1.Result.Reason == ResultReason.RecognizedSpeech && !string.IsNullOrEmpty(e1.Result.Text))
{
var speakerId = e1.Result.SpeakerId;
// 計算實際時間 = 切割檔案中的位置 + 切割起點
offset = e1.Result.OffsetInTicks / 10000000.0 + startOffsetForCut;
var duration = e1.Result.Duration.TotalSeconds;
var endTime = offset + duration;
var timeInfo = $"{FormatTime(offset)}~{FormatTime(endTime)}";
var speakerInfo = string.IsNullOrEmpty(speakerId) ? "?" : speakerId.Replace("Guest-", "某").Replace("Unknown", "?");
var resultText = $"{timeInfo} {speakerInfo}: {e1.Result.Text}{Environment.NewLine}";
// 直接輸出結果
this.Invoke(new Action(() =>
{
txtResult.AppendText(resultText);
}));
// 更新當前位置為結束時間
offset = endTime;
}
};
conversationTranscriber.Transcribing += (s, e1) =>
{
if (cancellationToken.IsCancellationRequested)
stopTranscription.TrySetResult(0);
};
conversationTranscriber.Canceled += (s, e1) =>
{
if (e1.Reason == CancellationReason.Error)
{
var errorMsg = $"{FormatTime(offset)}: {e1.ErrorDetails}";
stopTranscription.TrySetException(new Exception(errorMsg));
return;
}
stopTranscription.TrySetResult(0);
};
conversationTranscriber.SessionStopped += (s, e1) =>
{
stopTranscription.TrySetResult(0);
};
await conversationTranscriber.StartTranscribingAsync();
using (cancellationToken.Register(() => stopTranscription.TrySetResult(0)))
await stopTranscription.Task;
await conversationTranscriber.StopTranscribingAsync();
if (cancellationToken.IsCancellationRequested)
throw new OperationCanceledException(cancellationToken);
}
}
/// <summary>
/// 格式化時間為 HH:mm:ss 格式
/// </summary>
private string FormatTime(double seconds)
{
var timeSpan = TimeSpan.FromSeconds(seconds);
return $"{(int)timeSpan.TotalHours:D2}:{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}";
}
/// <summary>
/// 判斷是否為影片檔案
/// </summary>
private bool IsVideoFile(string extension)
{
string[] videoExtensions = { ".mp4", ".avi", ".mov", ".wmv", ".mkv", ".flv", ".webm" };
return Array.Exists(videoExtensions, ext => ext.Equals(extension, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// 將 MP3 轉換為 WAV 格式
/// </summary>
private void ConvertMp3ToWav(string mp3Path, string wavPath)
{
using (var reader = new Mp3FileReader(mp3Path))
using (var writer = new WaveFileWriter(wavPath, reader.WaveFormat))
{
reader.CopyTo(writer);
}
}
/// <summary>
/// 將影片檔案轉換為 WAV 格式(提取音訊)
/// </summary>
private void ConvertVideoToWav(string videoPath, string wavPath)
{
try
{
using (var reader = new MediaFoundationReader(videoPath))
using (var writer = new WaveFileWriter(wavPath, reader.WaveFormat))
{
reader.CopyTo(writer);
}
}
catch (Exception ex)
{
throw new Exception($"無法從影片提取音訊: {ex.Message}", ex);
}
}
/// <summary>
/// 附加訊息至 txtStatus,如果與上次訊息相同則略過
/// </summary>
private void AppendStatusText(string message)
{
if (string.IsNullOrEmpty(lastStatusMessage) || !message.StartsWith(lastStatusMessage))
{
txtStatus.AppendText($"{DateTime.Now:HH:mm:ss} {message}{Environment.NewLine}");
lastStatusMessage = message;
}
}
}
}
Taiwan is a country. 臺灣是我的國家