From 584be6d64aa5030dfcc0bfffcaa3a04701f43fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 16 Jul 2026 12:03:09 -0400 Subject: [PATCH] fix(transcribe): select multilingual model before download (#2303) --- packages/cli/src/commands/init.test.ts | 9 +++++++++ packages/cli/src/commands/init.ts | 11 ++++++++--- packages/cli/src/whisper/transcribe.test.ts | 19 +++++++++++++++++++ packages/cli/src/whisper/transcribe.ts | 10 +++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts index 13a86b090..3d2a25dbc 100644 --- a/packages/cli/src/commands/init.test.ts +++ b/packages/cli/src/commands/init.test.ts @@ -11,6 +11,7 @@ import { } from "./init.js"; const cliEntry = resolve(fileURLToPath(import.meta.url), "..", "..", "cli.ts"); +const initSource = readFileSync(new URL("./init.ts", import.meta.url), "utf-8"); const tailwindScript = ''; @@ -47,6 +48,14 @@ function expectScaffoldedScripts(target: string): void { } describe("hyperframes init flag rename", () => { + it("selects the language-compatible model before both eager init downloads", () => { + expect(initSource).toMatch( + /const initialTranscriptionModel = initialModelForLanguage\(\s*modelFlag \?\? DEFAULT_MODEL,\s*languageFlag,?\s*\);/, + ); + expect(initSource.match(/await ensureModel\(initialTranscriptionModel/g)).toHaveLength(2); + expect(initSource).not.toMatch(/await ensureModel\(modelFlag/g); + }); + it("requires an explicit source in non-interactive mode", () => { const dir = mkdtempSync(join(tmpdir(), "hf-init-test-")); const target = join(dir, "proj"); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 921320d68..709112df5 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -45,7 +45,8 @@ import { } from "../templates/generators.js"; import { fetchRemoteTemplate } from "../templates/remote.js"; import { trackInitTemplate } from "../telemetry/events.js"; -import { hasFFmpeg } from "../whisper/manager.js"; +import { DEFAULT_MODEL, hasFFmpeg } from "../whisper/manager.js"; +import { initialModelForLanguage } from "../whisper/transcribe.js"; import { findFFmpeg, findFFprobe, getFFmpegInstallHint } from "../browser/ffmpeg.js"; import { VERSION } from "../version.js"; import { @@ -769,6 +770,10 @@ export default defineCommand({ const nonInteractive = args["non-interactive"] === true; const modelFlag = args.model; const languageFlag = args.language; + const initialTranscriptionModel = initialModelForLanguage( + modelFlag ?? DEFAULT_MODEL, + languageFlag, + ); const interactive = !nonInteractive && process.stdout.isTTY === true; if (skipSkillsFlagIgnored) { @@ -866,7 +871,7 @@ export default defineCommand({ try { const { ensureWhisper, ensureModel } = await import("../whisper/manager.js"); await ensureWhisper(); - await ensureModel(modelFlag); + await ensureModel(initialTranscriptionModel); console.log("Transcribing..."); const { transcribe: runTranscribe } = await import("../whisper/transcribe.js"); const result = await runTranscribe(sourceFilePath, destDir, { @@ -1044,7 +1049,7 @@ export default defineCommand({ await ensureWhisper({ onProgress: (msg) => spin.message(msg), }); - await ensureModel(modelFlag, { + await ensureModel(initialTranscriptionModel, { onProgress: (msg) => spin.message(msg), }); diff --git a/packages/cli/src/whisper/transcribe.test.ts b/packages/cli/src/whisper/transcribe.test.ts index 370988a4a..b6ad787a9 100644 --- a/packages/cli/src/whisper/transcribe.test.ts +++ b/packages/cli/src/whisper/transcribe.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, test } from "vitest"; import { dtwPresetForModel, + initialModelForLanguage, isWhisperTimeoutError, resolveAudioPreparationTimeoutMs, resolveWhisperTimeoutMs, @@ -245,3 +246,21 @@ describe("resolveAudioPreparationTimeoutMs", () => { expect(resolveAudioPreparationTimeoutMs(Number.NaN)).toBe(120_000); }); }); + +describe("initialModelForLanguage", () => { + test("keeps the default English-only model when no language is specified", () => { + expect(initialModelForLanguage("small.en", undefined)).toBe("small.en"); + }); + + test("uses the multilingual model before downloading for explicit German", () => { + expect(initialModelForLanguage("small.en", "de")).toBe("small"); + }); + + test("keeps English-only models for English locale variants", () => { + expect(initialModelForLanguage("small.en", "en-US")).toBe("small.en"); + }); + + test("keeps an already multilingual model unchanged", () => { + expect(initialModelForLanguage("large-v3", "de")).toBe("large-v3"); + }); +}); diff --git a/packages/cli/src/whisper/transcribe.ts b/packages/cli/src/whisper/transcribe.ts index 7d71614df..6641edb95 100644 --- a/packages/cli/src/whisper/transcribe.ts +++ b/packages/cli/src/whisper/transcribe.ts @@ -386,6 +386,14 @@ export function dtwPresetForModel(model: string): string { return model.replace(/-/g, "."); } +export function initialModelForLanguage(model: string, language?: string): string { + const baseLanguage = language?.trim().toLowerCase().split(/[-_]/, 1)[0]; + if (baseLanguage && baseLanguage !== "en" && model.endsWith(".en")) { + return model.slice(0, -3); + } + return model; +} + /** * Transcribe an audio or video file and save transcript.json to the output directory. */ @@ -395,7 +403,7 @@ export async function transcribe( outputDir: string, options?: TranscribeOptions, ): Promise { - const model = options?.model ?? DEFAULT_MODEL; + const model = initialModelForLanguage(options?.model ?? DEFAULT_MODEL, options?.language); // 1. Ensure whisper binary options?.onProgress?.("Checking whisper...");