tylersf's picture
Update app.py
d2982e0 verified
# 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()