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:
parent
0abef056e7
commit
aeecb6e32a
1 changed files with 80 additions and 26 deletions
|
|
@ -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-<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);
|
||||
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`);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue