Spaces:
Paused
Paused
MacBook pro
commited on
Commit
·
32226d2
1
Parent(s):
79dbfa1
docs: metrics endpoints & smoothing env vars; feat: OneEuro env initialization
Browse files- README.md +55 -0
- avatar_pipeline.py +16 -10
- liveportrait_engine.py +23 -18
README.md
CHANGED
|
@@ -150,6 +150,11 @@ MIT License - Feel free to use and modify for your projects!
|
|
| 150 |
## Metrics Endpoints
|
| 151 |
- `GET /metrics` – JSON with audio/video counters, EMAs (loop interval, inference), rolling FPS, frame interval EMA.
|
| 152 |
- `GET /gpu` – GPU availability & memory (torch or `nvidia-smi` fallback).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
Example:
|
| 155 |
```bash
|
|
@@ -198,6 +203,56 @@ If the Space shows a perpetual "Restarting" badge:
|
|
| 198 |
If problems persist, capture the Container log stack trace and open an issue.
|
| 199 |
|
| 200 |
## Enable ONNX Model Downloads (Safe LivePortrait)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
To pull LivePortrait ONNX files into the container at runtime and enable the safe animation path:
|
| 203 |
|
|
|
|
| 150 |
## Metrics Endpoints
|
| 151 |
- `GET /metrics` – JSON with audio/video counters, EMAs (loop interval, inference), rolling FPS, frame interval EMA.
|
| 152 |
- `GET /gpu` – GPU availability & memory (torch or `nvidia-smi` fallback).
|
| 153 |
+
- `GET /metrics/async` – Async worker stats (frames submitted/processed, queue depth, last latency ms).
|
| 154 |
+
- `GET /metrics/stage_histogram` – Histogram buckets of recent inference stage latencies (snapshot window).
|
| 155 |
+
- `GET /metrics/motion` – Recent motion magnitudes (normalized) plus tail statistics.
|
| 156 |
+
- `GET /metrics/pacing` – Latency EMA and pacing hint multiplier ( >1.0 suggests you can raise FPS, <1.0 suggests throttling ).
|
| 157 |
+
- `POST /smoothing/update` – Runtime update of One Euro keypoint smoothing params. JSON body keys: `min_cutoff`, `beta`, `d_cutoff` (all optional floats).
|
| 158 |
|
| 159 |
Example:
|
| 160 |
```bash
|
|
|
|
| 203 |
If problems persist, capture the Container log stack trace and open an issue.
|
| 204 |
|
| 205 |
## Enable ONNX Model Downloads (Safe LivePortrait)
|
| 206 |
+
## Advanced Real-time Metrics & Control
|
| 207 |
+
|
| 208 |
+
New runtime observability & control surfaces were added to tune real-time performance:
|
| 209 |
+
|
| 210 |
+
### Endpoints Recap
|
| 211 |
+
See Metrics Endpoints section above. Typical usage examples:
|
| 212 |
+
|
| 213 |
+
```bash
|
| 214 |
+
curl -s http://localhost:7860/metrics/async | jq
|
| 215 |
+
curl -s http://localhost:7860/metrics/pacing | jq '.latency_ema_ms, .pacing_hint'
|
| 216 |
+
curl -s http://localhost:7860/metrics/motion | jq '.recent_motion[-5:]'
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
### Pacing Hint Logic
|
| 220 |
+
`pacing_hint` is derived from a latency exponential moving average vs target frame time:
|
| 221 |
+
- ~1.0: Balanced.
|
| 222 |
+
- <0.85: System overloaded – consider lowering capture FPS or resolution.
|
| 223 |
+
- >1.15: Headroom available – you may increase FPS modestly.
|
| 224 |
+
|
| 225 |
+
### Motion Magnitude
|
| 226 |
+
Aggregated from per-frame keypoint motion vectors; higher values trigger more frequent face detection to avoid drift. Low motion stretches automatically reduce detection frequency to save compute.
|
| 227 |
+
|
| 228 |
+
### One Euro Smoothing Parameters
|
| 229 |
+
You can initialize or override smoothing parameters via environment variables:
|
| 230 |
+
|
| 231 |
+
| Variable | Default | Meaning |
|
| 232 |
+
|----------|---------|---------|
|
| 233 |
+
| `MIRAGE_ONEEURO_MIN_CUTOFF` | 1.0 | Base cutoff frequency controlling overall smoothing strength |
|
| 234 |
+
| `MIRAGE_ONEEURO_BETA` | 0.05 | Speed coefficient (higher reduces lag during fast motion) |
|
| 235 |
+
| `MIRAGE_ONEEURO_D_CUTOFF` | 1.0 | Derivative cutoff for velocity filtering |
|
| 236 |
+
|
| 237 |
+
Runtime adjustments:
|
| 238 |
+
```bash
|
| 239 |
+
curl -X POST http://localhost:7860/smoothing/update \
|
| 240 |
+
-H 'Content-Type: application/json' \
|
| 241 |
+
-d '{"min_cutoff":0.8, "beta":0.07}'
|
| 242 |
+
```
|
| 243 |
+
Missing keys leave existing values unchanged. The response echoes the active parameters.
|
| 244 |
+
|
| 245 |
+
### Latency Histogram Snapshots
|
| 246 |
+
`/metrics/stage_histogram` exposes periodic snapshots (e.g. every N frames) of stage latency distribution to help identify tail regressions. Use to tune pacing thresholds or decide on model quantization.
|
| 247 |
+
|
| 248 |
+
## Environment Variables Summary (New Additions)
|
| 249 |
+
|
| 250 |
+
| Name | Purpose | Default |
|
| 251 |
+
|------|---------|---------|
|
| 252 |
+
| `MIRAGE_ONEEURO_MIN_CUTOFF` | One Euro base cutoff | 1.0 |
|
| 253 |
+
| `MIRAGE_ONEEURO_BETA` | One Euro speed coefficient | 0.05 |
|
| 254 |
+
| `MIRAGE_ONEEURO_D_CUTOFF` | One Euro derivative cutoff | 1.0 |
|
| 255 |
+
|
| 256 |
|
| 257 |
To pull LivePortrait ONNX files into the container at runtime and enable the safe animation path:
|
| 258 |
|
avatar_pipeline.py
CHANGED
|
@@ -378,21 +378,27 @@ class RealTimeAvatarPipeline:
|
|
| 378 |
self._frame_submit_count = 0
|
| 379 |
self._frame_process_count = 0
|
| 380 |
# Keypoint smoothing filter
|
| 381 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
self._prev_motion_raw = None
|
| 383 |
# Adaptive detection interval tracking
|
| 384 |
self._dynamic_detect_interval = self.config.detect_interval
|
| 385 |
self._recent_motion_magnitudes = deque(maxlen=30)
|
| 386 |
self._consecutive_detect_fail = 0
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
|
| 397 |
async def initialize(self):
|
| 398 |
"""Initialize all models"""
|
|
|
|
| 378 |
self._frame_submit_count = 0
|
| 379 |
self._frame_process_count = 0
|
| 380 |
# Keypoint smoothing filter
|
| 381 |
+
try:
|
| 382 |
+
min_cut = float(os.getenv('MIRAGE_ONEEURO_MIN_CUTOFF', '1.0'))
|
| 383 |
+
beta = float(os.getenv('MIRAGE_ONEEURO_BETA', '0.05'))
|
| 384 |
+
d_cut = float(os.getenv('MIRAGE_ONEEURO_D_CUTOFF', '1.0'))
|
| 385 |
+
except Exception:
|
| 386 |
+
min_cut, beta, d_cut = 1.0, 0.05, 1.0
|
| 387 |
+
self._kp_filter = KeypointOneEuro(K=21, C=3, min_cutoff=min_cut, beta=beta, d_cutoff=d_cut)
|
| 388 |
self._prev_motion_raw = None
|
| 389 |
# Adaptive detection interval tracking
|
| 390 |
self._dynamic_detect_interval = self.config.detect_interval
|
| 391 |
self._recent_motion_magnitudes = deque(maxlen=30)
|
| 392 |
self._consecutive_detect_fail = 0
|
| 393 |
+
# Extended motion history for metrics
|
| 394 |
+
self._motion_history = deque(maxlen=300)
|
| 395 |
+
# Latency histogram snapshots (long window)
|
| 396 |
+
self._latency_history = deque(maxlen=500)
|
| 397 |
+
self._latency_hist_snapshots = [] # list of {timestamp, buckets}
|
| 398 |
+
# Frame pacing
|
| 399 |
+
self._pacing_hint = 1.0 # multiplier suggestion (1.0 = normal)
|
| 400 |
+
self._target_frame_time = 1.0 / max(self.config.target_fps, 1)
|
| 401 |
+
self._latency_ema = None
|
| 402 |
|
| 403 |
async def initialize(self):
|
| 404 |
"""Initialize all models"""
|
liveportrait_engine.py
CHANGED
|
@@ -63,7 +63,8 @@ class LivePortraitONNX:
|
|
| 63 |
|
| 64 |
# Performance tracking
|
| 65 |
self.inference_times = []
|
| 66 |
-
|
|
|
|
| 67 |
|
| 68 |
def _get_onnx_providers(self) -> List[str]:
|
| 69 |
"""Get optimal ONNX execution providers. Enforce GPU if required."""
|
|
@@ -378,10 +379,30 @@ class LivePortraitONNX:
|
|
| 378 |
return None
|
| 379 |
|
| 380 |
def extract_motion_parameters(self, driving_image: np.ndarray) -> Optional[np.ndarray]:
|
| 381 |
-
"""Extract motion parameters from driving image
|
|
|
|
|
|
|
| 382 |
if self.motion_session is None:
|
| 383 |
logger.error("Motion model not loaded")
|
| 384 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
def warmup(self) -> bool:
|
| 387 |
"""Run a minimal generator inference with zero motion to prime kernels and caches.
|
|
@@ -414,22 +435,6 @@ class LivePortraitONNX:
|
|
| 414 |
except Exception as e:
|
| 415 |
logger.debug(f"Warmup skipped: {e}")
|
| 416 |
return False
|
| 417 |
-
|
| 418 |
-
try:
|
| 419 |
-
motion = self._run_motion_for_image(driving_image)
|
| 420 |
-
# Prefer explicit kp_driving from motion model if available
|
| 421 |
-
if isinstance(motion, dict):
|
| 422 |
-
kp_drive = motion.get('kp_driving') or motion.get('driving')
|
| 423 |
-
if kp_drive is None:
|
| 424 |
-
# Fallback to first array value
|
| 425 |
-
kp_drive = next((v for v in motion.values() if isinstance(v, np.ndarray)), None)
|
| 426 |
-
return kp_drive
|
| 427 |
-
else:
|
| 428 |
-
return motion
|
| 429 |
-
|
| 430 |
-
except Exception as e:
|
| 431 |
-
logger.error(f"Motion parameter extraction failed: {e}")
|
| 432 |
-
return None
|
| 433 |
|
| 434 |
def _run_motion_for_image(self, img: np.ndarray):
|
| 435 |
"""Helper: run motion/keypoint extractor on an image and return structured outputs."""
|
|
|
|
| 63 |
|
| 64 |
# Performance tracking
|
| 65 |
self.inference_times = []
|
| 66 |
+
# Warmup sentinel
|
| 67 |
+
self._did_warmup = False
|
| 68 |
|
| 69 |
def _get_onnx_providers(self) -> List[str]:
|
| 70 |
"""Get optimal ONNX execution providers. Enforce GPU if required."""
|
|
|
|
| 379 |
return None
|
| 380 |
|
| 381 |
def extract_motion_parameters(self, driving_image: np.ndarray) -> Optional[np.ndarray]:
|
| 382 |
+
"""Extract motion parameters from driving image.
|
| 383 |
+
Returns keypoints/pose tensor shaped [1,K,3] if possible.
|
| 384 |
+
"""
|
| 385 |
if self.motion_session is None:
|
| 386 |
logger.error("Motion model not loaded")
|
| 387 |
return None
|
| 388 |
+
try:
|
| 389 |
+
motion = self._run_motion_for_image(driving_image)
|
| 390 |
+
# Prefer explicit kp_driving from motion model if available
|
| 391 |
+
if isinstance(motion, dict):
|
| 392 |
+
kp_drive = (motion.get('kp_driving') or motion.get('driving') or
|
| 393 |
+
motion.get('kp_drive') or motion.get('drive'))
|
| 394 |
+
if kp_drive is None:
|
| 395 |
+
# Fallback to first ndarray value
|
| 396 |
+
kp_drive = next((v for v in motion.values() if isinstance(v, np.ndarray)), None)
|
| 397 |
+
if kp_drive is None:
|
| 398 |
+
logger.error("Motion model outputs did not include a usable keypoint array")
|
| 399 |
+
return None
|
| 400 |
+
return kp_drive
|
| 401 |
+
else:
|
| 402 |
+
return motion
|
| 403 |
+
except Exception as e:
|
| 404 |
+
logger.error(f"Motion parameter extraction failed: {e}")
|
| 405 |
+
return None
|
| 406 |
|
| 407 |
def warmup(self) -> bool:
|
| 408 |
"""Run a minimal generator inference with zero motion to prime kernels and caches.
|
|
|
|
| 435 |
except Exception as e:
|
| 436 |
logger.debug(f"Warmup skipped: {e}")
|
| 437 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
def _run_motion_for_image(self, img: np.ndarray):
|
| 440 |
"""Helper: run motion/keypoint extractor on an image and return structured outputs."""
|