Spaces:
Running
Running
# 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( | |
""" | |
<div class="header-box"> | |
<div class="kicker">Analyzer</div> | |
<h1 class="title">Running Form Analyzer</h1> | |
<p class="subtle">Upload a short video to get stride length, symmetry, posture flags, and a simple coach report.</p> | |
</div> | |
""" | |
) | |
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('<span class="sublabel">Tip: 10–30s clip · MP4 works best · Keep the runner fully in frame.</span>') | |
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( | |
""" | |
<div class="note">Privacy: Your video is processed to compute metrics and generate your report. Files aren’t shared or indexed.</div> | |
""" | |
) | |
# ---------- 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('<div class="footer-space"></div>') | |
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() | |