@cliangdev/conductor 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/claude/skills/conductor-creative/SKILL.md +24 -0
- package/assets/cli-manifest.json +6 -1
- package/dist/commands/creative.d.ts.map +1 -1
- package/dist/commands/creative.js +4 -1
- package/dist/commands/creative.js.map +1 -1
- package/dist/creative/placements.js +28 -0
- package/dist/creative/placements.json +36 -0
- package/dist/lib/creative-render.d.ts +9 -3
- package/dist/lib/creative-render.d.ts.map +1 -1
- package/dist/lib/creative-render.js +28 -4
- package/dist/lib/creative-render.js.map +1 -1
- package/dist/lib/media-probe.d.ts +55 -0
- package/dist/lib/media-probe.d.ts.map +1 -0
- package/dist/lib/media-probe.js +221 -0
- package/dist/lib/media-probe.js.map +1 -0
- package/dist/lib/poster-sheet.d.ts +10 -0
- package/dist/lib/poster-sheet.d.ts.map +1 -0
- package/dist/lib/poster-sheet.js +57 -0
- package/dist/lib/poster-sheet.js.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +36 -7
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tools/creatives.d.ts +23 -2
- package/dist/mcp/tools/creatives.d.ts.map +1 -1
- package/dist/mcp/tools/creatives.js +213 -27
- package/dist/mcp/tools/creatives.js.map +1 -1
- package/package.json +3 -1
|
@@ -121,6 +121,30 @@ worst. A logo lockup and CTA claim land on the last beat only by default; `cta:
|
|
|
121
121
|
beat overrides that. A carousel renders every card at one shared aspect ratio (`carouselRatio`),
|
|
122
122
|
since the destination platforms require that.
|
|
123
123
|
|
|
124
|
+
## Video you already have
|
|
125
|
+
|
|
126
|
+
Not every video needs the brand layout above — a finished clip (a demo recording, an existing edit,
|
|
127
|
+
something a person or another tool already produced) can go up as-is. That's a CLIP Creative: no
|
|
128
|
+
photo, headline or layout, just the video.
|
|
129
|
+
|
|
130
|
+
1. `upload_creative_media` (local path or URL) — same provenance fields as a photo (`source`,
|
|
131
|
+
`licence`, `aiGenerated`). It detects video vs. audio vs. photo automatically and, for a video,
|
|
132
|
+
extracts a poster frame. Use the returned media's id below.
|
|
133
|
+
2. `create_creative` with `kind: "CLIP"` and `clipMedia`. One file for every placement:
|
|
134
|
+
`{"default": mediaId}`. A different cut per placement (e.g. a square edit for feed, a vertical one
|
|
135
|
+
for Reels): `{"9x16": mediaId, "4x5": mediaId}` — `default` still covers any placement without its
|
|
136
|
+
own entry. A reel takes exactly one video, so don't split a reel-eligible placement across two.
|
|
137
|
+
3. `render_creative` — instant for a CLIP (the backend assembles the frames; nothing renders locally
|
|
138
|
+
here the way a STILL creative's browser-driven render does).
|
|
139
|
+
4. `preview_creative` — returns a small poster sheet across placements instead of the STILL contact
|
|
140
|
+
sheet, plus each placement's duration. Still worth a look: a poster is one frame, not the whole
|
|
141
|
+
cut, so open the actual video before calling a rough or misframed clip done.
|
|
142
|
+
5. `attach_creative_to_post`, same as any other Creative.
|
|
143
|
+
|
|
144
|
+
Mind each destination's own duration limit (per docs/publishing.md) — a clip that's fine on one
|
|
145
|
+
platform may get rejected or trimmed on another; check before attaching to a Post that fans out
|
|
146
|
+
broadly.
|
|
147
|
+
|
|
124
148
|
## Failures that are silent (carried over from this skill's predecessor)
|
|
125
149
|
|
|
126
150
|
Automated checks are real but incomplete. These are the ones worth remembering to look for by eye:
|
package/assets/cli-manifest.json
CHANGED
|
@@ -752,9 +752,14 @@
|
|
|
752
752
|
"version"
|
|
753
753
|
]
|
|
754
754
|
},
|
|
755
|
+
{
|
|
756
|
+
"name": "upload_creative_media",
|
|
757
|
+
"description": "Upload a photo, video or audio file into the project's media library from a local file or a public URL, recording its provenance for the readiness checklist. Kind, dimensions, duration and audio are detected automatically; a video also gets a poster frame.",
|
|
758
|
+
"category": "marketing"
|
|
759
|
+
},
|
|
755
760
|
{
|
|
756
761
|
"name": "upload_creative_photo",
|
|
757
|
-
"description": "
|
|
762
|
+
"description": "Deprecated alias for upload_creative_media — same behavior, including for video/audio.",
|
|
758
763
|
"category": "marketing"
|
|
759
764
|
},
|
|
760
765
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"creative.d.ts","sourceRoot":"","sources":["../../src/commands/creative.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKnC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"creative.d.ts","sourceRoot":"","sources":["../../src/commands/creative.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKnC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoEvD"}
|
|
@@ -43,7 +43,10 @@ Examples:
|
|
|
43
43
|
for (const frame of result.frames) {
|
|
44
44
|
const label = frame.sequenceIndex != null ? `${frame.placementKey}[${frame.sequenceIndex}]` : frame.placementKey;
|
|
45
45
|
const size = frame.width && frame.height ? `${frame.width}x${frame.height}` : '';
|
|
46
|
-
|
|
46
|
+
// CLIP frames are videos, not JPEGs — durationSeconds is only ever set on those.
|
|
47
|
+
const duration = frame.durationSeconds != null ? `${frame.durationSeconds}s` : '';
|
|
48
|
+
const parts = [label, size, duration, frame.url ?? ''].filter(Boolean);
|
|
49
|
+
console.log(` ${parts.join(' ')}`);
|
|
47
50
|
}
|
|
48
51
|
if (result.error)
|
|
49
52
|
console.error(result.error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"creative.js","sourceRoot":"","sources":["../../src/commands/creative.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAE1D,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,oDAAoD,CAAC,CAAA;IAE9G,QAAQ;SACL,OAAO,CAAC,qBAAqB,CAAC;SAC9B,WAAW,CAAC,kGAAkG,CAAC;SAC/G,MAAM,CAAC,WAAW,EAAE,4DAA4D,CAAC;SACjF,MAAM,CAAC,mBAAmB,EAAE,qDAAqD,CAAC;SAClF,MAAM,CAAC,wBAAwB,EAAE,yCAAyC,CAAC;SAC3E,WAAW,CACV,OAAO,EACP;;;;8EAIwE,CACzE;SACA,MAAM,CAAC,KAAK,EAAE,UAAkB,EAAE,OAAyE,EAAE,EAAE;QAC9G,IAAI,MAAM,CAAA;QACV,IAAI,CAAC;YACH,MAAM,GAAG,SAAS,EAAE,CAAA;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;YAC/D,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;YACpB,OAAM;QACR,CAAC;QAED,MAAM,UAAU,GAAG,cAAc,CAAC,MAAM,CAAC,CAAA;QACzC,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CACX,2CAA2C,OAAO,CAAC,GAAG,EAAE,0CAA0C;gBAChG,uFAAuF,CAC1F,CAAA;YACD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;YACpB,OAAM;QACR,CAAC;QACD,MAAM,CAAC,SAAS,GAAG,UAAU,CAAC,SAAS,CAAA;QAEvC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,cAAc,CACjC;gBACE,UAAU,EAAE,MAAM,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC;gBACvD,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO;gBAC9B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,KAAK;gBACnC,aAAa,EAAE,OAAO,CAAC,aAAa;aACrC,EACD,MAAM,EACN,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAC7C,CAAA;YAED,OAAO,CAAC,GAAG,CAAC,UAAU,MAAM,CAAC,QAAQ,IAAI,QAAQ,KAAK,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;YAC7G,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAClC,MAAM,KAAK,GAAG,KAAK,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAA;gBAChH,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;gBAChF,
|
|
1
|
+
{"version":3,"file":"creative.js","sourceRoot":"","sources":["../../src/commands/creative.ts"],"names":[],"mappings":"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"}
|
|
@@ -195,6 +195,34 @@ export const placements = {
|
|
|
195
195
|
cardGap: 56,
|
|
196
196
|
},
|
|
197
197
|
},
|
|
198
|
+
'16x9': {
|
|
199
|
+
w: 1920,
|
|
200
|
+
h: 1080,
|
|
201
|
+
label: '16:9 YouTube and landscape video',
|
|
202
|
+
platform: 'youtube',
|
|
203
|
+
default: false,
|
|
204
|
+
safe: { top: 0, bottom: 0, right: 0 },
|
|
205
|
+
fit: { max: 72, min: 34, lines: 3 },
|
|
206
|
+
badgeHeight: 56,
|
|
207
|
+
scale: {
|
|
208
|
+
lockupGap: 13,
|
|
209
|
+
iconSize: 44,
|
|
210
|
+
iconRadius: 11,
|
|
211
|
+
markHeight: 27,
|
|
212
|
+
chipPadding: '8px 20px 8px 8px',
|
|
213
|
+
chipGap: 12,
|
|
214
|
+
bodySize: 23,
|
|
215
|
+
bodyLineHeight: 1.3,
|
|
216
|
+
bodyMaxWidth: 880,
|
|
217
|
+
bodyColor: 'rgba(255,255,255,0.75)',
|
|
218
|
+
ctaGap: 24,
|
|
219
|
+
ctaMarginTop: 6,
|
|
220
|
+
noteSize: 21,
|
|
221
|
+
noteColor: 'rgba(255,255,255,0.6)',
|
|
222
|
+
cardPad: '64px 64px 72px 64px',
|
|
223
|
+
cardGap: 40,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
198
226
|
};
|
|
199
227
|
|
|
200
228
|
export default placements;
|
|
@@ -228,5 +228,41 @@
|
|
|
228
228
|
"cardPad": "72px 72px 88px 72px",
|
|
229
229
|
"cardGap": 56
|
|
230
230
|
}
|
|
231
|
+
},
|
|
232
|
+
"16x9": {
|
|
233
|
+
"w": 1920,
|
|
234
|
+
"h": 1080,
|
|
235
|
+
"label": "16:9 YouTube and landscape video",
|
|
236
|
+
"platform": "youtube",
|
|
237
|
+
"default": false,
|
|
238
|
+
"safe": {
|
|
239
|
+
"top": 0,
|
|
240
|
+
"bottom": 0,
|
|
241
|
+
"right": 0
|
|
242
|
+
},
|
|
243
|
+
"fit": {
|
|
244
|
+
"max": 72,
|
|
245
|
+
"min": 34,
|
|
246
|
+
"lines": 3
|
|
247
|
+
},
|
|
248
|
+
"badgeHeight": 56,
|
|
249
|
+
"scale": {
|
|
250
|
+
"lockupGap": 13,
|
|
251
|
+
"iconSize": 44,
|
|
252
|
+
"iconRadius": 11,
|
|
253
|
+
"markHeight": 27,
|
|
254
|
+
"chipPadding": "8px 20px 8px 8px",
|
|
255
|
+
"chipGap": 12,
|
|
256
|
+
"bodySize": 23,
|
|
257
|
+
"bodyLineHeight": 1.3,
|
|
258
|
+
"bodyMaxWidth": 880,
|
|
259
|
+
"bodyColor": "rgba(255,255,255,0.75)",
|
|
260
|
+
"ctaGap": 24,
|
|
261
|
+
"ctaMarginTop": 6,
|
|
262
|
+
"noteSize": 21,
|
|
263
|
+
"noteColor": "rgba(255,255,255,0.6)",
|
|
264
|
+
"cardPad": "64px 64px 72px 64px",
|
|
265
|
+
"cardGap": 40
|
|
266
|
+
}
|
|
231
267
|
}
|
|
232
268
|
}
|
|
@@ -9,6 +9,11 @@ export interface RenderFrame {
|
|
|
9
9
|
height?: number;
|
|
10
10
|
sizeBytes?: number;
|
|
11
11
|
warnings?: string[];
|
|
12
|
+
/** Video-frame fields — only present on a CLIP creative's frames, which reference a video, not a JPEG. */
|
|
13
|
+
durationSeconds?: number;
|
|
14
|
+
hasAudio?: boolean;
|
|
15
|
+
posterUrl?: string;
|
|
16
|
+
contentType?: string;
|
|
12
17
|
}
|
|
13
18
|
export interface RenderCreativeParams {
|
|
14
19
|
creativeId: string;
|
|
@@ -24,9 +29,10 @@ export interface RenderCreativeResult {
|
|
|
24
29
|
frames: RenderFrame[];
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
|
-
* Renders a Creative end to end
|
|
28
|
-
* API,
|
|
29
|
-
* render itself succeeded — a FAILED render still has an id and an error message worth
|
|
32
|
+
* Renders a Creative end to end. STILL creatives find a local browser and run the render core against
|
|
33
|
+
* the external API, reading back the stored render (frames with signed URLs, final state) whether or
|
|
34
|
+
* not the render itself succeeded — a FAILED render still has an id and an error message worth
|
|
35
|
+
* returning. CLIP (and MOTION) creatives never launch a browser — see {@link renderViaApi}.
|
|
30
36
|
*/
|
|
31
37
|
export declare function renderCreative(params: RenderCreativeParams, config: Config, log?: (...args: unknown[]) => void): Promise<RenderCreativeResult>;
|
|
32
38
|
//# sourceMappingURL=creative-render.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"creative-render.d.ts","sourceRoot":"","sources":["../../src/lib/creative-render.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAI9C,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"creative-render.d.ts","sourceRoot":"","sources":["../../src/lib/creative-render.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAI9C,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,0GAA0G;IAC1G,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAA;IACX,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,WAAW,EAAE,CAAA;CACtB;AA+ED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAe,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAyC/B"}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { apiGet } from '../mcp/api.js';
|
|
13
|
+
import { apiGet, apiPost, ApiError } from '../mcp/api.js';
|
|
14
14
|
import { findBrowserFactory } from './creative-browser.js';
|
|
15
15
|
/** Where `npm run build` copies `conductor-creative/`'s runtime files, relative to this file's own
|
|
16
16
|
* compiled location (`dist/lib/creative-render.js` -> `dist/creative/job/`). */
|
|
@@ -34,11 +34,35 @@ async function loadRenderCore() {
|
|
|
34
34
|
return { run: renderMod.run, createApiTransport: transportMod.createApiTransport };
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* A CLIP (or, once PR 2 lands, MOTION) creative has no local browser render path — the backend either
|
|
38
|
+
* assembles the frames itself synchronously (CLIP) or refuses with a 422 explaining why (MOTION, until
|
|
39
|
+
* its renderer ships). Either way this just POSTs the render and relays what came back — `ApiError`'s
|
|
40
|
+
* `message` is already the server's own plain-English sentence (an RFC 7807 `detail`), so a MOTION 422
|
|
41
|
+
* reaches the caller unchanged rather than wrapped in a generic "never started" message.
|
|
42
|
+
*/
|
|
43
|
+
async function renderViaApi(params, config) {
|
|
44
|
+
try {
|
|
45
|
+
const detail = await apiPost(`/api/v2/projects/${config.projectId}/marketing/creatives/${params.creativeId}/renders`, { previewOnly: !!params.previewOnly, renderer: params.renderer, workflowRunId: params.workflowRunId }, config);
|
|
46
|
+
return { ok: detail.state === 'SUCCEEDED', renderId: detail.id, state: detail.state, error: detail.error ?? null, frames: detail.frames ?? [] };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof ApiError) {
|
|
50
|
+
return { ok: false, error: err.message, frames: [] };
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Renders a Creative end to end. STILL creatives find a local browser and run the render core against
|
|
57
|
+
* the external API, reading back the stored render (frames with signed URLs, final state) whether or
|
|
58
|
+
* not the render itself succeeded — a FAILED render still has an id and an error message worth
|
|
59
|
+
* returning. CLIP (and MOTION) creatives never launch a browser — see {@link renderViaApi}.
|
|
40
60
|
*/
|
|
41
61
|
export async function renderCreative(params, config, log = () => { }) {
|
|
62
|
+
const creative = await apiGet(`/api/v2/projects/${config.projectId}/marketing/creatives/${params.creativeId}`, config);
|
|
63
|
+
if (creative.kind && creative.kind !== 'STILL') {
|
|
64
|
+
return renderViaApi(params, config);
|
|
65
|
+
}
|
|
42
66
|
const { run, createApiTransport } = await loadRenderCore();
|
|
43
67
|
const transport = createApiTransport({
|
|
44
68
|
apiUrl: config.apiUrl,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"creative-render.js","sourceRoot":"","sources":["../../src/lib/creative-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"creative-render.js","sourceRoot":"","sources":["../../src/lib/creative-render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type MediaKind = 'IMAGE' | 'VIDEO' | 'AUDIO';
|
|
2
|
+
export interface MediaProbeResult {
|
|
3
|
+
kind: MediaKind;
|
|
4
|
+
contentType: string;
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
durationSeconds?: number;
|
|
8
|
+
hasAudio?: boolean;
|
|
9
|
+
codec?: string;
|
|
10
|
+
}
|
|
11
|
+
interface FfprobeStream {
|
|
12
|
+
codec_type?: string;
|
|
13
|
+
codec_name?: string;
|
|
14
|
+
width?: number;
|
|
15
|
+
height?: number;
|
|
16
|
+
duration?: string;
|
|
17
|
+
nb_frames?: string;
|
|
18
|
+
disposition?: {
|
|
19
|
+
attached_pic?: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
interface FfprobeFormat {
|
|
23
|
+
duration?: string;
|
|
24
|
+
format_name?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface FfprobeJson {
|
|
27
|
+
streams?: FfprobeStream[];
|
|
28
|
+
format?: FfprobeFormat;
|
|
29
|
+
}
|
|
30
|
+
export declare const NO_FFPROBE_HINT: string;
|
|
31
|
+
export declare const NO_FFMPEG_HINT: string;
|
|
32
|
+
/**
|
|
33
|
+
* Pure parsing of an already-fetched `ffprobe -show_streams -show_format` JSON document — split out
|
|
34
|
+
* from `probeMedia` so tests can exercise it against fixture JSON without spawning a real process.
|
|
35
|
+
*
|
|
36
|
+
* A video stream marked `disposition.attached_pic` is cover art embedded in an audio file (common in
|
|
37
|
+
* mp3/m4a), not a picture track — it's excluded from the video-stream check so a song with cover art
|
|
38
|
+
* doesn't get classified as VIDEO. A video stream with no duration and at most one frame is a still
|
|
39
|
+
* image ffprobe happened to be pointed at (format `image2`/`png_pipe`/etc.), not a video.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseProbeJson(json: FfprobeJson, filePath: string): MediaProbeResult;
|
|
42
|
+
export declare function resolveFfprobePath(): Promise<string>;
|
|
43
|
+
export declare function resolveFfmpegPath(): Promise<string>;
|
|
44
|
+
/** Test-only: binary discovery caches its result for the process lifetime (real launches are slow
|
|
45
|
+
* enough to matter within one CLI/MCP run), so tests that vary system-binary availability need to
|
|
46
|
+
* reset it between cases. */
|
|
47
|
+
export declare function resetMediaBinaryCache(): void;
|
|
48
|
+
/** Probes a local media file with ffprobe and classifies it as IMAGE, VIDEO or AUDIO. Throws
|
|
49
|
+
* `NO_FFPROBE_HINT` when no usable ffprobe binary can be found at all. */
|
|
50
|
+
export declare function probeMedia(filePath: string): Promise<MediaProbeResult>;
|
|
51
|
+
/** Extracts a single JPEG poster frame from a video at `atSeconds` (default 1s). Falls back to 0s
|
|
52
|
+
* when the clip is shorter than that (or the seek otherwise fails) rather than giving up. */
|
|
53
|
+
export declare function extractPoster(videoPath: string, outJpg: string, atSeconds?: number): Promise<void>;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=media-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-probe.d.ts","sourceRoot":"","sources":["../../src/lib/media-probe.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAA;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACxC;AAED,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,aAAa,CAAA;CACvB;AAED,eAAO,MAAM,eAAe,QAE8D,CAAA;AAC1F,eAAO,MAAM,cAAc,QAEiD,CAAA;AA0B5E;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAmCpF;AA8BD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAiB1D;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAgBzD;AAED;;6BAE6B;AAC7B,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAwBD;0EAC0E;AAC1E,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAI5E;AAgBD;6FAC6F;AAC7F,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAQnG"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inspects and extracts frames from video/audio Creative media with ffprobe/ffmpeg: a system binary
|
|
3
|
+
* on PATH first (respects whatever the user already has — no bundled Chromium-sized re-download),
|
|
4
|
+
* else the ones bundled by `ffprobe-static`/`ffmpeg-static` (lazy-imported so requiring them doesn't
|
|
5
|
+
* add to MCP server startup cost for sessions that never touch a video/audio Creative). Mirrors the
|
|
6
|
+
* browser discovery in creative-browser.ts.
|
|
7
|
+
*
|
|
8
|
+
* `upload_creative_media` uses `probeMedia` for VIDEO/AUDIO (images keep the existing magic-byte
|
|
9
|
+
* header parse in image-dimensions.ts — no decode needed there); it and `render_creative`/CLIP use
|
|
10
|
+
* `extractPoster` for a VIDEO's poster JPEG.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { extname } from 'node:path';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
// ffprobe-static/ffmpeg-static are plain CJS with no published types; `require` (rather than a typed
|
|
17
|
+
// `import`) keeps them lazy (only resolved the first time a binary is actually needed) without fighting
|
|
18
|
+
// TypeScript over declarations for an untyped module.
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
export const NO_FFPROBE_HINT = 'No ffprobe could be found to inspect this media. Install ffmpeg (it includes ffprobe) system-wide, ' +
|
|
21
|
+
'or reinstall @cliangdev/conductor so its bundled ffprobe-static dependency is present.';
|
|
22
|
+
export const NO_FFMPEG_HINT = 'No ffmpeg could be found to process this media. Install ffmpeg system-wide, or reinstall ' +
|
|
23
|
+
'@cliangdev/conductor so its bundled ffmpeg-static dependency is present.';
|
|
24
|
+
const EXTENSION_CONTENT_TYPES = {
|
|
25
|
+
'.mp4': 'video/mp4',
|
|
26
|
+
'.m4v': 'video/mp4',
|
|
27
|
+
'.mov': 'video/quicktime',
|
|
28
|
+
'.webm': 'video/webm',
|
|
29
|
+
'.mp3': 'audio/mpeg',
|
|
30
|
+
'.m4a': 'audio/mp4',
|
|
31
|
+
'.aac': 'audio/aac',
|
|
32
|
+
'.wav': 'audio/wav',
|
|
33
|
+
'.png': 'image/png',
|
|
34
|
+
'.jpg': 'image/jpeg',
|
|
35
|
+
'.jpeg': 'image/jpeg',
|
|
36
|
+
'.webp': 'image/webp',
|
|
37
|
+
};
|
|
38
|
+
function contentTypeFor(kind, filePath, codecName) {
|
|
39
|
+
const ext = extname(filePath).toLowerCase();
|
|
40
|
+
const byExtension = EXTENSION_CONTENT_TYPES[ext];
|
|
41
|
+
if (byExtension)
|
|
42
|
+
return byExtension;
|
|
43
|
+
if (kind === 'VIDEO')
|
|
44
|
+
return 'video/mp4';
|
|
45
|
+
if (kind === 'AUDIO')
|
|
46
|
+
return codecName === 'mp3' ? 'audio/mpeg' : 'audio/mp4';
|
|
47
|
+
return codecName === 'png' ? 'image/png' : 'image/jpeg';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Pure parsing of an already-fetched `ffprobe -show_streams -show_format` JSON document — split out
|
|
51
|
+
* from `probeMedia` so tests can exercise it against fixture JSON without spawning a real process.
|
|
52
|
+
*
|
|
53
|
+
* A video stream marked `disposition.attached_pic` is cover art embedded in an audio file (common in
|
|
54
|
+
* mp3/m4a), not a picture track — it's excluded from the video-stream check so a song with cover art
|
|
55
|
+
* doesn't get classified as VIDEO. A video stream with no duration and at most one frame is a still
|
|
56
|
+
* image ffprobe happened to be pointed at (format `image2`/`png_pipe`/etc.), not a video.
|
|
57
|
+
*/
|
|
58
|
+
export function parseProbeJson(json, filePath) {
|
|
59
|
+
const streams = json.streams ?? [];
|
|
60
|
+
const format = json.format ?? {};
|
|
61
|
+
const videoStreams = streams.filter((s) => s.codec_type === 'video' && s.disposition?.attached_pic !== 1);
|
|
62
|
+
const audioStreams = streams.filter((s) => s.codec_type === 'audio');
|
|
63
|
+
if (videoStreams.length > 0) {
|
|
64
|
+
const v = videoStreams[0];
|
|
65
|
+
const durationRaw = format.duration ?? v.duration;
|
|
66
|
+
const duration = durationRaw ? parseFloat(durationRaw) : undefined;
|
|
67
|
+
const nbFrames = v.nb_frames ? parseInt(v.nb_frames, 10) : undefined;
|
|
68
|
+
const looksLikeStillImage = !duration && (nbFrames === undefined || nbFrames <= 1);
|
|
69
|
+
if (looksLikeStillImage && audioStreams.length === 0) {
|
|
70
|
+
return { kind: 'IMAGE', contentType: contentTypeFor('IMAGE', filePath, v.codec_name), width: v.width, height: v.height, codec: v.codec_name };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
kind: 'VIDEO',
|
|
74
|
+
contentType: contentTypeFor('VIDEO', filePath, v.codec_name),
|
|
75
|
+
width: v.width,
|
|
76
|
+
height: v.height,
|
|
77
|
+
durationSeconds: duration,
|
|
78
|
+
hasAudio: audioStreams.length > 0,
|
|
79
|
+
codec: v.codec_name,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (audioStreams.length > 0) {
|
|
83
|
+
const a = audioStreams[0];
|
|
84
|
+
const durationRaw = format.duration ?? a.duration;
|
|
85
|
+
const duration = durationRaw ? parseFloat(durationRaw) : undefined;
|
|
86
|
+
return { kind: 'AUDIO', contentType: contentTypeFor('AUDIO', filePath, a.codec_name), durationSeconds: duration, codec: a.codec_name };
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`ffprobe found no usable video or audio stream in "${filePath}"`);
|
|
89
|
+
}
|
|
90
|
+
let cachedFfprobePath;
|
|
91
|
+
let cachedFfmpegPath;
|
|
92
|
+
/** True once the named command launches at all (any exit code) rather than failing to spawn
|
|
93
|
+
* (ENOENT) — that's enough to know it's a real binary on PATH, without caring what `-version`
|
|
94
|
+
* prints. Bounded by a timeout so a binary that launches but hangs doesn't wedge discovery forever. */
|
|
95
|
+
function systemBinaryAvailable(command) {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
let settled = false;
|
|
98
|
+
const finish = (ok) => {
|
|
99
|
+
if (settled)
|
|
100
|
+
return;
|
|
101
|
+
settled = true;
|
|
102
|
+
resolve(ok);
|
|
103
|
+
};
|
|
104
|
+
let child;
|
|
105
|
+
try {
|
|
106
|
+
child = spawn(command, ['-version'], { stdio: 'ignore' });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
finish(false);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
child.on('error', () => finish(false));
|
|
113
|
+
child.on('exit', (code) => finish(code === 0));
|
|
114
|
+
const timer = setTimeout(() => finish(false), 5000);
|
|
115
|
+
timer.unref?.();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export async function resolveFfprobePath() {
|
|
119
|
+
if (cachedFfprobePath)
|
|
120
|
+
return cachedFfprobePath;
|
|
121
|
+
if (await systemBinaryAvailable('ffprobe')) {
|
|
122
|
+
cachedFfprobePath = 'ffprobe';
|
|
123
|
+
return cachedFfprobePath;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const mod = require('ffprobe-static');
|
|
127
|
+
const bundledPath = mod.path;
|
|
128
|
+
if (bundledPath && existsSync(bundledPath)) {
|
|
129
|
+
cachedFfprobePath = bundledPath;
|
|
130
|
+
return bundledPath;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// fall through to the error below
|
|
135
|
+
}
|
|
136
|
+
throw new Error(NO_FFPROBE_HINT);
|
|
137
|
+
}
|
|
138
|
+
export async function resolveFfmpegPath() {
|
|
139
|
+
if (cachedFfmpegPath)
|
|
140
|
+
return cachedFfmpegPath;
|
|
141
|
+
if (await systemBinaryAvailable('ffmpeg')) {
|
|
142
|
+
cachedFfmpegPath = 'ffmpeg';
|
|
143
|
+
return cachedFfmpegPath;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const bundledPath = require('ffmpeg-static');
|
|
147
|
+
if (bundledPath && existsSync(bundledPath)) {
|
|
148
|
+
cachedFfmpegPath = bundledPath;
|
|
149
|
+
return bundledPath;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// fall through to the error below
|
|
154
|
+
}
|
|
155
|
+
throw new Error(NO_FFMPEG_HINT);
|
|
156
|
+
}
|
|
157
|
+
/** Test-only: binary discovery caches its result for the process lifetime (real launches are slow
|
|
158
|
+
* enough to matter within one CLI/MCP run), so tests that vary system-binary availability need to
|
|
159
|
+
* reset it between cases. */
|
|
160
|
+
export function resetMediaBinaryCache() {
|
|
161
|
+
cachedFfprobePath = undefined;
|
|
162
|
+
cachedFfmpegPath = undefined;
|
|
163
|
+
}
|
|
164
|
+
function runFfprobe(ffprobePath, filePath) {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const child = spawn(ffprobePath, ['-v', 'error', '-print_format', 'json', '-show_streams', '-show_format', filePath]);
|
|
167
|
+
let stdout = '';
|
|
168
|
+
let stderr = '';
|
|
169
|
+
child.stdout.on('data', (d) => (stdout += d));
|
|
170
|
+
child.stderr.on('data', (d) => (stderr += d));
|
|
171
|
+
child.on('error', reject);
|
|
172
|
+
child.on('exit', (code) => {
|
|
173
|
+
if (code !== 0) {
|
|
174
|
+
reject(new Error(`ffprobe failed (exit ${code}) for "${filePath}": ${stderr.trim() || 'no error output'}`));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
resolve(JSON.parse(stdout));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
reject(new Error(`ffprobe produced unparsable JSON for "${filePath}": ${err instanceof Error ? err.message : String(err)}`));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** Probes a local media file with ffprobe and classifies it as IMAGE, VIDEO or AUDIO. Throws
|
|
187
|
+
* `NO_FFPROBE_HINT` when no usable ffprobe binary can be found at all. */
|
|
188
|
+
export async function probeMedia(filePath) {
|
|
189
|
+
const ffprobePath = await resolveFfprobePath();
|
|
190
|
+
const json = await runFfprobe(ffprobePath, filePath);
|
|
191
|
+
return parseProbeJson(json, filePath);
|
|
192
|
+
}
|
|
193
|
+
function runFfmpegPoster(ffmpegPath, videoPath, outJpg, atSeconds) {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const args = ['-y', '-ss', String(atSeconds), '-i', videoPath, '-frames:v', '1', '-q:v', '2', outJpg];
|
|
196
|
+
const child = spawn(ffmpegPath, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
197
|
+
let stderr = '';
|
|
198
|
+
child.stderr.on('data', (d) => (stderr += d));
|
|
199
|
+
child.on('error', reject);
|
|
200
|
+
child.on('exit', (code) => {
|
|
201
|
+
if (code === 0 && existsSync(outJpg))
|
|
202
|
+
resolve();
|
|
203
|
+
else
|
|
204
|
+
reject(new Error(`ffmpeg poster extraction failed (exit ${code}) for "${videoPath}": ${stderr.trim().slice(-2000) || 'no error output'}`));
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** Extracts a single JPEG poster frame from a video at `atSeconds` (default 1s). Falls back to 0s
|
|
209
|
+
* when the clip is shorter than that (or the seek otherwise fails) rather than giving up. */
|
|
210
|
+
export async function extractPoster(videoPath, outJpg, atSeconds = 1) {
|
|
211
|
+
const ffmpegPath = await resolveFfmpegPath();
|
|
212
|
+
try {
|
|
213
|
+
await runFfmpegPoster(ffmpegPath, videoPath, outJpg, atSeconds);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (atSeconds === 0)
|
|
217
|
+
throw err;
|
|
218
|
+
await runFfmpegPoster(ffmpegPath, videoPath, outJpg, 0);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=media-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-probe.js","sourceRoot":"","sources":["../../src/lib/media-probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** One `scale=-2:360` per input, then an hstack chaining every scaled output left to right. A single
|
|
2
|
+
* input skips the stack (nothing to stack) and just scales. */
|
|
3
|
+
export declare function buildSheetFilterComplex(count: number): string;
|
|
4
|
+
/**
|
|
5
|
+
* Composes the given local poster JPEGs left to right into one JPEG sheet, each scaled to ~360px
|
|
6
|
+
* high. Throws on any ffmpeg failure (no binary, a bad/corrupt poster, etc.) — callers should catch
|
|
7
|
+
* this and fall back to showing a single poster on its own rather than nothing.
|
|
8
|
+
*/
|
|
9
|
+
export declare function composePosterSheet(posterPaths: string[]): Promise<Buffer>;
|
|
10
|
+
//# sourceMappingURL=poster-sheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poster-sheet.d.ts","sourceRoot":"","sources":["../../src/lib/poster-sheet.ts"],"names":[],"mappings":"AAiBA;+DAC+D;AAC/D,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB/E"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes several poster JPEGs (one per CLIP placement) into a single small "key moments" sheet
|
|
3
|
+
* image with ffmpeg's hstack filter, so `preview_creative` has something to show for a CLIP
|
|
4
|
+
* creative — which has no server-rendered contact sheet the way a STILL creative's previewOnly
|
|
5
|
+
* render does (rendering one locally isn't possible either: CLIP has no browser-driven render path,
|
|
6
|
+
* see render_creative in tools/creatives.ts). Shares ffmpeg discovery with media-probe.ts rather
|
|
7
|
+
* than duplicating it.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { readFile, unlink } from 'node:fs/promises';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { resolveFfmpegPath } from './media-probe.js';
|
|
15
|
+
const SHEET_HEIGHT = 360;
|
|
16
|
+
/** One `scale=-2:360` per input, then an hstack chaining every scaled output left to right. A single
|
|
17
|
+
* input skips the stack (nothing to stack) and just scales. */
|
|
18
|
+
export function buildSheetFilterComplex(count) {
|
|
19
|
+
if (count === 1)
|
|
20
|
+
return `[0:v]scale=-2:${SHEET_HEIGHT}[v]`;
|
|
21
|
+
const scales = Array.from({ length: count }, (_, i) => `[${i}:v]scale=-2:${SHEET_HEIGHT}[v${i}]`).join(';');
|
|
22
|
+
const stackInputs = Array.from({ length: count }, (_, i) => `[v${i}]`).join('');
|
|
23
|
+
return `${scales};${stackInputs}hstack=inputs=${count}[v]`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Composes the given local poster JPEGs left to right into one JPEG sheet, each scaled to ~360px
|
|
27
|
+
* high. Throws on any ffmpeg failure (no binary, a bad/corrupt poster, etc.) — callers should catch
|
|
28
|
+
* this and fall back to showing a single poster on its own rather than nothing.
|
|
29
|
+
*/
|
|
30
|
+
export async function composePosterSheet(posterPaths) {
|
|
31
|
+
if (posterPaths.length === 0) {
|
|
32
|
+
throw new Error('composePosterSheet needs at least one poster image');
|
|
33
|
+
}
|
|
34
|
+
const ffmpegPath = await resolveFfmpegPath();
|
|
35
|
+
const outPath = join(tmpdir(), `conductor-sheet-${randomUUID()}.jpg`);
|
|
36
|
+
const inputs = posterPaths.flatMap((p) => ['-i', p]);
|
|
37
|
+
const args = ['-y', ...inputs, '-filter_complex', buildSheetFilterComplex(posterPaths.length), '-map', '[v]', '-q:v', '3', outPath];
|
|
38
|
+
await new Promise((resolve, reject) => {
|
|
39
|
+
const child = spawn(ffmpegPath, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
40
|
+
let stderr = '';
|
|
41
|
+
child.stderr.on('data', (d) => (stderr += d));
|
|
42
|
+
child.on('error', reject);
|
|
43
|
+
child.on('exit', (code) => {
|
|
44
|
+
if (code === 0)
|
|
45
|
+
resolve();
|
|
46
|
+
else
|
|
47
|
+
reject(new Error(`ffmpeg sheet composition failed (exit ${code}): ${stderr.trim().slice(-2000) || 'no error output'}`));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
return await readFile(outPath);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await unlink(outPath).catch(() => { });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=poster-sheet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poster-sheet.js","sourceRoot":"","sources":["../../src/lib/poster-sheet.ts"],"names":[],"mappings":"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"}
|