Create your own: design for approval (on the TV, or from a phone by QR code)
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m52s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m40s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-06 08:25:43 +00:00
co-authored by Claude Opus 5.5
parent c606809250
commit caef801652
5 changed files with 147 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Create your own, design proposal (Paul, 6 November 2026): make a channel on the TV, or from your phone by scanning a QR
code on the TV. The phone opens a page the TV itself serves on the home network: no app to install, no account, no password.
Channels go in a new 700s block, Yours. The QR pattern drawn here is illustrative only. For approval: nothing is built yet."""
import ast, random
from pathlib import Path
HERE = Path(__file__).resolve().parent
SRC = HERE.parent / 'profiles' / 'build-profiles.py'
tree = ast.parse(SRC.read_text())
keep = []
for n in tree.body:
if isinstance(n, ast.Expr) and isinstance(n.value, ast.Constant): continue
if isinstance(n, ast.Expr) and isinstance(n.value, ast.Call) and getattr(n.value.func, 'id', '') == 'screen': break
keep.append(n)
ns = {'__file__': str(SRC)}
exec(compile(ast.Module(keep, []), str(SRC), 'exec'), ns)
globals().update({k: v for k, v in ns.items() if k not in ('HERE', '__file__', 'save')})
from PIL import Image
def save(name): ns['im'].resize((W, H), Image.LANCZOS).save(HERE / name); print(name)
def D(): return ns['d']
GREEN = (110, 220, 170); LATER = (15, 44, 92); TEAL = (110, 240, 216)
def head(title, line, step=None):
text((80, 70), title, anton(48), WHITE)
if step: text((1840, 84), step, semi(26), MUTED, 'ra')
text((80, 140), line, reg(26), MUTED)
def qr(x, y, size):
"""An illustrative QR pattern: three finder squares and a scatter of modules."""
n = 29; m = size / n
box(x - 16, y - 16, size + 32, size + 32, fill=WHITE, r=10)
rnd = random.Random(7)
for i in range(n):
for j in range(n):
finder = any(fi <= i < fi + 7 and fj <= j < fj + 7 for fi, fj in ((0, 0), (0, n - 7), (n - 7, 0)))
if finder:
a, b = (i % 7 if i < 7 else (i - (n - 7)) % 7), (j % 7 if j < 7 else (j - (n - 7)) % 7)
on = a in (0, 6) or b in (0, 6) or (2 <= a <= 4 and 2 <= b <= 4)
else: on = rnd.random() < .45
if on: D().rectangle((S(x + j * m), S(y + i * m), S(x + (j + 1) * m), S(y + (i + 1) * m)), fill=NAVY)
def phone(x, y, w=430, h=880):
box(x, y, w, h, fill=(20, 20, 24), r=48); box(x + 14, y + 14, w - 28, h - 28, fill=NAVY, r=38)
box(x + w / 2 - 50, y + 26, 100, 22, fill=(20, 20, 24), r=11)
def chip(x, y, label, on=False, w=None):
w = w or (len(label) * 13 + 40)
box(x, y, w, 44, fill=YELLOW if on else PANEL, r=22); text((x + w / 2, y + 8), label, semi(22), NAVY if on else WHITE, 'ma')
return w
# 1. Where it starts: on this TV, or on your phone.
screen()
head('Create a channel', 'Make a channel of your own from your library. It joins the guide in Yours, from 701.', 'Settings › Channels')
box(140, 230, 760, 620, fill=NAVY, r=20, outline=YELLOW, width=4)
text((520, 280), 'On this TV', anton(44), WHITE, 'ma')
for i, line in enumerate(['Name it and pick its number', 'Add shows, or let the Director\'s Chair suggest them', 'Choose how it plays: in order, shuffled,', 'blocks or marathon', 'Choose when it\'s on']):
text((200, 380 + i * 52), ('· ' if i != 3 else ' ') + line, reg(28), WHITE)
box(380, 740, 280, 70, fill=YELLOW, r=12); text((520, 754), 'Start', semi(34), NAVY, 'ma')
box(1020, 230, 760, 620, fill=PANEL, r=20)
text((1400, 280), 'On your phone', anton(44), WHITE, 'ma')
qr(1200, 370, 300)
text((1560, 380), 'Scan with', reg(26), MUTED); text((1560, 416), 'your phone\'s', reg(26), MUTED); text((1560, 452), 'camera', reg(26), MUTED)
text((1560, 520), 'Or go to', reg(26), MUTED); text((1560, 556), 'orbit.local', semi(28), WHITE); text((1560, 600), 'and enter', reg(26), MUTED)
box(1560, 640, 170, 60, fill=NAVY, r=10); text((1645, 650), '4827', semi(38), YELLOW, 'ma')
text((1400, 720), 'Same Wi-Fi as this TV. No app, no account, no password.', reg(24), MUTED, 'ma')
text((1400, 756), 'The code works once and lasts two minutes.', reg(24), MUTED, 'ma')
text((960, 900), 'Grown-ups only: the phone asks for the PIN if one is set, and Kids never sees these screens.', reg(26), MUTED, 'ma')
keys([('◄►', 'Choose'), ('OK', 'Start on this TV'), ('Back', 'Channels')])
save('create-1-start.png')
# 2. Shows: search, or describe the channel and the Chair proposes.
screen()
head('Shows', 'Search your library, or describe the channel and the Director\'s Chair suggests what fits.', 'Step 2 of 4')
box(80, 200, 1060, 76, fill=PANEL, r=12); text((110, 218), 'Describe it:', semi(28), MUTED); text((290, 216), '90s sci-fi, late evenings', semi(30), WHITE)
box(1160, 200, 220, 76, fill=YELLOW, r=12); text((1270, 216), 'Suggest', semi(30), NAVY, 'ma')
text((80, 310), 'THE CHAIR SUGGESTS', semi(24), MUTED)
props = [('The X-Files', 'Series · 1993 · 202 episodes', 'Sci-fi, 1990s, mystery: a strong fit'), ('Babylon 5', 'Series · 1993 · 110 episodes', 'Space opera from the decade'),
('Stargate SG-1', 'Series · 1997 · 214 episodes', 'Sci-fi adventure, long run'), ('Sliders', 'Series · 1995 · 88 episodes', 'Also on Paradox: it can be on both')]
for i, (t, d, why) in enumerate(props):
y = 350 + i * 130; focus = i == 0
box(80, y, 1060, 112, fill=NAVY if focus else PANEL, r=12, outline=YELLOW if focus else None, width=4)
text((110, y + 14), t, semi(32), WHITE); text((110, y + 58), d, reg(22), MUTED); text((560, y + 22), why, reg(24), WHITE)
box(890, y + 30, 110, 52, fill=YELLOW if focus else BLUE, r=10); text((945, y + 38), 'Add', semi(26), NAVY if focus else WHITE, 'ma')
box(1015, y + 30, 100, 52, fill=BLUE, r=10); text((1065, y + 38), 'Skip', semi(26), WHITE, 'ma')
box(1200, 310, 640, 640, fill=(10, 30, 66), r=16)
text((1230, 330), 'IN YOUR CHANNEL', semi(24), MUTED)
for i, t in enumerate(['Star Trek: Deep Space Nine', 'Farscape', 'Quantum Leap']):
text((1230, 380 + i * 52), '· ' + t, reg(28), WHITE)
text((1230, 560), 'Add and skip both teach the Chair', reg(22), MUTED); text((1230, 592), 'what this channel is for.', reg(22), MUTED)
keys([('▲▼', 'Shows'), ('OK', 'Add'), ('►', 'Skip'), ('Back', 'Name')])
save('create-2-shows.png')
# 3. How it plays, and when it's on.
screen()
head('How it plays', 'One style for the whole channel; you can change it later. It shapes what\'s planned from then on, never what\'s booked.', 'Step 3 of 4')
styles = [('In order', 'Each show from where it left off,', 'taking turns with the others'), ('Shuffled', 'Episodes in a mixed order,', 'every one before any repeats'),
('Blocks', 'Half-hour or hour slots by show;', 'short episodes grouped as on ToonZ'), ('Marathon', 'One show at a time, several', 'episodes back to back')]
for i, (t, a, b) in enumerate(styles):
x = 80 + i * 450; focus = i == 2
box(x, 220, 420, 300, fill=NAVY if focus else PANEL, r=16, outline=YELLOW if focus else None, width=4)
for k in range(3 if i != 3 else 1):
for j in range(4 if i != 3 else 6):
col = [(255, 138, 61), (110, 220, 170), (120, 190, 255)][((k * 4 + j) % 3) if i == 0 else ((k + j * 2) % 3)] if i != 3 else (255, 138, 61)
if i == 2: col = [(255, 138, 61), (110, 220, 170), (120, 190, 255)][k]
box(x + 30 + j * (90 if i != 3 else 60), 250 + k * 40, (80 if i != 3 else 50), 28, fill=col, r=5)
text((x + 30, 400), t, anton(40), WHITE); text((x + 30, 452), a, reg(22), MUTED); text((x + 30, 482), b, reg(22), MUTED)
text((80, 570), 'When it\'s on', semi(32), WHITE)
w = chip(80, 620, 'All day', True); w2 = chip(80 + w + 20, 620, 'Evenings, 18:00 to midnight'); chip(80 + w + w2 + 40, 620, 'Set your own hours')
text((80, 700), 'Outside its hours the channel shows a "back at 18:00" card, as Kids sees a channel that\'s asleep.', reg(24), MUTED)
box(80, 780, 1760, 150, fill=(10, 30, 66), r=16)
box(110, 810, 200, 90, fill=(40, 20, 70), r=10); text((210, 828), '701', anton(44), YELLOW, 'ma')
text((340, 812), '90s Sci-Fi', anton(40), WHITE); text((340, 866), 'Yours · 7 shows · Blocks · All day · planned like any Orbit channel', reg(24), MUTED)
keys([('◄►', 'Style'), ('▼', 'Hours'), ('OK', 'Choose'), ('Back', 'Shows')])
save('create-3-style.png')
# 4. The phone: the page the QR opens, served by the TV itself.
screen()
head('On your phone', 'The page the QR code opens. It comes from the TV itself, over your home Wi-Fi: nothing to install.', 'Phone companion')
phone(120, 190); x = 150
text((x + 20, 240), 'Orbit', anton(40), WHITE); text((x + 20, 292), 'Living Room TV · connected', reg(22), GREEN)
text((x + 20, 340), 'YOUR CHANNELS', semi(20), MUTED)
for i, (n, t, s) in enumerate([('701', '90s Sci-Fi', 'Blocks · All day'), ('702', 'Sunday Westerns', 'Marathon · Sundays'), ('703', 'Cartoon Club', 'Shuffled · 07:00 to 12:00')]):
y = 376 + i * 130
box(x + 12, y, 346, 114, fill=PANEL, r=14); text((x + 32, y + 14), n, semi(24), YELLOW); text((x + 90, y + 12), t, semi(28), WHITE); text((x + 32, y + 56), s, reg(20), MUTED)
box(x + 32, y + 78, 150, 28, fill=BLUE, r=8); text((x + 107, y + 80), 'Watch on TV', semi(18), WHITE, 'ma')
box(x + 196, y + 78, 90, 28, fill=BLUE, r=8); text((x + 241, y + 80), 'Edit', semi(18), WHITE, 'ma')
box(x + 12, 790, 346, 70, fill=YELLOW, r=14); text((x + 185, 808), '+ Create a channel', semi(28), NAVY, 'ma')
phone(640, 190); x = 670
text((x + 20, 240), 'New channel', anton(36), WHITE); text((x + 20, 290), 'Step 2 of 4 · Shows', reg(22), MUTED)
box(x + 12, 330, 346, 56, fill=PANEL, r=12); text((x + 30, 344), 'Search your library…', reg(22), MUTED)
text((x + 20, 410), 'THE CHAIR SUGGESTS', semi(20), MUTED)
for i, (t, why) in enumerate([('The X-Files', 'Sci-fi, 1990s, mystery'), ('Babylon 5', 'Space opera, 1990s'), ('Stargate SG-1', 'Sci-fi adventure')]):
y = 446 + i * 110
box(x + 12, y, 346, 96, fill=PANEL, r=12); text((x + 30, y + 12), t, semi(26), WHITE); text((x + 30, y + 52), why, reg(19), MUTED)
box(x + 250, y + 22, 92, 50, fill=YELLOW, r=10); text((x + 296, y + 32), 'Add', semi(22), NAVY, 'ma')
box(x + 12, 790, 346, 70, fill=YELLOW, r=14); text((x + 185, 808), 'Next: how it plays', semi(26), NAVY, 'ma')
box(1160, 190, 680, 400, fill=PANEL, r=16)
text((1190, 214), 'ON THE TV MEANWHILE', semi(24), MUTED)
box(1190, 260, 620, 150, fill=NAVY, r=12); text((1214, 280), 'Paul\'s phone is connected', semi(32), WHITE); text((1214, 330), 'Making a channel: 90s Sci-Fi', reg(24), MUTED); text((1214, 366), 'Back on the remote disconnects it', reg(22), MUTED)
text((1190, 440), '"Watch on TV" tunes the TV to that channel, like a remote:', reg(24), WHITE)
text((1190, 474), 'no screen casting needed, and the picture stays full quality.', reg(24), WHITE)
box(1160, 620, 680, 330, fill=(10, 30, 66), r=16)
text((1190, 644), 'KEEPING IT SAFE', semi(24), MUTED)
for i, line in enumerate(['Home Wi-Fi only; the page is never on the internet', 'A one-time code from the TV, lasting two minutes', 'The PIN, if Grown-ups has one', 'Your Emby sign-in never leaves the TV',
'Settings › Phones lists phones and removes them']):
text((1190, 690 + i * 48), '· ' + line, reg(24), WHITE)
save('create-4-phone.png')
Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB