Spaces:
Sleeping
Sleeping
| """A2A-Video demo UI, rebuilt to match video4m-demo-prototype.html. | |
| Two tabs: | |
| - Any-to-Any Generation: pick one input representation (video / caption / | |
| abstract modality of an example) and an ordered generation chain. The | |
| input card and all predictions appear together in a 3-per-row grid when | |
| the chain finishes; mid-run feedback is a plain "generating" line (the | |
| backend still streams via generate_any_to_any_stream — the display is | |
| buffered). | |
| - Future Prediction: ported functionally unchanged from the previous app | |
| (single non-streaming call, two-stage layout-then-generate), restyled. | |
| Compatibility: written against the API subset present in BOTH gradio 4.44.1 | |
| (the cluster conda env actually used by run_demo.sh) and gradio 5.6.0 (the | |
| pin in requirements.txt / the HF Space sdk_version). Custom CSS targets our | |
| own elem_ids/elem_classes and leaf tags only, never gradio-internal class | |
| names, so it survives the DOM differences between the two versions. | |
| Env toggles (both off by default): | |
| FOURM_FAKE_STREAM=1 exercise the full streaming UI with synthetic | |
| events + placeholder media, no model / GPU. | |
| FOURM_DISABLE_STREAMING=1 kill switch: run the proven single-call | |
| generate_any_to_any and synthesize the same | |
| events from its returned dict at the end. | |
| """ | |
| import base64 | |
| import io | |
| import os | |
| import tempfile | |
| import time | |
| import traceback | |
| from pathlib import Path | |
| from PIL import Image as PILImage | |
| import gradio as gr | |
| import gradio_client.utils as _gradio_client_utils | |
| import numpy as np | |
| import spaces | |
| from decord import VideoReader, cpu | |
| # gradio==5.6.0 pins gradio_client==1.4.3 exactly, and that exact pair has a | |
| # bug: _json_schema_to_python_type() crashes (TypeError: argument of type | |
| # 'bool' is not iterable) whenever a component's JSON schema has | |
| # `additionalProperties: true` -- a bare bool, which pydantic emits for | |
| # Dict[str, Any]-shaped fields -- because get_type() unconditionally does | |
| # `"const" in schema`. This fires on every page load (routes.py's `/` handler | |
| # builds the API-docs schema unconditionally), so it's not optional to avoid | |
| # triggering; patch the recursive step to treat a bool schema as "Any", same | |
| # as an empty `{}` schema is already treated. | |
| _original_json_schema_to_python_type = _gradio_client_utils._json_schema_to_python_type | |
| def _patched_json_schema_to_python_type(schema, defs): | |
| if isinstance(schema, bool): | |
| return "Any" | |
| return _original_json_schema_to_python_type(schema, defs) | |
| _gradio_client_utils._json_schema_to_python_type = _patched_json_schema_to_python_type | |
| # diffusers==0.28.0 (pinned for its internal unet_2d_blocks module, needed by | |
| # fourm/vq/models/uvit.py) unconditionally does `from huggingface_hub import | |
| # cached_download` at import time, in the code path that loads custom | |
| # pipeline code from the Hub -- a feature this app never uses. cached_download | |
| # itself was removed in huggingface_hub 0.26.0, and huggingface_hub is pinned | |
| # >=0.30.0 (transformers>=4.53.0, needed for V-JEPA2, requires that floor), so | |
| # the two pins are individually necessary but jointly incompatible without | |
| # this shim. hf_hub_download is a reasonable stand-in since the real | |
| # cached_download is provably dead code on every path this app exercises. | |
| import huggingface_hub as _huggingface_hub | |
| if not hasattr(_huggingface_hub, "cached_download"): | |
| _huggingface_hub.cached_download = _huggingface_hub.hf_hub_download | |
| # fourm/vq/scheduling/scheduling_{ddpm,pndm,ddim}.py each do | |
| # `from diffusers.utils import BaseOutput, randn_tensor`. diffusers 0.20.0 | |
| # (this code's original target) re-exported randn_tensor from | |
| # diffusers.utils.torch_utils at the diffusers.utils top level; 0.28.0 | |
| # dropped that re-export (BaseOutput is still re-exported, only randn_tensor | |
| # moved), so patch it back the same way rather than edit three fourm/ files. | |
| import diffusers.utils as _diffusers_utils | |
| from diffusers.utils.torch_utils import randn_tensor as _randn_tensor | |
| if not hasattr(_diffusers_utils, "randn_tensor"): | |
| _diffusers_utils.randn_tensor = _randn_tensor | |
| from helper_functions import save_video_with_imageio | |
| from inference import ( | |
| generate_any_to_any, generate_any_to_any_stream, generate_future_prediction, | |
| list_examples, list_future_examples, get_example_preview, | |
| get_example_transcription_tagged, | |
| FPS, CONFIGS, HYPERPARAM_PRESETS, | |
| FUTURE_CHAIN, FUTURE_SEED_MODALITIES, FUTURE_SEED_TOKEN_OPTIONS, | |
| SEED_TOKENS_TO_FRAMES, | |
| ) | |
| # --- modality registry -------------------------------------------------------- | |
| MODALITY_DISPLAY_NAMES = { | |
| "rgb": "RGB", | |
| "siglip": "SigLIP-2", | |
| "dinov2": "DINOv2", | |
| "vjepa": "V-JEPA-2", | |
| "caption": "Caption", | |
| "transcription": "Transcription", | |
| "det": "Bboxes", | |
| "depth": "Depth", | |
| "normal": "S. Normals", | |
| "opticalflow": "Optical Flow", | |
| } | |
| # Fixed order for the hyperparameter accordion only (unrelated to the | |
| # dynamic, chain-order-following output slots). | |
| ALL_MODALITIES = ["rgb", "depth", "normal", "opticalflow", "dinov2", "vjepa", "siglip", "det", "caption", "transcription"] | |
| TEXT_MODALITIES = {"caption", "transcription"} | |
| # Which tuned HYPERPARAM_PRESETS set fits a given input modality -- dense | |
| # visual/feature-map inputs carry a lot of information already, sparse | |
| # text-like inputs need the more exploratory sparse-to-dense tuning. | |
| DENSE_INPUT_MODALITIES = {"rgb", "depth", "normal", "opticalflow", "dinov2", "vjepa", "siglip"} | |
| # Accent colors, one per modality (prototype :root palette). rgb's original | |
| # "#c6ccd8" was tuned for contrast against a dark background -- against the | |
| # white theme it was nearly invisible, so it's a darker slate-gray here. | |
| MODALITY_COLORS = { | |
| "rgb": "#6b7280", | |
| "caption": "#a78bfa", | |
| "transcription": "#22d3ee", | |
| "dinov2": "#34d399", | |
| "siglip": "#f472b6", | |
| "depth": "#fbbf24", | |
| "normal": "#84cc16", | |
| "opticalflow": "#60a5fa", | |
| "det": "#f87171", | |
| "vjepa": "#e879f9", | |
| } | |
| MODALITY_TIPS = { | |
| "rgb": "Raw pixel video — what a camera sees", | |
| "caption": "Natural-language description of the clip", | |
| "transcription": "Per-second dense description with [SEC_n] markers", | |
| "dinov2": "Self-supervised semantic feature maps — layout and object identity", | |
| "siglip": "Vision-language feature maps aligned with text", | |
| "depth": "Per-pixel distance from the camera", | |
| "normal": "Per-pixel surface orientation", | |
| "opticalflow": "Per-pixel motion between frames", | |
| "det": "Object detections as labeled bounding boxes", | |
| "vjepa": "Video self-supervised feature maps", | |
| } | |
| # Modalities offered as "abstract representation" inputs: the prototype's | |
| # ABS_MODS extended with the two remaining feature-map modalities (siglip, | |
| # vjepa) — the backend accepts any CONFIGS key as conditioning input. | |
| ABS_MODALITIES = ["depth", "dinov2", "siglip", "vjepa", "normal", "opticalflow", "det", "transcription"] | |
| # Per-input coarse-to-fine preset chains: abstract/semantic first, concrete | |
| # pixels last — a specific tuned ladder for every possible input modality. | |
| # Edit freely; the only rule is that a list must not contain its own key. | |
| COARSE_TO_FINE_BY_INPUT = { | |
| "rgb": ["siglip", "dinov2", "vjepa", "caption", "transcription", "det", "depth", "normal", "opticalflow"], | |
| "caption": ["transcription", "siglip", "dinov2", "vjepa", "det", "opticalflow", "normal", "depth", "rgb"], | |
| "transcription": ["caption", "siglip", "dinov2", "vjepa", "det", "opticalflow", "normal", "depth", "rgb"], | |
| "dinov2": ["siglip", "vjepa", "caption", "transcription", "det", "opticalflow", "normal", "depth", "rgb"], | |
| "siglip": ["dinov2", "vjepa", "caption", "transcription", "det", "opticalflow", "normal", "depth", "rgb"], | |
| "vjepa": ["siglip", "dinov2", "caption", "transcription", "det", "opticalflow", "normal", "depth", "rgb"], | |
| "depth": ["siglip", "dinov2", "vjepa", "caption", "transcription", "det", "normal", "opticalflow", "rgb"], | |
| "normal": ["siglip", "dinov2", "vjepa", "caption", "transcription", "det", "depth", "opticalflow", "rgb"], | |
| "opticalflow": ["siglip", "dinov2", "vjepa", "caption", "transcription", "det", "depth", "normal", "rgb"], | |
| "det": ["caption", "transcription", "siglip", "dinov2", "vjepa", "depth", "normal", "opticalflow", "rgb"], | |
| } | |
| DEFAULT_INPUT_MODALITY = "rgb" | |
| DEFAULT_ABS_MODALITY = "depth" | |
| DEFAULT_CHAIN_STATE = list(COARSE_TO_FINE_BY_INPUT[DEFAULT_INPUT_MODALITY]) | |
| # Predictions are 128x128; result cards show them at native size (upscaling | |
| # just looks pixelated). | |
| VIDEO_SIZE = 128 | |
| RESULT_VIDEO_HEIGHT = 128 | |
| # 1 input slot + up to 9 chain targets (every other modality). | |
| MAX_SLOTS = 10 | |
| TEXT_OUTPUT_LINES = 6 | |
| def _hyperparam_preset_key_for_modality(input_modality): | |
| return "rgb_to_others" if input_modality in DENSE_INPUT_MODALITIES else "text_to_rgb" | |
| def _save_to_mp4(video_array): | |
| tmp_dir = tempfile.mkdtemp(prefix="4m_demo_") | |
| out_path = str(Path(tmp_dir) / "video.mp4") | |
| save_video_with_imageio(list(video_array), out_path, FPS) | |
| return out_path | |
| def _safe_preview(stem, modality_key): | |
| """get_example_preview that can never take the UI down: any fetch/cache | |
| failure (offline node, missing per-modality preview on the Hub) just | |
| means 'no preview'.""" | |
| try: | |
| return get_example_preview(stem, modality_key) | |
| except Exception: | |
| traceback.print_exc() | |
| return None | |
| def _clean_chain(input_modality, chain): | |
| """Drops the input modality from chain if it's stuck there from before | |
| a modality switch (rather than wiping the whole chain), and dedupes. | |
| """ | |
| seen = set() | |
| cleaned = [] | |
| for k in chain: | |
| if k != input_modality and k not in seen: | |
| cleaned.append(k) | |
| seen.add(k) | |
| return cleaned | |
| def _preset_chain(preset, input_modality): | |
| """The chain a preset stands for, given the current input modality. | |
| Returns None for "custom" (leave the chain exactly as the user built it). | |
| """ | |
| if preset == "coarse": | |
| fallback = ["caption", "dinov2", "depth", "rgb"] | |
| return _clean_chain(input_modality, list(COARSE_TO_FINE_BY_INPUT.get(input_modality, fallback))) | |
| if preset == "direct": | |
| return ["depth"] if input_modality == "rgb" else ["rgb"] | |
| return None | |
| def _chain_for_input_change(preset, new_input_modality, current_chain): | |
| """What the chain becomes when the input modality changes: coarse | |
| rebuilds its per-input ladder, direct keeps its single chosen target | |
| while it stays valid (falling back to the direct default if the new | |
| input swallowed it), custom just drops the new input from the chain. | |
| """ | |
| if preset == "coarse": | |
| return _preset_chain("coarse", new_input_modality) | |
| cleaned = _clean_chain(new_input_modality, current_chain) | |
| if preset == "direct": | |
| return cleaned[:1] if cleaned else _preset_chain("direct", new_input_modality) | |
| return cleaned | |
| # --- HTML fragment builders (pure) -------------------------------------------- | |
| def _step_label_html(n, title, hint=None): | |
| hint_html = f'<p class="hint">{hint}</p>' if hint else "" | |
| return f'<div class="step-label"><span class="step-num">{n}</span><h2>{title}</h2></div>{hint_html}' | |
| def _chip_html(key, is_input=False): | |
| name = MODALITY_DISPLAY_NAMES[key] | |
| in_tag = ' <span class="in-tag">in</span>' if is_input else "" | |
| return ( | |
| f'<span class="chip solid accent-{key}" title="{MODALITY_TIPS[key]}">' | |
| f'<span class="dot"></span>{name}{in_tag}</span>' | |
| ) | |
| def _card_head_html(key, role=""): | |
| """No step numbers or timings here on purpose: predictions should read | |
| as one parallel set, not a sequence.""" | |
| name = MODALITY_DISPLAY_NAMES[key] | |
| role_html = f'<span class="role">{role}</span>' if role else "" | |
| return ( | |
| f'<div class="rc-head accent-{key}"><span class="dot"></span>' | |
| f'<span class="name">{name}</span>{role_html}</div>' | |
| ) | |
| def _prog_html(key, pct, indeterminate=False): | |
| if indeterminate: | |
| return f'<div class="prog indet accent-{key}"><i></i></div>' | |
| return f'<div class="prog accent-{key}"><i style="width:{pct:.0f}%"></i></div>' | |
| def _status_html(text): | |
| return f'<div class="status-line mono">{text}</div>' | |
| def _empty_state_html(): | |
| return '<div class="empty-state"><p>Predictions will appear here.</p></div>' | |
| def _loading_state_html(seed_value, status_text=None): | |
| """Shown in the results panel while the model runs; status_text swaps the | |
| "warming up" line for the current step once generation is underway. The | |
| spinner is an SVG animated via SMIL (<animateTransform>), NOT CSS — so it | |
| keeps visibly spinning even under CSS animation resets, including the | |
| prefers-reduced-motion blanket rule and gradio theme quirks.""" | |
| return ( | |
| '<div class="empty-state loading-state">' | |
| '<svg width="42" height="42" viewBox="0 0 42 42" role="img" aria-label="Generating"' | |
| ' style="display:block;margin:0 auto 16px">' | |
| '<circle cx="21" cy="21" r="17" fill="none" stroke="#cdd2db" stroke-width="4.5"/>' | |
| '<path d="M21 4 a17 17 0 0 1 17 17" fill="none" stroke="#ff7a45" stroke-width="4.5"' | |
| ' stroke-linecap="round">' | |
| '<animateTransform attributeName="transform" type="rotate" from="0 21 21" to="360 21 21"' | |
| ' dur="0.9s" repeatCount="indefinite"/>' | |
| "</path></svg>" | |
| f'<p class="mono" style="margin-bottom:6px;color:var(--text-2)">{status_text or "warming up"} · seed {seed_value}</p>' | |
| "<p>Inference has started — the first run can take an extra 1-2 minutes while tokenizers load.</p></div>" | |
| ) | |
| def _header_html(): | |
| return """ | |
| <header class="app-header"> | |
| <div> | |
| <h1>A2A-Video: Modeling the world across time and modalities</h1> | |
| <p>A2A-Video is an any-to-any multimodal video model that is able to predict any modality at any point in time, given any combination of observations from any other modalities -- thus able to traverse along both the modality and temporal axes.</p> | |
| <p>We show two ways to use this demo:<br> | |
| <b>i) Any-to-any generation</b> -- for a given video, you can select any modality as input, and any modality as output. Optionally you can define the intermediate modalities as the chain.<br> | |
| <b>ii) Future prediction in abstract spaces</b> -- given initial observations in any modality space, predict the future in any desired modality, both unconditionally as well as conditionally (e.g. extra condition in the form of caption/transcription).</p> | |
| <nav class="header-links"> | |
| <a href="https://video-4m.epfl.ch" target="_blank" rel="noopener">Website</a> | |
| <a href="https://github.com/EPFL-VILAB/video-4m" target="_blank" rel="noopener">GitHub</a> | |
| <a href="https://video-4m.epfl.ch/assets/Video4M_NeurIPS_2026.pdf" target="_blank" rel="noopener">Paper</a> | |
| </nav> | |
| </div> | |
| </header> | |
| """ | |
| # --- hyperparameter accordion (ported) ---------------------------------------- | |
| def _build_hyperparam_controls(default_preset_key=None): | |
| """One row per CONFIGS modality with sliders for temp / cfg. Decoding | |
| steps aren't exposed -- they're applied silently from whichever | |
| HYPERPARAM_PRESETS set matches the current input modality (see | |
| run_demo_stream), same as temp/cfg defaults below. | |
| default_preset_key: which HYPERPARAM_PRESETS set to source initial slider | |
| values from. Pass None (Future Prediction tab) to source initial values | |
| straight from CONFIGS instead. | |
| Returns (flat_inputs, flat_spec): flat_inputs is the list of Gradio | |
| components to wire up as click() inputs; flat_spec is the matching list | |
| of (modality_key, param_name) tuples used to rebuild the overrides dict. | |
| """ | |
| flat_inputs, flat_spec = [], [] | |
| hp_defaults = HYPERPARAM_PRESETS[default_preset_key] if default_preset_key else {} | |
| gr.Markdown( | |
| "Temperature and CFG scale per modality -- defaults are auto-tuned to match your " | |
| "current input modality (Step 1); adjust here to override. Sliders for modalities " | |
| "not in your chain are ignored.", | |
| elem_classes=["hint-md"], | |
| ) | |
| for key in ALL_MODALITIES: | |
| _, _, _, _, _, temp_fallback, cfg_fallback = CONFIGS[key] | |
| temp_default = hp_defaults.get(key, {}).get("temp", temp_fallback) | |
| cfg_default = hp_defaults.get(key, {}).get("cfg", cfg_fallback) | |
| with gr.Row(): | |
| gr.Markdown(f"**{MODALITY_DISPLAY_NAMES.get(key, key)}**") | |
| temp = gr.Slider(0.0, 5.0, value=temp_default, step=0.01, label="Temperature") | |
| cfg = gr.Slider(0.0, 10.0, value=cfg_default, step=0.1, label="CFG scale") | |
| flat_inputs += [temp, cfg] | |
| flat_spec += [(key, "temp"), (key, "cfg")] | |
| return flat_inputs, flat_spec | |
| def _hyperparam_slider_updates(input_modality, hyperparam_spec): | |
| """The tuned hyperparameter set to load for a given input modality -- | |
| switching modality always gets you sensible defaults.""" | |
| hp = HYPERPARAM_PRESETS[_hyperparam_preset_key_for_modality(input_modality)] | |
| updates = [] | |
| for key, param in hyperparam_spec: | |
| if param in hp.get(key, {}): | |
| updates.append(gr.update(value=hp[key][param])) | |
| else: | |
| updates.append(gr.update()) | |
| return updates | |
| # --- example gallery (ported, restyled) ---------------------------------------- | |
| def _list_examples_safe(stems_fn): | |
| try: | |
| return [s for s in stems_fn() if _safe_preview(s, "rgb") is not None] | |
| except Exception: | |
| traceback.print_exc() | |
| return [] | |
| def _build_example_gallery(gallery_stems): | |
| """Renders the hover-to-preview / click-to-select example-clip grid and | |
| returns (selected_example_md, example_select_btns); callers wire their | |
| own .click() handlers onto example_select_btns (a list of (stem, button) | |
| pairs — the buttons are hidden by CSS, tile clicks are forwarded to them | |
| by _LOAD_JS) and own the shared example_state. | |
| """ | |
| if not gallery_stems: | |
| gr.Markdown("No example previews available yet.", elem_classes=["hint-md"]) | |
| else: | |
| gr.Markdown("Hover to preview · click to select.", elem_classes=["hint-md"]) | |
| example_select_btns = [] | |
| with gr.Column(elem_classes=["example-gallery-scroll"]): | |
| with gr.Row(elem_classes=["tile-grid"]): | |
| for stem in gallery_stems: | |
| with gr.Column(min_width=0, elem_classes=["example-tile-col"]): | |
| gr.Video( | |
| value=_safe_preview(stem, "rgb"), height=82, width=110, | |
| interactive=False, show_label=False, show_download_button=False, | |
| elem_classes=["example-preview-video", "clip-tile"], | |
| ) | |
| btn = gr.Button("Select", size="sm", elem_classes=["select-btn"]) | |
| example_select_btns.append((stem, btn)) | |
| selected_example_md = gr.Markdown("No example selected yet.", elem_classes=["hint-md"], elem_id="a2a-selected") | |
| return selected_example_md, example_select_btns | |
| # --- streaming plumbing --------------------------------------------------------- | |
| # Output tuple layout for the streaming click handler: | |
| # [status, empty_state, run_btn, results_state, | |
| # slot0(col, head, video, text, prog, use_btn), slot1(...), ...] | |
| IDX_STATUS, IDX_EMPTY, IDX_BTN, IDX_RESULTS = 0, 1, 2, 3 | |
| SLOT_BASE = 4 | |
| SLOT_WIDTH = 6 | |
| OFF_COL, OFF_HEAD, OFF_VIDEO, OFF_TEXT, OFF_PROG, OFF_USE = range(SLOT_WIDTH) | |
| N_STREAM_OUTPUTS = SLOT_BASE + MAX_SLOTS * SLOT_WIDTH | |
| # Only these generated modalities can be fed back as a new input: rgb via | |
| # re-tokenizing the generated mp4, caption via the raw text. | |
| REUSABLE_AS_INPUT = {"rgb", "caption"} | |
| def _slot_idx(i, off): | |
| return SLOT_BASE + i * SLOT_WIDTH + off | |
| def _new_stream_ctx(input_modality, chain): | |
| return { | |
| "input_modality": input_modality, | |
| "chain": list(chain), | |
| "next_slot": 0, | |
| "results": {}, | |
| "slot_keys": {}, | |
| } | |
| def _display_value(key, payload): | |
| """(value_for_ui, is_text). Visual payloads (numpy videos) are written to | |
| an mp4; failures degrade to an empty card rather than killing the run.""" | |
| if key in TEXT_MODALITIES: | |
| return payload, True | |
| if payload is None: | |
| return None, False | |
| try: | |
| return _save_to_mp4(payload), False | |
| except Exception: | |
| print(f"[stream] failed to save '{key}' to mp4:") | |
| traceback.print_exc() | |
| return None, False | |
| def _apply_stream_event(event, ctx): | |
| """Maps one inference stream event to sparse {output_index: gr.update}. | |
| Pure of gradio components (only gr.update dicts), so it's unit-testable | |
| without a browser or model; mutates ctx (slot bookkeeping + results). | |
| """ | |
| kind, key, payload = event | |
| updates = {} | |
| if kind == "input": | |
| i = 0 | |
| ctx["slot_keys"][i] = key | |
| ctx["next_slot"] = 1 | |
| value, is_text = _display_value(key, payload) | |
| ctx["results"][f"input_{key}"] = value | |
| # First real content: swap the loading indicator out for the cards. | |
| updates[IDX_EMPTY] = gr.update(value=_empty_state_html(), visible=False) | |
| updates[_slot_idx(i, OFF_COL)] = gr.update(visible=True) | |
| updates[_slot_idx(i, OFF_HEAD)] = gr.update(value=_card_head_html(key, "input")) | |
| updates[_slot_idx(i, OFF_VIDEO)] = gr.update(value=None if is_text else value, visible=not is_text) | |
| updates[_slot_idx(i, OFF_TEXT)] = gr.update(value=value if is_text else None, visible=is_text) | |
| updates[_slot_idx(i, OFF_PROG)] = gr.update(visible=False) | |
| updates[_slot_idx(i, OFF_USE)] = gr.update(visible=False) | |
| return updates | |
| if kind == "start": | |
| i = ctx["next_slot"] | |
| ctx["next_slot"] = i + 1 | |
| ctx["slot_keys"][i] = key | |
| total = payload | |
| updates[_slot_idx(i, OFF_COL)] = gr.update(visible=True) | |
| updates[_slot_idx(i, OFF_HEAD)] = gr.update(value=_card_head_html(key)) | |
| updates[_slot_idx(i, OFF_VIDEO)] = gr.update(value=None, visible=False) | |
| updates[_slot_idx(i, OFF_TEXT)] = gr.update(value=None, visible=False) | |
| updates[_slot_idx(i, OFF_PROG)] = gr.update(value=_prog_html(key, 0, indeterminate=total <= 1), visible=True) | |
| updates[_slot_idx(i, OFF_USE)] = gr.update(visible=False) | |
| return updates | |
| if kind == "progress": | |
| i = ctx["next_slot"] - 1 | |
| done, total = payload | |
| pct = 100.0 * done / max(total, 1) | |
| updates[_slot_idx(i, OFF_PROG)] = gr.update(value=_prog_html(key, pct)) | |
| return updates | |
| if kind == "result": | |
| i = ctx["next_slot"] - 1 | |
| value, is_text = _display_value(key, payload) | |
| ctx["results"][key] = value | |
| updates[_slot_idx(i, OFF_HEAD)] = gr.update(value=_card_head_html(key)) | |
| updates[_slot_idx(i, OFF_VIDEO)] = gr.update(value=None if is_text else value, visible=not is_text) | |
| updates[_slot_idx(i, OFF_TEXT)] = gr.update(value=value if is_text else None, visible=is_text) | |
| updates[_slot_idx(i, OFF_PROG)] = gr.update(visible=False) | |
| updates[_slot_idx(i, OFF_USE)] = gr.update(visible=key in REUSABLE_AS_INPUT and value is not None) | |
| return updates | |
| if kind == "done": | |
| updates[IDX_STATUS] = gr.update(value=_status_html("done")) | |
| updates[IDX_BTN] = gr.update(value="Generate again", interactive=True) | |
| results = dict(ctx["results"]) | |
| results["_slot_keys"] = dict(ctx["slot_keys"]) | |
| updates[IDX_RESULTS] = results | |
| return updates | |
| return updates | |
| def _reset_frame_updates(seed_value): | |
| """The 'arming' frame: show the spinner card, disable the button, hide | |
| and reset every result slot. Predictions are deliberately NOT revealed | |
| during generation — they all appear together at the end (see the | |
| buffering loop in run_demo_stream).""" | |
| updates = { | |
| IDX_STATUS: gr.update(value=_status_html(f"warming up · seed {seed_value}")), | |
| IDX_EMPTY: gr.update(value=_loading_state_html(seed_value), visible=True), | |
| IDX_BTN: gr.update(value="Generating…", interactive=False), | |
| IDX_RESULTS: {}, | |
| } | |
| for i in range(MAX_SLOTS): | |
| updates[_slot_idx(i, OFF_COL)] = gr.update(visible=False) | |
| updates[_slot_idx(i, OFF_HEAD)] = gr.update(value="") | |
| updates[_slot_idx(i, OFF_VIDEO)] = gr.update(value=None, visible=False) | |
| updates[_slot_idx(i, OFF_TEXT)] = gr.update(value=None, visible=False) | |
| updates[_slot_idx(i, OFF_PROG)] = gr.update(value="", visible=False) | |
| updates[_slot_idx(i, OFF_USE)] = gr.update(visible=False) | |
| return updates | |
| def _frame(updates): | |
| out = [gr.skip()] * N_STREAM_OUTPUTS | |
| for idx, upd in updates.items(): | |
| out[idx] = upd | |
| return tuple(out) | |
| def _synthesize_events_from_results(input_modality, chain, results): | |
| """Turns generate_any_to_any's returned dict into the same event stream | |
| generate_any_to_any_stream would emit (all at once, at the end).""" | |
| yield ("input", input_modality, results.get(f"input_{input_modality}")) | |
| for key in chain: | |
| yield ("start", key, 1) | |
| if results.get(key) is not None: | |
| yield ("result", key, results[key]) | |
| yield ("done", None, results) | |
| def _hex_to_rgb(hex_color): | |
| hex_color = hex_color.lstrip("#") | |
| return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) | |
| _FAKE_TEXT = { | |
| "caption": "A placeholder caption from FOURM_FAKE_STREAM — no model was run.", | |
| "transcription": "[SEC_1] placeholder… [SEC_2] streaming works… [SEC_3] wire me to the GPU. [EOS]", | |
| } | |
| def _fake_tinted_video(key): | |
| r, g, b = _hex_to_rgb(MODALITY_COLORS[key]) | |
| vid = np.zeros((17, 64, 64, 3), dtype=np.uint8) | |
| vid[..., 0], vid[..., 1], vid[..., 2] = r // 3, g // 3, b // 3 | |
| for t in range(17): | |
| vid[t, 8 + t:24 + t, 8:56] = (r, g, b) | |
| return vid | |
| def _fake_stream_events(input_modality, chain): | |
| """FOURM_FAKE_STREAM=1: synthetic events with placeholder media so the | |
| entire streaming UI (cards, progress, use-as-input) can be exercised on | |
| a login node with no GPU and no checkpoints.""" | |
| if input_modality in TEXT_MODALITIES: | |
| yield ("input", input_modality, _FAKE_TEXT[input_modality]) | |
| else: | |
| yield ("input", input_modality, _fake_tinted_video(input_modality)) | |
| results = {} | |
| for key in chain: | |
| total = CONFIGS[key][2] or 1 | |
| yield ("start", key, total) | |
| for done in range(5, total, 5): | |
| time.sleep(0.12) | |
| yield ("progress", key, (done, total)) | |
| time.sleep(0.3) | |
| value = _FAKE_TEXT[key] if key in TEXT_MODALITIES else _fake_tinted_video(key) | |
| results[key] = value | |
| yield ("result", key, value) | |
| yield ("done", None, results) | |
| def _chip_busy_js(scope_class, host_selector): | |
| """Client-side js= hook for chip clicks: dims that region's chip rows, | |
| blocks further clicks, and centers ONE spinner overlay on the region's | |
| main box until the re-rendered rows replace it (the busy state lives in | |
| the old DOM, so it self-clears). gradio executes this directly.""" | |
| return ( | |
| "() => { document.querySelectorAll('." + scope_class + "').forEach((r) => {" | |
| " r.classList.add('chips-busy');" | |
| " setTimeout(() => r.classList.remove('chips-busy'), 30000); });" | |
| " const host = document.querySelector('" + host_selector + "');" | |
| " if (host && !host.querySelector('.chips-overlay')) {" | |
| " const s = document.createElement('span'); s.className = 'chips-overlay';" | |
| " s.innerHTML = '<svg width=\"24\" height=\"24\" viewBox=\"0 0 38 38\">" | |
| "<circle cx=\"19\" cy=\"19\" r=\"15.5\" fill=\"none\" stroke=\"#242c3b\" stroke-width=\"6\"/>" | |
| "<path d=\"M19 3.5 a15.5 15.5 0 0 1 15.5 15.5\" fill=\"none\" stroke=\"#ff7a45\" stroke-width=\"6\" stroke-linecap=\"round\">" | |
| "<animateTransform attributeName=\"transform\" type=\"rotate\" from=\"0 19 19\" to=\"360 19 19\" dur=\"0.9s\" repeatCount=\"indefinite\"/>" | |
| "</path></svg>'; host.appendChild(s); } }" | |
| ) | |
| def _sel_busy_js(host_id): | |
| """Client-side js= hook for sample-select clicks: dims the 'Selected:' | |
| line and centers our circle spinner on it. Removal is driven by a | |
| js-only .then() on the python event (_sel_done_js), which gradio runs | |
| when the round trip completes -- fully deterministic (15s failsafe).""" | |
| return ( | |
| "() => { const host = document.getElementById('" + host_id + "');" | |
| " if (!host || host.querySelector('.chips-overlay')) return;" | |
| " host.classList.add('chips-busy'); host.style.position = 'relative';" | |
| " const s = document.createElement('span'); s.className = 'chips-overlay';" | |
| " s.innerHTML = '<svg width=\"18\" height=\"18\" viewBox=\"0 0 38 38\">" | |
| "<circle cx=\"19\" cy=\"19\" r=\"15.5\" fill=\"none\" stroke=\"#242c3b\" stroke-width=\"6\"/>" | |
| "<path d=\"M19 3.5 a15.5 15.5 0 0 1 15.5 15.5\" fill=\"none\" stroke=\"#ff7a45\" stroke-width=\"6\" stroke-linecap=\"round\">" | |
| "<animateTransform attributeName=\"transform\" type=\"rotate\" from=\"0 19 19\" to=\"360 19 19\" dur=\"0.9s\" repeatCount=\"indefinite\"/>" | |
| "</path></svg>'; host.appendChild(s);" | |
| " setTimeout(() => { s.remove(); host.classList.remove('chips-busy'); }, 15000); }" | |
| ) | |
| def _sel_done_js(host_id): | |
| """js-only .then() hook: clears the spinner set by _sel_busy_js once the | |
| select event's server round trip has finished.""" | |
| return ( | |
| "() => { const host = document.getElementById('" + host_id + "');" | |
| " if (!host) return; host.classList.remove('chips-busy');" | |
| " const s = host.querySelector('.chips-overlay'); if (s) s.remove(); }" | |
| ) | |
| # --- theme / CSS / JS ----------------------------------------------------------- | |
| # Wire up the example tiles: hover previews the clip (gr.Video has no built-in | |
| # hover-play), clicking anywhere on the tile selects it — the click is | |
| # forwarded to the tile's hidden gradio Select button (which carries the | |
| # python handler) and the tile gets a .sel class (orange border + ✓ badge, | |
| # prototype-style). A MutationObserver re-attaches after Gradio re-renders | |
| # (e.g. the abstract gallery re-rendering on modality switch). The same | |
| # observer pass also starts every result-card video together once the whole | |
| # batch is ready to play, instead of each one autoplaying independently. | |
| _LOAD_JS = """ | |
| () => { | |
| function attach() { | |
| document.querySelectorAll('.example-preview-video').forEach((container) => { | |
| if (container.dataset.hoverBound) return; | |
| container.dataset.hoverBound = "1"; | |
| container.addEventListener('mouseenter', () => { | |
| const video = container.querySelector('video'); | |
| if (!video) return; | |
| video.controls = false; | |
| video.muted = true; | |
| video.loop = true; | |
| video.play().catch(() => {}); | |
| }); | |
| container.addEventListener('mouseleave', () => { | |
| const video = container.querySelector('video'); | |
| if (video) video.pause(); | |
| }); | |
| }); | |
| document.querySelectorAll('.example-tile-col').forEach((col) => { | |
| if (col.dataset.selBound) return; | |
| col.dataset.selBound = "1"; | |
| col.addEventListener('click', (e) => { | |
| // Ignore the synthetic click we fire on the hidden button | |
| // below, or we'd recurse forever. | |
| if (e.target.closest && e.target.closest('button.select-btn')) return; | |
| const btn = col.querySelector('button.select-btn'); | |
| if (!btn) return; | |
| const scroller = col.closest('.example-gallery-scroll'); | |
| if (scroller) { | |
| scroller.querySelectorAll('.example-tile-col.sel') | |
| .forEach((x) => x.classList.remove('sel')); | |
| } | |
| col.classList.add('sel'); | |
| btn.click(); | |
| }); | |
| }); | |
| // Result cards all land in the DOM together, but each <video>'s data | |
| // becomes ready at a slightly different time -- left to its own | |
| // autoplay, that means a visible cascade of clips starting one after | |
| // another instead of the grid starting together. Hold every | |
| // just-updated video at frame 0 until the whole batch can play, then | |
| // start them all in the same tick (same fix as the notebooks' demos). | |
| // Keyed on src, not "seen before": the MAX_SLOTS card slots are | |
| // fixed gradio components reused across every run, not recreated | |
| // each generation, so a plain seen-once flag would only ever | |
| // resync on the very first run. | |
| const freshVideos = Array.from(document.querySelectorAll('.rc-video video')).filter((v) => { | |
| const src = v.getAttribute('src'); | |
| if (!src || v.dataset.syncedSrc === src) return false; | |
| v.dataset.syncedSrc = src; | |
| return true; | |
| }); | |
| if (freshVideos.length) { | |
| freshVideos.forEach((v) => { | |
| v.muted = true; v.loop = true; v.playsInline = true; | |
| v.autoplay = false; | |
| v.pause(); | |
| }); | |
| let ready = 0; | |
| const startAll = () => freshVideos.forEach((v) => { v.currentTime = 0; v.play().catch(() => {}); }); | |
| freshVideos.forEach((v) => { | |
| if (v.readyState >= 3) { | |
| ready++; | |
| } else { | |
| v.addEventListener('canplay', function onReady() { | |
| v.removeEventListener('canplay', onReady); | |
| ready++; | |
| if (ready === freshVideos.length) startAll(); | |
| }); | |
| } | |
| }); | |
| if (ready === freshVideos.length) startAll(); | |
| } | |
| // Switching gallery modality re-renders the tiles server-side, which | |
| // can take a while (preview downloads). Drop a loading overlay into | |
| // the CURRENT gallery immediately; it lives in the old DOM, so it | |
| // disappears by itself when the fresh gallery replaces it. | |
| document.querySelectorAll('#abs-mods label, #fp-obs-mods label').forEach((lab) => { | |
| if (lab.dataset.loadBound) return; | |
| lab.dataset.loadBound = "1"; | |
| lab.addEventListener('click', () => { | |
| if (lab.querySelector('input:checked')) return; // already active | |
| const radio = lab.closest('#abs-mods, #fp-obs-mods'); | |
| if (!radio || !radio.parentNode) return; | |
| const gal = radio.parentNode.querySelector('.example-gallery-scroll'); | |
| if (!gal || gal.querySelector('.gallery-loading')) return; | |
| const ov = document.createElement('div'); | |
| ov.className = 'gallery-loading'; | |
| ov.innerHTML = '<svg width="26" height="26" viewBox="0 0 38 38">' + | |
| '<circle cx="19" cy="19" r="15.5" fill="none" stroke="#e3e6eb" stroke-width="5"/>' + | |
| '<path d="M19 3.5 a15.5 15.5 0 0 1 15.5 15.5" fill="none" stroke="#ff7a45" stroke-width="5" stroke-linecap="round">' + | |
| '<animateTransform attributeName="transform" type="rotate" from="0 19 19" to="360 19 19" dur="0.9s" repeatCount="indefinite"/>' + | |
| '</path></svg><span>loading previews…</span>'; | |
| gal.appendChild(ov); | |
| setTimeout(() => ov.remove(), 45000); // safety net if the render errors | |
| }); | |
| }); | |
| } | |
| attach(); | |
| new MutationObserver(attach).observe(document.body, {childList: true, subtree: true}); | |
| } | |
| """ | |
| _PALETTE = { | |
| "bg": "#ffffff", "panel": "#f7f8fa", "panel2": "#eef0f3", | |
| "line": "#e3e6eb", "line2": "#cdd2db", | |
| "text": "#1a1d23", "text2": "#5b6472", "text3": "#8a92a0", | |
| "go": "#ff7a45", "go_dim": "#ffb28c", | |
| } | |
| _THEME = gr.themes.Base( | |
| font=["ui-sans-serif", "system-ui", "sans-serif"], | |
| font_mono=["ui-monospace", "Consolas", "monospace"], | |
| ).set( | |
| body_background_fill=_PALETTE["bg"], | |
| body_background_fill_dark=_PALETTE["bg"], | |
| body_text_color=_PALETTE["text"], | |
| body_text_color_dark=_PALETTE["text"], | |
| body_text_color_subdued=_PALETTE["text2"], | |
| body_text_color_subdued_dark=_PALETTE["text2"], | |
| background_fill_primary=_PALETTE["panel"], | |
| background_fill_primary_dark=_PALETTE["panel"], | |
| background_fill_secondary=_PALETTE["panel2"], | |
| background_fill_secondary_dark=_PALETTE["panel2"], | |
| border_color_primary=_PALETTE["line"], | |
| border_color_primary_dark=_PALETTE["line"], | |
| block_background_fill="transparent", | |
| block_background_fill_dark="transparent", | |
| block_border_color=_PALETTE["line"], | |
| block_border_color_dark=_PALETTE["line"], | |
| block_title_text_color=_PALETTE["text2"], | |
| block_title_text_color_dark=_PALETTE["text2"], | |
| block_label_text_color=_PALETTE["text3"], | |
| block_label_text_color_dark=_PALETTE["text3"], | |
| input_background_fill=_PALETTE["panel"], | |
| input_background_fill_dark=_PALETTE["panel"], | |
| input_border_color=_PALETTE["line"], | |
| input_border_color_dark=_PALETTE["line"], | |
| input_placeholder_color=_PALETTE["text3"], | |
| input_placeholder_color_dark=_PALETTE["text3"], | |
| button_primary_background_fill=_PALETTE["go"], | |
| button_primary_background_fill_dark=_PALETTE["go"], | |
| button_primary_background_fill_hover="#ff8a5c", | |
| button_primary_background_fill_hover_dark="#ff8a5c", | |
| button_primary_text_color="#1a0d05", | |
| button_primary_text_color_dark="#1a0d05", | |
| button_secondary_background_fill=_PALETTE["panel"], | |
| button_secondary_background_fill_dark=_PALETTE["panel"], | |
| button_secondary_background_fill_hover=_PALETTE["panel2"], | |
| button_secondary_background_fill_hover_dark=_PALETTE["panel2"], | |
| button_secondary_text_color=_PALETTE["text"], | |
| button_secondary_text_color_dark=_PALETTE["text"], | |
| slider_color=_PALETTE["go"], | |
| slider_color_dark=_PALETTE["go"], | |
| loader_color=_PALETTE["go"], | |
| loader_color_dark=_PALETTE["go"], | |
| ) | |
| _ACCENT_CSS = "\n".join( | |
| f".accent-{key}{{--c:{color};}}" for key, color in MODALITY_COLORS.items() | |
| ) | |
| _CARD_TINT_CSS = "\n".join( | |
| f".res-card:has(.accent-{key}) {{ border-color: color-mix(in srgb, {color} 45%, {_PALETTE['line']}); }}" | |
| for key, color in MODALITY_COLORS.items() | |
| ) | |
| # Per-chip accent colors for the abstract-modality pills (fixed choice order). | |
| _ABS_CHIP_CSS = "\n".join( | |
| f'#abs-mods label:nth-child({i + 1}) {{ --c:{MODALITY_COLORS[key]}; }}' | |
| for i, key in enumerate(ABS_MODALITIES) | |
| ) | |
| # Same accent treatment for the future tab's "observed as" chips. | |
| _FP_OBS_CHIP_CSS = "\n".join( | |
| f'#fp-obs-mods label:nth-child({i + 1}) {{ --c:{MODALITY_COLORS[key]}; }}' | |
| for i, key in enumerate(FUTURE_SEED_MODALITIES) | |
| ) | |
| _CSS = f""" | |
| /* ---------- palette & base ---------- */ | |
| .gradio-container {{ | |
| --bg:{_PALETTE['bg']}; --panel:{_PALETTE['panel']}; --panel-2:{_PALETTE['panel2']}; | |
| --line:{_PALETTE['line']}; --line-2:{_PALETTE['line2']}; | |
| --text:{_PALETTE['text']}; --text-2:{_PALETTE['text2']}; --text-3:{_PALETTE['text3']}; | |
| --go:{_PALETTE['go']}; --go-dim:{_PALETTE['go_dim']}; | |
| background: var(--bg) !important; | |
| max-width: 1320px !important; | |
| margin: 0 auto !important; | |
| }} | |
| .mono {{ font-family: ui-monospace, monospace; }} | |
| footer {{ display: none !important; }} | |
| {_ACCENT_CSS} | |
| /* ---------- header ---------- */ | |
| .app-header {{ padding:14px 4px 18px; border-bottom:1px solid var(--line); }} | |
| .app-header h1 {{ font-size:23px; font-weight:600; margin:0; }} | |
| .app-header p {{ color:var(--text-2); font-size:14.5px; margin:4px 0 0; max-width:640px; }} | |
| .header-links {{ display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }} | |
| .header-links a {{ color:var(--text-2) !important; font-size:14px; text-decoration:none !important; | |
| border:1px solid var(--line); border-radius:999px; padding:5px 13px; transition:.15s; }} | |
| .header-links a:hover {{ color:var(--text) !important; border-color:var(--line-2); }} | |
| /* ---------- step labels & hints ---------- */ | |
| .step-label {{ display:flex; align-items:center; gap:10px; margin:18px 0 4px; }} | |
| .step-num {{ width:22px; height:22px; border-radius:50%; background:var(--panel-2); | |
| border:1px solid var(--line-2); display:flex; align-items:center; justify-content:center; | |
| font-size:13px; font-weight:600; color:var(--text-2); flex:0 0 22px; }} | |
| .step-label h2 {{ font-size:16.5px; font-weight:500; margin:0; }} | |
| .hint, .eta-note {{ font-size:13.5px; color:var(--text-3); margin:4px 0 8px; }} | |
| .hint-md p, .hint-md {{ font-size:13.5px !important; color:var(--text-3) !important; }} | |
| /* ---------- source cards / preset cards / abs chips (styled radios) ---------- */ | |
| #src-cards, #chain-presets, #abs-mods {{ border:none; background:transparent; padding:0; }} | |
| #src-cards .wrap, #chain-presets .wrap {{ display:grid; grid-template-columns:repeat(3, 1fr); gap:10px; }} | |
| #src-cards label, #chain-presets label {{ | |
| display:block; background:var(--panel); border:1.5px solid var(--line); border-radius:10px; | |
| padding:12px 14px; cursor:pointer; transition:.15s; margin:0; | |
| font-weight:600; font-size:14.5px; color:var(--text); | |
| }} | |
| #src-cards label:hover, #chain-presets label:hover {{ border-color:var(--line-2); transform:translateY(-1px); }} | |
| #src-cards label.selected, #src-cards label:has(input:checked), | |
| #chain-presets label.selected, #chain-presets label:has(input:checked) {{ | |
| border-color:var(--go); background:color-mix(in srgb, var(--go) 6%, var(--panel)); | |
| }} | |
| #src-cards input, #chain-presets input, #abs-mods input {{ display:none; }} | |
| #abs-mods .wrap {{ display:flex; flex-wrap:wrap; gap:7px; }} | |
| #abs-mods label {{ background:transparent; border:1px solid var(--line-2); border-radius:999px; | |
| padding:4px 13px; font-size:13.5px; font-weight:500; color:var(--text-2); cursor:pointer; | |
| transition:.15s; margin:0; }} | |
| #abs-mods label::before {{ content:''; display:inline-block; width:7px; height:7px; | |
| border-radius:50%; background:currentColor; margin-right:7px; vertical-align:1px; }} | |
| {_ABS_CHIP_CSS} | |
| #abs-mods label:hover {{ color:var(--c, var(--text)); border-color:var(--c, var(--text-3)); }} | |
| #abs-mods label.selected, #abs-mods label:has(input:checked) {{ | |
| color:var(--c, var(--go)); border-color:color-mix(in srgb, var(--c, var(--go)) 55%, transparent); | |
| background:color-mix(in srgb, var(--c, var(--go)) 13%, transparent); | |
| }} | |
| /* ---------- example gallery tiles ---------- */ | |
| /* Compact wrapping thumbnail grid, prototype-style: fixed 110x82 (4:3-ish) | |
| tiles with 10px gaps, video cover-cropped to fill — instead of gradio's | |
| default stretchy equal-width Row columns. */ | |
| .example-gallery-scroll {{ max-height:236px; overflow-y:auto; padding-right:6px; position:relative; }} | |
| .gallery-loading {{ position:absolute; inset:0; z-index:10; display:flex; flex-direction:column; | |
| align-items:center; justify-content:center; gap:8px; background:rgba(255,255,255,.85); | |
| color:var(--text-2); font-size:13.5px; border-radius:10px; }} | |
| .chips-busy {{ pointer-events:none; }} | |
| .chips-busy > *:not(.chips-overlay) {{ opacity:.4; transition:opacity .15s; }} | |
| .chips-overlay {{ position:absolute; inset:0; z-index:12; display:flex; align-items:center; | |
| justify-content:center; pointer-events:none; }} | |
| /* gradio's pending state forces a large min-height on busy output blocks | |
| (sometimes on inner wrappers, not the block itself); keep the Selected | |
| line one text-row tall no matter what */ | |
| #a2a-selected, #abs-selected, #fp-selected {{ | |
| min-height:24px !important; height:auto !important; max-height:44px; overflow:hidden; }} | |
| #a2a-selected *, #abs-selected *, #fp-selected * {{ | |
| min-height:0 !important; }} | |
| .tile-grid {{ display:flex !important; flex-wrap:wrap !important; gap:10px !important; }} | |
| .tile-grid > * {{ flex:0 0 110px !important; flex-grow:0 !important; width:110px !important; | |
| min-width:0 !important; }} | |
| .example-tile-col {{ display:flex; flex-direction:column; gap:0 !important; | |
| position:relative; cursor:pointer; }} | |
| .clip-tile {{ width:110px !important; height:82px !important; | |
| border:1.5px solid var(--line) !important; border-radius:10px !important; | |
| overflow:hidden; background:var(--panel) !important; position:relative; transition:.15s; }} | |
| .clip-tile video {{ width:110px !important; height:82px !important; object-fit:cover; | |
| display:block; background:#0a0d12; }} | |
| /* hide gradio's own player chrome (play bar, "0:04 / 0:04" time pill, | |
| fullscreen…) on gallery tiles — they're hover-previews with their own | |
| static duration badge, and the overlay doesn't fit a 110px tile */ | |
| .clip-tile .controls, .clip-tile button {{ display:none !important; }} | |
| /* Deliberately stays a dark badge with light text regardless of theme -- | |
| it overlays arbitrary video pixels, not page chrome, so it needs to stay | |
| legible against whatever the clip's own colors happen to be. */ | |
| .clip-tile::before {{ content:'0:04'; position:absolute; left:5px; bottom:5px; z-index:4; | |
| font-family:ui-monospace, monospace; font-size:11px; color:#e8ecf3; | |
| background:rgba(13,16,21,.75); padding:1px 5px; border-radius:4px; pointer-events:none; }} | |
| .example-tile-col:hover .clip-tile {{ border-color:var(--line-2) !important; transform:translateY(-1px); }} | |
| /* the whole tile is the click target: JS forwards tile clicks to the real | |
| (hidden) per-tile Select button, which carries the gradio handler */ | |
| button.select-btn {{ display:none !important; }} | |
| .example-tile-col.sel .clip-tile, .example-tile-col.sel .abs-text-tile {{ | |
| border-color:var(--go) !important; }} | |
| .example-tile-col.sel .clip-tile::after, .example-tile-col.sel .abs-text-tile::after {{ | |
| content:'✓'; position:absolute; top:5px; right:5px; width:18px; height:18px; border-radius:50%; | |
| background:var(--go); color:#1a0d05; font-size:12.5px; font-weight:700; | |
| display:flex; align-items:center; justify-content:center; z-index:5; }} | |
| .abs-text-tile {{ border:1.5px solid var(--line); border-radius:10px; background:var(--panel); | |
| width:110px; height:82px; overflow:hidden; padding:7px; font-family:ui-monospace, monospace; | |
| font-size:10px; line-height:1.45; color:var(--text-2); position:relative; transition:.15s; }} | |
| .example-tile-col:hover .abs-text-tile {{ border-color:var(--line-2); }} | |
| /* ---------- upload dropbox ---------- */ | |
| #upload-dropbox {{ border:1.5px dashed var(--line-2) !important; border-radius:10px !important; | |
| background:transparent !important; }} | |
| /* fixed compact height in BOTH states (empty dropzone / uploaded clip): | |
| gradio's nested upload wrappers otherwise force a very tall box */ | |
| #upload-dropbox {{ height:180px !important; max-height:180px !important; overflow:hidden; }} | |
| #upload-dropbox * {{ min-height:0 !important; }} | |
| #upload-dropbox video {{ height:128px !important; width:auto !important; max-width:100%; | |
| margin:0 auto; display:block; object-fit:contain; background:#0a0d12; }} | |
| #upload-dropbox:hover {{ border-color: var(--go) !important; }} | |
| /* ---------- caption samples ---------- */ | |
| /* Real example captions vary a lot in length -- rather than truncating (a | |
| pill can't show a full sentence anyway), each one is a full-width, | |
| left-aligned, wrapping list item, and the list itself scrolls instead of | |
| growing the column past its container. */ | |
| .cap-chip {{ border-radius:10px !important; font-size:13px !important; | |
| color:var(--text-2) !important; background:var(--panel) !important; | |
| border:1px solid var(--line) !important; text-align:left !important; | |
| white-space:normal !important; line-height:1.4 !important; padding:8px 12px !important; }} | |
| .cap-chip:hover {{ border-color:{MODALITY_COLORS['caption']} !important; color:var(--text) !important; }} | |
| /* ---------- chain rail & chips ---------- */ | |
| .chain-rail {{ background:var(--panel); border:1px solid var(--line); border-radius:10px; | |
| padding:12px 14px !important; min-height:56px; align-items:center !important; | |
| flex-wrap:wrap !important; gap:8px !important; position:relative; }} | |
| .chain-rail > *, .avail-chips > *, .cap-samples > * {{ flex:0 0 auto !important; min-width:0 !important; width:auto !important; }} | |
| .avail-chips, .cap-samples {{ flex-wrap:wrap !important; gap:7px !important; margin-top:8px; }} | |
| .avail-chips {{ position:relative; }} | |
| /* .cap-samples is a gr.Column (vertical stack), not a wrapping chip row | |
| like .avail-chips -- scroll instead of growing past the left column, and | |
| let each chip take the full row width instead of sizing to its content. */ | |
| .cap-samples {{ flex-wrap:nowrap !important; max-height:190px; overflow-y:auto; padding-right:4px; }} | |
| .cap-samples > .cap-chip {{ width:100% !important; }} | |
| .chip {{ display:inline-flex; align-items:center; gap:6px; font-size:13.5px; font-weight:500; | |
| padding:5px 12px; border-radius:999px; | |
| background:color-mix(in srgb, var(--c) 16%, transparent); color:var(--c); | |
| box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--c) 40%, transparent); }} | |
| .chip .dot {{ width:7px; height:7px; border-radius:50%; background:currentColor; }} | |
| .chip .in-tag {{ opacity:.55; font-size:11.5px; }} | |
| .arrow-html span, .arrow {{ color:var(--text-3); font-size:15.5px; user-select:none; }} | |
| button.chip-solid {{ border-radius:999px !important; font-size:13.5px !important; font-weight:500 !important; | |
| padding:5px 12px !important; color:var(--c) !important; | |
| background:color-mix(in srgb, var(--c) 16%, transparent) !important; | |
| border:1px solid color-mix(in srgb, var(--c) 40%, transparent) !important; }} | |
| button.chip-solid::before {{ content:''; display:inline-block; width:7px; height:7px; | |
| border-radius:50%; background:currentColor; margin-right:6px; vertical-align:1px; }} | |
| button.chip-solid:hover {{ border-color:var(--c) !important; }} | |
| button.chip-ghost {{ border-radius:999px !important; font-size:13px !important; | |
| padding:4px 11px !important; color:var(--text-2) !important; background:transparent !important; | |
| border:1px solid var(--line-2) !important; }} | |
| button.chip-ghost:hover {{ color:var(--c) !important; border-color:var(--c) !important; }} | |
| /* ---------- generate row ---------- */ | |
| /* One compact 52px-tall row: the seed Number's stacked label would otherwise | |
| stretch the whole row (and the Generate button with it). The seed becomes a | |
| prototype-style pill with the label inline, left of the value. */ | |
| .gen-row {{ gap:10px !important; align-items:stretch !important; }} | |
| .gen-row > * {{ height:52px !important; min-height:52px !important; }} | |
| #gen-btn {{ font-weight:600 !important; font-size:16px !important; | |
| border-radius:10px !important; padding:0 18px !important; }} | |
| #gen-btn:disabled {{ background:var(--go-dim) !important; cursor:wait; }} | |
| #seed-box {{ background:var(--panel) !important; border:1px solid var(--line) !important; | |
| border-radius:10px !important; padding:0 8px 0 12px !important; | |
| display:flex !important; align-items:center !important; overflow:hidden; }} | |
| #seed-box label {{ display:flex !important; flex-direction:row !important; align-items:center !important; | |
| gap:8px; margin:0 !important; width:100%; }} | |
| #seed-box label > span {{ margin:0 !important; font-size:13.5px !important; | |
| color:var(--text-3) !important; }} | |
| #seed-box input {{ font-family:ui-monospace, monospace; text-align:center; font-size:16px; | |
| background:transparent !important; border:none !important; box-shadow:none !important; | |
| padding:0 !important; width:100%; min-width:48px; appearance:textfield; -moz-appearance:textfield; }} | |
| #seed-box input::-webkit-outer-spin-button, #seed-box input::-webkit-inner-spin-button {{ | |
| -webkit-appearance:none; margin:0; }} | |
| /* ---------- results panel ---------- */ | |
| .res-head-row {{ align-items:center !important; }} | |
| .res-title h2, .res-title {{ font-size:16.5px !important; font-weight:500; }} | |
| .status-line {{ font-size:12.5px; color:var(--text-3); text-align:right; }} | |
| .empty-state {{ border:1.5px dashed var(--line); border-radius:10px; padding:40px 24px; | |
| text-align:center; color:var(--text-3); }} | |
| .empty-state p {{ font-size:14.5px; max-width:290px; margin:0 auto; }} | |
| .loading-state {{ border-style:solid; padding:24px; | |
| border-color:color-mix(in srgb, var(--go) 30%, var(--line)); margin-bottom:12px; }} | |
| /* 3-per-row prediction grid: hidden cards (visible=False) leave the grid | |
| flow automatically, so the same 10 pre-declared slots serve any chain. */ | |
| .results-grid {{ display:grid !important; grid-template-columns:repeat(3, minmax(0,1fr)); | |
| gap:10px !important; align-items:start; | |
| /* the grid box gets stretched to the tall right column; without packing, | |
| grid distributes the spare height BETWEEN the rows (huge row gaps) */ | |
| align-content:start !important; grid-auto-rows:min-content; | |
| flex-grow:0 !important; }} | |
| .res-card {{ background:var(--panel) !important; border:1px solid var(--line) !important; | |
| border-radius:10px !important; overflow:hidden; padding:0 !important; gap:0 !important; | |
| position:relative; animation: slideIn .35s ease; }} | |
| @keyframes slideIn {{ from {{ opacity:0; transform:translateY(8px); }} to {{ opacity:1; transform:none; }} }} | |
| {_CARD_TINT_CSS} | |
| .res-card .block {{ border:none !important; background:transparent !important; }} | |
| .rc-head {{ display:flex; align-items:center; gap:9px; height:32px; padding:0 12px; | |
| white-space:nowrap; overflow:hidden; border-bottom:1px solid var(--line); }} | |
| .rc-head .dot {{ width:8px; height:8px; border-radius:50%; background:var(--c); flex:0 0 8px; }} | |
| .rc-head .name {{ font-size:14px; font-weight:600; overflow:hidden; text-overflow:ellipsis; }} | |
| .rc-head .role {{ font-size:12px; color:var(--text-3); text-transform:uppercase; letter-spacing:.05em; }} | |
| /* Uniform card bodies: every card's content area is the same fixed height | |
| (video or text), so all cards in a grid row line up cleanly with no | |
| ragged gaps between rows. NOTE: never put an !important `display` on | |
| these blocks — it would override gradio's visible=False hiding, which is | |
| exactly what made empty video players appear inside the text cards. */ | |
| .rc-video {{ height:140px !important; min-height:0 !important; padding:6px 10px; }} | |
| .rc-video > * {{ min-height:0 !important; height:auto !important; }} | |
| .rc-video video {{ height:{RESULT_VIDEO_HEIGHT}px !important; width:auto !important; max-width:100%; | |
| margin:0 auto; display:block; object-fit:contain; background:#0a0d12; border-radius:7px; }} | |
| .rc-text {{ min-height:0 !important; padding:4px 10px 8px; }} | |
| .rc-text textarea {{ height:124px !important; overflow-y:auto; background:transparent !important; | |
| border:none !important; color:var(--text-2) !important; font-style:italic; | |
| font-size:13.5px !important; line-height:1.55; }} | |
| .rc-prog {{ padding:0 14px 12px; }} | |
| .prog {{ height:3px; background:var(--panel-2); border-radius:2px; overflow:hidden; }} | |
| .prog i {{ display:block; height:100%; background:var(--c); border-radius:2px; | |
| transition:width .2s linear; }} | |
| .prog.indet i {{ width:35%; animation: indet 1.1s ease-in-out infinite; }} | |
| @keyframes indet {{ 0% {{ margin-left:-35%; }} 100% {{ margin-left:100%; }} }} | |
| /* Overlaid at the card's bottom-right corner so cards with the button stay | |
| exactly the same height as cards without it — and it can't collide with | |
| the modality name in the head. */ | |
| .use-as-input {{ position:absolute !important; bottom:6px; right:8px; z-index:6; | |
| margin:0 !important; width:auto !important; min-width:0 !important; | |
| font-size:12px !important; padding:2px 9px !important; border-radius:999px !important; }} | |
| /* ---------- future prediction tab ---------- */ | |
| #fp-obs-mods, #fp-ctx, #cond-mod, #steer-cards {{ border:none; background:transparent; padding:0; }} | |
| #fp-obs-mods input, #fp-ctx input, #cond-mod input, #steer-cards input {{ display:none; }} | |
| #fp-obs-mods .wrap {{ display:flex; flex-wrap:wrap; gap:7px; }} | |
| #fp-obs-mods label {{ background:transparent; border:1px solid var(--line-2); border-radius:999px; | |
| padding:4px 13px; font-size:13.5px; font-weight:500; color:var(--text-2); cursor:pointer; | |
| transition:.15s; margin:0; }} | |
| #fp-obs-mods label::before {{ content:''; display:inline-block; width:7px; height:7px; | |
| border-radius:50%; background:currentColor; margin-right:7px; vertical-align:1px; }} | |
| {_FP_OBS_CHIP_CSS} | |
| #fp-obs-mods label:hover {{ color:var(--c, var(--text)); border-color:var(--c, var(--text-3)); }} | |
| #fp-obs-mods label.selected, #fp-obs-mods label:has(input:checked) {{ | |
| color:var(--c, var(--go)); border-color:color-mix(in srgb, var(--c, var(--go)) 55%, transparent); | |
| background:color-mix(in srgb, var(--c, var(--go)) 13%, transparent); | |
| }} | |
| #fp-ctx, #cond-mod {{ margin-top:6px; }} | |
| #fp-ctx .wrap, #cond-mod .wrap {{ display:inline-flex; gap:4px; background:var(--panel); border:1px solid var(--line); | |
| border-radius:999px; padding:3px; }} | |
| #fp-ctx label, #cond-mod label {{ border:none; background:transparent; border-radius:999px; padding:5px 14px; | |
| font-size:13.5px; color:var(--text-2); cursor:pointer; transition:.15s; margin:0; }} | |
| #fp-ctx label.selected, #fp-ctx label:has(input:checked), | |
| #cond-mod label.selected, #cond-mod label:has(input:checked) {{ | |
| background:var(--panel-2); color:var(--text); box-shadow:inset 0 0 0 1px var(--line-2); }} | |
| #steer-cards .wrap {{ display:grid; grid-template-columns:repeat(2, 1fr); gap:10px; }} | |
| #steer-cards label {{ display:block; background:var(--panel); border:1.5px solid var(--line); | |
| border-radius:10px; padding:11px 13px; cursor:pointer; transition:.15s; margin:0; | |
| font-weight:600; font-size:14px; color:var(--text); }} | |
| #steer-cards label:hover {{ border-color:var(--line-2); }} | |
| #steer-cards label::after {{ display:block; font-weight:400; font-size:12.5px; color:var(--text-3); | |
| line-height:1.45; margin-top:2px; }} | |
| #steer-cards label:nth-child(1) {{ --sc:var(--go); }} | |
| #steer-cards label:nth-child(2) {{ --sc:{MODALITY_COLORS['caption']}; }} | |
| #steer-cards label:nth-child(1)::after {{ content:"Nothing extra is given to the model"; }} | |
| #steer-cards label:nth-child(2)::after {{ content:"The prediction must satisfy a given caption or transcription"; }} | |
| #steer-cards label.selected, #steer-cards label:has(input:checked) {{ | |
| border-color:var(--sc); background:color-mix(in srgb, var(--sc) 6%, var(--panel)); }} | |
| .fp-timeline {{ background:var(--panel) !important; border:1px solid var(--line) !important; | |
| border-radius:10px !important; padding:12px 14px !important; gap:8px !important; margin:10px 0; }} | |
| .fp-timeline .block {{ border:none !important; background:transparent !important; padding:0 !important; }} | |
| /* gradio's pending state must not inflate the timeline while a select | |
| event is updating it -- its height comes from the strip image alone */ | |
| .fp-timeline, .fp-timeline * {{ min-height:0 !important; }} | |
| #fp-timeline-img {{ border:none !important; background:transparent !important; }} | |
| .tl-strip-img {{ width:100%; border-radius:6px; display:block; }} | |
| .tl-title {{ font-size:13.5px; color:var(--text-2); }} | |
| .tl-legend {{ display:flex; gap:16px; font-size:12.5px; color:var(--text-3); }} | |
| .tl-legend i {{ display:inline-block; width:10px; height:10px; border-radius:3px; margin-right:5px; | |
| vertical-align:-1px; }} | |
| .tl-legend i.obs {{ background:color-mix(in srgb, var(--go) 45%, var(--panel-2)); }} | |
| .tl-legend i.fut {{ background:var(--panel-2); border:1px dashed var(--line-2); }} | |
| .order-line {{ font-size:13px; color:var(--text-3); margin:4px 0 8px; line-height:1.6; }} | |
| #fp-gen-btn {{ font-weight:600 !important; font-size:16px !important; border-radius:10px !important; | |
| padding:0 18px !important; }} | |
| #fp-gen-btn:disabled {{ background:var(--go-dim) !important; cursor:wait; }} | |
| #fp-seed-box {{ background:var(--panel) !important; border:1px solid var(--line) !important; | |
| border-radius:10px !important; padding:0 8px 0 12px !important; | |
| display:flex !important; align-items:center !important; overflow:hidden; }} | |
| #fp-seed-box label {{ display:flex !important; flex-direction:row !important; align-items:center !important; | |
| gap:8px; margin:0 !important; width:100%; }} | |
| #fp-seed-box label > span {{ margin:0 !important; font-size:13.5px !important; color:var(--text-3) !important; }} | |
| #fp-seed-box input {{ font-family:ui-monospace, monospace; text-align:center; font-size:16px; | |
| background:transparent !important; border:none !important; box-shadow:none !important; | |
| padding:0 !important; width:100%; min-width:48px; appearance:textfield; -moz-appearance:textfield; }} | |
| #fp-seed-box input::-webkit-outer-spin-button, #fp-seed-box input::-webkit-inner-spin-button {{ | |
| -webkit-appearance:none; margin:0; }} | |
| /* gradio's floating "x.xs" progress timers are visual noise next to our | |
| own spinners -- hide them app-wide */ | |
| .progress-text {{ display:none !important; }} | |
| /* ---------- misc ---------- */ | |
| .left-col {{ border-right:1px solid var(--line); padding-right:22px !important; }} | |
| .right-col {{ padding-left:6px !important; }} | |
| @media (max-width: 900px) {{ .left-col {{ border-right:none; padding-right:0 !important; }} }} | |
| @media (prefers-reduced-motion: reduce) {{ * {{ animation:none !important; transition:none !important; }} }} | |
| button:focus-visible, textarea:focus-visible, input:focus-visible {{ | |
| outline:2px solid var(--go); outline-offset:2px; }} | |
| """ | |
| # --- shared example stems (computed once; offline-safe) ------------------------- | |
| _GALLERY_STEMS = _list_examples_safe(list_examples) | |
| # One clickable caption chip per curated example, so a visitor typing a | |
| # caption has real ground-truth captions to start from rather than a | |
| # hand-picked few. Same offline-safe pattern as _GALLERY_STEMS: any example | |
| # whose caption preview can't be fetched is just skipped. | |
| CAPTION_SAMPLES = [c for c in (_safe_preview(stem, "caption") for stem in _GALLERY_STEMS) if c] | |
| # --- Future Prediction tab helpers ---------------------------------------------- | |
| # 1 seed card + 1 optional steering-condition card + up to 10 target cards. | |
| FP_MAX_SLOTS = 12 | |
| # Clips are 17 frames @ 4 fps; the timeline shows one slot per frame. | |
| FP_TOTAL_FRAMES = 17 | |
| def _fp_extra(cond_mode, cond_modality): | |
| """Effective conditioning modality: None when unconditional.""" | |
| return None if cond_mode == "none" else cond_modality | |
| def _fp_chain(seed_modality, extra, targets): | |
| """The chain generate_future_prediction runs: always the fixed, tuned | |
| FUTURE_CHAIN order filtered down to what's needed -- the user picks WHICH | |
| modalities to predict, never the order. The seed modality (its given | |
| frames are completed in place) and the conditioning modality (fully | |
| given; the schedule builder pops it) are structurally required in-chain. | |
| """ | |
| keep = set(targets) | {seed_modality} | ({extra} if extra else set()) | |
| return [m for m in FUTURE_CHAIN if m in keep] | |
| def _load_preview_frames(stem, modality, n): | |
| """First n frames of the per-modality ground-truth preview mp4 as uint8 | |
| [n,H,W,3], or None when there is no preview / decoding fails.""" | |
| path = _safe_preview(stem, modality) if stem else None | |
| if path is None: | |
| return None | |
| try: | |
| vr = VideoReader(path, ctx=cpu()) | |
| return vr.get_batch(list(range(min(n, len(vr))))).asnumpy() | |
| except Exception: | |
| traceback.print_exc() | |
| return None | |
| def _timeline_strip_image(frames, n_obs, n_total=FP_TOTAL_FRAMES, tile_h=64, tile_w=48, gap=4): | |
| """The observation timeline: n_total portrait slots (frames center-cropped | |
| to the tile aspect). Only the first n_obs slots show frames -- exactly | |
| what the model receives; everything after the thick orange 'now' divider | |
| stays a dark placeholder, so the user never sees the ground-truth | |
| continuation. Pure numpy so it is unit-testable without gradio or a GPU.""" | |
| h = tile_h | |
| w = n_total * tile_w + (n_total - 1) * gap | |
| img = np.zeros((h, w, 3), dtype=np.uint8) | |
| img[:] = (13, 16, 21) # page background in the gaps | |
| for i in range(n_total): | |
| x = i * (tile_w + gap) | |
| if frames is not None and i < min(n_obs, len(frames)): | |
| f = frames[i] | |
| src_h, src_w = f.shape[:2] | |
| crop_w = min(src_w, max(1, round(src_h * tile_w / tile_h))) | |
| x0 = (src_w - crop_w) // 2 | |
| f = f[:, x0:x0 + crop_w] | |
| ys = np.arange(tile_h) * f.shape[0] // tile_h | |
| xs = np.arange(tile_w) * f.shape[1] // tile_w | |
| img[:, x:x + tile_w] = f[ys][:, xs] | |
| else: | |
| img[:, x:x + tile_w] = (14, 18, 25) | |
| if 0 < n_obs < n_total: | |
| xd = n_obs * (tile_w + gap) - gap | |
| img[:, max(xd - 1, 0):xd + gap + 1] = (255, 122, 69) # the 'now' divider | |
| return img | |
| def _strip_html(img): | |
| """Embeds the timeline strip as a data-URI <img> inside plain HTML. | |
| gr.Image wraps its content in gradio chrome (including a full-size | |
| preview <button>) that varies between gradio 4.44 and 5.6 and fights our | |
| styling -- raw HTML has none of that.""" | |
| buf = io.BytesIO() | |
| PILImage.fromarray(img).save(buf, format="PNG") | |
| b64 = base64.b64encode(buf.getvalue()).decode("ascii") | |
| return f'<img class="tl-strip-img" alt="observation timeline" src="data:image/png;base64,{b64}">' | |
| def _tl_title_html(obs_mod, n_frames, stem): | |
| color = MODALITY_COLORS[obs_mod] | |
| name = MODALITY_DISPLAY_NAMES[obs_mod] | |
| stem_html = ( | |
| f' of <span class="mono" style="color:var(--text-3)">{stem}</span>' | |
| if stem else " — pick an example above" | |
| ) | |
| return ( | |
| f'<div class="tl-title">Input to the model: <b style="color:{color}">{name}</b> · ' | |
| f"frames 1–{n_frames}{stem_html} · 4 fps</div>" | |
| ) | |
| _TL_LEGEND_HTML = ( | |
| '<div class="tl-legend"><span><i class="obs"></i>given as input</span>' | |
| '<span><i class="fut"></i>future — not given to the model</span></div>' | |
| ) | |
| def _fake_future_results(chain, extra_cond, steer_text): | |
| """FOURM_FAKE_STREAM=1 stand-in for generate_future_prediction: the same | |
| results-dict shape, placeholder media, no model / GPU.""" | |
| results = {} | |
| for key in chain: | |
| if key == extra_cond: | |
| continue | |
| results[key] = _FAKE_TEXT[key] if key in TEXT_MODALITIES else _fake_tinted_video(key) | |
| if extra_cond: | |
| results[f"input_{extra_cond}"] = steer_text or _FAKE_TEXT[extra_cond] | |
| time.sleep(1.0) | |
| return results | |
| # --- UI ------------------------------------------------------------------------- | |
| with gr.Blocks(title="A2A-Video: Any-to-All Video Generation", theme=_THEME, css=_CSS, js=_LOAD_JS) as demo: | |
| gr.HTML(_header_html()) | |
| with gr.Tabs(): | |
| # ======================= TAB 1: ANY-TO-ANY ========================= | |
| with gr.Tab("Any-to-Any Generation"): | |
| example_state = gr.State(None) | |
| input_modality_state = gr.State(DEFAULT_INPUT_MODALITY) | |
| chain_state = gr.State(DEFAULT_CHAIN_STATE) | |
| results_state = gr.State({}) | |
| with gr.Row(): | |
| # ------------------------- LEFT ---------------------------- | |
| with gr.Column(scale=7, elem_classes=["left-col"]): | |
| gr.HTML(_step_label_html( | |
| 1, "Select input to A2A-Video", | |
| "Pick an example clip or upload your own video for RGB input, choose another " | |
| "modality from a curated example, or provide a caption -- whichever you choose " | |
| "becomes the input A2A-Video conditions on.", | |
| )) | |
| with gr.Column(elem_id="src-zone"): | |
| src_radio = gr.Radio( | |
| choices=[ | |
| ("RGB video", "video"), | |
| ("Abstract modality", "abstract"), | |
| ("Caption", "caption"), | |
| ], | |
| value="video", show_label=False, container=False, elem_id="src-cards", | |
| ) | |
| with gr.Column(visible=True) as body_video: | |
| selected_md, rgb_select_btns = _build_example_gallery(_GALLERY_STEMS) | |
| upload_video = gr.Video( | |
| label="Upload a short clip — the first ~4 s are used, center-cropped to 128×128", | |
| sources=["upload"], elem_id="upload-dropbox", height=200, | |
| ) | |
| with gr.Column(visible=False) as body_caption: | |
| caption_box = gr.Textbox( | |
| show_label=False, lines=3, | |
| placeholder="A short description of a scene to generate a video for...", | |
| elem_id="caption-input", | |
| ) | |
| with gr.Column(elem_classes=["cap-samples"]): | |
| cap_sample_btns = [] | |
| for sample in CAPTION_SAMPLES: | |
| cap_sample_btns.append( | |
| (sample, gr.Button(f"“{sample}”", size="sm", elem_classes=["cap-chip"], min_width=0)) | |
| ) | |
| with gr.Column(visible=False) as body_abstract: | |
| abs_mod_radio = gr.Radio( | |
| choices=[(MODALITY_DISPLAY_NAMES[k], k) for k in ABS_MODALITIES], | |
| value=DEFAULT_ABS_MODALITY, show_label=False, container=False, elem_id="abs-mods", | |
| ) | |
| abs_selected_md = gr.Markdown("No example selected yet.", elem_classes=["hint-md"], elem_id="abs-selected") | |
| def render_abs_gallery(abs_mod): | |
| if not _GALLERY_STEMS: | |
| gr.Markdown("No example previews available yet.", elem_classes=["hint-md"]) | |
| with gr.Column(elem_classes=["example-gallery-scroll"]): | |
| with gr.Row(elem_classes=["tile-grid"]): | |
| for stem in _GALLERY_STEMS: | |
| with gr.Column(min_width=0, elem_classes=["example-tile-col"]): | |
| if abs_mod == "transcription": | |
| try: | |
| text = get_example_transcription_tagged(stem) or "" | |
| except Exception: | |
| text = "(no transcript preview)" | |
| gr.HTML(f'<div class="abs-text-tile">{text[:150]}…</div>') | |
| else: | |
| path = _safe_preview(stem, abs_mod) | |
| if path is not None: | |
| gr.Video( | |
| value=path, height=82, width=110, | |
| interactive=False, show_label=False, show_download_button=False, | |
| elem_classes=["example-preview-video", "clip-tile"], | |
| ) | |
| else: | |
| gr.HTML('<div class="abs-text-tile">no preview —<br>still selectable</div>') | |
| b = gr.Button("Select", size="sm", elem_classes=["select-btn"]) | |
| def _abs_select(_stem=stem): | |
| return _stem, f"**Selected example:** `{_stem}`" | |
| b.click(None, js=_sel_busy_js("abs-selected")) | |
| b.click(_abs_select, outputs=[example_state, abs_selected_md], show_progress="hidden").then(None, js=_sel_done_js("abs-selected")) | |
| gr.HTML(_step_label_html( | |
| 2, "Generation chain", | |
| "Add modalities one at a time, in the order you want them generated -- " | |
| "each one conditions on everything added before it.", | |
| )) | |
| with gr.Column(elem_id="chain-zone"): | |
| preset_radio = gr.Radio( | |
| choices=[("Recommended chain", "coarse"), ("Direct", "direct"), ("Custom", "custom")], | |
| value="coarse", show_label=False, container=False, elem_id="chain-presets", | |
| ) | |
| def render_chain(input_mod, chain): | |
| with gr.Row(elem_classes=["chain-rail", "a2a-chips"]): | |
| gr.HTML(_chip_html(input_mod, is_input=True)) | |
| for i, key in enumerate(chain): | |
| gr.HTML('<span class="arrow">→</span>', elem_classes=["arrow-html"]) | |
| rm = gr.Button( | |
| f"{MODALITY_DISPLAY_NAMES[key]} ✕", size="sm", min_width=0, | |
| elem_classes=["chip-solid", f"accent-{key}"], | |
| ) | |
| def _remove(ch, preset, _i=i): | |
| ch = list(ch) | |
| if _i < len(ch): | |
| ch.pop(_i) | |
| # Direct mode stays Direct (empty rail until the | |
| # next single-target click); anything else | |
| # becomes a custom chain. | |
| return ch, ("direct" if preset == "direct" else "custom") | |
| rm.click(None, js=_chip_busy_js("a2a-chips", ".chain-rail.a2a-chips")) | |
| rm.click(_remove, inputs=[chain_state, preset_radio], outputs=[chain_state, preset_radio], show_progress="hidden") | |
| if not chain: | |
| gr.HTML('<span class="hint">→ add at least one target modality</span>') | |
| with gr.Row(elem_classes=["avail-chips", "a2a-chips"]): | |
| for key in [k for k in MODALITY_DISPLAY_NAMES if k != input_mod and k not in chain]: | |
| add = gr.Button( | |
| f"+ {MODALITY_DISPLAY_NAMES[key]}", size="sm", min_width=0, | |
| elem_classes=["chip-ghost", f"accent-{key}"], | |
| ) | |
| def _append(ch, imod, preset, _k=key): | |
| # In Direct mode a modality click REPLACES the | |
| # chain (input -> that single target) and the | |
| # preset stays Direct; otherwise it appends and | |
| # the chain becomes custom. | |
| if preset == "direct": | |
| return [_k], "direct" | |
| ch = list(ch) | |
| if _k != imod and _k not in ch: | |
| ch.append(_k) | |
| return ch, "custom" | |
| add.click(None, js=_chip_busy_js("a2a-chips", ".chain-rail.a2a-chips")) | |
| add.click(_append, inputs=[chain_state, input_modality_state, preset_radio], | |
| outputs=[chain_state, preset_radio], show_progress="hidden") | |
| gr.HTML( | |
| f'<p class="eta-note">Total {len(chain)} modalit{"y" if len(chain) == 1 else "ies"} ' | |
| f'selected in the chain</p>' | |
| ) | |
| gr.HTML(_step_label_html(3, "Generate")) | |
| with gr.Row(elem_classes=["gen-row"]): | |
| run_btn = gr.Button("Generate", variant="primary", elem_id="gen-btn", scale=4) | |
| seed_box = gr.Number(value=0, precision=0, label="seed", elem_id="seed-box", scale=0, min_width=130) | |
| with gr.Accordion("Advanced: generation hyperparameters", open=False): | |
| with gr.Row(): | |
| top_p_slider = gr.Slider(0.0, 1.0, value=0.8, step=0.01, label="Top-p (nucleus)") | |
| top_k_slider = gr.Slider(0, 200, value=0, step=1, label="Top-k (0 = off)") | |
| hyperparam_inputs, hyperparam_spec = _build_hyperparam_controls( | |
| _hyperparam_preset_key_for_modality(DEFAULT_INPUT_MODALITY) | |
| ) | |
| # ------------------------- RIGHT --------------------------- | |
| with gr.Column(scale=6, elem_classes=["right-col"]): | |
| with gr.Row(elem_classes=["res-head-row"]): | |
| gr.HTML('<h2 class="res-title">Generation results</h2>') | |
| status_html = gr.HTML(_status_html("idle"), elem_id="status-line") | |
| empty_state = gr.HTML(_empty_state_html(), elem_id="empty-state") | |
| slot_cols, slot_heads, slot_videos, slot_texts, slot_progs, slot_uses = [], [], [], [], [], [] | |
| with gr.Column(elem_classes=["results-grid"]): | |
| for i in range(MAX_SLOTS): | |
| with gr.Column(visible=False, elem_id=f"res-card-{i}", elem_classes=["res-card"]) as col: | |
| head = gr.HTML("") | |
| video = gr.Video( | |
| interactive=False, autoplay=False, loop=True, show_label=False, | |
| height=RESULT_VIDEO_HEIGHT, visible=False, elem_classes=["rc-video"], | |
| ) | |
| text = gr.Textbox( | |
| visible=False, show_label=False, lines=TEXT_OUTPUT_LINES, | |
| max_lines=TEXT_OUTPUT_LINES, show_copy_button=True, elem_classes=["rc-text"], | |
| ) | |
| prog = gr.HTML("", visible=False, elem_classes=["rc-prog"]) | |
| use_btn = gr.Button("Use as input ↺", visible=False, size="sm", elem_classes=["use-as-input"]) | |
| slot_cols.append(col) | |
| slot_heads.append(head) | |
| slot_videos.append(video) | |
| slot_texts.append(text) | |
| slot_progs.append(prog) | |
| slot_uses.append(use_btn) | |
| STREAM_OUTPUTS = [status_html, empty_state, run_btn, results_state] | |
| for i in range(MAX_SLOTS): | |
| STREAM_OUTPUTS += [slot_cols[i], slot_heads[i], slot_videos[i], slot_texts[i], slot_progs[i], slot_uses[i]] | |
| assert len(STREAM_OUTPUTS) == N_STREAM_OUTPUTS | |
| # ---------------------- event wiring --------------------------- | |
| for stem, btn in rgb_select_btns: | |
| def _rgb_select(_stem=stem): | |
| return _stem, f"**Selected example:** `{_stem}`" | |
| btn.click(None, js=_sel_busy_js("a2a-selected")) | |
| btn.click(_rgb_select, outputs=[example_state, selected_md], show_progress="hidden").then(None, js=_sel_done_js("a2a-selected")) | |
| for sample, btn in cap_sample_btns: | |
| btn.click(lambda _s=sample: _s, outputs=[caption_box]) | |
| def on_src_change(src_mode, abs_mod, chain, preset): | |
| input_mod = {"video": "rgb", "caption": "caption"}.get(src_mode, abs_mod) | |
| new_chain = _chain_for_input_change(preset, input_mod, chain) | |
| return ( | |
| gr.update(visible=src_mode == "video"), | |
| gr.update(visible=src_mode == "caption"), | |
| gr.update(visible=src_mode == "abstract"), | |
| input_mod, new_chain, | |
| *_hyperparam_slider_updates(input_mod, hyperparam_spec), | |
| ) | |
| src_radio.change(None, js=_sel_busy_js("src-zone")) | |
| src_radio.change( | |
| on_src_change, | |
| inputs=[src_radio, abs_mod_radio, chain_state, preset_radio], | |
| outputs=[body_video, body_caption, body_abstract, input_modality_state, chain_state, | |
| *hyperparam_inputs], | |
| show_progress="hidden", | |
| ).then(None, js=_sel_done_js("src-zone")) | |
| def on_abs_change(abs_mod, src_mode, chain, preset): | |
| input_mod = abs_mod if src_mode == "abstract" else {"video": "rgb", "caption": "caption"}[src_mode] | |
| new_chain = _chain_for_input_change(preset, input_mod, chain) | |
| return ( | |
| input_mod, new_chain, | |
| *_hyperparam_slider_updates(input_mod, hyperparam_spec), | |
| ) | |
| abs_mod_radio.change( | |
| on_abs_change, | |
| inputs=[abs_mod_radio, src_radio, chain_state, preset_radio], | |
| outputs=[input_modality_state, chain_state, *hyperparam_inputs], | |
| ) | |
| def on_preset(preset, input_mod): | |
| new_chain = _preset_chain(preset, input_mod) | |
| return gr.skip() if new_chain is None else new_chain | |
| # .input (not .change): flipping the radio to "custom" | |
| # programmatically from a chip click must not loop back here. | |
| preset_radio.input(None, js=_sel_busy_js("chain-zone")) | |
| preset_radio.input( | |
| on_preset, inputs=[preset_radio, input_modality_state], outputs=[chain_state], | |
| show_progress="hidden", | |
| ).then(None, js=_sel_done_js("chain-zone")) | |
| def run_demo_stream(src_mode, example_stem, upload_path, caption_text, abs_mod, | |
| chain, seed_value, top_p, top_k, *hyperparam_values): | |
| # Pin down what's really being generated from the source mode | |
| # rather than trusting any UI state that could lag. | |
| if src_mode == "caption": | |
| input_modality = "caption" | |
| elif src_mode == "abstract": | |
| input_modality = abs_mod | |
| else: | |
| input_modality = "rgb" | |
| chain = _clean_chain(input_modality, chain) | |
| if not chain: | |
| raise gr.Error("Please add at least one modality to the chain first.") | |
| kwargs = {} | |
| if src_mode == "video" and upload_path: | |
| kwargs["raw_video_path"] = upload_path | |
| elif src_mode == "video": | |
| if not example_stem: | |
| raise gr.Error("Please pick an example clip or upload a video first.") | |
| kwargs["example_stem"] = example_stem | |
| elif src_mode == "caption": | |
| if not caption_text or not caption_text.strip(): | |
| raise gr.Error("Please type a caption first.") | |
| kwargs["raw_caption_text"] = caption_text.strip() | |
| else: | |
| if not example_stem: | |
| raise gr.Error("Please pick an example clip first.") | |
| kwargs["example_stem"] = example_stem | |
| # Decoding steps aren't exposed as sliders -- seed overrides | |
| # from the tuned preset matching the input modality, then | |
| # layer the user-adjustable temp/cfg slider values on top. | |
| preset_key = _hyperparam_preset_key_for_modality(input_modality) | |
| overrides = {key: dict(vals) for key, vals in HYPERPARAM_PRESETS[preset_key].items()} | |
| for (key, param), value in zip(hyperparam_spec, hyperparam_values): | |
| overrides.setdefault(key, {})[param] = value | |
| seed_value = int(seed_value) | |
| ctx = _new_stream_ctx(input_modality, chain) | |
| yield _frame(_reset_frame_updates(seed_value)) | |
| if os.environ.get("FOURM_FAKE_STREAM") == "1": | |
| events = _fake_stream_events(input_modality, chain) | |
| elif os.environ.get("FOURM_DISABLE_STREAMING") == "1": | |
| results = generate_any_to_any( | |
| input_modality=input_modality, chain=chain, overrides=overrides, | |
| seed=seed_value, top_p=float(top_p), top_k=float(top_k), **kwargs, | |
| ) | |
| events = _synthesize_events_from_results(input_modality, chain, results) | |
| else: | |
| events = generate_any_to_any_stream( | |
| input_modality=input_modality, chain=chain, overrides=overrides, | |
| seed=seed_value, top_p=float(top_p), top_k=float(top_k), **kwargs, | |
| ) | |
| # The input card and ALL predictions are revealed TOGETHER at | |
| # the end: slot updates are buffered into `pending` while the | |
| # run is live. Mid-run feedback is a bare "generating" line | |
| # (no percentage, never per-modality steps — the results | |
| # should feel like one parallel batch). | |
| try: | |
| pending = {} | |
| for event in events: | |
| kind = event[0] | |
| pending.update(_apply_stream_event(event, ctx)) | |
| if kind in ("start", "progress", "result"): | |
| status = "generating" | |
| yield _frame({ | |
| IDX_STATUS: gr.update(value=_status_html(status)), | |
| IDX_EMPTY: gr.update(value=_loading_state_html(seed_value, status), visible=True), | |
| }) | |
| elif kind == "done": | |
| pending[IDX_EMPTY] = gr.update(value=_empty_state_html(), visible=False) | |
| yield _frame(pending) | |
| except gr.Error: | |
| raise | |
| except Exception as e: | |
| traceback.print_exc() | |
| yield _frame({ | |
| IDX_STATUS: gr.update(value=_status_html("error — see server logs")), | |
| IDX_EMPTY: gr.update(value=_empty_state_html(), visible=False), | |
| IDX_BTN: gr.update(value="Generate", interactive=True), | |
| }) | |
| raise gr.Error(f"Generation failed: {e}") | |
| run_btn.click( | |
| run_demo_stream, | |
| inputs=[src_radio, example_state, upload_video, caption_box, abs_mod_radio, | |
| chain_state, seed_box, top_p_slider, top_k_slider, *hyperparam_inputs], | |
| outputs=STREAM_OUTPUTS, | |
| show_progress="hidden", | |
| ) | |
| def _make_use_handler(i): | |
| def _use(results): | |
| key = (results or {}).get("_slot_keys", {}).get(i) | |
| stored = (results or {}).get(key) | |
| if key == "rgb" and stored: | |
| # Generated RGB mp4 becomes a fresh upload: it gets | |
| # re-tokenized through the raw-video path on the next run. | |
| return gr.update(value="video"), gr.update(value=stored), gr.skip() | |
| if key == "caption" and stored: | |
| return gr.update(value="caption"), gr.skip(), gr.update(value=stored) | |
| return gr.skip(), gr.skip(), gr.skip() | |
| return _use | |
| for i in range(MAX_SLOTS): | |
| # Setting src_radio's value cascades through its .change | |
| # handler, which flips the visible body and re-derives | |
| # input_modality_state — same path as a manual click. | |
| slot_uses[i].click(_make_use_handler(i), inputs=[results_state], | |
| outputs=[src_radio, upload_video, caption_box]) | |
| # ======================= TAB 2: FUTURE PREDICTION =================== | |
| with gr.Tab("Future Prediction"): | |
| fp_targets_state = gr.State(["depth", "det"]) | |
| with gr.Row(): | |
| with gr.Column(scale=7, elem_classes=["left-col"]): | |
| gr.HTML(_step_label_html( | |
| 1, "Select observation", | |
| "Pick an example clip and the modality you want to observe -- its future " | |
| "frames are what gets predicted.", | |
| )) | |
| _future_stems = _list_examples_safe(list_future_examples) | |
| future_example_state = gr.State(None) | |
| if not _future_stems: | |
| gr.Markdown("No example previews available yet.", elem_classes=["hint-md"]) | |
| else: | |
| gr.Markdown("Hover to preview · click to select.", elem_classes=["hint-md"]) | |
| future_selected_example_md = gr.Markdown("No example selected yet.", elem_classes=["hint-md"], elem_id="fp-selected") | |
| fp_obs_radio = gr.Radio( | |
| choices=[(MODALITY_DISPLAY_NAMES[m], m) for m in FUTURE_SEED_MODALITIES], | |
| value=DEFAULT_INPUT_MODALITY, show_label=False, container=False, elem_id="fp-obs-mods", | |
| ) | |
| # Gallery tiles re-render in whichever modality the model | |
| # will observe (same pattern as tab 1's abstract gallery). | |
| def render_fp_gallery(obs_mod): | |
| with gr.Column(elem_classes=["example-gallery-scroll"]): | |
| with gr.Row(elem_classes=["tile-grid"]): | |
| for stem in _future_stems: | |
| with gr.Column(min_width=0, elem_classes=["example-tile-col"]): | |
| path = _safe_preview(stem, obs_mod) | |
| if path is not None: | |
| gr.Video( | |
| value=path, height=82, width=110, | |
| interactive=False, show_label=False, show_download_button=False, | |
| elem_classes=["example-preview-video", "clip-tile"], | |
| ) | |
| else: | |
| gr.HTML('<div class="abs-text-tile">no preview —<br>still selectable</div>') | |
| b = gr.Button("Select", size="sm", elem_classes=["select-btn"]) | |
| b.click(None, js=_sel_busy_js("fp-selected")) | |
| b.click( | |
| fn=lambda seed_tokens, cond_mode, cond_mod, _stem=stem, _mod=obs_mod: on_fp_example_select( | |
| _stem, _mod, seed_tokens, _fp_extra(cond_mode, cond_mod) or "none"), | |
| inputs=[fp_ctx_radio, cond_mode_radio, cond_mod_radio], | |
| outputs=[future_example_state, future_selected_example_md, | |
| fp_tl_title, fp_tl_image, | |
| future_extra_preview_text, future_extra_tags_note], | |
| show_progress="hidden", | |
| ).then(None, js=_sel_done_js("fp-selected")) | |
| gr.HTML('<p class="hint">How much of it to give as input:</p>') | |
| fp_ctx_radio = gr.Radio( | |
| choices=list(FUTURE_SEED_TOKEN_OPTIONS.items()), value=512, | |
| show_label=False, container=False, elem_id="fp-ctx", | |
| ) | |
| gr.HTML('<p class="hint">Providing more frames makes the prediction more deterministic.</p>') | |
| with gr.Column(elem_classes=["fp-timeline"], elem_id="fp-tl-panel"): | |
| fp_tl_title = gr.HTML(_tl_title_html(DEFAULT_INPUT_MODALITY, SEED_TOKENS_TO_FRAMES[512], None)) | |
| fp_tl_image = gr.HTML( | |
| _strip_html(_timeline_strip_image(None, SEED_TOKENS_TO_FRAMES[512])), | |
| elem_id="fp-timeline-img", | |
| ) | |
| gr.HTML(_TL_LEGEND_HTML) | |
| gr.HTML(_step_label_html( | |
| 2, "Conditioning", | |
| "Optionally condition the prediction on a caption or a transcription, " | |
| "to steer the predicted trajectory.", | |
| )) | |
| with gr.Column(elem_id="cond-zone"): | |
| cond_mode_radio = gr.Radio( | |
| choices=[("Unconditional", "none"), ("Conditional", "cond")], | |
| value="none", show_label=False, container=False, elem_id="steer-cards", | |
| ) | |
| cond_mod_radio = gr.Radio( | |
| choices=[("Caption", "caption"), ("Transcription", "transcription")], | |
| value="caption", show_label=False, container=False, elem_id="cond-mod", | |
| visible=False, | |
| ) | |
| future_extra_preview_text = gr.Textbox( | |
| show_label=False, visible=False, interactive=True, | |
| lines=TEXT_OUTPUT_LINES, max_lines=TEXT_OUTPUT_LINES, elem_id="steer-text", | |
| ) | |
| future_extra_tags_note = gr.Markdown( | |
| "**Don't remove the `[SEC_1]`-`[SEC_4]`/`[EOS]` tags** -- they're structurally " | |
| "required by the model; edit the text between/around them freely.", | |
| visible=False, elem_classes=["hint-md"], | |
| ) | |
| gr.HTML(_step_label_html( | |
| 3, "Select modalities to predict", | |
| "Generation uses chaining, where earlier predictions condition later ones.", | |
| )) | |
| def render_fp_targets(targets, obs_mod, cond_mode, cond_mod): | |
| extra = _fp_extra(cond_mode, cond_mod) | |
| with gr.Row(elem_classes=["avail-chips", "fp-chips"]): | |
| for key in [k for k in MODALITY_DISPLAY_NAMES if k != obs_mod and k != extra]: | |
| selected = key in targets | |
| b = gr.Button( | |
| MODALITY_DISPLAY_NAMES[key] if selected else f"+ {MODALITY_DISPLAY_NAMES[key]}", | |
| size="sm", min_width=0, | |
| elem_classes=[("chip-solid" if selected else "chip-ghost"), f"accent-{key}"], | |
| ) | |
| def _toggle(ts, _k=key): | |
| ts = list(ts) | |
| if _k in ts: | |
| ts.remove(_k) | |
| else: | |
| ts.append(_k) | |
| return ts | |
| b.click(None, js=_chip_busy_js("fp-chips", ".avail-chips.fp-chips")) | |
| b.click(_toggle, inputs=[fp_targets_state], outputs=[fp_targets_state], show_progress="hidden") | |
| if not targets: | |
| gr.HTML('<p class="order-line">Pick at least one modality to predict.</p>') | |
| gr.HTML(_step_label_html(4, "Generate")) | |
| with gr.Row(elem_classes=["gen-row"]): | |
| future_run_btn = gr.Button("Predict the future", variant="primary", elem_id="fp-gen-btn", scale=4) | |
| future_seed_number = gr.Number(value=0, precision=0, label="seed", elem_id="fp-seed-box", scale=0, min_width=130) | |
| with gr.Accordion("Advanced: generation hyperparameters", open=False): | |
| with gr.Row(): | |
| fp_top_p = gr.Slider(0.0, 1.0, value=0.8, step=0.01, label="Top-p (nucleus)") | |
| fp_top_k = gr.Slider(0, 200, value=0, step=1, label="Top-k (0 = off)") | |
| future_hyperparam_inputs, future_hyperparam_spec = _build_hyperparam_controls() | |
| with gr.Column(scale=6, elem_classes=["right-col"]): | |
| with gr.Row(elem_classes=["res-head-row"]): | |
| gr.HTML('<h2 class="res-title">Future predictions</h2>') | |
| fp_status_html = gr.HTML(_status_html("idle"), elem_id="fp-status-line") | |
| fp_empty_state = gr.HTML('<div class="empty-state"><p>Future predictions will appear here.</p></div>') | |
| fp_slot_cols, fp_slot_heads, fp_slot_videos, fp_slot_texts = [], [], [], [] | |
| with gr.Column(elem_classes=["results-grid"]): | |
| for i in range(FP_MAX_SLOTS): | |
| with gr.Column(visible=False, elem_id=f"fp-card-{i}", elem_classes=["res-card"]) as col: | |
| head = gr.HTML("") | |
| video = gr.Video( | |
| interactive=False, autoplay=False, loop=True, show_label=False, | |
| height=RESULT_VIDEO_HEIGHT, visible=False, elem_classes=["rc-video"], | |
| ) | |
| text = gr.Textbox( | |
| visible=False, show_label=False, lines=TEXT_OUTPUT_LINES, | |
| max_lines=TEXT_OUTPUT_LINES, show_copy_button=True, elem_classes=["rc-text"], | |
| ) | |
| fp_slot_cols.append(col) | |
| fp_slot_heads.append(head) | |
| fp_slot_videos.append(video) | |
| fp_slot_texts.append(text) | |
| FP_IDX_STATUS, FP_IDX_EMPTY, FP_IDX_BTN = 0, 1, 2 | |
| FP_SLOT_BASE, FP_SLOT_WIDTH = 3, 4 | |
| FP_OUTPUTS = [fp_status_html, fp_empty_state, future_run_btn] | |
| for i in range(FP_MAX_SLOTS): | |
| FP_OUTPUTS += [fp_slot_cols[i], fp_slot_heads[i], fp_slot_videos[i], fp_slot_texts[i]] | |
| N_FP_OUTPUTS = len(FP_OUTPUTS) | |
| def _fp_slot(i, off): | |
| return FP_SLOT_BASE + i * FP_SLOT_WIDTH + off | |
| def _fp_frame(updates): | |
| out = [gr.skip()] * N_FP_OUTPUTS | |
| for idx, upd in updates.items(): | |
| out[idx] = upd | |
| return tuple(out) | |
| # ---------------------- event wiring --------------------------- | |
| def _future_extra_preview(example_stem, steer): | |
| """Prefills the steering textarea from the selected example | |
| (tagged transcription / caption); empty but visible when no | |
| example is picked yet so the user can still type their own.""" | |
| if steer == "none": | |
| return gr.update(value=None, visible=False), gr.update(visible=False) | |
| text = None | |
| if example_stem: | |
| try: | |
| if steer == "transcription": | |
| text = get_example_transcription_tagged(example_stem) | |
| else: | |
| text = get_example_preview(example_stem, steer) | |
| except Exception: | |
| traceback.print_exc() | |
| return gr.update(value=text, visible=True), gr.update(visible=steer == "transcription") | |
| def _fp_timeline_update(example_stem, obs_mod, seed_tokens): | |
| n = SEED_TOKENS_TO_FRAMES[seed_tokens] | |
| # Only the input frames are loaded/shown -- nothing from the | |
| # future, so the user is not biased by the GT continuation. | |
| frames = _load_preview_frames(example_stem, obs_mod, n) | |
| return _tl_title_html(obs_mod, n, example_stem), _strip_html(_timeline_strip_image(frames, n)) | |
| def on_fp_example_select(_stem, obs_mod, seed_tokens, steer): | |
| title, strip = _fp_timeline_update(_stem, obs_mod, seed_tokens) | |
| extra_t, extra_note = _future_extra_preview(_stem, steer) | |
| return _stem, f"**Selected example:** `{_stem}`", title, strip, extra_t, extra_note | |
| def on_fp_obs_change(obs_mod, example_stem, seed_tokens, targets): | |
| title, img = _fp_timeline_update(example_stem, obs_mod, seed_tokens) | |
| return title, img, [t for t in targets if t != obs_mod] | |
| fp_obs_radio.change(None, js=_sel_busy_js("fp-tl-panel")) | |
| fp_obs_radio.change( | |
| on_fp_obs_change, | |
| inputs=[fp_obs_radio, future_example_state, fp_ctx_radio, fp_targets_state], | |
| outputs=[fp_tl_title, fp_tl_image, fp_targets_state], | |
| show_progress="hidden", | |
| ).then(None, js=_sel_done_js("fp-tl-panel")) | |
| fp_ctx_radio.change(None, js=_sel_busy_js("fp-tl-panel")) | |
| fp_ctx_radio.change( | |
| lambda seed_tokens, example_stem, obs_mod: _fp_timeline_update(example_stem, obs_mod, seed_tokens), | |
| inputs=[fp_ctx_radio, future_example_state, fp_obs_radio], | |
| outputs=[fp_tl_title, fp_tl_image], | |
| show_progress="hidden", | |
| ).then(None, js=_sel_done_js("fp-tl-panel")) | |
| def on_cond_change(cond_mode, cond_mod, example_stem, targets): | |
| extra = _fp_extra(cond_mode, cond_mod) | |
| extra_t, extra_note = _future_extra_preview(example_stem, extra or "none") | |
| pruned = [t for t in targets if t != extra] | |
| return gr.update(visible=cond_mode == "cond"), extra_t, extra_note, pruned | |
| # Both switches busy the whole Conditioning section (cards, pills, | |
| # textbox) until the updated widgets arrive. | |
| for _cond_radio, _busy_host in ((cond_mode_radio, "cond-zone"), (cond_mod_radio, "cond-zone")): | |
| _cond_radio.change(None, js=_sel_busy_js(_busy_host)) | |
| _cond_radio.change( | |
| on_cond_change, | |
| inputs=[cond_mode_radio, cond_mod_radio, future_example_state, fp_targets_state], | |
| outputs=[cond_mod_radio, future_extra_preview_text, future_extra_tags_note, fp_targets_state], | |
| show_progress="hidden", | |
| ).then(None, js=_sel_done_js(_busy_host)) | |
| def run_future_demo(example_stem, obs_mod, seed_tokens, cond_mode, cond_mod, steer_text, targets, | |
| seed_value, top_p, top_k, *hyperparam_values): | |
| if not example_stem: | |
| raise gr.Error("Please pick an example clip first.") | |
| extra = _fp_extra(cond_mode, cond_mod) | |
| targets = [t for t in targets if t != obs_mod and t != extra] | |
| if not targets: | |
| raise gr.Error("Pick at least one modality to predict the future in.") | |
| chain = _fp_chain(obs_mod, extra, targets) | |
| # No HYPERPARAM_PRESETS base here -- Future Prediction uses | |
| # plain CONFIGS hyperparameters directly (sliders already | |
| # default to those values; only overridden if changed). | |
| overrides = {} | |
| for (key, param), value in zip(future_hyperparam_spec, hyperparam_values): | |
| overrides.setdefault(key, {})[param] = value | |
| seed_value = int(seed_value) | |
| arm = { | |
| FP_IDX_STATUS: gr.update(value=_status_html(f"warming up · seed {seed_value}")), | |
| FP_IDX_EMPTY: gr.update(value=_loading_state_html(seed_value), visible=True), | |
| FP_IDX_BTN: gr.update(value="Predicting…", interactive=False), | |
| } | |
| for i in range(FP_MAX_SLOTS): | |
| arm[_fp_slot(i, 0)] = gr.update(visible=False) | |
| arm[_fp_slot(i, 1)] = gr.update(value="") | |
| arm[_fp_slot(i, 2)] = gr.update(value=None, visible=False) | |
| arm[_fp_slot(i, 3)] = gr.update(value=None, visible=False) | |
| yield _fp_frame(arm) | |
| try: | |
| if os.environ.get("FOURM_FAKE_STREAM") == "1": | |
| results = _fake_future_results(chain, extra, steer_text) | |
| else: | |
| results = generate_future_prediction( | |
| example_stem, obs_mod, seed_tokens, chain, | |
| extra_cond_modality=extra, overrides=overrides, | |
| override_caption_text=steer_text if extra == "caption" else None, | |
| override_transcription_text=steer_text if extra == "transcription" else None, | |
| seed=seed_value, top_p=float(top_p), top_k=float(top_k), | |
| ) | |
| except gr.Error: | |
| raise | |
| except Exception as e: | |
| traceback.print_exc() | |
| yield _fp_frame({ | |
| FP_IDX_STATUS: gr.update(value=_status_html("error — see server logs")), | |
| FP_IDX_EMPTY: gr.update(value=_empty_state_html(), visible=False), | |
| FP_IDX_BTN: gr.update(value="Predict the future", interactive=True), | |
| }) | |
| raise gr.Error(f"Generation failed: {e}") | |
| # All cards revealed together: seed card, optional steering | |
| # condition, then the targets in the user's click order. | |
| n_given = SEED_TOKENS_TO_FRAMES[seed_tokens] | |
| cards = [(obs_mod, f"input · {n_given} frame" + ("s" if n_given != 1 else ""), results.get(obs_mod))] | |
| if extra: | |
| cards.append((extra, "condition", results.get(f"input_{extra}") or steer_text)) | |
| # targets displayed in the fixed chain order, not click order | |
| display_targets = [m for m in chain if m != obs_mod and m != extra] | |
| cards += [(t, "", results.get(t)) for t in display_targets] | |
| final = { | |
| FP_IDX_STATUS: gr.update(value=_status_html("done")), | |
| FP_IDX_EMPTY: gr.update(value=_empty_state_html(), visible=False), | |
| FP_IDX_BTN: gr.update(value="Predict again", interactive=True), | |
| } | |
| for i in range(FP_MAX_SLOTS): | |
| if i < len(cards): | |
| key, role, payload = cards[i] | |
| value, is_text = _display_value(key, payload) | |
| final[_fp_slot(i, 0)] = gr.update(visible=True) | |
| final[_fp_slot(i, 1)] = gr.update(value=_card_head_html(key, role)) | |
| final[_fp_slot(i, 2)] = gr.update(value=None if is_text else value, visible=not is_text) | |
| final[_fp_slot(i, 3)] = gr.update(value=value if is_text else None, visible=is_text) | |
| yield _fp_frame(final) | |
| future_run_btn.click( | |
| run_future_demo, | |
| inputs=[future_example_state, fp_obs_radio, fp_ctx_radio, cond_mode_radio, cond_mod_radio, | |
| future_extra_preview_text, fp_targets_state, future_seed_number, | |
| fp_top_p, fp_top_k, *future_hyperparam_inputs], | |
| outputs=FP_OUTPUTS, | |
| show_progress="hidden", | |
| ) | |
| gr.HTML( | |
| '<footer class="app-footer" style="color:var(--text-3);font-size:13px;' | |
| 'border-top:1px solid var(--line);padding:14px 4px;margin-top:18px">' | |
| "A2A-Video · predictions are 128×128 @ 4 fps · feature maps visualized via PCA-to-RGB.</footer>" | |
| ) | |
| if __name__ == "__main__": | |
| # share=True's gradio.live tunnel times out held-open connections after a | |
| # couple of minutes -- any-to-any generation across ~9 modalities easily | |
| # takes that long, so the result silently never reaches the browser even | |
| # though the backend finishes fine. Use an SSH tunnel to localhost:7860 | |
| # instead (see README.md), which doesn't have that proxy-level timeout. | |
| # allowed_paths: example clips are served straight from hf_hub_download's | |
| # cache (not copied into cwd/tmp first), which is outside gradio's | |
| # default allowed roots -- without this it refuses to serve them with | |
| # InvalidPathError. | |
| from huggingface_hub import constants as _hf_constants | |
| demo.queue().launch(share=False, allowed_paths=[_hf_constants.HF_HUB_CACHE]) | |