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
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ APP_EXECUTABLE_TARGET := $(subst $(space),\ ,$(APP_EXECUTABLE))

SOURCES = $(shell find Sources -name '*.swift' -type f | LC_ALL=C sort)
TEST_RUNNER = $(BUILD_DIR)/FreeFlowTests
TRANSCRIPTION_TEST_RUNNER = $(BUILD_DIR)/FreeFlowTranscriptionTests
RESOURCES = $(CONTENTS)/Resources
ARCH ?= $(shell uname -m)

Expand Down Expand Up @@ -69,8 +70,18 @@ endif
@codesign --force --options runtime --sign "$(CODESIGN_IDENTITY)" --entitlements FreeFlow.entitlements "$(APP_BUNDLE)"
@echo "Built $(APP_BUNDLE)"

test: $(TEST_RUNNER)
test: $(TEST_RUNNER) $(TRANSCRIPTION_TEST_RUNNER)
@$(TEST_RUNNER)
@$(TRANSCRIPTION_TEST_RUNNER)

$(TRANSCRIPTION_TEST_RUNNER): Sources/TranscriptionService.swift Sources/LLMAPITransport.swift Tests/TranscriptionServiceTests.swift
@mkdir -p "$(BUILD_DIR)"
swiftc \
-parse-as-library \
-o "$(TRANSCRIPTION_TEST_RUNNER)" \
-sdk $(shell xcrun --show-sdk-path) \
-target $(ARCH)-apple-macosx13.0 \
Sources/TranscriptionService.swift Sources/LLMAPITransport.swift Tests/TranscriptionServiceTests.swift

$(TEST_RUNNER): Sources/AppContextService.swift Sources/LLMAPITransport.swift Sources/ModelConfiguration.swift Tests/AppContextServiceTests.swift
@mkdir -p "$(BUILD_DIR)"
Expand Down
34 changes: 32 additions & 2 deletions Sources/TranscriptionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,33 @@ class TranscriptionService {
"you"
]

// On silence whisper also emits a subtitle-credit line in whatever language it
// guessed ("Undertekster av Ai-Media", "Untertitel von ...", "字幕by..."). The
// wording varies endlessly, so match the credit word instead of the full phrase.
// Compared against diacritic-folded text, and only for short outputs, so a real
// sentence that happens to mention subtitles is not swallowed.
private let hallucinationMarkers = [
"amara.org",
"subtitles by", "subtitle by", "subs by", "captions by", "captioning by",
"undertekster", "undertitel", "tekstet av", // no/da
"undertext", "textning", // sv
"untertitel", // de
"ondertitel", // nl
"sous-titr", // fr
"subtitulos", "subtitulado", // es
"sottotitoli", // it
"legendas", // pt
"napisy", // pl
"tekstitys", // fi
"altyaz", // tr
"субтитр", // ru
"字幕", // zh/ja
"자막", // ko
"ترجمة" // ar
]
Comment on lines +315 to +333

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Avoid classifying every short subtitle mention as a credit line.

The substring check treats standalone words such as untertitel, undertekster, and 字幕—as well as phrases like subtitles by—as hallucinations in any output up to 60 characters. For example, a legitimate utterance such as “Can you add subtitles by Friday?” is filtered whenever no_speech_prob >= 0.1. Require stronger credit-line/attribution context or narrow these markers, and add a short-sentence regression test; the current test only protects sentences exceeding the length limit.

Also applies to: 360-367

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/TranscriptionService.swift` around lines 315 - 333, Refine the
hallucination filtering around the `hallucinationMarkers` checks in
`TranscriptionService` so standalone subtitle-related words and ordinary phrases
such as “subtitles by Friday” are not rejected solely because the output is
short. Require stronger credit-line or attribution context, while preserving
detection of genuine subtitle credits; add a regression test for the short
legitimate sentence alongside the existing length-boundary coverage.


private let hallucinationMarkerMaxLength = 60

private let hallucinationNoSpeechThreshold = 0.1

private func parseTranscript(from data: Data) throws -> String {
Expand All @@ -330,11 +357,14 @@ class TranscriptionService {
return text
}

private func isHallucination(text: String, json: [String: Any]) -> Bool {
func isHallucination(text: String, json: [String: Any]) -> Bool {
let normalized = text
.folding(options: .diacriticInsensitive, locale: nil)
.lowercased()
.trimmingCharacters(in: CharacterSet.punctuationCharacters.union(.whitespacesAndNewlines))
guard hallucinationPhrases.contains(normalized) else {
let matchesMarker = normalized.count <= hallucinationMarkerMaxLength
&& hallucinationMarkers.contains { normalized.contains($0) }
guard hallucinationPhrases.contains(normalized) || matchesMarker else {
return false
}

Expand Down
38 changes: 38 additions & 0 deletions Tests/TranscriptionServiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation

@main
struct TranscriptionServiceTests {
static func main() throws {
let service = try TranscriptionService(apiKey: "test-key")

// Silent audio: whisper's subtitle-credit hallucinations get dropped in any language.
for text in [
"Undertekster av Ai-Media",
"Untertitel von ZDF, 2021",
"Sous-titres réalisés para la communauté d'Amara.org",
"Subtítulos realizados por la comunidad de Amara.org",
"字幕by索兰娅",
"Thank you.",
] {
assert(service.isHallucination(text: text, json: response(text, noSpeechProb: 0.9)),
"expected '\(text)' to be filtered on silent audio")
}

// Real speech: same phrases survive when whisper is confident there is speech.
for text in ["Undertekster av Ai-Media", "Thank you."] {
assert(!service.isHallucination(text: text, json: response(text, noSpeechProb: 0.01)),
"expected '\(text)' to survive when no_speech_prob is low")
}

// A real sentence that merely mentions subtitles is not a credit line.
let sentence = "Can you add subtitles by tomorrow so the team can review the launch video?"
assert(!service.isHallucination(text: sentence, json: response(sentence, noSpeechProb: 0.9)),
"expected a long sentence mentioning subtitles to survive")

print("TranscriptionServiceTests passed")
}

private static func response(_ text: String, noSpeechProb: Double) -> [String: Any] {
["text": text, "segments": [["no_speech_prob": noSpeechProb]]]
}
}