an.ir.validate#
Schema and semantic validation for SceneIR documents.
Two layers, called separately so callers can pick how strict to be:
validate_schema— Pydantic validation only. Wrong types, missing required fields, malformed JSON.validate_semantic— cross-field checks. Unknown asset references, zero-duration shots, voice refs missing from a voices store.
Layout-overlap checks (boxes off-screen, text behind sprites) live in
an.verify.layout, not here, because they need a render context.
Module Attributes
Keys an#106 retired, and what to write instead. |
|
an#109's removed camera fields. |
|
position in scene px, and the size of its scale (its SIGN is facing, which the genre's view continuity check says, an#203). |
|
a pixel, or a hundredth of the scale. |
|
a frame at 60 fps. |
|
The registered checks whose answer depends on what synthesis produced — a line's real length, hence where it starts and ends — and that |
Functions
|
The ids of |
|
|
|
The findings of the three registry checks alone — every action kind, entity kind and renderer the scene names must be registered — without the rest of |
|
|
|
|
|
|
|
Validate that |
|
Cross-field semantic checks. |
Classes
|
What a registered semantic check ( |
|
A single validation issue with a path into the IR. |
|
Result of running one or more validators. |
Exceptions
|
A scene names kinds or renderers nothing registered: refused at load. |
- an.ir.validate.CUT_CONTINUITY_PROPS: tuple[str, ...] = ('x', 'y', 'scale_x', 'scale_y')#
position in scene px, and the size of its scale (its SIGN is facing, which the genre’s view continuity check says, an#203).
- Type:
What an entity’s placement across a cut is compared on (an#394)
- an.ir.validate.CUT_CONTINUITY_TOLERANCE: dict[str, float] = {'scale_x': 0.01, 'scale_y': 0.01, 'x': 1.0, 'y': 1.0}#
a pixel, or a hundredth of the scale.
- Type:
How far a value may move across a cut and still be “the same”
- an.ir.validate.DIALOGUE_OVERRUN_TOLERANCE_S: float = 0.016666666666666666#
a frame at 60 fps.
- Type:
Slack before a line counts as running past its shot
- an.ir.validate.POST_SYNTHESIS_CHECKS: tuple[str, ...] = ('dialogue_fits', 'dialogue_in_dissolve', 'cutout.hidden_mouth_while_speaking')#
The registered checks whose answer depends on what synthesis produced — a line’s real length, hence where it starts and ends — and that
an rendertherefore runs again AFTER the audio pipeline, on the timing it will mux (post_synthesis_findings()runs exactly these, by name, through the registry). A check added later that readsDialogue.durationorstartbelongs here;tests/test_render_findings.pylists the ones that do.
- an.ir.validate.RETIRED_CAMERA_KEYS: frozenset[str] = frozenset({'focal_length', 'position', 'target'})#
an#109’s removed camera fields. A WARNING, not an error, and the difference is the harm: a surviving
stylesilently picks the wrong RENDERER, while these three selected nothing — they described a 3D camera this package never had. What is left is dead weight in a file, so it is worth saying and not worth failing over.Reported at all because the migration cannot reach them: a document already at the current version is never migrated again, so a camera block that came through a sync between the version bump and this check keeps them forever as
extra="allow"extras, and nothing else looks.
- an.ir.validate.RETIRED_KEYS: dict[str, dict[str, str]] = {'meta': {'default_style': 'default_renderer'}, 'shot': {'style': 'renderer'}}#
Keys an#106 retired, and what to write instead.
SceneIR’s models areextra="allow"(deliberately — forward compatibility), so a document that still carries one of these validates cleanly and renders with the DEFAULT renderer. The migration rewrites stored 0.1.x documents, but nothing rewrites a document that is already 0.2.0: an agent patch, a hand edit, or a caller passingstyle=toShot(...)all produce a permanently dead key that no later migration will touch. So it is caught here, at ERROR, by name.
- exception an.ir.validate.UnregisteredInSceneError(findings, *, where='')[source]#
Bases:
UnregisteredKindErrorA scene names kinds or renderers nothing registered: refused at load.
Raised by
require_registered_kinds()— whatan.load(project)(and soan render) runs, so a typo’dkind:orrenderer:can no longer render silently wrong now that the schema holds them asstr(review-244 S2).findingskeeps each one with its IR path.
- class an.ir.validate.ValidationContext(scene, report, stores, voices=None, characters=None, sounds=None, library_lock=None, shot=None, index=None, memo=<factory>)[source]#
Bases:
objectWhat a registered semantic check (
an.genres.SemanticCheck) reads.storesholds only the stores actually supplied, keyed by MALL name (an absent one means its checks did not RUN — never that what it holds is missing).shotandindexare set while theshotstage runs.memois shared by every check of onevalidate_semanticcall, so two checks that need the same derived fact compute it once (cached()).
- class an.ir.validate.ValidationFinding(severity, ir_path, description, location=None)[source]#
Bases:
objectA single validation issue with a path into the IR.
- class an.ir.validate.ValidationReport(passed=True, findings=<factory>)[source]#
Bases:
objectResult of running one or more validators.
passedis True iff there are no error-severity findings.
- an.ir.validate.counter_block_ids(shot, stores)[source]#
The ids of
shot’s counter text blocks (an#342): the only targets avalueis authored on in the core. Empty without the props store.
- an.ir.validate.post_synthesis_findings(scene, *, fps=None, checks=('dialogue_fits', 'dialogue_in_dissolve', 'cutout.hidden_mouth_while_speaking'), **stores)[source]#
(check, finding)for each finding the synthesized timing gives.The SAME registered checks
an validateruns, selected by name (POST_SYNTHESIS_CHECKS): dialogue past its shot’s end, a speaker overlapping themself, a line heard during a dissolve, a line spoken while the speaker’s view hides its mouth.an rendercalls this once the audio pipeline has stamped every line’s realduration, so whatan validatecould only estimate is reported exactly, at the moment it becomes known (an#254).fpsis the render’s (it decides the dissolve overlaps); default the scene’s.storesarevalidate_semantic()’savailable_*keywords. A check no loaded genre registered is skipped.- Return type:
- an.ir.validate.registered_kind_problems(scene)[source]#
The findings of the three registry checks alone — every action kind, entity kind and renderer the scene names must be registered — without the rest of
validate_semantic(no stores, no rig builds; cheap).- Return type:
- an.ir.validate.require_registered_kinds(scene, *, where='')[source]#
scene, orUnregisteredInSceneErrornaming every action kind, entity kind and renderer it uses that is not registered.- Return type:
- an.ir.validate.rig_stores()[source]#
{entity kind: (mall store, descriptor kind)}for every kind that has a rig.The core’s own (
prop) plus the kinds genres register with adescriptor_kind(the cut-out genre’scharacter): derived from the registry, not a table the core edits (an#246).>>> rig_stores()["prop"] ('props', 'PropDescriptor')
- an.ir.validate.shot_dialogue_overruns(shot, *, effects_of=None, synthesized_only=False, tolerance_s=0.016666666666666666)[source]#
(k, message)for each line ofshotthat ends past the shot’s end.The ONE overrun check:
an validateruns it, the audio pipeline runs it after synthesis (an.audio.pipeline.dialogue_overruns()), andan renderreports it from the rendered timing (post_synthesis_findings()).The audio is cut at the shot end (each shot’s mix is trimmed to its duration), and the lines play back to back, so a shot shortened below its dialogue loses the tail of it — silently, until now (an e2e run shrank an 8.2 s shot holding 7.1 s of speech to 3.0 s and
an validatesaid nothing).What is known depends on when this runs. After the audio pipeline, a line carries its real
durationand the check is exact. Before it, the duration is the offline voice’s estimate (an.audio.offline_tts.estimate_speech_duration()over the voice’stempo— exactly what an offline render will give, and an under-estimate for a real voice). Either way the lines are laid out by the pipeline’s own rule,an.ir.schema.Dialogue.planned_start()— back to back from the shot start, shifted by each line’spauseor pinned by itsat(an#187) — so a pause edited after synthesis is judged where it will play, not where the stale stamp says.effects_of(``line -> `` its voice’s normalised effects) supplies the tempo, and — for a synthesized line whose voice does not trim — the fix of trimming the silence a real voice pads a line with.>>> from an.ir.schema import Dialogue, Shot >>> shot = Shot(id="s", duration=1.0, dialogue=[ ... Dialogue(speaker="a", text="hi", start=0.2, duration=1.3, audio_ref="k")]) >>> [(k, m[:44]) for k, m in shot_dialogue_overruns(shot)] [(0, 'line 0 (a) ends at 1.30s as synthesized, pas')]
- an.ir.validate.validate_schema(doc)[source]#
Validate that
doc(dict, JSON string, or SceneIR) conforms to the schema.- Return type:
>>> validate_schema({"meta": {"title": "x"}, "timeline": []}).passed True >>> r = validate_schema({"meta": {"title": "x"}, "timeline": [{"id": "s", "duration": "not-a-number"}]}) >>> r.passed False
- an.ir.validate.validate_semantic(scene, *, available_voices=None, available_characters=None, available_props=None, available_environments=None, available_sounds=None, available_library_lock=None, available_styles=None, only=None, fps=None)[source]#
Cross-field semantic checks. Pass live stores in for cross-store checks.
Both
available_voicesandavailable_charactersaccept any mapping;available_propsis the same thing forkind="prop"entities (an#108), andavailable_environmentslets the flat-pan warning read a stage’s planes (an#111) — without it a prop’s swaps are reported as having no descriptor, which is validate refusing what compile accepts. Voices are consulted via__contains__only; characters additionally via__getitem__(the swap-reference andplaychecks read descriptor dicts, an#87 / an#7). PassNoneto skip those checks — and know that skipping them is what it sounds like: aplayor a swap the compiler will refuse passes silently without the store (the CLI,an validate, always passes it).available_stylesis the project’sstylesstore: a check reads the StylePack the scene names (meta.style_pack) fromctx.stores["styles"](a style’spolicy, an#348).available_library_lockis the project’s asset-library lockfile (mall["library_lock"]): with it, every scenelibrary:pin is checked against the lockfile (warningon disagreement) and every pinned check-out against its library version (infowhen it has been edited).The checks are a REGISTRY (
an.genres.registry.register_check()): the core’s own register below, a genre’s when it is loaded (the cut-out genre’splay,expression, turn and view checks), and they run in stages —scene, thenshotonce per shot, thenfinish— each by itsorder. An action or entity kind no loaded genre registered is one error naming the genre that provides it; checks that would trip over it skip that shot rather than crash.onlyruns just the registered checks of those names (whatan renderdoes after synthesis,post_synthesis_findings());fpsis the one the film is assembled at when it is not the scene’s (an render --fps), which decides how long a dissolve’s overlap is.- Return type: