@officexapp/vidfarm-devcli 0.21.17 → 0.21.18

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.
@@ -146,6 +146,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
146
146
 
147
147
  - **Determine the surface before claiming capabilities.** The web chat has only its declared tools and REST routes. It cannot execute arbitrary JavaScript/Python, open a shell, create a local repository script, or use the user's filesystem. Never tell a web-chat user that you ran code or wrote a script unless a dedicated declared tool actually did so. A desktop coding agent has a real shell and filesystem and MAY write/run scripts, perform arbitrary local computations over paginated API results, create reports/CSVs/JSON, edit composition files, and orchestrate long devcli workflows within the user's authorization.
148
148
  - **The web AI chat can do all three paintbrushes** — clip raws, author HTML/hyperframe motion, and generate AI media — and it drives edits directly on the live timeline. Keep small-to-medium jobs here: text/caption swaps, a scene or two replaced, single generations, captions, approve/schedule. Just do them.
149
+ - **Every production adheres to the TikTok-native caption standard.** On-screen text lives inside the readable safe zone (**~8%–85%** of a 9:16 frame — never pinned to the top/bottom edges the phone UI clips) and uses the composition's bold font regime (**Montserrat** default / TikTok Sans, weight ≥700). Decomposed forks often inherit the source's edge-pinned caption in an off-regime font — fix it, don't inherit it. Local devcli renders auto-normalize this, but author it correctly so the editor preview matches. Full rules in `references/editor-workflows.md` ("TikTok-native caption standard").
149
150
  - **Where the web chat struggles: complex, long, multi-step transformations.** A full multi-scene re-theme, an iterative render-critique-iterate loop, heavy scripted or batch work, or anything needing a real filesystem and many sequential tool calls will hit context limits, turn/timeout ceilings, and the web editor's constraints (CSS/declarative motion only — JS animation adapters are stripped on save). Don't grind a big transformation one layer at a time in a chat turn and stall.
150
151
  - **Practical workaround — hand the heavy job to local devcli.** When a task is genuinely large or long-running, **proactively recommend the director run it locally with an AI coding agent** (Claude Code / OpenAI Codex / any capable agent): `vidfarm pull <forkId>` writes the composition + the `.harness/` grounding bundle to disk, the agent edits with the full devcli verb set and JS animation adapters, renders free with `vidfarm serve`, and `vidfarm publish` pushes it back. This is the **best-quality (B) harness's** natural home (adversarial grading with a coding agent). Frame it as "this is a big rebuild — you'll get a better, faster result running it locally with a coding agent; here's how," not as a dead end.
151
152
  - **Offer a handoff, do not impersonate the desktop agent.** When web chat reaches that boundary, offer to save a Markdown handoff in My Files containing the objective, selected template/fork IDs, asset paths, grounding, constraints, completed work, and suggested devcli commands. Create it only after the user agrees. The desktop agent should read that document, pull the referenced fork, and then use its actual code/shell capabilities.
@@ -344,6 +344,17 @@ Three surfaces:
344
344
  - **Editor UI (human)** — the Inspector's **Animate (Ken Burns)** dropdown on any selected image.
345
345
  - **Desktop agents (devcli)** — `vidfarm place ./work --src <img> --kind image --ken-burns zoom-in [--ken-burns-intensity 0.3]`, or fused with generation: `vidfarm generate image --prompt "…" --place ./work --ken-burns pan-left`.
346
346
 
347
+ ### TikTok-native caption standard (position + font) — always adhere
348
+
349
+ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **Never pin on-screen text to the extreme top or bottom** — the top ~8% sits under the status bar / "Following · For You" tabs and the bottom ~15% under the username, caption text, music marquee, and action rail. Text there is literally clipped and reads as amateur. Two rules, applied to **every** caption/title/overlay you place or inherit:
350
+
351
+ - **Position → readable safe zone.** Keep the text box's vertical extent inside **~8%–85%** of canvas height (9:16). A lower-third subtitle lives around **y≈70%** (the `captions generate` default); a "me when…" / "POV:" meme line sits near the **top of the safe zone (y≈8%), not y:0**. Preserve the original top-vs-bottom anchoring — just pull it off the dead-zone edge, don't recenter everything. Also keep wide captions clear of the **right ~12%** action rail (a centered box at `x:10 width:80` is safe).
352
+ - **Font → the composition regime.** Use the bundled display fonts only — **Montserrat** (bold default, weight ≥700), **TikTok Sans**, Abel, Source Code Pro, Yesteryear. Don't request a font the composition doesn't import (it silently falls back). For captions, heavy weight + an outline/pill background (`spotlight` preset, `background_style:"outline"`) is the TikTok-native look.
353
+
354
+ **A common trap: decomposed templates mirror the source's caption placement**, so a forked meme can arrive with its caption pinned at `top:0` in a non-regime font. When you re-theme or recaption, **fix it to the standard** rather than inheriting the edge-pinned original. When placing text yourself (`set_captions`, `set_layer_text`, `add_layer`, devcli `place`/`captions`), set `y`/`font_family` to the standard from the start.
355
+
356
+ > Local devcli renders enforce this automatically: `renderCompositionLocally` runs `normalizeTikTokCaptionLayout` (src/devcli/composition-edit.ts) on every production, clamping caption/text layers into the 8%–85% safe zone and coercing off-regime primary fonts to Montserrat. It's a safety net, not a license to author edge-pinned captions — get it right in the composition so the editor preview and any cloud render match.
357
+
347
358
  ### Animated captions — word-by-word caption styles (TikTok/CapCut)
348
359
 
349
360
  Compositions support **first-class animated captions**: caption layers whose words animate one at a time, synced to the spoken audio, identical in the editor preview and both render paths. A caption *run* is one layer per cue (a page of ~3-5 words) on a shared track; each layer carries `data-caption-animation="<preset>"` and per-word `<span data-cap-word>` timing — no keyframe authoring, and the text stays editable like any text layer.
package/SKILL.director.md CHANGED
@@ -146,6 +146,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
146
146
 
147
147
  - **Determine the surface before claiming capabilities.** The web chat has only its declared tools and REST routes. It cannot execute arbitrary JavaScript/Python, open a shell, create a local repository script, or use the user's filesystem. Never tell a web-chat user that you ran code or wrote a script unless a dedicated declared tool actually did so. A desktop coding agent has a real shell and filesystem and MAY write/run scripts, perform arbitrary local computations over paginated API results, create reports/CSVs/JSON, edit composition files, and orchestrate long devcli workflows within the user's authorization.
148
148
  - **The web AI chat can do all three paintbrushes** — clip raws, author HTML/hyperframe motion, and generate AI media — and it drives edits directly on the live timeline. Keep small-to-medium jobs here: text/caption swaps, a scene or two replaced, single generations, captions, approve/schedule. Just do them.
149
+ - **Every production adheres to the TikTok-native caption standard.** On-screen text lives inside the readable safe zone (**~8%–85%** of a 9:16 frame — never pinned to the top/bottom edges the phone UI clips) and uses the composition's bold font regime (**Montserrat** default / TikTok Sans, weight ≥700). Decomposed forks often inherit the source's edge-pinned caption in an off-regime font — fix it, don't inherit it. Local devcli renders auto-normalize this, but author it correctly so the editor preview matches. Full rules in `references/editor-workflows.md` ("TikTok-native caption standard").
149
150
  - **Where the web chat struggles: complex, long, multi-step transformations.** A full multi-scene re-theme, an iterative render-critique-iterate loop, heavy scripted or batch work, or anything needing a real filesystem and many sequential tool calls will hit context limits, turn/timeout ceilings, and the web editor's constraints (CSS/declarative motion only — JS animation adapters are stripped on save). Don't grind a big transformation one layer at a time in a chat turn and stall.
150
151
  - **Practical workaround — hand the heavy job to local devcli.** When a task is genuinely large or long-running, **proactively recommend the director run it locally with an AI coding agent** (Claude Code / OpenAI Codex / any capable agent): `vidfarm pull <forkId>` writes the composition + the `.harness/` grounding bundle to disk, the agent edits with the full devcli verb set and JS animation adapters, renders free with `vidfarm serve`, and `vidfarm publish` pushes it back. This is the **best-quality (B) harness's** natural home (adversarial grading with a coding agent). Frame it as "this is a big rebuild — you'll get a better, faster result running it locally with a coding agent; here's how," not as a dead end.
151
152
  - **Offer a handoff, do not impersonate the desktop agent.** When web chat reaches that boundary, offer to save a Markdown handoff in My Files containing the objective, selected template/fork IDs, asset paths, grounding, constraints, completed work, and suggested devcli commands. Create it only after the user agrees. The desktop agent should read that document, pull the referenced fork, and then use its actual code/shell capabilities.
@@ -863,6 +864,17 @@ Three surfaces:
863
864
  - **Editor UI (human)** — the Inspector's **Animate (Ken Burns)** dropdown on any selected image.
864
865
  - **Desktop agents (devcli)** — `vidfarm place ./work --src <img> --kind image --ken-burns zoom-in [--ken-burns-intensity 0.3]`, or fused with generation: `vidfarm generate image --prompt "…" --place ./work --ken-burns pan-left`.
865
866
 
867
+ ### TikTok-native caption standard (position + font) — always adhere
868
+
869
+ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **Never pin on-screen text to the extreme top or bottom** — the top ~8% sits under the status bar / "Following · For You" tabs and the bottom ~15% under the username, caption text, music marquee, and action rail. Text there is literally clipped and reads as amateur. Two rules, applied to **every** caption/title/overlay you place or inherit:
870
+
871
+ - **Position → readable safe zone.** Keep the text box's vertical extent inside **~8%–85%** of canvas height (9:16). A lower-third subtitle lives around **y≈70%** (the `captions generate` default); a "me when…" / "POV:" meme line sits near the **top of the safe zone (y≈8%), not y:0**. Preserve the original top-vs-bottom anchoring — just pull it off the dead-zone edge, don't recenter everything. Also keep wide captions clear of the **right ~12%** action rail (a centered box at `x:10 width:80` is safe).
872
+ - **Font → the composition regime.** Use the bundled display fonts only — **Montserrat** (bold default, weight ≥700), **TikTok Sans**, Abel, Source Code Pro, Yesteryear. Don't request a font the composition doesn't import (it silently falls back). For captions, heavy weight + an outline/pill background (`spotlight` preset, `background_style:"outline"`) is the TikTok-native look.
873
+
874
+ **A common trap: decomposed templates mirror the source's caption placement**, so a forked meme can arrive with its caption pinned at `top:0` in a non-regime font. When you re-theme or recaption, **fix it to the standard** rather than inheriting the edge-pinned original. When placing text yourself (`set_captions`, `set_layer_text`, `add_layer`, devcli `place`/`captions`), set `y`/`font_family` to the standard from the start.
875
+
876
+ > Local devcli renders enforce this automatically: `renderCompositionLocally` runs `normalizeTikTokCaptionLayout` (src/devcli/composition-edit.ts) on every production, clamping caption/text layers into the 8%–85% safe zone and coercing off-regime primary fonts to Montserrat. It's a safety net, not a license to author edge-pinned captions — get it right in the composition so the editor preview and any cloud render match.
877
+
866
878
  ### Animated captions — word-by-word caption styles (TikTok/CapCut)
867
879
 
868
880
  Compositions support **first-class animated captions**: caption layers whose words animate one at a time, synced to the spoken audio, identical in the editor preview and both render paths. A caption *run* is one layer per cue (a page of ~3-5 words) on a shared track; each layer carries `data-caption-animation="<preset>"` and per-word `<span data-cap-word>` timing — no keyframe authoring, and the text stays editable like any text layer.
@@ -377,6 +377,91 @@ export function insertMediaLayer(html, opts) {
377
377
  return { html: serialize(document), layerKey: id };
378
378
  }
379
379
  const CAPTION_DEFAULT_FRAME = { x: 10, y: 70, width: 80, height: 14 };
380
+ // ── TikTok-native caption standard ────────────────────────────────────────────
381
+ // A 9:16 phone feed hides the extreme top and bottom of every frame behind
382
+ // platform chrome: the top ~8% sits under the status bar / "Following · For You"
383
+ // tabs, and the bottom ~15% under the username, caption text, music marquee, and
384
+ // action rail. Text pinned into those bands reads as amateur AND gets literally
385
+ // clipped. So a TikTok-native caption lives inside a readable band, and uses the
386
+ // composition's own bold display font — not whatever font the source happened to
387
+ // bake in. `captions generate` already defaults to y:70 / Montserrat, but a
388
+ // DECOMPOSED template mirrors the source's edge-pinned caption (e.g. a "me when…"
389
+ // meme caption at top:0), so we normalize at render time to guarantee every LOCAL
390
+ // production adheres regardless of how the composition was authored.
391
+ export const TIKTOK_CAPTION_SAFE_ZONE = { top: 8, bottom: 85 }; // % of canvas height
392
+ // The composition font regime — mirrors COMPOSITION_FONT_IMPORT's family list in
393
+ // services/studio-project-adapter.ts. A caption/text layer whose primary family
394
+ // is outside this set isn't even imported (so it silently falls back at render),
395
+ // which means coercing it to the bold default is strictly an improvement.
396
+ const CAPTION_FONT_REGIME = ["tiktok sans", "montserrat", "abel", "source code pro", "yesteryear"];
397
+ const CAPTION_REGIME_FALLBACK_FONT = "Montserrat";
398
+ const CAPTION_FONT_FALLBACK_CHAIN = "'Montserrat', 'TikTok Sans', Abel, sans-serif";
399
+ function setStylePercent(node, prop, value) {
400
+ if (node?.style)
401
+ node.style[prop] = `${Number(value.toFixed(2))}%`;
402
+ }
403
+ // Pull a caption/text layer's vertical position into the readable safe band,
404
+ // PRESERVING its top-vs-bottom anchoring (a top "me when…" meme stays near the
405
+ // top of the band; a lower-third subtitle stays low) — we only nudge layers that
406
+ // spill into the edge dead-zones, never re-center ones already safe.
407
+ function clampCaptionIntoSafeZone(top, height) {
408
+ if (top === undefined)
409
+ return null;
410
+ const h = height !== undefined && height > 0 ? height : CAPTION_DEFAULT_FRAME.height;
411
+ const { top: safeTop, bottom: safeBottom } = TIKTOK_CAPTION_SAFE_ZONE;
412
+ // Bottom edge of the band the box may occupy without its content crossing safeBottom.
413
+ const maxTop = Math.max(safeTop, safeBottom - h);
414
+ let next = top;
415
+ if (next < safeTop)
416
+ next = safeTop;
417
+ else if (next > maxTop)
418
+ next = maxTop;
419
+ return Math.abs(next - top) < 0.01 ? null : next;
420
+ }
421
+ /**
422
+ * Enforce the TikTok-native caption standard on a composition: keep every
423
+ * caption/text layer inside the readable safe zone (off the top/bottom UI
424
+ * dead-zones) and coerce any off-regime primary font to the bold default. Pure
425
+ * function — returns the rewritten HTML plus a change count/notes for logging.
426
+ * Idempotent: a composition already conformant is returned unchanged.
427
+ */
428
+ export function normalizeTikTokCaptionLayout(html) {
429
+ let document, root;
430
+ try {
431
+ ({ document, root } = readCompositionDoc(html));
432
+ }
433
+ catch {
434
+ return { html, changed: false, moved: 0, refont: 0, notes: [] };
435
+ }
436
+ const nodes = Array.from(root.querySelectorAll('[data-layer-kind="caption"], [data-layer-kind="text"]'));
437
+ let moved = 0;
438
+ let refont = 0;
439
+ const notes = [];
440
+ for (const node of nodes) {
441
+ // Position: clamp into the safe band.
442
+ const top = stylePercent(node, "top");
443
+ const height = stylePercent(node, "height");
444
+ const clamped = clampCaptionIntoSafeZone(top, height);
445
+ if (clamped !== null) {
446
+ setStylePercent(node, "top", clamped);
447
+ moved += 1;
448
+ const label = node.getAttribute?.("data-label") || node.getAttribute?.("data-hf-id") || "caption";
449
+ notes.push(`moved "${String(label).slice(0, 32)}" top ${top}%→${Number(clamped.toFixed(1))}% (safe zone)`);
450
+ }
451
+ // Font: coerce off-regime primary family to the bold default.
452
+ const primary = String(node.getAttribute?.("data-font-family") || "").trim();
453
+ if (primary && !CAPTION_FONT_REGIME.includes(primary.toLowerCase())) {
454
+ node.setAttribute?.("data-font-family", CAPTION_REGIME_FALLBACK_FONT);
455
+ if (node.style)
456
+ node.style.fontFamily = CAPTION_FONT_FALLBACK_CHAIN;
457
+ refont += 1;
458
+ notes.push(`refont "${primary.slice(0, 24)}"→${CAPTION_REGIME_FALLBACK_FONT} (regime)`);
459
+ }
460
+ }
461
+ if (!moved && !refont)
462
+ return { html, changed: false, moved: 0, refont: 0, notes: [] };
463
+ return { html: serialize(document), changed: true, moved, refont, notes };
464
+ }
380
465
  function resolveCaptionLook(opts) {
381
466
  const preset = captionStylePresetById(opts.style)
382
467
  ?? (opts.animation ? null : CAPTION_STYLE_PRESETS[0]);
@@ -20,6 +20,7 @@ import os from "node:os";
20
20
  import path from "node:path";
21
21
  import { prepareProjectMediaForRender } from "../lib/render-media-prep.js";
22
22
  import { runHyperframesCommand } from "./hyperframes-cli.js";
23
+ import { normalizeTikTokCaptionLayout } from "./composition-edit.js";
23
24
  // Same invariant as the backend's forceEvenCompositionDimensions: libx264
24
25
  // (yuv420p) rejects odd frame sizes, so round odd data-width/height down.
25
26
  function forceEvenDimensions(html) {
@@ -77,6 +78,20 @@ export async function renderCompositionLocally(input) {
77
78
  let prepNotes = [];
78
79
  try {
79
80
  let html = forceEvenDimensions(input.compositionHtml);
81
+ // TikTok-native caption standard: keep on-screen text off the top/bottom UI
82
+ // dead-zones and on the composition's bold font regime. Runs on every local
83
+ // production so a decomposed template that mirrored the source's edge-pinned
84
+ // caption still renders readable. Idempotent + best-effort (never blocks a render).
85
+ try {
86
+ const tiktok = normalizeTikTokCaptionLayout(html);
87
+ if (tiktok.changed) {
88
+ html = tiktok.html;
89
+ log("caption_safe_zone.normalized", { moved: tiktok.moved, refont: tiktok.refont, notes: tiktok.notes });
90
+ }
91
+ }
92
+ catch (error) {
93
+ log("caption_safe_zone.failed", { error: error instanceof Error ? error.message : String(error) });
94
+ }
80
95
  // Download-once/cut-segments pass — the same prep the backend renders run,
81
96
  // so a timeline that reuses one long source N times stays fast locally.
82
97
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/vidfarm-devcli",
3
- "version": "0.21.17",
3
+ "version": "0.21.18",
4
4
  "description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
5
5
  "type": "module",
6
6
  "bin": {