feat: controller supports vocabulary bias, prompt-echo guard, corrector and result hook
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import SstCore
|
||||
|
||||
@MainActor
|
||||
struct DictationControllerHooksTests {
|
||||
let recorder = FakeRecorder()
|
||||
let transcriber = FakeVocabularyTranscriber()
|
||||
let inserter = FakeInserter()
|
||||
static let loud = [Float](repeating: 0.1, count: 16_000)
|
||||
static let quiet = [Float](repeating: 0.01, count: 16_000)
|
||||
|
||||
func makeController(
|
||||
cleaner: Cleaner = FakeCleaner(),
|
||||
corrector: TextCorrecting? = nil,
|
||||
vocabulary: [String] = []
|
||||
) -> DictationController {
|
||||
DictationController(
|
||||
recorder: recorder,
|
||||
transcriber: transcriber,
|
||||
cleaner: cleaner,
|
||||
inserter: inserter,
|
||||
corrector: corrector,
|
||||
vocabularyProvider: { vocabulary }
|
||||
)
|
||||
}
|
||||
|
||||
func dictate(_ controller: DictationController) async {
|
||||
await controller.handle(.pressed)
|
||||
await controller.handle(.released)
|
||||
}
|
||||
|
||||
@Test func loudRecordingPassesVocabularyToTranscriber() async {
|
||||
recorder.samples = Self.loud
|
||||
await dictate(makeController(vocabulary: ["Anthropic", "Vercel"]))
|
||||
#expect(transcriber.vocabularies == [["Anthropic", "Vercel"]])
|
||||
}
|
||||
|
||||
@Test func quietRecordingSkipsVocabularyButStillTranscribes() async {
|
||||
recorder.samples = Self.quiet // RMS 0.01: above silence (0.005), below bias minimum (0.015)
|
||||
await dictate(makeController(vocabulary: ["Anthropic", "Vercel"]))
|
||||
#expect(transcriber.vocabularies == [[]])
|
||||
#expect(inserter.inserted == ["[hallo wereld]"])
|
||||
}
|
||||
|
||||
@Test func promptEchoIsDiscarded() async {
|
||||
recorder.samples = Self.loud
|
||||
transcriber.result = "Anthropic, Vercel."
|
||||
let controller = makeController(vocabulary: ["Anthropic", "Vercel", "Supabase"])
|
||||
var results: [DictationResult] = []
|
||||
controller.onResult = { results.append($0) }
|
||||
|
||||
await dictate(controller)
|
||||
|
||||
#expect(inserter.inserted.isEmpty)
|
||||
#expect(results.isEmpty)
|
||||
#expect(controller.state == .idle)
|
||||
}
|
||||
|
||||
@Test func singleVocabularyTermIsKept() async {
|
||||
recorder.samples = Self.loud
|
||||
transcriber.result = "Anthropic"
|
||||
await dictate(makeController(vocabulary: ["Anthropic", "Vercel", "Supabase"]))
|
||||
#expect(inserter.inserted == ["[Anthropic]"])
|
||||
}
|
||||
|
||||
@Test func correctorRunsBeforeCleanerAndCorrectionsSurviveAPassthroughCleaner() async {
|
||||
recorder.samples = Self.loud
|
||||
transcriber.result = "hallo"
|
||||
let controller = makeController(cleaner: PassthroughCleaner(), corrector: UppercaseCorrector())
|
||||
var results: [DictationResult] = []
|
||||
controller.onResult = { results.append($0) }
|
||||
|
||||
await dictate(controller)
|
||||
|
||||
#expect(inserter.inserted == ["HALLO"])
|
||||
#expect(results.count == 1)
|
||||
#expect(results[0].rawText == "hallo")
|
||||
#expect(results[0].finalText == "HALLO")
|
||||
#expect(results[0].corrections.count == 1)
|
||||
}
|
||||
|
||||
@Test func correctionsAreDroppedWhenTheCleanerChangesTheText() async {
|
||||
recorder.samples = Self.loud
|
||||
transcriber.result = "hallo"
|
||||
let controller = makeController(cleaner: FakeCleaner(), corrector: UppercaseCorrector())
|
||||
var results: [DictationResult] = []
|
||||
controller.onResult = { results.append($0) }
|
||||
|
||||
await dictate(controller)
|
||||
|
||||
#expect(results[0].finalText == "[HALLO]")
|
||||
#expect(results[0].corrections.isEmpty)
|
||||
}
|
||||
|
||||
@Test func resultIsReportedEvenWhenInsertionFails() async {
|
||||
recorder.samples = Self.loud
|
||||
inserter.error = TestError()
|
||||
let controller = makeController()
|
||||
var results: [DictationResult] = []
|
||||
var errors: [String] = []
|
||||
controller.onResult = { results.append($0) }
|
||||
controller.onError = { errors.append($0) }
|
||||
|
||||
await dictate(controller)
|
||||
|
||||
#expect(results.count == 1)
|
||||
#expect(errors.count == 1)
|
||||
}
|
||||
|
||||
@Test func resultCarriesDurationAndDate() async {
|
||||
recorder.samples = Self.loud // 16 000 samples = 1 s
|
||||
let controller = makeController()
|
||||
var results: [DictationResult] = []
|
||||
controller.onResult = { results.append($0) }
|
||||
let before = Date()
|
||||
|
||||
await dictate(controller)
|
||||
|
||||
#expect(results[0].duration == 1.0)
|
||||
#expect(results[0].date >= before)
|
||||
#expect(results[0].corrections.isEmpty)
|
||||
}
|
||||
}
|
||||
@@ -41,3 +41,31 @@ final class FakeInserter: TextInserting {
|
||||
inserted.append(text)
|
||||
}
|
||||
}
|
||||
|
||||
final class FakeVocabularyTranscriber: Transcriber {
|
||||
var result = "hallo wereld"
|
||||
private(set) var vocabularies: [[String]] = []
|
||||
|
||||
func transcribe(_ samples: [Float]) async throws -> String { result }
|
||||
|
||||
func transcribe(_ samples: [Float], vocabulary: [String]) async throws -> String {
|
||||
vocabularies.append(vocabulary)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
struct UppercaseCorrector: TextCorrecting {
|
||||
func correct(_ text: String) -> CorrectionResult {
|
||||
let upper = text.uppercased()
|
||||
return CorrectionResult(
|
||||
text: upper,
|
||||
corrections: [AppliedCorrection(heard: text, wrote: upper, rule: "upper", start: 0, length: upper.utf16.count)]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class PassthroughCleaner: Cleaner {
|
||||
func clean(_ text: String, context: CleaningContext) async throws -> String {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import Testing
|
||||
@testable import SstCore
|
||||
|
||||
struct PromptEchoTests {
|
||||
let vocabulary = ["Anthropic", "Vercel", "Supabase"]
|
||||
|
||||
@Test func firstTwoTermsAreAnEcho() {
|
||||
#expect(PromptEcho.isEcho("Anthropic, Vercel.", vocabulary: vocabulary))
|
||||
}
|
||||
|
||||
@Test func wholeListIsAnEcho() {
|
||||
#expect(PromptEcho.isEcho("anthropic vercel supabase", vocabulary: vocabulary))
|
||||
}
|
||||
|
||||
@Test func singleTermIsRealSpeech() {
|
||||
#expect(!PromptEcho.isEcho("Anthropic", vocabulary: vocabulary))
|
||||
}
|
||||
|
||||
@Test func termsOutOfOrderAreNotAnEcho() {
|
||||
#expect(!PromptEcho.isEcho("Vercel, Anthropic", vocabulary: vocabulary))
|
||||
}
|
||||
|
||||
@Test func sentenceContainingTermsIsNotAnEcho() {
|
||||
#expect(!PromptEcho.isEcho("Ik gebruik Anthropic en Vercel", vocabulary: vocabulary))
|
||||
}
|
||||
|
||||
@Test func tooShortVocabularyNeverEchoes() {
|
||||
#expect(!PromptEcho.isEcho("Anthropic", vocabulary: ["Anthropic"]))
|
||||
#expect(!PromptEcho.isEcho("", vocabulary: vocabulary))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user