import Foundation import Speech import SwiftData import WatchConnectivity /// iPhone 端接收胶水代码:拖进 YuJi iPhone 工程即可。 /// 在 YuJiApp 启动时调用 WatchSyncService.shared.activate(modelContext:)。 /// 收到手表传来的录音文件后:拷进 AudioStorage → 系统语音识别转写 → 生成一条 VoiceNote, /// 之后在 App 里点 AI 整理即可(和正常录音的笔记完全一样)。 final class WatchSyncService: NSObject, WCSessionDelegate, ObservableObject { static let shared = WatchSyncService() private var modelContext: ModelContext? private let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "zh-CN")) func activate(modelContext: ModelContext) { self.modelContext = modelContext guard WCSession.isSupported() else { return } let session = WCSession.default session.delegate = self session.activate() } // MARK: - WCSessionDelegate func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { // 激活完成即可收文件 } func sessionDidBecomeInactive(_ session: WCSession) {} func sessionDidDeactivate(_ session: WCSession) {} func session(_ session: WCSession, didReceive file: WCSessionFile) { let fileName = file.fileURL.lastPathComponent let destURL = AudioStorage.directory.appendingPathComponent(fileName) do { if FileManager.default.fileExists(atPath: destURL.path) { try FileManager.default.removeItem(at: destURL) } try FileManager.default.copyItem(at: file.fileURL, to: destURL) } catch { print("手表录音保存失败:\(error)") return } transcribeAndSave(audioURL: destURL, fileName: fileName) } // MARK: - 转写并建笔记 private func transcribeAndSave(audioURL: URL, fileName: String) { guard let recognizer, recognizer.isAvailable else { // 识别暂不可用:先建一条空转写笔记,用户可在 App 里重新整理 insertNote(title: "手表速记", transcript: "", fileName: fileName) return } let request = SFSpeechURLRecognitionRequest(url: audioURL) if recognizer.supportsOnDeviceRecognition { request.requiresOnDeviceRecognition = true } recognizer.recognitionTask(with: request) { [weak self] result, error in guard let self else { return } guard let result, result.isFinal else { if error != nil { self.insertNote(title: "手表速记", transcript: "", fileName: fileName) } return } let text = result.bestTranscription.formattedString let title = text.count > 20 ? String(text.prefix(20)) + "…" : (text.isEmpty ? "手表速记" : text) self.insertNote(title: title, transcript: text, fileName: fileName) } } @MainActor private func insertNote(title: String, transcript: String, fileName: String) { guard let modelContext else { return } let note = VoiceNote(title: title, transcript: transcript, audioFileName: fileName) modelContext.insert(note) try? modelContext.save() } }