cs686/ardy-motion-api
6
1"""Validation helpers for ARDY's continuous action timeline."""2 3from __future__ import annotations4 5import math6from typing import Any7 8 9MIN_SEGMENT_SECONDS = 2.010MAX_SEGMENT_SECONDS = 4.011MAX_TOTAL_SECONDS = 8.012MAX_SEGMENTS = 813MAX_PROMPT_CHARS = 50014 15 16def _rows(value: Any) -> list:17 if value is None:18 return []19 if hasattr(value, "values"):20 return value.values.tolist()21 if hasattr(value, "tolist") and not isinstance(value, list):22 return value.tolist()23 return list(value)24 25 26def normalize_timeline(value: Any, *, fps: float) -> list[dict]:27 """Normalize Gradio table rows and calculate exact frame ranges."""28 if not math.isfinite(float(fps)) or float(fps) <= 0:29 raise ValueError("FPS must be greater than zero.")30 31 clean_rows = []32 for row in _rows(value):33 if row is None:34 continue35 row = list(row)36 prompt = " ".join(str(row[0] if row else "").split())37 duration_value = row[1] if len(row) > 1 else None38 if not prompt and duration_value in (None, ""):39 continue40 if not prompt:41 raise ValueError("Every timeline row must contain an action prompt.")42 if len(prompt) > MAX_PROMPT_CHARS:43 raise ValueError(44 f"Each action prompt must contain at most {MAX_PROMPT_CHARS} characters."45 )46 try:47 duration = float(duration_value)48 except (TypeError, ValueError) as exc:49 raise ValueError(50 f"Duration for '{prompt}' must be a number."51 ) from exc52 if not math.isfinite(duration) or not MIN_SEGMENT_SECONDS <= duration <= MAX_SEGMENT_SECONDS:53 raise ValueError(54 f"Each segment must be between {MIN_SEGMENT_SECONDS:g} "55 f"and {MAX_SEGMENT_SECONDS:g} seconds."56 )57 clean_rows.append((prompt, duration))58 59 if not clean_rows:60 raise ValueError("Add at least one action to the timeline.")61 if len(clean_rows) > MAX_SEGMENTS:62 raise ValueError(f"The timeline supports at most {MAX_SEGMENTS} segments.")63 if sum(duration for _, duration in clean_rows) > MAX_TOTAL_SECONDS + 1e-6:64 raise ValueError(65 f"The combined timeline may not exceed {MAX_TOTAL_SECONDS:g} seconds."66 )67 68 segments = []69 start_frame = 070 for prompt, duration in clean_rows:71 frame_count = max(1, int(round(duration * float(fps))))72 end_frame = start_frame + frame_count73 segments.append(74 {75 "prompt": prompt,76 "duration_seconds": frame_count / float(fps),77 "start_frame": start_frame,78 "end_frame": end_frame,79 }80 )81 start_frame = end_frame82 return segments83 