統一 TTS 最小字數預設並即時套用設定
Docs/ChangeHistory.mdliveAPP/TTSService.swiftliveAPP/TTSSettingsView.swift
commit 8cd2e3d0272467e9f9d3d5159b88e6d590ca644bAuthor: Nuclear0709 <agp05432@gmail.com>Date: Thu Nov 12 08:20:24 2026 +0800 統一 TTS 最小字數預設並即時套用設定 diff --git a/Docs/ChangeHistory.md b/Docs/ChangeHistory.md--- a/Docs/ChangeHistory.md+++ b/Docs/ChangeHistory.md@@ -2,6 +2,7 @@ ## 2026.11.10 音訊主時鐘與 RTMP 診斷可讀性 +- TTS 最小朗讀字數預設改為 5,並把 UI 的 `@AppStorage` 與 `TTSService` 的 fallback 統一由 `TTSDefaults` 提供,避免兩邊預設值不同。最小/最大字數、佇列上限與溢出策略現在變更時立即 `updateDefault()`,不再只依賴離開設定頁或按「套用設定」才生效;`[TTS]` 同步日誌會輸出目前生效的 `minLen`/`maxLen`/`maxQueue`。 - 贊助訊息 `text` 內的圖片 URL 不再只依副檔名判斷:`extractAllImageURLs` 可接受呼叫端提供的 `knownImageURLs`(例如 `iconURL`),因此 `/media/<hash>` 這類沒有 `.png` 的網址也能被解析成行內 emoji,不再把原始 URL 留在文案中被切成多頁。插圖位置改以 UTF-16 index 計算,修正 emoji 在前時與 CoreText/NSString 位置不一致;一般非圖片連結仍保留原文。 - 音訊 PTS 缺口改為大小分流:小於 250 ms 才補零;超過時重建來源時間軸(`rebase`),不再用數秒靜音塞滿環形緩衝並擠掉歷史音訊。`AudioRingBuffer` 產生最多 8 筆 `GapEvent`,含前後 PTS、gap samples、mode、緩衝前後量與 dropped samples;SCK/ReplayKit 會寫出 `[CaptureAudioGap]` event-level log,週期摘要也新增 `zeroFill`/`rebase` 計數。 - `CaptureMediaPipeline.audioSettings` 在麥克風啟用時改用 mic(track 1)作為 Mixer 主時鐘,輸出格式仍取 App 軌;避免 App 軌靜默但持續 callback 時,mic 被當成非主軌反覆 align/丟樣本而斷續。mic 停止或關閉時,底層 `activeClockTrack` 會 fallback 到 App 軌。diff --git a/liveAPP/TTSService.swift b/liveAPP/TTSService.swift--- a/liveAPP/TTSService.swift+++ b/liveAPP/TTSService.swift@@ -15,6 +15,14 @@ enum TTSQueueOverflowAction: Int { case clearPending = 2 // 清空待朗讀(停止目前 + 清佇列),不讀新訊息 } +/// TTS 設定的單一預設來源;UI 與服務不各自寫死不同數字。+enum TTSDefaults {+ /// 預設至少要 5 個字才朗讀;這是預設值,不是可設定下限。+ static let minLength = 5+ /// 未指定上限時採用的最大朗讀字數。+ static let maxLength = 120+}+ @MainActor final class TTSService: NSObject, AVSpeechSynthesizerDelegate { static let shared = TTSService()@@ -32,7 +40,7 @@ final class TTSService: NSObject, AVSpeechSynthesizerDelegate { #endif var isEnabled:Bool- var minLen:Int = 3+ var minLen:Int = TTSDefaults.minLength var readMainOnly:Bool = true var includeUser:Bool = true var maxLen:Int = 0@@ -63,7 +71,7 @@ final class TTSService: NSObject, AVSpeechSynthesizerDelegate { func updateDefault() { isEnabled = userDefaults?.bool(forKey: "TTSEnabled") ?? false- minLen = userDefaults?.integer(forKey: "TTSMinLength") ?? 3+ minLen = userDefaults?.integer(forKey: "TTSMinLength") ?? TTSDefaults.minLength readMainOnly = (userDefaults?.object(forKey: "TTSReadMainOnly") as? Bool) ?? true includeUser = (userDefaults?.object(forKey: "TTSReadUserName") as? Bool) ?? true@@ -88,7 +96,8 @@ final class TTSService: NSObject, AVSpeechSynthesizerDelegate { storedPitch = userDefaults?.double(forKey: "TTSPitch") ?? 0 storedVolume = userDefaults?.double(forKey: "TTSVolume") ?? 0 - sendlog(message:"已同步TTS設定")+ let effectiveMaxLen = maxLen > 0 ? maxLen : TTSDefaults.maxLength+ sendlog(message:"已同步TTS設定 minLen=\(minLen) maxLen=\(effectiveMaxLen) maxQueue=\(maxQueueSize)") } func updateState(isON:Bool? = nil,min_Len:Int? = nil,MainOnly:Bool? = nil) {@@ -114,7 +123,7 @@ final class TTSService: NSObject, AVSpeechSynthesizerDelegate { // 部分初始化不能分離 應保留部分在此位置 isEnabled = userDefaults?.bool(forKey: "TTSEnabled") ?? false- minLen = userDefaults?.integer(forKey: "TTSMinLength") ?? 3+ minLen = userDefaults?.integer(forKey: "TTSMinLength") ?? TTSDefaults.minLength readMainOnly = (userDefaults?.object(forKey: "TTSReadMainOnly") as? Bool) ?? true includeUser = (userDefaults?.object(forKey: "TTSReadUserName") as? Bool) ?? true@@ -221,7 +230,7 @@ final class TTSService: NSObject, AVSpeechSynthesizerDelegate { let storedMaxLength = maxLen- let maxLength = storedMaxLength > 0 ? storedMaxLength : 120+ let maxLength = storedMaxLength > 0 ? storedMaxLength : TTSDefaults.maxLength // TTS 郎讀內容diff --git a/liveAPP/TTSSettingsView.swift b/liveAPP/TTSSettingsView.swift--- a/liveAPP/TTSSettingsView.swift+++ b/liveAPP/TTSSettingsView.swift@@ -720,8 +720,8 @@ struct TTSSettingsView: View { @AppStorage("TTSRate", store: userDefaults) private var rate = Double(AVSpeechUtteranceDefaultSpeechRate) @AppStorage("TTSPitch", store: userDefaults) private var pitch = 1.0 @AppStorage("TTSVolume", store: userDefaults) private var volume = 1.0- @AppStorage("TTSMaxLength", store: userDefaults) private var maxLength = 120- @AppStorage("TTSMinLength", store: userDefaults) private var minLength = 3+ @AppStorage("TTSMaxLength", store: userDefaults) private var maxLength = TTSDefaults.maxLength+ @AppStorage("TTSMinLength", store: userDefaults) private var minLength = TTSDefaults.minLength //@State private var Cache_MaxLength = 100 let options = Array(stride(from: 5, through: 500, by: 5))@@ -972,5 +972,19 @@ struct TTSSettingsView: View { TTSService.shared.updateDefault() }+ // 訊息限制是直接寫入 @AppStorage,不能只依賴離開頁面時才同步;+ // 使用者在頁面上改完立刻送下一則訊息時也要套用新值。+ .onChange(of: minLength) { _ in+ TTSService.shared.updateDefault()+ }+ .onChange(of: maxLength) { _ in+ TTSService.shared.updateDefault()+ }+ .onChange(of: maxQueueSize) { _ in+ TTSService.shared.updateDefault()+ }+ .onChange(of: queueOverflowAction) { _ in+ TTSService.shared.updateDefault()+ } } }