# app.py import os import pathlib import gradio as gr from analyzer import process_video, OUTPUT_DIR, make_zip # ---- Brand knobs (match to your site) ---- ACCENT = "#ff6b2c" # Change to your brand color if you like CARD_BG = "var(--panel-background-fill)" # respects HF light/dark RADIUS = "18px" SHADOW = "0 10px 30px rgba(0,0,0,0.06)" MAX_WIDTH = "1100px" # overall page width cap def run(video_path, units, height, weight, slow_factor): if not video_path: raise gr.Error("Please upload a video (mp4/mov/avi).") stem = pathlib.Path(video_path).stem out_prefix = os.path.join(OUTPUT_DIR, f"{stem}_simple") outs = process_video( video_path, out_prefix, units.startswith("Metric"), float(height), float(weight), float(slow_factor), ) zip_path = make_zip(outs, bundle_name=stem) or None return outs["normal"], outs["slow"], outs["report"], zip_path # Keep default theme for compatibility theme = gr.themes.Soft() # CSS (escaped braces for f-string) CUSTOM_CSS = f""" :root {{ --brand-accent: {ACCENT}; }} .gradio-container {{ max-width: {MAX_WIDTH}; margin: 0 auto; font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; }} /* Primary button color */ button.primary, button.svelte-1ipelgc, button.svelte-1ipgb8n {{ background: {ACCENT} !important; border: none !important; color: white !important; }} button.primary:hover {{ filter: brightness(0.95); }} .header-box {{ background: linear-gradient(180deg, rgba(0,0,0,0.02), transparent); border-radius: {RADIUS}; padding: 28px 28px 20px; box-shadow: {SHADOW}; border: 1px solid rgba(0,0,0,0.05); margin-bottom: 14px; }} .section-grid {{ display: grid; grid-template-columns: 1.1fr 1fr; gap: 20px; }} @media (max-width: 960px) {{ .section-grid {{ grid-template-columns: 1fr; }} }} .card {{ background: {CARD_BG}; border-radius: {RADIUS}; box-shadow: {SHADOW}; border: 1px solid rgba(0,0,0,0.06); padding: 16px; }} .card .gradio-row, .card .gradio-column {{ gap: 12px; }} .kicker {{ font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: rgba(0,0,0,0.55); margin-bottom: 6px; }} h1.title {{ font-size: 28px; line-height: 1.2; margin: 0 0 4px 0; }} .subtle {{ color: rgba(0,0,0,0.6); font-size: 14px; }} .note {{ background: rgba(0,0,0,0.035); border: 1px dashed rgba(0,0,0,0.08); border-radius: 12px; padding: 10px 12px; font-size: 12.5px; }} label.sublabel {{ font-size: 12px; color: rgba(0,0,0,0.60); margin-top: -8px; display: block; }} .footer-space {{ height: 8px; }} """ with gr.Blocks(title="Running Form Analyzer", theme=theme, css=CUSTOM_CSS) as demo: # Header (use HTML wrapper instead of gr.Box for compatibility) gr.HTML( """
Analyzer

Running Form Analyzer

Upload a short video to get stride length, symmetry, posture flags, and a simple coach report.

""" ) with gr.Row(elem_classes=["section-grid"]): # ---------- LEFT: INPUTS ---------- with gr.Column(elem_classes=["card"]): gr.Markdown("**Upload**") video = gr.Video( label="Drop video here or click to upload", sources=["upload"], interactive=True, height=340, ) gr.Markdown('Tip: 10–30s clip · MP4 works best · Keep the runner fully in frame.') gr.Markdown("**Settings**") units = gr.Radio( ["Imperial (in/lb)", "Metric (cm/kg)"], value="Imperial (in/lb)", label="Units", ) with gr.Row(): height = gr.Number(value=66.0, label="Height") weight = gr.Number(value=120.0, label="Weight") slow = gr.Slider(1.0, 6.0, value=2.0, step=0.5, label="Slow factor") run_btn = gr.Button("Run analysis") # keep generic; CSS colors it gr.Markdown( """
Privacy: Your video is processed to compute metrics and generate your report. Files aren’t shared or indexed.
""" ) # ---------- RIGHT: OUTPUTS ---------- with gr.Column(elem_classes=["card"]): gr.Markdown("**Results**") out_norm = gr.Video(label="Annotated (real-time)", height=220) out_slow = gr.Video(label="Slow motion", height=220) out_report = gr.File(label="HTML report") out_zip = gr.File(label="ZIP bundle (videos + report)") gr.Markdown('') run_btn.click( fn=run, inputs=[video, units, height, weight, slow], outputs=[out_norm, out_slow, out_report, out_zip], api_name="analyze", ) if __name__ == "__main__": demo.queue(max_size=2).launch()