
CityEons
7.1K subscribers3.7M views
An aerial camera drifts over a stylized 3D city while buildings appear in the year they were built.
The production instructions and source for this visual style.
kit/ and the skill into skills/faceless-city-builder/.SKILL.md in your coding agent and follow the workflow.---
name: faceless-city-builder
description: Build a faceless 3D city timelapse video (one continuous aerial flight, buildings rising in the year they were built, year and population counters, dated event cards, music bed) from a dated script, ending in final.mp4 + thumbnail.png and a passed QC gate.
---
## Public package setup
Export `KIT` to your extracted kit directory and `SKILL` to this extracted style directory.
Read README.md for dependencies, missing assets and setup. Example media and private review
material mentioned below are not bundled; create your own input/output paths.
# faceless-city-builder
A stylized 3D city grows on a real or generated site while one camera flies over it. Built on the shared kit (`kit/README.md`).
Everything on screen comes from `timeline.json`; the renderer holds no story. `SPEC.md` has the measured style.
```
SKILL=$SKILL
KIT=$KIT
PY=$KIT/.venv/bin/python
```
## Workflow
```
# one command: script -> final.mp4 + thumbnail.png + qc/
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --quality standard
# text-first is the default (no VO in 9 of 9 reference channels). For narration:
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --voice English_CaptivatingStoryteller
# plan only, then look at stills before spending a render
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --plan-only
cat runs/lisbon/shots.md
node $KIT/js/render.mjs --root $SKILL --page render/scene.html --data $PWD/runs/lisbon/timeline.json --ss 2 --still 30 --out s.png
# long videos: run one stage per tool call. Every stage resumes where it stopped (same arguments each time)
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --stage plan
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --stage preview
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --stage render # 100 s chunks, finished ones are kept
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --stage audio
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --stage thumb
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --stage qc # 90 s segments, finished ones are kept
# after QC: fix the cause. Renderer change: re-render the span. Planner change: re-run --stage plan (it invalidates all chunks).
$SKILL/tools/rerender.sh runs/lisbon 42 55
rm -rf runs/lisbon/qc_seg/00000 # the segment that held the finding
$SKILL/build.sh --script script.txt --city city.json --out runs/lisbon --stage qc
```
`build.sh` steps: words → plan → preview slices with vision QC (standard, max) → render → music → mix → thumbnail → QC gate.
Options: `--words`, `--music bed.wav` (reuse a bed), `--no-preview`, `--no-qc`, `--stage`. Standard renders about 10 fps, so 100 s of video take 5 minutes. Vision QC takes about 10 minutes per
90 s segment. Stills take about 25 s each, most of it world generation.
## Inputs
**script.txt.** Plain text. Each sentence with a year becomes one event; a sentence without a year extends the previous one. Optional header lines:
`CITY:`, `TITLE:`, `SUBTITLE:`, `BIOME:`, `PLAN:`, `SITE:`, `HOOK:`, `LAT:`, `LON:`.
**city.json (strongly recommended).** Without it, a plain script still works, but the site is generated and landmarks are placed by keyword.
```json
{
"name": "New York",
"geo": { "lat": 40.72, "lon": -73.99, "origin": [40.704, -74.014], "origin_fixed": true,
"rivers": [{ "w": 500, "points": [[40.9, -73.9], [40.7, -74.0]] }] },
"start": 1609, "end": 2026, "biome": "temperate", "plan": "grid", "extent_km": 9,
"population": [[1609, 50], [1700, 5000], [1850, 696000], [1950, 7890000], [2026, 8600000]],
"eras": [{ "from": 1609, "label": "New Netherland", "regime": "Dutch Republic", "sub": "Dutch West India Company", "colors": ["#D9722E", "#EFE4C8", "#2F5C8C"] }],
"events": [
{ "year": 1626, "type": "founding", "name": "Fort Amsterdam", "title": "1626 · Fort Amsterdam", "text": "Dutch traders raise a fort on the southern tip.", "kind": "fort", "where": "waterfront" },
{ "year": 1883, "type": "bridge", "name": "Brooklyn Bridge", "style": "suspension", "text": "The longest suspension bridge in the world opens." }
]
}
```
| field | values |
|---|---|
| `geo` | lat/lon of the map centre. The real coast loads offline from Natural Earth 1:10m (`world-atlas`, npm). Narrow rivers are below its resolution: add `rivers` as lat/lon polylines; `cut` and `add` are polygons that carve or fill water. `origin` is the founding point. |
| `site` | no `geo`: `{ "type": "river_mouth", "bearing": 30, "seed": 7 }`, types `river river_mouth coast bay island peninsula confluence lake plain` |
| `biome` | `temperate mediterranean tropical arid nordic` |
| `plan` | `grid` (planned lattices beyond a fan-shaped old town) or `organic` (long winding blocks) |
| event `type` | `founding landmark bridge fire rail wall park tower harbor district growth note` |
| `kind` (landmark) | `fort castle cathedral church palace capitol station stadium lighthouse windmill monument lattice_tower skyscraper factory hall dome mosque temple village` |
| `where` | `center waterfront hill edge island north south east west northeast ...`, or `"at": [x, z]` metres, or `"latlon": [la, lo]` with `geo` |
| bridge `style` | `arch truss suspension cable_stayed trestle`, default by year |
| extras | `height` (tower, m), `size`, `gone` (year removed), `radius` (fire, m), `model` (GLB served path, e.g. `/kit/assets/sketchfab/<uid>/model.glb`, with `height`), `frame: ["M","high","orbit"]` to force a shot |
| event direction | `dist` (camera distance in metres, overrides the altitude solver; the camera audit reports the cost) |
| short openings | city.json `lead` (hook seconds), `opening: true` (no finale), `cards: false` (year and labels only), `site.river_scale`; `node tools/text_words.mjs script.txt words.json --fit SECONDS`; plan with `--duration` |
| city extras | `cbd: [[x, z]]` downtown centres in local metres, `extent_km`, `max_tower_m`, `parks`, `villages`, `ferry_year`, `hook: "start"` to open at the first year instead of the rewind |
**OSM footprints (optional).** Download an Overpass export by hand, then
`node $SKILL/tools/osm_import.mjs export.json --lat L --lon L --out city.osm.json` and add `"osm": "city.osm.json"` to city.json.
Real roads and footprints replace the generated ones. `start_date` and `height` tags are used; undated buildings take their date from the
growth model. The tool is offline: the skill never fetches map data itself.
Facts must be true for a real city. Population values and dates need sources; give `population` and `eras` yourself rather than relying
on the defaults, which are placeholders.
## Style rules (full version with numbers: SPEC.md)
**Look.** Hazy aerial, warm horizon, teal water carrying the sun, thousands of red and tan roofs, hard long shadows. No characters. Life comes
from ships with wakes, traffic specks, trains, chimney smoke and cloud shadow.
**HUD.** Georgia with old-style figures. Year top centre 80 px, era tracked caps under it, regime with shield top left, population top
right, compass under it, event card bottom centre, landmark labels on stems. Zones are fixed in `render/hud.js`, solved in `planner/layout.mjs`.
## Cinematography and transitions
One continuous flight, measured from optical flow on the references: 0 to 0.2 cuts per 10 s.
- A new camera **beat** per event, each with its own size (`XW W M C CU`), angle (`house 27°, high 34°, map 58°, low 17°`) and move
(`establish flyover push_in pull_out orbit truck crane hop`). `planner/shots.mjs` keeps neighbours different. Inspect `shots.md`.
- **Altitude is solved.** Speed is capped at 0.09 camera-distances per second and zoom at 14 %/s. When the next subject is far, the camera rises and glides.
Events spaced closely across the map force a wide flight: to get close shots, cluster nearby events or space them further apart.
- Year runs between events and **holds** on each while the card reads. Structures start building 5.5 s before their card (bridges 9 s) and finish
exactly as it appears.
- Transitions, in order of preference: continuous; year rewind (hook); title-bridged dissolve (after the rewind); haze flash; dissolve; fade from and to
black. At most one spent transition per ~62 s. Never whip pans, glitches, wipes or hard cuts on beats: no reference uses them.
- Opening: 2026 skyline with water in front, hold 3.4 s, year spins back 4.9 s while the city un-builds, title, dissolve to the bare site. Closing: back to
the same skyline low over the water, sun dropping, windows on, fade out.
## Script rules
- **Title formula** (top channels): `[CITY]: [N] Years in [M] Minutes | 3D Timelapse ([START]–[END])`, `EVOLUTION OF [CITY] | 3D CITY TIMELAPSE ([START]–NOW)`,
`Watch [CITY] Grow for [N] Years - 3D Timelapse (No AI imagery)`.
- **Hook:** the present in the first frame, then the promise. `[CITY] TODAY`, `[POP] people`, rewind to the first year. No question, no greeting.
- **Length:** references run 6 to 11 minutes. Text mode paces at about 13 s per event (references: 11 to 12 s), so 4 to 5 per minute. Fewer events per minute give closer shots. The planner prints a camera audit, a year audit and short-card warnings: act on them.
- **Card:** title under 44 characters; text one sentence under 140 characters, present tense, concrete: who did what, and what changed on the map. Any number on a card must agree with the population counter at that moment.
- **POV:** third person, no "you", no opinions, no filler. Each event must be something the picture can show: a place, a structure, a disaster, a growth step.
- **Voice mode:** 140 wpm with the year in the first clause of each sentence. Script order must be chronological.
## Sound rules
Continuous bed, 9 of 9 channels. `music.txt` holds the prompt, shaped by biome: slow, no drums, no vocals. Bed at -9 dB alone, -16 dB under a voice, mixed to
-14 LUFS. SFX are rare and earned: swell on the rewind, soft whoosh on transitions, one low impact on a disaster, a soft bell on a bridge or founding, 13 s minimum between event sounds.
Never a sound per year tick. Cues live in `cues.json`.
## Quality levels
| level | render | scene | QC |
|---|---|---|---|
| draft | `--ss 1` | shadow 2048, no AO, no bloom, no reflections, bake 4096 | gate on the final |
| standard | `--ss 1` | shadow 4096, MSAA 4x, AO, bloom, reflections at 1/2, bake 4096 | + preview slices with vision QC |
| max | `--ss 2` | shadow 8192, AO at 1/1.5, reflections at 3/4, bake 8192 | + the same preview slices, second vision pass after fixes |
Levels change cost, not correctness. Every level must pass `qc.py`.
## QC loop
`build.sh` ends with `tools/qc_all.sh`, which runs `kit/py/qc.py` with vision on 90 s segments and once more code-only over the whole video, and fails unless all of them pass. Read
`qc_seg/*/report.md`, `qc/report.md` and `flagged.jpg`. Fix the cause in the planner or the renderer, never with an `ignore.json` entry unless the finding is truly intended; give the reason in it.
The planner writes `ignore.json` with the two planned fades only. `plan.mjs` also prints a camera audit: anything over 430 px/s means events are too close for the distance, so space them further apart.
| finding | usual cause | fix |
|---|---|---|
| overlap, label | camera changed after planning | re-run the planner; labels are re-solved |
| pop, structure | window too short | a structure needs several seconds of year run before its card: put the event further from the one before |
| hard jump off-plan | a transition not in `cuts` | check `transitions` and `cuts` in timeline.json |
| logic, wrong subject | `where` resolved far from the story's place | set `at` or `latlon` on the event |
| picture does not match card | event too wide to read | add a `frame`, or space events further apart |
## Assets and credits
Everything is code except the items in `CREDITS.md`. Add a line to the video description for each CC-BY model (`assets.py credits`). To use a real landmark, pick one with
`assets.py peek sketchfab "..."`, fetch it, set `model` on the event and record it in `ASSETS.md`. Skip brands, game rips and real people.
## Files
```
build.sh one command
planner/plan.mjs script + words -> timeline.json, cues.json, music.txt, shots.md
planner/script.mjs sentence parsing, event types
planner/shots.mjs flight plan, altitude solver, transitions
planner/layout.mjs caption wrapping, label placement by camera simulation
planner/geo.mjs real coast from Natural Earth
world/ deterministic world, shared by planner and page (same city, same camera)
render/ scene page: terrain, water, buildings, roads, trees, ships, fx, post, HUD
thumb/ thumbnail: thumbnail.png (Cornerstone layout), thumbnail_split.png (past/present)
tools/ text_words, osm_import, rerender, qc_all, refsheet (stills beside reference frames)
```
Source only. Fetch assets separately; setup and credits are in the README. All 11 skills ↗

7.1K subscribers3.7M views

5.6K subscribers3.5M views

2.6K subscribers1.2M views

3.4K subscribers937K views

2.9K subscribers878K views

17.6K subscribers475K views

934 subscribers355K views

245 subscribers185K views

379 subscribers174K views

2M views

1.7M views

684K views

371K views

334K views

252K views

178K views

155K views