diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx
index 084777e1..b356cbd7 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx
@@ -270,6 +270,19 @@ type TaskInputFormProps = {
const trackLabel = (t: { title: string; lang: string; id: number }) =>
t.title || (t.lang && t.lang !== 'und' && t.lang !== 'unknown' ? t.lang.toUpperCase() : '') || `Track ${t.id + 1}`;
+// subtitle_edit input: per-subtitle keep flag + editable label, serialized to JSON in the form value.
+type SubtitleEditEntry = { id: number; keep: boolean; label: string };
+
+const parseSubtitleSpec = (raw?: string): SubtitleEditEntry[] => {
+ if (!raw) return [];
+ try {
+ const parsed = JSON.parse(raw);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+};
+
const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTracks, subtitleTracks, probing, entryType }: TaskInputFormProps) => {
const configurableInputs = Object.entries(inputDefs).filter(([key]) => !autoFilledKeys.has(key));
if (configurableInputs.length === 0) return null;
@@ -277,6 +290,58 @@ const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys, audioTrack
return (
{configurableInputs.map(([key, def]) => {
+ if (def.type === 'subtitle_edit') {
+ const tracks = subtitleTracks ?? [];
+ const byId = new Map(parseSubtitleSpec(values[key]).map((e) => [e.id, e]));
+ const entryFor = (t: SubtitleTrack): SubtitleEditEntry => byId.get(t.id) ?? { id: t.id, keep: true, label: trackLabel(t) };
+ const update = (id: number, patch: Partial
) =>
+ onChange(key, JSON.stringify(tracks.map((t) => (t.id === id ? { ...entryFor(t), ...patch } : entryFor(t)))));
+ const keptCount = tracks.filter((t) => entryFor(t).keep).length;
+ return (
+
+
{def.description ?? key}
+ {probing ? (
+
+ {entryType === 'directory' ? 'Analyzing files…' : 'Probing…'}
+
+ ) : tracks.length === 0 ? (
+
No subtitles
+ ) : (
+ <>
+
+
+ Keeping {keptCount} of {tracks.length}
+
+ >
+ )}
+
+ );
+ }
+
if (def.type === 'audio_tracks' || def.type === 'subtitle_tracks') {
const isAudio = def.type === 'audio_tracks';
const tracks = (isAudio ? audioTracks : subtitleTracks) ?? [];
@@ -470,7 +535,7 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa
// Track pickers: audio defaults to keep-all, subtitles to keep-none. A folder probes every
// video and drives the pickers off the largest matching group (the rest convert separately).
const hasTrackPickers = Object.values(defs).some(
- (d) => d.type === 'audio_tracks' || d.type === 'subtitle_tracks',
+ (d) => d.type === 'audio_tracks' || d.type === 'subtitle_tracks' || d.type === 'subtitle_edit',
);
if (!hasTrackPickers || !filePath) return;
@@ -482,6 +547,8 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePa
for (const [key, def] of Object.entries(defs)) {
if (def.type === 'audio_tracks') next[key] = aud.map((t) => t.id).join(',') || 'none';
if (def.type === 'subtitle_tracks') next[key] = 'none';
+ if (def.type === 'subtitle_edit')
+ next[key] = JSON.stringify(sub.map((t) => ({ id: t.id, keep: true, label: trackLabel(t) })));
}
return next;
});