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
This commit is contained in:
2026-07-15 13:30:43 -04:00
parent 6a93cedaa6
commit 86d74806c9
44 changed files with 3329 additions and 123 deletions
+14
View File
@@ -4,6 +4,9 @@ import SwiftUI
@main
struct NotesApp: App {
#if os(iOS)
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
#endif
@State private var appServices: AppServices?
@State private var pendingBackupURL: URL?
@Environment(\.scenePhase) private var scenePhase
@@ -16,6 +19,9 @@ struct NotesApp: App {
.modelContainer(appServices.modelContainer)
.environment(appServices.syncEngine)
.environment(appServices.contextService)
.environment(appServices.audioRecorder)
.environment(appServices.transcriptionSettings)
.environment(appServices.transcriptionService)
.environmentObject(appServices.backupController)
} else {
ProgressView()
@@ -25,6 +31,11 @@ struct NotesApp: App {
if appServices == nil {
let services = await AppServices()
appServices = services
#if os(iOS)
// Hand the (already-activated) watch receiver the live sync
// engine so a delivery arriving now drains immediately.
appDelegate.watchReceiver.syncEngine = services.syncEngine
#endif
await services.startDeferredServices()
if let pending = pendingBackupURL {
pendingBackupURL = nil
@@ -58,6 +69,9 @@ struct NotesApp: App {
// file swap would import a half-restored state.
if !appServices.backupController.isRestoring {
await syncEngine.reconcile()
// Ingest any watch recordings staged since we last
// looked (delivered while backgrounded).
await syncEngine.ingestStagedWatchRecordings()
}
case .checking:
break