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:
@@ -0,0 +1,94 @@
|
||||
import Foundation
|
||||
import Speech
|
||||
import Testing
|
||||
@testable import Notes
|
||||
|
||||
struct LanguagePickerTests {
|
||||
/// Build an attributed transcript out of `(text, confidence?)` fragments, so a
|
||||
/// test can hand-craft the confidence attributes the picker reads.
|
||||
private func transcript(_ fragments: [(String, Double?)]) -> AttributedString {
|
||||
var result = AttributedString()
|
||||
for (text, confidence) in fragments {
|
||||
var fragment = AttributedString(text)
|
||||
if let confidence { fragment.transcriptionConfidence = confidence }
|
||||
result.append(fragment)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Confidence extraction
|
||||
|
||||
@Test func meanConfidenceIsCharacterWeighted() {
|
||||
// "hello" (5 chars @ 0.9) + " world" (6 chars @ 0.4) → 0.627…
|
||||
let attributed = transcript([("hello", 0.9), (" world", 0.4)])
|
||||
let mean = LanguagePicker.meanConfidence(of: attributed)
|
||||
#expect(abs(mean - ((0.9 * 5 + 0.4 * 6) / 11)) < 0.0001)
|
||||
}
|
||||
|
||||
@Test func meanConfidenceIgnoresRunsWithoutConfidence() {
|
||||
// Only the confident run counts toward the mean.
|
||||
let attributed = transcript([("sure", 1.0), (" maybe", nil)])
|
||||
#expect(abs(LanguagePicker.meanConfidence(of: attributed) - 1.0) < 0.0001)
|
||||
}
|
||||
|
||||
@Test func meanConfidenceOfEmptyOrUnattributedIsZero() {
|
||||
#expect(LanguagePicker.meanConfidence(of: AttributedString("")) == 0)
|
||||
#expect(LanguagePicker.meanConfidence(of: transcript([("no attributes", nil)])) == 0)
|
||||
}
|
||||
|
||||
// MARK: - Argmax
|
||||
|
||||
@Test func pickChoosesHighestConfidence() {
|
||||
let winner = LanguagePicker.pick(from: [
|
||||
LanguageCandidate(localeID: "en-US", meanConfidence: 0.42, transcriptLength: 20),
|
||||
LanguageCandidate(localeID: "ru-RU", meanConfidence: 0.88, transcriptLength: 18),
|
||||
LanguageCandidate(localeID: "fr-FR", meanConfidence: 0.31, transcriptLength: 25),
|
||||
])
|
||||
#expect(winner?.localeID == "ru-RU")
|
||||
}
|
||||
|
||||
@Test func pickReturnsNilForNoCandidates() {
|
||||
#expect(LanguagePicker.pick(from: []) == nil)
|
||||
}
|
||||
|
||||
// MARK: - Empty-transcript zeroing
|
||||
|
||||
@Test func emptyTranscriptNeverBeatsRealText() {
|
||||
// A zero-length, zero-confidence candidate must lose to any real result.
|
||||
let winner = LanguagePicker.pick(from: [
|
||||
LanguageCandidate(localeID: "en-US", meanConfidence: 0, transcriptLength: 0),
|
||||
LanguageCandidate(localeID: "ru-RU", meanConfidence: 0.05, transcriptLength: 3),
|
||||
])
|
||||
#expect(winner?.localeID == "ru-RU")
|
||||
}
|
||||
|
||||
// MARK: - Tie-break determinism
|
||||
|
||||
@Test func equalConfidencePrefersLongerTranscript() {
|
||||
let winner = LanguagePicker.pick(from: [
|
||||
LanguageCandidate(localeID: "en-US", meanConfidence: 0.7, transcriptLength: 10),
|
||||
LanguageCandidate(localeID: "ru-RU", meanConfidence: 0.7, transcriptLength: 30),
|
||||
])
|
||||
#expect(winner?.localeID == "ru-RU")
|
||||
}
|
||||
|
||||
@Test func fullTieBreaksByLocaleID() {
|
||||
// Identical confidence and length → smallest localeID wins, deterministically.
|
||||
let candidates = [
|
||||
LanguageCandidate(localeID: "ru-RU", meanConfidence: 0.5, transcriptLength: 10),
|
||||
LanguageCandidate(localeID: "en-US", meanConfidence: 0.5, transcriptLength: 10),
|
||||
LanguageCandidate(localeID: "fr-FR", meanConfidence: 0.5, transcriptLength: 10),
|
||||
]
|
||||
#expect(LanguagePicker.pick(from: candidates)?.localeID == "en-US")
|
||||
// Order of input must not change the result.
|
||||
#expect(LanguagePicker.pick(from: candidates.reversed())?.localeID == "en-US")
|
||||
}
|
||||
|
||||
@Test func allEmptyCandidatesStillPickDeterministically() {
|
||||
let candidates = [
|
||||
LanguageCandidate(localeID: "ru-RU", meanConfidence: 0, transcriptLength: 0),
|
||||
LanguageCandidate(localeID: "en-US", meanConfidence: 0, transcriptLength: 0),
|
||||
]
|
||||
#expect(LanguagePicker.pick(from: candidates)?.localeID == "en-US")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user