From aeecb6e32a2d7bb09eccd18818aecda232da5088 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 14:08:49 -0400 Subject: [PATCH] =?UTF-8?q?fix(worker):=20conform=20=E2=80=94=20resolve=20?= =?UTF-8?q?clips=20from=20sequence=5Fclips=20instead=20of=20filename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Panel had been sending xmeml with clipitem/name = the local Premiere file path's basename (e.g. "dragonflight-Interstellar - Docking Scene 1080p IMAX HD.mp4"). The worker's old filename lookup ran SELECT id, original_s3_key FROM assets WHERE filename = $1 which never matched, because the assets row's filename is the original MAM ingest name without the "dragonflight-" prefix. Fix: when job.data has sequenceId (always set by the conform endpoint at routes/sequences.js:317), pull edits directly from sequence_clips, which the panel already wrote with authoritative asset_id mappings on push. We JOIN to assets for original_s3_key + filename and order by (timeline_in_frames, track) so segment indices stay deterministic. The XML is still parsed for sequence-level metadata (name, fps) when provided, but its clipitems are no longer authoritative. The legacy filename path (EDL input or fcpXml without sequenceId) stays unchanged for backward compatibility. Co-Authored-By: Claude Opus 4.7 --- services/worker/src/workers/conform.js | 106 +++++++++++++++++++------ 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/services/worker/src/workers/conform.js b/services/worker/src/workers/conform.js index f30a873..566f2ff 100644 --- a/services/worker/src/workers/conform.js +++ b/services/worker/src/workers/conform.js @@ -79,7 +79,7 @@ function parseFrame(value, fps) { } export const conformWorker = async (job) => { - const { edl, fcpXml, projectId, sequenceName, frameRate, codec, quality, resolution, audio } = job.data; + const { edl, fcpXml, projectId, sequenceId, sequenceName, frameRate, codec, quality, resolution, audio } = job.data; const jobId = job.id; const tmpDir = tmpdir(); @@ -92,8 +92,56 @@ export const conformWorker = async (job) => { let seqName = sequenceName || 'Conformed'; let seqFps = parseFloat(frameRate) || 29.97; - // Parse input — accept EDL, FCP XML, or structured JSON - if (edl) { + // ── Resolve edits ──────────────────────────────────────────────── + // + // Preference order: + // 1) sequenceId — read sequence_clips, which the Premiere panel + // populated with authoritative asset_id mappings on push. This + // avoids any filename matching, which is brittle because the + // panel's local Premiere file paths (e.g. "dragonflight-" + // with sanitised characters) do not match the original MAM + // filenames in the assets table. + // 2) edl — legacy EDL input, filename-resolved. + // 3) fcpXml — parse the XML for clipitems, filename-resolved. + // + // The XML is still parsed when sequenceId is also provided, because + // we want its sequence name + frame rate metadata even when the + // authoritative clip list comes from the DB. + if (sequenceId) { + await job.updateProgress(5); + console.log(`[conform] Resolving edits from sequence_clips for sequence ${sequenceId}`); + const clipRows = await query( + `SELECT sc.asset_id, sc.source_in_frames, sc.source_out_frames, + sc.timeline_in_frames, sc.timeline_out_frames, sc.track, + a.original_s3_key, a.filename + FROM sequence_clips sc + JOIN assets a ON a.id = sc.asset_id + WHERE sc.sequence_id = $1 + ORDER BY sc.timeline_in_frames ASC, sc.track ASC`, + [sequenceId] + ); + if (!clipRows.rows.length) { + throw new Error('Sequence has no clips. Push the timeline from Premiere first.'); + } + edits = clipRows.rows.map((r, i) => ({ + editNumber: i + 1, + reelName: r.filename, + asset_id: r.asset_id, + original_s3_key: r.original_s3_key, + sourceIn: r.source_in_frames, + sourceOut: r.source_out_frames, + })); + // Parse XML for sequence-level metadata if it's also provided. + if (fcpXml) { + try { + const parsed = parseFcpXml(fcpXml); + seqName = parsed.name || seqName; + seqFps = parsed.frameRate || seqFps; + } catch (e) { + console.warn(`[conform] XML metadata parse skipped: ${e.message}`); + } + } + } else if (edl) { await job.updateProgress(5); console.log(`[conform] Parsing EDL for job ${jobId}`); edits = parseEDL(edl).map((e, i) => ({ @@ -127,36 +175,42 @@ export const conformWorker = async (job) => { await job.updateProgress(Math.min(5 + (processedEdits / edits.length) * 50, 55)); console.log(`[conform] Processing edit ${edit.editNumber}: ${edit.reelName}`); - // BUG FIX #9: Scope asset lookup by project_id to prevent cross-project - // collisions when two projects contain assets with the same filename. - let assetRes; - if (projectId) { - assetRes = await query( - `SELECT id, original_s3_key FROM assets - WHERE filename = $1 AND project_id = $2 - LIMIT 1`, - [edit.reelName, projectId] - ); - // Fall back to unscoped lookup if no match in the current project - // (EDL reel names may reference assets not yet assigned to a project) - if (assetRes.rows.length === 0) { + // If the edit was resolved from sequence_clips above, the asset's + // original_s3_key is already attached — skip the filename lookup + // entirely (it would 0-match anyway because the panel's reelName + // is the local Premiere file path with "dragonflight-" prefix). + let sourceKey = edit.original_s3_key || null; + + if (!sourceKey) { + // Legacy path (EDL or fcpXml without sequenceId): match by filename, + // preferring same-project assets to avoid cross-project collisions. + let assetRes; + if (projectId) { + assetRes = await query( + `SELECT id, original_s3_key FROM assets + WHERE filename = $1 AND project_id = $2 + LIMIT 1`, + [edit.reelName, projectId] + ); + if (assetRes.rows.length === 0) { + assetRes = await query( + 'SELECT id, original_s3_key FROM assets WHERE filename = $1 LIMIT 1', + [edit.reelName] + ); + } + } else { assetRes = await query( 'SELECT id, original_s3_key FROM assets WHERE filename = $1 LIMIT 1', [edit.reelName] ); } - } else { - assetRes = await query( - 'SELECT id, original_s3_key FROM assets WHERE filename = $1 LIMIT 1', - [edit.reelName] - ); + + if (assetRes.rows.length === 0) { + throw new Error(`Asset not found for reel: ${edit.reelName}`); + } + sourceKey = assetRes.rows[0].original_s3_key; } - if (assetRes.rows.length === 0) { - throw new Error(`Asset not found for reel: ${edit.reelName}`); - } - - const { original_s3_key: sourceKey } = assetRes.rows[0]; const segmentInputPath = join(segmentsDir, `segment-${edit.editNumber}-src`); const segmentOutputPath = join(segmentsDir, `segment-${edit.editNumber}.mov`);