2026-08-31 15:57:13 +08:00

364 lines
14 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""MiniMax H3 canonical request validation.
Entry fail-fast for `minimax_h3.request/v1`: every violation raises ValueError
with the offending field path. Output is a normalized canonical dict (frame
indices validated but semantic -1 preserved, nothing else rewritten — prompt passes through verbatim and
conditions order is semantic, never reordered).
"""
from __future__ import annotations
import math
from collections.abc import Mapping, Sequence
from typing import Any
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.constants import (
MINIMAX_H3_MAX_DURATION_SECONDS,
MINIMAX_H3_MIN_DURATION_SECONDS,
MINIMAX_H3_SUPPORTED_FPS,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.task_profiles import (
MINIMAX_H3_CONDITION_ROLE_KEYFRAME,
MINIMAX_H3_CONDITION_ROLE_REFERENCE,
MINIMAX_H3_FINITE_ASPECT_RATIOS,
MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES,
MINIMAX_H3_TASK_FL2VA,
MINIMAX_H3_TASK_REF2VA,
MINIMAX_H3_TASK_T2VA,
MiniMaxH3TaskProfile,
canonical_minimax_h3_task,
minimax_h3_task_profile,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.time_request import (
minimax_h3_align_frame_count,
)
MINIMAX_H3_REQUEST_SCHEMA = "minimax_h3.request/v1"
MINIMAX_H3_MAX_SIGNED_SEED = (1 << 63) - 1
_ALLOWED_CONDITION_KEYS = frozenset(
{"type", "uri", "role", "frame_index", "start_time_seconds"}
)
def _require_str(value: Any, path: str) -> str:
if not isinstance(value, str) or value == "":
raise ValueError(f"{path} must be a non-empty string")
return value
def _require_int(value: Any, path: str) -> int:
if isinstance(value, bool) or not isinstance(value, int):
raise ValueError(f"{path} must be an integer")
return value
def _optional_positive_finite_float(value: Any, path: str) -> float | None:
if value is None:
return None
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(f"{path} must be a number")
normalized = float(value)
if not math.isfinite(normalized) or normalized <= 0.0:
raise ValueError(f"{path} must be a positive finite number")
return normalized
def _optional_nonnegative_finite_float(value: Any, path: str) -> float | None:
if value is None:
return None
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(f"{path} must be a number")
normalized = float(value)
if not math.isfinite(normalized) or normalized < 0.0:
raise ValueError(f"{path} must be a non-negative finite number")
return normalized
def _validate_target(target: Any, *, profile: MiniMaxH3TaskProfile) -> dict[str, Any]:
path = "target"
if not isinstance(target, Mapping):
raise ValueError(f"{path} is required and must be an object")
# The canonical target has a deliberately small projection. Transport
# compatibility keys are ignored; only these three declared values are
# validated and emitted below.
short_edge = _require_int(target.get("short_edge"), f"{path}.short_edge")
supported_short_edges = (480, 720, 768, 1080)
if short_edge not in supported_short_edges:
raise ValueError(
f"{path}.short_edge must be one of {supported_short_edges} for "
f"this MiniMax H3 benchmark deployment, got {short_edge}"
)
aspect_ratio = _require_str(target.get("aspect_ratio"), f"{path}.aspect_ratio")
if profile.aspect_ratio_forced_auto and aspect_ratio != "auto":
raise ValueError(
f'{path}.aspect_ratio must be "auto" for task {profile.task!r}, '
f"got {aspect_ratio!r}"
)
has_duration = target.get("duration_seconds") is not None
if (
profile.task in {MINIMAX_H3_TASK_T2VA, MINIMAX_H3_TASK_REF2VA}
and aspect_ratio != "auto"
and aspect_ratio not in MINIMAX_H3_FINITE_ASPECT_RATIOS
):
raise ValueError(
f"{path}.aspect_ratio for task {profile.task!r} must be 'auto' or "
f"one of {list(MINIMAX_H3_FINITE_ASPECT_RATIOS)!r}, got "
f"{aspect_ratio!r}"
)
if not has_duration:
if not profile.duration_from_audio_reference:
raise ValueError(f"{path}.duration_seconds is required")
# ref2va: duration may derive from a reference audio; the
# audio-condition presence is enforced after conditions validate.
out: dict[str, Any] = {
"short_edge": short_edge,
"aspect_ratio": aspect_ratio,
}
if has_duration:
duration = target["duration_seconds"]
if isinstance(duration, bool) or not isinstance(duration, (int, float)):
raise ValueError(f"{path}.duration_seconds must be a number")
if duration <= 0:
raise ValueError(f"{path}.duration_seconds must be positive")
if not (
MINIMAX_H3_MIN_DURATION_SECONDS
<= float(duration)
<= MINIMAX_H3_MAX_DURATION_SECONDS
):
raise ValueError(
f"{path}.duration_seconds must be in "
f"[{MINIMAX_H3_MIN_DURATION_SECONDS:g}, "
f"{MINIMAX_H3_MAX_DURATION_SECONDS:g}], got {duration}"
)
out["duration_seconds"] = float(duration)
return out
def _validate_conditions(
conditions: Any,
*,
profile: MiniMaxH3TaskProfile,
frame_count: int | None,
) -> list[dict[str, Any]]:
path = "conditions"
if conditions is None:
conditions = []
if not isinstance(conditions, Sequence) or isinstance(conditions, (str, bytes)):
raise ValueError(f"{path} must be a list")
if not profile.conditions_required:
if len(conditions) > 0:
raise ValueError(
f"{path} must be empty for task {profile.task!r} "
f"(got {len(conditions)} entries)"
)
return []
if len(conditions) == 0:
raise ValueError(
f"{path} requires at least one entry for task {profile.task!r}"
)
if (
profile.min_condition_count is not None
and len(conditions) < profile.min_condition_count
):
raise ValueError(
f"{path} requires at least {profile.min_condition_count} entries "
f"for task {profile.task!r}, got {len(conditions)}"
)
if (
profile.max_condition_count is not None
and len(conditions) > profile.max_condition_count
):
raise ValueError(
f"{path} allows at most {profile.max_condition_count} entries "
f"for task {profile.task!r}, got {len(conditions)}"
)
aligned_frame_count = (
minimax_h3_align_frame_count(frame_count) if frame_count is not None else None
)
normalized: list[dict[str, Any]] = []
seen_frame_indices: dict[int, int] = {}
for index, cond in enumerate(conditions):
cpath = f"{path}[{index}]"
if not isinstance(cond, Mapping):
raise ValueError(f"{cpath} must be an object")
unknown = set(cond) - _ALLOWED_CONDITION_KEYS
if unknown:
raise ValueError(f"{cpath} has unknown fields: {sorted(unknown)}")
role = _require_str(cond.get("role"), f"{cpath}.role")
if role not in (
MINIMAX_H3_CONDITION_ROLE_KEYFRAME,
MINIMAX_H3_CONDITION_ROLE_REFERENCE,
):
raise ValueError(
f"{cpath}.role must be keyframe or reference, " f"got {role!r}"
)
cond_type = _require_str(cond.get("type"), f"{cpath}.type")
try:
rule = profile.rule_for(role=role, condition_type=cond_type)
except ValueError as exc:
raise ValueError(f"{cpath}: {exc}") from exc
uri = _require_str(cond.get("uri"), f"{cpath}.uri")
entry: dict[str, Any] = {"type": cond_type, "uri": uri, "role": role}
if rule.requires_frame_index:
frame_index = _require_int(cond.get("frame_index"), f"{cpath}.frame_index")
if aligned_frame_count is None:
raise ValueError(
f"{cpath}.frame_index requires a resolved target duration"
)
if frame_index == -1:
resolved = aligned_frame_count - 1
elif 0 <= frame_index < aligned_frame_count:
resolved = frame_index
else:
raise ValueError(
f"{cpath}.frame_index must be -1 or in "
f"[0, {aligned_frame_count}) after 17n+5 frame alignment, "
f"got {frame_index}"
)
if resolved in seen_frame_indices:
raise ValueError(
f"{cpath}.frame_index resolves to {resolved}, already "
f"bound by conditions[{seen_frame_indices[resolved]}]"
)
seen_frame_indices[resolved] = index
# Preserve the request-level semantic index. In particular, -1 is
# the canonical last-frame sentinel; the resolved pixel frame is
# carried separately by MiniMaxH3ResolvedPlan.
entry["frame_index"] = frame_index
elif cond.get("frame_index") is not None:
raise ValueError(f"{cpath}.frame_index is not allowed for role={role!r}")
start_time_seconds = _optional_nonnegative_finite_float(
cond.get("start_time_seconds"), f"{cpath}.start_time_seconds"
)
if start_time_seconds is not None:
if cond_type not in {"video", "video_audio"}:
raise ValueError(
f"{cpath}.start_time_seconds is only allowed for video "
"or video_audio references"
)
entry["start_time_seconds"] = start_time_seconds
normalized.append(entry)
return normalized
def _validate_fl2va_conditions(conditions: Sequence[Mapping[str, Any]]) -> None:
"""Enforce the public FL contract after per-entry schema validation."""
frame_indices = tuple(condition.get("frame_index") for condition in conditions)
if frame_indices not in MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES:
raise ValueError(
"conditions for task 'fl2va' must be one or two ordered "
"image/keyframe entries with frame_index [0], [-1], or [0, -1], "
f"got {list(frame_indices)!r}"
)
def minimax_h3_validate_canonical_request(
*,
task: Any,
prompt: Any,
conditions: Any,
target: Any,
flow_shift: Any = None,
audio_flow_shift: Any = None,
seed: Any = None,
**_extra_kwargs: Any,
) -> dict[str, Any]:
"""Validate and normalize a `minimax_h3.request/v1` canonical request.
Returns the normalized canonical dict; raises ValueError with a field
path on any violation. Conditions order is preserved (it is semantic:
prompt ordinal labels reference it). seed=0 is a legal value.
"""
# Accept transport wrappers and compatibility kwargs at this boundary, but
# never copy them into the canonical request.
del _extra_kwargs
# Normalize the task name before profile lookup so offline callers match
# the adapter behaviour.
task_name = canonical_minimax_h3_task(_require_str(task, "task"))
profile = minimax_h3_task_profile(task_name)
prompt_text = _require_str(prompt, "prompt")
normalized_target = _validate_target(target, profile=profile)
requested_frame_count = None
if normalized_target.get("duration_seconds") is not None:
requested_frame_count = int(
round(
float(normalized_target["duration_seconds"]) * MINIMAX_H3_SUPPORTED_FPS
)
)
normalized_conditions = _validate_conditions(
conditions,
profile=profile,
frame_count=requested_frame_count,
)
if profile.task == MINIMAX_H3_TASK_FL2VA:
_validate_fl2va_conditions(normalized_conditions)
# ref2va accepts ordered material streams containing any mix of
# image/audio/video/video_audio references. Type admission is handled by
# the task profile; temporal ambiguity is validated later when target
# duration is omitted.
if not profile.video_reference_supported:
for index, cond in enumerate(normalized_conditions):
if cond["type"] in ("video", "video_audio"):
raise ValueError(
f"conditions[{index}]: video references are not supported "
f"in v1 for task {profile.task!r} (image/audio only)"
)
if normalized_target.get("duration_seconds") is None:
# Only reachable for duration_from_audio_reference profiles.
duration_sources = [
cond
for cond in normalized_conditions
if cond["type"] in ("audio", "video", "video_audio")
]
if not duration_sources:
raise ValueError(
"target.duration_seconds is required, or exactly one "
"audio reference to derive duration from (including "
f"video/video_audio soundtracks; task {profile.task!r})"
)
if len(duration_sources) > 1:
raise ValueError(
"target.duration_seconds is required when multiple "
"audio-bearing references are provided"
)
canonical: dict[str, Any] = {
"schema": MINIMAX_H3_REQUEST_SCHEMA,
"task": task_name,
"prompt": prompt_text,
"conditions": normalized_conditions,
"target": normalized_target,
}
normalized_flow_shift = _optional_positive_finite_float(flow_shift, "flow_shift")
normalized_audio_flow_shift = _optional_positive_finite_float(
audio_flow_shift, "audio_flow_shift"
)
if normalized_flow_shift is not None:
canonical["flow_shift"] = normalized_flow_shift
if normalized_audio_flow_shift is not None:
canonical["audio_flow_shift"] = normalized_audio_flow_shift
if seed is not None:
normalized_seed = _require_int(seed, "seed")
if normalized_seed < 0:
raise ValueError(f"seed must be non-negative, got {normalized_seed}")
if normalized_seed > MINIMAX_H3_MAX_SIGNED_SEED:
raise ValueError(
f"seed must not exceed the signed int64 maximum, got {normalized_seed}"
)
canonical["seed"] = normalized_seed
return canonical
__all__ = [
"MINIMAX_H3_REQUEST_SCHEMA",
"MINIMAX_H3_MAX_SIGNED_SEED",
"MINIMAX_H3_SUPPORTED_FPS",
"minimax_h3_validate_canonical_request",
]