From 2d5bf50880d09914a2cae9edd044b9d2a72ea256 Mon Sep 17 00:00:00 2001 From: halfaipg Date: Tue, 20 Oct 2026 18:20:07 -0400 Subject: [PATCH 1/5] feat(video): prepare offline FastH3 execution contracts Keep FastH3 outside live recipe loading, pricing and admission while preparing exact frame units and distinct image/audio source roles. Model-use rights, native qualification and the worker/billing/UI rollout remain separate gates. --- grid_api/services/AGENTS.md | 9 + grid_api/services/fasth3_contract.py | 185 +++++++++++++ .../services/tests/test_fasth3_contract.py | 243 ++++++++++++++++++ recipes/AGENTS.md | 2 +- recipes/qualification/AGENTS.md | 6 +- recipes/qualification/fasth3/AGENTS.md | 31 +++ recipes/qualification/fasth3/README.md | 68 +++++ .../fasth3/omni-nvfp4-qad-v1.json | 92 +++++++ 8 files changed, 634 insertions(+), 2 deletions(-) create mode 100644 grid_api/services/fasth3_contract.py create mode 100644 grid_api/services/tests/test_fasth3_contract.py create mode 100644 recipes/qualification/fasth3/AGENTS.md create mode 100644 recipes/qualification/fasth3/README.md create mode 100644 recipes/qualification/fasth3/omni-nvfp4-qad-v1.json diff --git a/grid_api/services/AGENTS.md b/grid_api/services/AGENTS.md index cf6834a7..2f3ce1a3 100644 --- a/grid_api/services/AGENTS.md +++ b/grid_api/services/AGENTS.md @@ -7,6 +7,15 @@ content sanitization, and reward settlement. ## Ownership +- `fasth3_contract.py` owns an offline-only FastH3 qualification compiler, + exact17k+5frame units, integer quote math and separate appearance/opening/ + conditioning-audio/preserved-soundtrack slots. Its fixed template lives in + `recipes/qualification/fasth3/`; the live registry rejects its schema and no + HTTP, price-book or worker-advertisement path calls it. Candidate bounds are + not native capacity/quality. Model-use rights, source protocol, purchased + billing and public rollout remain unresolved release gates. Tests prove + startup/public-async/unpriced-money denial, not license or native execution. + - Live book `2026-10-20-a` adds the owner-approved `MOSS Image SFT BF16` image-only launch peg of USD0.010/output on Core0d2ee0b7. Its exact model-specific defaults are50steps/CFG3/Euler; other model defaults diff --git a/grid_api/services/fasth3_contract.py b/grid_api/services/fasth3_contract.py new file mode 100644 index 00000000..f53ad7d0 --- /dev/null +++ b/grid_api/services/fasth3_contract.py @@ -0,0 +1,185 @@ +# SPDX-FileCopyrightText: 2026 AI Power Grid +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Offline FastH3 qualification compiler, deliberately outside live recipe admission. + +The live registry does not parse this schema. No model registration, pricing-book +entry, credit authorization, source decoding, storage or network request occurs. +""" + +import copy +import json +import math +import re +from fractions import Fraction +from pathlib import Path +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +MODEL = "FastH3 Omni NVFP4 QAD" +CANDIDATE = Path(__file__).resolve().parents[2] / "recipes/qualification/fasth3/omni-nvfp4-qad-v1.json" + + +class FastH3Contract(BaseModel): + model_config = ConfigDict(extra="forbid", strict=True, frozen=True) + + contract_schema: Literal["aipg.fasth3-video.v1"] = Field(alias="schema") + variant: Literal["omni-nvfp4-qad"] + min_frames: int = Field(ge=124, le=362) + max_frames: int = Field(ge=124, le=362) + canvases: list[Literal["1344x768", "768x1344", "768x768"]] = Field(min_length=1, max_length=3) + + @model_validator(mode="after") + def validate_matrix(self): + if ( + self.min_frames > self.max_frames + or self.min_frames % 17 != 5 + or self.max_frames % 17 != 5 + or len(set(self.canvases)) != len(self.canvases) + ): + raise ValueError("invalid FastH3 candidate matrix") + return self + + +class FastH3Inputs(BaseModel): + model_config = ConfigDict(extra="forbid", strict=True, frozen=True, allow_inf_nan=False) + + prompt: str = Field(default="", max_length=8000) + seed: int = Field(default=29, ge=0, le=2**53 - 1) + frames: int = Field(default=124, ge=124, le=362) + seconds: float | None = Field(default=None, gt=0, le=362 / 24) + width: int = Field(default=1344) + height: int = Field(default=768) + fps: Literal[24] = 24 + steps: Literal[8] = 8 + cfg: Literal[1.0] = 1.0 + sampler: Literal["res_multistep"] = "res_multistep" + scheduler: Literal["simple"] = "simple" + audio: Literal[True] = True + appearance_image: bool = False + opening_image: bool = False + audio_mode: Literal["generated", "reference", "preserve"] = "generated" + + @field_validator("fps", "steps", "audio", mode="before") + @classmethod + def primitive_literals(cls, value, info): + if type(value) is not (bool if info.field_name == "audio" else int): + raise ValueError("invalid primitive type") + return value + + @field_validator("cfg", mode="before") + @classmethod + def numeric_cfg(cls, value): + if type(value) not in (int, float): + raise ValueError("CFG must be numeric, not boolean") + return value + + @model_validator(mode="after") + def validate_roles(self): + if not self.prompt.strip() or len(self.prompt.encode()) > 8000: + raise ValueError("invalid prompt") + if self.frames % 17 != 5: + raise ValueError("frames must be exact 17k+5; no silent rounding") + if self.seconds is not None and not math.isclose( + self.seconds, + self.frames / 24, + rel_tol=0, + abs_tol=1e-9, + ): + raise ValueError("seconds disagree with exact frames") + if self.audio_mode != "generated" and not (self.appearance_image or self.opening_image): + raise ValueError("audio candidates require an explicit image") + available = set() + if self.appearance_image: + available.add(("Picture", "1")) + if self.audio_mode == "reference": + available.add(("Audio", "1")) + if any(label not in available for label in re.findall(r"<(Picture|Audio|Video)\s*(\d+)>", self.prompt)): + raise ValueError("prompt refers to an absent reference slot") + return self + + +def normalize(contract: FastH3Contract, values: dict) -> dict: + """Return exact candidate units, never a price, live capability or permission.""" + try: + contract = FastH3Contract.model_validate(contract.model_dump(by_alias=True)) + inputs = FastH3Inputs.model_validate(values) + if not contract.min_frames <= inputs.frames <= contract.max_frames or f"{inputs.width}x{inputs.height}" not in contract.canvases: + raise ValueError("outside candidate matrix") + except (ValueError, TypeError, AttributeError): + raise ValueError("Invalid FastH3 qualification inputs") from None + duration = Fraction(inputs.frames, 24) + return { + **inputs.model_dump(exclude_none=True), + "seconds": float(duration), + "duration_numerator": duration.numerator, + "duration_denominator": duration.denominator, + } + + +def quote_units(frames: int, micro_usd_per_second: int) -> int: + """Offline integer ceiling for a supplied server rate; no tariff is approved here.""" + if ( + type(frames) is not int + or not 124 <= frames <= 362 + or frames % 17 != 5 + or type(micro_usd_per_second) is not int + or micro_usd_per_second <= 0 + ): + raise ValueError("invalid FastH3 quote units") + return (frames * micro_usd_per_second + 23) // 24 + + +def compile_candidate(values: dict) -> dict: + """Materialize one Core-owned candidate and explicit unbound source slots. + + Source bytes must be independently validated and committed by a future H3 + source adapter before execution. Placeholder filenames are not input assets. + This is intentionally not called by recipes.resolve or public HTTP routes. + """ + workflow = json.loads(CANDIDATE.read_text()) + metadata = workflow["_grid"] + if ( + metadata["modelName"] != MODEL + or metadata.get("qualificationOnly") is not True + or metadata.get("engine") != "comfyui" + or metadata.get("jobType") != "video" + ): + raise ValueError("invalid FastH3 qualification metadata") + contract = FastH3Contract.model_validate(metadata["videoContract"]) + execution = normalize(contract, values) + graph = copy.deepcopy({key: node for key, node in workflow.items() if key != "_grid"}) + graph["conditioning"]["inputs"].update( + { + "prompt": execution["prompt"], + "width": execution["width"], + "height": execution["height"], + "length": execution["frames"], + }, + ) + graph["noise"]["inputs"]["noise_seed"] = execution["seed"] + bindings = [] + if execution["appearance_image"]: + graph["appearance"] = {"class_type": "LoadImage", "inputs": {"image": "grid_appearance.png"}} + graph["conditioning"]["inputs"]["ref_images.ref_image_0"] = ["appearance", 0] + bindings.append({"role": "appearance_image", "path": "appearance.inputs.image"}) + if execution["audio_mode"] == "reference": + graph["reference_sound"] = {"class_type": "LoadAudio", "inputs": {"audio": "grid_reference.wav"}} + graph["conditioning"]["inputs"]["ref_audios.ref_audio_0"] = ["reference_sound", 0] + bindings.append({"role": "conditioning_audio", "path": "reference_sound.inputs.audio"}) + if execution["opening_image"] or execution["audio_mode"] == "preserve": + guide = {"positive": ["conditioning", 0], "latent": ["conditioning", 1], "frame_idx": 0} + if execution["opening_image"]: + graph["opening"] = {"class_type": "LoadImage", "inputs": {"image": "grid_opening.png"}} + guide.update(image=["opening", 0], vae=["video_vae", 0]) + bindings.append({"role": "opening_image", "path": "opening.inputs.image"}) + if execution["audio_mode"] == "preserve": + graph["soundtrack"] = {"class_type": "LoadAudio", "inputs": {"audio": "grid_soundtrack.wav"}} + guide.update(audio=["soundtrack", 0], audio_vae=["audio_vae", 0]) + graph["video"]["inputs"]["audio"] = ["soundtrack", 0] + del graph["decode_audio"] + bindings.append({"role": "soundtrack", "path": "soundtrack.inputs.audio"}) + graph["guide"] = {"class_type": "MiniMaxH3AddGuide", "inputs": guide} + graph["guider"]["inputs"]["conditioning"] = ["guide", 0] + return {"model": MODEL, "execution": execution, "spec": graph, "source_bindings": bindings, "qualification_only": True} diff --git a/grid_api/services/tests/test_fasth3_contract.py b/grid_api/services/tests/test_fasth3_contract.py new file mode 100644 index 00000000..ca659a05 --- /dev/null +++ b/grid_api/services/tests/test_fasth3_contract.py @@ -0,0 +1,243 @@ +# SPDX-FileCopyrightText: 2026 AI Power Grid +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Offline H3 contract checks, not GPU, license, paid-billing or deployment proof.""" + +import copy +import itertools +import json +from fractions import Fraction +from pathlib import Path +from uuid import uuid4 + +import pytest +from fastapi import HTTPException +from pydantic import ValidationError + +from grid_api.config import GridSettings +from grid_api.services import credits, generation_admission, pricing, recipes +from grid_api.services import fasth3_contract as h3 + + +@pytest.fixture +def registry(monkeypatch): + for name in ("_BY_ROOT", "_BY_NAME", "_BY_ID", "_BY_MODEL", "_LOCAL_RECIPES", "_ONCHAIN_RECIPES"): + monkeypatch.setattr(recipes, name, {}) + + +def candidate(**values): + return h3.compile_candidate({"prompt": "A sunny park.", **values}) + + +def test_startup_catalog_never_loads_or_advertises_candidate(registry): + root = Path(__file__).resolve().parents[3] + assert recipes.load_local_recipes(str(root / "recipes")) > 0 + assert recipes.get_recipe(h3.MODEL) is None + assert recipes.video_contract_for_model(h3.MODEL) is None + assert recipes.generation_modes(h3.MODEL) == [] + assert recipes.param_schema(h3.MODEL) is None + assert h3.MODEL not in {recipe.model_name for recipe in recipes.list_recipes()} + assert pricing.get_price(h3.MODEL) is None + # This low-level helper returns zero for a missing rate; enforcement must + # distinguish unpriced from free before reserving or dispatching work. + assert pricing.quote_video(h3.MODEL, 124 / 24) == 0 + assert not pricing.is_priced_for(h3.MODEL, "video") + + +def test_unreviewed_schema_cannot_register_as_live_ltx(registry): + workflow = json.loads(h3.CANDIDATE.read_text()) + with pytest.raises(ValidationError): + recipes.register_recipe("0xunqualified-h3", h3.MODEL, workflow) + assert recipes.list_recipes() == [] + + +@pytest.mark.parametrize("source", [False, True]) +def test_open_public_async_switch_does_not_enable_h3(source): + settings = GridSettings( + _env_file=None, + media_async_enabled=True, + media_async_admission_mode="open", + media_async_nvfp4_enabled=True, + media_async_source_admission_mode="open", + ) + with pytest.raises(HTTPException) as error: + generation_admission.require_async_video(settings, uuid4(), model=h3.MODEL, has_source=source) + assert error.value.status_code == 503 + + +def test_fixed_sampling_and_component_provenance(): + result = candidate(appearance_image=True, seed=0) + graph = result["spec"] + meta = json.loads(h3.CANDIDATE.read_text())["_grid"] + assert result["qualification_only"] is True + assert result["model"] == h3.MODEL + assert graph["model"]["inputs"]["unet_name"] == "fasth3_omniref_qad_nvfp4_comfy.safetensors" + assert graph["encoder"]["inputs"]["type"] == "minimax" + assert graph["conditioning"]["inputs"]["ref_image_size"] == "match" + assert graph["noise"]["inputs"]["noise_seed"] == 0 + assert graph["sampler"]["inputs"]["sampler_name"] == "res_multistep" + assert graph["sigmas"]["inputs"]["steps"] == 8 + assert graph["sigmas"]["inputs"]["scheduler"] == "simple" + assert graph["guider"]["class_type"] == "BasicGuider" + assert graph["shift"]["inputs"] == {"model": ["model", 0], "shift_video": 12.0, "shift_audio": 3.0} + assert meta["provenance"]["model_revision"] == "b1b4d67d79930007a3f73ca8e98739796ded95a0" + assert meta["provenance"]["comfy_revision"] == "b1a247c9cf9dbfda4b2c0a0c1d50950667eeaa0c" + assert meta["provenance"]["license_sha256"] == "59b99642b95ea21630e311198ddbfffbfe05aadba0c2f5d884cbdf4efcc90f44" + for node_name, field in (("model", "unet_name"), ("encoder", "clip_name"), ("video_vae", "vae_name"), ("audio_vae", "vae_name")): + filename = graph[node_name]["inputs"][field] + assert len(meta["provenance"]["components"][filename]) == 64 + + +@pytest.mark.parametrize( + "appearance,opening,audio_mode", + list(itertools.product([False, True], [False, True], ["generated", "reference", "preserve"])), +) +def test_all_explicit_source_combinations_have_exact_slots(appearance, opening, audio_mode): + values = {"appearance_image": appearance, "opening_image": opening, "audio_mode": audio_mode} + if not appearance and not opening and audio_mode != "generated": + with pytest.raises(ValueError, match="Invalid FastH3"): + candidate(**values) + return + result = candidate(**values) + graph = result["spec"] + roles = [binding["role"] for binding in result["source_bindings"]] + assert ("appearance_image" in roles) == appearance + assert ("opening_image" in roles) == opening + assert ("conditioning_audio" in roles) == (audio_mode == "reference") + assert ("soundtrack" in roles) == (audio_mode == "preserve") + for binding in result["source_bindings"]: + node, inputs, field = binding["path"].split(".") + assert isinstance(graph[node][inputs][field], str) + for node in graph.values(): + for value in node["inputs"].values(): + if isinstance(value, list): + assert value[0] in graph + conditioning = graph["conditioning"]["inputs"] + assert ("ref_images.ref_image_0" in conditioning) == appearance + assert ("ref_audios.ref_audio_0" in conditioning) == (audio_mode == "reference") + assert ("guide" in graph) == (opening or audio_mode == "preserve") + if opening: + assert graph["guide"]["inputs"]["image"] == ["opening", 0] + if audio_mode == "preserve": + assert graph["guide"]["inputs"]["audio"] == ["soundtrack", 0] + assert graph["video"]["inputs"]["audio"] == ["soundtrack", 0] + assert "decode_audio" not in graph + else: + assert graph["video"]["inputs"]["audio"] == ["decode_audio", 0] + + +@pytest.mark.parametrize("frames", range(123, 364)) +def test_exact_h3_grid_never_rounds_ltx_counts_or_underquotes(frames): + if 124 <= frames <= 362 and frames % 17 == 5: + result = candidate(frames=frames) + execution = result["execution"] + assert result["spec"]["conditioning"]["inputs"]["length"] == frames + assert Fraction(execution["duration_numerator"], execution["duration_denominator"]) == Fraction(frames, 24) + for rate in (1, 1999, 10000, 20000): # Synthetic rates, not approved prices. + amount = h3.quote_units(frames, rate) + exact = Fraction(frames * rate, 24) + assert amount - 1 < exact <= amount + else: + with pytest.raises(ValueError): + candidate(frames=frames) + with pytest.raises(ValueError): + h3.quote_units(frames, 10000) + + +@pytest.mark.parametrize("size", [(1344, 768), (768, 1344), (768, 768)]) +def test_qualification_canvases_are_exact_not_auto_fit(size): + result = candidate(width=size[0], height=size[1]) + assert result["execution"]["width"] == size[0] + assert result["spec"]["conditioning"]["inputs"]["height"] == size[1] + + +@pytest.mark.parametrize( + "changes", + [ + {"frames": True}, + {"frames": "124"}, + {"frames": 125}, + {"frames": 625}, + {"seconds": 5.0}, + {"seconds": float("nan")}, + {"seconds": float("inf")}, + {"width": True}, + {"width": 1344.0}, + {"width": 512}, + {"height": 512}, + {"seed": True}, + {"seed": -1}, + {"seed": 2**53}, + {"seed": "29"}, + {"steps": 7}, + {"steps": 8.0}, + {"cfg": True}, + {"cfg": 2.0}, + {"fps": 24.0}, + {"audio": 1}, + {"audio": False}, + {"sampler": "euler"}, + {"scheduler": "normal"}, + {"precision": "bf16"}, + {"reference_video": "x.mp4"}, + {"appearance_image": "x.png"}, + {"opening_image": 1}, + {"audio_mode": "audio"}, + {"prompt": " "}, + {"prompt": "x" * 8001}, + {"prompt": "\u00e9" * 4001}, + {"prompt": ""}, + {"prompt": "