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 0d105e9f..4a8bd77e 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -12,6 +12,7 @@ import { useClient } from 'hooks/useClient'; import type { TaskSummary } from '../../useTasks'; import { useTaskRunner } from './useTaskRunner'; import { usePipelineRunner } from './usePipelineRunner'; +import { useFilesAPI, type AudioTrack, type SubtitleTrack } from '../../../../hooks/useFilesAPI'; const playDing = () => { const ctx = new AudioContext(); @@ -61,7 +62,7 @@ const PiMonoInner = ({ taskDirName, defaultInput, cwd, initialModel, taskInfo, s // Fetch task detail for inputs and body useEffect(() => { client.get<{ inputs?: Record; body?: string }>(`/tasks/${taskDirName}`).then((task) => { - const defs = task.inputs ?? {}; + const defs: Record = task.inputs ?? {}; setInputDefs(defs); setTaskBody(task.body ?? null); // Initialize from defaults and autofill @@ -260,15 +261,67 @@ type TaskInputFormProps = { values: Record; onChange: (key: string, value: string) => void; autoFilledKeys: Set; + audioTracks?: AudioTrack[]; + subtitleTracks?: SubtitleTrack[]; + probing?: boolean; + entryType?: 'file' | 'directory'; }; -const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys }: 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}`; + +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; return (
{configurableInputs.map(([key, def]) => { + if (def.type === 'audio_tracks' || def.type === 'subtitle_tracks') { + const isAudio = def.type === 'audio_tracks'; + const tracks = (isAudio ? audioTracks : subtitleTracks) ?? []; + const selected = new Set( + (values[key] ?? '') + .split(',') + .filter(Boolean) + .map(Number) + .filter((n) => !Number.isNaN(n)), + ); + const toggle = (id: number) => { + const next = new Set(selected); + if (next.has(id)) next.delete(id); + else next.add(id); + const ids = [...next].sort((a, b) => a - b); + onChange(key, ids.length > 0 ? ids.join(',') : 'none'); + }; + return ( +
+ {def.description ?? key} + {entryType !== 'file' ? ( + All tracks kept (folder mode) + ) : probing ? ( + + Probing… + + ) : tracks.length === 0 ? ( + None + ) : ( +
+ {tracks.map((t) => ( + + ))} +
+ )} +
+ ); + } + if (def.type === 'boolean') { const isTrue = values[key] === 'true'; return ( @@ -338,19 +391,25 @@ type ScriptRunnerProps = { autoInputs: Record; context: Record; cwd?: string; + entryType?: 'file' | 'directory'; + filePath?: string; }; -const ScriptRunner = ({ taskDirName, autoInputs, context, cwd }: ScriptRunnerProps) => { +const ScriptRunner = ({ taskDirName, autoInputs, context, cwd, entryType, filePath }: ScriptRunnerProps) => { const runner = useTaskRunner(); const client = useClient(); + const files = useFilesAPI('home'); const bottomRef = useRef(null); const [inputDefs, setInputDefs] = useState | null>(null); const [formValues, setFormValues] = useState>({}); + const [audioTracks, setAudioTracks] = useState([]); + const [subtitleTracks, setSubtitleTracks] = useState([]); + const [probing, setProbing] = useState(false); // Fetch task detail to get input definitions useEffect(() => { client.get<{ inputs?: Record }>(`/tasks/${taskDirName}`).then((task) => { - const defs = task.inputs ?? {}; + const defs: Record = task.inputs ?? {}; setInputDefs(defs); // Initialize from autofill context, then defaults const initial: Record = {}; @@ -359,6 +418,30 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd }: ScriptRunnerPro else if (def.default !== undefined) initial[key] = def.default; } setFormValues(initial); + + // If the task has track pickers and we're on a single file, probe it and seed keep-all. + const hasAudio = Object.values(defs).some((d) => d.type === 'audio_tracks'); + const hasSubs = Object.values(defs).some((d) => d.type === 'subtitle_tracks'); + if ((hasAudio || hasSubs) && entryType === 'file' && filePath) { + setProbing(true); + Promise.all([ + hasAudio ? files.audioTracks(filePath).catch(() => [] as AudioTrack[]) : Promise.resolve([] as AudioTrack[]), + hasSubs ? files.subtitles(filePath).catch(() => [] as SubtitleTrack[]) : Promise.resolve([] as SubtitleTrack[]), + ]) + .then(([aud, sub]: [AudioTrack[], SubtitleTrack[]]) => { + setAudioTracks(aud); + setSubtitleTracks(sub); + setFormValues((prev) => { + const next = { ...prev }; + 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'; + } + return next; + }); + }) + .finally(() => setProbing(false)); + } }); }, [taskDirName]); @@ -404,12 +487,16 @@ const ScriptRunner = ({ taskDirName, autoInputs, context, cwd }: ScriptRunnerPro values={formValues} onChange={handleInputChange} autoFilledKeys={autoFilledKeys} + audioTracks={audioTracks} + subtitleTracks={subtitleTracks} + probing={probing} + entryType={entryType} /> )}