"""mise — the film-structure reference finder, live. Upload a clip (or browse the public-domain atlas) and find films that are structurally kin — PER AXIS, because the axes are measured near-independent (tone vs rhythm distance correlation: 0.11). There is no single "style similarity" and this instrument refuses to invent one: a work can share a palette with one film and a cutting rhythm with another, and both facts are true. Design law (mise): descriptive only. No scores, no rankings of quality, no "correct" anything. """ import os, pickle import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import gradio as gr from huggingface_hub import hf_hub_download import mise from mise.aggregate import artist_signature AXES = {"A": "composition — where visual mass sits", "E": "tone — light, warmth, palette", "D": "rhythm — the cut", "B": "staging — bodies in the frame", "M": "motion — the camera's gesture"} BG, INK, ACC = "#0e0d0b", "#e8e4da", "#c8a24a" # corpus ships with the Space (public-domain-derived signatures only); hub fallback for updates _local = os.path.join(os.path.dirname(__file__), "atlas_v2.pkl") if os.path.exists(_local): _d = pickle.load(open(_local, "rb")) else: _p = hf_hub_download("Chucks90/football-gsr-data", "mise_corpus/atlas_v2.pkl", repo_type="dataset") _d = pickle.load(open(_p, "rb")) SIGS, META = _d["sigs"], _d["meta"] NAMES = sorted(SIGS) # per-axis embedding blocks for the corpus (computed once) BLOCKS = {} for ax in AXES: E = np.array([artist_signature(n, [SIGS[n]], axes={ax}).embedding for n in NAMES], float) if E.std(0).max() > 1e-9: mu, sd = E.mean(0), E.std(0) sd[sd < 1e-9] = 1.0 BLOCKS[ax] = (E, mu, sd) def _neighbours_for(emb_by_axis, exclude=None, k=3): out = {} for ax, (E, mu, sd) in BLOCKS.items(): if ax not in emb_by_axis: continue z = (E - mu) / sd q = (np.array(emb_by_axis[ax], float) - mu) / sd d = np.linalg.norm(z - q, axis=1) order = [i for i in np.argsort(d) if NAMES[i] != exclude][:k] out[ax] = [(NAMES[i], float(d[i])) for i in order] return out def _md(neigh, title): lines = [f"### {title}", "_Each axis answers separately — there is no single 'style match'._", ""] for ax, label in AXES.items(): if ax not in neigh: continue lines.append(f"**{label}**") for n, d in neigh[ax]: m = META.get(n, {}) lines.append(f"- {n} · *{m.get('genre', m.get('collection', ''))}* `{d:.2f}`") lines.append("") return "\n".join(lines) def _portrait(sig, title): """A small structural portrait: shot lengths coloured by tonal warmth. Description, not verdict.""" shots = sig.per_shot L = [s["duration"] for s in shots] warm = [s["E"].warm_fraction if s.get("E") else 0.5 for s in shots] key = [s["E"].key[0] if s.get("E") else 0.5 for s in shots] fig, ax = plt.subplots(figsize=(9.5, 2.6), facecolor=BG) ax.set_facecolor(BG) cols = [(0.35 + 0.6 * w, 0.42 + 0.25 * k, 0.85 - 0.55 * w) for w, k in zip(warm, key)] ax.bar(range(len(L)), L, width=0.92, color=cols) ax.set_xticks([]); ax.set_yticks([]) for s in ax.spines.values(): s.set_visible(False) ax.set_title(f"{title} — every bar a shot; height = duration, colour = tonal warmth/key", color=INK, fontsize=10, pad=8) fig.tight_layout() return fig def browse(name): sig = SIGS[name] emb = {ax: artist_signature(name, [sig], axes={ax}).embedding for ax in BLOCKS} return _portrait(sig, name), _md(_neighbours_for(emb, exclude=name), f"structural kin of “{name}”") def analyze(file): if not file: raise gr.Error("Upload a clip first — a scene or a whole film both work.") path = file if isinstance(file, str) else file.name tl, _ = mise.analyze_windows(path, n_windows=3, frames_per_window=300, motion=True, max_width=560) if len(tl.shots) < 3: raise gr.Error(f"Only {len(tl.shots)} shots detected — the clip may be too short or a single take.") sig = mise.compose_signature(tl) emb = {ax: artist_signature("you", [sig], axes={ax}).embedding for ax in BLOCKS if ax != "B"} md = _md(_neighbours_for(emb), "films structurally kin to your clip") md += ("\n_Staging (B) is compared only within the atlas — the upload path skips person detection to stay " "fast._\n\n_Your clip is analysed in memory and not stored._") return _portrait(sig, "your clip"), md with gr.Blocks(title="mise — reference finder") as demo: gr.Markdown("# mise — find your film's structural kin\n" "An instrument, not a judge: it reads **composition · tone · rhythm · staging · motion** as " "separate axes (they are measured near-independent) and finds public-domain films that share " "each one. *Palette like yours, cutting unlike yours — both answers, separately.*") with gr.Tab("Your clip"): with gr.Row(): with gr.Column(scale=3): up_plot = gr.Plot(label="") up_file = gr.File(label="video clip", file_types=["video"], type="filepath") up_btn = gr.Button("read the structure", variant="primary") with gr.Column(scale=2): up_md = gr.Markdown() up_btn.click(analyze, [up_file], [up_plot, up_md]) with gr.Tab("Browse the atlas"): with gr.Row(): with gr.Column(scale=3): b_plot = gr.Plot(label="") b_name = gr.Dropdown(NAMES, value=NAMES[0], label=f"{len(NAMES)} public-domain films") with gr.Column(scale=2): b_md = gr.Markdown() b_name.change(browse, [b_name], [b_plot, b_md]) demo.load(browse, [b_name], [b_plot, b_md]) if __name__ == "__main__": demo.launch(theme=gr.themes.Base(primary_hue="amber", neutral_hue="stone"))