Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/next/fixed-issue-4159.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Creative Director plan render previews now sync to selected peers.
36 changes: 29 additions & 7 deletions server/services/sharing/peerSyncAssets.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,12 +362,12 @@ export async function buildTrackAssetManifest(track) {
* VIDEO renders are NOT hashed here: they live in the project's linked media
* collection, which federates as its own record and ships its bytes via that
* collection's manifest — duplicating them here would double the transfer.
* The music bed has no such alternate channel (unlike scene video, it isn't
* filed into the linked collection), so without bundling it here a subscribed
* peer would receive a dangling `musicBed.filename` reference — mirrors how
* `buildTrackAssetManifest` / `buildMusicVideoAssetManifest` bundle their
* `PATHS.music` audio. Each direct asset is missing-local-file skipped
* silently (mirrors buildAuthorAssetManifest).
* Directive-plan render steps do not enter that collection, so their settled
* image/video job ids are bundled directly. The music bed has no alternate
* channel either, so without bundling it a subscribed peer would receive a
* dangling `musicBed.filename` reference — mirrors how `buildTrackAssetManifest`
* / `buildMusicVideoAssetManifest` bundle their `PATHS.music` audio. Each direct
* asset is missing-local-file skipped silently (mirrors buildAuthorAssetManifest).
*/
export async function buildProjectAssetManifest(project) {
const entries = [];
Expand All @@ -381,6 +381,28 @@ export async function buildProjectAssetManifest(project) {
const musicEntry = await hashSimpleAsset(musicBedFilename, 'music', PATHS.music);
if (musicEntry) entries.push(musicEntry);
}
const planSteps = Array.isArray(project?.plan?.steps) ? project.plan.steps : [];
const renderEntries = await Promise.all(planSteps.map((step) => {
const jobId = step?.status === 'done' && isStr(step?.result?.jobId) && step.result.jobId.trim()
? step.result.jobId.trim()
: null;
if (!jobId) return null;
if (step.toolName === 'media_enqueueImageJob') {
return hashImageForManifest(`${jobId}.png`);
}
if (step.toolName === 'media_enqueueVideoJob') {
return hashSimpleAsset(`${jobId}.mp4`, 'video', PATHS.videos);
}
return null;
}));
const seen = new Set(entries.map((entry) => `${entry.kind}:${entry.filename}`));
for (const entry of renderEntries) {
const key = entry && `${entry.kind}:${entry.filename}`;
if (entry && !seen.has(key)) {
seen.add(key);
entries.push(entry);
}
}
return entries;
}

Expand Down Expand Up @@ -909,4 +931,4 @@ async function doPullOneAsset(peer, base, entry, urlPrefix, localDir, safeName)
const videoPath = join(localDir, safeName);
await generateThumbnail(videoPath, jobId).catch(() => null);
}
}
}
39 changes: 39 additions & 0 deletions server/services/sharing/peerSyncAssets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,45 @@ describe('buildProjectAssetManifest — first-pass music bed (#1928)', () => {
expect(manifest).toContainEqual(expect.objectContaining({ filename: 'music-gen-def.wav', kind: 'music', sha256: sha(musicBytes) }));
expect(manifest).toHaveLength(2);
});

it('bundles completed directive-plan image and video renders that have no collection channel (#4159)', async () => {
const imageBytes = Buffer.from('directive-image');
const videoBytes = Buffer.from('directive-video');
writeImage('plan-image.png', imageBytes);
writeVideo('plan-video.mp4', videoBytes);
const manifest = await buildProjectAssetManifest({
plan: {
steps: [
{ toolName: 'media_enqueueImageJob', status: 'done', result: { jobId: 'plan-image' } },
{ toolName: 'media_enqueueVideoJob', status: 'done', result: { jobId: 'plan-video' } },
],
},
});
expect(manifest).toContainEqual(expect.objectContaining({
filename: 'plan-image.png', kind: 'image', sha256: sha(imageBytes),
}));
expect(manifest).toContainEqual({ filename: 'plan-video.mp4', kind: 'video', sha256: sha(videoBytes) });
});

it('ignores unfinished, non-render, missing, and duplicate directive-plan results', async () => {
const imageBytes = Buffer.from('one-render');
writeImage('done-image.png', imageBytes);
const manifest = await buildProjectAssetManifest({
plan: {
steps: [
{ toolName: 'media_enqueueImageJob', status: 'done', result: { jobId: 'done-image' } },
{ toolName: 'media_enqueueImageJob', status: 'done', result: { jobId: 'done-image' } },
{ toolName: 'media_enqueueImageJob', status: 'done', result: { jobId: ' ' } },
{ toolName: 'media_enqueueImageJob', status: 'running', result: { jobId: 'still-running' } },
{ toolName: 'pipeline_createSeries', status: 'done', result: { jobId: 'not-media' } },
{ toolName: 'media_enqueueVideoJob', status: 'done', result: { jobId: 'missing-video' } },
],
},
});
expect(manifest).toEqual([expect.objectContaining({
filename: 'done-image.png', kind: 'image', sha256: sha(imageBytes),
})]);
});
});

describe('buildBoardAssetManifest — video items (#4188)', () => {
Expand Down