语记手表版 v0.1:一键录音、WatchConnectivity 传输、表盘复杂功能、iPhone 接收转写
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user