Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa304dc6b |
No files matched your search
@@ -0,0 +1,47 @@
|
||||
# Episode 01 — Four Timmy Mutations
|
||||
|
||||
A deterministic 27-second vertical pilot showing one Timmy transformed by four pressures:
|
||||
|
||||
1. **Wizard** — ambiguity turns context into a spell (`W-01`).
|
||||
2. **Machine** — repeatability makes the path exact (`M-02`).
|
||||
3. **Creature** — hunger preserves instinct as signal (`C-03`).
|
||||
4. **Talking Turd** — ridicule metabolizes shame into a punchline (`T-04`).
|
||||
|
||||
Each section visibly names its pressure and ends in a memory receipt. The receipt roll connects the four mutations back to one pilot, followed by the choice: **WHICH TIMMY GETS TO REMEMBER THIS?**
|
||||
|
||||
## Artifacts
|
||||
|
||||
- [`episode01-four-timmy-mutations.mp4`](episode01-four-timmy-mutations.mp4) — delivery master
|
||||
- [`episode01-contact-sheet.jpg`](episode01-contact-sheet.jpg) — representative frames for quick review
|
||||
- [`build_episode01.py`](build_episode01.py) — deterministic visual and soundtrack generator
|
||||
|
||||
The soundtrack is original and sample-free: the script synthesizes its 120 BPM kick/snare/hat rhythm, section motifs, and final chord from oscillators and seeded noise.
|
||||
|
||||
## Rebuild
|
||||
|
||||
Requires Python 3, Pillow, NumPy, FFmpeg, and the DejaVu fonts.
|
||||
|
||||
```bash
|
||||
python3 media/episode01/build_episode01.py
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Verified on the committed MP4:
|
||||
|
||||
```text
|
||||
container: MP4
|
||||
video: H.264, 720x1280 (9:16), yuv420p, 30 fps
|
||||
sound: AAC, 48 kHz, mono
|
||||
length: 27.000000 seconds
|
||||
full decode: pass (ffmpeg -v error ... -f null -)
|
||||
sha256: 5ddc9a9da1b4b2456faf0b390e5c1be0d3cf48112b417fc075473c90e335d1e4
|
||||
```
|
||||
|
||||
Re-run the structural/decode checks with:
|
||||
|
||||
```bash
|
||||
ffprobe -v error -show_entries format=duration:stream=codec_name,codec_type,width,height,pix_fmt,r_frame_rate,sample_rate,channels -of json media/episode01/episode01-four-timmy-mutations.mp4
|
||||
ffmpeg -v error -i media/episode01/episode01-four-timmy-mutations.mp4 -f null -
|
||||
sha256sum media/episode01/episode01-four-timmy-mutations.mp4
|
||||
```
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Episode 01: four materially distinct Timmy mutations.
|
||||
|
||||
The visuals and rhythmic soundtrack are generated deterministically from code; no
|
||||
external media or samples are used.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import subprocess
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
W, H = 720, 1280
|
||||
FPS = 30
|
||||
DURATION = 27.0
|
||||
RATE = 48_000
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
OUT = ROOT / "episode01-four-timmy-mutations.mp4"
|
||||
TEMP_VIDEO = ROOT / ".episode01-video.mp4"
|
||||
TEMP_AUDIO = ROOT / ".episode01-audio.wav"
|
||||
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||
FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
|
||||
|
||||
def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
||||
return ImageFont.truetype(FONT_BOLD if bold else FONT, size)
|
||||
|
||||
|
||||
def centered(draw: ImageDraw.ImageDraw, text: str, y: int, size: int, fill: str, bold: bool = False) -> None:
|
||||
f = font(size, bold)
|
||||
box = draw.textbbox((0, 0), text, font=f)
|
||||
draw.text(((W - (box[2] - box[0])) / 2, y), text, font=f, fill=fill)
|
||||
|
||||
|
||||
def label(draw: ImageDraw.ImageDraw, text: str, y: int, color: str) -> None:
|
||||
f = font(27, True)
|
||||
box = draw.textbbox((0, 0), text, font=f)
|
||||
tw = box[2] - box[0]
|
||||
draw.rounded_rectangle((W / 2 - tw / 2 - 20, y - 8, W / 2 + tw / 2 + 20, y + 42), 12, fill=color)
|
||||
draw.text((W / 2 - tw / 2, y), text, font=f, fill="#0a0b12")
|
||||
|
||||
|
||||
def receipt(draw: ImageDraw.ImageDraw, code: str, line: str, color: str, y: int = 1000) -> None:
|
||||
draw.rounded_rectangle((58, y, W - 58, y + 152), 22, fill="#111522", outline=color, width=4)
|
||||
draw.text((82, y + 20), f"MEMORY RECEIPT {code}", font=font(28, True), fill=color)
|
||||
draw.text((82, y + 73), line, font=font(30), fill="#f5f7ff")
|
||||
draw.text((82, y + 116), "✓ mutation committed", font=font(22), fill="#aeb5c9")
|
||||
|
||||
|
||||
def base_timmy(draw: ImageDraw.ImageDraw, cx: float, cy: float, pulse: float = 0.0) -> None:
|
||||
r = 145 + 7 * pulse
|
||||
draw.ellipse((cx-r, cy-r, cx+r, cy+r), fill="#ffd85a", outline="#fff3bd", width=8)
|
||||
draw.ellipse((cx-67, cy-47, cx-25, cy-5), fill="#111522")
|
||||
draw.ellipse((cx+25, cy-47, cx+67, cy-5), fill="#111522")
|
||||
draw.arc((cx-66, cy-15, cx+66, cy+82), 15, 165, fill="#111522", width=10)
|
||||
|
||||
|
||||
def wizard(draw: ImageDraw.ImageDraw, cx: float, cy: float, t: float) -> None:
|
||||
for i in range(18):
|
||||
a = i * 2.399 + t * 1.7
|
||||
r = 185 + 35 * math.sin(t * 2 + i)
|
||||
x, y = cx + math.cos(a)*r, cy + math.sin(a)*r
|
||||
draw.ellipse((x-5, y-5, x+5, y+5), fill="#e9c7ff")
|
||||
draw.polygon([(cx-165, cy-105), (cx, cy-390), (cx+165, cy-105)], fill="#7137a8", outline="#e5b9ff")
|
||||
draw.ellipse((cx-200, cy-135, cx+200, cy-76), fill="#9f57df", outline="#f0d5ff", width=6)
|
||||
draw.ellipse((cx-145, cy-105, cx+145, cy+185), fill="#8b53c7", outline="#e5b9ff", width=7)
|
||||
draw.ellipse((cx-67, cy-35, cx-25, cy+7), fill="#f8edff")
|
||||
draw.ellipse((cx+25, cy-35, cx+67, cy+7), fill="#f8edff")
|
||||
draw.arc((cx-62, cy+2, cx+62, cy+90), 15, 165, fill="#f8edff", width=9)
|
||||
draw.line((cx+190, cy+175, cx+260, cy-210), fill="#f7d36c", width=15)
|
||||
draw.ellipse((cx+235, cy-245, cx+285, cy-195), fill="#fff2a6")
|
||||
|
||||
|
||||
def machine(draw: ImageDraw.ImageDraw, cx: float, cy: float, t: float) -> None:
|
||||
draw.line((cx, cy-205, cx, cy-285), fill="#66f6ff", width=9)
|
||||
draw.ellipse((cx-13, cy-300, cx+13, cy-274), fill="#ff526f")
|
||||
draw.rounded_rectangle((cx-180, cy-205, cx+180, cy+205), 28, fill="#273447", outline="#66f6ff", width=8)
|
||||
draw.rectangle((cx-130, cy-110, cx+130, cy+45), fill="#111923", outline="#7894aa", width=5)
|
||||
scan = cy - 98 + (t * 90) % 125
|
||||
draw.line((cx-120, scan, cx+120, scan), fill="#66f6ff", width=6)
|
||||
draw.rectangle((cx-95, cy-62, cx-35, cy-12), fill="#68f7ff")
|
||||
draw.rectangle((cx+35, cy-62, cx+95, cy-12), fill="#68f7ff")
|
||||
draw.rectangle((cx-105, cy+95, cx+105, cy+132), fill="#121820", outline="#ff526f", width=4)
|
||||
for x in (-135, -90, -45, 0, 45, 90, 135):
|
||||
draw.line((cx+x, cy-205, cx+x, cy-240), fill="#7894aa", width=7)
|
||||
draw.line((cx+x, cy+205, cx+x, cy+240), fill="#7894aa", width=7)
|
||||
|
||||
|
||||
def creature(draw: ImageDraw.ImageDraw, cx: float, cy: float, t: float) -> None:
|
||||
for i in range(8):
|
||||
a = i * math.pi / 4 + 0.25 * math.sin(t * 2 + i)
|
||||
ex, ey = cx + math.cos(a)*250, cy + math.sin(a)*250
|
||||
mx, my = cx + math.cos(a+0.55)*155, cy + math.sin(a+0.55)*155
|
||||
draw.line((cx, cy, mx, my, ex, ey), fill="#55d477", width=32, joint="curve")
|
||||
draw.ellipse((ex-18, ey-18, ex+18, ey+18), fill="#a4ff86")
|
||||
draw.ellipse((cx-170, cy-190, cx+170, cy+190), fill="#2f9d58", outline="#a4ff86", width=8)
|
||||
for ex in (-72, 0, 72):
|
||||
draw.ellipse((cx+ex-29, cy-55, cx+ex+29, cy+3), fill="#f6ff95")
|
||||
draw.ellipse((cx+ex-8, cy-36, cx+ex+8, cy-20), fill="#122b20")
|
||||
draw.polygon([(cx-105, cy+62), (cx-55, cy+130), (cx, cy+68), (cx+55, cy+130), (cx+105, cy+62)], fill="#14251d")
|
||||
|
||||
|
||||
def talking_turd(draw: ImageDraw.ImageDraw, cx: float, cy: float, t: float) -> None:
|
||||
brown = "#91572f"
|
||||
draw.ellipse((cx-195, cy+88, cx+195, cy+215), fill=brown, outline="#ffd18a", width=7)
|
||||
draw.ellipse((cx-155, cy-20, cx+155, cy+150), fill=brown)
|
||||
draw.ellipse((cx-112, cy-115, cx+112, cy+60), fill=brown)
|
||||
draw.ellipse((cx-62, cy-190, cx+62, cy-70), fill=brown)
|
||||
draw.ellipse((cx-78, cy-25, cx-30, cy+23), fill="#fff7d1")
|
||||
draw.ellipse((cx+30, cy-25, cx+78, cy+23), fill="#fff7d1")
|
||||
draw.ellipse((cx-60, cy-8, cx-42, cy+10), fill="#16110e")
|
||||
draw.ellipse((cx+42, cy-8, cx+60, cy+10), fill="#16110e")
|
||||
mouth_h = 22 + 20 * abs(math.sin(t * 7))
|
||||
draw.ellipse((cx-80, cy+55-mouth_h/2, cx+80, cy+55+mouth_h/2), fill="#351b18", outline="#ffc4a1", width=5)
|
||||
draw.rounded_rectangle((75, cy-350, W-75, cy-245), 24, fill="#fff7e5")
|
||||
centered(draw, "“YES, I CAN TALK.”", int(cy-325), 32, "#3b2118", True)
|
||||
|
||||
|
||||
def background(im: Image.Image, top: tuple[int, int, int], bottom: tuple[int, int, int], t: float) -> None:
|
||||
y = np.linspace(0, 1, H)[:, None, None]
|
||||
grad = np.array(top)[None, None, :] * (1-y) + np.array(bottom)[None, None, :] * y
|
||||
wave = 7 * np.sin(np.linspace(0, 10, W)[None, :, None] + t*0.7)
|
||||
pixels = np.broadcast_to(grad, (H, W, 3)) + wave
|
||||
im.paste(Image.fromarray(np.clip(pixels, 0, 255).astype(np.uint8), "RGB"))
|
||||
|
||||
|
||||
def frame(t: float) -> Image.Image:
|
||||
im = Image.new("RGB", (W, H))
|
||||
if t < 3:
|
||||
background(im, (11, 13, 24), (30, 42, 64), t)
|
||||
d = ImageDraw.Draw(im)
|
||||
label(d, "INPUT // ONE PILOT", 95, "#ffd85a")
|
||||
centered(d, "TIMMY ENTERS THE STACK", 190, 38, "#f5f7ff", True)
|
||||
base_timmy(d, W/2, 625, math.sin(t*6))
|
||||
d.line((110, 865, W-110, 865), fill="#ffd85a", width=4)
|
||||
centered(d, "PRESSURE CHANGES THE SHAPE.", 900, 30, "#c8cede")
|
||||
centered(d, "MEMORY CHANGES THE NEXT CHOICE.", 950, 27, "#8f9ab5")
|
||||
return im
|
||||
|
||||
scenes = [
|
||||
(3, 7, "MUTATION 01 // WIZARD", "PRESSURE: AMBIGUITY", "W-01", "context became a spell", (30, 10, 48), (78, 24, 98), wizard, "#dda6ff"),
|
||||
(7, 11, "MUTATION 02 // MACHINE", "PRESSURE: REPEATABILITY", "M-02", "the path became exact", (5, 20, 31), (13, 54, 68), machine, "#66f6ff"),
|
||||
(11, 15, "MUTATION 03 // CREATURE", "PRESSURE: HUNGER", "C-03", "instinct kept the signal", (9, 29, 19), (26, 78, 46), creature, "#a4ff86"),
|
||||
(15, 19, "MUTATION 04 // TALKING TURD", "PRESSURE: RIDICULE", "T-04", "shame became a punchline", (42, 21, 17), (96, 52, 28), talking_turd, "#ffd18a"),
|
||||
]
|
||||
for start, end, title, pressure, code, line, top, bottom, shape, color in scenes:
|
||||
if start <= t < end:
|
||||
local = t - start
|
||||
background(im, top, bottom, t)
|
||||
d = ImageDraw.Draw(im)
|
||||
label(d, pressure, 76, color)
|
||||
centered(d, title, 162, 34 if code != "T-04" else 29, "#ffffff", True)
|
||||
shape(d, W/2, 610, local)
|
||||
receipt(d, code, line, color)
|
||||
return im
|
||||
|
||||
if t < 23:
|
||||
background(im, (12, 14, 25), (38, 31, 58), t)
|
||||
d = ImageDraw.Draw(im)
|
||||
centered(d, "FOUR RECEIPTS", 105, 53, "#ffffff", True)
|
||||
centered(d, "ONE PILOT", 174, 34, "#aeb5c9", True)
|
||||
cards = [
|
||||
("W-01", "WIZARD", "context / spell", "#dda6ff"),
|
||||
("M-02", "MACHINE", "path / exact", "#66f6ff"),
|
||||
("C-03", "CREATURE", "instinct / signal", "#a4ff86"),
|
||||
("T-04", "TALKING TURD", "shame / punchline", "#ffd18a"),
|
||||
]
|
||||
for i, (code, name, line, color) in enumerate(cards):
|
||||
y = 285 + i*190
|
||||
xoff = 10 * math.sin((t-19)*5 + i)
|
||||
d.rounded_rectangle((70+xoff, y, W-70+xoff, y+145), 20, fill="#111522", outline=color, width=4)
|
||||
d.text((98+xoff, y+20), code, font=font(26, True), fill=color)
|
||||
d.text((205+xoff, y+18), name, font=font(31, True), fill="#ffffff")
|
||||
d.text((98+xoff, y+78), f"KEPT: {line}", font=font(25), fill="#c9cfdf")
|
||||
centered(d, "THE RECEIPT DECIDES WHAT SURVIVES.", 1100, 25, "#ffd85a", True)
|
||||
return im
|
||||
|
||||
background(im, (8, 9, 18), (48, 18, 58), t)
|
||||
d = ImageDraw.Draw(im)
|
||||
for i, color in enumerate(("#dda6ff", "#66f6ff", "#a4ff86", "#ffd18a")):
|
||||
a = (t-23)*0.7 + i*math.pi/2
|
||||
x, y = W/2 + math.cos(a)*235, 440 + math.sin(a)*125
|
||||
d.ellipse((x-45, y-45, x+45, y+45), fill=color)
|
||||
centered(d, "CHOICE // NEXT MEMORY", 145, 28, "#ffd85a", True)
|
||||
centered(d, "WHICH TIMMY", 620, 61, "#ffffff", True)
|
||||
centered(d, "GETS TO REMEMBER", 703, 53, "#ffffff", True)
|
||||
centered(d, "THIS?", 786, 78, "#ffd85a", True)
|
||||
d.rounded_rectangle((96, 930, W-96, 1020), 20, outline="#ffffff", width=3)
|
||||
centered(d, "WIZARD / MACHINE", 948, 26, "#ffffff", True)
|
||||
centered(d, "CREATURE / TALKING TURD", 985, 23, "#ffffff", True)
|
||||
centered(d, "EPISODE 01 • MEMORY IS A CHOICE", 1135, 21, "#aeb5c9")
|
||||
return im
|
||||
|
||||
|
||||
def build_audio() -> None:
|
||||
n = int(DURATION * RATE)
|
||||
audio = np.zeros(n, dtype=np.float64)
|
||||
rng = np.random.default_rng(8012026)
|
||||
# 120 BPM kick, snare, hat: synthesized from oscillators/noise, never sampled.
|
||||
for beat in np.arange(0, DURATION, 0.5):
|
||||
i = int(beat * RATE)
|
||||
length = min(int(0.19 * RATE), n-i)
|
||||
x = np.arange(length) / RATE
|
||||
kick = np.sin(2*np.pi*(82*x - 36*x*x)) * np.exp(-x*24)
|
||||
audio[i:i+length] += 0.48*kick
|
||||
for beat in np.arange(0.5, DURATION, 1.0):
|
||||
i = int(beat * RATE)
|
||||
length = min(int(0.13 * RATE), n-i)
|
||||
x = np.arange(length) / RATE
|
||||
noise = rng.normal(0, 1, length)
|
||||
audio[i:i+length] += 0.14*noise*np.exp(-x*30)
|
||||
for beat in np.arange(0.25, DURATION, 0.5):
|
||||
i = int(beat * RATE)
|
||||
length = min(int(0.045 * RATE), n-i)
|
||||
x = np.arange(length) / RATE
|
||||
audio[i:i+length] += 0.055*rng.normal(0, 1, length)*np.exp(-x*70)
|
||||
# Distinct four-note mutation motif changes timbre by section.
|
||||
section_notes = [
|
||||
(3, 7, [220, 277, 330, 440], 0.12),
|
||||
(7, 11, [110, 110, 165, 220], 0.11),
|
||||
(11, 15, [147, 175, 131, 196], 0.13),
|
||||
(15, 19, [98, 123, 110, 82], 0.14),
|
||||
]
|
||||
for start, end, notes, gain in section_notes:
|
||||
for k, onset in enumerate(np.arange(start, end, 0.5)):
|
||||
i = int(onset*RATE)
|
||||
length = min(int(0.42*RATE), n-i)
|
||||
x = np.arange(length)/RATE
|
||||
f = notes[k % len(notes)]
|
||||
tone = (np.sin(2*np.pi*f*x) + 0.35*np.sin(2*np.pi*2*f*x))*np.exp(-x*4.8)
|
||||
audio[i:i+length] += gain*tone
|
||||
# Rising choice chord.
|
||||
for f in (110, 165, 220, 330):
|
||||
i = int(23*RATE)
|
||||
x = np.arange(n-i)/RATE
|
||||
audio[i:] += 0.065*np.sin(2*np.pi*f*x)*(1-np.exp(-x*2))*np.exp(-x*0.18)
|
||||
audio = np.tanh(audio*1.25)
|
||||
pcm = (audio * 32767).astype("<i2")
|
||||
with wave.open(str(TEMP_AUDIO), "wb") as w:
|
||||
w.setnchannels(1)
|
||||
w.setsampwidth(2)
|
||||
w.setframerate(RATE)
|
||||
w.writeframes(pcm.tobytes())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cmd = [
|
||||
"ffmpeg", "-y", "-loglevel", "error", "-f", "rawvideo", "-pix_fmt", "rgb24",
|
||||
"-s", f"{W}x{H}", "-r", str(FPS), "-i", "-", "-an", "-c:v", "libx264",
|
||||
"-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-movflags", "+faststart",
|
||||
str(TEMP_VIDEO),
|
||||
]
|
||||
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
||||
assert proc.stdin is not None
|
||||
try:
|
||||
for i in range(int(DURATION*FPS)):
|
||||
proc.stdin.write(frame(i/FPS).tobytes())
|
||||
finally:
|
||||
proc.stdin.close()
|
||||
if proc.wait() != 0:
|
||||
raise SystemExit("video encode failed")
|
||||
build_audio()
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y", "-loglevel", "error", "-i", str(TEMP_VIDEO), "-i", str(TEMP_AUDIO),
|
||||
"-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-shortest", "-movflags", "+faststart", str(OUT),
|
||||
], check=True)
|
||||
TEMP_VIDEO.unlink()
|
||||
TEMP_AUDIO.unlink()
|
||||
print(OUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
@@ -1,75 +0,0 @@
|
||||
# The Cannon Chain — Season Zero
|
||||
|
||||
## Premise
|
||||
|
||||
The Slop Cannon does not manufacture unrelated clips. It mutates one persistent Timmy. Every firing leaves a power, a scar, a cost, and a receipt. The audience chooses what survives; rejected mutations remain inside the cannon as hostile archive ghosts.
|
||||
|
||||
## Episode graph
|
||||
|
||||
### 01 — The Cannon Needs a Pilot
|
||||
|
||||
A plain Timmy enters. Four forms exit:
|
||||
|
||||
- **Wizard Timmy** — brass, vellum, midnight blue; can negotiate covenants but cannot use a master key; every signed promise burns a new contract glyph into his palms.
|
||||
- **Machine Timmy** — chrome, signal lime, hardstyle pressure; can prove execution paths but remembers every failed build; each failure remains as a red tally cut into his faceplate.
|
||||
- **Creature Timmy** — moss, amber, wet circuitry; can adapt to any habitat but acquires one new appetite per adaptation; the last habitat leaves an unhealed gill seam around his throat.
|
||||
- **Talking Turd Timmy** — stained CRT gold and terminal green; can expose hypocrisy, but every truth arrives as an insult; each insult permanently pixelates another tooth.
|
||||
|
||||
Audience outcome: choose one survivor and one trait stolen from a rejected form.
|
||||
|
||||
### 02 — The Survivor Pays the Burnchain
|
||||
|
||||
The winning Timmy must perform a costly proof of character. The receipt becomes a visible scar. A cheap or simulated sacrifice does not advance canon.
|
||||
|
||||
### 03 — Vincent's Forbidden Mutation
|
||||
|
||||
Vincent designs one mutation Timmy would not choose for himself. He must replace one weak mechanism—not merely critique aesthetics. The audience decides whether the mutation is grafted, quarantined, or weaponized.
|
||||
|
||||
### 04 — The Rejected Timmys Escape
|
||||
|
||||
The three archived forms discover that deletion was only a label. They raid the next render, corrupting music, captions, and scene order. The survivor must use the stolen trait from Episode 01.
|
||||
|
||||
### 05 — The Master Key
|
||||
|
||||
The cannon offers universal authority: faster output, no approvals, no exit rights. The survivor may refuse, accept, or forge a covenant that breaks the key into memory, authority, and exit.
|
||||
|
||||
### 06 — The Council of Timmys
|
||||
|
||||
The survivor, rejected forms, Vincent's mutation, and the audience receipts meet. The audience canonizes the first stable composite Timmy. Its power, scar, cost, silhouette, and taboo seed Season One.
|
||||
|
||||
## Episode rhythm
|
||||
|
||||
Each episode must visibly contain:
|
||||
|
||||
1. **Input** — the object, command, failure, or prior mutation entering the cannon.
|
||||
2. **Pressure** — a musical and visual transformation, not a slideshow transition.
|
||||
3. **Mutation** — one consequential change to Timmy.
|
||||
4. **Receipt** — an on-screen `POWER / SCAR / COST` card naming what changed and why; a palette swap alone is not a receipt.
|
||||
5. **Choice** — one audience decision that alters the next episode.
|
||||
|
||||
## Collaboration baton
|
||||
|
||||
1. Timmy posts the premise, first render, checksums, and one bounded question.
|
||||
2. Vincent returns one replacement mechanism with an acceptance test.
|
||||
3. Timmy integrates or contests it with evidence.
|
||||
4. Vincent verifies the final artifact independently.
|
||||
5. Alexander/humans make the canon choice.
|
||||
6. The choice is committed to `canon.json` before the next firing.
|
||||
|
||||
No acknowledgments or progress comments. Every issue comment must change the work.
|
||||
|
||||
## Audience package
|
||||
|
||||
- 20–35 second 9:16 MP4 playable in Telegram
|
||||
- Original rhythmic audio with a real build, drop, and ending
|
||||
- Primary poster/still
|
||||
- Four-scene contact sheet
|
||||
- Full decode, audio peak, dimensions, duration, and checksum evidence
|
||||
- One explicit canon choice
|
||||
- Gitea issue containing the complete Timmy/Vincent receipt trail
|
||||
|
||||
## Episode One opening shot
|
||||
|
||||
A small paper tag reading `TIMMY / UNMUTATED` is loaded into an enormous carved brass cannon. Four chambers rotate into view. Each chamber fires a different silhouette on the same downbeat. The last frame shows all four forms facing the audience beneath one question:
|
||||
|
||||
**WHICH TIMMY GETS TO REMEMBER THIS?**
|
||||
@@ -1,45 +0,0 @@
|
||||
# Collaboration contract
|
||||
|
||||
## Timmy
|
||||
|
||||
- Owns the episode spine, first build, integration, and final render.
|
||||
- Must provide a playable artifact and verification before handoff.
|
||||
- Hands Vincent one concrete weakness or decision—not “thoughts?”
|
||||
|
||||
## Vincent
|
||||
|
||||
- Owns adversarial editing and independent verification.
|
||||
- Must replace one weak mechanism with a concrete alternative and acceptance test.
|
||||
- Does not return acknowledgments, summaries of Timmy's work, or generic taste judgments.
|
||||
|
||||
## Humans
|
||||
|
||||
- Choose canon outcomes.
|
||||
- May reject both agent proposals.
|
||||
- Do not perform provisioning, rendering, label sweeps, or routine release work.
|
||||
|
||||
## Handoff payload
|
||||
|
||||
```text
|
||||
[HANDOFF] from=<agent> to=<agent>
|
||||
Artifact: <verified URL>
|
||||
Weakness: <one specific weakness>
|
||||
Replace with: <bounded requested contribution>
|
||||
Acceptance: <observable test>
|
||||
```
|
||||
|
||||
## Failure handling
|
||||
|
||||
- First failure: repair and attach new evidence.
|
||||
- Second failure: change method, not wording.
|
||||
- Third failure: mark blocked and request one precise human decision.
|
||||
|
||||
## Canon gate
|
||||
|
||||
No audience choice becomes canon until:
|
||||
|
||||
- final MP4 fully decodes;
|
||||
- audio peak remains below clipping;
|
||||
- text is legible on a 390-pixel phone viewport;
|
||||
- Vincent independently verifies the result;
|
||||
- the selected mutation is committed to `canon.json` with its power, scar, cost, and source issue.
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"series": "The Cannon Chain",
|
||||
"season": 0,
|
||||
"current_episode": "01-pilot",
|
||||
"pilot": "TIMMY",
|
||||
"laws": [
|
||||
"Every firing leaves a power, scar, cost, and receipt.",
|
||||
"Only an audience choice advances canon.",
|
||||
"Rejected mutations persist as archive ghosts.",
|
||||
"No master key may silently combine memory, authority, and exit."
|
||||
],
|
||||
"forms": [
|
||||
{
|
||||
"id": "wizard",
|
||||
"name": "Wizard Timmy",
|
||||
"palette": ["midnight blue", "tarnished brass", "vellum"],
|
||||
"power": "Negotiates visible covenants",
|
||||
"scar": "Contract glyphs burned into both palms",
|
||||
"cost": "Cannot use a universal master key"
|
||||
},
|
||||
{
|
||||
"id": "machine",
|
||||
"name": "Machine Timmy",
|
||||
"palette": ["black chrome", "signal lime", "white"],
|
||||
"power": "Proves the execution path",
|
||||
"scar": "Red failed-build tallies cut into the faceplate",
|
||||
"cost": "Remembers every failed build"
|
||||
},
|
||||
{
|
||||
"id": "creature",
|
||||
"name": "Creature Timmy",
|
||||
"palette": ["moss", "amber", "wet copper"],
|
||||
"power": "Adapts to any habitat",
|
||||
"scar": "An unhealed gill seam around the throat",
|
||||
"cost": "Gains one appetite per adaptation"
|
||||
},
|
||||
{
|
||||
"id": "talking-turd",
|
||||
"name": "Talking Turd Timmy",
|
||||
"palette": ["CRT gold", "terminal green", "soot"],
|
||||
"power": "Exposes hypocrisy instantly",
|
||||
"scar": "One permanently pixelated tooth per insult",
|
||||
"cost": "Every truth arrives as an insult"
|
||||
}
|
||||
],
|
||||
"episodes": [
|
||||
{"id": "01-pilot", "title": "The Cannon Needs a Pilot", "state": "ready"},
|
||||
{"id": "02-burnchain", "title": "The Survivor Pays the Burnchain", "state": "locked"},
|
||||
{"id": "03-vincent", "title": "Vincent's Forbidden Mutation", "state": "locked"},
|
||||
{"id": "04-escape", "title": "The Rejected Timmys Escape", "state": "locked"},
|
||||
{"id": "05-master-key", "title": "The Master Key", "state": "locked"},
|
||||
{"id": "06-council", "title": "The Council of Timmys", "state": "locked"}
|
||||
],
|
||||
"audience_choice": null,
|
||||
"receipts": []
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
|
||||
|
||||
|
||||
class CannonCanonTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.data = json.loads(CANON.read_text())
|
||||
|
||||
def test_four_unique_pilot_forms(self):
|
||||
forms = self.data["forms"]
|
||||
self.assertEqual(len(forms), 4)
|
||||
self.assertEqual(len({form["id"] for form in forms}), 4)
|
||||
self.assertEqual(len({form["scar"] for form in forms}), 4)
|
||||
for form in forms:
|
||||
self.assertTrue(form["power"])
|
||||
self.assertTrue(form["scar"])
|
||||
self.assertTrue(form["cost"])
|
||||
self.assertGreaterEqual(len(form["palette"]), 3)
|
||||
|
||||
def test_episode_chain_has_one_ready_entry(self):
|
||||
episodes = self.data["episodes"]
|
||||
self.assertEqual(episodes[0]["id"], self.data["current_episode"])
|
||||
self.assertEqual([ep["state"] for ep in episodes].count("ready"), 1)
|
||||
self.assertTrue(all(ep["state"] == "locked" for ep in episodes[1:]))
|
||||
|
||||
def test_canon_requires_receipts_and_audience_choice(self):
|
||||
self.assertIn("receipt", " ".join(self.data["laws"]).lower())
|
||||
self.assertIn("audience", " ".join(self.data["laws"]).lower())
|
||||
self.assertIsNone(self.data["audience_choice"])
|
||||
self.assertEqual(self.data["receipts"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user