ardy-motion-api / timeline_utils.py
cs686's picture
Deploy ARDY ZeroGPU Blender motion API
1b78d7e verified
Raw
History Blame Contribute Delete
2.81 kB
"""Validation helpers for ARDY's continuous action timeline."""
from __future__ import annotations
import math
from typing import Any
MIN_SEGMENT_SECONDS = 2.0
MAX_SEGMENT_SECONDS = 4.0
MAX_TOTAL_SECONDS = 8.0
MAX_SEGMENTS = 8
MAX_PROMPT_CHARS = 500
def _rows(value: Any) -> list:
if value is None:
return []
if hasattr(value, "values"):
return value.values.tolist()
if hasattr(value, "tolist") and not isinstance(value, list):
return value.tolist()
return list(value)
def normalize_timeline(value: Any, *, fps: float) -> list[dict]:
"""Normalize Gradio table rows and calculate exact frame ranges."""
if not math.isfinite(float(fps)) or float(fps) <= 0:
raise ValueError("FPS must be greater than zero.")
clean_rows = []
for row in _rows(value):
if row is None:
continue
row = list(row)
prompt = " ".join(str(row[0] if row else "").split())
duration_value = row[1] if len(row) > 1 else None
if not prompt and duration_value in (None, ""):
continue
if not prompt:
raise ValueError("Every timeline row must contain an action prompt.")
if len(prompt) > MAX_PROMPT_CHARS:
raise ValueError(
f"Each action prompt must contain at most {MAX_PROMPT_CHARS} characters."
)
try:
duration = float(duration_value)
except (TypeError, ValueError) as exc:
raise ValueError(
f"Duration for '{prompt}' must be a number."
) from exc
if not math.isfinite(duration) or not MIN_SEGMENT_SECONDS <= duration <= MAX_SEGMENT_SECONDS:
raise ValueError(
f"Each segment must be between {MIN_SEGMENT_SECONDS:g} "
f"and {MAX_SEGMENT_SECONDS:g} seconds."
)
clean_rows.append((prompt, duration))
if not clean_rows:
raise ValueError("Add at least one action to the timeline.")
if len(clean_rows) > MAX_SEGMENTS:
raise ValueError(f"The timeline supports at most {MAX_SEGMENTS} segments.")
if sum(duration for _, duration in clean_rows) > MAX_TOTAL_SECONDS + 1e-6:
raise ValueError(
f"The combined timeline may not exceed {MAX_TOTAL_SECONDS:g} seconds."
)
segments = []
start_frame = 0
for prompt, duration in clean_rows:
frame_count = max(1, int(round(duration * float(fps))))
end_frame = start_frame + frame_count
segments.append(
{
"prompt": prompt,
"duration_seconds": frame_count / float(fps),
"start_frame": start_frame,
"end_frame": end_frame,
}
)
start_frame = end_frame
return segments