cajpany's picture
Environment-aware Writer note (Nemotron live in local/GPU build; falls back on CPU Space)
2b36fdd verified
Raw
History Blame Contribute Delete
46.2 kB
"""Gradio app surface for Midnight Static."""
from __future__ import annotations
import html
import json
from pathlib import Path
from .fonts import INLINE_FONTS_CSS
from .pipeline import run_fixture_pipeline
from .schema import Genre
from .showcases import default_showcase, load_showcase, showcase_labels
STATIONS = {
Genre.NOIR: {"call": "KNOX", "frequency": "87.9", "label": "Noir", "left": 8},
Genre.WEIRD: {"call": "WEIRD", "frequency": "91.5", "label": "Weird", "left": 25},
Genre.WESTERN: {"call": "GOLD", "frequency": "94.7", "label": "Western", "left": 42},
Genre.ROMANCE: {"call": "HEART", "frequency": "98.3", "label": "Romance", "left": 58},
Genre.COMEDY: {"call": "LAFF", "frequency": "101.1", "label": "Comedy", "left": 75},
Genre.HINDI_MELODRAMA: {
"call": "RAAT",
"frequency": "104.7",
"label": "Hindi melodrama",
"left": 92,
},
}
GENRE_CHOICES = [(station["label"], genre.value) for genre, station in STATIONS.items()]
PREMISE_PLACEHOLDER = "a train conductor hears tomorrow's crash over the radio"
ON_AIR_STAGE_TEXT = "87.9 WRITING ... 91.5 CASTING ... 94.7 FOLEY ... 98.3 SCORING ... 101.1 ON AIR"
RADIO_CSS = """
/* Fonts are self-hosted (inlined as data: URIs) via INLINE_FONTS_CSS — no
runtime network fetch, keeping the off-the-grid claim clean. */
:root {
--ms-bg: #0c0907;
--ms-wood: #4a2f1d;
--ms-wood-light: #6f4527;
--ms-wood-dark: #200e06;
--ms-brass: #c9a86a;
--ms-brass-light: #f3d495;
--ms-brass-dark: #8f6f35;
--ms-cream: #f3e9d2;
--ms-amber: #ffb84d;
--ms-amber-hot: #ff7e2e;
--ms-red: #b5341f;
--ms-ink: #21170f;
--ms-font-deco: "Limelight", "Georgia", serif;
--ms-font-ident: "Oswald", "Helvetica Neue", sans-serif;
--ms-font-body: "EB Garamond", Georgia, "Times New Roman", serif;
--ms-font-script: "Special Elite", "Courier New", Courier, monospace;
}
body,
.gradio-container {
background:
radial-gradient(circle at 50% 8%, rgba(255, 184, 77, 0.18), transparent 34rem),
radial-gradient(ellipse at 50% 120%, rgba(201, 168, 106, 0.10), transparent 46rem),
radial-gradient(circle at 50% 50%, #15100b 0%, #0c0907 62%, #060403 100%),
#0c0907 !important;
color: var(--ms-cream);
font-family: var(--ms-font-body);
}
/* Soft vignette to pull focus toward the radio. */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
background: radial-gradient(ellipse at 50% 38%, transparent 38%, rgba(0, 0, 0, 0.55) 100%);
animation: ms-flicker 6s ease-in-out infinite;
}
/* Film-grain overlay for analog texture. */
body::after {
content: "";
position: fixed;
inset: -50%;
z-index: 9998;
pointer-events: none;
opacity: 0.05;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
animation: ms-grain 600ms steps(4) infinite;
}
.gradio-container > * {
position: relative;
z-index: 2;
}
.gradio-container {
max-width: 880px !important;
margin: 0 auto !important;
padding: 24px 16px 40px !important;
}
/* ---- Tame Gradio's default light chrome into the walnut room ---- */
gradio-app,
.gradio-container,
.gradio-container .main,
.gradio-container .wrap,
.gradio-container .contain {
background: transparent !important;
}
.gradio-container {
--body-background-fill: transparent;
--background-fill-primary: transparent;
--background-fill-secondary: transparent;
--block-background-fill: rgba(18, 12, 8, 0.60);
--block-border-color: rgba(201, 168, 106, 0.22);
--block-label-background-fill: transparent;
--block-label-text-color: #e9d9b6;
--block-title-text-color: #e9d9b6;
--block-info-text-color: rgba(243, 233, 210, 0.62);
--body-text-color: var(--ms-cream);
--body-text-color-subdued: rgba(243, 233, 210, 0.6);
--border-color-primary: rgba(201, 168, 106, 0.28);
--border-color-accent: rgba(201, 168, 106, 0.45);
--color-accent-soft: rgba(255, 184, 77, 0.14);
--input-background-fill: #f3e9d2;
--input-background-fill-focus: #fffaf0;
--input-border-color: rgba(143, 111, 53, 0.55);
--input-border-color-focus: var(--ms-amber);
--input-text-color: #21170f;
--input-placeholder-color: rgba(88, 52, 24, 0.66);
--checkbox-background-color: #f3e9d2;
--checkbox-background-color-selected: var(--ms-red);
--checkbox-border-color: rgba(143, 111, 53, 0.6);
--checkbox-border-color-selected: var(--ms-red);
--checkbox-label-background-fill: rgba(243, 233, 210, 0.06);
--checkbox-label-background-fill-selected: rgba(255, 184, 77, 0.18);
--checkbox-label-background-fill-hover: rgba(255, 184, 77, 0.1);
--checkbox-label-text-color: var(--ms-cream);
--checkbox-label-border-color: rgba(201, 168, 106, 0.3);
--neutral-950: #0c0907;
}
/* Remove leftover white panels behind grouped blocks. */
.gradio-container .gap,
.gradio-container .form,
.gradio-container .panel {
background: transparent !important;
}
/* Hide the multipage navbar on the styled radio (this CSS only loads here);
navigation to the plain feed is offered via the footer link instead. */
.nav-holder {
display: none !important;
}
.ms-stage {
display: grid;
place-items: center;
margin: 0 auto 22px;
}
.ms-radio {
width: min(780px, 100%);
min-height: 440px;
border-radius: 42px 42px 26px 26px;
position: relative;
overflow: hidden;
padding: 30px;
background:
linear-gradient(90deg, rgba(255,255,255,0.06), transparent 16%, rgba(0,0,0,0.22) 54%, rgba(255,255,255,0.05)),
repeating-linear-gradient(86deg, rgba(255,255,255,0.03) 0 2px, transparent 2px 7px, rgba(0,0,0,0.06) 7px 9px),
radial-gradient(ellipse at 28% 18%, rgba(255, 220, 160, 0.16), transparent 50%),
linear-gradient(135deg, var(--ms-wood-light), var(--ms-wood) 44%, var(--ms-wood-dark));
box-shadow:
inset 0 0 0 3px rgba(255, 225, 160, 0.10),
inset 0 2px 1px rgba(255, 235, 190, 0.18),
inset 0 -20px 34px rgba(0,0,0,0.40),
0 2px 0 rgba(255,235,190,0.06),
0 40px 90px rgba(0,0,0,0.62);
}
.ms-radio::before {
content: "";
position: absolute;
inset: 15px;
border: 2px solid rgba(201, 168, 106, 0.46);
border-radius: 30px 30px 18px 18px;
box-shadow: inset 0 0 22px rgba(0,0,0,0.28), 0 0 1px rgba(255,225,160,0.4);
pointer-events: none;
}
/* Brass cabinet screws. */
.ms-screw {
position: absolute;
width: 11px;
height: 11px;
border-radius: 999px;
background: radial-gradient(circle at 38% 32%, var(--ms-brass-light), var(--ms-brass) 45%, var(--ms-brass-dark) 90%);
box-shadow: inset 0 0 2px rgba(0,0,0,0.5), 0 1px 2px rgba(0,0,0,0.55);
z-index: 3;
}
.ms-screw::after {
content: "";
position: absolute;
inset: 3px;
border-top: 1.4px solid rgba(60, 40, 16, 0.7);
transform: rotate(34deg);
}
.ms-screw.tl { top: 22px; left: 22px; }
.ms-screw.tr { top: 22px; right: 22px; }
.ms-screw.bl { bottom: 22px; left: 22px; }
.ms-screw.br { bottom: 22px; right: 22px; }
.ms-brand {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding: 0 8px;
}
.ms-brand-name {
font-family: var(--ms-font-deco);
color: var(--ms-brass-light);
letter-spacing: 0.14em;
text-transform: uppercase;
font-size: clamp(1.3rem, 3vw, 2.15rem);
text-shadow:
0 1px 0 rgba(60, 38, 14, 0.9),
0 0 18px rgba(255, 184, 77, 0.32);
}
.ms-onair {
display: inline-flex;
align-items: center;
gap: 9px;
font-family: var(--ms-font-ident);
font-weight: 600;
letter-spacing: 0.22em;
font-size: 0.72rem;
color: #ffcaa0;
padding: 5px 12px 5px 11px;
border: 1px solid rgba(181, 52, 31, 0.55);
border-radius: 4px;
background: linear-gradient(180deg, rgba(60, 14, 8, 0.6), rgba(20, 6, 4, 0.5));
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
}
.ms-lamp {
width: 12px;
height: 12px;
border-radius: 999px;
background: radial-gradient(circle at 35% 30%, #ffe3b0, var(--ms-amber) 55%, var(--ms-amber-hot));
box-shadow: 0 0 12px var(--ms-amber), 0 0 26px rgba(255,184,77,0.55);
animation: ms-neon 3.2s ease-in-out infinite;
}
.ms-radio.is-scanning .ms-lamp {
animation: ms-lamp-pulse 700ms ease-in-out infinite alternate;
}
.ms-face {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(250px, 0.9fr);
gap: 28px;
align-items: stretch;
}
.ms-speaker {
position: relative;
min-height: 252px;
border-radius: 18px;
border: 2px solid rgba(201, 168, 106, 0.5);
overflow: hidden;
background:
radial-gradient(circle at 50% 46%, rgba(255, 150, 60, 0.14), transparent 58%),
repeating-radial-gradient(circle at 50% 46%, rgba(35, 24, 16, 0.0) 0 6px, rgba(0,0,0,0.22) 6px 7px),
repeating-linear-gradient(90deg, rgba(35, 24, 16, 0.9) 0 7px, rgba(243, 233, 210, 0.13) 7px 10px),
#20140d;
box-shadow: inset 0 0 40px rgba(0,0,0,0.72), inset 0 0 0 1px rgba(0,0,0,0.4);
}
/* Warm cone glow behind the grille; intensifies while broadcasting. */
.ms-speaker::after {
content: "";
position: absolute;
left: 50%;
top: 46%;
width: 64%;
aspect-ratio: 1;
transform: translate(-50%, -50%);
border-radius: 999px;
background: radial-gradient(circle, rgba(255, 138, 46, 0.28), rgba(255, 138, 46, 0.07) 42%, transparent 68%);
filter: blur(2px);
animation: ms-cone 5s ease-in-out infinite;
}
.ms-radio.is-scanning .ms-speaker::after {
animation: ms-cone-live 900ms ease-in-out infinite;
}
.ms-panel {
display: grid;
gap: 18px;
}
.ms-dial {
min-height: 118px;
border-radius: 14px;
border: 2px solid rgba(201, 168, 106, 0.85);
background:
linear-gradient(180deg, rgba(255,255,255,0.45), transparent 26%),
repeating-linear-gradient(90deg, rgba(90, 58, 24, 0.30) 0 1px, transparent 1px 7.6%),
radial-gradient(ellipse at 50% 40%, #fff3d6, var(--ms-cream) 40%, #d9c187 100%);
color: var(--ms-ink);
box-shadow:
inset 0 0 26px rgba(55, 35, 10, 0.32),
inset 0 0 0 1px rgba(255,255,255,0.4),
0 0 26px rgba(255,184,77,0.20);
position: relative;
overflow: hidden;
padding: 16px 18px 14px;
}
/* Curved glass reflection across the tuning window. */
.ms-dial::before {
content: "";
position: absolute;
left: -20%;
top: -60%;
width: 60%;
height: 200%;
transform: rotate(18deg);
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent);
pointer-events: none;
}
.ms-frequency {
display: flex;
justify-content: space-between;
gap: 8px;
font-family: var(--ms-font-ident);
font-weight: 500;
font-size: 0.76rem;
letter-spacing: 0.08em;
position: relative;
z-index: 1;
}
.ms-frequency span {
color: #3a2410;
opacity: 0.92;
}
.ms-frequency span.is-tuned {
color: #8a2114;
font-weight: 800;
opacity: 1;
}
.ms-needle {
position: absolute;
left: var(--ms-needle-left, 25%);
top: 10px;
bottom: 10px;
width: 2px;
z-index: 2;
background: linear-gradient(180deg, var(--ms-red), #e2502f, var(--ms-red));
box-shadow: 0 0 10px rgba(226, 80, 47, 0.7), 0 0 22px rgba(226, 80, 47, 0.4);
transition: left 360ms cubic-bezier(.18, .89, .32, 1.28);
}
.ms-needle::before {
content: "";
position: absolute;
top: -4px;
left: 50%;
transform: translateX(-50%);
width: 9px;
height: 9px;
border-radius: 999px;
background: radial-gradient(circle at 38% 32%, #ff8a5a, var(--ms-red));
box-shadow: 0 0 10px rgba(226, 80, 47, 0.8);
}
.ms-radio.is-scanning .ms-needle {
animation: ms-needle-scan 1.35s ease-in-out infinite alternate;
}
.ms-station {
position: absolute;
left: 18px;
right: 18px;
bottom: 13px;
z-index: 1;
color: #4a2c12;
font-family: var(--ms-font-ident);
font-weight: 600;
font-size: 0.82rem;
letter-spacing: 0.2em;
text-align: center;
}
.ms-station strong {
color: var(--ms-red);
font-weight: 700;
text-shadow: 0 0 10px rgba(226, 80, 47, 0.35);
}
.ms-scanner {
position: absolute;
left: 18px;
right: 18px;
bottom: 13px;
z-index: 1;
color: var(--ms-red);
font-family: var(--ms-font-ident);
font-weight: 600;
letter-spacing: 0.14em;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ms-radio.is-scanning .ms-frequency span {
animation: ms-frequency-glow 1.1s ease-in-out infinite;
}
.ms-radio.is-scanning .ms-frequency span:nth-child(2) {
animation-delay: 140ms;
}
.ms-radio.is-scanning .ms-frequency span:nth-child(3) {
animation-delay: 280ms;
}
.ms-radio.is-scanning .ms-frequency span:nth-child(4) {
animation-delay: 420ms;
}
.ms-radio.is-scanning .ms-frequency span:nth-child(5) {
animation-delay: 560ms;
}
.ms-radio.is-scanning .ms-frequency span:nth-child(6) {
animation-delay: 700ms;
}
.ms-knobs {
display: flex;
justify-content: space-between;
gap: 18px;
}
.ms-knob {
position: relative;
width: 84px;
aspect-ratio: 1;
border-radius: 999px;
background:
radial-gradient(circle at 35% 28%, var(--ms-brass-light), var(--ms-brass) 38%, var(--ms-brass-dark) 72%, #6a5024 100%);
box-shadow:
inset -9px -11px 18px rgba(0,0,0,0.34),
inset 6px 7px 12px rgba(255, 235, 190, 0.32),
0 12px 22px rgba(0,0,0,0.32);
}
.ms-knob::before {
content: "";
position: absolute;
inset: 18%;
border-radius: 999px;
background: repeating-conic-gradient(rgba(60,40,16,0.0) 0deg 8deg, rgba(60,40,16,0.32) 8deg 10deg);
-webkit-mask: radial-gradient(circle, transparent 56%, #000 57%);
mask: radial-gradient(circle, transparent 56%, #000 57%);
}
.ms-knob::after {
content: "";
position: absolute;
top: 13%;
left: 50%;
width: 3px;
height: 26%;
transform: translateX(-50%);
border-radius: 2px;
background: rgba(40, 26, 10, 0.8);
}
.ms-telegram {
max-width: 780px;
margin: 0 auto !important;
border-radius: 10px;
border: 1px solid rgba(201, 168, 106, 0.3) !important;
background:
radial-gradient(ellipse at 50% 0%, rgba(255, 184, 77, 0.06), transparent 60%),
linear-gradient(180deg, rgba(40, 28, 19, 0.92), rgba(22, 15, 10, 0.92)) !important;
padding: 20px !important;
box-shadow: inset 0 0 0 1px rgba(255, 235, 190, 0.04), 0 18px 36px rgba(0,0,0,0.42);
}
.ms-telegram label,
.ms-telegram .wrap label,
.ms-telegram span {
color: #f3e9d2 !important;
}
.ms-telegram textarea,
.ms-telegram input,
.ms-telegram select {
background: #f3e9d2 !important;
color: #21170f !important;
border-color: rgba(201, 168, 106, 0.72) !important;
}
/* ---- Showcase "rerun" tuner: a clearly-themed dropdown ---- */
.ms-showcase {
margin-bottom: 16px !important;
}
.ms-showcase label > span {
font-family: var(--ms-font-ident) !important;
letter-spacing: 0.18em !important;
text-transform: uppercase !important;
font-size: 0.72rem !important;
color: var(--ms-brass-light) !important;
}
/* The collapsed field reads like a brass-framed station selector. */
.ms-showcase .wrap,
.ms-showcase .secondary-wrap {
background: linear-gradient(180deg, #f7efda, #e7d8b2) !important;
border: 1px solid var(--ms-brass) !important;
border-radius: 8px !important;
box-shadow: inset 0 1px 2px rgba(255,255,255,0.6), inset 0 -3px 8px rgba(90,60,20,0.18) !important;
}
.ms-showcase input {
background: transparent !important;
color: #21170f !important;
font-family: var(--ms-font-body) !important;
font-weight: 600 !important;
border: none !important;
}
/* Brass chevron so it's obviously a control. */
.ms-showcase .icon-wrap,
.ms-showcase svg {
color: var(--ms-brass-dark) !important;
fill: var(--ms-brass-dark) !important;
opacity: 1 !important;
}
/* The open options list: dark walnut so it sits on the cabinet, not a white slab. */
.ms-showcase ul.options,
.ms-telegram ul.options {
background: linear-gradient(180deg, rgba(34,23,15,0.98), rgba(22,15,10,0.98)) !important;
border: 1px solid var(--ms-brass) !important;
border-radius: 8px !important;
box-shadow: 0 14px 28px rgba(0,0,0,0.5) !important;
color: #f3e9d2 !important;
z-index: 40 !important;
}
.ms-showcase ul.options li,
.ms-telegram ul.options li {
color: #f3e9d2 !important;
background: transparent !important;
}
.ms-showcase ul.options li:hover,
.ms-showcase ul.options li.selected,
.ms-showcase ul.options li.active,
.ms-telegram ul.options li:hover,
.ms-telegram ul.options li.active {
background: rgba(255, 184, 77, 0.18) !important;
color: #fff3d8 !important;
}
/* Only the ON AIR button is the big brass lozenge. (Scoping to .primary keeps
the audio recorder/player's own controls — play, skip, trim, clear — as their
compact default icon buttons instead of bloated brass pills.) */
.ms-telegram button.primary {
position: relative !important;
border-radius: 999px !important;
background:
radial-gradient(circle at 50% 30%, var(--ms-brass-light), var(--ms-brass) 42%, var(--ms-brass-dark) 88%) !important;
color: #2a1709 !important;
border: 1px solid #f3d99a !important;
font-family: var(--ms-font-ident) !important;
font-weight: 700 !important;
letter-spacing: 0.28em !important;
text-transform: uppercase !important;
padding: 12px 22px 12px 38px !important;
box-shadow:
inset 0 2px 2px rgba(255, 245, 215, 0.6),
inset 0 -5px 10px rgba(60, 40, 14, 0.4),
0 8px 18px rgba(0,0,0,0.4) !important;
transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease !important;
}
/* Audio recorder + player controls: small, neutral icon buttons that read on
the slip — never brass lozenges. */
.ms-callin button:not(.primary),
.ms-output button:not(.primary) {
border-radius: 7px !important;
letter-spacing: normal !important;
text-transform: none !important;
font-family: var(--ms-font-body) !important;
font-weight: 500 !important;
padding: 4px 9px !important;
background: rgba(243, 233, 210, 0.9) !important;
color: #2a1709 !important;
border: 1px solid rgba(201, 168, 106, 0.55) !important;
box-shadow: none !important;
}
.ms-callin button:not(.primary):hover,
.ms-output button:not(.primary):hover {
background: #fffaf0 !important;
border-color: var(--ms-brass) !important;
}
/* Red transmit lamp embedded in the ON AIR button. */
.ms-telegram button.primary::before {
content: "";
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
width: 11px;
height: 11px;
border-radius: 999px;
background: radial-gradient(circle at 36% 30%, #ff9b6e, var(--ms-red) 60%, #6e1a0e);
box-shadow: 0 0 10px rgba(226, 80, 47, 0.85), inset 0 0 3px rgba(0,0,0,0.5);
animation: ms-neon 2.6s ease-in-out infinite;
}
.ms-telegram button.primary:hover {
filter: brightness(1.06);
transform: translateY(-1px);
box-shadow:
inset 0 2px 2px rgba(255, 245, 215, 0.7),
inset 0 -5px 10px rgba(60, 40, 14, 0.4),
0 12px 24px rgba(0,0,0,0.46),
0 0 22px rgba(255, 184, 77, 0.3) !important;
}
.ms-telegram button.primary:active {
transform: translateY(1px);
box-shadow: inset 0 3px 8px rgba(40, 24, 8, 0.55) !important;
}
.ms-status {
border-radius: 5px;
border: 1px solid rgba(255, 184, 77, 0.34);
background:
repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 3px),
linear-gradient(180deg, rgba(18, 12, 8, 0.72), rgba(8, 6, 4, 0.7));
color: #ffd99a;
font-family: var(--ms-font-ident);
font-weight: 500;
font-size: 0.8rem;
letter-spacing: 0.22em;
margin-top: 12px;
min-height: 44px;
padding: 13px 15px;
text-transform: uppercase;
text-shadow: 0 0 12px rgba(255, 184, 77, 0.4);
}
.ms-status.is-live {
border-color: rgba(255, 184, 77, 0.6);
box-shadow: inset 0 0 18px rgba(255, 184, 77, 0.12), 0 0 22px rgba(255, 184, 77, 0.22);
animation: ms-flicker 4s ease-in-out infinite;
}
.ms-slip {
position: relative;
overflow: hidden;
border-radius: 5px !important;
border: 1px solid rgba(111, 72, 30, 0.48) !important;
background:
linear-gradient(90deg, rgba(125, 83, 38, 0.08) 0 1px, transparent 1px 100%),
repeating-linear-gradient(0deg, transparent 0 29px, rgba(89, 55, 24, 0.16) 29px 30px),
#f3e9d2 !important;
padding: 14px 16px 16px !important;
box-shadow: inset 0 0 24px rgba(72, 40, 16, 0.12), 0 10px 22px rgba(0,0,0,0.18) !important;
}
.ms-slip::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at 14% 24%, rgba(83, 51, 23, 0.08), transparent 9rem),
linear-gradient(90deg, transparent 0 72%, rgba(95, 58, 25, 0.08));
mix-blend-mode: multiply;
}
.ms-slip-head {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: end;
color: #6b4220;
font-family: var(--ms-font-script);
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1.5px dashed rgba(111, 72, 30, 0.4);
}
.ms-slip-head strong {
display: block;
color: var(--ms-red);
font-size: 1rem;
letter-spacing: 0.14em;
}
.ms-slip-head span:last-child {
text-align: right;
}
.ms-slip .form,
.ms-slip .wrap,
.ms-slip label,
.ms-slip textarea {
position: relative;
z-index: 1;
}
.ms-slip label > span {
color: #583418 !important;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.ms-slip textarea {
min-height: 86px !important;
resize: vertical !important;
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
color: #21170f !important;
box-shadow: none !important;
font-family: var(--ms-font-script) !important;
font-size: 0.98rem !important;
line-height: 1.85 !important;
}
.ms-slip textarea::placeholder {
color: rgba(88, 52, 24, 0.66) !important;
}
.ms-genre label {
color: #f3e9d2 !important;
}
/* ---- Radio option pills (Genre / Render mode / Writer): make the
selected station unmistakable — a lit brass pill with dark ink. ---- */
.ms-telegram .wrap label {
border: 1px solid rgba(201, 168, 106, 0.38) !important;
border-radius: 999px !important;
background: rgba(0, 0, 0, 0.28) !important;
padding: 6px 14px !important;
transition: background 120ms ease, box-shadow 120ms ease, border-color 120ms ease !important;
}
.ms-telegram .wrap label:hover {
border-color: var(--ms-brass) !important;
background: rgba(255, 184, 77, 0.12) !important;
}
.ms-telegram .wrap label.selected,
.ms-telegram .wrap label:has(input:checked) {
background:
radial-gradient(circle at 50% 28%, var(--ms-brass-light), var(--ms-brass) 62%, var(--ms-brass-dark) 100%) !important;
border-color: var(--ms-brass-light) !important;
box-shadow:
inset 0 1px 1px rgba(255, 245, 215, 0.6),
0 0 12px rgba(255, 184, 77, 0.45) !important;
}
.ms-telegram .wrap label.selected span,
.ms-telegram .wrap label:has(input:checked) span {
color: #2a1709 !important;
font-weight: 700 !important;
}
/* Recolor the native radio dot so the checked state reads on the brass pill. */
.ms-telegram .wrap label input[type="radio"] {
accent-color: var(--ms-red) !important;
}
.ms-output {
max-width: 780px;
margin: 18px auto 0 !important;
border-radius: 10px;
border: 1px solid rgba(201, 168, 106, 0.24) !important;
background: linear-gradient(180deg, rgba(20, 14, 9, 0.78), rgba(10, 7, 5, 0.78)) !important;
padding: 18px !important;
}
.ms-script {
position: relative;
overflow: hidden;
border-radius: 5px;
border: 1px solid rgba(111, 72, 30, 0.48);
background:
repeating-linear-gradient(0deg, transparent 0 27px, rgba(89, 55, 24, 0.12) 27px 28px),
#f3e9d2;
color: #21170f;
padding: 22px 24px 24px;
font-family: var(--ms-font-script);
box-shadow: inset 0 0 24px rgba(72, 40, 16, 0.12), 0 10px 22px rgba(0,0,0,0.22);
}
/* Red "ON AIR" cue stamp in the script corner. */
.ms-script::after {
content: "ON AIR";
position: absolute;
top: 18px;
right: 20px;
transform: rotate(7deg);
background: rgba(181, 52, 31, 0.12);
color: rgba(181, 52, 31, 0.66);
border: 2px solid rgba(181, 52, 31, 0.5);
border-radius: 4px;
font-family: var(--ms-font-ident);
font-weight: 700;
letter-spacing: 0.22em;
font-size: 0.72rem;
padding: 5px 12px;
pointer-events: none;
}
.ms-script-head {
border-bottom: 2px solid rgba(89, 55, 24, 0.42);
padding-bottom: 12px;
margin-bottom: 14px;
}
.ms-script-title {
margin: 0;
font-family: var(--ms-font-deco);
font-size: clamp(1.25rem, 2.8vw, 1.85rem);
letter-spacing: 0.04em;
color: #5a1d10;
}
.ms-script-logline {
margin: 8px 0 0;
font-family: var(--ms-font-body);
font-style: italic;
font-size: 1.05rem;
color: #4a3320;
}
.ms-script-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
font-family: var(--ms-font-ident);
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.ms-script-tag {
border: 1px solid rgba(111, 72, 30, 0.5);
border-radius: 999px;
padding: 2px 10px;
color: #5a3a1c;
}
.ms-script-cast {
display: grid;
gap: 4px;
margin-bottom: 16px;
font-size: 0.84rem;
}
.ms-script-cast-row strong {
color: #5a1d10;
letter-spacing: 0.04em;
}
.ms-script-cast-row span {
color: #6a4a2c;
}
.ms-scene-slug {
margin: 14px 0 8px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #5a3a1c;
}
.ms-scene-sfx {
color: #8a5a2c;
font-style: italic;
margin: 4px 0 10px;
font-size: 0.82rem;
}
.ms-line {
display: grid;
grid-template-columns: minmax(86px, 22%) 1fr;
gap: 10px;
padding: 3px 0;
line-height: 1.5;
}
.ms-script,
.ms-line-text,
.ms-script-cast-row {
color: #21170f;
}
.ms-line-who {
text-align: right;
font-weight: 700;
color: #5a1d10;
letter-spacing: 0.04em;
}
.ms-line-delivery {
color: #9a6a34;
font-style: italic;
}
.ms-script-empty {
color: #6a4a2c;
font-style: italic;
}
.ms-footer {
max-width: 780px;
margin: 16px auto 0;
text-align: center;
font-family: var(--ms-font-ident);
font-size: 0.74rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(243, 233, 210, 0.5);
}
.ms-footer a {
color: var(--ms-brass);
text-decoration: none;
border-bottom: 1px dotted rgba(201, 168, 106, 0.5);
}
.ms-footer a:hover {
color: var(--ms-amber);
}
@media (max-width: 760px) {
.ms-line {
grid-template-columns: 1fr;
gap: 1px;
padding: 6px 0;
}
.ms-line-who {
text-align: left;
}
}
@media (max-width: 760px) {
.gradio-container {
padding: 14px 10px 28px !important;
}
.ms-radio {
min-height: 0;
padding: 20px;
border-radius: 24px 24px 18px 18px;
}
.ms-brand {
align-items: flex-start;
flex-direction: column;
letter-spacing: 0.12em;
}
.ms-face {
grid-template-columns: 1fr;
}
.ms-speaker {
min-height: 170px;
}
.ms-scanner,
.ms-station {
font-size: 0.72rem;
letter-spacing: 0.08em;
}
}
@keyframes ms-lamp-pulse {
from {
opacity: 0.62;
box-shadow: 0 0 10px var(--ms-amber), 0 0 18px rgba(255,184,77,0.35);
}
to {
opacity: 1;
box-shadow: 0 0 18px var(--ms-amber), 0 0 44px rgba(255,184,77,0.68);
}
}
@keyframes ms-needle-scan {
from {
left: 8%;
}
to {
left: 92%;
}
}
@keyframes ms-frequency-glow {
0%, 100% {
color: #583418;
opacity: 0.55;
}
50% {
color: #9f2f1c;
opacity: 1;
}
}
@keyframes ms-flicker {
0%, 100% { opacity: 1; }
48% { opacity: 1; }
50% { opacity: 0.94; }
52% { opacity: 1; }
78% { opacity: 0.97; }
}
@keyframes ms-grain {
0% { transform: translate(0, 0); }
25% { transform: translate(-6%, 4%); }
50% { transform: translate(4%, -6%); }
75% { transform: translate(-4%, -3%); }
100% { transform: translate(5%, 5%); }
}
@keyframes ms-neon {
0%, 100% {
opacity: 1;
box-shadow: 0 0 12px var(--ms-amber), 0 0 26px rgba(255,184,77,0.55);
}
43% { opacity: 1; }
45% {
opacity: 0.55;
box-shadow: 0 0 6px var(--ms-amber), 0 0 12px rgba(255,184,77,0.3);
}
47% { opacity: 1; }
92% { opacity: 1; }
94% { opacity: 0.7; }
}
@keyframes ms-cone {
0%, 100% { opacity: 0.5; transform: translate(-50%, -50%) scale(1); }
50% { opacity: 0.78; transform: translate(-50%, -50%) scale(1.06); }
}
@keyframes ms-cone-live {
0%, 100% { opacity: 0.7; transform: translate(-50%, -50%) scale(0.96); }
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.12); }
}
/* ============================================================
LEGIBILITY HARDENING
The dial glass and the script paper are LIGHT insets inside the dark
walnut room. Gradio paints HTML text with --body-text-color, which we set
to cream for the dark chrome — that cream leaked onto these light surfaces
and made the frequencies, station line, and script text invisible. Force
ink on those surfaces with !important (Gradio's variable rule wins
otherwise), then re-assert the few accent colors so they survive.
============================================================ */
.ms-dial,
.ms-dial div,
.ms-dial span,
.ms-script,
.ms-script p,
.ms-script div,
.ms-script span,
.ms-script strong {
color: #21170f !important;
}
/* Accent colors re-asserted after the blanket ink rule above. */
.ms-frequency span.is-tuned { color: #8a2114 !important; }
.ms-station strong { color: var(--ms-red) !important; }
.ms-scanner { color: var(--ms-red) !important; }
.ms-script-title,
.ms-line-who,
.ms-script-cast-row strong { color: #5a1d10 !important; }
.ms-script-logline { color: #4a3320 !important; }
.ms-scene-sfx { color: #8a5a2c !important; }
.ms-line-delivery { color: #9a6a34 !important; }
"""
def _radio_shell_html(
genre: Genre | str = Genre.WEIRD,
state: str = "idle",
scanner_text: str = ON_AIR_STAGE_TEXT,
) -> str:
tuned = _coerce_genre(genre)
current = STATIONS[tuned]
is_scanning = state == "scanning"
frequencies = "".join(
f'<span class="{"is-tuned" if station_genre == tuned else ""}">'
f'{station["frequency"]}</span>'
for station_genre, station in STATIONS.items()
)
station_line = " · ".join(
f'<strong>{station["call"]}</strong>' if station_genre == tuned else station["call"]
for station_genre, station in STATIONS.items()
)
dial_message = (
f'<div class="ms-scanner">{scanner_text}</div>'
if is_scanning
else f'<div class="ms-station">{station_line}</div>'
)
radio_class = "ms-radio is-scanning" if is_scanning else "ms-radio"
return f"""
<div class="ms-stage">
<div class="{radio_class}" aria-label="Midnight Static radio cabinet">
<span class="ms-screw tl"></span>
<span class="ms-screw tr"></span>
<span class="ms-screw bl"></span>
<span class="ms-screw br"></span>
<div class="ms-brand">
<span class="ms-brand-name">Midnight Static</span>
<span class="ms-onair"><span class="ms-lamp"></span>ON AIR</span>
</div>
<div class="ms-face">
<div class="ms-speaker"></div>
<div class="ms-panel">
<div class="ms-dial" style="--ms-needle-left: {current["left"]}%;">
<div class="ms-frequency">
{frequencies}
</div>
<div class="ms-needle"></div>
{dial_message}
</div>
<div class="ms-knobs" aria-hidden="true">
<div class="ms-knob"></div>
<div class="ms-knob"></div>
</div>
</div>
</div>
</div>
</div>
"""
def _coerce_genre(value: Genre | str) -> Genre:
try:
return Genre(value)
except ValueError:
return Genre.WEIRD
def generate_broadcast(
premise: str,
mode: str = "placeholder",
writer: str = "fixture",
genre: str = Genre.WEIRD.value,
output_dir: str = "outputs",
) -> tuple[str, str]:
"""Generate a broadcast and return `(audio_path, script_json)` for Gradio."""
result = run_fixture_pipeline(
premise=premise,
output_dir=Path(output_dir),
mode=mode,
writer_name=writer,
genre=genre,
)
script_path = Path(result["script"])
return result["audio"], script_path.read_text(encoding="utf-8")
def transcribe_call_in(audio_path):
"""Transcribe a call-in recording into the premise box for confirm/edit.
Never auto-submits: the transcript only populates the editable premise field;
the caller reviews/edits it and presses ON AIR themselves. Degrades to a
status message if ASR is unavailable or no speech is heard.
"""
import gradio as gr
if not audio_path:
return gr.update(), status_html("CALL IN: NOTHING RECORDED")
try:
from .asr import transcribe
text = transcribe(audio_path)
except Exception: # noqa: BLE001 - ASR runtime missing, etc.
return gr.update(), status_html("CALL IN UNAVAILABLE - TYPE YOUR PREMISE")
if not text:
return gr.update(), status_html("CALL IN: NO SPEECH HEARD - TYPE YOUR PREMISE")
return text, status_html("CALL IN TRANSCRIBED - REVIEW, THEN PRESS ON AIR")
def radio_shell_for_genre(genre: str) -> str:
return _radio_shell_html(genre)
def on_air_start(genre: str) -> tuple[str, str]:
return _radio_shell_html(genre, state="scanning"), status_html(
"WRITING ... CASTING ... FOLEY ... SCORING ..."
)
def status_html(message: str = "RERUN READY") -> str:
live_class = " is-live" if message != "RERUN READY" else ""
return f'<div class="ms-status{live_class}">{message}</div>'
def script_display_html(script_json: str) -> str:
"""Render a script JSON string as a readable, in-world broadcast script."""
try:
payload = json.loads(script_json)
except (json.JSONDecodeError, TypeError):
return '<div class="ms-script"><p class="ms-script-empty">No script on air yet.</p></div>'
def esc(value: object) -> str:
return html.escape(str(value))
title = esc(payload.get("title", "Untitled Broadcast"))
logline = esc(payload.get("logline", ""))
genre = str(payload.get("genre", ""))
station = STATIONS.get(_coerce_genre(genre)) if genre else None
seconds = payload.get("estimated_seconds")
cast = payload.get("cast", []) or []
cast_names = {str(member.get("id")): str(member.get("name", member.get("id", "?"))) for member in cast}
meta_tags = []
if station:
meta_tags.append(f'{esc(station["frequency"])} {esc(station["call"])}')
if genre:
meta_tags.append(esc(genre.replace("_", " ")))
if seconds:
meta_tags.append(f"~{esc(seconds)}s")
meta = "".join(f'<span class="ms-script-tag">{tag}</span>' for tag in meta_tags)
cast_rows = "".join(
f'<div class="ms-script-cast-row"><strong>{esc(member.get("name", member.get("id", "?")))}</strong>'
f' — <span>{esc(member.get("description", "")) or esc(member.get("voice", ""))}</span></div>'
for member in cast
)
scene_blocks = []
for scene in payload.get("scenes", []) or []:
slug = esc(scene.get("title", "Scene"))
block = [f'<div class="ms-scene-slug">{slug}</div>']
sfx = [esc(item) for item in (scene.get("sfx") or [])]
if sfx:
block.append(f'<div class="ms-scene-sfx">SFX: {", ".join(sfx)}</div>')
for line in scene.get("lines", []) or []:
who = esc(cast_names.get(str(line.get("cast")), line.get("cast", "?")))
text = esc(line.get("text", ""))
delivery = str(line.get("delivery", "neutral"))
delivery_tag = (
f' <span class="ms-line-delivery">({esc(delivery)})</span>'
if delivery and delivery != "neutral"
else ""
)
block.append(
f'<div class="ms-line"><div class="ms-line-who">{who}</div>'
f'<div class="ms-line-text">{text}{delivery_tag}</div></div>'
)
scene_blocks.append("".join(block))
body = "".join(scene_blocks) or '<p class="ms-script-empty">Tune in for the next broadcast.</p>'
cast_section = f'<div class="ms-script-cast">{cast_rows}</div>' if cast_rows else ""
return f"""
<div class="ms-script">
<div class="ms-script-head">
<h3 class="ms-script-title">{title}</h3>
<p class="ms-script-logline">{logline}</p>
<div class="ms-script-meta">{meta}</div>
</div>
{cast_section}
{body}
</div>
"""
def load_showcase_for_ui(label: str) -> tuple[str | None, str, str, str, str, str]:
audio_path, script_json = load_showcase(label)
payload = json.loads(script_json)
genre = str(payload.get("genre", Genre.WEIRD.value))
return (
audio_path,
script_display_html(script_json),
script_json,
genre,
_radio_shell_html(genre),
status_html("RERUN READY"),
)
# The radio is a dark-walnut room with light paper/dial insets — designed for
# Gradio's light mode. Force it so a visitor's system dark-mode preference can't
# flip block backgrounds dark and wash out the dial text and script notes.
# Injected into <head> so it runs before first paint; the guard reloads at most
# once. (Gradio 6 moved `js=` to launch(); `head=` works from the Blocks here.)
FORCE_LIGHT_THEME_HEAD = """
<script>
(function () {
try {
var url = new URL(window.location.href);
if (url.searchParams.get('__theme') !== 'light') {
url.searchParams.set('__theme', 'light');
window.location.replace(url.href);
}
} catch (e) {}
})();
</script>
"""
def _nemotron_runs_here() -> bool:
"""True when the local Nemotron runtime (llama.cpp) is installed.
The hosted cpu-basic Space can't package llama-cpp-python (musl/glibc + the
SDK builder can't compile it), so Nemotron falls back to the fixture there.
The local and GPU builds have it, so the writer note adapts to the truth of
whichever environment is running.
"""
import importlib.util
return importlib.util.find_spec("llama_cpp") is not None
def _writer_radio_kwargs() -> dict:
if _nemotron_runs_here():
return {
"choices": [
("Fixture · instant", "fixture"),
("Nemotron 4B · local, ~2 min", "nemotron"),
],
"value": "fixture",
"label": "Writer",
"info": "Nemotron 4B writes an original script on CPU (~1–2 min); fixture is instant.",
}
return {
"choices": [
("Fixture · instant", "fixture"),
("Nemotron 4B · full build only", "nemotron"),
],
"value": "fixture",
"label": "Writer",
"info": (
"The real Nemotron 4B writer runs in the local & GPU builds (shown in the demo "
"video). On this free CPU Space it falls back to the instant fixture writer."
),
}
def create_radio_app():
"""Create the primary styled radio Gradio Blocks app (served at `/`)."""
import gradio as gr
showcase_audio, showcase_script = default_showcase()
labels = showcase_labels()
with gr.Blocks(title="Midnight Static") as app:
# Persistent style block: kept in its own gr.HTML so it survives every
# event re-render. The cabinet (`radio_shell`) below is rewritten on
# genre/showcase/ON-AIR changes — if the <style> lived there too, the
# first interaction would wipe all CSS and revert the page to plain
# Gradio chrome.
gr.HTML(f"<style>{INLINE_FONTS_CSS}{RADIO_CSS}</style>")
radio_shell = gr.HTML(_radio_shell_html())
with gr.Group(elem_classes=["ms-telegram"]):
showcase = gr.Dropdown(
choices=labels,
value=labels[0] if labels else None,
label="Showcase rerun",
elem_classes=["ms-showcase"],
)
with gr.Row():
with gr.Group(elem_classes=["ms-slip"]):
gr.HTML(
"""
<div class="ms-slip-head">
<span><strong>Telegram</strong>Dispatch to the midnight desk</span>
<span>Max 300 chars</span>
</div>
"""
)
premise = gr.Textbox(
label="Premise",
value="a phone booth that rings under the sea",
placeholder=PREMISE_PLACEHOLDER,
max_length=300,
max_lines=3,
elem_classes=["ms-premise"],
)
call_in = gr.Audio(
sources=["microphone"],
type="filepath",
label="CALL IN — speak a premise (transcript fills the slip; review before ON AIR)",
elem_classes=["ms-callin"],
)
with gr.Row():
genre = gr.Radio(
choices=GENRE_CHOICES,
value=Genre.WEIRD.value,
label="Genre station",
elem_classes=["ms-genre"],
)
mode = gr.Radio(
choices=["crude", "placeholder"],
value="crude",
label="Render mode",
)
writer = gr.Radio(**_writer_radio_kwargs())
generate = gr.Button("ON AIR", variant="primary")
status = gr.HTML(status_html())
with gr.Group(elem_classes=["ms-output"]):
audio = gr.Audio(label="Broadcast", type="filepath", value=showcase_audio)
script_view = gr.HTML(script_display_html(showcase_script))
with gr.Accordion("Script JSON", open=False):
script = gr.Code(label="Script JSON", language="json", value=showcase_script)
gr.HTML(
'<div class="ms-footer">Trouble tuning in? '
'<a href="/plain/">Switch to the plain studio feed</a></div>'
)
def _run(premise_value: str, mode_value: str, writer_value: str, genre_value: str):
audio_path, script_json = generate_broadcast(
premise=premise_value,
mode=mode_value,
writer=writer_value,
genre=genre_value,
)
parsed = json.loads(script_json)
tuned_genre = str(parsed.get("genre", genre_value))
pretty_json = json.dumps(parsed, indent=2)
return (
audio_path,
script_display_html(pretty_json),
pretty_json,
_radio_shell_html(tuned_genre),
status_html("ON AIR - BROADCAST READY"),
)
generate_event = generate.click(
on_air_start,
inputs=[genre],
outputs=[radio_shell, status],
)
generate_event.then(
_run,
inputs=[premise, mode, writer, genre],
outputs=[audio, script_view, script, radio_shell, status],
)
genre.change(radio_shell_for_genre, inputs=[genre], outputs=[radio_shell])
showcase.change(
load_showcase_for_ui,
inputs=[showcase],
outputs=[audio, script_view, script, genre, radio_shell, status],
)
# CALL IN: when the caller stops recording, transcribe into the premise
# slip for review — it does NOT trigger ON AIR (no auto-submit).
call_in.stop_recording(
transcribe_call_in, inputs=[call_in], outputs=[premise, status]
)
# REQ-02 insurance: a CSS-free fallback served at `/plain` by the same
# `demo.launch()` (native Gradio multipage routing, HF-Spaces safe). Must be
# declared outside the Blocks() context. Hidden from the navbar so the main
# radio keeps its immersive chrome.
with app.route("Plain", "/plain", show_in_navbar=False):
_build_fallback_ui()
return app
# Backwards-compatible alias: the historical name for the primary app.
create_plain_app = create_radio_app
def _build_fallback_ui():
"""Build the minimal, CSS-free fallback controls into the current context.
Usable inside a standalone ``gr.Blocks`` or a ``Blocks.route`` page. Uses
only default Gradio components so the station stays usable if the styled
radio UI fails to render (custom fonts/CSS blocked, old browser, etc.).
"""
import gradio as gr
showcase_audio, showcase_script = default_showcase()
labels = showcase_labels()
gr.Markdown(
"# Midnight Static — plain studio feed\n"
"Minimal fallback interface. The full vintage radio lives on "
"[the main page](/)."
)
showcase = gr.Dropdown(
choices=labels,
value=labels[0] if labels else None,
label="Showcase rerun",
)
premise = gr.Textbox(
label="Premise",
value="a phone booth that rings under the sea",
placeholder=PREMISE_PLACEHOLDER,
max_length=300,
max_lines=3,
)
with gr.Row():
genre = gr.Dropdown(
choices=GENRE_CHOICES,
value=Genre.WEIRD.value,
label="Genre station",
)
mode = gr.Radio(choices=["crude", "placeholder"], value="crude", label="Render mode")
writer = gr.Radio(**_writer_radio_kwargs())
generate = gr.Button("Generate broadcast", variant="primary")
audio = gr.Audio(label="Broadcast", type="filepath", value=showcase_audio)
script = gr.Code(label="Script JSON", language="json", value=showcase_script)
def _run(premise_value: str, mode_value: str, writer_value: str, genre_value: str):
audio_path, script_json = generate_broadcast(
premise=premise_value,
mode=mode_value,
writer=writer_value,
genre=genre_value,
)
return audio_path, json.dumps(json.loads(script_json), indent=2)
generate.click(_run, inputs=[premise, mode, writer, genre], outputs=[audio, script])
showcase.change(load_showcase, inputs=[showcase], outputs=[audio, script])
def create_fallback_app():
"""Standalone Gradio Blocks wrapping the CSS-free fallback (REQ-02 insurance)."""
import gradio as gr
with gr.Blocks(title="Midnight Static — Plain", analytics_enabled=False) as app:
_build_fallback_ui()
return app