fix(worker): conform — resolve clips from sequence_clips instead of filename

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 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-05-28 14:08:49 -04:00
parent 0abef056e7
commit aeecb6e32a

View file

@ -79,7 +79,7 @@ function parseFrame(value, fps) {
} }
export const conformWorker = async (job) => { 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 jobId = job.id;
const tmpDir = tmpdir(); const tmpDir = tmpdir();
@ -92,8 +92,56 @@ export const conformWorker = async (job) => {
let seqName = sequenceName || 'Conformed'; let seqName = sequenceName || 'Conformed';
let seqFps = parseFloat(frameRate) || 29.97; let seqFps = parseFloat(frameRate) || 29.97;
// Parse input — accept EDL, FCP XML, or structured JSON // ── Resolve edits ────────────────────────────────────────────────
if (edl) { //
// 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-<name>"
// 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); await job.updateProgress(5);
console.log(`[conform] Parsing EDL for job ${jobId}`); console.log(`[conform] Parsing EDL for job ${jobId}`);
edits = parseEDL(edl).map((e, i) => ({ edits = parseEDL(edl).map((e, i) => ({
@ -127,8 +175,15 @@ export const conformWorker = async (job) => {
await job.updateProgress(Math.min(5 + (processedEdits / edits.length) * 50, 55)); await job.updateProgress(Math.min(5 + (processedEdits / edits.length) * 50, 55));
console.log(`[conform] Processing edit ${edit.editNumber}: ${edit.reelName}`); console.log(`[conform] Processing edit ${edit.editNumber}: ${edit.reelName}`);
// BUG FIX #9: Scope asset lookup by project_id to prevent cross-project // If the edit was resolved from sequence_clips above, the asset's
// collisions when two projects contain assets with the same filename. // 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; let assetRes;
if (projectId) { if (projectId) {
assetRes = await query( assetRes = await query(
@ -137,8 +192,6 @@ export const conformWorker = async (job) => {
LIMIT 1`, LIMIT 1`,
[edit.reelName, projectId] [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 (assetRes.rows.length === 0) {
assetRes = await query( assetRes = await query(
'SELECT id, original_s3_key FROM assets WHERE filename = $1 LIMIT 1', 'SELECT id, original_s3_key FROM assets WHERE filename = $1 LIMIT 1',
@ -155,8 +208,9 @@ export const conformWorker = async (job) => {
if (assetRes.rows.length === 0) { if (assetRes.rows.length === 0) {
throw new Error(`Asset not found for reel: ${edit.reelName}`); throw new Error(`Asset not found for reel: ${edit.reelName}`);
} }
sourceKey = assetRes.rows[0].original_s3_key;
}
const { original_s3_key: sourceKey } = assetRes.rows[0];
const segmentInputPath = join(segmentsDir, `segment-${edit.editNumber}-src`); const segmentInputPath = join(segmentsDir, `segment-${edit.editNumber}-src`);
const segmentOutputPath = join(segmentsDir, `segment-${edit.editNumber}.mov`); const segmentOutputPath = join(segmentsDir, `segment-${edit.editNumber}.mov`);