82 lines
3.3 KiB
Swift
82 lines
3.3 KiB
Swift
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()
|
|
}
|
|
}
|