diff --git a/docs/design/server_contracts/openai.md b/docs/design/server_contracts/openai.md index debe41f4d..ccddc76e9 100644 --- a/docs/design/server_contracts/openai.md +++ b/docs/design/server_contracts/openai.md @@ -16,6 +16,8 @@ requests. HTTP handling and job polling remain asynchronous. (including FastH3 and Omni) and Pika H3 Flex. Version 1 reports the 24 fps frame grid, integer duration choices from 5 to 15 seconds, and recommended 480p/768p width/height pairs by aspect ratio. Other families return `null`. +The family is resolved from the registry and local checkpoints only, so +discovery never downloads a Hub manifest. These values describe model geometry, independently of the serve file's default request. `recommended_frame_sizes` is not an exhaustive canvas list, and `hardware_capacity_verified: false` means the response does not establish diff --git a/fastvideo/entrypoints/openai/generation_capabilities.py b/fastvideo/entrypoints/openai/generation_capabilities.py index 6ffb7f087..54332705d 100644 --- a/fastvideo/entrypoints/openai/generation_capabilities.py +++ b/fastvideo/entrypoints/openai/generation_capabilities.py @@ -2,6 +2,10 @@ """Output geometry advertised by a loaded pipeline, independent of device capacity.""" from typing import Any +from fastvideo.logger import init_logger + +logger = init_logger(__name__) + def video_output_capabilities(args: Any) -> dict[str, Any] | None: """Return known output constraints, or None for an unregistered family. @@ -11,7 +15,12 @@ def video_output_capabilities(args: Any) -> dict[str, Any] | None: """ from fastvideo.registry import get_preset_selection - _, family = get_preset_selection(args.model_path) + try: + # Resolve locally: model discovery must never gate /v1/model_info on a Hub lookup. + _, family = get_preset_selection(args.model_path, allow_remote=False) + except Exception as exc: # discovery is best-effort; report unknown instead of failing the request + logger.warning("Could not resolve output capabilities for %r: %s", args.model_path, exc) + return None if family not in {"minimax_h3", "pika_h3_flex"}: return None @@ -37,13 +46,6 @@ def video_output_capabilities(args: Any) -> dict[str, Any] | None: height, width = canvas_for_aspect(aspect, resolution) else: height, width = resolve_canvas_size(aw, ah, short_edge=short_edge, max_pixels=max_pixels) - # Nearest-lattice rounding can exceed the area cap. These are recommendations for the - # explicit width/height path, whose adapter enforces the cap before admission. - while width * height > max_pixels: - if width >= height: - width -= MINIMAX_H3_CANVAS_MULTIPLE - else: - height -= MINIMAX_H3_CANVAS_MULTIPLE sizes[aspect][resolution] = [width, height] return { "schema_version": 1, diff --git a/fastvideo/registry.py b/fastvideo/registry.py index 0a0d18361..2dbfad417 100644 --- a/fastvideo/registry.py +++ b/fastvideo/registry.py @@ -1467,14 +1467,17 @@ def get_default_preset(model_path: str) -> str | None: return config_info.default_preset -def get_preset_selection(model_path: str) -> tuple[str | None, str | None]: +def get_preset_selection(model_path: str, *, allow_remote: bool = True) -> tuple[str | None, str | None]: """Return ``(default_preset, model_family)`` for a model path. Single-lookup variant of :func:`get_default_preset` + :func:`get_model_family`; callers that need both should prefer this - to avoid walking the registry twice. + to avoid walking the registry twice. With ``allow_remote=False`` the + family comes only from the registry and local directories, so an + unresolved path returns ``(None, None)`` instead of downloading a Hub + manifest. """ - config_info = _get_config_info(model_path, raise_on_missing=False) + config_info = _get_config_info(model_path, raise_on_missing=False, allow_remote=allow_remote) if config_info is None: return None, None return config_info.default_preset, config_info.model_family diff --git a/fastvideo/tests/entrypoints/test_generation_capabilities.py b/fastvideo/tests/entrypoints/test_generation_capabilities.py index b17bf8519..6592d9b8a 100644 --- a/fastvideo/tests/entrypoints/test_generation_capabilities.py +++ b/fastvideo/tests/entrypoints/test_generation_capabilities.py @@ -7,12 +7,17 @@ import pytest from fastvideo.entrypoints.openai.generation_capabilities import video_output_capabilities from fastvideo.pipelines.basic.minimax_h3.packing import MINIMAX_H3_MAX_PIXELS from fastvideo.pipelines.basic.minimax_h3.stages.minimax_h3_input_preparation import resolve_target_num_frames -from fastvideo.pipelines.basic.pika_h3_flex.buckets import canvas_for_aspect +from fastvideo.pipelines.basic.pika_h3_flex.buckets import (PIKA_FPS, PIKA_MAX_DURATION, PIKA_MIN_DURATION, + canvas_for_aspect, resolve_frames) + + +def _family(monkeypatch, family: str | None) -> None: + monkeypatch.setattr("fastvideo.registry.get_preset_selection", lambda *_args, **_kwargs: (None, family)) @pytest.mark.parametrize("family", ["minimax_h3", "pika_h3_flex"]) def test_h3_discovery_includes_longer_clips_and_768p(monkeypatch, family): - monkeypatch.setattr("fastvideo.registry.get_preset_selection", lambda _: (None, family)) + _family(monkeypatch, family) result = video_output_capabilities(SimpleNamespace(model_path="local-model")) assert result["duration_seconds"] == {"min": 5, "max": 15} assert result["recommended_frame_sizes"]["16:9"] == {"480p": [832, 480], "768p": [1344, 768]} @@ -23,24 +28,41 @@ def test_h3_discovery_includes_longer_clips_and_768p(monkeypatch, family): for aspect, tiers in result["recommended_frame_sizes"].items(): for tier, (width, height) in tiers.items(): assert width % 32 == height % 32 == 0 - if family == "minimax_h3": - assert width * height <= MINIMAX_H3_MAX_PIXELS - else: - assert [height, width] == list(canvas_for_aspect(aspect, tier)) + # Both families share the released tier geometry, so neither may advertise a + # bucket the other would reject for the same aspect and resolution. + assert (height, width) == canvas_for_aspect(aspect, tier) + assert width * height <= MINIMAX_H3_MAX_PIXELS assert result["hardware_capacity_verified"] is False +def test_duration_grid_reuses_pika_frame_rules(monkeypatch): + _family(monkeypatch, "pika_h3_flex") + result = video_output_capabilities(SimpleNamespace(model_path="local-model")) + assert result["fps"] == PIKA_FPS + assert result["duration_seconds"] == {"min": int(PIKA_MIN_DURATION), "max": int(PIKA_MAX_DURATION)} + for seconds, frames in result["num_frames_by_duration_seconds"].items(): + assert frames == resolve_frames(int(seconds) * PIKA_FPS) + + def test_unknown_family_does_not_advertise_h3_options(monkeypatch): - monkeypatch.setattr("fastvideo.registry.get_preset_selection", lambda _: (None, "wan")) + _family(monkeypatch, "wan") assert video_output_capabilities(SimpleNamespace(model_path="local-model")) is None +def test_unresolved_model_path_stays_offline(monkeypatch): + def _no_download(*_args, **_kwargs): + raise AssertionError("model discovery must not download a Hub manifest") + + monkeypatch.setattr("fastvideo.registry.maybe_download_model_index", _no_download) + assert video_output_capabilities(SimpleNamespace(model_path="unregistered-org/unregistered-model")) is None + + def test_model_info_exposes_output_constraints_without_generation(monkeypatch): from fastapi import FastAPI from fastapi.testclient import TestClient from fastvideo.entrypoints.openai import common_api - monkeypatch.setattr("fastvideo.registry.get_preset_selection", lambda _: (None, "minimax_h3")) + _family(monkeypatch, "minimax_h3") monkeypatch.setattr(common_api, "get_server_args", lambda: SimpleNamespace(model_path="local-model", lora_path=None)) monkeypatch.setattr(common_api, "get_serving_engine", lambda: SimpleNamespace(lora_runtime=None)) monkeypatch.setattr(common_api, "get_served_model_name", lambda: "custom-omni-alias")