@officexapp/vidfarm-devcli 0.21.58 → 0.21.60
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 +3 -3
- package/.agents/skills/vidfarm/recipes/retheme-template.md +6 -5
- package/.agents/skills/vidfarm/references/content-ideas.md +97 -14
- package/.agents/skills/vidfarm/references/core-workflows.md +4 -0
- package/.agents/skills/vidfarm/references/editor-workflows.md +25 -0
- package/SKILL.director.md +135 -22
- package/SKILL.md +9 -2
- package/clipper.md +7 -3
- package/dist/src/cli.js +201 -16
- package/dist/src/devcli/clipper-panel.js +1828 -0
- package/dist/src/devcli/clipper-run.js +643 -126
- package/dist/src/devcli/marketplace-gigs.js +307 -4
- package/dist/src/devcli/proof-verify.js +465 -0
- package/dist/src/devcli/skill-docs.js +14 -0
- package/experimental/flash-harness.md +142 -18
- package/experimental/meme-recaption.md +1044 -79
- package/experimental/sticker-slideshow-tips.md +511 -86
- package/marketplace.md +427 -12
- package/package.json +6 -1
- package/update.md +21 -1
|
@@ -28,6 +28,15 @@ checks:
|
|
|
28
28
|
- learn more
|
|
29
29
|
# the bait lives in the POST caption, never on the meme:
|
|
30
30
|
- comment below
|
|
31
|
+
# Rule 10 — context debt. Each of these is an admission that the caption needs
|
|
32
|
+
# something the viewer does not have on the frame. See "The four contexts a
|
|
33
|
+
# viewer does not have". Banned here as literal substrings so gate 1 catches
|
|
34
|
+
# them before a render exists:
|
|
35
|
+
- iykyk
|
|
36
|
+
- if you know you know
|
|
37
|
+
- you already know
|
|
38
|
+
- as promised
|
|
39
|
+
- part 2
|
|
31
40
|
---
|
|
32
41
|
|
|
33
42
|
# Meme Recaption
|
|
@@ -45,7 +54,7 @@ Two ingredients, no more:
|
|
|
45
54
|
|
|
46
55
|
| Ingredient | What it is | Where it comes from |
|
|
47
56
|
|---|---|---|
|
|
48
|
-
| 🎬 **The meme** | A borrowed clip everyone already half-recognises, doing one absurd physical thing | `vidfarm.cc/explore` → public raws, `sourceType: MemeScreens` (851 of them
|
|
57
|
+
| 🎬 **The meme** | A borrowed clip everyone already half-recognises, doing one absurd physical thing | `vidfarm.cc/explore` → public raws, `sourceType: MemeScreens` (851 of them — reached with `vector_q`, **not** with the CLI, which shows you 200 and does not say so) |
|
|
49
58
|
| ✍️ **The caption** | One sentence naming a specific lived experience, in the second person or the first | You. This is the only original work in the video, and it is 90% of the outcome |
|
|
50
59
|
|
|
51
60
|
**The offer is named on the video.** First choice: inside the caption, as a character in the setup
|
|
@@ -63,14 +72,54 @@ Three questions. **All three must be yes** or you have not got a meme recaption
|
|
|
63
72
|
|
|
64
73
|
| # | Question | If no |
|
|
65
74
|
|---|---|---|
|
|
66
|
-
| 1 | **Standalone** — Show it to someone who has never heard of the offer. Do they laugh? | You wrote
|
|
75
|
+
| 1 | **Standalone** — Show it to someone who has never heard of the offer, and who did not see your last post, and who has never seen the source meme. Do they laugh? | You wrote something that needs a briefing. See Rule 10 |
|
|
67
76
|
| 2 | **Problem space** — Is the experience it names one the product actually removes? | You wrote a meme. Funny, worthless. Post it from a personal account |
|
|
68
|
-
| 3 | **Enactment** —
|
|
77
|
+
| 3 | **Enactment** — Does the clip *enact* the caption — same situation, told twice, once in a body and once in words? Run the two covers below. **Not "does it suit the vibe"** | You wrote a caption over stock footage. See Rule 3 |
|
|
69
78
|
|
|
70
|
-
Test 3
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
> **Amended.** Test 3 used to read *"is the meme's physical action the literal body of the feeling
|
|
80
|
+
> the caption names?"* That is still what it means, but it was a single look, and a single look
|
|
81
|
+
> cannot fail — see the read-order paragraph below. It is now two covers and a comparison, and the
|
|
82
|
+
> word *literal* has been pinned down: **literal about the VERB, never about the NOUNS.** A caption
|
|
83
|
+
> about 10 tabs enacted by someone staring at 10 tabs is a stock photo, not a meme.
|
|
84
|
+
|
|
85
|
+
### The two-cover test — the enactment test, made testable
|
|
86
|
+
|
|
87
|
+
You cannot check a match by looking at the finished frame, because by the time you look at it you
|
|
88
|
+
already know the answer. So you take the two channels apart, describe each one **cold and in
|
|
89
|
+
writing**, and then compare the two sentences you wrote — not the video.
|
|
90
|
+
|
|
91
|
+
| Step | What you do | What you write down | If it fails |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| **Cover the caption** | Watch the clip alone, muted, with the caption hidden | `clip_reads_blind:` one sentence — what is happening to that person, named by a stranger | The clip is a mood, not an action. **Recast.** No caption rescues a clip that does nothing |
|
|
94
|
+
| **Cover the visual** | Read the caption alone, cold, with no clip at all | `caption_reads_blind:` one sentence — the situation a stranger holds after one read | The caption is a stage direction waiting for a picture. **Rewrite.** The caption must already be a complete joke |
|
|
95
|
+
| **Uncover both** | Read your two sentences side by side | — | **They are two different situations.** This is the defect. Change one of them |
|
|
96
|
+
|
|
97
|
+
Both sentences go in the composition, in the enactment declaration, and gate 2 reads them. See
|
|
98
|
+
"Declare the enactment" under Casting.
|
|
99
|
+
|
|
100
|
+
#### The failure mode, and why it is invisible to you
|
|
101
|
+
|
|
102
|
+
The defect this catches has a name: **vibe-adjacency** — a clip that is emotionally *compatible*
|
|
103
|
+
with the caption without being the same event. A caption about quietly giving up, over a clip of
|
|
104
|
+
someone rolling their eyes: both are "annoyed", both are the right *temperature*, and neither is
|
|
105
|
+
the other one. It is the most common ship-blocking defect in this format, and it is the one the
|
|
106
|
+
author is structurally unable to see.
|
|
107
|
+
|
|
108
|
+
*(Its opposite twin is worth naming here too: a clip that is the same event and also the same
|
|
109
|
+
picture — the caption about 10 tabs over someone glaring at 10 tabs. That one is not adjacent, it
|
|
110
|
+
is redundant, and it is Rule 3.3. Both are failures of the same knob, turned the two different
|
|
111
|
+
ways.)*
|
|
112
|
+
|
|
113
|
+
> **The reason is read order.** You wrote the caption first. So you approach the clip already
|
|
114
|
+
> holding the situation in your head, and you see the clip *as* that situation. Any clip that does
|
|
115
|
+
> not actively contradict the caption looks like a perfect match to you, and only to you. The
|
|
116
|
+
> viewer has no such pre-load: they get 1.2 seconds and two channels, and if the channels are only
|
|
117
|
+
> adjacent, the recognition never fires. They do not think "that's a mismatch" — they feel nothing
|
|
118
|
+
> and keep scrolling, which is why this defect never shows up as a complaint, only as a flat
|
|
119
|
+
> number.
|
|
120
|
+
|
|
121
|
+
That is why the test is written as two covers and a comparison rather than as one look. The two
|
|
122
|
+
covers force you to meet each channel without the other one loading the answer for you.
|
|
74
123
|
|
|
75
124
|
---
|
|
76
125
|
|
|
@@ -265,11 +314,111 @@ feel specific when you write them, and only one survives being read by a strange
|
|
|
265
314
|
> This test is also **elegance-test question 5** (Rule 1). It is listed twice on purpose: it decides
|
|
266
315
|
> whether the caption works at all, *and* whether the offer can be named inside it.
|
|
267
316
|
|
|
317
|
+
### The four contexts a viewer does not have
|
|
318
|
+
|
|
319
|
+
The cold-viewer test above covers **one** source of missing context — the product. There are three
|
|
320
|
+
more, and they kill a caption just as dead. The rule underneath all four is Rule 10:
|
|
321
|
+
|
|
322
|
+
> **Every word the caption needs must already be on the frame or already in the viewer's life.
|
|
323
|
+
> Nothing may be borrowed from off-screen.**
|
|
324
|
+
|
|
325
|
+
| # | The context they do not have | What it looks like when you lean on it | Why it is fatal |
|
|
326
|
+
|---|---|---|---|
|
|
327
|
+
| 1 | **The product** | brand names, feature words, category jargon, the founder's demo | Lands only on people who already bought — which defeats a meme ad. *(the existing cold-viewer test)* |
|
|
328
|
+
| 2 | **Your previous post** | `part 2`, `as promised`, `like i said`, a callback to another video | The platform serves this video cold, on purpose. Most people who see video 2 never saw video 1, and the ones who did are the ones you already have |
|
|
329
|
+
| 3 | **The source meme's own lore** | `for 47 seconds`, `and my day is ruined`, naming the character, riffing on what the meme originally meant | Caps your reach at **the meme's fandom**, which is not your niche. The clip is borrowed *equipment*, not a shared reference. Checked by `caption_does_not_borrow_lore` |
|
|
330
|
+
| 4 | **Your own life** | a detail only you can verify — the specific street, the specific colleague, the actual date | This is **rung 4** on the ladder above, arriving by a different road. It is your story, not theirs |
|
|
331
|
+
|
|
332
|
+
**And the phrase that admits all four at once: `iykyk`.** *"If you know, you know"* is a caption
|
|
333
|
+
telling you it did not do its job, while explicitly excluding everyone you needed to reach. It is
|
|
334
|
+
banned in the front matter, so gate 1 stops it before a render exists.
|
|
335
|
+
|
|
336
|
+
**The mechanical version:** hand the caption, as plain text, to someone who has seen none of your
|
|
337
|
+
posts, does not know the product, and has never seen the clip. If they need one question answered
|
|
338
|
+
before they get it, the caption is not finished. **A cold viewer with zero setup gets it, or it
|
|
339
|
+
does not ship.**
|
|
340
|
+
|
|
268
341
|
**Get the specifics from the customer, not from your imagination.** Their testimonials, their
|
|
269
342
|
support inbox and their landing-page copy are full of rung-3 sentences written by actual users. In
|
|
270
343
|
the worked example in Appendix D, the shipped caption is a lightly compressed version of a
|
|
271
344
|
testimonial printed on the customer's own homepage — which also happens to satisfy Rule 6.
|
|
272
345
|
|
|
346
|
+
### Strictness vs relatability — the two rules above do NOT mean "play it safe"
|
|
347
|
+
|
|
348
|
+
Read Rule 3 and Rule 10 naively and you get this:
|
|
349
|
+
|
|
350
|
+
> ❌ `me when my code doesn't work`
|
|
351
|
+
|
|
352
|
+
It passes both. It is enacted by literally any frowning clip. It needs no context at all. It is
|
|
353
|
+
also completely dead, and if this file let you ship it, the file would be worse than useless. So
|
|
354
|
+
this section is not optional colour — it is the half of the format the strict rules do not carry.
|
|
355
|
+
|
|
356
|
+
**The diagnosis:** that caption is standalone *because* it is universal, and universal is the same
|
|
357
|
+
thing as unfelt. The ladder already says so at rung 1: *"true of everyone, felt by no one."*
|
|
358
|
+
Tightening for context and tightening for specificity feel like the same move — they are opposites.
|
|
359
|
+
|
|
360
|
+
#### The one distinction that resolves it: context vs detail
|
|
361
|
+
|
|
362
|
+
> **Context is what the viewer must be TOLD. Detail is what the viewer already HAS.
|
|
363
|
+
> Rule 10 turns context to zero. The ladder turns detail as high as it goes. They are two dials.**
|
|
364
|
+
|
|
365
|
+
A safe, dead caption is what you get by turning both down at once — which is the default mistake,
|
|
366
|
+
because at 2am both feel like "things I'm assuming the reader knows".
|
|
367
|
+
|
|
368
|
+
**The test that separates them, one question per specific in your caption:**
|
|
369
|
+
|
|
370
|
+
> **Who had to have been there?**
|
|
371
|
+
|
|
372
|
+
| The specific | Who had to have been there | Verdict |
|
|
373
|
+
|---|---|---|
|
|
374
|
+
| `10 tabs` | anyone who has ever tried to pick dinner | **Detail.** Keep it. Add more |
|
|
375
|
+
| `since 7pm` | anyone with an evening | **Detail** |
|
|
376
|
+
| `one scallop roll` | anyone who has wanted one exact thing | **Detail** — and it is the best word in the caption |
|
|
377
|
+
| `the v2 sync` | someone who bought the product | **Context.** Cut |
|
|
378
|
+
| `part 2` | someone who saw your last post | **Context.** Cut |
|
|
379
|
+
| `for 47 seconds` | someone who has seen the source meme | **Context.** Cut |
|
|
380
|
+
| `that place on ludlow` | you | **Context.** Cut — this is rung 4 |
|
|
381
|
+
|
|
382
|
+
Detail is *shared* and specific. Context is *private* and specific. They look identical while you
|
|
383
|
+
are typing, which is exactly why you write the question down and answer it per word.
|
|
384
|
+
|
|
385
|
+
#### Strictness makes the caption MORE relatable, in two mechanical ways
|
|
386
|
+
|
|
387
|
+
Not as a consolation. As arithmetic:
|
|
388
|
+
|
|
389
|
+
1. **The word budget.** A caption is 8–20 words and it spends every one of them on either context
|
|
390
|
+
or detail. Every word you win back by cutting context is a word you can spend on detail —
|
|
391
|
+
**so spend it.** Cutting `on resy` and not replacing it leaves a weaker caption; cutting it and
|
|
392
|
+
buying `since 7pm` leaves a stronger one. **Deleting context without re-spending the refund is
|
|
393
|
+
how a correct edit produces a worse video**, and it is the most common way this section gets
|
|
394
|
+
misread.
|
|
395
|
+
2. **The cover-the-caption step will not accept a flat line.** You have to write down, in one
|
|
396
|
+
sentence, the situation a stranger holds after reading the caption cold. *"A person is sad"* is
|
|
397
|
+
not a situation and will not survive being compared to a clip's blind read — the two sentences
|
|
398
|
+
have nothing to agree on. **The test that looks like it would flatten the caption is the test
|
|
399
|
+
that refuses a flat one.**
|
|
400
|
+
|
|
401
|
+
#### And the same distinction on the visual side
|
|
402
|
+
|
|
403
|
+
> **Enactment is about the VERB, not the WORLD. The verb must match; the world must not.**
|
|
404
|
+
|
|
405
|
+
- The clip must **do** the caption's verb — deflate, blank, loop, stare.
|
|
406
|
+
- The clip must **not depict** the caption's nouns. Ten tabs, a laptop, a restaurant, a
|
|
407
|
+
spreadsheet: if the clip shows the thing the caption is about, you have filmed a re-enactment,
|
|
408
|
+
and a re-enactment is a stock photo with a joke written under it.
|
|
409
|
+
|
|
410
|
+
The comedy lives in the gap between where the feeling came from (10 tabs at 7pm) and the body it
|
|
411
|
+
arrives in (a hungover dog). That gap is the same one "Match the INTENSITY" is talking about, and
|
|
412
|
+
it is why `caption_does_not_borrow_lore` fails a caption that reuses a word from the clip's slug:
|
|
413
|
+
a caption that names what the clip literally *is* has closed the gap.
|
|
414
|
+
|
|
415
|
+
#### If you cut the context and the caption goes generic
|
|
416
|
+
|
|
417
|
+
Then you never had a caption — you had a reference. **Go back to Rule 6.** A sentence a real user
|
|
418
|
+
actually said is reliably both context-free and specific, because they said it to a stranger who
|
|
419
|
+
also had none of the context. That is why testimonials outperform anything you would invent, and
|
|
420
|
+
it is the cheapest route out of a caption that got safe.
|
|
421
|
+
|
|
273
422
|
### Banned captions
|
|
274
423
|
|
|
275
424
|
- **The product as the thing that WORKS.** The name may appear as a subject in the setup (Rule 1,
|
|
@@ -283,58 +432,262 @@ testimonial printed on the customer's own homepage — which also happens to sat
|
|
|
283
432
|
- Two-part setups split across a cut. That is a different format and it is weaker.
|
|
284
433
|
- Anything naming a real competitor doing something bad. Personify a *category* ("every delivery
|
|
285
434
|
app"), not a company you can be sued by.
|
|
435
|
+
- **Anything with context debt** (Rule 10): `iykyk`, `if you know you know`, `you already know`,
|
|
436
|
+
`part 2`, `as promised`, `like i said`, `see my last`. Each one asks the viewer for something
|
|
437
|
+
they do not have. Banned in the front matter and checked by `caption_no_context_debt`.
|
|
438
|
+
- **Anything riffing on the source meme's own lore.** The clip is borrowed equipment, not a shared
|
|
439
|
+
reference. If the caption only works for people who recognise the meme, its ceiling is that
|
|
440
|
+
meme's fandom. Checked by `caption_does_not_borrow_lore`.
|
|
441
|
+
- **Anything that narrates the clip.** `me quietly giving up` over a clip of someone quietly giving
|
|
442
|
+
up is a subtitle, not a joke — the caption's job is to supply the *situation* the body is
|
|
443
|
+
reacting to, and a clip that already contains the situation leaves it nothing to do.
|
|
286
444
|
|
|
287
445
|
---
|
|
288
446
|
|
|
289
447
|
## Casting the meme
|
|
290
448
|
|
|
291
|
-
###
|
|
449
|
+
### Two channels: the slug is the IDENTITY, the vector is the BODY
|
|
450
|
+
|
|
451
|
+
The shelf answers two completely different questions and it answers them through two different
|
|
452
|
+
fields. Casting badly is almost always the result of asking one of them the other's question.
|
|
453
|
+
|
|
454
|
+
| You want to know | Ask | Why |
|
|
455
|
+
|---|---|---|
|
|
456
|
+
| **which meme is this** | the `slugId` | The AI descriptions are blind to the meme's identity — see the warning below |
|
|
457
|
+
| **what does the body DO** | `vector_q`, semantic search | The embedding is built from the description **plus the action / emotion / motion / subject tags**, which is exactly the verb axis this format casts on |
|
|
292
458
|
|
|
293
459
|
> ⚠️ **The descriptions on this shelf were written by a model that has never heard of the meme.**
|
|
294
|
-
> This is not a small problem — it is the defining fact of sourcing this format.
|
|
295
|
-
>
|
|
296
460
|
> `my-brain-every-10-seconds` is described as *"A man in a suit gestures and looks at the camera
|
|
297
461
|
> with a neutral expression."* `jonah-jameson-laugh-you-serious` is described as *"A solid green
|
|
298
|
-
> screen background."*
|
|
299
|
-
> that contains a dozen clips of people eating.
|
|
462
|
+
> screen background."*
|
|
300
463
|
>
|
|
301
|
-
> **The `slugId` is the index. The description is noise
|
|
302
|
-
>
|
|
464
|
+
> **The `slugId` is the index of identity. The description is noise about identity.** But that same
|
|
465
|
+
> description says *what the body is doing*, and **that is the only thing this format casts on.** A
|
|
466
|
+
> model that does not know it is looking at J. Jonah Jameson can still tell you the man is laughing
|
|
467
|
+
> in your face — so the descriptions are worthless for the question you were not going to ask and
|
|
468
|
+
> useful for the one you were.
|
|
469
|
+
|
|
470
|
+
#### 🚨 `--query` is LEXICAL. Semantic search is a different parameter, and the CLI has no flag for it
|
|
471
|
+
|
|
472
|
+
This is the single most expensive mistake in sourcing this format, and an earlier draft of this file
|
|
473
|
+
made it and drew the wrong conclusion from it.
|
|
474
|
+
|
|
475
|
+
`vidfarm public-raws --query "…"` sends `?q=`, and `q` is a **substring filter that runs first**. It
|
|
476
|
+
throws away every clip whose text does not literally contain your words, and only *then* does the
|
|
477
|
+
server rank whatever survived. On a shelf whose descriptions never use your vocabulary, the filter
|
|
478
|
+
empties the set before the ranking gets to see it — so a perfectly good semantic query returns
|
|
479
|
+
nothing, and it looks like semantic search is broken. It is not. It never ran.
|
|
303
480
|
|
|
304
481
|
```bash
|
|
305
|
-
|
|
306
|
-
vidfarm public-raws
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
vidfarm public-raws
|
|
482
|
+
# ❌ lexical filter. Measured: 0 results, on a shelf full of blank stares
|
|
483
|
+
vidfarm api GET "/api/v1/public-raws?category=greenscreen&limit=8&q=blank%20stare%20confused"
|
|
484
|
+
|
|
485
|
+
# ✅ semantic. Same shelf, same words, ranked by MEANING
|
|
486
|
+
vidfarm api GET "/api/v1/public-raws?category=greenscreen&limit=8&vector_q=blank%20stare%20confused%20no%20comprehension"
|
|
487
|
+
# cat-staring · mrbean-wtf-greenscreen · james-franco-shocked · spongebob-panting …
|
|
310
488
|
```
|
|
311
489
|
|
|
490
|
+
> **Pass `vector_q` and do NOT pass `q`.** The server defaults `vector_q` to `q` when you omit it,
|
|
491
|
+
> which means the CLI's `--query` gets you the lexical filter *and* the ranking — and the filter has
|
|
492
|
+
> already deleted the answer. Sending only `vector_q` skips the filter and ranks the whole shelf.
|
|
493
|
+
>
|
|
494
|
+
> **`vidfarm public-raws` cannot do this.** It maps `--query` to `q` and has no vector flag. Every
|
|
495
|
+
> semantic cast in this file goes through `vidfarm api GET`.
|
|
496
|
+
|
|
497
|
+
**It needs a login and an embedding key, and it fails SILENTLY without one.** The ranking requires a
|
|
498
|
+
logged-in customer with a saved gemini or openai provider key. With no key — or logged out — the
|
|
499
|
+
server skips the ranking, returns **HTTP 200 with a full page of results**, and says nothing. You
|
|
500
|
+
get the shelf in recency order wearing the costume of a search result.
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
vidfarm whoami # must show a login
|
|
504
|
+
vidfarm api GET /api/v1/user/me/provider-keys | grep -iE "gemini|openai"
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**Confirm the ranking is live before you trust a cast:** query `arms up jumping celebration confetti`
|
|
508
|
+
and query `shoulders drop giving up defeated sigh`. If the top result is the same clip for both, the
|
|
509
|
+
embedder is not running and you are reading a recency list.
|
|
510
|
+
|
|
511
|
+
#### Semantic search reaches the whole shelf. The CLI reaches a quarter of it
|
|
512
|
+
|
|
312
513
|
> 🚨 **`vidfarm public-raws` shows you 200 clips and stops, and it does not tell you.** The server
|
|
313
514
|
> caps a shelf at 200 per call and returns a `next_cursor` — but the CLI has **no `--cursor` flag**,
|
|
314
|
-
> so raising `--limit` to 1200 still returns exactly 200.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
515
|
+
> so raising `--limit` to 1200 still returns exactly 200.
|
|
516
|
+
|
|
517
|
+
`vector_q` on a **curated shelf** (`greenscreen`, `reaction`, `lifestyle`, `text-graphics`, …) is
|
|
518
|
+
not a rerank of that 200. The server scans **1500 records** for a curated category and ranks all of
|
|
519
|
+
them, so one call sees essentially the entire MemeScreens library and hands you its top N.
|
|
520
|
+
|
|
521
|
+
Measured on the `greenscreen` shelf, ten verb queries against one plain `--limit 200` page:
|
|
522
|
+
|
|
523
|
+
| | |
|
|
524
|
+
|---|---|
|
|
525
|
+
| Distinct clips the ten queries returned | **143** |
|
|
526
|
+
| Of those, **not present** in the CLI's first 200 | **93 — 65%** |
|
|
527
|
+
| Of those, hallucinated / not really on the shelf | **0.** All 143 verified against the fully-paged 693 |
|
|
528
|
+
|
|
529
|
+
**Two thirds of what semantic search finds is invisible to the CLI**, and the two thirds it hides
|
|
530
|
+
are not the boring ones — they are the animal and anonymous clips (`cat-staring`, `cat-sleeping`,
|
|
531
|
+
`hangover-dog`) that are 🟢 **safe tier**, which is the tier client work is supposed to default to.
|
|
532
|
+
Casting from the first 200 quietly pushes you toward the celebrity clips you are not allowed to use.
|
|
533
|
+
|
|
534
|
+
```bash
|
|
535
|
+
vidfarm public-raws --categories # the shelves + live counts. Start here
|
|
536
|
+
vidfarm public-raws --category greenscreen --limit 200 # the first 200 only — for a browse, not a cast
|
|
537
|
+
```
|
|
319
538
|
|
|
320
539
|
**Every shelf the CLI exposes is dominated by MemeScreens** — `b-roll`, `lifestyle` and
|
|
321
540
|
`text-graphics` all come back as greenscreen memes. Good news for this format, bad news for
|
|
322
541
|
backgrounds (see below).
|
|
323
542
|
|
|
543
|
+
### The diversity sweep — one query per verb, then dedupe
|
|
544
|
+
|
|
545
|
+
> **A single vector query is a relevance tool. It is the OPPOSITE of a diversity tool.** It returns
|
|
546
|
+
> the twenty clips nearest one point, and near-neighbours are near-duplicates: four `cat-sitting`
|
|
547
|
+
> variants of the same cat. Cast ten videos off one query and you will ship ten videos that look
|
|
548
|
+
> like one video.
|
|
549
|
+
|
|
550
|
+
Diversity comes from **querying ten different points and taking the union.** The ten points are
|
|
551
|
+
already written down — they are the ten verb tokens in the table below, which is why the list is
|
|
552
|
+
closed. One query per verb, top 20 each, deduped, is a 143-clip casting pool spread across the whole
|
|
553
|
+
shelf by meaning instead of by upload date.
|
|
554
|
+
|
|
555
|
+
```python
|
|
556
|
+
#!/usr/bin/env python3
|
|
557
|
+
"""meme-sweep.py — one vector query per verb token -> a deduped casting pool.
|
|
558
|
+
|
|
559
|
+
Needs a vidfarm login + a saved gemini/openai key, or the ranking silently
|
|
560
|
+
does not happen (see above). Run it once per session; it is ~10 API calls.
|
|
561
|
+
"""
|
|
562
|
+
import json, subprocess, urllib.parse
|
|
563
|
+
|
|
564
|
+
VERBS = {
|
|
565
|
+
"deflate": "shoulders drop eyes go flat accepting a bad outcome without a fight",
|
|
566
|
+
"blank": "total non-comprehension head tilt dead eyes nothing processing",
|
|
567
|
+
"loop": "the same thought going round again mechanically forever repetitive",
|
|
568
|
+
"smug": "arms folded self-satisfied already solved it insufferable",
|
|
569
|
+
"overreact": "full body screaming sobbing flailing at something trivial",
|
|
570
|
+
"correct": "turning to camera to tell you patiently that you are wrong",
|
|
571
|
+
"unbothered": "completely relaxed doing nothing while chaos happens calm",
|
|
572
|
+
"celebrate": "arms up jumping celebration confetti scale joy",
|
|
573
|
+
"stare": "holding eye contact with the camera saying nothing silent",
|
|
574
|
+
"wait": "still waiting visibly longer than is reasonable impatient",
|
|
575
|
+
}
|
|
576
|
+
SHELVES = ("greenscreen", "reaction", "lifestyle", "text-graphics")
|
|
577
|
+
|
|
578
|
+
def get(path):
|
|
579
|
+
out = subprocess.run(["vidfarm", "api", "GET", path], capture_output=True, text=True).stdout
|
|
580
|
+
brace = out.find("{") # first line is a coloured "GET … → 200" status line
|
|
581
|
+
return json.JSONDecoder().raw_decode(out[brace:])[0] if brace >= 0 else {}
|
|
582
|
+
|
|
583
|
+
pool, tops = {}, {}
|
|
584
|
+
for shelf in SHELVES:
|
|
585
|
+
for verb, q in VERBS.items():
|
|
586
|
+
d = get(f"/api/v1/public-raws?category={shelf}&limit=20"
|
|
587
|
+
f"&vector_q={urllib.parse.quote(q)}") # vector_q ONLY. Never q
|
|
588
|
+
raws = d.get("raws", [])
|
|
589
|
+
if raws:
|
|
590
|
+
tops.setdefault(shelf, []).append(raws[0]["rawId"])
|
|
591
|
+
for rank, raw in enumerate(raws):
|
|
592
|
+
slug = raw.get("slugId") or raw["rawId"]
|
|
593
|
+
pool.setdefault(slug, {"verb": verb, "rank": rank, "id": raw["rawId"],
|
|
594
|
+
"desc": (raw.get("description") or "")[:90]})
|
|
595
|
+
|
|
596
|
+
# The ranking fails SILENTLY (no login / no embedding key) and still returns a
|
|
597
|
+
# full page. The only reliable tell is differential: ten different queries that
|
|
598
|
+
# rank must not all return the same clip first. Do NOT test this by looking for
|
|
599
|
+
# a slugId — several shelves legitimately return bare UUIDs.
|
|
600
|
+
for shelf, first in tops.items():
|
|
601
|
+
if len(set(first)) < len(first) / 2:
|
|
602
|
+
raise SystemExit(f"{shelf}: {len(set(first))} distinct top hits from {len(first)} queries — "
|
|
603
|
+
"the embedder is not running. Check `vidfarm whoami` and your gemini/openai key.")
|
|
604
|
+
|
|
605
|
+
with open("casting-pool.tsv", "w") as f:
|
|
606
|
+
f.write("slug\tverb\trank\trawId\tdescription\n")
|
|
607
|
+
for slug, v in sorted(pool.items(), key=lambda kv: (kv[1]["verb"], kv[1]["rank"])):
|
|
608
|
+
f.write(f"{slug}\t{v['verb']}\t{v['rank']}\t{v['id']}\t{v['desc']}\n")
|
|
609
|
+
print(len(pool), "distinct clips -> casting-pool.tsv")
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
**Then cast off the TSV, and cast off a verb you have not shipped this week:**
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
awk -F'\t' '$2=="unbothered"' casting-pool.tsv | cut -f1,5 # the whole neighbourhood, one verb
|
|
616
|
+
cut -f2 casting-pool.tsv | sort | uniq -c # how deep each verb actually goes
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Three rules for keeping the pool diverse, all of them cheap:**
|
|
620
|
+
|
|
621
|
+
1. **Drop the near-duplicate families.** `cat-sitting-5e58…` and `cat-sitting-8edd…` are one casting
|
|
622
|
+
choice, not two. Collapse on the slug stem (the slug minus its trailing hex) before you count
|
|
623
|
+
your options, or you will believe you have 143 and be choosing among about 110.
|
|
624
|
+
2. **Take the top ~20 per verb, never the top 60 of one.** The tail of one query is further from
|
|
625
|
+
that verb than the head of the next query is — depth on one point buys worse clips, breadth
|
|
626
|
+
across points buys better ones.
|
|
627
|
+
3. **Subtract the delivery log before you look, not after.** `join` the pool against
|
|
628
|
+
`DELIVERED.tsv` so a retired clip is never on the sheet you are choosing from. A clip you have
|
|
629
|
+
already sent is not an option you rejected, it is an option you should not have seen.
|
|
630
|
+
|
|
631
|
+
```bash
|
|
632
|
+
cut -f5 CLIPPER/DELIVERED.tsv | sort -u > /tmp/used.txt
|
|
633
|
+
grep -vFf /tmp/used.txt casting-pool.tsv > casting-pool.fresh.tsv # cast from THIS
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
> **The sweep is how "there was nothing else" stops being sayable.** Rule 11 asks you not to reuse a
|
|
637
|
+
> clip you have already delivered; the honest objection has always been that finding a fresh one
|
|
638
|
+
> meant paging a 693-clip shelf by hand. It is now ten API calls and a TSV.
|
|
639
|
+
|
|
640
|
+
> **Delivery work: open your delivery log in the same breath as the sweep.** Anything you already
|
|
641
|
+
> sent this client is out (Rule 11), and anything that has been passed on twice is out for everybody.
|
|
642
|
+
> **A clip you have already used is not a shortcut, it is the shelf you did not read** — and the
|
|
643
|
+
> sweep hands you 143 candidates, so "there was nothing else" is almost never the real reason.
|
|
644
|
+
>
|
|
645
|
+
> ```bash
|
|
646
|
+
> cut -f1,5,8 CLIPPER/DELIVERED.tsv # client · clip · verdict — read it BEFORE the contact sheet
|
|
647
|
+
> ```
|
|
648
|
+
>
|
|
649
|
+
> Reusing a clip that has *earned* it, on a different client, is correct and encouraged. See
|
|
650
|
+
> *Across clients* under Rule 11.
|
|
651
|
+
|
|
324
652
|
### The enactment test, in practice
|
|
325
653
|
|
|
326
654
|
You are casting for a **verb**, not a mood. Write down the verb your caption's subject is doing
|
|
327
|
-
with their body, then find the slug that does that verb
|
|
655
|
+
with their body, then find the slug that does that verb.
|
|
328
656
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
|
335
|
-
|
|
336
|
-
|
|
|
337
|
-
|
|
|
657
|
+
**The verb comes from a closed list of ten tokens.** Not because ten is the number of human
|
|
658
|
+
feelings, but because a free-text verb is not comparable to anything — and the whole point of the
|
|
659
|
+
enactment test is a comparison. A closed token makes "does the clip do what the caption does" a
|
|
660
|
+
string equality instead of a judgement, which is the only form of it a gate can enforce:
|
|
661
|
+
|
|
662
|
+
| Verb token | The body doing it | Slugs that enact it |
|
|
663
|
+
|---|---|---|
|
|
664
|
+
| `deflate` | shoulders drop, eyes go flat, the bad outcome is accepted without a fight | `my-disappointment-is-immeasurable-and-my-day-is-ruined`, `jonah-hill-sigh`, `girl-eyerolls` |
|
|
665
|
+
| `blank` | total non-comprehension — head tilt, dead eyes, nothing processing | `huh-cat`, `hangover-dog`, `zoolander-staring`, `nervous` |
|
|
666
|
+
| `loop` | the same thought going round again, mechanically, forever | `my-brain-every-10-seconds`, `monkey-cymbal-jolly-chimp`, `zoning-out` |
|
|
667
|
+
| `smug` | arms folded, already solved it, insufferable about it | `gigachad`, `chad-head-bopping`, `check-out-the-big-brain-on-brad` |
|
|
668
|
+
| `overreact` | a full-body scream, sob or flail at something trivial | `neon-screaming`, `jontron-nooooooo`, `girl-fake-crying` |
|
|
669
|
+
| `correct` | turning to camera to tell you, patiently, that you are wrong | `jonathan-frakes-telling-you-youre-wrong-for-47-seconds`, `women-mocking` |
|
|
670
|
+
| `unbothered` | the room is on fire and the body has not moved | `my-favorite-thing-in-the-world-to-do-is-nothing`, `boss-baby-chilling` |
|
|
671
|
+
| `celebrate` | arms up, jumping, confetti-scale joy | 🔎 `celebration-dance`, `girl-dancing`, `jojo-siwa-dancing` — the natural win-pole verb |
|
|
672
|
+
| `stare` | holding eye contact with the camera and saying nothing | 🔎 `cat-staring` (🟢), `mortal-kombat-1-shang-tsung` (🔴) — the `<Entity> watching me …` verb (Rule 1, tier 1) |
|
|
673
|
+
| `wait` | still present, still waiting, visibly longer than is reasonable | 🔎 `wait-hold-on-wait` — for captions whose collapse is elapsed time |
|
|
674
|
+
|
|
675
|
+
**The queries that produce this table are the `VERBS` dict in `meme-sweep.py` above** — the table is
|
|
676
|
+
its cached output, not a separate list to maintain. Re-run the sweep and you regenerate it.
|
|
677
|
+
|
|
678
|
+
> 🔎 **marks a slug found by the vector sweep and NOT yet confirmed on a contact sheet.** The first
|
|
679
|
+
> seven rows were cast by hand and shipped; the last three were empty until the sweep filled them,
|
|
680
|
+
> and the sweep ranks descriptions, not footage. **Build the sheet before you use one** — that rule
|
|
681
|
+
> does not relax because a machine agreed with you.
|
|
682
|
+
|
|
683
|
+
**Do not add an eleventh token** to avoid the work of choosing among ten. If a clip genuinely does
|
|
684
|
+
something none of the ten covers, that is a real finding: add the token here, **with its body line
|
|
685
|
+
and its vector query**, before you use it.
|
|
686
|
+
|
|
687
|
+
> **The sweep also grades the token list.** `deflate` and `celebrate` came back with 20 and 19 clips
|
|
688
|
+
> the other nine queries had not already found; `stare` and `wait` came back with 9 each. A verb
|
|
689
|
+
> whose neighbourhood is that thin is a verb the shelf cannot really cast — treat a low count as a
|
|
690
|
+
> warning that you are about to force a clip, not as a challenge.
|
|
338
691
|
|
|
339
692
|
**Then confirm by eye.** Build a contact sheet before you commit — a slug promises an action the
|
|
340
693
|
clip does not always deliver, and half these clips are 16:9 with the subject small at the bottom:
|
|
@@ -343,6 +696,63 @@ clip does not always deliver, and half these clips are 16:9 with the subject sma
|
|
|
343
696
|
ffmpeg -v error -i meme.mp4 -vf "fps=1,scale=240:-1,tile=7x1" -frames:v 1 sheet.png
|
|
344
697
|
```
|
|
345
698
|
|
|
699
|
+
### Declare the enactment — in the composition, where the gate can read it
|
|
700
|
+
|
|
701
|
+
The two-cover test is a human act. What a script can enforce is that **you actually performed it**
|
|
702
|
+
and that the answers are self-consistent. So the answers live in `composition.html`, at the top,
|
|
703
|
+
above the first layer:
|
|
704
|
+
|
|
705
|
+
```html
|
|
706
|
+
<!-- meme-recaption: enactment
|
|
707
|
+
clip_slug: my-disappointment-is-immeasurable-and-my-day-is-ruined
|
|
708
|
+
caption_verb: deflate
|
|
709
|
+
clip_verb: deflate
|
|
710
|
+
feeling_size: small
|
|
711
|
+
clip_size: large
|
|
712
|
+
clip_reads_blind: a man in a suit goes flat and accepts something without arguing
|
|
713
|
+
caption_reads_blind: someone gives up on dinner after an hour and settles for pizza
|
|
714
|
+
-->
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
**It is an HTML comment, not a layer, on purpose.** A text layer would count against
|
|
718
|
+
`max_text_cards`, would need a `data-start`, and — worst — could render. This must never reach a
|
|
719
|
+
pixel. It is build metadata that happens to live in the file the gate already reads, so there is
|
|
720
|
+
no second file to lose.
|
|
721
|
+
|
|
722
|
+
| Field | Rule | Why the gate can hold you to it |
|
|
723
|
+
|---|---|---|
|
|
724
|
+
| `clip_slug` | the raw's slug, as cast | Feeds `caption_does_not_borrow_lore` — the caption may not reuse a word from it |
|
|
725
|
+
| `caption_verb` / `clip_verb` | both from the ten tokens, and **equal** | This is Test 3 as a string equality. Two different tokens is a declared mismatch and the build stops |
|
|
726
|
+
| `feeling_size` / `clip_size` | `small` or `large`, and **different** | "Match the INTENSITY" made enforceable — see below |
|
|
727
|
+
| `clip_reads_blind` | ≥5 words, written while the caption was covered, and it may **not reuse a content word from the caption** | The cover-the-caption step, on the record — and the one field the gate can genuinely interrogate |
|
|
728
|
+
| `caption_reads_blind` | ≥5 words, written while the clip was covered, and not the same sentence as `clip_reads_blind` | The cover-the-visual step, on the record |
|
|
729
|
+
|
|
730
|
+
#### Why `clip_reads_blind` may not borrow the caption's words
|
|
731
|
+
|
|
732
|
+
This is the check with actual teeth, and the reason is the read-order problem again:
|
|
733
|
+
|
|
734
|
+
> **If the only way you can describe the clip is with the caption's own nouns, one of two things is
|
|
735
|
+
> true, and both are fatal.** Either you never saw the clip cold — you projected the caption onto
|
|
736
|
+
> it, which is exactly the blindness the two covers exist to break — or the clip really does depict
|
|
737
|
+
> the caption's nouns, which is a re-enactment and fails Rule 3.3.
|
|
738
|
+
|
|
739
|
+
A correct pair reads with almost no vocabulary in common, because the two channels are *supposed*
|
|
740
|
+
to be different worlds:
|
|
741
|
+
|
|
742
|
+
- ✅ caption `when i've had 10 tabs open since 7pm for one scallop roll and we're getting pizza again`
|
|
743
|
+
· clip `a man in a suit goes flat and accepts something without arguing` — nothing shared, and
|
|
744
|
+
they are obviously one situation
|
|
745
|
+
- ❌ clip `a guy who has had 10 tabs open gives up` — you wrote the caption again with "a guy" in
|
|
746
|
+
front of it. `clip_read_is_independent` fails
|
|
747
|
+
|
|
748
|
+
> **What no gate can check, and this file will not claim otherwise:** whether the two sentences
|
|
749
|
+
> describe the *same* situation. Lexical overlap does not measure it — an early draft of this
|
|
750
|
+
> section required the two blind reads to share a word, and a correct pair shares none, so the
|
|
751
|
+
> check would have fired on every good build and been switched off within a day. **The comparison
|
|
752
|
+
> is gate 3. It always was.** What the declaration buys is that the comparison becomes a discrete
|
|
753
|
+
> act with a written output, performed before you have seen the finished frame — not a feeling you
|
|
754
|
+
> had at 2am with both channels already loaded in your head.
|
|
755
|
+
|
|
346
756
|
### Match the INTENSITY, not just the verb
|
|
347
757
|
|
|
348
758
|
The enactment test gets you the right action. This gets you the right *size* of it — and it is
|
|
@@ -357,6 +767,17 @@ where the comedy actually comes from.
|
|
|
357
767
|
- Matching a big feeling to a big reaction is not a joke, it is a re-enactment. Matching a small
|
|
358
768
|
feeling to a small reaction is not anything at all.
|
|
359
769
|
|
|
770
|
+
**Declare both sizes and they must differ.** `feeling_size` and `clip_size` in the enactment block
|
|
771
|
+
are `small` or `large`, and `intensity_mismatched` fails when they are equal. Two coarse buckets,
|
|
772
|
+
not a scale, because a scale invites a 3-vs-4 that means nothing; the question the format actually
|
|
773
|
+
asks is only ever *"is the reaction the wrong size for the cause?"* — and the answer to that is
|
|
774
|
+
binary.
|
|
775
|
+
|
|
776
|
+
> **This does not contradict "the verb must match".** The verb is *what* the body does; the size is
|
|
777
|
+
> *how much*. Enactment requires the same verb; comedy requires a different amount of it. A caption
|
|
778
|
+
> about a mildly annoying evening (`small`) enacted by a man whose day is immeasurably ruined
|
|
779
|
+
> (`large`) is both rules satisfied at once — same verb, wrong size — and it is the worked example.
|
|
780
|
+
|
|
360
781
|
**Choose the pain or win that the clip's energy already fits, rather than fighting the footage.**
|
|
361
782
|
You have 851 clips and one caption you have not written yet; it is far cheaper to pick the feeling
|
|
362
783
|
that suits a great clip than to hunt for a clip that suits a feeling you are attached to. When a
|
|
@@ -395,21 +816,68 @@ class before you write a single ffmpeg command.**
|
|
|
395
816
|
| Where the caption goes | On the background, above the subject | On a plate above the clip, outside the picture |
|
|
396
817
|
| Share of the shelf | The large majority | A minority — check for burned-in text on the contact sheet |
|
|
397
818
|
|
|
398
|
-
|
|
399
|
-
always what it looks like:
|
|
819
|
+
### Decide the class MECHANICALLY, then read the plate
|
|
400
820
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
821
|
+
> **The class is not a judgement call and it must not be one.** "Is there a plate" has to be
|
|
822
|
+
> answered before "what colour is the plate", or you key a Class B clip against a muddy average of
|
|
823
|
+
> somebody's living room and punch holes in it. **Nothing downstream catches that** — the key
|
|
824
|
+
> "works", a subject is found, the render is quietly ruined, and `no_residual_green` passes because
|
|
825
|
+
> there was never any green.
|
|
826
|
+
|
|
827
|
+
Run this on every clip, before Appendix A. It answers both questions in the right order:
|
|
828
|
+
|
|
829
|
+
```python
|
|
830
|
+
#!/usr/bin/env python3
|
|
831
|
+
"""plate-check.py <meme.mp4> — Class A or Class B, and the plate if there is one."""
|
|
832
|
+
import subprocess, sys, numpy as np
|
|
833
|
+
|
|
834
|
+
src, W, H = sys.argv[1], 320, 320
|
|
835
|
+
p = subprocess.run(["ffmpeg", "-v", "error", "-i", src, "-vf", f"fps=2,scale={W}:{H}",
|
|
836
|
+
"-pix_fmt", "rgb24", "-f", "rawvideo", "-"], capture_output=True)
|
|
837
|
+
a = np.frombuffer(p.stdout, np.uint8)
|
|
838
|
+
a = a[: len(a) // (W * H * 3) * (W * H * 3)].reshape(-1, H, W, 3).astype(np.int16)
|
|
839
|
+
|
|
840
|
+
# All four corners, not just the top-left: on a keyed clip every corner is plate,
|
|
841
|
+
# on a baked clip they are four different bits of a room.
|
|
842
|
+
c = np.concatenate([a[:, :24, :24], a[:, :24, -24:],
|
|
843
|
+
a[:, -24:, :24], a[:, -24:, -24:]], axis=2).reshape(-1, 3)
|
|
844
|
+
plate = c.mean(0).astype(int)
|
|
845
|
+
flat = c.std(0).mean() # one colour, or a scene
|
|
846
|
+
cover = (np.abs(a - plate).max(3) < 40).mean() # how much of the frame IS plate
|
|
847
|
+
sat = int(plate.max() - plate.min()) # letterbox bars read flat too
|
|
848
|
+
|
|
849
|
+
print(f"plate rgb {plate.tolist()} hex {'%02X%02X%02X' % tuple(plate)} "
|
|
850
|
+
f"corner stdev {flat:.1f} coverage {cover*100:.0f}% saturation {sat}")
|
|
851
|
+
|
|
852
|
+
if flat > 12 or cover < 0.25 or sat < 60:
|
|
853
|
+
sys.exit("CLASS B — no saturated flat plate. Do NOT key. Layout A, crop the burned-in text")
|
|
854
|
+
|
|
855
|
+
digital = plate[1] > 240 and plate[0] < 40 and plate[2] < 40
|
|
856
|
+
print("CLASS A — " + ("digital plate: chromakey 0.18:0.03, NO despill" if digital
|
|
857
|
+
else "filmed plate: chromakey 0.30:0.08, light despill"))
|
|
408
858
|
```
|
|
409
859
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
860
|
+
**The three thresholds and what each one is for:** `flat > 12` is a scene wearing a plate's
|
|
861
|
+
costume, `cover < 0.25` is a plate too small to be the background, `sat < 60` is black or white
|
|
862
|
+
letterbox bars, which are flat and are not a plate. All three are first-pass numbers measured on one
|
|
863
|
+
shelf — **calibrate them against a clip you already agree about before you let them fail anything.**
|
|
864
|
+
|
|
865
|
+
**Then read the colour, because these clips are re-encodes and the plate is not always what it looks
|
|
866
|
+
like.** A pure `00FF00` reading means a digital plate: key tight (`0.18:0.03`) and **skip despill.**
|
|
867
|
+
A reading like `1F D3 1F` means a filmed plate: key looser and despill lightly. Getting this
|
|
868
|
+
backwards is the first defect in the list below.
|
|
869
|
+
|
|
870
|
+
> **The shelf is overwhelmingly green, and the gate only knows green.** `no_residual_green` (gate 2)
|
|
871
|
+
> tests a green hue, so it catches both defects that matter on this shelf — a fringe from a tight
|
|
872
|
+
> key, and a key that never ran at all. `no_unkeyed_plate` sits beside it and is hue-agnostic, for
|
|
873
|
+
> the blue or magenta plate the green test is blind to. **If `plate-check.py` prints a plate that is
|
|
874
|
+
> not green, say so in the build log** — the named check is no longer the one protecting you.
|
|
875
|
+
|
|
876
|
+
> **Why this harness does not use `vidfarm remove-greenscreen` for the video.** The skill's keying
|
|
877
|
+
> surface (`remove-greenscreen --local` / `--smart`, `cutout`, `sticker-pack`) is the right answer
|
|
878
|
+
> almost everywhere else, and it is a **transparent-output** surface. This format never needs
|
|
879
|
+
> transparency in its output — it composites onto a background and ships an opaque MP4 — and the
|
|
880
|
+
> alpha intermediate is where the free path breaks. See the warning under "Cost-saving mode".
|
|
413
881
|
|
|
414
882
|
### Class B — dealing with the burned-in text
|
|
415
883
|
|
|
@@ -713,10 +1181,14 @@ are checked (`offer_not_in_payoff`, `caption_does_not_resolve`).
|
|
|
713
1181
|
2. Is it still **≤20 words** with the offer in it?
|
|
714
1182
|
3. Does the offer read as a **character**, or as a sponsor who bought a slot?
|
|
715
1183
|
4. Does the **collapse still land on the person**?
|
|
716
|
-
5. Would it still be funny to someone who has **never heard of the offer
|
|
1184
|
+
5. Would it still be funny to someone who has **never heard of the offer**, **did not see your last
|
|
1185
|
+
post**, and has **never seen the source meme**? (Rule 10 — all four contexts, not just the
|
|
1186
|
+
product)
|
|
717
1187
|
|
|
718
1188
|
Question 5 is the one that catches most failures. If the joke needs the reader to know what the
|
|
719
1189
|
product does, the product is not a character — it is an explanation, and explanations are not funny.
|
|
1190
|
+
The same is true of the other three borrowed contexts: each one turns the caption into something
|
|
1191
|
+
that has to be introduced, and nobody introduces a meme.
|
|
720
1192
|
|
|
721
1193
|
#### The handle (tier 2), specced
|
|
722
1194
|
|
|
@@ -763,10 +1235,32 @@ the last clause; on the win pole it may be in both, and the discipline moves to
|
|
|
763
1235
|
of it. **If you did not decide the pole, you are on the pain pole** — it is the safer default and the
|
|
764
1236
|
one that travels.
|
|
765
1237
|
|
|
766
|
-
### Rule 3 —
|
|
1238
|
+
### Rule 3 — the clip ENACTS the caption. Same verb, wrong size, different world
|
|
1239
|
+
|
|
1240
|
+
Written above as the two-cover test; repeated here because it is the rule most often lost between
|
|
1241
|
+
casting and the timeline, and because "cast for the verb" alone was too easy to satisfy.
|
|
767
1242
|
|
|
768
|
-
|
|
769
|
-
|
|
1243
|
+
> **Amended.** Rule 3 used to read *"cast for the verb, not the mood"*, and that is still the first
|
|
1244
|
+
> half of it. It was not enough: a verb named only in the build log is a verb nobody compares, so
|
|
1245
|
+
> the rule passed every time it was read. Rule 3 is now three claims, all of which are declared in
|
|
1246
|
+
> the composition and two of which the gate can fail you on.
|
|
1247
|
+
|
|
1248
|
+
1. **Same verb.** `caption_verb` and `clip_verb` are the same token from the closed list of ten.
|
|
1249
|
+
Not similar. The same. — `enactment_verbs_match`
|
|
1250
|
+
2. **Wrong size.** `feeling_size` ≠ `clip_size`. The mismatch between the size of the cause and the
|
|
1251
|
+
size of the reaction is where the comedy actually is. — `intensity_mismatched`
|
|
1252
|
+
3. **Different world.** The clip does the caption's verb; it must not depict the caption's nouns. A
|
|
1253
|
+
clip that shows the thing the caption is about is a re-enactment, and the gap is the joke. —
|
|
1254
|
+
partially caught by `caption_does_not_borrow_lore`
|
|
1255
|
+
|
|
1256
|
+
**And the test you run before any of that:** cover the caption, cover the visual, write one
|
|
1257
|
+
sentence for each, compare the sentences. The failure it exists for is vibe-adjacency, and the
|
|
1258
|
+
reason it is written as two covers is that you read the caption first and can no longer see the
|
|
1259
|
+
clip cold. Both sentences go in the composition, and `clip_reads_blind` may not reuse a content
|
|
1260
|
+
word from the caption. — `enactment_declared`, `clip_read_is_independent`
|
|
1261
|
+
|
|
1262
|
+
**The comparison itself is yours.** No script decides whether two sentences name one situation.
|
|
1263
|
+
The gate makes sure you wrote them, wrote them cold, and did not write the caption twice.
|
|
770
1264
|
|
|
771
1265
|
### Rule 4 — the action lands AFTER the caption is read, and frame 0 has a person on it
|
|
772
1266
|
|
|
@@ -825,6 +1319,108 @@ same complaint — so ask for exactly that.
|
|
|
825
1319
|
> Post caption pattern: `<the caption's situation, restated in one line>. <one question inviting
|
|
826
1320
|
> their version>`
|
|
827
1321
|
|
|
1322
|
+
### Rule 10 — the caption stands alone. Zero setup, zero borrowed context
|
|
1323
|
+
|
|
1324
|
+
**A cold viewer with nothing gets it, or it does not ship.** Nothing they see was explained
|
|
1325
|
+
anywhere else, because nowhere else is reachable: the platform hands them one muted 7-second video
|
|
1326
|
+
between two strangers' videos, and everything the joke needs has to already be on that frame or
|
|
1327
|
+
already in their life.
|
|
1328
|
+
|
|
1329
|
+
Four things you may not borrow — the product, your previous post, the source meme's lore, and your
|
|
1330
|
+
own private history. They are set out with their reasons under "The four contexts a viewer does not
|
|
1331
|
+
have". `iykyk` is banned outright, in the front matter, because it is the caption admitting it
|
|
1332
|
+
failed while excluding the people you needed.
|
|
1333
|
+
|
|
1334
|
+
**Rule 10 is not a licence to go generic, and read carelessly it produces exactly that.** Context
|
|
1335
|
+
and detail are two separate dials: Rule 10 turns *context* to zero, the specificity ladder turns
|
|
1336
|
+
*detail* to maximum, and the caption gets more relatable, not less, as long as you **re-spend the
|
|
1337
|
+
words you win back.** Cutting `on resy` and stopping there leaves a weaker caption; cutting it and
|
|
1338
|
+
buying `since 7pm` leaves a stronger one. The one question that sorts a word into the right dial is
|
|
1339
|
+
**"who had to have been there?"** — see "Strictness vs relatability", which is the load-bearing
|
|
1340
|
+
half of this rule.
|
|
1341
|
+
|
|
1342
|
+
Checked by `caption_no_context_debt` and `caption_does_not_borrow_lore`, and by the front matter's
|
|
1343
|
+
`forbid_text` list at gate 1.
|
|
1344
|
+
|
|
1345
|
+
### Rule 11 — one client never gets the same meme, or the same joke, twice
|
|
1346
|
+
|
|
1347
|
+
**This rule only applies when you are delivering to somebody** — a marketplace buyer, a retainer
|
|
1348
|
+
client, a customer's own account. On an account you own, ignore it and run the matrix.
|
|
1349
|
+
|
|
1350
|
+
> **No two videos delivered to the same client may share a `clip_slug`, a caption frame, or a pain —
|
|
1351
|
+
> unless that client asked for a series, in writing.**
|
|
1352
|
+
|
|
1353
|
+
Three separate keys, because this format repeats in three separate ways and only the first one is
|
|
1354
|
+
obvious:
|
|
1355
|
+
|
|
1356
|
+
| Key | The repeat | Why it is fatal here specifically |
|
|
1357
|
+
|---|---|---|
|
|
1358
|
+
| `clip_slug` | the same borrowed clip twice | It is not *similar* footage, it is the **same footage**. The buyer recognises it instantly, and the second video looks like the first one with the words changed — which is exactly what it is |
|
|
1359
|
+
| the caption frame | `When I …` twice, or `<Entity> when they …` twice | There are only **eight**, so two pains in one frame land as one joke. The words differ and the shape does not, and the shape is what a viewer remembers |
|
|
1360
|
+
| the pain | the same complaint restaged | You sold one insight three times. This is the one the buyer notices last and resents most |
|
|
1361
|
+
|
|
1362
|
+
**The rights tier compounds it.** 🟢 is the default for client work, which is a small shelf, so a
|
|
1363
|
+
clipper working one buyer for a month runs out of safe clips faster than they expect and starts
|
|
1364
|
+
reaching for one they already used. Cast against your delivery log, not against your memory. When
|
|
1365
|
+
the 🟢 shelf really is exhausted for that client, that is a signal to change the *pain* and cast
|
|
1366
|
+
fresh — not to re-run a clip.
|
|
1367
|
+
|
|
1368
|
+
**Your style repeats. Your joke does not.** The caption plate, the font, the 62% band, the grade on
|
|
1369
|
+
the background, the handle — that is the visual identity the client hired and it should be identical
|
|
1370
|
+
across every delivery. Rule 11 is about the two ingredients at the top of this file: the meme and
|
|
1371
|
+
the caption. Nothing else.
|
|
1372
|
+
|
|
1373
|
+
**The exception, and how narrow it is.** A client who asked for a series, a variant set, or "five
|
|
1374
|
+
cuts of this one joke" has asked for controlled repetition, and then repeating is the deliverable.
|
|
1375
|
+
It must be **written** — in the task payload, in the gig terms, or in their feedback on a proof they
|
|
1376
|
+
kept (`"More like this one."` counts). **An approval is not an ask.** A meme that worked is a reason
|
|
1377
|
+
to write another meme, not a reason to send the same one again.
|
|
1378
|
+
|
|
1379
|
+
#### Across clients — reuse a great raw, retire a mid one
|
|
1380
|
+
|
|
1381
|
+
Rule 11 is a hard rule *within* one client. Across clients it relaxes into a preference, and the
|
|
1382
|
+
thing that decides it is **not novelty, it is how the raw has actually performed.**
|
|
1383
|
+
|
|
1384
|
+
> **A raw that lands is an asset. Carry it to the next buyer. A raw that was only ever okay gets one
|
|
1385
|
+
> outing, and then it is done.**
|
|
1386
|
+
|
|
1387
|
+
Refusing to reuse a proven clip in the name of variety throws away the only edge casting gives you.
|
|
1388
|
+
Five buyers who each see `hangover-dog` once is five good videos. The failure is the other one, and
|
|
1389
|
+
it is specific: **one mediocre clip appearing in most of your recent deliveries**, because it was
|
|
1390
|
+
nearby and you had already keyed it. That is not a style, it is a rut, and it is visible to everyone
|
|
1391
|
+
except you.
|
|
1392
|
+
|
|
1393
|
+
| What the raw has collected in `DELIVERED.tsv` | What you may do with it |
|
|
1394
|
+
|---|---|
|
|
1395
|
+
| nothing — never delivered | **one use.** It is unproven no matter how good the contact sheet looked |
|
|
1396
|
+
| an approval, no passes | **reuse it across other clients**, once each. This is what a proven raw is for |
|
|
1397
|
+
| passed on twice, never approved | **retire it.** Two buyers said no to that face. The third will too |
|
|
1398
|
+
| mixed | a fit question, not a quality one. Reuse it only where the pain genuinely matches |
|
|
1399
|
+
|
|
1400
|
+
**The shelf is public, which makes this sharper than it looks.** You cast from `vidfarm public-raws`
|
|
1401
|
+
and so does every other clipper on the marketplace. A slug that is easy to find is easy for everyone
|
|
1402
|
+
to find, so the buyer may have seen it this week from somebody else — and 851 MemeScreens means the
|
|
1403
|
+
excuse "there was nothing else" is almost never true. **Run the diversity sweep before you reach for
|
|
1404
|
+
a clip you have already used**; it costs ten API calls and it deletes the excuse.
|
|
1405
|
+
|
|
1406
|
+
**And note what "easy to find" now means.** Every clipper on this marketplace reads the same CLI, and
|
|
1407
|
+
the CLI shows everybody the same first 200. If you cast from that page you are competing for 200
|
|
1408
|
+
clips while 65% of the shelf sits unread. The sweep is not only how you avoid repeating *yourself* —
|
|
1409
|
+
it is how you avoid arriving at the buyer with the same clip as everyone who did not run it.
|
|
1410
|
+
|
|
1411
|
+
**The number, if you want one:** the same `clip_slug` in more than about **3 of your last 20
|
|
1412
|
+
deliveries** is a signature you did not choose. Either it has the approvals to justify it, or you
|
|
1413
|
+
are casting from memory instead of from the shelf.
|
|
1414
|
+
|
|
1415
|
+
```bash
|
|
1416
|
+
cut -f5 CLIPPER/DELIVERED.tsv | tail -20 | sort | uniq -c | sort -rn | head
|
|
1417
|
+
```
|
|
1418
|
+
|
|
1419
|
+
**No gate checks this and none can.** Gate 1 sees one composition, gate 2 sees one MP4, and neither
|
|
1420
|
+
has ever seen the other videos you sent that buyer. It is a **ledger** rule, not a frame rule: one
|
|
1421
|
+
row per delivery, checked before you cast. The marketplace-wide version, with the file format, is
|
|
1422
|
+
`https://vidfarm.cc/marketplace.md` § *never send one client the same video twice*.
|
|
1423
|
+
|
|
828
1424
|
---
|
|
829
1425
|
|
|
830
1426
|
## Cost-saving mode — the whole build at $0
|
|
@@ -887,8 +1483,56 @@ interchangeable.
|
|
|
887
1483
|
is one ffmpeg command and the caption layer does not change. Do that before you write a second
|
|
888
1484
|
caption — you will learn more from five castings of one joke than from five jokes.
|
|
889
1485
|
|
|
890
|
-
|
|
891
|
-
|
|
1486
|
+
> ⚠️ **Take the five clips from five DIFFERENT verb neighbourhoods, not from the top five of one
|
|
1487
|
+
> query.** This is where a matrix quietly stops being an experiment. The top five of one vector
|
|
1488
|
+
> query are five near-neighbours — often the same performer in four crops — so the five videos
|
|
1489
|
+
> differ by almost nothing and the five data points are one data point with error bars. Pull one
|
|
1490
|
+
> clip per verb from `casting-pool.fresh.tsv` and the five castings actually span the shelf.
|
|
1491
|
+
|
|
1492
|
+
Keep three files: the identity index (Appendix C), a fresh `casting-pool.fresh.tsv` from the sweep,
|
|
1493
|
+
and a `captions.tsv` of rung-3 lines. **The sweep is the only one of the three that goes stale** —
|
|
1494
|
+
re-run it per session, because it is also the file that has your delivery log subtracted from it.
|
|
1495
|
+
The pairing is the creative act; everything downstream is a loop.
|
|
1496
|
+
|
|
1497
|
+
### ⚠️ The matrix is YOUR test set. It is not one client's inbox
|
|
1498
|
+
|
|
1499
|
+
Everything above is a testing discipline for an account you own, where five castings of one joke are
|
|
1500
|
+
five data points and the audience never sees them as a set. **The moment these become proofs on the
|
|
1501
|
+
marketplace, that arithmetic inverts.** A buyer does not see five data points. They open a swipe deck
|
|
1502
|
+
and see the same joke five times from the same name, and the fifth card is not a fifth chance — it is
|
|
1503
|
+
the reason they stop opening your cards.
|
|
1504
|
+
|
|
1505
|
+
> **Vary the clip if you are testing. Vary the JOKE if you are delivering.**
|
|
1506
|
+
|
|
1507
|
+
| | You are testing (your own account, one client's paid campaign, an asked-for A/B set) | You are delivering (proofs to a buyer) |
|
|
1508
|
+
|---|---|---|
|
|
1509
|
+
| One caption × five clips | ✅ the point of the exercise | ❌ one idea in five costumes |
|
|
1510
|
+
| Five pains × one clip | ✅ | ❌ the buyer sees your one clip five times |
|
|
1511
|
+
| What varies between proofs | one variable, deliberately | the **pain**, the **caption**, and the **clip**, all three |
|
|
1512
|
+
| Who sees the set | an algorithm | one person, in one sitting, next to each other |
|
|
1513
|
+
|
|
1514
|
+
**So build the matrix, ship one cell of it.** The matrix is still the right way to work — sit down
|
|
1515
|
+
with the testimonials, write ten pains, cast twenty clips, and pair them. Then send this buyer the
|
|
1516
|
+
one best pairing, and hold the rest for the next brief or the next buyer. The pairings you did not
|
|
1517
|
+
send are not waste; they are your inventory, and they are why the second delivery is fast.
|
|
1518
|
+
|
|
1519
|
+
**This format repeats more invisibly than any other in the set, for two reasons.** The clip is
|
|
1520
|
+
borrowed, so two videos built from the same `slugId` are *literally the same footage* — not similar,
|
|
1521
|
+
identical. And the caption sits in one of only eight frames, so two different pains written in the
|
|
1522
|
+
same frame land as the same joke even when every word differs. `When I …` twice in a row is one
|
|
1523
|
+
voice; `When I …` then `<Entity> watching me …` is two.
|
|
1524
|
+
|
|
1525
|
+
**Log the creative key on every delivery, per client.** Four fields, and no two proofs to one buyer
|
|
1526
|
+
may match on any of them (see Rule 11):
|
|
1527
|
+
|
|
1528
|
+
```
|
|
1529
|
+
client · clip_slug · caption frame · pain
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
`clip_slug` is already in the enactment declaration and the frame and the pain are already in the
|
|
1533
|
+
caption, so logging this costs one line appended to `CLIPPER/DELIVERED.tsv` at submit time.
|
|
1534
|
+
**Read it back before you cast, not before you render** — the clip is the part you cannot edit, and
|
|
1535
|
+
finding the repeat after the composite is a rebuild.
|
|
892
1536
|
|
|
893
1537
|
---
|
|
894
1538
|
|
|
@@ -899,9 +1543,15 @@ different things. **Running only the first one is the same as running none.**
|
|
|
899
1543
|
|
|
900
1544
|
| Gate | Tool | Sees | Catches |
|
|
901
1545
|
|---|---|---|---|
|
|
902
|
-
| 1. Markup | `vidfarm qa <dir> --harness ./experimental/meme-recaption.md` | the composition DOM | duration, aspect, font regime, banned strings, layer counts. **
|
|
903
|
-
| 2. Render | `python3 meme-qa.py <render>.mp4 <composition>.html` (Appendix E) | the finished MP4 | blank thumbnail, empty tail, residual green, caption centring/balance/placement, background not pushed down, silent audio, caption grammar |
|
|
904
|
-
| 3. Human | your eyes | the joke | the three-way lock. **No script will ever check whether it is funny** |
|
|
1546
|
+
| 1. Markup | `vidfarm qa <dir> --harness ./experimental/meme-recaption.md` | the composition DOM | duration, aspect, font regime, banned strings, layer counts. **Zero pixels.** One check per `forbid_text` phrase, so the total moves whenever that list grows — do not quote a number here, it will be wrong within a month |
|
|
1547
|
+
| 2. Render | `python3 meme-qa.py <render>.mp4 <composition>.html` (Appendix E) | the finished MP4 **and the enactment declaration** | blank thumbnail, empty tail, residual green, caption centring/balance/placement, background not pushed down, silent audio, caption grammar, context debt, borrowed lore, and the declared enactment |
|
|
1548
|
+
| 3. Human | your eyes, and a pen | the joke | **the two covers**, written down and compared — then the three-way lock. **No script will ever check whether it is funny** |
|
|
1549
|
+
|
|
1550
|
+
**And one rule sits outside all three, on purpose: Rule 11.** Every gate here sees exactly one video.
|
|
1551
|
+
Whether you have already sent this client that clip, that caption frame or that pain is a fact about
|
|
1552
|
+
your *other* videos, so no check that opens this composition can ever reach it. It lives in
|
|
1553
|
+
`CLIPPER/DELIVERED.tsv` and it is checked **before you cast**, which is also the only moment it is
|
|
1554
|
+
cheap.
|
|
905
1555
|
|
|
906
1556
|
> 🚨 **Gate 1 passes on a broken video.** Every defect this format actually produces is invisible to
|
|
907
1557
|
> a DOM check. During the run that produced this file, a build passed `vidfarm qa` **19/19 while
|
|
@@ -927,7 +1577,8 @@ What gate 2 enforces, and the number it uses:
|
|
|
927
1577
|
| Check | Threshold | The defect it exists for |
|
|
928
1578
|
|---|---|---|
|
|
929
1579
|
| `first_frame_has_subject` / `last_frame_has_subject` | lower-band stdev > 12 | Blank thumbnail / empty tail from un-trimmed plate |
|
|
930
|
-
| `no_residual_green` | < 0.5% green pixels | A key that was too tight |
|
|
1580
|
+
| `no_residual_green` | < 0.5% green pixels | A key that was too tight — a fringe on the shelf's usual `00FF00` plate |
|
|
1581
|
+
| `no_unkeyed_plate` | largest flat saturated colour < 25% of frame, **or** `--flat-bg-ok` | The key never ran, or it ran on a blue/magenta plate the green test is blind to. Pass `--flat-bg-ok` only on ladder tier 6, where a flat brand plate really is the background |
|
|
931
1582
|
| `caption_horizontally_centred` | block centre 50% ± 3% | The flush-left plate with a lake of dead space |
|
|
932
1583
|
| `caption_side_margins` | > 4% and < 96% | A plate touching an edge |
|
|
933
1584
|
| `caption_vertical_placement` | inside 8–85% | Under the platform UI, or on the subject's face |
|
|
@@ -936,7 +1587,7 @@ What gate 2 enforces, and the number it uses:
|
|
|
936
1587
|
| `background_not_blown_out` | top-strip luma < 150 | A bright background eating a white caption |
|
|
937
1588
|
| `audio_level` | −20…−13 LUFS, **or** < −50 with `--silent-ok` | A silent clip shipped as if it had sound |
|
|
938
1589
|
| `caption_word_count` | 8–20 words | Slogan, or unreadable in 0.6s |
|
|
939
|
-
| `caption_uses_a_frame` | one of the
|
|
1590
|
+
| `caption_uses_a_frame` | one of the **eight** frames | A caption that is a statement, not an experience |
|
|
940
1591
|
| `caption_no_cta_or_resolution` | no CTA strings, no `so i` / `then i found` | Rules 1 and 2 — the ad leaking back in |
|
|
941
1592
|
| `caption_lines_hand_broken` | a `<br>` is present | Renderer wrap landing mid-thought |
|
|
942
1593
|
| `offer_named_somewhere` | in the caption **or** in the handle | An untraceable meme that builds someone else's page. Also prints which tier you are on |
|
|
@@ -947,14 +1598,31 @@ What gate 2 enforces, and the number it uses:
|
|
|
947
1598
|
| `handle_is_dimmed` | alpha ≤0.85 *(only if a handle exists)* | Same |
|
|
948
1599
|
| `single_shot` | exactly 1 `<video>` | "There are no beats" |
|
|
949
1600
|
| `no_animation_on_caption` | no `animation` in `.memecap` | Kinetic captions, which break the read order |
|
|
1601
|
+
| `caption_no_context_debt` | none of `iykyk` / `if you know you know` / `you already know` / `part 2` / `as promised` / `like i said` / `see my last` … | Rule 10 — a caption asking the viewer for setup they were never given |
|
|
1602
|
+
| `enactment_declared` | the `<!-- meme-recaption: enactment -->` block, all seven fields, both blind reads ≥5 words and not the same sentence | Rule 3 — the two-cover test not run at all. An undeclared enactment is not a passing enactment |
|
|
1603
|
+
| `enactment_verbs_match` | `caption_verb` == `clip_verb`, both in the ten tokens | Rule 3.1 — vibe-adjacency, declared out loud. The clip does one thing and the caption is about another |
|
|
1604
|
+
| `intensity_mismatched` | `feeling_size` ≠ `clip_size`, both `small`/`large` | Rule 3.2 — a big feeling on a big reaction is a re-enactment; a small one on a small one is nothing |
|
|
1605
|
+
| `clip_read_is_independent` | `clip_reads_blind` shares no content word with the caption (stemmed, ≥4 chars, not a stopword) | Rule 3 — you described the clip with the caption's nouns, so either you never saw it cold or it is a re-enactment. **The gate cannot check that the two reads name one situation** — that stays gate 3 |
|
|
1606
|
+
| `caption_does_not_borrow_lore` | no content token of `clip_slug` (≥4 chars, stemmed, minus the offer's name) appears in the caption | Rule 10, context 3 — a joke that needs the source meme. Also catches the literal case, where the caption names what the clip already *is* |
|
|
950
1607
|
|
|
951
1608
|
**Pass `--offer <domain>`** or the handle checks are skipped silently — an unset offer is not a
|
|
952
1609
|
passing offer. **Pass `--pole win`** only when the caption is deliberately a win meme; the gate
|
|
953
1610
|
defaults to `pain`, the stricter of the two, so nobody accidentally ships a resolution.
|
|
954
1611
|
|
|
955
|
-
**
|
|
956
|
-
|
|
957
|
-
|
|
1612
|
+
> **Amended — gate 2 now reaches into enactment, halfway.** This paragraph used to say gate 2
|
|
1613
|
+
> deliberately does not check *"whether the meme enacts the caption's verb, and the rights tier,
|
|
1614
|
+
> because a script cannot."* Half of that was true and half was an excuse. A script cannot judge a
|
|
1615
|
+
> match; it **can** hold you to a declaration and check the declaration against itself. So the
|
|
1616
|
+
> enactment block is now mandatory and five checks run on it, and the sentence is narrowed:
|
|
1617
|
+
>
|
|
1618
|
+
> **What gate 2 still cannot do** is tell you the declaration is *honest* — that the sentence you
|
|
1619
|
+
> wrote about the clip is what a stranger would actually write. That is gate 3, and it is why the
|
|
1620
|
+
> two covers stay on the human checklist below. **The rights tier is unchanged and remains
|
|
1621
|
+
> unchecked**, in the build log, in writing.
|
|
1622
|
+
>
|
|
1623
|
+
> Do not read the five green enactment lines as "the match is verified". Read them as "the
|
|
1624
|
+
> comparison was performed and its answers do not contradict each other". That is a much smaller
|
|
1625
|
+
> claim, and it is the one the gate is entitled to make.
|
|
958
1626
|
|
|
959
1627
|
**Calibrate a checker before you trust it.** Two of these thresholds were wrong on their first
|
|
960
1628
|
version and fired on a correct build — `caption_lines_balanced` measured pixel rows instead of text
|
|
@@ -969,15 +1637,42 @@ you have already looked at and agree is good, and make it pass before you let it
|
|
|
969
1637
|
|
|
970
1638
|
Run this **before** you build. An unchecked box is a rewrite, not a fix in the edit.
|
|
971
1639
|
|
|
1640
|
+
**The client** — delivery work only. Skip the whole block on an account you own
|
|
1641
|
+
- [ ] `CLIPPER/DELIVERED.tsv` read for this client **before casting**, not before submitting
|
|
1642
|
+
- [ ] `clip_slug` not already delivered to them (Rule 11 — the same slug is the *same footage*)
|
|
1643
|
+
- [ ] Caption frame not already delivered to them — there are only eight
|
|
1644
|
+
- [ ] The pain not already delivered to them, restaged
|
|
1645
|
+
- [ ] If any of the three repeat: the client asked for a series **in writing**, and you can quote it
|
|
1646
|
+
- [ ] **Reusing this clip on a DIFFERENT client?** It has an approval on it, or it is unproven and
|
|
1647
|
+
this is its one outing. Passed on twice with no approval = retired
|
|
1648
|
+
- [ ] This `clip_slug` is not in more than ~3 of your last 20 deliveries
|
|
1649
|
+
- [ ] **The diversity sweep ran this session** and you cast off `casting-pool.fresh.tsv`, not off
|
|
1650
|
+
the CLI's first 200 and not off memory
|
|
1651
|
+
- [ ] The sweep's ranking was **live** — `vidfarm whoami` shows a login, a gemini/openai key is
|
|
1652
|
+
saved, and two different verb queries returned two different top clips. No key = no ranking,
|
|
1653
|
+
silently, with a full page of results to hide it
|
|
1654
|
+
- [ ] The style — plate, font, band, grade, handle — is **identical** to the last delivery. That part
|
|
1655
|
+
is meant to repeat
|
|
1656
|
+
|
|
972
1657
|
**The joke**
|
|
973
1658
|
- [ ] **Pole chosen and written down** — pain or win (default pain)
|
|
974
1659
|
- [ ] If win: the comedy is the size of the relief, not the feature
|
|
975
1660
|
- [ ] `WHO:` and `MOMENT:` are written at the top of the build log
|
|
976
|
-
- [ ] Test 1 — someone
|
|
1661
|
+
- [ ] Test 1 — someone with no knowledge of the offer, your last post, or the source meme laughs
|
|
977
1662
|
- [ ] Test 2 — the experience named is one the product removes
|
|
978
|
-
- [ ] Test 3 — the
|
|
979
|
-
- [ ]
|
|
1663
|
+
- [ ] Test 3 — **the two covers are written down**, not just felt:
|
|
1664
|
+
- [ ] Cover the caption → `clip_reads_blind:` one sentence, what a stranger names in the clip
|
|
1665
|
+
- [ ] Cover the visual → `caption_reads_blind:` one sentence, the situation the caption leaves
|
|
1666
|
+
- [ ] The two sentences are **the same situation**, told twice. Not adjacent. Not compatible
|
|
1667
|
+
- [ ] `caption_verb` == `clip_verb`, both from the ten tokens
|
|
1668
|
+
- [ ] `feeling_size` ≠ `clip_size` — the clip's INTENSITY mismatches the feeling's (that is the joke)
|
|
1669
|
+
- [ ] The clip does the caption's **verb** but does not depict its **nouns** (no re-enactment)
|
|
1670
|
+
- [ ] `clip_reads_blind` reuses **none** of the caption's words (if it does, you projected)
|
|
1671
|
+
- [ ] All seven enactment fields are in `composition.html` as an HTML comment, not a layer
|
|
980
1672
|
- [ ] Cold-viewer test — cover the brand and every feature word; does it still read?
|
|
1673
|
+
- [ ] **Rule 10** — nothing borrowed from the product, your last post, the meme's lore, or your life
|
|
1674
|
+
- [ ] Every specific survived **"who had to have been there?"** — detail kept, context cut
|
|
1675
|
+
- [ ] Context cut, and the freed words **re-spent on detail** (not just deleted)
|
|
981
1676
|
- [ ] The caption is on rung 3: a number, a proper noun, a deflating outcome
|
|
982
1677
|
- [ ] The numbers and proper nouns are traceable to the customer's own copy
|
|
983
1678
|
- [ ] One of the eight frames, unmixed, subject first, 8–20 words, no end punctuation
|
|
@@ -985,6 +1680,8 @@ Run this **before** you build. An unchecked box is a rewrite, not a fix in the e
|
|
|
985
1680
|
|
|
986
1681
|
**The clip**
|
|
987
1682
|
- [ ] Rights tier declared. 🟢 for client work, or a 🟢 alternate is built alongside
|
|
1683
|
+
- [ ] **`plate-check.py` run, and the class it printed is the class you built** — Class A keys and
|
|
1684
|
+
needs a background, Class B is never keyed and needs the burned-in text cropped
|
|
988
1685
|
- [ ] Plate colour sampled, not assumed
|
|
989
1686
|
- [ ] Contact sheet built and looked at
|
|
990
1687
|
- [ ] The action lands 0.6–1.0s in, and frame 0 has the subject on it (`-vframes 1`, looked at)
|
|
@@ -1002,6 +1699,8 @@ Run this **before** you build. An unchecked box is a rewrite, not a fix in the e
|
|
|
1002
1699
|
- [ ] 1080×1920, 30fps
|
|
1003
1700
|
- [ ] The meme is NOT cropped to 9:16
|
|
1004
1701
|
- [ ] Layout A or B chosen deliberately; in B the subject is bottom-anchored and full width
|
|
1702
|
+
- [ ] **Class A: the plate is gone and a background is behind the subject.** No green edge on the
|
|
1703
|
+
hair or shoulders, no green in the gaps between limbs, nothing green in the last frame
|
|
1005
1704
|
- [ ] Caption **block centred**, words left-aligned, lines hand-broken to near-equal length
|
|
1006
1705
|
- [ ] Caption fully legible at frame 0, static, plated, clear of the subject's head
|
|
1007
1706
|
- [ ] Poster layer present, so frame 0 is never bare background
|
|
@@ -1014,8 +1713,14 @@ Run this **before** you build. An unchecked box is a rewrite, not a fix in the e
|
|
|
1014
1713
|
- [ ] Audio kept and normalised, measured on the render — or deliberately silent and said so
|
|
1015
1714
|
- [ ] **Gate 1:** `vidfarm qa <dir> --harness ./experimental/meme-recaption.md`
|
|
1016
1715
|
- [ ] **Gate 2:** `python3 meme-qa.py <render>.mp4 <dir>/composition.html` exits 0
|
|
1017
|
-
- [ ] **Gate 3
|
|
1716
|
+
- [ ] **Gate 3 — the whole-render pass, in this order:**
|
|
1717
|
+
- [ ] Watch it muted with the caption covered. Does the clip still *do something namable*?
|
|
1718
|
+
- [ ] Read the caption alone, out loud, cold. Is it already a complete joke?
|
|
1719
|
+
- [ ] Watch it whole, once, at speed. Did the recognition fire, or did you only *approve* of it?
|
|
1720
|
+
- [ ] Ask the last one honestly: **would a stranger need one question answered?** If yes, Rule 10
|
|
1018
1721
|
- [ ] Post caption written, carrying the one comment ask
|
|
1722
|
+
- [ ] **Delivery work:** one row appended to `CLIPPER/DELIVERED.tsv` — `client · clip_slug · frame ·
|
|
1723
|
+
pain`. An unlogged delivery is the repeat you will ship next week
|
|
1019
1724
|
|
|
1020
1725
|
---
|
|
1021
1726
|
|
|
@@ -1026,11 +1731,18 @@ Diagnose by charge. Do not declare the video bad.
|
|
|
1026
1731
|
| Symptom | The weak charge | The actual cause, in order of likelihood |
|
|
1027
1732
|
|---|---|---|
|
|
1028
1733
|
| Almost no views | Hook | The first three words are generic · the frame-0 image is not weird enough · the caption is too long to read in 0.6s |
|
|
1029
|
-
| Views, everyone leaves at 3s | Loop / enactment | The action landed before the caption was read (Rule 4) · the meme does not enact the verb (
|
|
1734
|
+
| Views, everyone leaves at 3s | Loop / enactment | The action landed before the caption was read (Rule 4) · the meme does not enact the verb (Rule 3, test 3) |
|
|
1735
|
+
| **Watched to the end. Nothing. No leave, no laugh, no share** | **Enactment** | **Vibe-adjacency.** The clip was compatible with the caption instead of being the same event. This is the default cause and it is the one you cannot see, because you read the caption first. Run the two covers on the shipped video — write both sentences down before you look at them together |
|
|
1030
1736
|
| Watched to the end, no reaction | Payoff / specificity | Rung 2. Add the number and the proper noun · or the joke resolves (Rule 2) |
|
|
1737
|
+
| Only your existing followers engaged; no cold reach | **Rule 10** | Context debt. The caption borrowed from your last post, or the product, or the meme's lore. The algorithm did try strangers; the strangers needed a briefing |
|
|
1738
|
+
| It reached people who love the meme, not people with the problem | Rule 10, context 3 | The joke was about the meme, not about their week. Ceiling = the meme's fandom (`caption_does_not_borrow_lore`) |
|
|
1739
|
+
| "True but not funny" · reads like a caption on a stock photo | Rule 3.2 / 3.3 | The sizes matched (a big feeling on a big reaction = re-enactment) · or the clip depicted the caption's nouns instead of its verb |
|
|
1740
|
+
| Nobody in the niche felt seen; it could be about anyone | **Strictness overshot** | You cut context and did not re-spend the words on detail. Rung 1. Go back to Rule 6 and take a sentence a real user said |
|
|
1031
1741
|
| Good watch time, dead comments | Bait | No post caption ask · or the ask is a poll, not an invitation to add their own version |
|
|
1032
1742
|
| Comments are "what is this an ad for" | Rule 1 | The product leaked in — a logo, a screenshot, a category noun in the caption |
|
|
1033
1743
|
| Reach collapsed after 2 hours | Audio / rights | A music-sync meme got muted · a 🔴 clip got claimed |
|
|
1744
|
+
| **The client passed on a video that would have worked on its own** | **Rule 11** | Repetition. They already had this clip, this caption frame, or this pain from you. It is judged against your *last* delivery, not on its own — check `DELIVERED.tsv` for the collision, then change the pain and cast fresh |
|
|
1745
|
+
| The client's approvals dropped off across a batch, from the first card to the last | **Rule 11** | You shipped a row of the matrix instead of one video. Five castings of one joke is a test set, not a delivery |
|
|
1034
1746
|
|
|
1035
1747
|
---
|
|
1036
1748
|
|
|
@@ -1100,9 +1812,19 @@ def frames(path, w, h, vf):
|
|
|
1100
1812
|
n = len(p.stdout) // (w * h * 3)
|
|
1101
1813
|
return np.frombuffer(p.stdout, np.uint8)[:n * w * h * 3].reshape(n, h, w, 3).astype(np.int16)
|
|
1102
1814
|
|
|
1103
|
-
# --- 1.
|
|
1815
|
+
# --- 1. confirm there IS a plate, THEN read its colour off the corner ---
|
|
1816
|
+
# The order is load-bearing. Handed a Class B clip (its own world, no plate),
|
|
1817
|
+
# every line below still runs: it keys against a muddy average of somebody's
|
|
1818
|
+
# living room, step 2 finds a "subject", and the render ships with holes punched
|
|
1819
|
+
# in it. Nothing catches that downstream — no_residual_green passes, because
|
|
1820
|
+
# there was never any green. So refuse here, not at render.
|
|
1104
1821
|
corner = frames(meme, 200, 200, "fps=1,crop=200:200:20:20")
|
|
1822
|
+
flat = corner.reshape(-1, 3).std(0).mean()
|
|
1105
1823
|
plate = corner.reshape(-1, 3).mean(0).astype(int)
|
|
1824
|
+
if flat > 12 or int(plate.max() - plate.min()) < 60:
|
|
1825
|
+
sys.exit(f"corner stdev {flat:.1f}, plate {plate.tolist()} — not a flat saturated "
|
|
1826
|
+
"chroma plate. This is CLASS B: do not key it. Use Layout A and crop "
|
|
1827
|
+
"the burned-in text. (Run plate-check.py for the full read.)")
|
|
1106
1828
|
digital = bool(plate[1] > 240 and plate[0] < 40 and plate[2] < 40)
|
|
1107
1829
|
key_hex = "0x%02X%02X%02X" % tuple(plate)
|
|
1108
1830
|
tol, soft, despill = (0.18, 0.03, False) if digital else (0.30, 0.08, True)
|
|
@@ -1178,11 +1900,21 @@ def probe_key(tol_try, soft_try, despill_try, t):
|
|
|
1178
1900
|
lum = flat.mean(1)
|
|
1179
1901
|
lit = flat[lum > max(lum.max() * 0.75, 1)] # the subject's bright side
|
|
1180
1902
|
green_left = ((flat[:, 1] - flat[:, 0] > 40) & (flat[:, 1] - flat[:, 2] > 40)).mean()
|
|
1181
|
-
|
|
1903
|
+
# A radius that erased the whole subject leaves NO bright pixels, and
|
|
1904
|
+
# `lit.mean()` on an empty slice is nan, not 0. nan then loses every
|
|
1905
|
+
# comparison below in silence: `nan > 0.88` is False, so the radius is
|
|
1906
|
+
# rejected for the right reason by accident — but if the UN-keyed probe
|
|
1907
|
+
# comes back nan too, every `keep` is nan, `best` stays None, and the run
|
|
1908
|
+
# falls through to the sampled default with only a printed line. Return a
|
|
1909
|
+
# real 0.0 so a dead frame scores like a dead frame.
|
|
1910
|
+
return (float(lit.mean()) if lit.size else 0.0), green_left
|
|
1182
1911
|
|
|
1183
1912
|
t_probe = START + DUR / 2
|
|
1184
1913
|
# chromakey's similarity floor is 1e-05, not 0 — a literal 0 is a hard filter error
|
|
1185
1914
|
src_lit, _ = probe_key(1e-5, 0.0, False, t_probe) # ~nothing keyed = the truth
|
|
1915
|
+
if not src_lit:
|
|
1916
|
+
sys.exit(f"the un-keyed probe frame at {t_probe:.2f}s has no bright pixels — "
|
|
1917
|
+
"the clip is dark or the presence window is wrong. Check it by eye")
|
|
1186
1918
|
best = None
|
|
1187
1919
|
for tol_try in (0.34, 0.28, 0.22, 0.18, 0.14, 0.10, 0.07):
|
|
1188
1920
|
r = probe_key(tol_try, soft, despill, t_probe)
|
|
@@ -1246,6 +1978,19 @@ A static, plated card: block centred, words left-aligned, no animation, `data-st
|
|
|
1246
1978
|
This is the exact markup from the worked example, which measures 49.8% block centre.
|
|
1247
1979
|
|
|
1248
1980
|
```html
|
|
1981
|
+
<!-- meme-recaption: enactment
|
|
1982
|
+
Rule 3 + Rule 10, on the record. A COMMENT, never a layer: a layer would count
|
|
1983
|
+
against max_text_cards, would need a data-start, and could render. Gate 2 reads
|
|
1984
|
+
it out of this file, so there is no second file to lose.
|
|
1985
|
+
clip_slug: my-disappointment-is-immeasurable-and-my-day-is-ruined
|
|
1986
|
+
caption_verb: deflate
|
|
1987
|
+
clip_verb: deflate
|
|
1988
|
+
feeling_size: small
|
|
1989
|
+
clip_size: large
|
|
1990
|
+
clip_reads_blind: a man in a suit goes flat and accepts something without arguing
|
|
1991
|
+
caption_reads_blind: someone gives up on dinner after an hour and settles for pizza
|
|
1992
|
+
-->
|
|
1993
|
+
|
|
1249
1994
|
<!-- Layout B: caption on a translucent plate over the background, above the subject.
|
|
1250
1995
|
The WRAPPER is full width and centres; the PLATE is an inline-block that rides
|
|
1251
1996
|
that centre. left:7% would pin the plate to 7% and leave the frame lopsided. -->
|
|
@@ -1318,16 +2063,24 @@ ffmpeg -i media/scene.mp4 -vframes 1 -update 1 media/poster.png # frame 0 of t
|
|
|
1318
2063
|
|
|
1319
2064
|
## Appendix C — build the shelf index once
|
|
1320
2065
|
|
|
1321
|
-
|
|
1322
|
-
|
|
2066
|
+
> **This is the IDENTITY index, and it is the second of the two tools.** `meme-sweep.py` (in
|
|
2067
|
+
> *The diversity sweep*, above) is how you **cast** — it ranks the whole shelf by what the body does
|
|
2068
|
+
> and hands you a fresh pool in ten calls. This appendix builds the full local mirror you **grep**:
|
|
2069
|
+
> for a slug you half-remember, for the rights tier of a name, for the ledger joins in Rule 11, and
|
|
2070
|
+
> for working with no login or no embedding key. Sweep to choose a clip. Index to look one up.
|
|
2071
|
+
|
|
2072
|
+
The shelf pages at 200, the CLI cannot follow the cursor, and the slugs are the index of identity —
|
|
2073
|
+
so pull the whole thing once through the REST route and grep the file instead of re-querying.
|
|
1323
2074
|
|
|
1324
2075
|
```python
|
|
1325
2076
|
#!/usr/bin/env python3
|
|
1326
2077
|
"""meme-index.py — pull every MemeScreens raw into one greppable TSV.
|
|
1327
2078
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
2079
|
+
Note this pages with `q` unset entirely: the `q` filter is a LEXICAL substring
|
|
2080
|
+
match that runs BEFORE ranking, so any query here would silently shrink the
|
|
2081
|
+
mirror. Semantic search is a different parameter (`vector_q`) and lives in
|
|
2082
|
+
meme-sweep.py. The AI descriptions are blind to the meme's identity, so this
|
|
2083
|
+
file is indexed by slug.
|
|
1331
2084
|
|
|
1332
2085
|
Paging note: `vidfarm public-raws` has NO --cursor flag and the server caps a
|
|
1333
2086
|
shelf at 200 per call, so the CLI alone can only ever show you the first 200 of
|
|
@@ -1406,7 +2159,7 @@ MOMENT: the 40 minutes between the craving and giving up on it
|
|
|
1406
2159
|
| | |
|
|
1407
2160
|
|---|---|
|
|
1408
2161
|
| Frame | `When I …` — the complaint is embarrassing, so first person |
|
|
1409
|
-
| Verb cast for |
|
|
2162
|
+
| Verb cast for | `deflate` — shoulders drop, the bad outcome is accepted without a fight |
|
|
1410
2163
|
| Caption | `when i've had 10 tabs open` / `since 7pm for one scallop roll` / `and we're getting pizza again` |
|
|
1411
2164
|
| Clip (🟡) | `my-disappointment-is-immeasurable-and-my-day-is-ruined` · 1280×720 · plate `0x00FF00` digital · key 0.34 |
|
|
1412
2165
|
| Clip (🟢) | `hangover-dog` · 2160×3840 · plate `0x00DA00` filmed · key **0.22** |
|
|
@@ -1425,9 +2178,39 @@ and the object (scallop roll) are the customer's own words, per Rule 6.
|
|
|
1425
2178
|
> the company. **When a real brand name is what makes your caption feel sharp, that is the signal to
|
|
1426
2179
|
> replace it, not to keep it.**
|
|
1427
2180
|
|
|
1428
|
-
**
|
|
1429
|
-
|
|
1430
|
-
|
|
2181
|
+
**The enactment declaration, as shipped** (back-filled onto both builds when the block became
|
|
2182
|
+
mandatory):
|
|
2183
|
+
|
|
2184
|
+
```
|
|
2185
|
+
clip_slug: my-disappointment-is-immeasurable-and-my-day-is-ruined
|
|
2186
|
+
caption_verb: deflate
|
|
2187
|
+
clip_verb: deflate
|
|
2188
|
+
feeling_size: small # one annoying evening
|
|
2189
|
+
clip_size: large # a day that is immeasurably ruined
|
|
2190
|
+
clip_reads_blind: a man in a suit goes flat and accepts something without arguing
|
|
2191
|
+
caption_reads_blind: someone gives up on dinner after an hour and settles for pizza
|
|
2192
|
+
```
|
|
2193
|
+
|
|
2194
|
+
**Read the two blind lines against each other.** They share no vocabulary at all — no "tabs", no
|
|
2195
|
+
"flat", nothing — and they are plainly one situation. That is what a correct pair looks like, and
|
|
2196
|
+
it is why `clip_read_is_independent` bans overlap instead of requiring it: the clip's world (a man
|
|
2197
|
+
in a suit) and the caption's world (dinner, tabs, pizza) are *supposed* to be different. The verb
|
|
2198
|
+
is the only thing they have in common, and the verb is the one thing that is declared rather than
|
|
2199
|
+
described.
|
|
2200
|
+
|
|
2201
|
+
**Sizes:** `small` vs `large`. One annoying evening arriving in the body of a man whose day is
|
|
2202
|
+
immeasurably ruined. Same verb, wrong size — Rule 3.1 and 3.2 at once, and that gap is the laugh.
|
|
2203
|
+
|
|
2204
|
+
**Rule 10, checked field by field:** nothing here needs the product (`10 tabs`, `7pm`, `scallop
|
|
2205
|
+
roll` are the niche's week), nothing needs a previous post, nothing needs the source meme — the
|
|
2206
|
+
caption never says "disappointment", "immeasurable" or "ruined", so `caption_does_not_borrow_lore`
|
|
2207
|
+
passes — and nothing needs to have been *you*: **who had to have been there?** Anyone who has ever
|
|
2208
|
+
tried to pick dinner.
|
|
2209
|
+
|
|
2210
|
+
**Three-way lock:** *standalone* — 10 tabs since 7pm ending in pizza is funny with no context, no
|
|
2211
|
+
previous post and no knowledge of the meme; *problem space* — deleting that hour is the product's
|
|
2212
|
+
entire pitch; *enactment* — both blind reads name one situation, both clips hold a deadpan while
|
|
2213
|
+
something disappointing is already true.
|
|
1431
2214
|
|
|
1432
2215
|
**The background is the joke's world, not the product's.** An empty restaurant at night is the place
|
|
1433
2216
|
they never got to. Blurred so it reads as a set, dark so the plate and the keyed subject both pop.
|
|
@@ -1435,6 +2218,12 @@ they never got to. Blurred so it reads as a set, dark so the plate and the keyed
|
|
|
1435
2218
|
**Gates:** `vidfarm qa` 18/18 · `meme-qa.py` **26/26** on both builds (block centre measured at
|
|
1436
2219
|
**49.8%**). The 🟢 build needs `--silent-ok`, and fails without it — which is the check working.
|
|
1437
2220
|
|
|
2221
|
+
> Those two counts are **historical**, from the run that produced this file. Both gates have grown
|
|
2222
|
+
> since: gate 1 gained five `forbid_text` phrases and gate 2 gained the six Rule 3 / Rule 10
|
|
2223
|
+
> checks. The builds still pass — the declaration above is what was back-filled to make that true —
|
|
2224
|
+
> but do not treat `18/18` or `26/26` as the number you should see. **A check count is not a
|
|
2225
|
+
> measurement of anything; the verdict is.**
|
|
2226
|
+
|
|
1438
2227
|
**Naming the offer — both tiers, built and measured.**
|
|
1439
2228
|
|
|
1440
2229
|
| | **v003 — tier 1** ✅ preferred | **v001 / v002 — tier 2** |
|
|
@@ -1486,14 +2275,23 @@ names the product. All of those pass `vidfarm qa` today.
|
|
|
1486
2275
|
|
|
1487
2276
|
This checks the finished MP4 and the composition text together, and EXITS 1.
|
|
1488
2277
|
|
|
2278
|
+
It also reads the `<!-- meme-recaption: enactment -->` declaration out of the
|
|
2279
|
+
composition (Rule 3, Rule 10). It cannot judge whether the clip enacts the
|
|
2280
|
+
caption — nothing can — but it CAN require that the two-cover test was performed
|
|
2281
|
+
and that its answers do not contradict each other. That is a smaller claim than
|
|
2282
|
+
"the match is verified", and it is the one this script is entitled to make.
|
|
2283
|
+
|
|
1489
2284
|
usage: meme-qa.py <render.mp4> <composition.html> [--offer dishcover.io]
|
|
1490
|
-
[--pole pain|win] [--silent-ok] [--json]
|
|
2285
|
+
[--pole pain|win] [--silent-ok] [--flat-bg-ok] [--json]
|
|
1491
2286
|
"""
|
|
1492
2287
|
import json, re, subprocess, sys
|
|
1493
2288
|
import numpy as np
|
|
1494
2289
|
|
|
1495
2290
|
render, comp = sys.argv[1], sys.argv[2]
|
|
1496
2291
|
SILENT_OK = "--silent-ok" in sys.argv
|
|
2292
|
+
# Ladder tier 6 only: a flat brand plate really is the background. Everywhere
|
|
2293
|
+
# else a slab of one saturated colour is a plate that never got keyed.
|
|
2294
|
+
FLAT_BG_OK = "--flat-bg-ok" in sys.argv
|
|
1497
2295
|
OFFER = (sys.argv[sys.argv.index("--offer") + 1].strip().lower()
|
|
1498
2296
|
if "--offer" in sys.argv else None)
|
|
1499
2297
|
# The pole changes what the payoff is allowed to do (Rule 2). Default to PAIN:
|
|
@@ -1509,6 +2307,55 @@ def check(name, ok, detail, fatal=True):
|
|
|
1509
2307
|
results.append({"check": name, "ok": bool(ok), "detail": detail, "fatal": fatal})
|
|
1510
2308
|
|
|
1511
2309
|
|
|
2310
|
+
# --------------------------------------------- word tools for Rule 3 / Rule 10
|
|
2311
|
+
# These decide FATAL checks, so they are deliberately crude in the direction of
|
|
2312
|
+
# UNDER-firing. A gate that cries wolf on a correct build gets switched off
|
|
2313
|
+
# within a day, which is worse than not having it — see "Calibrate a checker
|
|
2314
|
+
# before you trust it". Concretely: a 4-letter floor (so `cat`, `dog`, `huh` in
|
|
2315
|
+
# a slug are ignored), a stemmer that is a suffix chop and misses give/giving,
|
|
2316
|
+
# and a stopword list that gives up any word that is also ordinary English.
|
|
2317
|
+
STOP = {
|
|
2318
|
+
"that", "this", "with", "when", "what", "your", "have", "from", "they", "them",
|
|
2319
|
+
"their", "there", "then", "than", "into", "just", "been", "being", "will",
|
|
2320
|
+
"would", "could", "should", "about", "over", "under", "some", "more", "most",
|
|
2321
|
+
"very", "really", "going", "gonna", "still", "again", "like", "know", "make",
|
|
2322
|
+
"made", "take", "took", "look", "time", "times", "thing", "things", "something",
|
|
2323
|
+
"someone", "anyone", "everyone", "nothing", "anything", "everything", "without",
|
|
2324
|
+
"while", "after", "before", "because", "which", "where", "here", "does", "much",
|
|
2325
|
+
"many", "other", "another", "same", "each", "every", "both", "since", "getting",
|
|
2326
|
+
"gets", "went", "goes", "want", "wants", "need", "needs", "keep", "keeps",
|
|
2327
|
+
"meme", "clip", "screen", "green", "video", "person", "people", "guy", "girl",
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
|
|
2331
|
+
def stem_word(w):
|
|
2332
|
+
"""A suffix chop, not a linguist. Enough to tie accepts/accepting together.
|
|
2333
|
+
|
|
2334
|
+
NOT called `stem`: section 7b binds a local `stem = OFFER.split(".")[0]` for
|
|
2335
|
+
the offer's name, and a helper called `stem` gets shadowed by it halfway down
|
|
2336
|
+
the file. The first run of this section died exactly there.
|
|
2337
|
+
"""
|
|
2338
|
+
for suf in ("ies", "ing", "ed", "es", "s"):
|
|
2339
|
+
if w.endswith(suf) and len(w) - len(suf) >= 4:
|
|
2340
|
+
return w[: -len(suf)] + ("y" if suf == "ies" else "")
|
|
2341
|
+
return w
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
def content(text):
|
|
2345
|
+
"""Stemmed content words of >=4 LETTERS. Apostrophes and digits are dropped
|
|
2346
|
+
on purpose: `i've`, `we're`, `10` and `7pm` carry no comparison value, and
|
|
2347
|
+
the numbers are the caption's best specifics precisely because they are
|
|
2348
|
+
shared, not private."""
|
|
2349
|
+
out = set()
|
|
2350
|
+
for w in re.findall(r"[a-z]{4,}", text.lower()):
|
|
2351
|
+
if w in STOP:
|
|
2352
|
+
continue
|
|
2353
|
+
s = stem_word(w)
|
|
2354
|
+
if s not in STOP:
|
|
2355
|
+
out.add(s)
|
|
2356
|
+
return out
|
|
2357
|
+
|
|
2358
|
+
|
|
1512
2359
|
def probe(path, vf, w, h, pre=(), post=("-vframes", "1")):
|
|
1513
2360
|
# -ss/-sseof are INPUT options and go before -i; -vframes is an OUTPUT option
|
|
1514
2361
|
# and goes after. Getting that backwards decodes nothing and reports no frame.
|
|
@@ -1543,7 +2390,21 @@ for label, fr in (("first", first), ("last", last)):
|
|
|
1543
2390
|
check(f"{label}_frame_has_subject", band.std() > 12,
|
|
1544
2391
|
f"lower-band stdev {band.std():.1f} (>12)")
|
|
1545
2392
|
|
|
1546
|
-
#
|
|
2393
|
+
# -------------------------------------------- 3. the plate is GONE, in any hue
|
|
2394
|
+
# Two checks, because these are two different defects. `no_residual_green` is a
|
|
2395
|
+
# FRINGE test on the shelf's usual 00FF00 plate — a key that was slightly too
|
|
2396
|
+
# tight. `no_unkeyed_plate` is hue-agnostic and catches the bigger, dumber one:
|
|
2397
|
+
# the key never ran, or it ran on a blue/magenta plate the green test cannot see.
|
|
2398
|
+
#
|
|
2399
|
+
# Measured on four fixtures: an unkeyed 00FF00 plate fails both; a photographic
|
|
2400
|
+
# background passes no_unkeyed_plate at 15%; a flat brand plate fails it at 79%
|
|
2401
|
+
# and needs --flat-bg-ok; an unkeyed MAGENTA plate reads 0.00% green and is
|
|
2402
|
+
# caught ONLY by no_unkeyed_plate. That last row is the whole reason it exists.
|
|
2403
|
+
#
|
|
2404
|
+
# Watch the green check if the joke's world is a lawn, a forest or a pool table:
|
|
2405
|
+
# 0.5% is tight, and a genuinely green BACKGROUND can trip it. That is a false
|
|
2406
|
+
# positive, not a bad key — confirm by eye, then pick a different world rather
|
|
2407
|
+
# than loosening the number.
|
|
1547
2408
|
mid = probe(render, "scale=270:480", 270, 480, pre=("-ss", str(dur / 2)))
|
|
1548
2409
|
if mid is not None:
|
|
1549
2410
|
a = mid[0]
|
|
@@ -1551,6 +2412,23 @@ if mid is not None:
|
|
|
1551
2412
|
green = ((g - r > 45) & (g - b > 45) & (g > 80)).mean()
|
|
1552
2413
|
check("no_residual_green", green < 0.005, f"{green*100:.2f}% green pixels (<0.5%)")
|
|
1553
2414
|
|
|
2415
|
+
# A quarter of the frame in ONE saturated colour is a plate, not a
|
|
2416
|
+
# background: a background is a photograph, and photographs vary. The
|
|
2417
|
+
# caption plate is white or black, so it is not saturated and not counted.
|
|
2418
|
+
# Ladder tier 6 (a flat brand plate as the background) is a legitimate
|
|
2419
|
+
# exception and the only one — pass --flat-bg-ok, the same way --silent-ok
|
|
2420
|
+
# works, so the exception is a decision somebody made rather than a default.
|
|
2421
|
+
sat_px = (a.max(2) - a.min(2)) > 60
|
|
2422
|
+
if sat_px.any():
|
|
2423
|
+
q = a[sat_px] // 32 # 8^3 colour buckets
|
|
2424
|
+
keys = q[:, 0] * 64 + q[:, 1] * 8 + q[:, 2]
|
|
2425
|
+
biggest = float(np.bincount(keys).max()) / a[..., 0].size
|
|
2426
|
+
else:
|
|
2427
|
+
biggest = 0.0
|
|
2428
|
+
check("no_unkeyed_plate", FLAT_BG_OK or biggest < 0.25,
|
|
2429
|
+
f"largest flat saturated colour = {biggest*100:.0f}% of frame (<25%)"
|
|
2430
|
+
+ (" [--flat-bg-ok]" if FLAT_BG_OK else ""))
|
|
2431
|
+
|
|
1554
2432
|
# ------------------------------------- 4. caption block is horizontally CENTRED
|
|
1555
2433
|
# Find the caption plate in frame 0 by its own darkness against the background,
|
|
1556
2434
|
# then compare its horizontal centre to the canvas centre.
|
|
@@ -1647,7 +2525,7 @@ else:
|
|
|
1647
2525
|
r"^my \w+ when\b", r"^why my\b",
|
|
1648
2526
|
r"^[\w][\w'.\- ]{0,24} (when|with|after|watching|reading|seeing|hearing) ")
|
|
1649
2527
|
check("caption_uses_a_frame", any(re.search(f, text_cap, re.I) for f in FRAMES),
|
|
1650
|
-
f"must open with one of the
|
|
2528
|
+
f"must open with one of the eight frames — got {' '.join(words[:4])!r}")
|
|
1651
2529
|
check("caption_is_lowercase_start", text_cap[:1].islower(),
|
|
1652
2530
|
"meme captions are typed, not headlined", fatal=False)
|
|
1653
2531
|
# the product must not be in the video (Rule 1), and it must not resolve (Rule 2)
|
|
@@ -1659,6 +2537,19 @@ else:
|
|
|
1659
2537
|
# hand-broken lines are required — the renderer's own wrap lands mid-thought
|
|
1660
2538
|
check("caption_lines_hand_broken", "<br" in cap.group(1),
|
|
1661
2539
|
"use <br> at the clause; never let the renderer wrap a meme caption")
|
|
2540
|
+
# Rule 10 — CONTEXT DEBT. Each of these asks the viewer for something they were
|
|
2541
|
+
# never given: a previous post, the source meme's lore, or a shared secret. The
|
|
2542
|
+
# platform serves this video cold to strangers, on purpose, so the debt is never
|
|
2543
|
+
# repaid — it just quietly costs you everyone who is not already following you.
|
|
2544
|
+
# "again" is deliberately NOT here: `and we're getting pizza again` is the
|
|
2545
|
+
# collapse of the worked example, and it borrows nothing.
|
|
2546
|
+
DEBT = ("iykyk", "if you know you know", "you know the one", "you already know",
|
|
2547
|
+
"part 2", "part two", "part 3", "part three", "as promised",
|
|
2548
|
+
"like i said", "as i said", "as mentioned", "see my last",
|
|
2549
|
+
"from my last", "same as last time", "remember when i said")
|
|
2550
|
+
dhit = [d for d in DEBT if d in text_cap.lower()]
|
|
2551
|
+
check("caption_no_context_debt", not dhit,
|
|
2552
|
+
f"context debt {dhit} — the viewer has no previous slide and no briefing (Rule 10)")
|
|
1662
2553
|
|
|
1663
2554
|
# ------------------------------------------------- 7b. naming the offer
|
|
1664
2555
|
# The offer IS named on the video. Two ways, in priority order:
|
|
@@ -1734,6 +2625,80 @@ if OFFER:
|
|
|
1734
2625
|
check("handle_is_dimmed", bool(dim) and float(dim.group(1)) <= 0.85,
|
|
1735
2626
|
f"handle alpha {dim.group(1) if dim else 'opaque'} (<=0.85)", fatal=False)
|
|
1736
2627
|
|
|
2628
|
+
# ------------------------------------- 7c. the enactment declaration (Rule 3)
|
|
2629
|
+
# The two-cover test is a human act, and no script will ever judge whether a clip
|
|
2630
|
+
# enacts a caption. What a script CAN do is refuse a build where the test was
|
|
2631
|
+
# never performed, or where its own answers contradict each other. So the answers
|
|
2632
|
+
# live in the composition as an HTML COMMENT — a comment and not a layer, because
|
|
2633
|
+
# a layer counts against max_text_cards and could render, and this must never
|
|
2634
|
+
# reach a pixel.
|
|
2635
|
+
VERBS = {"deflate", "blank", "loop", "smug", "overreact",
|
|
2636
|
+
"correct", "unbothered", "celebrate", "stare", "wait"}
|
|
2637
|
+
FIELDS = ("clip_slug", "caption_verb", "clip_verb", "feeling_size", "clip_size",
|
|
2638
|
+
"clip_reads_blind", "caption_reads_blind")
|
|
2639
|
+
|
|
2640
|
+
decl_m = re.search(r"<!--\s*meme-recaption:\s*enactment\b(.*?)-->", html, re.S)
|
|
2641
|
+
decl = {}
|
|
2642
|
+
if decl_m:
|
|
2643
|
+
for line in decl_m.group(1).splitlines():
|
|
2644
|
+
kv = re.match(r"\s*([a-z_]+)\s*:\s*(.+?)\s*$", line)
|
|
2645
|
+
if kv and kv.group(1) in FIELDS:
|
|
2646
|
+
decl[kv.group(1)] = re.sub(r"\s*#.*$", "", kv.group(2)).strip()
|
|
2647
|
+
|
|
2648
|
+
missing = [f for f in FIELDS if not decl.get(f)]
|
|
2649
|
+
short = [f for f in ("clip_reads_blind", "caption_reads_blind")
|
|
2650
|
+
if len(decl.get(f, "").split()) < 5]
|
|
2651
|
+
same = (decl.get("clip_reads_blind", "x").lower() ==
|
|
2652
|
+
decl.get("caption_reads_blind", "y").lower())
|
|
2653
|
+
check("enactment_declared",
|
|
2654
|
+
bool(decl_m) and not missing and not short and not same,
|
|
2655
|
+
("no <!-- meme-recaption: enactment --> block — run the two covers and write them down"
|
|
2656
|
+
if not decl_m else
|
|
2657
|
+
f"missing {missing}" if missing else
|
|
2658
|
+
f"{short} must be a real sentence (>=5 words)" if short else
|
|
2659
|
+
"the two blind reads are the same sentence — you wrote one and pasted it twice"
|
|
2660
|
+
if same else "7 fields, both reads written"))
|
|
2661
|
+
|
|
2662
|
+
if decl_m and not missing:
|
|
2663
|
+
cv, kv_ = decl["caption_verb"].lower(), decl["clip_verb"].lower()
|
|
2664
|
+
# Rule 3.1. Two different tokens is a mismatch you declared out loud; a token
|
|
2665
|
+
# outside the ten is a verb nobody can compare, which is the same as no verb.
|
|
2666
|
+
check("enactment_verbs_match",
|
|
2667
|
+
cv in VERBS and kv_ in VERBS and cv == kv_,
|
|
2668
|
+
f"caption_verb={cv!r} clip_verb={kv_!r} — must be the SAME token from {sorted(VERBS)}")
|
|
2669
|
+
# Rule 3.2. Two coarse buckets on purpose: a 1-5 scale invites a 3-vs-4 that
|
|
2670
|
+
# means nothing, and the only question the format asks is binary — is the
|
|
2671
|
+
# reaction the wrong size for the cause?
|
|
2672
|
+
fs, cs = decl["feeling_size"].lower(), decl["clip_size"].lower()
|
|
2673
|
+
check("intensity_mismatched",
|
|
2674
|
+
{fs, cs} == {"small", "large"},
|
|
2675
|
+
f"feeling_size={fs!r} clip_size={cs!r} — must be small/large and DIFFERENT "
|
|
2676
|
+
f"(matched sizes are a re-enactment, not a joke)")
|
|
2677
|
+
# Rule 3.3 + the read-order problem. If the only way you can describe the clip
|
|
2678
|
+
# is with the caption's own nouns, either you never saw it cold (you projected
|
|
2679
|
+
# the caption onto it) or the clip really does depict the caption's nouns,
|
|
2680
|
+
# which is a re-enactment. A correct pair shares NO vocabulary — the clip's
|
|
2681
|
+
# world and the caption's world are supposed to be different. This is why the
|
|
2682
|
+
# check bans overlap instead of requiring it: an earlier draft required the two
|
|
2683
|
+
# reads to share a word, and every correct build failed.
|
|
2684
|
+
if cap:
|
|
2685
|
+
bleed = sorted(content(decl["clip_reads_blind"]) & content(text_cap))
|
|
2686
|
+
check("clip_read_is_independent", not bleed,
|
|
2687
|
+
f"clip_reads_blind reuses the caption's words {bleed} — describe the clip, "
|
|
2688
|
+
f"not your plan for it")
|
|
2689
|
+
# Rule 10, context 3 — the source meme's own lore. A word that comes from the
|
|
2690
|
+
# meme's NAME comes from the meme's fandom, not from the audience's week, and
|
|
2691
|
+
# it caps reach at people who recognise the clip. It also catches the literal
|
|
2692
|
+
# case, where the caption names what the clip already is.
|
|
2693
|
+
if cap:
|
|
2694
|
+
offer_stem = OFFER.split(".")[0].lower() if OFFER else ""
|
|
2695
|
+
slug_words = content(decl["clip_slug"].replace("-", " ").replace("_", " "))
|
|
2696
|
+
slug_words.discard(stem_word(offer_stem))
|
|
2697
|
+
lore = sorted(slug_words & content(text_cap))
|
|
2698
|
+
check("caption_does_not_borrow_lore", not lore,
|
|
2699
|
+
f"caption reuses slug words {lore} — the clip is borrowed equipment, "
|
|
2700
|
+
f"not a shared reference (Rule 10)")
|
|
2701
|
+
|
|
1737
2702
|
# --------------------------------------------- 8. one shot, no brand chrome
|
|
1738
2703
|
check("single_shot", len(re.findall(r"<video\b", html)) == 1,
|
|
1739
2704
|
f"{len(re.findall(r'<video', html))} video layer(s) — a meme recaption is ONE shot")
|