From d6cbacf8be2e9feac33becac0ef6bbedb096afe3 Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 11 Sep 2026 02:46:26 -0700 Subject: [PATCH] fix(subtitles): check directory search permission before generation - Require write and search permissions before subtitle extraction - Add regression coverage and update subtitle-generation docs --- changes/japanese-subtitle-generation.md | 2 +- docs-site/subtitle-generation.md | 2 +- src/core/services/subtitle-generation.test.ts | 23 ++++++++++++++++++- src/core/services/subtitle-generation.ts | 4 ++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/changes/japanese-subtitle-generation.md b/changes/japanese-subtitle-generation.md index 4cfe9891..e32c92b8 100644 --- a/changes/japanese-subtitle-generation.md +++ b/changes/japanese-subtitle-generation.md @@ -2,5 +2,5 @@ type: added area: subtitles - Generate local Japanese SRT subtitles with whisper.cpp from a standalone modal opened with Ctrl+Shift+G, the empty subtitle sidebar's generation button, or `subminer generate-subs`, with shared progress reporting, cancellation, safe output files, and automatic loading into the matching mpv video. The sidebar button hides while subtitle lines are loaded. -- Configure an existing multilingual model in Settings or choose an official multilingual model, including quantized variants, in the modal or launcher. The modal shows download sizes, speed and accuracy guidance, and a recommended starting model before explicitly downloading a verified SubMiner-managed model. Executable paths are optional overrides; empty fields find whisper-cli, ffmpeg, and ffprobe on PATH. The modal's Local tools check and the launcher name any missing executable and its setting before downloading a model or extracting audio, and generation confirms the destination directory is writable up front. +- Configure an existing multilingual model in Settings or choose an official multilingual model, including quantized variants, in the modal or launcher. The modal shows download sizes, speed and accuracy guidance, and a recommended starting model before explicitly downloading a verified SubMiner-managed model. Executable paths are optional overrides; empty fields find whisper-cli, ffmpeg, and ffprobe on PATH. The modal's Local tools check and the launcher name any missing executable and its setting before downloading a model or extracting audio, and generation confirms the destination directory grants write and search permissions up front. - Optionally select Focus on spoken dialogue in the modal and use Download speech detection model to install the separate Silero model with progress and cancellation. The choice lasts for the session; a configured VAD model path sets the default. With the detector executable installed, retain brief utterances and extra audio around speech, split long passages near quiet pauses with overlapping context, and combine duplicate cues by greatest timing overlap. Keep original media timing and separate repeated dialogue without spanning omitted music breaks. diff --git a/docs-site/subtitle-generation.md b/docs-site/subtitle-generation.md index f2b2f139..a4a2798a 100644 --- a/docs-site/subtitle-generation.md +++ b/docs-site/subtitle-generation.md @@ -6,7 +6,7 @@ Generate Japanese SRT subtitles from a local video's audio using [whisper.cpp](h Install whisper.cpp's `whisper-cli` executable and FFmpeg, including `ffprobe`. SubMiner downloads models, not these executables. Leave `whisperPath`, `ffmpegPath`, and `ffprobePath` empty to find the executables on `PATH`. To use a specific installation, set a path override under **Settings → Integrations → Japanese Subtitle Generation**. -The generation modal checks for these executables under **Local tools** and keeps **Generate subtitles** disabled until every required one is found, naming the missing executable and its setting. Model downloads stay available in the meantime. After installing a tool or changing a path, click **Check again**. The launcher runs the same check before any model download. Generation also confirms the destination directory is writable before extracting audio. +The generation modal checks for these executables under **Local tools** and keeps **Generate subtitles** disabled until every required one is found, naming the missing executable and its setting. Model downloads stay available in the meantime. After installing a tool or changing a path, click **Check again**. The launcher runs the same check before any model download. Generation also confirms the destination directory grants write and search permissions before extracting audio. Choose one model source: diff --git a/src/core/services/subtitle-generation.test.ts b/src/core/services/subtitle-generation.test.ts index b9c499d9..cf2bf8f5 100644 --- a/src/core/services/subtitle-generation.test.ts +++ b/src/core/services/subtitle-generation.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; -import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { constants } from 'node:fs'; +import { access, chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import test from 'node:test'; @@ -10,6 +11,7 @@ import { } from '../../shared/subtitle-generation'; import { downloadSubtitleGenerationModel, + ensureWritableDirectory, generateJapaneseSubtitles, resolveSubtitleGenerationModel, } from './subtitle-generation'; @@ -310,6 +312,25 @@ test('generation rejects remote media, missing models, and missing tools before await assert.rejects(readFile(input.callsPath), /ENOENT/); })); +test( + 'directory permission preflight rejects a write-only destination', + { + skip: process.platform === 'win32' || process.getuid?.() === 0, + }, + () => + fixture(async (directory) => { + const writeOnly = path.join(directory, 'write-only'); + await mkdir(writeOnly); + try { + await chmod(writeOnly, 0o200); + await access(writeOnly, constants.W_OK); + await assert.rejects(ensureWritableDirectory(writeOnly), /write-only is not writable/); + } finally { + await chmod(writeOnly, 0o755); + } + }), +); + test( 'generation rejects an unwritable destination before extracting audio', { diff --git a/src/core/services/subtitle-generation.ts b/src/core/services/subtitle-generation.ts index ad26083a..0932ac96 100644 --- a/src/core/services/subtitle-generation.ts +++ b/src/core/services/subtitle-generation.ts @@ -125,9 +125,9 @@ async function ensureAvailableOutput(outputPath: string): Promise { } // Fail before extraction and transcription when the destination cannot take the file. -async function ensureWritableDirectory(directory: string): Promise { +export async function ensureWritableDirectory(directory: string): Promise { try { - await access(directory, constants.W_OK); + await access(directory, constants.W_OK | constants.X_OK); } catch { throw new Error(`Cannot save subtitles: ${directory} is not writable.`); }