import SwiftUI import AVFoundation import Combine // MARK: - 1. 資料模型 struct ChatMessage: Identifiable, Equatable { let id = UUID() var content: String // 改為 var 以便動態追加文字 let isUser: Bool } struct OllamaRequest: Encodable { let model: String let messages: [OllamaMessage] let stream: Bool } struct OllamaMessage: Codable { let role: String let content: String } // 串流回應的結構 (Ollama 回傳的是一行行的 JSON) struct OllamaStreamResponse: Decodable { let message: OllamaMessage? let done: Bool } // MARK: - 2. 語音管理器 (支援串流斷句) class SpeechManager: ObservableObject { private let synthesizer = AVSpeechSynthesizer() private var sentenceBuffer: String = "" // 用來暫存還沒唸的文字 @Published var isMuted: Bool = false // 定義句子的結束符號 (句號、問號、驚嘆號、換行) private let delimiters: CharacterSet = CharacterSet(charactersIn: "。!?!?\n") /// 處理串流進來的文字片段 func processStream(_ token: String) { if isMuted { return } sentenceBuffer += token // 檢查暫存區是否包含結束符號 if let _ = sentenceBuffer.rangeOfCharacter(from: delimiters) { // 嘗試分割句子 speakBufferedSentences() } } /// 串流結束時,強制唸出剩下的文字 func flush() { if !sentenceBuffer.isEmpty { speak(sentenceBuffer) sentenceBuffer = "" } } /// 內部邏輯:切分句子並朗讀 private func speakBufferedSentences() { // 這裡做簡單的切割:只要遇到標點符號就切斷 // 為了避免複雜的 Regex,我們逐字元檢查,或是簡單地 split // 這裡採用簡單策略:如果 buffer 裡有標點,就把標點前的內容拿去唸 var content = sentenceBuffer // 簡單迴圈:只要字串裡還有標點,就一直切出來唸 while let range = content.rangeOfCharacter(from: delimiters) { // 切割出第一句 (包含標點) let endIndex = range.upperBound let sentence = String(content[..