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:
@@ -30,6 +30,17 @@ final class NoteEntity {
|
||||
var timeZoneID: String?
|
||||
var device: String?
|
||||
|
||||
// Flattened AudioInfo — nil columns mean a plain text note. `hasAudio` is
|
||||
// stored rather than derived from `audioDuration` so a zero-length recording
|
||||
// is still recognizably a voice note. `transcribedAt` is deliberately not
|
||||
// cached (the file is authoritative for that timestamp), so a cache-only
|
||||
// reconstruction of a completed note has a nil `transcribedAt`.
|
||||
var hasAudio: Bool = false
|
||||
var audioDuration: Double?
|
||||
var transcriptionStatusRaw: String?
|
||||
var transcriptionLocaleID: String?
|
||||
var transcriberDeviceID: String?
|
||||
|
||||
init(id: String, jsonRelativePath: String) {
|
||||
self.id = id
|
||||
self.jsonRelativePath = jsonRelativePath
|
||||
@@ -70,6 +81,31 @@ extension NoteEntity {
|
||||
Note.Source(rawValue: sourceRaw) ?? .typed
|
||||
}
|
||||
|
||||
/// Reconstructs `AudioInfo` from the flattened columns (nil for a text
|
||||
/// note). `transcribedAt` is not cached, so it comes back nil even for a
|
||||
/// completed transcription — acceptable for the cache-only offline fallback,
|
||||
/// where the file (which carries it) simply wasn't reachable.
|
||||
var audio: Note.AudioInfo? {
|
||||
get {
|
||||
guard hasAudio else { return nil }
|
||||
return Note.AudioInfo(
|
||||
duration: audioDuration ?? 0,
|
||||
transcriberDeviceID: transcriberDeviceID ?? "",
|
||||
transcriptionStatus: transcriptionStatusRaw
|
||||
.flatMap(Note.TranscriptionStatus.init(rawValue:)) ?? .pending,
|
||||
transcriptionLocaleID: transcriptionLocaleID,
|
||||
transcribedAt: nil
|
||||
)
|
||||
}
|
||||
set {
|
||||
hasAudio = newValue != nil
|
||||
audioDuration = newValue?.duration
|
||||
transcriberDeviceID = newValue?.transcriberDeviceID
|
||||
transcriptionStatusRaw = newValue?.transcriptionStatus.rawValue
|
||||
transcriptionLocaleID = newValue?.transcriptionLocaleID
|
||||
}
|
||||
}
|
||||
|
||||
/// First line of the text, used as the display title.
|
||||
var title: String {
|
||||
text.split(separator: "\n", omittingEmptySubsequences: true)
|
||||
|
||||
Reference in New Issue
Block a user