Add dictation-locale transcription (incl. Russian), rework list capture UI
- TranscriptionSettings/Service route per-locale to SpeechTranscriber or DictationTranscriber (union of both supported sets, speech preferred); neutral-confidence ranking fallback keeps mixed-engine auto-pick working - NotesListView: text + voice capture buttons above the list (glassProminent), search moved to the iOS 26 toolbar search accessory (.minimize) - Changelog entries for languages, capture buttons, search placement
This commit is contained in:
@@ -172,9 +172,13 @@ final class TranscriptionService {
|
||||
var scored: [LanguageCandidate] = []
|
||||
for locale in candidates {
|
||||
let transcript = (try? await runTranscription(fileURL: sample.url, locale: locale)) ?? AttributedString()
|
||||
// `rankingConfidence`, not `meanConfidence`: a `DictationTranscriber`
|
||||
// locale may leave runs unattributed, so an unscored-but-non-empty
|
||||
// transcript gets a neutral score and competes on length rather than
|
||||
// losing outright to a confidence-reporting `SpeechTranscriber` locale.
|
||||
scored.append(LanguageCandidate(
|
||||
localeID: TranscriptionSettings.canonicalID(locale),
|
||||
meanConfidence: LanguagePicker.meanConfidence(of: transcript),
|
||||
meanConfidence: LanguagePicker.rankingConfidence(of: transcript),
|
||||
transcriptLength: transcript.characters.count
|
||||
))
|
||||
}
|
||||
@@ -207,17 +211,51 @@ final class TranscriptionService {
|
||||
// MARK: - Analyzer
|
||||
|
||||
/// Transcribe a whole file in one locale and return the finalized attributed
|
||||
/// transcript (carrying per-run confidence). Follows the documented pattern:
|
||||
/// start a collector over `transcriber.results`, feed the file, finalize.
|
||||
/// transcript (carrying per-run confidence where the engine emits it). Routes
|
||||
/// to whichever engine supports the locale — `SpeechTranscriber` or, for its
|
||||
/// dictation-only locales (ru-RU, uk-UA, pl-PL …), `DictationTranscriber` —
|
||||
/// via `TranscriptionSettings.engine(for:)`. Both request confidence; the two
|
||||
/// `Result` types share `isFinal`/`text` but only concretely, so the engine
|
||||
/// branch supplies the module and its result stream to a shared `analyze`.
|
||||
private func runTranscription(fileURL: URL, locale: Locale) async throws -> AttributedString {
|
||||
let transcriber = SpeechTranscriber(
|
||||
locale: locale,
|
||||
transcriptionOptions: [],
|
||||
reportingOptions: [],
|
||||
attributeOptions: [.transcriptionConfidence]
|
||||
)
|
||||
switch settings.engine(for: locale) {
|
||||
case .speech:
|
||||
let transcriber = SpeechTranscriber(
|
||||
locale: locale,
|
||||
transcriptionOptions: [],
|
||||
reportingOptions: [],
|
||||
attributeOptions: [.transcriptionConfidence]
|
||||
)
|
||||
return try await analyze(module: transcriber, results: transcriber.results, fileURL: fileURL) { $0.text }
|
||||
case .dictation:
|
||||
let transcriber = DictationTranscriber(
|
||||
locale: locale,
|
||||
contentHints: [],
|
||||
transcriptionOptions: [],
|
||||
reportingOptions: [],
|
||||
attributeOptions: [.transcriptionConfidence]
|
||||
)
|
||||
return try await analyze(module: transcriber, results: transcriber.results, fileURL: fileURL) { $0.text }
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive `SpeechAnalyzer` over the whole file with a single module and fold its
|
||||
/// finalized results into one transcript. Generic over the module's result
|
||||
/// stream so one pipeline serves both engines (their `Result`s share `isFinal`
|
||||
/// via `SpeechModuleResult` but expose `text` only concretely — hence `textOf`).
|
||||
/// Follows the documented pattern: start a collector over `results`, feed the
|
||||
/// file, finalize.
|
||||
private func analyze<Results>(
|
||||
module: any SpeechModule,
|
||||
results: Results,
|
||||
fileURL: URL,
|
||||
textOf: @escaping @Sendable (Results.Element) -> AttributedString
|
||||
) async throws -> AttributedString
|
||||
where Results: AsyncSequence & Sendable,
|
||||
Results.Element: SpeechModuleResult & Sendable,
|
||||
Results.Failure == any Error {
|
||||
let analyzer = SpeechAnalyzer(
|
||||
modules: [transcriber],
|
||||
modules: [module],
|
||||
options: SpeechAnalyzer.Options(priority: .utility, modelRetention: .whileInUse)
|
||||
)
|
||||
let file = try AVAudioFile(forReading: fileURL)
|
||||
@@ -226,8 +264,8 @@ final class TranscriptionService {
|
||||
// analyzer finishes, ending the loop.
|
||||
let collector = Task {
|
||||
var full = AttributedString()
|
||||
for try await result in transcriber.results where result.isFinal {
|
||||
full.append(result.text)
|
||||
for try await result in results where result.isFinal {
|
||||
full.append(textOf(result))
|
||||
}
|
||||
return full
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user