@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.
@@ -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:
@@ -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": "Upload a photo into the project's photo library from a local file or a public URL, recording its provenance for the readiness checklist. Width/height are read from the file.",
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,CAiEvD"}
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
- console.log(` ${label} ${size} ${frame.url ?? ''}`.trimEnd());
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,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;YAClE,CAAC;YACD,IAAI,MAAM,CAAC,KAAK;gBAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC5C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;YACtB,CAAC;QACH,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;QACtB,CAAC;IACH,CAAC,CAAC,CAAA;AACN,CAAC"}
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: finds a local browser, runs the render core against the external
28
- * API, and reads back the stored render (frames with signed URLs, final state) whether or not the
29
- * render itself succeeded — a FAILED render still has an id and an error message worth returning.
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;CACpB;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;AAwDD;;;;GAIG;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,CAiC/B"}
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
- * Renders a Creative end to end: finds a local browser, runs the render core against the external
38
- * API, and reads back the stored render (frames with signed URLs, final state) whether or not the
39
- * render itself succeeded — a FAILED render still has an id and an error message worth returning.
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":"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"}
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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"}