Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ endif
test: $(TEST_RUNNER)
@$(TEST_RUNNER)

$(TEST_RUNNER): Sources/AppContextService.swift Sources/LLMAPITransport.swift Sources/ModelConfiguration.swift Tests/AppContextServiceTests.swift
$(TEST_RUNNER): Sources/AppContextService.swift Sources/LLMAPITransport.swift Sources/ModelConfiguration.swift Sources/LLMCooldownManager.swift Sources/PostProcessingService.swift Tests/AppContextServiceTests.swift
@mkdir -p "$(BUILD_DIR)"
swiftc \
-parse-as-library \
-o "$(TEST_RUNNER)" \
-sdk $(shell xcrun --show-sdk-path) \
-target $(ARCH)-apple-macosx13.0 \
Sources/AppContextService.swift Sources/LLMAPITransport.swift Sources/ModelConfiguration.swift Tests/AppContextServiceTests.swift
Sources/AppContextService.swift Sources/LLMAPITransport.swift Sources/ModelConfiguration.swift Sources/LLMCooldownManager.swift Sources/PostProcessingService.swift Tests/AppContextServiceTests.swift

icon: $(ICON_ICNS)

Expand Down
13 changes: 12 additions & 1 deletion Sources/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,16 @@ final class AppState: ObservableObject, @unchecked Sendable {
return stored.trimmingCharacters(in: .whitespacesAndNewlines)
}

/// English name of the selected dictation language, for the cleanup prompt to
/// name explicitly. Empty on Auto-detect, because there is nothing to name, and
/// empty for English, because the prompt's own examples are already English and
/// English dictation never drifts.
static func dictationLanguageName(for language: String) -> String {
let normalized = normalizeTranscriptionLanguage(language)
guard !normalized.isEmpty, normalized != "en" else { return "" }
return transcriptionLanguageOptions.first { $0.code == normalized }?.name ?? ""
}

private static func normalizeTranscriptionLanguage(_ language: String) -> String {
let normalized = language.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard transcriptionLanguageOptions.contains(where: { $0.code == normalized }) else {
Expand Down Expand Up @@ -2573,7 +2583,8 @@ final class AppState: ObservableObject, @unchecked Sendable {
context: context,
customVocabulary: customVocabulary,
customSystemPrompt: customSystemPrompt,
outputLanguage: outputLanguage
outputLanguage: outputLanguage,
dictationLanguage: Self.dictationLanguageName(for: transcriptionLanguage)
)
return (result.transcript, .postProcessingSucceeded, result.prompt)
} catch {
Expand Down
32 changes: 26 additions & 6 deletions Sources/PostProcessingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ Behavior:
context: AppContext,
customVocabulary: String,
customSystemPrompt: String = "",
outputLanguage: String = ""
outputLanguage: String = "",
dictationLanguage: String = ""
) async throws -> PostProcessingResult {
let vocabularyTerms = mergedVocabularyTerms(rawVocabulary: customVocabulary)

Expand All @@ -180,7 +181,8 @@ Behavior:
contextSummary: context.contextSummary,
customVocabulary: vocabularyTerms,
customSystemPrompt: customSystemPrompt,
outputLanguage: outputLanguage
outputLanguage: outputLanguage,
dictationLanguage: dictationLanguage
)
}

Expand Down Expand Up @@ -310,7 +312,8 @@ Behavior:
contextSummary: String,
customVocabulary: [String],
customSystemPrompt: String = "",
outputLanguage: String = ""
outputLanguage: String = "",
dictationLanguage: String = ""
) async throws -> PostProcessingResult {
var primaryModel = resolvedPrimaryModel()
let retryModel = resolvedRetryModel(for: primaryModel)
Expand All @@ -330,7 +333,8 @@ Behavior:
model: primaryModel,
customVocabulary: customVocabulary,
customSystemPrompt: customSystemPrompt,
outputLanguage: outputLanguage
outputLanguage: outputLanguage,
dictationLanguage: dictationLanguage
)
} catch let error as PostProcessingError {
// Unified fallback policy: decide whether to retry on the other model.
Expand Down Expand Up @@ -377,7 +381,8 @@ Behavior:
model: retryModel,
customVocabulary: customVocabulary,
customSystemPrompt: customSystemPrompt,
outputLanguage: outputLanguage
outputLanguage: outputLanguage,
dictationLanguage: dictationLanguage
)
} catch PostProcessingError.suspectedInstructionExecution {
return PostProcessingResult(
Expand Down Expand Up @@ -472,7 +477,8 @@ Behavior:
model: String,
customVocabulary: [String],
customSystemPrompt: String = "",
outputLanguage: String = ""
outputLanguage: String = "",
dictationLanguage: String = ""
) async throws -> PostProcessingResult {
var request = URLRequest(url: URL(string: "\(baseURL)/chat/completions")!)
request.httpMethod = "POST"
Expand All @@ -495,8 +501,11 @@ Use these spellings exactly in the output when relevant:
? Self.defaultSystemPrompt
: customSystemPrompt
let trimmedOutputLanguage = outputLanguage.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedDictationLanguage = dictationLanguage.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedOutputLanguage.isEmpty {
systemPrompt = Self.applyOutputLanguage(systemPrompt, language: trimmedOutputLanguage)
} else if !trimmedDictationLanguage.isEmpty {
systemPrompt = Self.applyDictationLanguage(systemPrompt, language: trimmedDictationLanguage)
}
if !vocabularyPrompt.isEmpty {
systemPrompt += "\n\n" + vocabularyPrompt
Expand Down Expand Up @@ -741,6 +750,17 @@ Model: \(model)
prompt + "\n\nIMPORTANT: Translate the final cleaned text into \(language). Output ONLY in \(language), regardless of the original spoken language."
}

/// Counterpart to `applyOutputLanguage` for when no output language is set: name
/// the language the user dictates in so cleanup keeps it. Every example in the
/// system prompt is English, and smaller models follow the examples over the
/// "Preserve the speaker's ... language" rule, returning English for non-English
/// dictation. Naming the language is what stops that; generic wording does not.
/// "Primarily" and the explicit carve-out keep this consistent with the prompt's
/// own "Preserve mixed-language text exactly as mixed" rule.
static func applyDictationLanguage(_ prompt: String, language: String) -> String {
prompt + "\n\nIMPORTANT: The dictation is primarily in \(language). Write the cleaned text in \(language). Preserve mixed-language words and spans in their original languages."
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// System prompt used for verbatim translation. Deliberately
/// minimal — the whole point of this path is to translate word-
/// for-word without cleanup, so we avoid every rewrite / formatting
Expand Down
26 changes: 26 additions & 0 deletions Tests/AppContextServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ struct AppContextServiceTests {
testNonStrippingModelPreservesExistingBehavior()
testDeprecatedGroqModelsAreNotPredefined()
testQwenCleanupDisablesReasoning()
testDictationLanguageIsNamedInPrompt()
testDictationLanguageLeavesPromptBodyIntact()
print("AppContextServiceTests passed")
}

Expand Down Expand Up @@ -74,6 +76,30 @@ struct AppContextServiceTests {
expect(config.includeReasoning == false, "Qwen cleanup should exclude reasoning output")
}

private static func testDictationLanguageIsNamedInPrompt() {
let prompt = PostProcessingService.applyDictationLanguage("PROMPT", language: "Russian")

expect(prompt.hasPrefix("PROMPT"), "Directive must be appended, not replace the prompt")
expect(prompt.contains("primarily in Russian"), "Language must be named explicitly")
expect(prompt.contains("Write the cleaned text in Russian"),
"Output language must be named explicitly")
expect(prompt.contains("Preserve mixed-language words and spans in their original languages"),
"Naming a language must not override mixed-language preservation")
}

private static func testDictationLanguageLeavesPromptBodyIntact() {
// applyOutputLanguage asks for a translation, applyDictationLanguage asks for the
// opposite; neither may edit the prompt body, only append to it.
let body = PostProcessingService.defaultSystemPrompt
let translated = PostProcessingService.applyOutputLanguage(body, language: "German")
let preserved = PostProcessingService.applyDictationLanguage(body, language: "German")

expect(translated.hasPrefix(body), "applyOutputLanguage must only append")
expect(preserved.hasPrefix(body), "applyDictationLanguage must only append")
expect(!preserved.contains("Translate the final cleaned text"),
"Preserving a language must not ask for a translation")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private static func expectEqual(_ actual: String?, _ expected: String, file: StaticString = #file, line: UInt = #line) {
expect(actual == expected, "Expected \(expected.debugDescription), got \((actual ?? "nil").debugDescription)", file: file, line: line)
}
Expand Down