File size: 2,806 Bytes
76a8377
 
 
 
 
 
 
 
1b78d7e
76a8377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""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