From 7a4153eda07fdcadda31db443972cb5b01bd9c40 Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Tue, 21 Jul 2026 18:10:22 -0400 Subject: [PATCH] Desktop: support multicam filter pipelines (filter_*_2-cam / 3-cam) Pipes with a camera suffix are categorized under '2-cam'/'3-cam', so a multicam filter pipe like filter_register_frames_2-cam.pipe never matched the type === 'filter' checks: its image writers got no output prefix and no new dataset was created from the results. Filter output handling is now recognized from the pipe filename as well as the type, and the output prefix is passed to the per-camera writers (image_writer, image_writer2, image_writer3) that multicam filter pipes use; extra -s keys for absent processes are ignored by the runner. Co-Authored-By: Claude Fable 5 --- .../platform/desktop/backend/native/viame.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/client/platform/desktop/backend/native/viame.ts b/client/platform/desktop/backend/native/viame.ts index 2ab5f8dd0..ed65010fa 100644 --- a/client/platform/desktop/backend/native/viame.ts +++ b/client/platform/desktop/backend/native/viame.ts @@ -158,6 +158,13 @@ async function runPipeline( ): Promise { const { datasetId, pipeline } = runPipelineArgs; const frameRange = runPipelineArgs.pipelineParams?.runtimeParams?.frameRange ?? undefined; + // Pipes with a camera suffix (e.g. filter_register_frames_2-cam.pipe) are + // categorized under '2-cam'/'3-cam' rather than by their filename prefix, + // so output handling (image writing, new-dataset creation) is recognized + // from the pipe filename as well as the type. + const createsNewDataset = pipelineCreatesDatasetMarkers.includes(pipeline.type) + || pipelineCreatesDatasetMarkers.some((marker) => pipeline.pipe.startsWith(`${marker}_`)); + const isFilterPipe = pipeline.type === 'filter' || pipeline.pipe.startsWith('filter_'); const isValid = await validateViamePath(settings); if (isValid !== true) { @@ -175,7 +182,7 @@ async function runPipeline( const timestamp = (new Date()).toISOString().replace(/[:.]/g, '-'); const outputDirName = `${runPipelineArgs.pipeline.name}_${runPipelineArgs.datasetId}_${timestamp}`; const outputDir = `${npath.join(settings.dataPath, JobsOutputFolderName, outputDirName)}`; - if (pipelineCreatesDatasetMarkers.includes(runPipelineArgs.pipeline.type)) { + if (createsNewDataset) { if (outputDir !== jobWorkDir) { await fs.mkdir(outputDir, { recursive: true }); } @@ -185,7 +192,7 @@ async function runPipeline( const trackOutputFileName = 'track_output.csv'; let trackOutput: string; let detectorOutput: string; - if (pipelineCreatesDatasetMarkers.includes(runPipelineArgs.pipeline.type)) { + if (createsNewDataset) { detectorOutput = npath.join(outputDir, detectorOutputFileName); trackOutput = npath.join(outputDir, trackOutputFileName); } else { @@ -246,7 +253,7 @@ async function runPipeline( command.push(`-s downsampler:frame_range_is_native=${isNative}`); // Transcode/filter pipes: output frames renumbered relative to new range // All other pipes: output frames relative to original video - const renumber = pipeline.type === 'transcode' || pipeline.type === 'filter'; + const renumber = pipeline.type === 'transcode' || isFilterPipe; command.push(`-s downsampler:renumber_frames=${renumber}`); command.push(`-s downsampler:adjust_timestamps=${renumber}`); } @@ -285,9 +292,14 @@ async function runPipeline( } } - if (runPipelineArgs.pipeline.type === 'filter') { + if (isFilterPipe) { command.push(`-s kwa_writer:output_directory="${outputDir}/"`); + // Multicam filter pipes have one writer per camera (image_writer, + // image_writer2, image_writer3); extra -s keys for absent processes are + // ignored by the runner. command.push(`-s image_writer:file_name_prefix="${outputDir}/"`); + command.push(`-s image_writer2:file_name_prefix="${outputDir}/"`); + command.push(`-s image_writer3:file_name_prefix="${outputDir}/"`); } let transcodedFilename: string; @@ -395,7 +407,7 @@ async function runPipeline( job.on('exit', async (code) => { if (code === 0) { try { - if (!pipelineCreatesDatasetMarkers.includes(runPipelineArgs.pipeline.type)) { + if (!createsNewDataset) { let finalDetectorOutput = detectorOutput; let finalTrackOutput = trackOutput; @@ -429,7 +441,7 @@ async function runPipeline( } // Check if this is a transcode/filter pipeline and create a new dataset - if (pipelineCreatesDatasetMarkers.includes(runPipelineArgs.pipeline.type)) { + if (createsNewDataset) { updater({ ...jobBase, body: ['Creating dataset from output...'],