Files
notes/Notes Watch App/WatchAppServices.swift
T
rzen 86d74806c9 Add voice notes: on-device transcription, multi-language, watch capture
- Schema v2: Note.AudioInfo, .m4a sidecar next to the note JSON in iCloud
- AudioRecorderService (iOS/macOS) + SpeechAnalyzer transcription pipeline
  with enabled-language set and confidence-based auto-pick, re-transcribe menu
- Settings > Transcription > Languages with asset download/reserve handling
- watchOS app + accessory complication: one-button recorder, WCSession
  transferFile to phone, WatchInbox staging, direct-upsert ingestion
- Player UI, transcription status chips, quick-capture mic in notes list
- Version 0.2, changelog + README updated
2026-07-15 13:30:43 -04:00

74 lines
2.6 KiB
Swift

import Foundation
import Observation
import SwiftUI
/// Composition root for the watch app. Owns the recorder and the transfer
/// service and mediates between them: a finished recording is handed to the
/// transfer queue. The watch has no iCloud or local database — its whole job is
/// to capture audio and get it to the phone.
@Observable
@MainActor
final class WatchAppServices {
let recorder = WatchAudioRecorder()
let transfer = WatchTransferService()
func bootstrap() {
transfer.activate()
transfer.reconcileQueue()
// The 10-minute safety cap ends a take without a Stop tap — queue it
// rather than lose it.
recorder.onInvoluntaryFinish = { [weak self] recording in
self?.transfer.enqueue(recording)
}
}
/// Single-button behaviour: start if idle, stop-and-queue if recording.
func toggleRecording() {
if recorder.isRecording {
stopAndQueue()
} else {
Task { await startRecording() }
}
}
/// Request permission (once) then begin recording. A no-op if already
/// recording or permission is refused.
func startRecording() async {
guard !recorder.isRecording else { return }
guard await recorder.requestPermission() else { return }
try? recorder.startRecording()
}
private func stopAndQueue() {
if let recording = recorder.stopRecording() {
transfer.enqueue(recording)
}
}
/// Complication deep link (`contextful://record?autostart=1`) → auto-start a
/// recording. Plain launches never carry the URL, so they never auto-record.
func handleDeepLink(_ url: URL) {
guard url.scheme == "contextful", url.host == "record" else { return }
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let autostart = components?.queryItems?.first { $0.name == "autostart" }?.value == "1"
guard autostart else { return }
Task { await startRecording() }
}
func handleScenePhase(_ phase: ScenePhase) {
switch phase {
case .active:
// Catch up any file whose delivery we never confirmed.
transfer.reconcileQueue()
case .background:
// Never lose audio to the app being suspended: stop-and-queue
// whatever is recording rather than dropping the take. (A brief
// wrist-down is `.inactive` and is covered by the recorder's
// frontmost-timeout extension, so it keeps recording.)
stopAndQueue()
default:
break
}
}
}