Repository navigation
Two modalities were missing or only half there. 3D had no skill at all, and
model-maker now returns a GLB from text or a photo. Video could illustrate a
voice but not direct it: reel-builder's film mode now records the take, draws a
storyboard, designs every camera move as a clay previs in Blender and fits the
voice back to the lips, all before or after the one paid step. Around them, two
gates that reported green without looking now look, thirteen prices no longer
understate the bill, and writer catches the props a book edit kept putting back.
Changed — film mode directs instead of illustrating
The first paid film had held seams and timings but no direction. It was a run of centred, static medium shots in grey, even light, with a picture that repeated every line. Its lips were also seconds off the take, and the cause was the prompt, not the model. Seedance re-performs the audio reference through a mouth on screen, and our prompt put lines over shots with no face in them. BytePlus's own 2.5 guide documents the fixes, and the chain now follows it.
- Director system prompt rewritten for the official 2.5 format. It writes a shot list with integer-second ranges and dual-contrast hard cuts. Lines go in verbatim in
{}, either on camera or(V.O.)with "his lips stay closed". The prompt also carries[Overall style], an audio policy and[Strictly exclude], and runs 350–700 words where it used to be capped at 330. The dramaturgy rules add counterpoint over illustration, a colour and light arc, acting through physical action, and a poster frame. reel_boardCLI: a storyboard before the video. Nano Banana Pro renders one keyframe per shot through OpenRouter (new provideropenrouter-nano-banana-pro, about $0.14 a frame). A Gemini critic scores each frame against its shot, and frames below 8/10 are re-rolled with the critic's fix. Approved frames go to Seedance as ordered keyframe references.reel_previsCLI. The camera is designed as a clay previs, and Blender renders it headless (common/runners/previs.pyholds the spec, checks and orbit expansion;common/runners/blender/previs_scene.pybuilds the scene). Each shot gets its clip as@Video1. On the trial shot Seedance followed the previs frame for frame, and motion went from 0.2–5 to 8–12. OpenRouter video references go up throughmedia_host(audio_hoststill works).reel --redo <labels>regenerates individual shots.- One generation per shot, the new default. The keyframed one-generation film measured as a slideshow, with near-zero motion for seconds at a time. Each shot is now generated from its keyframe with handles. The runner cuts shots frame-exact (
trim_start/trim_seconds), andlip_syncshots get their own slice of the master. New rules: every shot moves, cuts land on action, and the ending stays in a known space. reel_conformCLI: the salvage step for a picture that is already paid for. ElevenLabs speech-to-text reads what the picture's mouth says, and each phrase of the master take is moved there, with stretching capped at ±15%. On the trial film the median voice-to-lip offset dropped from 1.13 s to 0.25 s.- OpenRouter audio references go up through a one-hour Litterbox link, and only when the plan sets
audio_host: "litterbox", because OpenRouter refuses inline audio.
Added — film mode: voiced Seedance films that hold together at the seams
A voiced Seedance film came out with the picture jumping at the seams, the voice
breaking mid-word, events missing their lines, and about twenty staged events
in thirty seconds. Each of those has a cause in how the film was made, and each
cause now has a component.
reel_voiceCLI (python3 -m common.runners.cli.reel_voice): script →
ElevenLabs take with character timestamps (/with-timestamps, falling back
to/v1/forced-alignment, or--voice-filefor an existing take) → master
track on the film clock → cut plan →timing.json. Cuts are chosen by an
exact shortest-path over the pauses (common/runners/voice_timing.py):
never mid-word, never past the model's limit, no runt segment at the end.- Seedance providers:
seedance-2-5andseedance-2-0(BytePlus ModelArk,
ARK_API_KEY),fal-seedance-2-5(FAL_KEY). One kwarg contract on all
three, and reference order is preserved on the wire, because@Image1and
@Audio1are positional. 2.5 renders 4-30 s, so a ≤30 s film is one
generation with no seams. - Chained segments in
reel:continue_from_previoushands each segment
the frame its predecessor was cut on (as the last@Image, orfirst_frame
on BytePlus).trim_secondscuts frame-exact, andvoiceoverlays the one
master take over the join. Seedance's re-voiced audio is dropped. - Film system prompt (
common/video-prompt-library/film-system-prompt.md):
dramaturgy before prompts. It enforces one controlling image, at most one
event per 4 s, and a quiet turn. Hits land on word starts, speech goes only
on visible faces, and each prompt has role lines per reference, a timecoded
timeline, no quoted dialogue and no meta-commentary. - Designed soundtrack (
common/runners/soundtrack.py,reel_soundCLI,
eleven-sfxprovider): Seedance's audio is discarded. The chain writes music
cues (Eleven Music composition plans), effects and ambience beds
(/v1/sound-generation) plus their placements on the film clock. Kills,
dips and "distant" re-use are edits of one cue, not separate generations.
Effects markedsnapmove to the brightness jump where the rendered flash
actually is. The mix ducks music under the voice and is mastered to
−14 LUFS / −1 dBTP.reel_soundrenders it before the picture is paid for. reel-builder/references/film-mode.md: the pipeline, why each rule exists,
and a worked rewrite of a 1,800-word prompt into a 330-word one.
Fixed
- Reel music went silent half a second in.
MixOptions.music_filter
emittedafade=t=out:st=0, a fade-out starting at zero. Every reel and every
mixrun since the option-object refactor faded its music out at 0.5 s, and
the tests pinned the string. The fade now startsfade_outseconds before
the video ends, and is omitted when the end is unknown.
Changed
video-promptSeedance reference: 2.5 added, and the audio claims corrected
(native audio plus lip-sync to an@Audioreference, not "unconfirmed").
The--modellist now namesseedance-2-5/seedance-2-0instead of
seedance-1-pro.
The roadmap asked the next session to audit the structure. The audit found that
two of the nine documentation gates had never run, and that the numbers the
skills quote for what a run costs had drifted from the table that bills for it.
Added — writer learns the props a book edit kept reintroducing
A full proofread of a Russian non-fiction manuscript turned up more than a
hundred constructions the linter passed as clean, and most of them had been
inserted by the editor itself while fixing other hits. They share a shape: the
phrase announces a thought instead of stating it.
- 29. PROP_TURN — announcing props («самое интересное», «ирония в том, что»,
«откуда его меньше всего ждали», «стоит запомнить простую вещь», «и в этом весь
смысл»), bodily reactions used as drama («сказал вещь, от которой вздрогнули»),
figurative «держится на / держит на себе / удержаться от соблазна», and the
abstract middleman «разрыв между X и Y». - 30. ORDINAL_SCAFFOLD — a chapter read as a report: «Вторая сцена. … Третья
сцена», «Второй слой … Седьмой слой», «Дальше --- права».
Categories 26–28 (PERCEPT_METAPHOR, SYNTH_WORD, ANGLICISM) and 56 further
patterns had been living only in a local install and never reached the repo;
they are ported here with their catalogue sections. Six snapshots drift, each by
a true positive the old list missed («через призму», «как водится», «скилл»).
Fixed — two gates that reported green without looking at anything
check-docs-consistency.sh check 2 globbed */ at the repo root for
directories containing a SKILL.md. Nothing at the root has one; they all live
under skills/. The loop iterated an empty list and passed, every commit since
the move.
Check 5 had the same shape: it asked git for added */SKILL.md paths, then
discarded every result containing a slash as "nested". After the move that is
all of them, so post-publisher and schema-maker both shipped without the
gate ever opening the CHANGELOG.
Both now assert their scan found something. A gate that scans nothing prints the
same green as a gate that scans everything, which is the only reason these
survived two years of releases.
Fixed — thirteen prices that understated the bill
common/references/model-pricing.md is generated from cost.PRICE_TABLE and so
cannot lie. The skills quote prices somewhere else entirely — in decision tables,
worked examples, and the sentence that says whether a batch trips the budget —
and 153 such claims were hand-written. Thirteen were wrong:
nano-banana-proadvertised at $0.05 an image instyle-transfer, billed at
$0.134.thumbnail-makerquoted a nine-image batch at $0.45. It costs $1.21 — 2.7x,
and the doc was what a user read before deciding to run it.gif-makerpriced six video models per clip against a per-second table.logo-maker's $/image table had three of five rows wrong.lyria-3-clipat $0.05,eleven-ttsat $0.15 per 1000 characters, both stale.
Added — scripts/check-prices.py, gate 8
A line naming a model and quoting a dollar figure is a claim about that model.
It has to resolve to the unit price, or to the unit price times a batch size the
file declares with <!-- prices: batch=8 -->. Undeclared batch totals fail,
which is what makes the check worth running: $0.45 and $1.21 are both
plausible-looking numbers, and only one of them is nine times $0.134.
Fractional batches (batch=0.5) cover per-minute models quoted for a 30-second
clip. ignore exists for pipeline totals that span three skills, and takes a
reason after an em dash.
Added — docs/skill-descriptions.lock.json, gate 9
Every skill describes itself twice: SKILL.md frontmatter is the routing
contract Claude reads, the skills.json blurb is what a person browsing the
README sees. All 43 pairs are worded independently and should stay that way —
forcing them identical would make both worse.
What must not happen is the catalog going quietly stale, which is how
skills.json came to advertise "17 Claude Code skills" against 42 on disk. The
lock records the description each blurb was written against; when that text
moves, the failure prints the old wording, the new wording, and the blurb that
now contradicts it, so re-freezing is a decision rather than a keystroke.
Added — docs/skills.schema.json
Line 2 of skills.json has always pointed at this file. It 404'd. Nothing
validated the manifest, which is how a deps entry came to hold a directory
path and how layer and tags grew their vocabularies by accident. The schema
is now real, and test_skills_manifest.py validates against it with a
stdlib-only checker — depending on jsonschema would mean skipping in CI, and a
gate that skips is the thing this release removed.
Added — a dated marker on the distribution table
docs/distribution.md closed by predicting that its hand-maintained status
column would rot and prescribing the fix. It rotted first: the npm row read
"published" while the registry sat a release behind, because the row recorded
that make release had been wired to make publish-npm, not that an upload had
happened. Statuses now come from a four-word vocabulary, routes must name live
make targets, and the review goes stale after 90 days.
Added — scripts/check-cli-docs.py, gate 10
Nine maker modules built their argparse parser inside the call that consumed it,
so the only way to learn which flags they accepted was to run them. Every CLI
module now exposes a zero-arg build_parser(), and the gate checks the 88
literal python3 -m common.runners.cli.X --flag commands in the docs against
the real parsers. Skill-level flags (/logo-maker --variants 4) are conventions
Claude reads, not CLI arguments, and are left alone.
Nothing had drifted. The class had simply never been checkable.
Decided — the eight single-image makers stay separate
The roadmap twice suspected flyer-maker, cover-maker, thumbnail-maker,
avatar-maker, logo-maker, quote-card-maker, banner-maker and
meme-card-maker of being one skill wearing eight hats. Measured, their
same-named reference files share 4–19% of their text, and 656 bytes of prose is
duplicated verbatim across three or more of them. The pipeline they share is
already shared, in _maker.py.
Merging would trade eight precise description: triggers for one vague enough
to match all eight — and description: is the whole of how Claude finds a skill.
The rule is written up in CONTRIBUTING: split on routing, merge on defaults,
extract shared data into common/.
Two things fixed on the way past: the "prices below are illustrative" disclaimer
in ten model-picker.md files, which gate 8 had made false, and
make new-skill name=… in CONTRIBUTING, which is NAME= and had been failing
for anyone following the onboarding.
Added — publisher contract tests, and a correction about skill size
Both ElevenLabs bugs were field-name mistakes, and those are invisible to a test
that checks the value it just inserted: body["duration_secs"] == 30 passes
whether or not the vendor has heard of duration_secs. The new tests pin the
vocabulary instead — every key each article publisher sends, against the field
list in the vendor's own documentation, cited with the date it was checked.
Renaming body_markdown to body_md fails three of them. What they still
cannot prove is that the vendor accepts that vocabulary; that needs a recorded
fixture and a key, and stays on the roadmap.
The roadmap also asked whether writer/SKILL.md at ~27 KB should move its body
into references/. 27 KB is its size on disk, where 59% of the file is Cyrillic
and each such character costs two UTF-8 bytes. In characters it is 16,633 —
smaller than carousel-builder — and its longest section is an index of 25
category names pointing at the catalogue rather than a copy of it. Nothing to
move. tests/unit/test_skill_size.py holds the line for the next skill instead:
400 lines maximum, and a references/ link required past 10,000 characters.
Changed — proposal_*.py became common/runners/proposal/
Six modules, 1,747 lines, one skill's implementation occupying a fifth of the
runner's top level — reading the layout told you more about proposal-maker
than about the runner. Now a package, beside providers/, publishers/,
cli/ and storage/. Top level: 31 modules → 25.
Import paths change: common.runners.proposal_parse → common.runners.proposal.parse.
The move was not free, which is the argument for having made it deliberately.
Two lazy imports inside kit.py — from . import config, keysfile and
from .providers.base import JobHandle — silently changed meaning, and both sit
inside a bare except Exception that returns False. The second was caught by
a test written because that swallow makes mistakes invisible; the first had no
test and would have disabled photo generation without printing anything.
styles* and typography* stay flat: shared infrastructure rather than one
skill's guts, with styles.py already acting as a facade over the other two.
Fixed — every Ideogram v3 tier was priced under what the vendor charges
ideogram-3-turbo was carrying $0.02 against a published $0.03, ideogram-3
$0.04 against $0.06, ideogram-3-quality $0.08 against $0.09. cost.py says of
itself that actual cost "may come in slightly lower; it should never come in
higher" — this was the one direction it is not allowed to be wrong in, on the
default path of logo-maker, whose whole premise is Ideogram's typography.
Re-read from ideogram.ai/api-pricing on 2026-08-08 and corrected. Gate 8 then
pulled twenty-six documented figures along with it, which is what it is for.
Added — Ideogram 4
The endpoint the roadmap was waiting for exists: ideogram-4-turbo /
ideogram-4 / ideogram-4-quality at $0.03 / $0.06 / $0.10, posting to
/v1/ideogram-v4/generate with rendering_speed ∈ {TURBO, DEFAULT, QUALITY}.
The prompt field is text_prompt, where v3 takes prompt. Both the path and
the field name are pinned in tests, because the failure is quiet: a v4 slug sent
to the v3 path returns a perfectly good image, from the model nobody asked for,
billed at the other rate. rendering_speed=FLASH returns 400 on v4 today, so
ideogram-3-flash stays the cheapest tier and stays on v3.
Defaults did not move. The pickers name v4 and say when to prefer it; flipping
logo-maker to it would change what every existing user pays and gets, and that
needs a side-by-side generation to justify, not a changelog entry. The other half
of the roadmap's Ideogram item — sending json_prompt instead of flattening
layout into prose — is untouched and is the larger piece.
Fixed — the fal router would have billed for seventeen layers and returned one
bytedance/seedream/v5/pro/layerize went live on fal on 2026-07-08: a poster
decomposes into 2-17 transparent PNGs, each with a z-index and a bounding box,
billed at $0.03375 per layer. --fal-model would have reached it today, and
GenerationResult carries exactly one asset — so the router would have taken
images[0], returned normally, and said nothing about the other sixteen.
It now refuses a multi-asset response and names the count. Returning a set is a
real feature: GenerationResult has to grow one, and batch.py, output.py and
the maker CLIs all assume one file per item. Until then, failing costs the user
exactly what succeeding wrongly costs, and tells them why.
Checked — the other three pending vendor items
FLUX 3 is still application-gated with no public API and no pricing. Muse Image
still runs only inside Meta's apps; the Meta Model API that did open is for Muse
Spark 1.1, a reasoning model, and is a different product. Hashnode comes off the
watch list unchanged: the Pro requirement hardened rather than lifted, and
publishers/hashnode.py already fails preflight with that reason.
Added — a call can return a set of files, and Seedream's layers arrive whole
GenerationResult grew companions. It is a suffix, not a rewrite: content is
still the one asset every provider has always returned, the field is empty for
all but one model, and a provider that returns a single file is untouched.
bytedance/seedream/v5/pro/layerize is that model — 2-17 transparent PNGs from
one image, each with a z-index, a bounding box and a name. output.save_result()
writes them ordered and named after themselves (-01-subject.png, not -01.png),
batch.py records them in the manifest, and --resume no longer re-buys a layer
set to recover a filename it already had.
The estimate quotes the 17-layer ceiling, because layerize bills per layer
produced and the count is not knowable before the call. cost.py may come in
over the receipt; it may never come in under.
No skill reaches for layerize on its own yet. The payoff the roadmap argued for —
a typo becoming a layer edit instead of a full regeneration, which is the main
cause of style drift across a carousel — needs a retry-loop change, not a
provider one.
Added — structured layout prompts, the other half of the Ideogram item
Ideogram 4 accepts json_prompt: layout as structure — headline here, image
there — instead of a sentence the model has to re-infer an arrangement from.
That re-inference is why two runs of the same prose prompt put the headline in
two places, and why an aspect set stops looking like one thing.
The provider sends json_prompt or the prose field, never both, because the API
documents them as mutually exclusive. On a v3 tier it refuses: v3 has no such
field, so the call would succeed and return a picture built from nothing the
caller specified, which is worse than an error.
The contract is written once, in common/references/json-prompt.md;
banner-maker, flyer-maker and logo-maker each point at it. They already
held the structure internally — this is them no longer flattening it on the way
out.
Fixed — validate.sh reported a correct link as broken
The cross-link check grepped for a path shape: one optional ../, then
references/. A correct ../../common/references/json-prompt.md matched only
its tail and was reported broken as ../common/…, while a path merely mentioned
in prose was checked as though it were a link. It now extracts markdown link
targets and asks the filesystem, which also means it covers links that are not
under references/ at all — and it reports how many it checked, so "no links"
and "every link resolves" stop printing the same thing.
Added — the AEO linter learned the two rules the 2026 measurements added
The extractability rules were built on research that held the words constant and
changed only structure. The 2026 follow-up measured two things that set of rules
did not cover.
Evidence. Citing sources scored +40% visibility, adding statistics +41%,
quotations +28%, and they compound. lint_aeo now reports which of the three a
page shows and warns below two — a page evidencing none of them is asking to be
believed rather than cited.
Per-section answers. The page-level answer only wins the query the title
matches; a section long enough to be retrieved on its own is scored on its own
first sentences. Sections past 120 words that open with setup are flagged.
Not added: freshness. 83% of AI citations now come from pages updated inside
twelve months, and pages not refreshed quarterly are 3x more likely to lose them
— but that needs a date the linter cannot see from the text.
The number that moved most is not a rule at all. In mid-2025 roughly 76% of AI
Overview citations were also top-ten organic results; by early 2026 Ahrefs put
that near 38% and BrightEdge as low as 17%. Ranking no longer buys the citation.
lint_aeo.py passed the module-size gate on the way in, so the thresholds,
patterns and parser moved to lint_aeo_core.py.
Checked — where this collection stands against the ecosystem
npx skills (Vercel Labs, launched 2026-01-20) became the ecosystem's default
installer: GitHub as registry, 40+ agent platforms, skills.sh as the public
index. npx skills add Mikefluff/skills --list finds all 43 — the skills/
layout already matched, by accident rather than intent. Now written down in
distribution.md, both as a compatibility property not to break and as a listing
that was never submitted.
Document generation comes off the roadmap and onto the non-goals: Claude ships
native pptx / xlsx / docx / pdf skills, free to every account since
2026-02-11. Voice cloning joins it — the APIs exist, the consent workflow this
collection could enforce does not. 3D stays open and is the one real modality
gap: Tripo, Meshy and Rodin all have APIs, and a model-maker would reuse the
existing plan/batch/receipt path rather than needing a new one.
Added — model-maker, and a fifth modality to hold it
3D was the one real gap the ecosystem check turned up, so it is filled: text or
a reference photo in, a GLB mesh out, via Tripo v3.
Modality grew model. That is the honest shape — a mesh is not an image with
extra steps, it lands in ./generated/model/, and cost.py bills it per model
rather than per second or per image. cli/_shared.py turned out to be entirely
modality-agnostic already, so the CLI is a 25-line declaration like image.py.
Two things the provider does that the others do not:
- It downloads inside the poll. A finished Tripo URL expires five minutes
after the task succeeds, and the asset is already billed. Handing back a link
and fetching later loses paid work on any slow path — a batch queue, a retry,
a user reading the output before acting on it. - It quotes the ceiling. Text-to-model runs 10 to 40 credits by tier and the
tier is chosen per call, so the estimate says $0.40. The receipt may come in
lower; it may not come in higher.
The skill is deliberately blunt about what a generated mesh is not. Topology is
triangles without edge loops — a prop or a sculpting base, not something to rig —
and nothing checks watertightness, so it is not print-ready until a repair pass
says so. Both are in references/limits.md rather than discovered in Blender.
Fixed — the pre-commit hook had not linted anything since the skills/ move
scripts/install-precommit-hook.sh calls the writer linter at
$ROOT/writer/scripts/lint.py. It has lived at skills/writer/scripts/lint.py
since the move, and the call is wrapped in 2>/dev/null || echo "clean pass" —
so every commit since then got a fabricated clean verdict from a script that was
never found.
That is the fourth gate this release found passing without looking: two in
check-docs-consistency.sh, one in validate.sh's link check, and this. The
common shape is a fallback that cannot tell "nothing wrong" from "nothing
happened".
Tests: 721 → 820.