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
506 changes: 506 additions & 0 deletions main/helpers/asrRepetitionGuard.ts

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion main/helpers/engines/builtinEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
getVadSettings,
isReduceRepetitionEnabled,
getNumericSetting,
guardAsrSubtitleCues,
} from './transcribeShared';
import { resolveEffectiveSettings } from './outcomePresets';
import {
Expand Down Expand Up @@ -370,7 +371,14 @@ async function transcribeBuiltin(ctx: TranscribeContext): Promise<string> {
tempAudioFile,
);
}
const formattedSrt = formatSrtContent(subtitles);
const guarded = guardAsrSubtitleCues(
subtitles,
formData as Record<string, unknown>,
settings,
'builtin',
);
if (guarded.diagnostic) logMessage(guarded.diagnostic, 'warning');
const formattedSrt = formatSrtContent(guarded.cues);
await fs.promises.writeFile(srtFile, formattedSrt);

event.sender.send('taskFileChange', { ...file, extractSubtitle: 'done' });
Expand Down
17 changes: 15 additions & 2 deletions main/helpers/engines/cloudAsrEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
type CloudAudioChunk,
} from '../audioProcessor';
import { formatSrtContent } from '../fileUtils';
import { logMessage } from '../storeManager';
import { logMessage, store } from '../storeManager';
import {
getTaskContext,
TaskCancelledError,
Expand All @@ -35,6 +35,8 @@ import {
import { resplitSubtitleCues } from '../subtitleSegmentation';
import type { AsrWord } from '../../service/asr/types';
import { getCloudProviderGate } from './cloudProviderGate';
import { resolveEffectiveSettings } from './outcomePresets';
import { guardAsrSubtitleCues } from './transcribeShared';
import type { TranscribeContext, TranscriptionEngineAdapter } from './types';

/** 无时间戳(text-only 模型)降级时,用更细的静音切片换取更细的粗粒度时间轴。 */
Expand Down Expand Up @@ -107,6 +109,10 @@ async function transcribeCloud(ctx: TranscribeContext): Promise<string> {

const { tempAudioFile, srtFile } = file;
const signal = ctx.signal ?? getTaskContext()?.signal;
const settings = resolveEffectiveSettings(
formData,
store.get('settings') as Record<string, unknown>,
);

const f = formData as {
asrProviderId?: string;
Expand Down Expand Up @@ -227,7 +233,14 @@ async function transcribeCloud(ctx: TranscribeContext): Promise<string> {
throwIfSignalCancelled(signal);
// 词级/段级路径统一补一次「裁尾」护栏(基于原始 16kHz WAV 能量)。
const subtitles = trimSubtitleTrailingSilence(cues, tempAudioFile);
const formattedSrt = formatSrtContent(subtitles);
const guarded = guardAsrSubtitleCues(
subtitles,
formData as Record<string, unknown>,
settings,
'cloud',
);
if (guarded.diagnostic) logMessage(guarded.diagnostic, 'warning');
const formattedSrt = formatSrtContent(guarded.cues);
await fs.promises.writeFile(srtFile, formattedSrt);

event.sender.send('taskProgressChange', file, 'extractSubtitle', 100);
Expand Down
10 changes: 9 additions & 1 deletion main/helpers/engines/fasterWhisperEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
getNumericSetting,
getWhisperLanguage,
getFasterWhisperAntiRepetitionParams,
guardAsrSubtitleCues,
} from './transcribeShared';
import { resolveEffectiveSettings } from './outcomePresets';
import type { TranscribeContext, TranscriptionEngineAdapter } from './types';
Expand Down Expand Up @@ -336,7 +337,14 @@ async function transcribeFasterWhisper(
subtitles = segments.map(subtitleCueFromSegment);
}
subtitles = trimSubtitleTrailingSilence(subtitles, tempAudioFile);
const formattedSrt = formatSrtContent(subtitles);
const guarded = guardAsrSubtitleCues(
subtitles,
formData as Record<string, unknown>,
settings,
'faster-whisper',
);
if (guarded.diagnostic) logMessage(guarded.diagnostic, 'warning');
const formattedSrt = formatSrtContent(guarded.cues);
await fs.promises.writeFile(srtFile, formattedSrt);

event.sender.send('taskProgressChange', file, 'extractSubtitle', 100);
Expand Down
10 changes: 9 additions & 1 deletion main/helpers/engines/fireRedEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { resplitSubtitleCues } from '../subtitleSegmentation';
import { buildFireRedParams } from './fireRedParams';
import { resolveEffectiveSettings } from './outcomePresets';
import { guardAsrSubtitleCues } from './transcribeShared';
import type { TranscribeContext, TranscriptionEngineAdapter } from './types';

/** 在途转写 id 集合:任务级并发下可能同时存在多个(排队+执行),取消须精确到 id。 */
Expand Down Expand Up @@ -144,7 +145,14 @@ async function transcribeFireRed(ctx: TranscribeContext): Promise<string> {
),
tempAudioFile,
);
const formattedSrt = formatSrtContent(subtitles);
const guarded = guardAsrSubtitleCues(
subtitles,
formData as Record<string, unknown>,
settings,
'firered',
);
if (guarded.diagnostic) logMessage(guarded.diagnostic, 'warning');
const formattedSrt = formatSrtContent(guarded.cues);
await fs.promises.writeFile(srtFile, formattedSrt);

event.sender.send('taskProgressChange', file, 'extractSubtitle', 100);
Expand Down
10 changes: 9 additions & 1 deletion main/helpers/engines/funasrEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { resplitSubtitleCues } from '../subtitleSegmentation';
import { buildFunasrParams } from './funasrParams';
import { resolveEffectiveSettings } from './outcomePresets';
import { guardAsrSubtitleCues } from './transcribeShared';
import type { TranscribeContext, TranscriptionEngineAdapter } from './types';

/** 在途转写 id 集合:任务级并发下可能同时存在多个(排队+执行),取消须精确到 id。 */
Expand Down Expand Up @@ -154,7 +155,14 @@ async function transcribeFunasr(ctx: TranscribeContext): Promise<string> {
),
tempAudioFile,
);
const formattedSrt = formatSrtContent(subtitles);
const guarded = guardAsrSubtitleCues(
subtitles,
formData as Record<string, unknown>,
settings,
'funasr',
);
if (guarded.diagnostic) logMessage(guarded.diagnostic, 'warning');
const formattedSrt = formatSrtContent(guarded.cues);
await fs.promises.writeFile(srtFile, formattedSrt);

event.sender.send('taskProgressChange', file, 'extractSubtitle', 100);
Expand Down
39 changes: 36 additions & 3 deletions main/helpers/engines/localCliEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import path from 'path';
import fs from 'fs';
import type { EngineStatus } from '../../../types/engine';
import { logMessage, store } from '../storeManager';
import { formatSrtContent, secondsToSubtitleTime } from '../fileUtils';
import { parseSubtitleCues } from '../subtitleFormats';
import { getTaskContext, TaskCancelledError } from '../taskContext';
import { getWhisperLanguage } from './transcribeShared';
import { resolveEffectiveSettings } from './outcomePresets';
import { getWhisperLanguage, guardAsrSubtitleCues } from './transcribeShared';
import type { TranscribeContext, TranscriptionEngineAdapter } from './types';

/** 在途 CLI 子进程集合:任务级并发下可能同时存在多个,取消须精确到进程。 */
Expand Down Expand Up @@ -39,8 +42,11 @@ function transcribeLocalCli(ctx: TranscribeContext): Promise<string> {
sourceLanguage?: string;
};
const whisperModel = model?.toLowerCase();
const settings = store.get('settings');
const whisperCommand = settings?.whisperCommand;
const settings = resolveEffectiveSettings(
formData,
store.get('settings') as Record<string, unknown>,
);
const whisperCommand = String(settings?.whisperCommand ?? '');
const { tempAudioFile, srtFile, directory } = file;

let runShell = whisperCommand
Expand Down Expand Up @@ -130,6 +136,33 @@ function transcribeLocalCli(ctx: TranscribeContext): Promise<string> {
fs.renameSync(tempSrtFile, srtFile);
}

// 外部 CLI 直接产出 SRT:解析成共享 cue 形状后走同一模型外重复护栏。
// 未命中时不重写文件,保留 CLI 的原始格式;解析失败也不影响已成功的转写。
if (fs.existsSync(srtFile)) {
try {
const parsed = parseSubtitleCues(
fs.readFileSync(srtFile, 'utf-8'),
'srt',
);
const guarded = guardAsrSubtitleCues(
parsed.map((cue) => [
secondsToSubtitleTime(cue.startMs / 1000),
secondsToSubtitleTime(cue.endMs / 1000),
cue.text,
]),
formData as Record<string, unknown>,
settings,
'local-cli',
);
if (guarded.diagnostic) {
logMessage(guarded.diagnostic, 'warning');
fs.writeFileSync(srtFile, formatSrtContent(guarded.cues), 'utf-8');
}
} catch (error) {
logMessage(`local CLI repetition guard skipped: ${error}`, 'warning');
}
}

event.sender.send('taskFileChange', { ...file, extractSubtitle: 'done' });
resolve(srtFile);
});
Expand Down
10 changes: 9 additions & 1 deletion main/helpers/engines/qwenEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { resplitSubtitleCues } from '../subtitleSegmentation';
import { buildQwenParams } from './qwenParams';
import { resolveEffectiveSettings } from './outcomePresets';
import { guardAsrSubtitleCues } from './transcribeShared';
import type { TranscribeContext, TranscriptionEngineAdapter } from './types';

/** 在途转写 id 集合:任务级并发下可能同时存在多个(排队+执行),取消须精确到 id。 */
Expand Down Expand Up @@ -142,7 +143,14 @@ async function transcribeQwen(ctx: TranscribeContext): Promise<string> {
),
tempAudioFile,
);
const formattedSrt = formatSrtContent(subtitles);
const guarded = guardAsrSubtitleCues(
subtitles,
formData as Record<string, unknown>,
settings,
'qwen',
);
if (guarded.diagnostic) logMessage(guarded.diagnostic, 'warning');
const formattedSrt = formatSrtContent(guarded.cues);
await fs.promises.writeFile(srtFile, formattedSrt);

event.sender.send('taskProgressChange', file, 'extractSubtitle', 100);
Expand Down
37 changes: 35 additions & 2 deletions main/helpers/engines/transcribeShared.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/**
* 各引擎转写实现共用的纯工具:数值兜底、语言归一、SRT 时间格式化、VAD 设置归一
* 不依赖任何引擎实现,供 builtin / faster-whisper / localCli 适配器复用。
* 各引擎转写实现共用的纯工具:数值兜底、语言归一、SRT 时间格式化、VAD 设置归一
* 模型外重复护栏。不依赖任何具体引擎实现,供所有 ASR 适配器复用。
*/
import {
applyAsrRepetitionGuard,
formatRepetitionGuardDiagnostic,
getAsrRepetitionGuardMode,
type AsrSubtitleCue,
type RepetitionGuardStats,
} from '../asrRepetitionGuard';

export function getNumericSetting(
value: unknown,
Expand Down Expand Up @@ -72,6 +79,32 @@ export function getFasterWhisperAntiRepetitionParams(
};
}

export interface GuardedAsrSubtitleCues {
cues: AsrSubtitleCue[];
stats: RepetitionGuardStats;
diagnostic: string | null;
}

/**
* 各引擎写 SRT 前的统一出口:根据任务意图启用模型外重复护栏,并生成不含用户文本的诊断。
*/
export function guardAsrSubtitleCues(
cues: AsrSubtitleCue[],
formData: Record<string, unknown> | undefined,
effectiveSettings: Record<string, unknown> | undefined,
engine: string,
): GuardedAsrSubtitleCues {
const taskMode = getAsrRepetitionGuardMode(formData, effectiveSettings);
const result = applyAsrRepetitionGuard(cues, {
enabled: taskMode !== 'off',
mode: taskMode === 'off' ? 'standard' : taskMode,
});
return {
...result,
diagnostic: formatRepetitionGuardDiagnostic(engine, result.stats),
};
}

/** 从 store 的 settings 归一化出 VAD 参数(各引擎再映射到自己的字段名)。 */
export function getVadSettings(settings: Record<string, unknown>): VadSettings {
return {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"native:fetch": "node scripts/fetch-sherpa-native.mjs && node scripts/fetch-whisper-addon.mjs",
"size:check": "node scripts/check-bundle-size.mjs",
"test:engines": "tsc scripts/test-engine-units.ts --outDir node_modules/.cache/engine-tests --module commonjs --moduleResolution node --target es2019 --esModuleInterop --skipLibCheck --resolveJsonModule && node node_modules/.cache/engine-tests/scripts/test-engine-units.js",
"test:asr-repetition-guard": "tsc scripts/test-asr-repetition-guard.ts --outDir node_modules/.cache/asr-repetition-guard-tests --module commonjs --moduleResolution node --target es2019 --esModuleInterop --skipLibCheck && node node_modules/.cache/asr-repetition-guard-tests/scripts/test-asr-repetition-guard.js",
"test:ass-builder": "tsc scripts/test-ass-builder.ts --outDir node_modules/.cache/ass-builder-tests --module commonjs --moduleResolution node --target es2019 --esModuleInterop --skipLibCheck --resolveJsonModule && node node_modules/.cache/ass-builder-tests/scripts/test-ass-builder.js",
"test:compose": "tsc scripts/compose/test-compose-builder.ts --outDir node_modules/.cache/compose-tests --module commonjs --moduleResolution node --target es2019 --esModuleInterop --skipLibCheck --resolveJsonModule && node node_modules/.cache/compose-tests/scripts/compose/test-compose-builder.js",
"smoke:compose": "tsc scripts/compose/smoke-compose.ts --outDir node_modules/.cache/compose-tests --module commonjs --moduleResolution node --target es2019 --esModuleInterop --skipLibCheck --resolveJsonModule && node node_modules/.cache/compose-tests/scripts/compose/smoke-compose.js",
Expand Down
4 changes: 2 additions & 2 deletions renderer/public/locales/en/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
"title": "Custom",
"desc": "Manually tune context length, VAD, repetition reduction and other low-level parameters.",
"sectionTitle": "Custom parameters",
"sherpaNote": "For the current engine (FunASR / Qwen / FireRed), VAD is always on and there is no context-length / repetition concept. To fine-tune VAD sensitivity, use the Settings page."
"sherpaNote": "For the current engine (FunASR / Qwen / FireRed), VAD is always on and there are no decoder-level context or repetition controls. Cleanest & most stable enables the shared high-confidence repetition post-processor. To fine-tune VAD sensitivity, use the Settings page."
},
"compare": {
"toggle": "See how the effects differ",
Expand All @@ -169,7 +169,7 @@
"label": "Reduce repetition / hallucinated lines",
"on": "On: drops previous-text conditioning and suppresses repeats, greatly reducing looped lines over silence/repetition; coherence on clean speech may drop slightly.",
"off": "Off: engine defaults (keeps cross-segment coherence); long videos or silence may produce repeated or hallucinated lines.",
"hint": "Turn on when subtitles loop or repeat heavily. Global setting; applies to both faster-whisper and whisper.cpp."
"hint": "Repetition post-processing runs only with Cleanest & most stable or when this custom option is enabled. In addition to engine-native controls, it blocks sliding, exact-repeat and multi-line window cycles."
},
"subtitleLength": {
"label": "Subtitle line breaking",
Expand Down
4 changes: 2 additions & 2 deletions renderer/public/locales/zh/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
"title": "自定义",
"desc": "手动调整上下文长度、VAD、抗重复等底层参数。",
"sectionTitle": "自定义参数",
"sherpaNote": "当前引擎(FunASR / Qwen / FireRed)的 VAD 为结构性常开,且无「上下文长度 / 抗重复」概念。如需细调 VAD 灵敏度,请到「设置」页调整。"
"sherpaNote": "当前引擎(FunASR / Qwen / FireRed)的 VAD 为结构性常开,且无解码层「上下文长度 / 抗重复」参数;选择「最干净最稳」会启用共享的高置信度重复后处理。如需细调 VAD 灵敏度,请到「设置」页调整。"
},
"compare": {
"toggle": "看看不同效果的字幕差异",
Expand All @@ -169,7 +169,7 @@
"label": "减少重复 / 幻觉字幕",
"on": "已开启:断开上文条件并抑制重复,显著减少静音/重复段的循环字幕;干净语音的连贯性可能略降。",
"off": "已关闭:使用引擎默认(保留上文连贯性);长视频或静音处可能出现重复或幻觉字幕。",
"hint": "遇到字幕大段重复/鬼畜时开启。全局设置,对 faster-whisper 与 whisper.cpp 同时生效。"
"hint": "仅在选择「最干净最稳」或自定义开启本项时运行重复后处理;除引擎自身的抗重复参数外,还会拦截滑动、完全重复与多条字幕窗口循环。"
},
"subtitleLength": {
"label": "字幕断句方式",
Expand Down
Loading
Loading