Decouple transcripts from note text; end-to-end watch transfer acks
Schema v3: transcription attempts accumulate on AudioInfo (text, locale, confidence, forced flag) — latest attempt is displayed, payload.text is purely user writing, v2 notes migrate at read time. Editor shows the transcript in its own live-updating section with insert-into-note; search matches transcripts weighted by confidence. Watch recordings are now deleted only on the phone's durable-ingest ack (transferUserInfo), closing every phone-side loss window; failed sidecar writes unstage instead of orphaning, and unreadable inbox recordings are surfaced in logs. Claude-Session: https://claude.ai/code/session_014esDWi42URLEC6Cj17hGQ3
This commit is contained in:
@@ -40,6 +40,10 @@ final class NoteEntity {
|
||||
var transcriptionStatusRaw: String?
|
||||
var transcriptionLocaleID: String?
|
||||
var transcriberDeviceID: String?
|
||||
// Latest transcription attempt, cached flat for list display and search.
|
||||
// The full attempt history lives only in the file (like `transcribedAt`).
|
||||
var transcriptText: String?
|
||||
var transcriptConfidence: Double?
|
||||
|
||||
init(id: String, jsonRelativePath: String) {
|
||||
self.id = id
|
||||
@@ -82,19 +86,31 @@ extension NoteEntity {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// note). `transcribedAt` and the attempt history are not fully cached —
|
||||
/// only the latest attempt survives (with a placeholder date), acceptable
|
||||
/// for the cache-only offline fallback, where the file (which carries the
|
||||
/// full history) simply wasn't reachable.
|
||||
var audio: Note.AudioInfo? {
|
||||
get {
|
||||
guard hasAudio else { return nil }
|
||||
var attempts: [Note.TranscriptionAttempt] = []
|
||||
if let transcriptText {
|
||||
attempts = [Note.TranscriptionAttempt(
|
||||
text: transcriptText,
|
||||
localeID: transcriptionLocaleID ?? "",
|
||||
confidence: transcriptConfidence,
|
||||
transcribedAt: modifiedAt,
|
||||
languageWasForced: false
|
||||
)]
|
||||
}
|
||||
return Note.AudioInfo(
|
||||
duration: audioDuration ?? 0,
|
||||
transcriberDeviceID: transcriberDeviceID ?? "",
|
||||
transcriptionStatus: transcriptionStatusRaw
|
||||
.flatMap(Note.TranscriptionStatus.init(rawValue:)) ?? .pending,
|
||||
transcriptionLocaleID: transcriptionLocaleID,
|
||||
transcribedAt: nil
|
||||
transcribedAt: nil,
|
||||
attempts: attempts
|
||||
)
|
||||
}
|
||||
set {
|
||||
@@ -103,12 +119,20 @@ extension NoteEntity {
|
||||
transcriberDeviceID = newValue?.transcriberDeviceID
|
||||
transcriptionStatusRaw = newValue?.transcriptionStatus.rawValue
|
||||
transcriptionLocaleID = newValue?.transcriptionLocaleID
|
||||
transcriptText = newValue?.latestAttempt?.text
|
||||
transcriptConfidence = newValue?.latestAttempt?.confidence
|
||||
}
|
||||
}
|
||||
|
||||
/// First line of the text, used as the display title.
|
||||
/// What a reader should see as this note's body: the user's own text when
|
||||
/// they wrote any, else the latest transcript.
|
||||
var displayText: String {
|
||||
text.isEmpty ? (transcriptText ?? "") : text
|
||||
}
|
||||
|
||||
/// First line of the display text, used as the display title.
|
||||
var title: String {
|
||||
text.split(separator: "\n", omittingEmptySubsequences: true)
|
||||
displayText.split(separator: "\n", omittingEmptySubsequences: true)
|
||||
.first.map(String.init) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user