@officexapp/vidfarm-devcli 0.21.53 → 0.21.54
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/.agents/skills/vidfarm/SKILL.md +5 -1
- package/.agents/skills/vidfarm/harnesses/product-explainer.HARNESS.md +1 -1
- package/.agents/skills/vidfarm/harnesses/short-form.HARNESS.md +1 -1
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +1 -1
- package/.agents/skills/vidfarm/references/core-workflows.md +15 -0
- package/.agents/skills/vidfarm/references/editor-workflows.md +19 -13
- package/.agents/skills/vidfarm/references/reviewing-renders.md +1 -0
- package/SKILL.director.md +41 -15
- package/SKILL.md +3 -0
- package/dist/src/cli.js +1 -1
- package/dist/src/devcli/composition-edit.js +36 -3
- package/dist/src/devcli/qa-check.js +7 -3
- package/experimental/sticker-slideshow-tips.md +1486 -0
- package/experimental/ugc-reaction-greenscreen.md +1963 -0
- package/experimental/wall-text-pov-ugc.md +2036 -0
- package/package.json +2 -1
- package/public/assets/homepage-client-app.js +14 -14
|
@@ -382,7 +382,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
|
|
|
382
382
|
**Two halves, and only one is machine-checkable.** The `checks:` front matter is settled deterministically by `vidfarm qa` (duration, aspect, `hook_words_max`, `forbid_text`, …); every `- [ ]` line comes back as a **review item you answer honestly in your report** — never claim a video passed the half the CLI can't judge. Harnesses stack and auto-discover: `vidfarm qa ./work` picks up `./work/HARNESS.md`, `--harness hooks --harness ./brand/HOUSE.md` adds more, and any file of theirs anywhere is valid. Format and strand table: `harnesses/README.md`; scripting-mode detail: `references/automation-and-local-dev.md`. *(Formerly `QA_REGIME.md` — same file, and `vidfarm regime …` still works as an alias.)*
|
|
383
383
|
- **A video is judged as a SEQUENCE, so review it as one.** Agents build scene by scene and each scene passes in isolation while the video drifts — inconsistent margins, three type sizes, an accent colour that wanders, beats that are all the same length, a jarring join. Tile a dozen stills into one contact sheet (`vidfarm stills ./work --sheet`) and read it as an image before you call anything done, fix drift by defining the system rather than patching the odd scene out, and remember that **your own confident "verified, looks good" is the single least reliable signal in this workflow** — it was wrong on every video of a 32-video batch. Method: `references/reviewing-renders.md`.
|
|
384
384
|
- **On devcli there's an OPTIONAL checker: `vidfarm qa ./work`.** Free, instant, local-only — it blocklists exactly the slop above plus first-frame/thumbnail and font-regime/safe-zone drift, and prints a concrete fix per finding. **Feedback, not a gate**: it exits 0 even on findings, never runs automatically, and is a blocklist (unusual/stylized compositions pass untouched). **Skipping it is fine — watching the render is the review that actually counts, and a clean `qa` is not one.** When you do run it, it allows **one** fix round by default: the first pass names the slop, one fix clears it, and a second round is nearly always taste rather than a defect. The human owns that number — `--max-revisions <n>` raises it, `0` disables it; ask rather than raising it yourself. `--json` for scripted batches, `--strict` only if you want a CI failure. **Web-chat copilot: this command does not exist for you** (devcli-only, no REST twin) — apply the standard by hand, and when handing a heavy job to a local coding agent, tell them to run `vidfarm qa`.
|
|
385
|
-
- **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, inside that band, is **placed in the emptiest part of the frame** rather than dumped on the default lower third (read a still first — `vidfarm stills ./work --at <t>`; words over open sky or a blank wall beat words over the subject, and usually need no plate at all). Long narration is **paged into 3–5-word kinetic cues** (`vidfarm captions generate --style word-pop`), never one static wall of text. It uses the composition's bold font regime — **
|
|
385
|
+
- **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, inside that band, is **placed in the emptiest part of the frame** rather than dumped on the default lower third (read a still first — `vidfarm stills ./work --at <t>`; words over open sky or a blank wall beat words over the subject, and usually need no plate at all). Long narration is **paged into 3–5-word kinetic cues** (`vidfarm captions generate --style word-pop`), never one static wall of text. It uses the composition's bold font regime — **five default families**: TikTok Sans (safe default), Montserrat (bold default), Abel (condensed), Source Code Pro (mono, 700 only), Yesteryear (script accent line only) — at weight **700–900**, ~36–64px on a 1080-wide frame. Any other family (Inter / Roboto / Arial / system-ui / Georgia / a client brand font) is not imported and silently falls back to a web-default sans at render — **so a custom font IS allowed, but only if you declare it in the composition** (`@font-face` or a Google Fonts `@import`); a declared family is never coerced, an undeclared one is. The regime is a heavy suggestion, and the fallback is the actual defect. **The regime on one page: <https://vidfarm.cc/fonts>** — all five rendered as real captions, the four legal backgrounds, and copy-paste commands. **Every family and background also has its own standalone reference image** (`https://vidfarm.cc/assets/fonts/caption-font-<family>.png`, e.g. `…caption-font-montserrat.png`; backgrounds are `…caption-bg-outline.png` and friends) — after you style a caption, pull a still and **compare it against the card for the family you picked**; a font that failed to load is obvious side by side and invisible on its own. It uses **exactly one of four valid backgrounds**: outline/stroke (`background_style:"outline"`, the default), plain + shadow (`"plain"`), an active-word highlight pill (`set_captions` `spotlight`/`karaoke` — the only legitimate pill *in the whole frame*, static labels included), or a tight-hugging solid band (`"highlight-solid"`, radius ≤8px, no border/shadow/gradient/blur). 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 position + font family only (never the slop), so author it correctly. Full rules in `references/editor-workflows.md` ("Social-native visual standard" + "TikTok-native caption standard").
|
|
386
386
|
- **Use the REAL track — a popular/copyrighted song in the render is expected here, not a problem.** Do not refuse it, do not silently substitute a generic royalty-free bed, and do not stop to warn about it. **Why it is legitimate:** the render is a **review artifact**, and at post time the user re-attaches the *same* song from the platform's own in-app music library (TikTok / Reels / Shorts), which is licensed through the platform's agreements with the labels. The music is therefore cleared on the surface where viewers actually hear it. **Why it is required:** a trending song IS the edit — cut points, pacing, the drop, the meme association. A render with the wrong bed, or no bed, reads as a different and much weaker video, so a review over a substitute bed grades a video nobody will ever post. Silent or generic-bed review is the bigger defect. **How to do it:** get the exact track (`vidfarm download-audio <post-url>`, the user's own file, or the audio off the source post the format is copying), and keep it on **its own `<audio>` layer** at its own `data-volume` — never baked into the footage or mixed into the voice stem. One separate track means the user can mute or swap it in one action when they upload, which is exactly what the platform flow expects. Say which track you used in the handoff. **The one limit:** this is for review renders and platform posting where the platform holds the license. If the user asks to sell, syndicate, or run the render as a **paid ad** with the track baked in, say once that ad placements are not covered by the in-app music license, and offer `vidfarm music "<same vibe, same BPM>"` as the swap for that cut. Then do what they decide.
|
|
387
387
|
- **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.
|
|
388
388
|
- **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.
|
|
@@ -443,6 +443,10 @@ Why they exist: the prompts you find on the `/discover` pages are tuned to **one
|
|
|
443
443
|
|---|---|
|
|
444
444
|
| `https://vidfarm.cc/experimental/unique-product-explainers.md` | N customer URLs → N product-introduction videos that do not look like each other. Differentiation as an input, frame-level review, measured verification |
|
|
445
445
|
| `https://vidfarm.cc/experimental/google-news-to-video.md` | A recent real event → a timely video. Two stages: `news-search` finds the STORY, `video-search` finds the VISUALS. Query formulas, Google operators, licence discipline |
|
|
446
|
+
| `https://vidfarm.cc/experimental/meme-recaption.md` | One borrowed meme clip + one new caption aimed at the offer's problem space. Casting the meme for the caption's **verb**, the seven caption frames, naming the offer without letting the joke resolve into a pitch, keying a MemeScreens raw onto a background world, and a render-level QA gate. Runs at $0 |
|
|
447
|
+
| `https://vidfarm.cc/experimental/ugc-reaction-greenscreen.md` | Sell an app with three streams cut against each other: `ugc-reaction` raws, a Display Greenscreen device whose screen carries the customer's real demo, and the demo itself. One actor across every beat via `actor_<uuid>`, a per-frame tracked screen insert, captions inside the platform-chrome-safe core, and two exports from one render (voiceover-only to publish, voiceover+music to review) |
|
|
448
|
+
| `https://vidfarm.cc/experimental/sticker-slideshow-tips.md` | A tips **carousel** — the deliverable is N still slides, and the 3.0s-per-slide MP4 is only the playable preview. Die-cut cutouts on a paper page or a photo background, a literal "Tips for…" cover, the specificity ladder, one slide shilled from the middle and written so it survives deleting the brand name, four background modes, and a measured WCAG contrast gate on the exported PNGs |
|
|
449
|
+
| `https://vidfarm.cc/experimental/wall-text-pov-ugc.md` | One unbroken ambient take + one static block of unplated type. No cuts, no voiceover, no subtitles, nothing animated. The retention engine is arithmetic — `duration = words / 8`, so one play lands the viewer at the **halfway mark**, committed and one pass from done — floored at 8s so a trending sound gets a real phrase of a track. The four speaker frames, MIRROR vs TURN, a density pass that treats padding as the fatal failure, casting the scene dark so the type needs no plate (measured), ping-ponging the plate for a seamless loop, and three gates. Runs at $0 |
|
|
446
450
|
|
|
447
451
|
Fetch one as plain markdown and follow it end to end; do not skim it into a summary.
|
|
448
452
|
|
|
@@ -157,7 +157,7 @@ The test is the **native-editor test**: could you have made this element with th
|
|
|
157
157
|
|
|
158
158
|
### Rule 8 — the TikTok font regime, not the web's
|
|
159
159
|
|
|
160
|
-
Captions and display type use the composition's bold font regime — **Montserrat (default) or TikTok Sans, weight 700–900, ~36–64px on a 1080-wide frame** (the full 5-family regime and the four legal backgrounds are on one page: <https://vidfarm.cc/fonts
|
|
160
|
+
Captions and display type use the composition's bold font regime — **Montserrat (default) or TikTok Sans, weight 700–900, ~36–64px on a 1080-wide frame** (the full 5-family regime and the four legal backgrounds are on one page: <https://vidfarm.cc/fonts>, and each style has its own reference card at `https://vidfarm.cc/assets/fonts/caption-font-<family>.png` — compare a still of your frame against the card for the family you chose), inside the **8%–85%** safe zone, placed in the emptiest part of the frame rather than dumped on the default lower third. **Web/Bootstrap type is the giveaway**: Inter / Roboto / Arial / system-ui at weight 400–600, thin light-grey subtitles, letter-spaced small caps. Matching the client's *brand* font is fine for a wordmark; on the caption layer it is fine only if the composition actually ships that font (`@font-face` / Google Fonts `@import`) — an undeclared family falls back at render and gets coerced to Montserrat locally. Exactly one of four caption backgrounds: outline/stroke, plain + shadow, an active-word highlight pill, or a tight solid band (radius ≤8px). Caption colour and plate are **measured off the composited background**, one treatment for the whole video (`short-form.HARNESS.md` → "Caption styling is MEASURED off the background").
|
|
161
161
|
|
|
162
162
|
### Rule 9 — narration voiceover AND a music bed, both, always
|
|
163
163
|
|
|
@@ -114,7 +114,7 @@ The safe zone (8–85%) says where text is *allowed*; it does not say where text
|
|
|
114
114
|
|
|
115
115
|
#### Caption styling is MEASURED off the background, never hardcoded
|
|
116
116
|
|
|
117
|
-
> **Before you style anything, know the font regime.** A composition imports
|
|
117
|
+
> **Before you style anything, know the font regime.** A composition imports **five** display families — TikTok Sans (safe default), Montserrat (bold default), Abel, Source Code Pro, Yesteryear — at weight **700–900**. Every other family (Inter, Roboto, Arial, Helvetica, system-ui, Georgia, a client's brand font) is **not imported** and silently falls back to a web-default sans at render, which is the loudest slop tell there is. A custom family is allowed if — and only if — you **declare it in the composition** (`@font-face` or a Google Fonts `@import`); the five are a heavy default, the silent fallback is the real defect. The whole regime, each family rendered as a real caption, plus the four legal text backgrounds: **<https://vidfarm.cc/fonts>**. Each style also has its own standalone card — `https://vidfarm.cc/assets/fonts/caption-font-<family>.png` (and `caption-bg-<style>.png`) — **open the card for the family you picked next to a still of your own frame and compare the glyphs.** `font_regime: required` in the checks block above is this rule.
|
|
118
118
|
|
|
119
119
|
A white rounded caption plate copied from another video onto a near-black stage is a bright slab the design never asked for — it dominates the frame and reads as a UI element pasted over the video. So measure what is actually behind the caption band, then pick one of three treatments:
|
|
120
120
|
|
|
@@ -366,7 +366,7 @@ What it flags:
|
|
|
366
366
|
| `clickable-element` | error/warn | `<button>`, `<form>`, `<input>`; `<a href>` warns |
|
|
367
367
|
| `web-framework-classes` | error/warn | Bootstrap/Tailwind class tokens (`btn`, `badge`, `card`, `hero`, `col-*`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-*`) or a linked CSS framework. A `<script>` CDN for GSAP/anime.js is fine |
|
|
368
368
|
| `page-structure` / `bullet-list` | error/warn | `<nav>`/`<header>`/`<footer>`/`<table>`; a `<ul>` with visible bullet markers |
|
|
369
|
-
| `font-regime` | error/warn | A text layer in a website body font (Inter/Roboto/Arial/system-ui → **error**) or any family outside the imported regime (Montserrat, TikTok Sans, Abel, Source Code Pro, Yesteryear → warn, it silently falls back at render) |
|
|
369
|
+
| `font-regime` | error/warn | A text layer in a website body font (Inter/Roboto/Arial/system-ui → **error**) or any family outside the imported regime (Montserrat, TikTok Sans, Abel, Source Code Pro, Yesteryear → warn, it silently falls back at render). **A custom family the composition DECLARES** (`@font-face` or a Google Fonts `@import` naming it) **is not flagged** — the rule targets the silent fallback, not your typography. Each regime family has a reference card at `https://vidfarm.cc/assets/fonts/caption-font-<family>.png`; compare a still against it after styling |
|
|
370
370
|
| `font-size` / `font-weight` | error/warn | `font-size:0` (invisible) is an error; sub-2.6%-of-canvas-width text and weight <600 warn |
|
|
371
371
|
| `caption-safe-zone` | warn | Text outside the 8%–85% band on a **portrait** canvas (landscape/square are exempt) |
|
|
372
372
|
| `caption-oversize` | warn | Display-size type (>7.5% of canvas width) on a line of **5+ words** — it runs edge-to-edge, wraps, covers the frame, and forces a full-width plate. Both signals required, so a giant 2-word hook card passes |
|
|
@@ -53,6 +53,21 @@ Templates are listed on the Vidfarm homepage and `/discover`. Each has a `templa
|
|
|
53
53
|
- API: `GET /discover/feed` — returns `{ templates: [{ templateId, slugId, title, previewUrl, viralDna, durationSeconds, sourceType, promotions, keywords, summary, ... }], next_cursor }`
|
|
54
54
|
- Search: `GET /discover/feed?q=<offer>&limit=20&sort=relevance` hybrid-searches the eligible public catalog using semantic embeddings plus lexical matches. Semantic query embedding uses Vidfarm's canonical OpenRouter-routed model and bills the provider cost × the standard 1.2 markup to the user's wallet. The response's `search` block reports `mode`, `embedding_space`, and any `semantic_limitation`; when it says `lexical_structured`, disclose the limitation briefly and continue rather than refusing. Decomposition adds `promotions`, `keywords`, `summary`, `categoryTags`, and `catalogIntelligence` (`wowScore`, `wowReason`, `automationScore`, `automationReason`, `contentStyles`, `searchText`). Use `sort=wow` for highest-quality/client-impressing formats, `sort=automation` for cheap repeatable bulk formats, and `sort=recent` only when freshness is the intent. Follow `next_cursor` with `cursor=<value>`; never call page one the whole catalog. `GET /api/v1/videos?q=<offer>&limit=20[&mine=true]` searches source **inspirations**. Undecomposed inspirations have only sparse ingest metadata, so they are harder to retrieve semantically. Explain that somebody in the world needs to decompose one once and the shared enrichment then benefits everyone; the current user need not act unless they want that specific inspiration immediately.
|
|
55
55
|
|
|
56
|
+
### Featured templates (members only)
|
|
57
|
+
|
|
58
|
+
Featured is the small, hand-curated shelf at the top of the Discover feed. Signed-in members open on it by default; anonymous visitors do not see it at all.
|
|
59
|
+
|
|
60
|
+
- `GET /discover/feed?view=featured` — the shelf, in curated order (`featured: true`, `featuredRank` ascending). Signed-out callers get `401`.
|
|
61
|
+
- In the ordinary `view=available` feed, a signed-in caller's featured picks sort first (an active `q=` search keeps relevance order instead).
|
|
62
|
+
- Browser: the **View** dropdown on `/discover/templates/feed` carries **View Featured**, and `?view=featured` deep-links to it.
|
|
63
|
+
|
|
64
|
+
Operators curate the shelf with the superagency key (`x-superagency-key`). Every id below accepts an `inspiration_...`, `template_...`, or `fork_...` id:
|
|
65
|
+
|
|
66
|
+
- `GET /api/v1/admin/discover/featured` — the current shelf, plus `unresolved` ids that no longer render (deleted, archived, or gone private).
|
|
67
|
+
- `POST /api/v1/admin/discover/featured { id, rank? }` — feature one template. Default rank appends it to the end.
|
|
68
|
+
- `PUT /api/v1/admin/discover/featured { items: [{ id, rank? }] }` — replace the whole shelf; array order is shelf order, and anything not listed is un-featured. Ids that resolve to nothing come back under `unknown`.
|
|
69
|
+
- `DELETE /api/v1/admin/discover/featured/:id` — drop one pick from the shelf.
|
|
70
|
+
|
|
56
71
|
Each template exposes a public preview:
|
|
57
72
|
|
|
58
73
|
- `GET /editor/:templateId` — opens the Trackpad Editor for the template (redirects to your fork of it, or to `/login`)
|
|
@@ -616,18 +616,24 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
|
|
|
616
616
|
- *Judgement call, inside that band:* **put the words where the picture isn't.** `y≈70%` is the `captions generate` default because most footage puts its subject mid-frame — it is a default, not a law. Before you place text, **look at an actual frame** (`vidfarm stills ./work --at <t>`, free) and find the region with the least going on: open sky above a dashboard, a blank wall behind a talking head, an out-of-focus background, an empty tabletop. If nothing else in the video is competing for attention there — no subject, no motion, no product, no second text layer — that is where the caption belongs, even if it means **high-centre at y≈10–25%** instead of a lower third. A caption dropped over the busiest third of the frame (hands on a steering wheel, a face, the product) fights the shot and forces you to armour it with a plate; the same words parked in the sky are legible with no plate at all.
|
|
617
617
|
- *When you're only rescuing an inherited caption* off a dead-zone edge, preserve its top-vs-bottom anchoring and just pull it inside the band — don't recentre a template you haven't re-read. When **you** are the one placing the text, place it deliberately.
|
|
618
618
|
- **Size → scaled to the line, not maxed out.** Sizes are PIXELS of a 1080-wide frame: **~36–64px** reads well; below ~28px is unreadable on a phone and **0 is invisible**. Above ~64px is a *hook-word* size — one to three words, on purpose. The failure this catches: a full sentence set at display size runs edge-to-edge, wraps to three lines, and eats a third of the frame, so it has to be armoured with a full-width plate and there is nowhere left to put it. **If a line reaches the frame edges, the fix is a smaller size (or fewer words per cue), not a wider box.** Keep captions to ~2 lines / ~5 words per line; `line_height` 0.95–1.15 for stacked display lines.
|
|
619
|
-
- **Font → the composition regime. Five families
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
|
624
|
-
|
|
625
|
-
| **
|
|
626
|
-
| **
|
|
627
|
-
| **
|
|
628
|
-
|
|
|
629
|
-
|
|
630
|
-
|
|
619
|
+
- **Font → the composition regime. Five families are the heavy default.** The whole regime on one page — each family rendered as a real caption, the four legal backgrounds, copy-paste `set-style` commands: **<https://vidfarm.cc/fonts>** (combined specimen sheet: `https://vidfarm.cc/assets/tiktok-caption-fonts.png`). Read it once before you style anything, and hand the link to a human director who is picking a look.
|
|
620
|
+
|
|
621
|
+
**Every style also has its OWN standalone reference image** — one family (or one background), rendered at caption size on real footage-like plate. **Use them for comparison.** The workflow: pick a family → style the layer → pull a still (`vidfarm stills ./work --at <t>`) → open that family's card next to the still and check the glyphs match. A fallback font is obvious side by side and nearly invisible on its own, and this is the only check that catches a family that failed to load. Fetch the one card you need instead of the whole sheet.
|
|
622
|
+
|
|
623
|
+
| Family | Weights it really has | Standalone reference card | Use it for |
|
|
624
|
+
|---|---|---|---|
|
|
625
|
+
| **TikTok Sans** | 400 / 600 / 700 / 800 / 900 | `https://vidfarm.cc/assets/fonts/caption-font-tiktok-sans.png` | The native TikTok caption look. **The safe default when unsure.** |
|
|
626
|
+
| **Montserrat** | 600 / 700 / 800 / 900 | `https://vidfarm.cc/assets/fonts/caption-font-montserrat.png` | Geometric bold display. Hooks, hard statements, the Hormozi caption. **The bold default.** |
|
|
627
|
+
| **Abel** | 400 only | `https://vidfarm.cc/assets/fonts/caption-font-abel.png` | Condensed headline / newsletter vibe. A long line that must stay on one row. |
|
|
628
|
+
| **Source Code Pro** | 700 only | `https://vidfarm.cc/assets/fonts/caption-font-source-code-pro.png` | Code / terminal beats only. Never a whole video. |
|
|
629
|
+
| **Yesteryear** | 400 only | `https://vidfarm.cc/assets/fonts/caption-font-yesteryear.png` | Cursive script. **One accent line** (a quote) — never a caption track; it is unreadable at cue size. |
|
|
630
|
+
| ~~Georgia~~ | — | `https://vidfarm.cc/assets/fonts/caption-font-georgia.png` (anti-example) | **Decompose-only. Do not author in it.** The decompose vision pass may report `Georgia` off a source video's serif (it is the 6th value in `ALLOWED_FONTS`, `src/services/hyperframes.ts`), but the composition does not import it, so `normalizeTikTokCaptionLayout` coerces it to Montserrat on every local render. Rebuild an editorial look in Abel or Montserrat instead. |
|
|
631
|
+
|
|
632
|
+
The four legal backgrounds have cards too: `caption-bg-outline.png`, `caption-bg-plain.png`, `caption-bg-spotlight.png`, `caption-bg-highlight-solid.png` (same `/assets/fonts/` path). Regenerate all ten with `node scripts/render-font-specimens.mjs`.
|
|
633
|
+
|
|
634
|
+
These five are exactly what the composition `@import`s from Google Fonts, and exactly what `CAPTION_FONT_REGIME` (`src/devcli/composition-edit.ts`) keeps. **Anything else is normally not imported**: `Inter`, `Roboto`, `Arial`, `Helvetica`, `system-ui`, a client's brand font — the render silently falls back to a web-default sans, which is exactly the slop look. Asking for a weight the family does not ship (Abel 900, Yesteryear 700) fakes it with a synthetic bold and looks smeared — pick a family that has the weight instead.
|
|
635
|
+
|
|
636
|
+
**Custom fonts are allowed — the regime is a heavy suggestion, not a ban.** What is forbidden is *naming* a family the composition never ships, because that one silently falls back. If you want a sixth family, **declare it in the composition**: an `@font-face` pointing at a real file, or a Google Fonts `@import`/`<link>` that names it. A declared family is left alone — `normalizeTikTokCaptionLayout` does not coerce it and `vidfarm qa` does not flag it (`compositionDeclaresFont`, `src/devcli/composition-edit.ts`). An undeclared one is coerced to Montserrat locally and reported as a `font-regime` finding. Deviate on purpose, ship the font, then confirm on a rendered still — the editor preview loads fonts your render machine may not have. Matching a client's brand font is always fine for a *wordmark image*; on the caption layer, only do it with the `@font-face` in place.
|
|
631
637
|
- **Background → one of exactly four valid treatments.** Any text you place uses one of these and nothing else:
|
|
632
638
|
|
|
633
639
|
| # | Treatment | How to set it | When |
|
|
@@ -652,7 +658,7 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
|
|
|
652
658
|
|
|
653
659
|
**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 — and a re-theme prompt ("make this for my tutoring service") is exactly where an agent starts inventing landing-page CTAs and benefit chips because the *subject* is a SaaS product. **Fix to the standard, don't inherit it, and don't import the website's design language into the video.** When placing text yourself (`set_captions`, `set_layer_text`, `set_layer_style`, `add_layer`, devcli `place`/`captions`), set `y` / `font_family` / `font_weight` / `background_style` to the standard from the start.
|
|
654
660
|
|
|
655
|
-
> Local devcli renders enforce part of 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 only fixes position and font family — it will happily render your Bootstrap card. Get it right in the composition so the editor preview, the local render, and any cloud render match.
|
|
661
|
+
> Local devcli renders enforce part of 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 — **unless the composition declares that family itself**, in which case your custom font renders as authored. It only fixes position and font family — it will happily render your Bootstrap card. Get it right in the composition so the editor preview, the local render, and any cloud render match.
|
|
656
662
|
|
|
657
663
|
### Animated captions — word-by-word caption styles (TikTok/CapCut)
|
|
658
664
|
|
|
@@ -39,6 +39,7 @@ Then answer these, out loud, in your report:
|
|
|
39
39
|
- **Balance.** Is weight distributed across the frame, or is every scene top-anchored with an empty band underneath? Does the composition use the canvas, or does it use the top third of the canvas and leave the rest as dead area? A sheet of twelve frames makes a recurring dead zone obvious; one frame at a time never will.
|
|
40
40
|
- **Fluff, named out loud.** Which beats would you cut? Answer with specific timestamps, not "it's tight". Every tile has to justify its seconds: a frame that repeats the previous one, a scene the video would survive losing, an intro, a tail after the last word, a hold that's just waiting. **Assume 30–50% of the first assembly can go** and name what you'd remove — "nothing to cut" on a first pass is almost always a review that didn't look. Then cut it and `ripple` the hole closed (craft: `references/hooks-and-virality.md` → "Density"; the mechanical half is `vidfarm qa`'s `dead-air` / `dead-tail` / `slow-scene`).
|
|
41
41
|
- **Spacing and breathing room.** Are margins consistent scene to scene? Does one beat have generous air and the next one crowd the safe zone? Uneven padding across scenes is the single loudest "assembled by a machine" tell, and it's invisible while you're inside any one scene.
|
|
42
|
+
- **Did the font you asked for actually render?** A family the composition never imported falls back to a web-default sans silently — the render *looks* fine, just generic, and you will not notice by memory. Open the standalone reference card for the family you chose (`https://vidfarm.cc/assets/fonts/caption-font-<family>.png` — `tiktok-sans`, `montserrat`, `abel`, `source-code-pro`, `yesteryear`, plus the `georgia` anti-example) next to your still and **compare the glyphs**. Same check for the text background: `caption-bg-outline|plain|spotlight|highlight-solid.png`. A fallback is obvious side by side and invisible on its own.
|
|
42
43
|
- **Typographic continuity.** One type system, or three? Headline sizes should belong to a small set (two, maybe three), not be individually chosen per scene. Same for weight, case, and colour. If scene 2's headline is 64px and scene 5's is 41px for no dramatic reason, that's drift, not design.
|
|
43
44
|
- **Colour and style coherence.** One accent colour, one background treatment, one illustration style. Assets generated or sourced at different moments drift — a flat-vector sticker next to a photographic cutout next to a gradient panel reads as three videos spliced together.
|
|
44
45
|
- **Rhythm and pacing.** Do scene durations form a deliberate pattern (a fast open, a longer explanation, a fast close), or is every scene the same length because a loop wrote them? Same-length beats are hypnotic in the bad way. Conversely, one 9-second hold in a video of 2-second cuts stalls it dead.
|
package/SKILL.director.md
CHANGED
|
@@ -382,7 +382,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
|
|
|
382
382
|
**Two halves, and only one is machine-checkable.** The `checks:` front matter is settled deterministically by `vidfarm qa` (duration, aspect, `hook_words_max`, `forbid_text`, …); every `- [ ]` line comes back as a **review item you answer honestly in your report** — never claim a video passed the half the CLI can't judge. Harnesses stack and auto-discover: `vidfarm qa ./work` picks up `./work/HARNESS.md`, `--harness hooks --harness ./brand/HOUSE.md` adds more, and any file of theirs anywhere is valid. Format and strand table: `harnesses/README.md`; scripting-mode detail: `references/automation-and-local-dev.md`. *(Formerly `QA_REGIME.md` — same file, and `vidfarm regime …` still works as an alias.)*
|
|
383
383
|
- **A video is judged as a SEQUENCE, so review it as one.** Agents build scene by scene and each scene passes in isolation while the video drifts — inconsistent margins, three type sizes, an accent colour that wanders, beats that are all the same length, a jarring join. Tile a dozen stills into one contact sheet (`vidfarm stills ./work --sheet`) and read it as an image before you call anything done, fix drift by defining the system rather than patching the odd scene out, and remember that **your own confident "verified, looks good" is the single least reliable signal in this workflow** — it was wrong on every video of a 32-video batch. Method: `references/reviewing-renders.md`.
|
|
384
384
|
- **On devcli there's an OPTIONAL checker: `vidfarm qa ./work`.** Free, instant, local-only — it blocklists exactly the slop above plus first-frame/thumbnail and font-regime/safe-zone drift, and prints a concrete fix per finding. **Feedback, not a gate**: it exits 0 even on findings, never runs automatically, and is a blocklist (unusual/stylized compositions pass untouched). **Skipping it is fine — watching the render is the review that actually counts, and a clean `qa` is not one.** When you do run it, it allows **one** fix round by default: the first pass names the slop, one fix clears it, and a second round is nearly always taste rather than a defect. The human owns that number — `--max-revisions <n>` raises it, `0` disables it; ask rather than raising it yourself. `--json` for scripted batches, `--strict` only if you want a CI failure. **Web-chat copilot: this command does not exist for you** (devcli-only, no REST twin) — apply the standard by hand, and when handing a heavy job to a local coding agent, tell them to run `vidfarm qa`.
|
|
385
|
-
- **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, inside that band, is **placed in the emptiest part of the frame** rather than dumped on the default lower third (read a still first — `vidfarm stills ./work --at <t>`; words over open sky or a blank wall beat words over the subject, and usually need no plate at all). Long narration is **paged into 3–5-word kinetic cues** (`vidfarm captions generate --style word-pop`), never one static wall of text. It uses the composition's bold font regime — **
|
|
385
|
+
- **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, inside that band, is **placed in the emptiest part of the frame** rather than dumped on the default lower third (read a still first — `vidfarm stills ./work --at <t>`; words over open sky or a blank wall beat words over the subject, and usually need no plate at all). Long narration is **paged into 3–5-word kinetic cues** (`vidfarm captions generate --style word-pop`), never one static wall of text. It uses the composition's bold font regime — **five default families**: TikTok Sans (safe default), Montserrat (bold default), Abel (condensed), Source Code Pro (mono, 700 only), Yesteryear (script accent line only) — at weight **700–900**, ~36–64px on a 1080-wide frame. Any other family (Inter / Roboto / Arial / system-ui / Georgia / a client brand font) is not imported and silently falls back to a web-default sans at render — **so a custom font IS allowed, but only if you declare it in the composition** (`@font-face` or a Google Fonts `@import`); a declared family is never coerced, an undeclared one is. The regime is a heavy suggestion, and the fallback is the actual defect. **The regime on one page: <https://vidfarm.cc/fonts>** — all five rendered as real captions, the four legal backgrounds, and copy-paste commands. **Every family and background also has its own standalone reference image** (`https://vidfarm.cc/assets/fonts/caption-font-<family>.png`, e.g. `…caption-font-montserrat.png`; backgrounds are `…caption-bg-outline.png` and friends) — after you style a caption, pull a still and **compare it against the card for the family you picked**; a font that failed to load is obvious side by side and invisible on its own. It uses **exactly one of four valid backgrounds**: outline/stroke (`background_style:"outline"`, the default), plain + shadow (`"plain"`), an active-word highlight pill (`set_captions` `spotlight`/`karaoke` — the only legitimate pill *in the whole frame*, static labels included), or a tight-hugging solid band (`"highlight-solid"`, radius ≤8px, no border/shadow/gradient/blur). 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 position + font family only (never the slop), so author it correctly. Full rules in `references/editor-workflows.md` ("Social-native visual standard" + "TikTok-native caption standard").
|
|
386
386
|
- **Use the REAL track — a popular/copyrighted song in the render is expected here, not a problem.** Do not refuse it, do not silently substitute a generic royalty-free bed, and do not stop to warn about it. **Why it is legitimate:** the render is a **review artifact**, and at post time the user re-attaches the *same* song from the platform's own in-app music library (TikTok / Reels / Shorts), which is licensed through the platform's agreements with the labels. The music is therefore cleared on the surface where viewers actually hear it. **Why it is required:** a trending song IS the edit — cut points, pacing, the drop, the meme association. A render with the wrong bed, or no bed, reads as a different and much weaker video, so a review over a substitute bed grades a video nobody will ever post. Silent or generic-bed review is the bigger defect. **How to do it:** get the exact track (`vidfarm download-audio <post-url>`, the user's own file, or the audio off the source post the format is copying), and keep it on **its own `<audio>` layer** at its own `data-volume` — never baked into the footage or mixed into the voice stem. One separate track means the user can mute or swap it in one action when they upload, which is exactly what the platform flow expects. Say which track you used in the handoff. **The one limit:** this is for review renders and platform posting where the platform holds the license. If the user asks to sell, syndicate, or run the render as a **paid ad** with the track baked in, say once that ad placements are not covered by the in-app music license, and offer `vidfarm music "<same vibe, same BPM>"` as the swap for that cut. Then do what they decide.
|
|
387
387
|
- **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.
|
|
388
388
|
- **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.
|
|
@@ -443,6 +443,10 @@ Why they exist: the prompts you find on the `/discover` pages are tuned to **one
|
|
|
443
443
|
|---|---|
|
|
444
444
|
| `https://vidfarm.cc/experimental/unique-product-explainers.md` | N customer URLs → N product-introduction videos that do not look like each other. Differentiation as an input, frame-level review, measured verification |
|
|
445
445
|
| `https://vidfarm.cc/experimental/google-news-to-video.md` | A recent real event → a timely video. Two stages: `news-search` finds the STORY, `video-search` finds the VISUALS. Query formulas, Google operators, licence discipline |
|
|
446
|
+
| `https://vidfarm.cc/experimental/meme-recaption.md` | One borrowed meme clip + one new caption aimed at the offer's problem space. Casting the meme for the caption's **verb**, the seven caption frames, naming the offer without letting the joke resolve into a pitch, keying a MemeScreens raw onto a background world, and a render-level QA gate. Runs at $0 |
|
|
447
|
+
| `https://vidfarm.cc/experimental/ugc-reaction-greenscreen.md` | Sell an app with three streams cut against each other: `ugc-reaction` raws, a Display Greenscreen device whose screen carries the customer's real demo, and the demo itself. One actor across every beat via `actor_<uuid>`, a per-frame tracked screen insert, captions inside the platform-chrome-safe core, and two exports from one render (voiceover-only to publish, voiceover+music to review) |
|
|
448
|
+
| `https://vidfarm.cc/experimental/sticker-slideshow-tips.md` | A tips **carousel** — the deliverable is N still slides, and the 3.0s-per-slide MP4 is only the playable preview. Die-cut cutouts on a paper page or a photo background, a literal "Tips for…" cover, the specificity ladder, one slide shilled from the middle and written so it survives deleting the brand name, four background modes, and a measured WCAG contrast gate on the exported PNGs |
|
|
449
|
+
| `https://vidfarm.cc/experimental/wall-text-pov-ugc.md` | One unbroken ambient take + one static block of unplated type. No cuts, no voiceover, no subtitles, nothing animated. The retention engine is arithmetic — `duration = words / 8`, so one play lands the viewer at the **halfway mark**, committed and one pass from done — floored at 8s so a trending sound gets a real phrase of a track. The four speaker frames, MIRROR vs TURN, a density pass that treats padding as the fatal failure, casting the scene dark so the type needs no plate (measured), ping-ponging the plate for a seamless loop, and three gates. Runs at $0 |
|
|
446
450
|
|
|
447
451
|
Fetch one as plain markdown and follow it end to end; do not skim it into a summary.
|
|
448
452
|
|
|
@@ -558,6 +562,21 @@ Templates are listed on the Vidfarm homepage and `/discover`. Each has a `templa
|
|
|
558
562
|
- API: `GET /discover/feed` — returns `{ templates: [{ templateId, slugId, title, previewUrl, viralDna, durationSeconds, sourceType, promotions, keywords, summary, ... }], next_cursor }`
|
|
559
563
|
- Search: `GET /discover/feed?q=<offer>&limit=20&sort=relevance` hybrid-searches the eligible public catalog using semantic embeddings plus lexical matches. Semantic query embedding uses Vidfarm's canonical OpenRouter-routed model and bills the provider cost × the standard 1.2 markup to the user's wallet. The response's `search` block reports `mode`, `embedding_space`, and any `semantic_limitation`; when it says `lexical_structured`, disclose the limitation briefly and continue rather than refusing. Decomposition adds `promotions`, `keywords`, `summary`, `categoryTags`, and `catalogIntelligence` (`wowScore`, `wowReason`, `automationScore`, `automationReason`, `contentStyles`, `searchText`). Use `sort=wow` for highest-quality/client-impressing formats, `sort=automation` for cheap repeatable bulk formats, and `sort=recent` only when freshness is the intent. Follow `next_cursor` with `cursor=<value>`; never call page one the whole catalog. `GET /api/v1/videos?q=<offer>&limit=20[&mine=true]` searches source **inspirations**. Undecomposed inspirations have only sparse ingest metadata, so they are harder to retrieve semantically. Explain that somebody in the world needs to decompose one once and the shared enrichment then benefits everyone; the current user need not act unless they want that specific inspiration immediately.
|
|
560
564
|
|
|
565
|
+
### Featured templates (members only)
|
|
566
|
+
|
|
567
|
+
Featured is the small, hand-curated shelf at the top of the Discover feed. Signed-in members open on it by default; anonymous visitors do not see it at all.
|
|
568
|
+
|
|
569
|
+
- `GET /discover/feed?view=featured` — the shelf, in curated order (`featured: true`, `featuredRank` ascending). Signed-out callers get `401`.
|
|
570
|
+
- In the ordinary `view=available` feed, a signed-in caller's featured picks sort first (an active `q=` search keeps relevance order instead).
|
|
571
|
+
- Browser: the **View** dropdown on `/discover/templates/feed` carries **View Featured**, and `?view=featured` deep-links to it.
|
|
572
|
+
|
|
573
|
+
Operators curate the shelf with the superagency key (`x-superagency-key`). Every id below accepts an `inspiration_...`, `template_...`, or `fork_...` id:
|
|
574
|
+
|
|
575
|
+
- `GET /api/v1/admin/discover/featured` — the current shelf, plus `unresolved` ids that no longer render (deleted, archived, or gone private).
|
|
576
|
+
- `POST /api/v1/admin/discover/featured { id, rank? }` — feature one template. Default rank appends it to the end.
|
|
577
|
+
- `PUT /api/v1/admin/discover/featured { items: [{ id, rank? }] }` — replace the whole shelf; array order is shelf order, and anything not listed is un-featured. Ids that resolve to nothing come back under `unknown`.
|
|
578
|
+
- `DELETE /api/v1/admin/discover/featured/:id` — drop one pick from the shelf.
|
|
579
|
+
|
|
561
580
|
Each template exposes a public preview:
|
|
562
581
|
|
|
563
582
|
- `GET /editor/:templateId` — opens the Trackpad Editor for the template (redirects to your fork of it, or to `/login`)
|
|
@@ -1645,18 +1664,24 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
|
|
|
1645
1664
|
- *Judgement call, inside that band:* **put the words where the picture isn't.** `y≈70%` is the `captions generate` default because most footage puts its subject mid-frame — it is a default, not a law. Before you place text, **look at an actual frame** (`vidfarm stills ./work --at <t>`, free) and find the region with the least going on: open sky above a dashboard, a blank wall behind a talking head, an out-of-focus background, an empty tabletop. If nothing else in the video is competing for attention there — no subject, no motion, no product, no second text layer — that is where the caption belongs, even if it means **high-centre at y≈10–25%** instead of a lower third. A caption dropped over the busiest third of the frame (hands on a steering wheel, a face, the product) fights the shot and forces you to armour it with a plate; the same words parked in the sky are legible with no plate at all.
|
|
1646
1665
|
- *When you're only rescuing an inherited caption* off a dead-zone edge, preserve its top-vs-bottom anchoring and just pull it inside the band — don't recentre a template you haven't re-read. When **you** are the one placing the text, place it deliberately.
|
|
1647
1666
|
- **Size → scaled to the line, not maxed out.** Sizes are PIXELS of a 1080-wide frame: **~36–64px** reads well; below ~28px is unreadable on a phone and **0 is invisible**. Above ~64px is a *hook-word* size — one to three words, on purpose. The failure this catches: a full sentence set at display size runs edge-to-edge, wraps to three lines, and eats a third of the frame, so it has to be armoured with a full-width plate and there is nowhere left to put it. **If a line reaches the frame edges, the fix is a smaller size (or fewer words per cue), not a wider box.** Keep captions to ~2 lines / ~5 words per line; `line_height` 0.95–1.15 for stacked display lines.
|
|
1648
|
-
- **Font → the composition regime. Five families
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
|
1653
|
-
|
|
1654
|
-
| **
|
|
1655
|
-
| **
|
|
1656
|
-
| **
|
|
1657
|
-
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1667
|
+
- **Font → the composition regime. Five families are the heavy default.** The whole regime on one page — each family rendered as a real caption, the four legal backgrounds, copy-paste `set-style` commands: **<https://vidfarm.cc/fonts>** (combined specimen sheet: `https://vidfarm.cc/assets/tiktok-caption-fonts.png`). Read it once before you style anything, and hand the link to a human director who is picking a look.
|
|
1668
|
+
|
|
1669
|
+
**Every style also has its OWN standalone reference image** — one family (or one background), rendered at caption size on real footage-like plate. **Use them for comparison.** The workflow: pick a family → style the layer → pull a still (`vidfarm stills ./work --at <t>`) → open that family's card next to the still and check the glyphs match. A fallback font is obvious side by side and nearly invisible on its own, and this is the only check that catches a family that failed to load. Fetch the one card you need instead of the whole sheet.
|
|
1670
|
+
|
|
1671
|
+
| Family | Weights it really has | Standalone reference card | Use it for |
|
|
1672
|
+
|---|---|---|---|
|
|
1673
|
+
| **TikTok Sans** | 400 / 600 / 700 / 800 / 900 | `https://vidfarm.cc/assets/fonts/caption-font-tiktok-sans.png` | The native TikTok caption look. **The safe default when unsure.** |
|
|
1674
|
+
| **Montserrat** | 600 / 700 / 800 / 900 | `https://vidfarm.cc/assets/fonts/caption-font-montserrat.png` | Geometric bold display. Hooks, hard statements, the Hormozi caption. **The bold default.** |
|
|
1675
|
+
| **Abel** | 400 only | `https://vidfarm.cc/assets/fonts/caption-font-abel.png` | Condensed headline / newsletter vibe. A long line that must stay on one row. |
|
|
1676
|
+
| **Source Code Pro** | 700 only | `https://vidfarm.cc/assets/fonts/caption-font-source-code-pro.png` | Code / terminal beats only. Never a whole video. |
|
|
1677
|
+
| **Yesteryear** | 400 only | `https://vidfarm.cc/assets/fonts/caption-font-yesteryear.png` | Cursive script. **One accent line** (a quote) — never a caption track; it is unreadable at cue size. |
|
|
1678
|
+
| ~~Georgia~~ | — | `https://vidfarm.cc/assets/fonts/caption-font-georgia.png` (anti-example) | **Decompose-only. Do not author in it.** The decompose vision pass may report `Georgia` off a source video's serif (it is the 6th value in `ALLOWED_FONTS`, `src/services/hyperframes.ts`), but the composition does not import it, so `normalizeTikTokCaptionLayout` coerces it to Montserrat on every local render. Rebuild an editorial look in Abel or Montserrat instead. |
|
|
1679
|
+
|
|
1680
|
+
The four legal backgrounds have cards too: `caption-bg-outline.png`, `caption-bg-plain.png`, `caption-bg-spotlight.png`, `caption-bg-highlight-solid.png` (same `/assets/fonts/` path). Regenerate all ten with `node scripts/render-font-specimens.mjs`.
|
|
1681
|
+
|
|
1682
|
+
These five are exactly what the composition `@import`s from Google Fonts, and exactly what `CAPTION_FONT_REGIME` (`src/devcli/composition-edit.ts`) keeps. **Anything else is normally not imported**: `Inter`, `Roboto`, `Arial`, `Helvetica`, `system-ui`, a client's brand font — the render silently falls back to a web-default sans, which is exactly the slop look. Asking for a weight the family does not ship (Abel 900, Yesteryear 700) fakes it with a synthetic bold and looks smeared — pick a family that has the weight instead.
|
|
1683
|
+
|
|
1684
|
+
**Custom fonts are allowed — the regime is a heavy suggestion, not a ban.** What is forbidden is *naming* a family the composition never ships, because that one silently falls back. If you want a sixth family, **declare it in the composition**: an `@font-face` pointing at a real file, or a Google Fonts `@import`/`<link>` that names it. A declared family is left alone — `normalizeTikTokCaptionLayout` does not coerce it and `vidfarm qa` does not flag it (`compositionDeclaresFont`, `src/devcli/composition-edit.ts`). An undeclared one is coerced to Montserrat locally and reported as a `font-regime` finding. Deviate on purpose, ship the font, then confirm on a rendered still — the editor preview loads fonts your render machine may not have. Matching a client's brand font is always fine for a *wordmark image*; on the caption layer, only do it with the `@font-face` in place.
|
|
1660
1685
|
- **Background → one of exactly four valid treatments.** Any text you place uses one of these and nothing else:
|
|
1661
1686
|
|
|
1662
1687
|
| # | Treatment | How to set it | When |
|
|
@@ -1681,7 +1706,7 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
|
|
|
1681
1706
|
|
|
1682
1707
|
**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 — and a re-theme prompt ("make this for my tutoring service") is exactly where an agent starts inventing landing-page CTAs and benefit chips because the *subject* is a SaaS product. **Fix to the standard, don't inherit it, and don't import the website's design language into the video.** When placing text yourself (`set_captions`, `set_layer_text`, `set_layer_style`, `add_layer`, devcli `place`/`captions`), set `y` / `font_family` / `font_weight` / `background_style` to the standard from the start.
|
|
1683
1708
|
|
|
1684
|
-
> Local devcli renders enforce part of 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 only fixes position and font family — it will happily render your Bootstrap card. Get it right in the composition so the editor preview, the local render, and any cloud render match.
|
|
1709
|
+
> Local devcli renders enforce part of 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 — **unless the composition declares that family itself**, in which case your custom font renders as authored. It only fixes position and font family — it will happily render your Bootstrap card. Get it right in the composition so the editor preview, the local render, and any cloud render match.
|
|
1685
1710
|
|
|
1686
1711
|
### Animated captions — word-by-word caption styles (TikTok/CapCut)
|
|
1687
1712
|
|
|
@@ -2114,6 +2139,7 @@ Then answer these, out loud, in your report:
|
|
|
2114
2139
|
- **Balance.** Is weight distributed across the frame, or is every scene top-anchored with an empty band underneath? Does the composition use the canvas, or does it use the top third of the canvas and leave the rest as dead area? A sheet of twelve frames makes a recurring dead zone obvious; one frame at a time never will.
|
|
2115
2140
|
- **Fluff, named out loud.** Which beats would you cut? Answer with specific timestamps, not "it's tight". Every tile has to justify its seconds: a frame that repeats the previous one, a scene the video would survive losing, an intro, a tail after the last word, a hold that's just waiting. **Assume 30–50% of the first assembly can go** and name what you'd remove — "nothing to cut" on a first pass is almost always a review that didn't look. Then cut it and `ripple` the hole closed (craft: `references/hooks-and-virality.md` → "Density"; the mechanical half is `vidfarm qa`'s `dead-air` / `dead-tail` / `slow-scene`).
|
|
2116
2141
|
- **Spacing and breathing room.** Are margins consistent scene to scene? Does one beat have generous air and the next one crowd the safe zone? Uneven padding across scenes is the single loudest "assembled by a machine" tell, and it's invisible while you're inside any one scene.
|
|
2142
|
+
- **Did the font you asked for actually render?** A family the composition never imported falls back to a web-default sans silently — the render *looks* fine, just generic, and you will not notice by memory. Open the standalone reference card for the family you chose (`https://vidfarm.cc/assets/fonts/caption-font-<family>.png` — `tiktok-sans`, `montserrat`, `abel`, `source-code-pro`, `yesteryear`, plus the `georgia` anti-example) next to your still and **compare the glyphs**. Same check for the text background: `caption-bg-outline|plain|spotlight|highlight-solid.png`. A fallback is obvious side by side and invisible on its own.
|
|
2117
2143
|
- **Typographic continuity.** One type system, or three? Headline sizes should belong to a small set (two, maybe three), not be individually chosen per scene. Same for weight, case, and colour. If scene 2's headline is 64px and scene 5's is 41px for no dramatic reason, that's drift, not design.
|
|
2118
2144
|
- **Colour and style coherence.** One accent colour, one background treatment, one illustration style. Assets generated or sourced at different moments drift — a flat-vector sticker next to a photographic cutout next to a gradient panel reads as three videos spliced together.
|
|
2119
2145
|
- **Rhythm and pacing.** Do scene durations form a deliberate pattern (a fast open, a longer explanation, a fast close), or is every scene the same length because a loop wrote them? Same-length beats are hypnotic in the bad way. Conversely, one 9-second hold in a video of 2-second cuts stalls it dead.
|
|
@@ -3185,7 +3211,7 @@ What it flags:
|
|
|
3185
3211
|
| `clickable-element` | error/warn | `<button>`, `<form>`, `<input>`; `<a href>` warns |
|
|
3186
3212
|
| `web-framework-classes` | error/warn | Bootstrap/Tailwind class tokens (`btn`, `badge`, `card`, `hero`, `col-*`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-*`) or a linked CSS framework. A `<script>` CDN for GSAP/anime.js is fine |
|
|
3187
3213
|
| `page-structure` / `bullet-list` | error/warn | `<nav>`/`<header>`/`<footer>`/`<table>`; a `<ul>` with visible bullet markers |
|
|
3188
|
-
| `font-regime` | error/warn | A text layer in a website body font (Inter/Roboto/Arial/system-ui → **error**) or any family outside the imported regime (Montserrat, TikTok Sans, Abel, Source Code Pro, Yesteryear → warn, it silently falls back at render) |
|
|
3214
|
+
| `font-regime` | error/warn | A text layer in a website body font (Inter/Roboto/Arial/system-ui → **error**) or any family outside the imported regime (Montserrat, TikTok Sans, Abel, Source Code Pro, Yesteryear → warn, it silently falls back at render). **A custom family the composition DECLARES** (`@font-face` or a Google Fonts `@import` naming it) **is not flagged** — the rule targets the silent fallback, not your typography. Each regime family has a reference card at `https://vidfarm.cc/assets/fonts/caption-font-<family>.png`; compare a still against it after styling |
|
|
3189
3215
|
| `font-size` / `font-weight` | error/warn | `font-size:0` (invisible) is an error; sub-2.6%-of-canvas-width text and weight <600 warn |
|
|
3190
3216
|
| `caption-safe-zone` | warn | Text outside the 8%–85% band on a **portrait** canvas (landscape/square are exempt) |
|
|
3191
3217
|
| `caption-oversize` | warn | Display-size type (>7.5% of canvas width) on a line of **5+ words** — it runs edge-to-edge, wraps, covers the frame, and forces a full-width plate. Both signals required, so a giant 2-word hook card passes |
|
package/SKILL.md
CHANGED
|
@@ -95,6 +95,9 @@ vidfarm publish <forkId> # push edits back to th
|
|
|
95
95
|
| A free plan / a 402 / cost mode `minimize` on any search or download | `vidfarm browser setup`, then `vidfarm browse videos\|images\|news\|page "<q>"`. Never answer a sourcing ask with "that needs a paid plan." | `references/browser-harness.md` |
|
|
96
96
|
| "Make a video about what just happened" | two stages: `vidfarm news-search "<topic>" --fresh w` for the STORY, then `video-search` for the VISUALS | `vidfarm.cc/experimental/google-news-to-video.md` |
|
|
97
97
|
| "Recaption this meme" / "make a meme for our product" / "a reaction video" | cast a MemeScreens raw for the caption's **verb** (`vidfarm public-raws --category greenscreen`), key it onto a background world, one static caption. $0 | `vidfarm.cc/experimental/meme-recaption.md` |
|
|
98
|
+
| "A UGC ad for my app" / "someone reacting, then the app" / a client's app demo you have to make watchable | three streams cut against each other: `ugc-reaction` raws (ONE actor, via `actor_<uuid>`), a Display Greenscreen device carrying their real demo on its screen, and the demo. Ships a voiceover-only cut to publish + a voiceover+music cut to review | `vidfarm.cc/experimental/ugc-reaction-greenscreen.md` |
|
|
99
|
+
| "A tips slideshow / carousel" / "5 tips for X" / a listicle post | N still slides at exactly 3.0s, die-cut cutouts on a page or a photo, a literal "Tips for…" cover, ONE slide shilled from the middle. **The slides are the deliverable; the MP4 is the preview.** $0–$0.15 | `vidfarm.cc/experimental/sticker-slideshow-tips.md` |
|
|
100
|
+
| "A confession / truth bomb over a POV or ambient scene" / "just text on a video" / a text-story post | ONE unbroken take + ONE static block of unplated type. Nothing animates, nothing is cut. `duration = words / 8` (min 8s) so one play reaches the HALFWAY mark and they loop to finish. Cast the scene DARK so the type needs no plate. $0 | `vidfarm.cc/experimental/wall-text-pov-ugc.md` |
|
|
98
101
|
| "Download this video from `<url>`" | `vidfarm download-video <url>` (paid). Free plan gets a 402 — `vidfarm browse page "<url>"` and save it from their Chrome yourself, else have them download it, then `vidfarm put-file`. Never answer "I can't." | `references/browser-harness.md` |
|
|
99
102
|
| "Turn this thread / subreddit / profile into a video" | `vidfarm recycle <source>` — returns the raw decomposition, unranked; you pick the hook (paid) | `references/assets-and-sourcing.md` |
|
|
100
103
|
| "Create an avatar / spokesperson / talking head" | `vidfarm avatar "<who>" --say "<line>"` — a greenscreen talking-head video, keyed in the same job | `references/primitives.md` |
|
package/dist/src/cli.js
CHANGED
|
@@ -2747,7 +2747,7 @@ Rules:
|
|
|
2747
2747
|
- When swapping visuals, match both the literal scene DNA and the narrative purpose of the beat.
|
|
2748
2748
|
- For replacement graphics, screenshots, or still-like scenes, prefer AI image generation plus Ken Burns before paying for AI video unless static_vs_pivot says motion footage is load-bearing.
|
|
2749
2749
|
- If narration must be customized, default to premium ElevenLabs first, then the user's own ElevenLabs path, then BYOK OpenAI/Gemini/OpenRouter. If captions or scenes were timed to the old VO, retime them to the new narration.
|
|
2750
|
-
- NO HTML SLOP. You are editing HTML, but the output is a social video, not a web page. THE TEST IS THE NATIVE-EDITOR TEST: could you have made this element with the tools inside TikTok's own editor? That toolset is a font, a color, a stroke/outline, a soft shadow, a tight text box, alignment, opacity, rotation, animation presets — plus stickers, emoji, drawn marks and clips. It has NO padded capsule, NO border, NO gradient fill, NO blur panel, NO card. If you reached past it, cut it. Never author landing-page furniture: CTA "buttons" (a filled/gradient rounded capsule with action copy like "Sign Up for a Free Trial →"), benefit chip/badge rows ("✓ No Credit Card Needed"), bordered/shadowed/frosted cards holding a headline + URL, gradient text fills, feature grids, bulleted lists, or web-default fonts (Inter/Roboto/Arial/system-ui). AND NOT A SINGLE PILL EITHER: one lonely rounded, padded, filled capsule around a static stat or label — "10 hrs / week", "STEP 2", "EP.01", "+40%" — is a web badge, and being the only one on screen does not make it native. The ONLY legitimate capsule in a video is the active-word spotlight/karaoke caption highlight, because it moves with the spoken word. Emphasize a stat the way the editor would: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle or underline, or its own beat on screen. Rule of thumb on anything holding words: border-radius over ~8px PLUS a background fill PLUS padding = a badge; drop the fill or drop the radius until the band hugs the glyphs. None of this appears in a real TikTok, and nothing in a video is clickable — say it as timed text on the footage instead. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all fine. Captions use one of the FIVE imported families
|
|
2750
|
+
- NO HTML SLOP. You are editing HTML, but the output is a social video, not a web page. THE TEST IS THE NATIVE-EDITOR TEST: could you have made this element with the tools inside TikTok's own editor? That toolset is a font, a color, a stroke/outline, a soft shadow, a tight text box, alignment, opacity, rotation, animation presets — plus stickers, emoji, drawn marks and clips. It has NO padded capsule, NO border, NO gradient fill, NO blur panel, NO card. If you reached past it, cut it. Never author landing-page furniture: CTA "buttons" (a filled/gradient rounded capsule with action copy like "Sign Up for a Free Trial →"), benefit chip/badge rows ("✓ No Credit Card Needed"), bordered/shadowed/frosted cards holding a headline + URL, gradient text fills, feature grids, bulleted lists, or web-default fonts (Inter/Roboto/Arial/system-ui). AND NOT A SINGLE PILL EITHER: one lonely rounded, padded, filled capsule around a static stat or label — "10 hrs / week", "STEP 2", "EP.01", "+40%" — is a web badge, and being the only one on screen does not make it native. The ONLY legitimate capsule in a video is the active-word spotlight/karaoke caption highlight, because it moves with the spoken word. Emphasize a stat the way the editor would: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle or underline, or its own beat on screen. Rule of thumb on anything holding words: border-radius over ~8px PLUS a background fill PLUS padding = a badge; drop the fill or drop the radius until the band hugs the glyphs. None of this appears in a real TikTok, and nothing in a video is clickable — say it as timed text on the footage instead. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all fine. Captions use one of the FIVE imported families by default (Montserrat default / TikTok Sans / Abel / Source Code Pro / Yesteryear - the full regime, with a rendered specimen of each, is at https://vidfarm.cc/fonts). A custom family is allowed only if the composition DECLARES it (@font-face or a Google Fonts @import); an undeclared family silently falls back to a web-default sans and local renders coerce it to Montserrat. Every family has its own standalone reference card at https://vidfarm.cc/assets/fonts/caption-font-<family>.png (backgrounds: caption-bg-outline|plain|spotlight|highlight-solid.png) - after styling, pull a still and COMPARE it against the card for the family you picked, because a font that failed to load looks fine on its own. Weight 700-900, ~36-64px on a 1080-wide frame, inside the 8%-85% safe zone, with exactly one of four backgrounds: outline, plain, an active-word spotlight/karaoke pill, or a tight-hugging solid band (radius <=8px, no border/shadow/gradient/blur).
|
|
2751
2751
|
- NO LAYOUT TEMPLATES — JUDGE THE WHOLE FRAME, NOT JUST THE ELEMENT. Every rule above judges one element, and a frame can pass element-by-element and still be a web page. The archetype is the MODAL: the backdrop dimmed and blurred out of focus, and floating on top of it a rounded bordered box holding a big headline, a smaller support line, and a fat CTA button. THE STACK IS THE TELL, NOT THE BOX — delete the border, the fill and the capsule, keep headline then subheadline then CTA centred in a well with even margins, and it STILL reads as a landing page, because a viewer recognizes the SHAPE before reading a single word. Banned at frame level: a modal/dialog staged on top of a backdrop that has been dimmed, blurred, greyed or scaled back (nothing in a video pops "above" the video); the hero triplet and its cousins (title + kicker + logo lockup, question + answer + URL); a full-frame dark wash used to stage a floating block (a legibility band on ONE caption is legal, a page-wide wash to stage a panel is not; likewise a blurred backdrop is fine alone — a blurred fill behind a 16:9 clip in a 9:16 frame is a real technique — but blur PLUS dimming is modal staging); nav strip / hero / three-up feature row / testimonial block / footer fine print; a blurred website screenshot used as the background plate (if the backdrop is a web page, the frame is a screen recording of a web page — show the real product UI full-bleed and in focus, or don't show it); a centred content column with even gutters and document margins. THE FIX IS ALWAYS TO UNSTACK IT INTO TIME: the headline is the hook at start:0, the support line lands on the next cut, the CTA is SPOKEN or a bare caption on the final frame. You lose nothing — a viewer reads one line at a time anyway — and you gain the pacing that makes it look shot rather than designed. Self-check before you place any text group: am I arranging words relative to EACH OTHER, or relative to the PICTURE? Relative to each other is a layout, which is web. Two on-screen text runs at once is the ceiling. Verify on real pixels: \`vidfarm stills . --at <t>\` — if the still could be a screenshot of a website, rebuild the beat. \`vidfarm qa\` catches only the mechanical half (layout-template, modal-scrim); the frame-level judgement is yours.
|
|
2752
2752
|
- STRUCTURE BEFORE POLISH — THE FOUR CHARGES, WRITTEN BEFORE YOU TOUCH THE TIMELINE. Most agent-made videos fail on structure, not polish, because the timeline is the fun part so it gets built first and the words get retrofitted. Invert it: (1) HOOK — write the opening line as text first: a complete clause (subject + verb), no jargon, naming a SITUATION ("I've quit six businesses") not a label ("anonymity"); it goes on screen at start:0, because caption chunk 1 is read before any audio and muted autoplay is the default. Banned openings: throat-clearing ("so I was thinking", "here's the thing"), a logo, a title card, a fade from black, context before the claim. (2) LOOP — one open question by 0:10, said ON SCREEN, closing INSIDE this video (state the timestamp it closes at; if you can't, there is no loop), and the withheld answer must be one the viewer CANNOT supply themselves — a formally-correct loop with a guessable answer passes every mechanical check and dies in the field. (3) PAYOFF — shown, not summarized, ≥5 uninterrupted seconds, landing BEFORE the final beat; the payoff is not the CTA. (4) BAIT — one ask in the final beat and in the post caption; a keyword comment ask ("comment CLIPPER and I'll send the breakdown") is standard and allowed, but never "follow for part two", ragebait, or an earnings/health claim traded for the reply. Then build the timeline. Re-theming a decomposed template: viral_dna already names the source's hook/retention/payoff — rebuild each charge for the new subject, never flatten the loop into a product statement. Full craft harness: the vidfarm skill's references/hooks-and-virality.md. Checkable form: \`vidfarm harness show hooks\`.
|
|
2753
2753
|
- ORIENT THE COLD VIEWER IN THE FIRST 3 SECONDS — THE VIEWER HAS NO CONTEXT AND DID NOT CHOOSE THIS VIDEO. Distinct from the hook: the hook makes them WANT to watch, orientation makes the watching POSSIBLE. A stranger mid-scroll must be able to answer three things by ~3s — what am I looking at (the CATEGORY noun), who is it for, and why is this on my screen (the situation). The failure is not a bad first frame, it is a good video that BEGINS AT BEAT TWO, and the author cannot see it because the author already knows what the thing is. Signatures, each a rebuild not a polish: a pronoun with no referent ("it just works", "this changes everything", "here's how they do it"); starting at step three (the process already running, the dashboard already full); a metaphor whose subject only lands at 6s; insider vocabulary, a product's own feature name, or an ACRONYM in the first line; a detail crop that reads as texture until you know the whole. Instead, the opening beat is BOTH channels at once: an EASY IMAGE (one large subject, already moving, legible at a glance and at thumbnail scale — a relevant die-cut sticker names the category before a word is read) AND an EASY LINE (first spoken sentence one clause, <=12 words, everyday words, concrete noun + verb, no subordinate clause, brand name said once plainly, and the CATEGORY named: "X is a language app that…"). Give the SITUATION, not the label — "the end of the month, and your receipts are in a shoebox" orients, "expense automation" does not. THIS IS NOT AN INTRO AND COSTS NO EXTRA SECONDS: it replaces the wind-up sentence, it never precedes it, and it never licenses a logo, a title card or a fade from black. Test it on the render, not the script: play the first 3 seconds ONLY to somebody with no context and stop — they should say what kind of thing it is and roughly who it is for. "Something about audio" is a fail. Fullest form: \`vidfarm harness show product-explainer\` (Rule 0).
|
|
@@ -391,11 +391,42 @@ const CAPTION_DEFAULT_FRAME = { x: 10, y: 70, width: 80, height: 14 };
|
|
|
391
391
|
export const TIKTOK_CAPTION_SAFE_ZONE = { top: 8, bottom: 85 }; // % of canvas height
|
|
392
392
|
// The composition font regime — mirrors COMPOSITION_FONT_IMPORT's family list in
|
|
393
393
|
// services/studio-project-adapter.ts. A caption/text layer whose primary family
|
|
394
|
-
// is outside this set
|
|
395
|
-
// which means coercing it to the bold default is strictly an
|
|
394
|
+
// is outside this set is normally not imported (so it silently falls back at
|
|
395
|
+
// render), which means coercing it to the bold default is strictly an
|
|
396
|
+
// improvement. The regime is a strong default, not a ban: a family the
|
|
397
|
+
// composition declares for itself (see compositionDeclaresFont) is left alone.
|
|
396
398
|
const CAPTION_FONT_REGIME = ["tiktok sans", "montserrat", "abel", "source code pro", "yesteryear"];
|
|
397
399
|
const CAPTION_REGIME_FALLBACK_FONT = "Montserrat";
|
|
398
400
|
const CAPTION_FONT_FALLBACK_CHAIN = "'Montserrat', 'TikTok Sans', Abel, sans-serif";
|
|
401
|
+
/**
|
|
402
|
+
* A CUSTOM family is allowed — as long as the composition actually ships it.
|
|
403
|
+
* The regime exists because an unimported family silently falls back to a
|
|
404
|
+
* web-default sans at render; it is not a ban on typography. So: if the
|
|
405
|
+
* composition declares the family itself (an `@font-face` for it, or a Google
|
|
406
|
+
* Fonts `@import`/`<link>` naming it), leave the layer alone. Only a family
|
|
407
|
+
* with no declaration anywhere gets coerced, because that one really is broken.
|
|
408
|
+
*/
|
|
409
|
+
export function compositionDeclaresFont(html, family) {
|
|
410
|
+
const name = family.trim().toLowerCase();
|
|
411
|
+
if (!name)
|
|
412
|
+
return false;
|
|
413
|
+
const hay = html.toLowerCase();
|
|
414
|
+
// @font-face { font-family: "Brand Sans" }
|
|
415
|
+
const faceRe = /@font-face\s*{[^}]*}/g;
|
|
416
|
+
for (const block of hay.match(faceRe) ?? []) {
|
|
417
|
+
const declared = block.match(/font-family\s*:\s*['"]?([^;'"}]+)/);
|
|
418
|
+
if (declared && declared[1].trim() === name)
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
// Google Fonts URL: family=Brand+Sans / family=Brand%20Sans
|
|
422
|
+
const urlName = name.replace(/\s+/g, "");
|
|
423
|
+
for (const m of hay.matchAll(/fonts\.googleapis\.com\/css2\?([^"')\s]+)/g)) {
|
|
424
|
+
const params = m[1].replace(/\+/g, "").replace(/%20/g, "");
|
|
425
|
+
if (params.includes(`family=${urlName}`))
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
399
430
|
function setStylePercent(node, prop, value) {
|
|
400
431
|
if (node?.style)
|
|
401
432
|
node.style[prop] = `${Number(value.toFixed(2))}%`;
|
|
@@ -450,7 +481,9 @@ export function normalizeTikTokCaptionLayout(html) {
|
|
|
450
481
|
}
|
|
451
482
|
// Font: coerce off-regime primary family to the bold default.
|
|
452
483
|
const primary = String(node.getAttribute?.("data-font-family") || "").trim();
|
|
453
|
-
if (primary &&
|
|
484
|
+
if (primary &&
|
|
485
|
+
!CAPTION_FONT_REGIME.includes(primary.toLowerCase()) &&
|
|
486
|
+
!compositionDeclaresFont(html, primary)) {
|
|
454
487
|
node.setAttribute?.("data-font-family", CAPTION_REGIME_FALLBACK_FONT);
|
|
455
488
|
if (node.style)
|
|
456
489
|
node.style.fontFamily = CAPTION_FONT_FALLBACK_CHAIN;
|
|
@@ -28,6 +28,7 @@ import { createHash } from "node:crypto";
|
|
|
28
28
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
29
29
|
import path from "node:path";
|
|
30
30
|
import { parseHTML } from "linkedom";
|
|
31
|
+
import { compositionDeclaresFont } from "./composition-edit.js";
|
|
31
32
|
// ── Revision governor ────────────────────────────────────────────────────────
|
|
32
33
|
//
|
|
33
34
|
// `vidfarm qa` is feedback, not a gate — which is exactly what makes it a loop
|
|
@@ -646,16 +647,19 @@ export function qaCompositionHtml(html) {
|
|
|
646
647
|
const style = styleString(node);
|
|
647
648
|
const cssMatch = style.match(/(?:^|;)\s*font-family\s*:\s*([^;]+)/);
|
|
648
649
|
const family = primaryFamily(attr || (cssMatch ? cssMatch[1] : ""));
|
|
649
|
-
|
|
650
|
+
// A custom family is allowed when the composition SHIPS it — an @font-face
|
|
651
|
+
// or a Google Fonts import naming it. The defect this rule exists for is a
|
|
652
|
+
// family that no render can load, so a declared one is not a finding.
|
|
653
|
+
if (family && !FONT_REGIME.includes(family) && !compositionDeclaresFont(html, family)) {
|
|
650
654
|
const isWebDefault = WEB_DEFAULT_FONTS.includes(family) || family === "sans-serif" || family === "serif";
|
|
651
655
|
push({
|
|
652
656
|
rule: "font-regime",
|
|
653
657
|
severity: isWebDefault ? "error" : "warn",
|
|
654
658
|
message: isWebDefault
|
|
655
659
|
? `Text layer in "${family}" — a website body font. This alone makes a frame read as a screenshot of a web page.`
|
|
656
|
-
: `Text layer in "${family}",
|
|
660
|
+
: `Text layer in "${family}", which this composition never imports — it will silently fall back to a web-default sans at render.`,
|
|
657
661
|
where: label(node, "text layer"),
|
|
658
|
-
fix: `Use
|
|
662
|
+
fix: `Use a regime family — Montserrat (default), TikTok Sans, Abel, Source Code Pro, Yesteryear — e.g. \`vidfarm set-style <dir> --layer <key> --font-family Montserrat\`; each has its own reference card, e.g. https://vidfarm.cc/assets/fonts/caption-font-montserrat.png (all five: https://vidfarm.cc/fonts). Keeping "${family}" is allowed, but then you must IMPORT it in the composition (@font-face or a Google Fonts @import) — otherwise local renders coerce it to Montserrat.`
|
|
659
663
|
});
|
|
660
664
|
}
|
|
661
665
|
// Weight: the TikTok caption look is heavy. Light weights are a legitimate
|