diff --git a/Sources/SstCore/Models.swift b/Sources/SstCore/Models.swift index 36444a2..9cdac52 100644 --- a/Sources/SstCore/Models.swift +++ b/Sources/SstCore/Models.swift @@ -24,6 +24,63 @@ public protocol AudioRecording { public protocol Transcriber { func transcribe(_ samples: [Float]) async throws -> String + /// Biases decoding towards `vocabulary`. Engines without bias support ignore the list. + func transcribe(_ samples: [Float], vocabulary: [String]) async throws -> String +} + +public extension Transcriber { + func transcribe(_ samples: [Float], vocabulary: [String]) async throws -> String { + try await transcribe(samples) + } +} + +/// One replacement made by the dictionary correction pass. +/// `start`/`length` are UTF-16 offsets into the corrected text. +public struct AppliedCorrection: Equatable, Codable, Sendable { + public var heard: String + public var wrote: String + public var rule: String + public var start: Int + public var length: Int + + public init(heard: String, wrote: String, rule: String, start: Int, length: Int) { + self.heard = heard + self.wrote = wrote + self.rule = rule + self.start = start + self.length = length + } +} + +public struct CorrectionResult: Equatable, Sendable { + public var text: String + public var corrections: [AppliedCorrection] + + public init(text: String, corrections: [AppliedCorrection]) { + self.text = text + self.corrections = corrections + } +} + +public protocol TextCorrecting { + func correct(_ text: String) -> CorrectionResult +} + +/// A finished dictation, emitted just before the text is inserted. +public struct DictationResult: Equatable, Sendable { + public var date: Date + public var duration: TimeInterval + public var rawText: String + public var finalText: String + public var corrections: [AppliedCorrection] + + public init(date: Date, duration: TimeInterval, rawText: String, finalText: String, corrections: [AppliedCorrection]) { + self.date = date + self.duration = duration + self.rawText = rawText + self.finalText = finalText + self.corrections = corrections + } } public protocol Cleaner { diff --git a/Tests/SstCoreTests/TranscriberVocabularyTests.swift b/Tests/SstCoreTests/TranscriberVocabularyTests.swift new file mode 100644 index 0000000..e066b40 --- /dev/null +++ b/Tests/SstCoreTests/TranscriberVocabularyTests.swift @@ -0,0 +1,21 @@ +import Foundation +import Testing +@testable import SstCore + +private struct PlainTranscriber: Transcriber { + func transcribe(_ samples: [Float]) async throws -> String { "plain \(samples.count)" } +} + +struct TranscriberVocabularyTests { + @Test func vocabularyVariantFallsBackToPlainTranscribe() async throws { + let result = try await PlainTranscriber().transcribe([0, 0, 0], vocabulary: ["Anthropic"]) + #expect(result == "plain 3") + } + + @Test func appliedCorrectionRoundTripsThroughJSON() throws { + let original = AppliedCorrection(heard: "cloud code", wrote: "Claude Code", rule: "cloud code", start: 12, length: 11) + let data = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(AppliedCorrection.self, from: data) + #expect(decoded == original) + } +}