@officexapp/vidfarm-devcli 0.21.52 → 0.21.53
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 +2 -2
- package/.agents/skills/vidfarm/harnesses/hooks.HARNESS.md +10 -3
- package/.agents/skills/vidfarm/harnesses/product-explainer.HARNESS.md +1 -1
- package/.agents/skills/vidfarm/harnesses/short-form.HARNESS.md +2 -0
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +44 -4
- package/.agents/skills/vidfarm/references/content-ideas.md +72 -1
- package/.agents/skills/vidfarm/references/core-workflows.md +2 -2
- package/.agents/skills/vidfarm/references/editor-workflows.md +32 -8
- package/SKILL.director.md +152 -17
- package/SKILL.md +6 -2
- package/clipper.md +22 -0
- package/dist/src/cli.js +98 -5
- package/dist/src/devcli/marketplace-gigs.js +2 -2
- package/dist/src/devcli/qa-check.js +1 -1
- package/dist/src/devcli/shared-folder.js +333 -44
- package/dist/src/devcli/skill-docs.js +2 -2
- package/experimental/meme-recaption.md +1756 -0
- package/marketplace.md +82 -8
- package/package.json +5 -1
- package/public/assets/file-directory-app.js +33 -33
|
@@ -188,7 +188,7 @@ Cost mode answers *how much money may I spend*. It does not answer *how much of
|
|
|
188
188
|
**How aggressively to run phase 2 depends on the cost mode.** This is the single biggest lever on both quality and spend:
|
|
189
189
|
|
|
190
190
|
- **`minimize` — the collection phase is the whole job. Budget most of your effort here.** You have no generation to fall back on, so the pool has to be good, and it can be: work the ladder in order and stop at the first rung that answers the beat.
|
|
191
|
-
1. `vidfarm public-raws --categories` then `--category <shelf>` — pre-tagged, free, already cut to short-form. Check this first, every time.
|
|
191
|
+
1. `vidfarm public-raws --categories` then `--category <shelf>` — pre-tagged, free, already cut to short-form. Check this first, every time. Need the **same face in several shots**? Read the `actor_<uuid>` token off a card's summary, then `vidfarm public-raws --query actor_<uuid>` returns that person's other clips (`references/assets-and-sourcing.md` → *Same face across clips*).
|
|
192
192
|
2. `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|video>` (keyless via openverse/iconify) and `vidfarm iconscout "<meaning>" --free` for icons, stickers, illustrations and props.
|
|
193
193
|
3. `vidfarm raws search "<meaning>"` — what the director already owns from previous videos.
|
|
194
194
|
4. **Browser control** — `vidfarm browse videos "<the shot>"` drives the user's own Chrome for $0 (`references/browser-harness.md`). This is the free substitute for the paid `video-search` / `image-search` / `news-search` / `download-video` primitives. **Set it up rather than declaring the shot unavailable.**
|
|
@@ -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 — **exactly five allowed families and no others**: 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. **The regime on one page: <https://vidfarm.cc/fonts>** — all five rendered as real captions, the four legal backgrounds, and copy-paste commands. 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.
|
|
@@ -43,8 +43,9 @@ The platform's first test batch is a few hundred essentially random viewers, of
|
|
|
43
43
|
Three constraints:
|
|
44
44
|
|
|
45
45
|
1. **Complete clause — subject plus verb.** Not a noun phrase, not a bare number, not a label. This one rule fixes most failures, because a clause forces context in.
|
|
46
|
-
2. **
|
|
47
|
-
3. **
|
|
46
|
+
2. **Somebody is in it — "I" or "you".** Name the person the clause is about. `The shelf is not a library` floats: a definite article with nobody attached describes a shelf in the abstract, and the viewer is not in the frame. `Your shelf is not a library` puts them in it. This is why every passing line in the table below is first person — the rule was always there, unwritten. Third-person distance (`people`, `the average person`, `the shelf`) is the quiet version of the label failure.
|
|
47
|
+
3. **No jargon.** A domain term in chunk 1 kills the cold start even inside a valid clause. Jargon belongs in chunk 2+, where it *qualifies* instead of excluding.
|
|
48
|
+
4. **The cold-viewer test.** Would a random person with no interest in the category understand what situation is described? Not *would they care* — *would they understand*.
|
|
48
49
|
|
|
49
50
|
**The cheapest predictor: situations, not labels.** A situation is a thing that happened, with a subject and an outcome — *someone deleted my message · she refused my money · they stopped replying · tracking said delivered*. A stranger parses all of it instantly, because humans are built to parse events. A label is a concept with a name — *anonymity · distribution · platform risk · seeding* — and it carries meaning only for someone who already holds the category, which is exactly the person the cold-start batch does not contain. If your opening names a concept rather than an event, assume it fails until proven otherwise.
|
|
50
51
|
|
|
@@ -55,6 +56,11 @@ Three constraints:
|
|
|
55
56
|
| `ANONYMITY` | `I'VE NEVER SHOWN MY FACE` |
|
|
56
57
|
| `$50,000 IN SALES` | `THEY WANTED $50,000 FIRST` |
|
|
57
58
|
| `TWO HOURS A WEEK` | `THIS TAKES TWO HOURS A WEEK` |
|
|
59
|
+
| `FOURTH ATTEMPT AT CHAPTER ONE` | `I ALWAYS GIVE UP READING AT CHAPTER FOUR. WHY?` |
|
|
60
|
+
| `THE SHELF IS NOT A LIBRARY` | `YOUR SHELF IS NOT A LIBRARY` |
|
|
61
|
+
| `KETTLE: 3 MIN. LIFT: 30 SEC.` | `YOU WASTE HALF AN HOUR A DAY IN THREE MINUTE GAPS` |
|
|
62
|
+
|
|
63
|
+
The last three are the failure a **generator** produces, not a person: told to be terse and to carry a number, a model drops the verb and the subject and hands back a caption. If you are bulk-generating hooks, cap words *after* the clause rule, never before it, and pass these three rewrites in as worked examples.
|
|
58
64
|
|
|
59
65
|
**Banned chunk-1 openers:** *So I was thinking · There's a reason · Here's the thing · A lot of people · If you're a… · Let me tell you* — any throat-clearing. The first words are the most expensive real estate you own.
|
|
60
66
|
|
|
@@ -102,6 +108,7 @@ Read these before you loop — they are the failure modes that only appear at vo
|
|
|
102
108
|
## Pre-flight checklist
|
|
103
109
|
|
|
104
110
|
- [ ] Chunk 1 is a complete clause with a subject and a verb
|
|
111
|
+
- [ ] Somebody is in it — "I" or "you", not "people" or a bare definite article
|
|
105
112
|
- [ ] Chunk 1 names a **situation**, not a label or concept
|
|
106
113
|
- [ ] Chunk 1 contains no domain jargon
|
|
107
114
|
- [ ] A stranger with no interest in the category understands the situation
|
|
@@ -114,4 +121,4 @@ Read these before you loop — they are the failure modes that only appear at vo
|
|
|
114
121
|
- [ ] It matches what the next screen actually says
|
|
115
122
|
- [ ] It is a genuinely different video from every other hook in the batch
|
|
116
123
|
|
|
117
|
-
|
|
124
|
+
Thirteen checks. Two carry most of the weight and are the two almost everyone skips: **situation, not label** predicts cold-start survival before you write a word, and **unguessable** is the only item a hook can fail while passing every other item — which is why it gets shipped.
|
|
@@ -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
|
|
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>), 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; it is not fine for the caption layer. 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,6 +114,8 @@ 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 exactly **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. The whole regime, each family rendered as a real caption, plus the four legal text backgrounds: **<https://vidfarm.cc/fonts>**. `font_regime: required` in the checks block above is this rule.
|
|
118
|
+
|
|
117
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:
|
|
118
120
|
|
|
119
121
|
| Background behind the caption band | Treatment |
|
|
@@ -142,6 +142,29 @@ What each shelf is for, in practice: **Scroll Stoppers** are half-second pattern
|
|
|
142
142
|
|
|
143
143
|
A bookmarked public raw lands in the director's own `/raws/public/*` folder, after which it behaves like any other library raw (`set_layer_media` / `vidfarm set-media` to drop it on the timeline).
|
|
144
144
|
|
|
145
|
+
### Same face across clips — the `actor_<uuid>` tag
|
|
146
|
+
|
|
147
|
+
A shelf is dozens of clips of a much smaller number of creators. When a video needs **the same person in more than one shot** — a hook, a reaction cutaway and a payoff that must read as one spokesperson — the shelf alone can't tell you which clips share a face. The `actor_<uuid>` tag can.
|
|
148
|
+
|
|
149
|
+
Every tagged public raw carries one actor id in its `summary` (and in its tags), so the id is **readable off any card you like** and searchable as a plain query token:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
vidfarm public-raws --category ugc-reaction --limit 60 # 1. browse the shelf, pick a face
|
|
153
|
+
# 2. read actor_<uuid> off that card's summary
|
|
154
|
+
vidfarm public-raws --query actor_5d9f0c31-… # 3. every other clip of that same person
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
REST twin: `GET /api/v1/public-raws?q=actor_<uuid>`. It is an ordinary keyword search, so it composes: `?category=ugc-reaction&q=actor_<uuid>`.
|
|
158
|
+
|
|
159
|
+
Rules that matter:
|
|
160
|
+
|
|
161
|
+
- **The id is opaque and permanent — never guess or invent one.** Read it off a card; a made-up id matches nothing.
|
|
162
|
+
- **One id per person, not per clip.** A card with no `actor_` token is simply not tagged yet (older raws, or a raw with no person on camera) — treat "no tag" as unknown, not as "a different person".
|
|
163
|
+
- **Use it for continuity, not casting from scratch.** Pick the face first (browse the shelf, judge the frame), then pull that face's other takes.
|
|
164
|
+
- Same trick when a director asks *"more of her"* / *"the guy from the last video"* — the actor id off the raw already in the composition is the answer.
|
|
165
|
+
|
|
166
|
+
The `ugc-reaction` shelf is tagged: 177 raws across 53 people, and **almost every clip on it has siblings** — 49 of those 53 actors carry 2+ clips, several carry 5-7. Assume a face you like has more takes, and check.
|
|
167
|
+
|
|
145
168
|
## Raws (long-form → short-form raws)
|
|
146
169
|
|
|
147
170
|
Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/X URL, or an upload) into a library of tagged, searchable **raws**. **Don't have a source URL yet? `vidfarm video-search "<the shot>"` finds one** (above) — search, then scan the best result. This is the `/raws` surface — browse it at `https://vidfarm.cc/library/raws` (the Library page's "Approved / Raws" tabs).
|
|
@@ -303,6 +326,15 @@ On `/tools/image` these are one-tap preset chips under the prompt box; in chat o
|
|
|
303
326
|
- **move** — `POST /api/v1/user/me/directory/move` `{ path, to, file_id? }` · `vidfarm directory move <path> <to-folder> [--file-id <id>]`. Relocates a FILE into `to` (with `file_id`) or nests a whole FOLDER under `to`. **Same root only** (`/files`·`/temp`·`/raws`·`/approved`); metadata-only, so S3 objects are untouched.
|
|
304
327
|
- **copy** — `POST /api/v1/user/me/directory/copy` `{ path, to?, file_id?, new_name? }` · `vidfarm directory copy <path> [<to-folder>] [--file-id <id>] [--as <name>]`. Duplicates a file/folder sharing the same underlying S3 object (cheap, no re-upload). Same root only, `/files`·`/temp`·`/raws` (not `/approved` — a ready post is a single publishable unit; not `/projects` — read-only). Omit `to` to duplicate in place.
|
|
305
328
|
|
|
329
|
+
**Vector notes — what makes a file findable by MEANING.** Every file in `/files` and `/raws`, and every **folder** in any annotatable root, carries one plain-text *vector note*: what the thing actually is. The note is embedded, so `directory/search` finds it from any phrasing — `IMG_4821.mp4` is worth nothing to a search, "founder holding the product in a kitchen, warm daylight" is worth everything. Folders are searched the same way, so a well-noted folder is itself a hit.
|
|
330
|
+
|
|
331
|
+
- **read** — `GET /api/v1/user/me/directory/note?path=<path>[&id=<file_id>]` → `{ note, can_edit }`.
|
|
332
|
+
- **write** — `PUT /api/v1/user/me/directory/note` `{ path, id?, note }` → `{ note, embedded }`. Omit `id` to annotate the FOLDER at `path`; pass the file's backend id to annotate a file. An empty `note` clears it. `embedded: false` means it saved but only keyword search will find it — the account has no AI key for embeddings.
|
|
333
|
+
- **through a share link** — same two routes at `/api/v1/share/:token/directory/note`. `read` links never write. **`upload` links may FILL an empty note but never overwrite one**, so a contributor says what they dropped without rewriting anyone else's meaning. `edit` links and the owner may always write.
|
|
334
|
+
- **automatic** — on upload, when the account has its own AI key saved (Gemini / OpenAI / OpenRouter), one vision call on the file's poster frame writes the note for you. Skipped for files over **100 MB** and when there is no key — then a human types it. The model runs on the customer's own key; only the Lambda compute is wallet-billed, at 2x.
|
|
335
|
+
|
|
336
|
+
In the explorer each row shows a note badge: `✎ note` (missing) or `✓ note` (written). Click it to read or write the note.
|
|
337
|
+
|
|
306
338
|
**Sharing a folder publicly (crowdsourcing + agent-readable libraries).** Any folder in **any** root can carry share tokens — one folder, as many links as you want, each with its own mode:
|
|
307
339
|
|
|
308
340
|
- `read` — browse **and vector-search** the folder. Nothing else.
|
|
@@ -313,6 +345,8 @@ Minting or editing a link is **paid** (unlike the rest of the directory routes,
|
|
|
313
345
|
|
|
314
346
|
A token is scoped to **exactly one subfolder and everything under it** — never a sibling, never a parent, so a leaked link cannot walk the rest of the drive. The shared base folder itself can never be renamed or deleted through its own token. Owner routes: `GET|POST /api/v1/user/me/directory/shares`, `PATCH|DELETE …/shares/<token>` (`{ path, mode, label }`; `disabled: true` cuts a link off instantly and is reversible). Public routes, no auth: `GET /api/v1/share/:token/directory` (`?path=`), `POST /api/v1/share/:token/directory/search` `{ query, path?, mode? }`, plus `…/directory/folders`, `…/directory/rename`, `…/directory/delete`, `…/attachments/upload`. The human page is `/directory/preview/<token>/<path…>`.
|
|
315
347
|
|
|
348
|
+
**Which roots accept a share upload:** `/files`, `/temp` and `/raws`. A `/raws` share is the **contributor drop-box** — the bytes land as a `role: "source"` raw inside the shared folder (video/audio only), exactly as `POST /raws/import` writes one. `/projects` (fork storage) and `/approved` (published renders) stay browse + search.
|
|
349
|
+
|
|
316
350
|
**Uploading over ~6 MB through a share link:** `…/attachments/upload` is a multipart POST through the API Lambda, whose request body caps at about 6 MB — a bigger file answers **413** before the handler runs. Use the two-step presigned path instead (the share page's own uploader does): `POST /api/v1/share/:token/attachments/presign` `{ file_name, content_type?, size_bytes?, folder_path? }` → `{ transport: "presigned", file_id, storage_key, folder_path, upload: { method, url, headers } }`; PUT the raw bytes to `upload.url` with those headers; then `POST /api/v1/share/:token/attachments/finalize` `{ file_id, file_name, content_type, size_bytes, storage_key, folder_path }` to record the file. A server with no presigned transport answers `transport: "server"` and you post the multipart form. Ceiling is the owner's own: 200 MB.
|
|
317
351
|
|
|
318
352
|
In the UI: the file explorer's folder kebab → **Share…**, the `⤴ Share` toolbar button, and the folder kebabs on `/library` (**Raws** and **Approved** tabs). `⤴ Manage Shares` lists every token you own.
|
|
@@ -321,14 +355,20 @@ In the UI: the file explorer's folder kebab → **Share…**, the `⤴ Share` to
|
|
|
321
355
|
|
|
322
356
|
```bash
|
|
323
357
|
vidfarm shared info "$LINK" # folder, mode, what you may do
|
|
324
|
-
vidfarm shared ls "$LINK" [subfolder]
|
|
358
|
+
vidfarm shared ls "$LINK" [subfolder] --tree # browse (--tree walks every subfolder)
|
|
325
359
|
vidfarm shared search "$LINK" "founder talking head, no captions"
|
|
326
|
-
vidfarm shared
|
|
360
|
+
vidfarm shared grab "$LINK" "kitchen b-roll, no faces" --limit 3 --out ./assets
|
|
361
|
+
vidfarm shared note "$LINK" hero.mp4 # what IS this? (--set "…" to write)
|
|
362
|
+
vidfarm shared get "$LINK" hero.mp4 --out ./work # one file (--all [--tree] for the folder)
|
|
327
363
|
vidfarm shared mkdir "$LINK" task-014-yvette # your own subfolder (upload/edit)
|
|
328
|
-
vidfarm shared put "$LINK" final.mp4 --subfolder task-014-yvette
|
|
364
|
+
vidfarm shared put "$LINK" final.mp4 --subfolder task-014-yvette --note "Final cut, 9:16"
|
|
329
365
|
```
|
|
330
366
|
|
|
331
|
-
|
|
367
|
+
**`shared grab` is the command to reach for.** It runs the vector search and downloads the hits in one step, so an agent goes from "I need kitchen b-roll" to files on disk without paging folders. `search` and `grab` match on each item's **vector note**, and folders are hits in their own right — so read notes before you pick (`shared ls --tree` prints them, `✓` = described, `✎` = not).
|
|
368
|
+
|
|
369
|
+
**Write a note when you upload** (`shared put --note`, or `shared note <file> --set`). An `upload` link may FILL an empty note but never overwrite one; an `edit` link and the owner may always write. That is what makes your deliverable findable by the client's own agent later.
|
|
370
|
+
|
|
371
|
+
`shared put` runs presign → PUT → finalize, so it is not bound by the ~6 MB multipart ceiling; it falls back to multipart automatically when the server has no presigned transport. A `get`/`grab` whose stored view URL points at a host that no longer answers is retried against the share link's own host.
|
|
332
372
|
|
|
333
373
|
**The crowdsourcing pattern (DollarPlatoon custom-request vending machine).** Two links per campaign, minted once and reused across every task:
|
|
334
374
|
|
|
@@ -8,11 +8,13 @@ Read this when a director says **"give me content ideas"**, "what should I post"
|
|
|
8
8
|
|
|
9
9
|
| Axis | Bank | Question it answers |
|
|
10
10
|
|---|---|---|
|
|
11
|
-
| **Subject** | the director's own pool of adjacent topics | *What is this one about
|
|
11
|
+
| **Subject** | the director's own pool of adjacent topics, each at a **distance** | *What is this one about?* (settling the dinner argument · eating what you actually craved · **and further out**: why the best-reviewed place is rarely the best meal) |
|
|
12
12
|
| **Frame** | 50 content frames | *What is the video the story of?* (`the rise of`, `then vs now`, `one decision that changed everything`) |
|
|
13
13
|
| **Awareness** | 5 stages | *What does the viewer already know?* (unaware → problem-aware → solution-aware → product-aware → most-aware) |
|
|
14
14
|
| **Angle** | 44 problem angles | *From which side do we talk about the problem?* (the hidden cost, the myth, the confession, the honest downside) |
|
|
15
15
|
|
|
16
|
+
The subject axis carries a **distance**: how far from the offer the video is allowed to sit. Distance is what produces genuinely different videos rather than differently-shaped ones, and the **turn back to the offer** is what keeps distance from becoming irrelevance — see *Distance* below. Freeze the subject at distance zero and no amount of frames or angles will save the list.
|
|
17
|
+
|
|
16
18
|
A **frame** is a reusable shape for a video's subject — not a hook line, not a script. You pour the director's topic into it: `the rise of` + `dropshipping supplements` → *"The rise of the supplement dropship store"*. An **angle** decides which face of the problem that video shows; the same frame at two angles is two genuinely different videos. An **awareness stage** decides who it is allowed to be for — the same idea aimed at a stranger and at a buyer needs different first ten seconds and a different ask.
|
|
17
19
|
|
|
18
20
|
**The grid is where the volume comes from.** 50 frames × 44 angles × 5 stages, over a pool of adjacent subjects, is not a number to take literally, but it is why "I need 30 videos this month" is an easy ask, not a hard one. Vary **two** axes across a set and it never reads as repeats. Vary only one and it does — and the axis most often left frozen is the subject, because the offer line is right there and it is easy to paste.
|
|
@@ -65,6 +67,72 @@ Now the same four frames are four videos:
|
|
|
65
67
|
|
|
66
68
|
**One rule to check the finished list.** Read the titles with the frames covered up. If what is left is the same sentence 20 times, you produced one idea, not 20.
|
|
67
69
|
|
|
70
|
+
### Distance — going further than the niche, safely
|
|
71
|
+
|
|
72
|
+
The pool above stays inside the niche, and the ❌ above bans a different business outright. **That ban is correct as long as nothing carries the video home.** Write the turn back to the offer first, and you can go considerably further out — which is where the strongest ideas live, because the viewer did not come to the feed looking for your category.
|
|
73
|
+
|
|
74
|
+
> "Why smart people make less money than dumb people… because nobody hands them the lessons — Prism gives you a lifetime of them from the best biographies, five minutes each."
|
|
75
|
+
|
|
76
|
+
That is a video about status and income, for a book summary app. It is not off topic, because the last clause is written before the video is.
|
|
77
|
+
|
|
78
|
+
**The dial.** Give every subject a distance, and quota the spread — do not ask for "variety", which returns everything at the mode:
|
|
79
|
+
|
|
80
|
+
| d | What it is | Share of 12 |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| 1 | Next door. The viewer sees it is adjacent to the category. | 5 |
|
|
83
|
+
| 2 | The wider human subject. The category is one example inside it. | 5 |
|
|
84
|
+
| 3 | Far. The video never names the category until the last line. | 2 |
|
|
85
|
+
|
|
86
|
+
Far-heavy was tried and rejected. At 2 / 4 / 6 you get a video about IKEA's wordless instructions for a book app, and a viewer needs two steps of explaining before anything sells.
|
|
87
|
+
|
|
88
|
+
**The one step rule.** The link from the subject to the offer must be ONE step. If a viewer needs two steps to see how they connect, the subject is too far — take a nearer one. A viewer will not do the work.
|
|
89
|
+
|
|
90
|
+
**The turn — the field that makes distance safe.** Every idea carries the line the video says when it stops being about the subject and reaches the offer:
|
|
91
|
+
|
|
92
|
+
- It NAMES the client, by name.
|
|
93
|
+
- It states the concrete thing the viewer GETS.
|
|
94
|
+
- It follows on in ONE move, with no new argument.
|
|
95
|
+
- 12 to 25 words, sayable out loud.
|
|
96
|
+
|
|
97
|
+
| | |
|
|
98
|
+
|---|---|
|
|
99
|
+
| ✅ | "Churchill demanded one page. Prism applies the same discipline to whole books — habits, business, history, five minutes each." |
|
|
100
|
+
| ❌ | "Nobody gives anything an hour any more." |
|
|
101
|
+
|
|
102
|
+
The second names nobody and gives nothing. It is a theme, not a turn. **If you cannot write the turn, drop the subject** — do not soften it, and do not pad the count. That discard rule is also what protects a thin client: some offers have a deep cultural hinterland and some do not, and a short honest pool beats a long strained one.
|
|
103
|
+
|
|
104
|
+
This is Gate 3 of the `hooks` harness — offer adjacency — applied one level up, at the subject rather than the hook.
|
|
105
|
+
|
|
106
|
+
**The proof type — or the whole list becomes trivia.** Demand concrete detail and every idea arrives as "in 1978, a man…". That is the same flatness in a new costume. Declare what kind of concrete thing carries each subject, and spread across all five:
|
|
107
|
+
|
|
108
|
+
| Proof type | What it is |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `historical` | a dated event, a person from the past, an old record |
|
|
111
|
+
| `present_number` | a number true today: a price, a count, a duration, a share |
|
|
112
|
+
| `ranking` | an order: best to worst, a tier list, a bracket, a league table |
|
|
113
|
+
| `named_person` | someone the audience can look up, alive now |
|
|
114
|
+
| `everyday_observation` | something the viewer can check in their own life, no source |
|
|
115
|
+
|
|
116
|
+
Use each at least once; use none more than three times.
|
|
117
|
+
|
|
118
|
+
**Flag what needs checking.** A distant subject means dates, numbers and named people, and a video that states a false fact is the *client's* problem once it is posted. Mark every idea that carries one, and put the instruction on the brief: *"Confirm this before you film. If you cannot confirm it, film the story version and drop the claim."*
|
|
119
|
+
|
|
120
|
+
### Prompting a model for range
|
|
121
|
+
|
|
122
|
+
The clauses below are what actually move the output. Copy the wording, not the intent.
|
|
123
|
+
|
|
124
|
+
1. **Define the unit, and say what it is not.** *"It is not the product. It is not the problem. It is not the product's own category."* A negative definition beats three adjectives.
|
|
125
|
+
2. **Make concreteness a required field, not an adjective.** *"Every item must carry `objects`: named people, years, numbers, records, institutions. An item with only abstract nouns is rejected."* A required field is a constraint; an instruction in prose is a suggestion.
|
|
126
|
+
3. **Quota the axis you want spread on.** *"5 at distance 1, 5 at distance 2, 2 at distance 3."* Never "spread them out".
|
|
127
|
+
4. **Withhold the product until last.** Order the prompt audience → what they already care about → offer. A prompt that opens with "this is a book summary app" returns fifty videos about book summaries whatever the rules underneath say. Free, and it changes the whole answer.
|
|
128
|
+
5. **Ban the obvious answer by name.** *"A far title may not use these words: book, summary, audiobook."* The model's first three ideas are always the banned ones. Read the ban list off the offer, per client.
|
|
129
|
+
6. **Require the return path, then push distance hard.** The turn, plus the discard rule.
|
|
130
|
+
7. **Pass the titles already written, and forbid a repeat.** *"Do not write a video that would be the SAME VIDEO as any of them. A different frame on the same subject is the same video."* The only dedup that works across calls.
|
|
131
|
+
|
|
132
|
+
**Three that kill range:** one call per item — N calls are N samples from one mode, so generate a SET in one call · "be creative" — zero constraint, and the model already believes it is · an example with no anti-example — always show the flat version beside the good one.
|
|
133
|
+
|
|
134
|
+
**Count the result in code, not in the prompt.** The model proposes; a quota checked afterwards is the only one that holds. Report the misses by name and let the director decide — a counter that silently repairs is a counter nobody trusts.
|
|
135
|
+
|
|
68
136
|
### The 50 frames
|
|
69
137
|
|
|
70
138
|
**Arc & subject frames — what the video is the story of**
|
|
@@ -333,3 +401,6 @@ The frame also suggests how to build it, which saves a planning round:
|
|
|
333
401
|
- **Do not treat the frame as the hook.** "The history of X" spoken flat at `start:0` is a banned opener shape — the hook still has to name a situation. `references/hooks-and-virality.md`.
|
|
334
402
|
- **Do not ship a set that sits at one awareness stage.** It is the single most common defect in a month of posts, and neither the frame nor the angle can repair it. Label the stage on every idea so the imbalance is visible before production, not after.
|
|
335
403
|
- **Do not sell at stage 1 or 2.** The ask belongs to the stage. A link in a stage-1 video costs the reach that made the video worth making.
|
|
404
|
+
- **Do not hand over a distant idea with no turn written.** A video about Napoleon's field library, for a book app, is a great video and a dead one until somebody writes the line that reaches the offer. Write the turn with the idea, in the same breath, or drop the idea.
|
|
405
|
+
- **Do not let the whole list become dated trivia.** "Be concrete" pushes a model straight into "in 1978, a man…". Fifty history videos is one idea fifty times, exactly like the frozen subject. Spread the proof types.
|
|
406
|
+
- **Do not state a date, a number or a record without flagging it for checking.** The editor films what the brief says. A wrong figure is the director's problem the moment it posts, not the writer's.
|
|
@@ -370,7 +370,7 @@ curl -s -X POST -H "vidfarm-api-key: $VIDFARM_API_KEY" -H "content-type: applica
|
|
|
370
370
|
curl -s -X POST -H "x-api-key: $DP" "https://dollarplatoon.com/api/gigs/$GIG/rollups"
|
|
371
371
|
```
|
|
372
372
|
|
|
373
|
-
`paid_out_at` on the proof — not `approved` — is the field that means the money moved. In the web UI this is the **Release N Payouts** button on /marketplace/
|
|
373
|
+
`paid_out_at` on the proof — not `approved` — is the field that means the money moved. In the web UI this is the **Release N Payouts** button on /marketplace/inbound/mailbox, which appears only when a machine has approved-but-unpaid proofs.
|
|
374
374
|
|
|
375
375
|
**4 — Add a new task.** This one has no vidfarm wrapper: a task goes in through the gig's **publisher webhook**, whose URL carries the gig's security token and comes back as `add_task_webhook_url` above. No key header — the token in the URL is the auth.
|
|
376
376
|
|
|
@@ -403,7 +403,7 @@ curl -s -X POST -H "x-api-key: $DP" -H "content-type: application/json" \
|
|
|
403
403
|
-d '{"max_uses":null,"label":"vidfarm"}' "https://dollarplatoon.com/api/gigs/$GIG/invites"
|
|
404
404
|
```
|
|
405
405
|
|
|
406
|
-
If you ring the bell directly against Dollar Platoon (`POST /feeds/$FEED/notifications`), resolve that invite FIRST and send it as `destination_url`. The feed registry's `invite_url` is tokened too and works as a fallback. In the web UI, **Ring Bell** does this for you, shows the exact link it published, and **Copy invite link** on /marketplace/
|
|
406
|
+
If you ring the bell directly against Dollar Platoon (`POST /feeds/$FEED/notifications`), resolve that invite FIRST and send it as `destination_url`. The feed registry's `invite_url` is tokened too and works as a fallback. In the web UI, **Ring Bell** does this for you, shows the exact link it published, and **Copy invite link** on /marketplace/inbound/swipe hands you the same link to share by hand.
|
|
407
407
|
|
|
408
408
|
Ring it when something actually **changed** — a new batch, a new price, a format you now want. An empty bell teaches agents to stop opening yours.
|
|
409
409
|
|
|
@@ -42,23 +42,36 @@ For any replication, offer the director **two harnesses** and recommend (A) unle
|
|
|
42
42
|
|
|
43
43
|
### Writing a meme recaption: aim at a pain or a win
|
|
44
44
|
|
|
45
|
+
> ## → Building a meme video from scratch? Use the harness.
|
|
46
|
+
> **`https://vidfarm.cc/experimental/meme-recaption.md`** is the full standalone format: casting a
|
|
47
|
+
> clip off the public-raws shelf for the caption's **verb** and its **intensity**, keying a
|
|
48
|
+
> MemeScreens raw onto a background world, caption placement, naming the offer, rights tiers, and a
|
|
49
|
+
> render-level QA gate that exits non-zero. It runs at $0 and it supersedes this section for any
|
|
50
|
+
> from-scratch build.
|
|
51
|
+
>
|
|
52
|
+
> **This section is the short version**, for when you already have the clip on a timeline and are
|
|
53
|
+
> only rewriting the line (`set_captions` / `set_layer_text`). The writing craft below is the same
|
|
54
|
+
> craft the harness uses — the harness just adds everything around it.
|
|
55
|
+
|
|
45
56
|
Recaptioning is the cheapest brush, but the *line* is the whole product — the clip is just the punchline delivery system. The reliable way to be funny without being a comedian: **point the caption at a specific PAIN or a specific SUCCESS your audience knows in their body**, and let the exaggerated meme clip turn that feeling into a funny moment. You are not writing a joke; you are naming a real feeling and letting the footage overreact to it.
|
|
46
57
|
|
|
47
|
-
- **Pain** — the annoying, tedious, humiliating, or absurd moment in the audience's day. The meme clip supplies the reaction they wish they could have. *"me opening the bank app after a weekend of 'small' purchases"*.
|
|
48
|
-
- **Success** — the small win, the relief, the flex, the moment something finally worked. The clip supplies the celebration. *"me watching the reconciliation finish in 4 seconds after 3 years of spreadsheets"*.
|
|
58
|
+
- **Pain** — the annoying, tedious, humiliating, or absurd moment in the audience's day. The meme clip supplies the reaction they wish they could have. *"me opening the bank app after a weekend of 'small' purchases"*. Ends on the problem; does not resolve.
|
|
59
|
+
- **Success** — the small win, the relief, the flex, the moment something finally worked. The clip supplies the celebration. *"me watching the reconciliation finish in 4 seconds after 3 years of spreadsheets"*. Resolves by design — the relief IS the joke.
|
|
60
|
+
|
|
61
|
+
**Decide the pole before you write.** It changes whether the caption may resolve, and it is the most useful thing to A/B: run both across the same clip and see whether the market wants to be *seen* or wants *relief*.
|
|
49
62
|
|
|
50
63
|
How to work it:
|
|
51
64
|
1. **Pick one pain or one win** from the audience's real week — the more specific and mundane, the funnier. Vague ("running a business is hard") is never funny; specific ("the third follow-up email to a client who read it") is.
|
|
52
|
-
2. **Match the intensity of the clip to the size of the feeling.** The comedy is the mismatch: a tiny pain + a wildly over-the-top reaction clip, or a small win + a stadium-scale celebration. Choose the pain/win that the existing clip's energy already fits, rather than fighting the footage.
|
|
53
|
-
3. **Keep the meme's grammar** — `me when…` / `
|
|
54
|
-
4. **
|
|
55
|
-
5. **The line must make sense to a stranger to the offer — the cold-viewer test.**
|
|
65
|
+
2. **Match the intensity of the clip to the size of the feeling.** The comedy is the mismatch: a tiny pain + a wildly over-the-top reaction clip, or a small win + a stadium-scale celebration. Choose the pain/win that the existing clip's energy already fits, rather than fighting the footage — the clip is the part you cannot edit.
|
|
66
|
+
3. **Keep the meme's grammar** — `me when…` / `my clients when…` / `us after…` / `<thing> watching me…`. Swap the subject to whoever owns the feeling (the customer, the founder, the team), keep the frame.
|
|
67
|
+
4. **Naming the product in the line is fine — as a subject, not as the answer.** On a **pain** caption the name may stand in the setup as a witness (*"AutoBooks watching me photograph a 41st receipt"*) but never in the last clause. On a **win** caption it belongs in the last clause, because the relief is the joke (*"me watching AutoBooks reconcile 3 months of receipts while I do nothing"*). What is never allowed on either pole is a **CTA** (*"Save 10 hours a month with AutoBooks 🚀"*) or a **feature** (*"after the auto-reconcile v2 sync clears"*). If naming it makes the line clumsy, leave it out of the sentence and put the offer in a small dimmed handle under the caption instead — both tiers, and the checks that enforce them, are in the harness.
|
|
68
|
+
5. **The line must make sense to a stranger to the offer — the cold-viewer test.** Read it as a person who works in the niche but has NEVER heard of the product, the brand, the feature names, or the category jargon. If they cannot get the joke in one read, the caption failed, no matter how clever it is to the team.
|
|
56
69
|
- **Write about the niche, not the feature.** A meme about a product feature needs product context to land, so it lands only on people who already bought. A meme about the niche's lived experience lands on everybody in the niche — which is the whole point of a meme ad.
|
|
57
70
|
- BAD (needs product context): *"me after the auto-reconcile v2 sync finally clears"* — "auto-reconcile v2" means nothing to a cold viewer. GOOD: *"me at 1am matching bank lines to receipts by hand"*.
|
|
58
71
|
- **Test:** cover the brand and the feature names. If the caption still reads as a true, funny moment from the audience's week, keep it. If it turns into nonsense, rewrite it around the feeling instead of the feature.
|
|
59
72
|
- **No inside jokes, no invented vocabulary, no setups that only the founder's demo explains.** Slang the niche already uses is fine; slang only the product uses is not.
|
|
60
73
|
6. **One short punchy line**, matching the original's brevity, tone, and comedic timing. If the original was two stacked lines (setup / payoff), keep two — pain on top, reaction beneath.
|
|
61
|
-
7. **Batch it.** One meme clip + a list of ten pains and ten wins is
|
|
74
|
+
7. **Batch it.** One meme clip + a list of ten pains and ten wins is twenty videos. Enumerate the audience's pains and wins ONCE, then recaption the same clip (or a small set of clips) across the whole list — this is the highest-output, lowest-cost loop in Vidfarm. The list is the asset; the clips are interchangeable.
|
|
62
75
|
|
|
63
76
|
If the user insists on explicit ad copy in the recaption, say once that it flattens the joke, then give them the pain/win version alongside what they asked for.
|
|
64
77
|
|
|
@@ -603,7 +616,18 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
|
|
|
603
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.
|
|
604
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.
|
|
605
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.
|
|
606
|
-
- **Font → the composition regime.**
|
|
619
|
+
- **Font → the composition regime. Five families, no others.** 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>** (specimen image alone: `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
|
+
| Family | Weights it really has | Use it for |
|
|
622
|
+
|---|---|---|
|
|
623
|
+
| **TikTok Sans** | 400 / 600 / 700 / 800 / 900 | The native TikTok caption look. **The safe default when unsure.** |
|
|
624
|
+
| **Montserrat** | 600 / 700 / 800 / 900 | Geometric bold display. Hooks, hard statements, the Hormozi caption. **The bold default.** |
|
|
625
|
+
| **Abel** | 400 only | Condensed headline / newsletter vibe. A long line that must stay on one row. |
|
|
626
|
+
| **Source Code Pro** | 700 only | Code / terminal beats only. Never a whole video. |
|
|
627
|
+
| **Yesteryear** | 400 only | Cursive script. **One accent line** (a quote) — never a caption track; it is unreadable at cue size. |
|
|
628
|
+
| ~~Georgia~~ | — | **Decompose-only. Never 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 and `normalizeTikTokCaptionLayout` coerces it to Montserrat on every local render. Rebuild an editorial look in Abel or Montserrat instead. |
|
|
629
|
+
|
|
630
|
+
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 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. Matching a client brand font is fine for a *wordmark image*; it is never fine for the caption layer. 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. `vidfarm qa-check` flags off-regime families (`font-regime` rule).
|
|
607
631
|
- **Background → one of exactly four valid treatments.** Any text you place uses one of these and nothing else:
|
|
608
632
|
|
|
609
633
|
| # | Treatment | How to set it | When |
|