app · 8cd2e3d

Dev index Recent changes / versions Changelog About TODO

統一 TTS 最小字數預設並即時套用設定

8cd2e3d · 2026-11-12 08:20 · app
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.mdindex 762daf0..00a1646 100644--- 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.swiftindex 60f1203..0bb9173 100644--- 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.swiftindex 56b879b..ba2e912 100644--- 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()+        }     } }