Spaces:
Sleeping
Sleeping
Upload streamlit_app.py
Browse files- streamlit_app.py +71 -46
streamlit_app.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
-
# streamlit_app.py — Motion Path Planner (Frontend
|
| 2 |
-
#
|
| 3 |
-
# /detect, /plan, /plan_report, /analyze_xyz
|
| 4 |
|
| 5 |
import os, io, uuid, base64, requests
|
| 6 |
-
from typing import List, Tuple, Optional
|
| 7 |
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
| 8 |
import streamlit as st
|
| 9 |
|
|
@@ -34,6 +34,7 @@ _defaults = {
|
|
| 34 |
"primary_labels": ["plate", "food"],
|
| 35 |
"extra_labels": "",
|
| 36 |
"box_pick": "Highest score", # "Highest score", "Largest area", "Center-most"
|
|
|
|
| 37 |
}
|
| 38 |
for k, v in _defaults.items():
|
| 39 |
st.session_state.setdefault(k, v)
|
|
@@ -80,13 +81,11 @@ def _center_crop_aspect(img: Image.Image, target_ratio: float) -> Image.Image:
|
|
| 80 |
y0 = (H - new_h) // 2
|
| 81 |
return img.crop((0, y0, W, y0 + new_h))
|
| 82 |
|
| 83 |
-
def _preprocess_image(file, target_w=1280, target_h=720,
|
| 84 |
-
aspect_mode="16:9 crop") -> Image.Image:
|
| 85 |
img = Image.open(file).convert("RGB")
|
| 86 |
if aspect_mode in _ASPECTS:
|
| 87 |
img = _center_crop_aspect(img, _ASPECTS[aspect_mode])
|
| 88 |
-
#
|
| 89 |
-
img = img.resize((target_w, target_h), Image.Resampling.LANCZOS)
|
| 90 |
return img
|
| 91 |
|
| 92 |
def _b64_from_pil(img: Image.Image, jpeg_quality=90) -> str:
|
|
@@ -94,8 +93,7 @@ def _b64_from_pil(img: Image.Image, jpeg_quality=90) -> str:
|
|
| 94 |
img.save(buf, format="JPEG", quality=int(jpeg_quality))
|
| 95 |
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
| 96 |
|
| 97 |
-
def _b64_from_file(file, target_w=1280, target_h=720, jpeg_quality=90,
|
| 98 |
-
aspect_mode="16:9 crop") -> Tuple[str, Image.Image]:
|
| 99 |
img = _preprocess_image(file, target_w, target_h, aspect_mode)
|
| 100 |
return _b64_from_pil(img, jpeg_quality), img
|
| 101 |
|
|
@@ -170,7 +168,7 @@ def _detect_boxes_backend(image_b64: str, labels: List[str]) -> Optional[dict]:
|
|
| 170 |
# ================= Sidebar (backend + constraints) =================
|
| 171 |
with st.sidebar:
|
| 172 |
st.title("Backend")
|
| 173 |
-
st.caption("
|
| 174 |
url_in = st.text_input("Backend URL", value=st.session_state["API_BASE"],
|
| 175 |
placeholder="https://<org>-backend.hf.space")
|
| 176 |
colb1, colb2 = st.columns([1,1])
|
|
@@ -179,16 +177,19 @@ with st.sidebar:
|
|
| 179 |
_success(f"Using {st.session_state['API_BASE']}")
|
| 180 |
st.rerun()
|
| 181 |
if colb2.button("Health"):
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
st.markdown("---")
|
| 190 |
st.subheader("Rig Limits (export.txt)")
|
| 191 |
-
st.caption("
|
| 192 |
export_file = st.file_uploader("Upload export.txt", type=["txt"], key="exp_txt")
|
| 193 |
cola, colb = st.columns(2)
|
| 194 |
with cola:
|
|
@@ -199,11 +200,7 @@ with st.sidebar:
|
|
| 199 |
r = _post_file("/constraints/upload", "file", export_file.read(), export_file.name)
|
| 200 |
if r.status_code == 200:
|
| 201 |
_success("Constraints loaded.")
|
| 202 |
-
|
| 203 |
-
st.json(data.get("parsed", {}), expanded=False)
|
| 204 |
-
if "parser_log" in data:
|
| 205 |
-
with st.expander("Parser log (constraints)"):
|
| 206 |
-
st.json(data["parser_log"], expanded=False)
|
| 207 |
else:
|
| 208 |
_err(f"{r.status_code}: {r.text[:300]}")
|
| 209 |
with colb:
|
|
@@ -215,16 +212,16 @@ with st.sidebar:
|
|
| 215 |
_err(f"{r.status_code}: {r.text[:300]}")
|
| 216 |
|
| 217 |
# ================= Main =================
|
| 218 |
-
st.title("Motion Path Planner for Flair")
|
| 219 |
-
st.caption("
|
| 220 |
|
| 221 |
with st.expander("How it works"):
|
| 222 |
st.markdown(
|
| 223 |
"""
|
| 224 |
-
1)
|
| 225 |
-
2)
|
| 226 |
-
3) Generate
|
| 227 |
-
|
| 228 |
"""
|
| 229 |
)
|
| 230 |
|
|
@@ -254,16 +251,16 @@ with col_img:
|
|
| 254 |
img_file = st.file_uploader("Camera image (JPG/PNG)", type=["jpg","jpeg","png"], key="scene")
|
| 255 |
with col_text:
|
| 256 |
presets = [
|
| 257 |
-
"Orbit 90° in 4 seconds, jib up 0.6 m, radius 0.5 m, keep subject centered.",
|
| 258 |
-
"Quick push-in 0.2 m over 3 seconds, then 40° orbit right, keep subject in frame.",
|
| 259 |
-
"Fast start, soft finish, orbit -60° in 3.5 seconds, jib down 0.2 m."
|
| 260 |
]
|
| 261 |
preset_pick = st.selectbox("Presets (optional)", options=["(none)"] + presets, index=0)
|
| 262 |
instr = st.text_area(
|
| 263 |
"Describe the move",
|
| 264 |
value=(preset_pick if preset_pick != "(none)" else ""),
|
| 265 |
height=110,
|
| 266 |
-
placeholder="Example: Orbit 90° in 4s, jib up 0.6 m, radius 0.45 m,
|
| 267 |
)
|
| 268 |
|
| 269 |
st.markdown("**Subject labels**")
|
|
@@ -284,6 +281,7 @@ with col_text:
|
|
| 284 |
["Highest score", "Largest area", "Center-most"],
|
| 285 |
index=["Highest score", "Largest area", "Center-most"].index(st.session_state["box_pick"])
|
| 286 |
)
|
|
|
|
| 287 |
|
| 288 |
def _gather_labels() -> List[str]:
|
| 289 |
labels = list(st.session_state.get("primary_labels") or [])
|
|
@@ -302,7 +300,6 @@ if img_file:
|
|
| 302 |
with det_card:
|
| 303 |
st.subheader("Preview")
|
| 304 |
st.caption("Subject placement and detection overlay.")
|
| 305 |
-
|
| 306 |
boxes_img = None
|
| 307 |
try:
|
| 308 |
labels = _gather_labels() or ["subject"]
|
|
@@ -317,8 +314,7 @@ if img_file:
|
|
| 317 |
except Exception as e:
|
| 318 |
_warn(f"Detection preview issue: {e}")
|
| 319 |
|
| 320 |
-
|
| 321 |
-
max_w = 900
|
| 322 |
if boxes_img is not None:
|
| 323 |
st.image(boxes_img, caption="Detection overlay", width=max_w)
|
| 324 |
else:
|
|
@@ -346,7 +342,12 @@ def _payload_prebuilt():
|
|
| 346 |
aspect_mode=st.session_state["aspect_mode"],
|
| 347 |
)
|
| 348 |
labels = _gather_labels() or ["subject"]
|
| 349 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
# Generate-only
|
| 352 |
if btn_generate and _require_inputs():
|
|
@@ -368,9 +369,6 @@ if btn_generate and _require_inputs():
|
|
| 368 |
data = r.json()
|
| 369 |
st.error("Violated constraints:"); st.write(data.get("violated", []))
|
| 370 |
st.info("Suggestions:"); st.write(data.get("suggestions", []))
|
| 371 |
-
if "parser_log" in data:
|
| 372 |
-
with st.expander("Parser log (instruction)"):
|
| 373 |
-
st.json(data["parser_log"], expanded=False)
|
| 374 |
except Exception:
|
| 375 |
_err(r.text[:600])
|
| 376 |
else:
|
|
@@ -383,6 +381,8 @@ if btn_report and _require_inputs():
|
|
| 383 |
if r.status_code == 200:
|
| 384 |
data = r.json()
|
| 385 |
_success("Report ready.")
|
|
|
|
|
|
|
| 386 |
m1, m2, m3 = st.columns(3)
|
| 387 |
with m1:
|
| 388 |
st.metric("Orbit requested (deg)", f"{data.get('orbit_requested', 0):.1f}")
|
|
@@ -394,14 +394,38 @@ if btn_report and _require_inputs():
|
|
| 394 |
st.metric("Duration (s)", f"{data.get('duration_s', 0):.2f}")
|
| 395 |
st.metric("FPS", f"{data.get('fps', 0)}")
|
| 396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
ok = bool(data.get("constraints_passed", False))
|
| 398 |
st.markdown(f"**Constraints:** {'✅ Passed' if ok else '❌ Failed'}")
|
| 399 |
if not ok:
|
| 400 |
st.error("Violated constraints:"); st.write(data.get("violated", []))
|
| 401 |
st.info("Suggestions:"); st.write(data.get("suggestions", []))
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
st.caption("Use “Generate Path (.xyz)” to download the file.")
|
| 406 |
else:
|
| 407 |
_err(f"{r.status_code}: {r.text[:600]}")
|
|
@@ -409,7 +433,7 @@ if btn_report and _require_inputs():
|
|
| 409 |
# ---------- Analyze existing .xyz ----------
|
| 410 |
st.markdown("---")
|
| 411 |
st.subheader("Analyze a .xyz file")
|
| 412 |
-
st.caption("
|
| 413 |
xyz_up = st.file_uploader("Upload .xyz", type=["xyz"], key="xyz_file")
|
| 414 |
if xyz_up and st.button("Analyze"):
|
| 415 |
with st.spinner("Analyzing…"):
|
|
@@ -425,5 +449,6 @@ st.markdown("---")
|
|
| 425 |
st.caption(
|
| 426 |
"Set env vars in this Space:\n"
|
| 427 |
"• MotionPath_AI_API = https://<your-private-backend>.hf.space\n"
|
| 428 |
-
"• MotionPath_AI_TOKEN = <same token as backend>"
|
| 429 |
-
)
|
|
|
|
|
|
| 1 |
+
# streamlit_app.py — AI Motion Path Planner (Frontend)
|
| 2 |
+
# Konuştuğu backend FastAPI uçları:
|
| 3 |
+
# /health, /constraints/upload, /constraints/reset, /detect, /plan, /plan_report, /analyze_xyz
|
| 4 |
|
| 5 |
import os, io, uuid, base64, requests
|
| 6 |
+
from typing import List, Tuple, Optional, Dict, Any
|
| 7 |
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
| 8 |
import streamlit as st
|
| 9 |
|
|
|
|
| 34 |
"primary_labels": ["plate", "food"],
|
| 35 |
"extra_labels": "",
|
| 36 |
"box_pick": "Highest score", # "Highest score", "Largest area", "Center-most"
|
| 37 |
+
"want_notes": True,
|
| 38 |
}
|
| 39 |
for k, v in _defaults.items():
|
| 40 |
st.session_state.setdefault(k, v)
|
|
|
|
| 81 |
y0 = (H - new_h) // 2
|
| 82 |
return img.crop((0, y0, W, y0 + new_h))
|
| 83 |
|
| 84 |
+
def _preprocess_image(file, target_w=1280, target_h=720, aspect_mode="16:9 crop") -> Image.Image:
|
|
|
|
| 85 |
img = Image.open(file).convert("RGB")
|
| 86 |
if aspect_mode in _ASPECTS:
|
| 87 |
img = _center_crop_aspect(img, _ASPECTS[aspect_mode])
|
| 88 |
+
img = img.resize((target_w, target_h), Image.Resampling.LANCZOS) # esnetme yok: önce crop, sonra resize
|
|
|
|
| 89 |
return img
|
| 90 |
|
| 91 |
def _b64_from_pil(img: Image.Image, jpeg_quality=90) -> str:
|
|
|
|
| 93 |
img.save(buf, format="JPEG", quality=int(jpeg_quality))
|
| 94 |
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
| 95 |
|
| 96 |
+
def _b64_from_file(file, target_w=1280, target_h=720, jpeg_quality=90, aspect_mode="16:9 crop") -> Tuple[str, Image.Image]:
|
|
|
|
| 97 |
img = _preprocess_image(file, target_w, target_h, aspect_mode)
|
| 98 |
return _b64_from_pil(img, jpeg_quality), img
|
| 99 |
|
|
|
|
| 168 |
# ================= Sidebar (backend + constraints) =================
|
| 169 |
with st.sidebar:
|
| 170 |
st.title("Backend")
|
| 171 |
+
st.caption("Private backend URL’inizi girin.")
|
| 172 |
url_in = st.text_input("Backend URL", value=st.session_state["API_BASE"],
|
| 173 |
placeholder="https://<org>-backend.hf.space")
|
| 174 |
colb1, colb2 = st.columns([1,1])
|
|
|
|
| 177 |
_success(f"Using {st.session_state['API_BASE']}")
|
| 178 |
st.rerun()
|
| 179 |
if colb2.button("Health"):
|
| 180 |
+
try:
|
| 181 |
+
r = _get("/health")
|
| 182 |
+
if r.status_code == 200:
|
| 183 |
+
_success("Backend reachable.")
|
| 184 |
+
st.json(r.json(), expanded=False)
|
| 185 |
+
else:
|
| 186 |
+
_err(f"{r.status_code}: {r.text[:300]}")
|
| 187 |
+
except Exception as e:
|
| 188 |
+
_err(str(e))
|
| 189 |
|
| 190 |
st.markdown("---")
|
| 191 |
st.subheader("Rig Limits (export.txt)")
|
| 192 |
+
st.caption("Flair export.txt yükleyin; hız/ivme/jerk ve açı limitleri uygulanır.")
|
| 193 |
export_file = st.file_uploader("Upload export.txt", type=["txt"], key="exp_txt")
|
| 194 |
cola, colb = st.columns(2)
|
| 195 |
with cola:
|
|
|
|
| 200 |
r = _post_file("/constraints/upload", "file", export_file.read(), export_file.name)
|
| 201 |
if r.status_code == 200:
|
| 202 |
_success("Constraints loaded.")
|
| 203 |
+
st.json(r.json(), expanded=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
else:
|
| 205 |
_err(f"{r.status_code}: {r.text[:300]}")
|
| 206 |
with colb:
|
|
|
|
| 212 |
_err(f"{r.status_code}: {r.text[:300]}")
|
| 213 |
|
| 214 |
# ================= Main =================
|
| 215 |
+
st.title("AI Motion Path Planner for Flair")
|
| 216 |
+
st.caption("İngilizce komutla kamera hareketini tarif edin; AI planı .xyz olarak üretir, rig limitlerine göre kontrol eder.")
|
| 217 |
|
| 218 |
with st.expander("How it works"):
|
| 219 |
st.markdown(
|
| 220 |
"""
|
| 221 |
+
1) Bir kamera görseli yükleyin.
|
| 222 |
+
2) Hareketi tarif edin (örn: “Orbit 90° in 4s, jib up 0.6 m, radius 0.5 m, 25 fps”).
|
| 223 |
+
3) **Generate + Report** ile AI yorumlarını, kinematik tepe değerleri ve kısıt kontrollerini görün.
|
| 224 |
+
4) **Generate Path** ile `.xyz`’yi indirin.
|
| 225 |
"""
|
| 226 |
)
|
| 227 |
|
|
|
|
| 251 |
img_file = st.file_uploader("Camera image (JPG/PNG)", type=["jpg","jpeg","png"], key="scene")
|
| 252 |
with col_text:
|
| 253 |
presets = [
|
| 254 |
+
"Orbit 90° in 4 seconds, jib up 0.6 m, radius 0.5 m, keep subject centered, 25 fps.",
|
| 255 |
+
"Quick push-in 0.2 m over 3 seconds, then 40° orbit right, keep subject in frame, 25 fps.",
|
| 256 |
+
"Fast start, soft finish, orbit -60° in 3.5 seconds, jib down 0.2 m, 25 fps."
|
| 257 |
]
|
| 258 |
preset_pick = st.selectbox("Presets (optional)", options=["(none)"] + presets, index=0)
|
| 259 |
instr = st.text_area(
|
| 260 |
"Describe the move",
|
| 261 |
value=(preset_pick if preset_pick != "(none)" else ""),
|
| 262 |
height=110,
|
| 263 |
+
placeholder="Example: Orbit 90° in 4s, jib up 0.6 m, radius 0.45 m, 25 fps."
|
| 264 |
)
|
| 265 |
|
| 266 |
st.markdown("**Subject labels**")
|
|
|
|
| 281 |
["Highest score", "Largest area", "Center-most"],
|
| 282 |
index=["Highest score", "Largest area", "Center-most"].index(st.session_state["box_pick"])
|
| 283 |
)
|
| 284 |
+
st.session_state["want_notes"] = st.checkbox("Show AI Notes in report", value=bool(st.session_state["want_notes"]))
|
| 285 |
|
| 286 |
def _gather_labels() -> List[str]:
|
| 287 |
labels = list(st.session_state.get("primary_labels") or [])
|
|
|
|
| 300 |
with det_card:
|
| 301 |
st.subheader("Preview")
|
| 302 |
st.caption("Subject placement and detection overlay.")
|
|
|
|
| 303 |
boxes_img = None
|
| 304 |
try:
|
| 305 |
labels = _gather_labels() or ["subject"]
|
|
|
|
| 314 |
except Exception as e:
|
| 315 |
_warn(f"Detection preview issue: {e}")
|
| 316 |
|
| 317 |
+
max_w = 900 # Sayfayı taşırmadan büyük göster
|
|
|
|
| 318 |
if boxes_img is not None:
|
| 319 |
st.image(boxes_img, caption="Detection overlay", width=max_w)
|
| 320 |
else:
|
|
|
|
| 342 |
aspect_mode=st.session_state["aspect_mode"],
|
| 343 |
)
|
| 344 |
labels = _gather_labels() or ["subject"]
|
| 345 |
+
return {
|
| 346 |
+
"instruction": instr.strip(),
|
| 347 |
+
"image_b64": img_b64,
|
| 348 |
+
"detect_query": ", ".join(labels),
|
| 349 |
+
"want_notes": bool(st.session_state["want_notes"]),
|
| 350 |
+
}
|
| 351 |
|
| 352 |
# Generate-only
|
| 353 |
if btn_generate and _require_inputs():
|
|
|
|
| 369 |
data = r.json()
|
| 370 |
st.error("Violated constraints:"); st.write(data.get("violated", []))
|
| 371 |
st.info("Suggestions:"); st.write(data.get("suggestions", []))
|
|
|
|
|
|
|
|
|
|
| 372 |
except Exception:
|
| 373 |
_err(r.text[:600])
|
| 374 |
else:
|
|
|
|
| 381 |
if r.status_code == 200:
|
| 382 |
data = r.json()
|
| 383 |
_success("Report ready.")
|
| 384 |
+
|
| 385 |
+
# Metrics
|
| 386 |
m1, m2, m3 = st.columns(3)
|
| 387 |
with m1:
|
| 388 |
st.metric("Orbit requested (deg)", f"{data.get('orbit_requested', 0):.1f}")
|
|
|
|
| 394 |
st.metric("Duration (s)", f"{data.get('duration_s', 0):.2f}")
|
| 395 |
st.metric("FPS", f"{data.get('fps', 0)}")
|
| 396 |
|
| 397 |
+
# Kinematics
|
| 398 |
+
kin = data.get("kinematics", {}) or {}
|
| 399 |
+
st.subheader("Kinematics (Pan)")
|
| 400 |
+
colk1, colk2, colk3, colk4 = st.columns(4)
|
| 401 |
+
colk1.metric("Max Speed (°/s)", f"{kin.get('pan_max_dps', 0.0):.2f}")
|
| 402 |
+
colk2.metric("Max Acc (°/s²)", f"{kin.get('pan_max_dps2', 0.0):.2f}")
|
| 403 |
+
colk3.metric("Max Jerk (°/s³)", f"{kin.get('pan_max_dps3', 0.0):.2f}")
|
| 404 |
+
colk4.metric("Avg Speed (°/s)", f"{kin.get('pan_avg_dps', 0.0):.2f}")
|
| 405 |
+
|
| 406 |
+
# Constraints result
|
| 407 |
ok = bool(data.get("constraints_passed", False))
|
| 408 |
st.markdown(f"**Constraints:** {'✅ Passed' if ok else '❌ Failed'}")
|
| 409 |
if not ok:
|
| 410 |
st.error("Violated constraints:"); st.write(data.get("violated", []))
|
| 411 |
st.info("Suggestions:"); st.write(data.get("suggestions", []))
|
| 412 |
+
|
| 413 |
+
# Detection summary
|
| 414 |
+
with st.expander("Detection summary"):
|
| 415 |
+
st.json(data.get("detection_summary", {}), expanded=False)
|
| 416 |
+
|
| 417 |
+
# AI Notes (UI’da görünür)
|
| 418 |
+
ai_notes = data.get("ai_notes", []) or []
|
| 419 |
+
if ai_notes:
|
| 420 |
+
st.subheader("AI Notes")
|
| 421 |
+
for n in ai_notes:
|
| 422 |
+
st.write(f"• {n}")
|
| 423 |
+
else:
|
| 424 |
+
st.caption("AI Notes unavailable (no key or parsing fallback).")
|
| 425 |
+
|
| 426 |
+
# Param kaynağı
|
| 427 |
+
st.caption(f"Param source: {data.get('param_source', 'unknown')}")
|
| 428 |
+
|
| 429 |
st.caption("Use “Generate Path (.xyz)” to download the file.")
|
| 430 |
else:
|
| 431 |
_err(f"{r.status_code}: {r.text[:600]}")
|
|
|
|
| 433 |
# ---------- Analyze existing .xyz ----------
|
| 434 |
st.markdown("---")
|
| 435 |
st.subheader("Analyze a .xyz file")
|
| 436 |
+
st.caption("Flair `.xyz` yükleyin; süre, FPS, orbit, jib ve kinematik pikleri görün.")
|
| 437 |
xyz_up = st.file_uploader("Upload .xyz", type=["xyz"], key="xyz_file")
|
| 438 |
if xyz_up and st.button("Analyze"):
|
| 439 |
with st.spinner("Analyzing…"):
|
|
|
|
| 449 |
st.caption(
|
| 450 |
"Set env vars in this Space:\n"
|
| 451 |
"• MotionPath_AI_API = https://<your-private-backend>.hf.space\n"
|
| 452 |
+
"• MotionPath_AI_TOKEN = <same token as backend>\n"
|
| 453 |
+
"This app shows concise **AI Notes** (not chain-of-thought)."
|
| 454 |
+
)
|