The language#

The intermediate representation: a validated semantic scene graph, and the real definition of the language. Nothing here carries a coordinate.

scenet.ir#

.. py:module:: scenet.ir

The intermediate representation: a validated semantic scene graph.

This is the language’s real definition. The YAML surface syntax is one way to produce it; a comic-script frontend will be another. Nothing here carries a coordinate – computing those is the solver’s job.

Validation is strict on purpose. Panel sources are untrusted input, and a typo in a predicate or an actor id should be a clear error at parse time rather than a silently wrong picture.

.. py:class:: AnchorX

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

Where along the panel width an actor would like to stand.

Horizontal only. Actors stand on a ground line, so their vertical position is derived from the camera rather than requested – which is why this has no vertical counterpart and :class:PlacementZone <scenet.ir.PlacementZone>, used for balloons, does.

These are weak preferences. Non-overlap and declared left-to-right ordering are required constraints and will override an anchor without complaint; two actors both asking for center will simply be pushed apart around it.

.. py:attribute:: AnchorX.LEFT_EDGE

module:

scenet.ir

value:

‘left_edge’

.. py:attribute:: AnchorX.LEFT_THIRD

module:

scenet.ir

value:

‘left_third’

.. py:attribute:: AnchorX.CENTRE

module:

scenet.ir

value:

‘center’

.. py:attribute:: AnchorX.RIGHT_THIRD

module:

scenet.ir

value:

‘right_third’

.. py:attribute:: AnchorX.RIGHT_EDGE

module:

scenet.ir

value:

‘right_edge’

.. py:method:: AnchorX.new(value)

module:

scenet.ir

.. py:class:: BalloonKind

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

What kind of balloon carries a line, which is how it gets drawn.

The kind changes the outline and the tail, never the placement: a whisper is subject to exactly the same face-avoidance and reading-order rules as a shout.

Kind

Outline

Tail

speech

plain ellipse

tapered pointer

thought

scalloped cloud

trail of bubbles

whisper

dashed ellipse

tapered pointer

shout

jagged burst

tapered pointer

.. py:attribute:: BalloonKind.SPEECH

module:

scenet.ir

value:

‘speech’

.. py:attribute:: BalloonKind.THOUGHT

module:

scenet.ir

value:

‘thought’

.. py:attribute:: BalloonKind.WHISPER

module:

scenet.ir

value:

‘whisper’

.. py:attribute:: BalloonKind.SHOUT

module:

scenet.ir

value:

‘shout’

.. py:method:: BalloonKind.new(value)

module:

scenet.ir

.. py:class:: CameraAngle

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

The camera’s height relative to the subject.

Affects headroom rather than perspective: this is a flat, orthographic compiler, so a tilted camera does not foreshorten anything. What it changes is how much air sits above the head – which is the compositional cue readers actually take from an angle, and one that survives being drawn flat.

A low camera looks up and the subject looms, so the head rides high in the frame with little space above it. A high camera looks down, so the head sits lower and more space opens up above. See

func:

headroom_for <scenet.solve.camera.headroom_for> for the exact factors.

.. py:attribute:: CameraAngle.LOW

module:

scenet.ir

value:

‘low’

.. py:attribute:: CameraAngle.EYE_LEVEL

module:

scenet.ir

value:

‘eye_level’

.. py:attribute:: CameraAngle.HIGH

module:

scenet.ir

value:

‘high’

.. py:method:: CameraAngle.new(value)

module:

scenet.ir

.. py:class:: CameraSpec

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

How the panel is framed.

.. attribute:: shot

Requested framing; an upper bound on tightness, see

class:

ShotType <scenet.ir.ShotType>.

.. attribute:: angle

Camera height, see :class:CameraAngle <scenet.ir.CameraAngle>.

There is exactly one camera per panel, and every actor is drawn at the scale it implies. Scaling each actor to its own crop landmark instead would make everybody the same apparent height and erase the body differences a comic uses to tell characters apart.

.. py:attribute:: CameraSpec.shot

module:

scenet.ir

type:

~scenet.ir.ShotType

.. py:attribute:: CameraSpec.angle

module:

scenet.ir

type:

~scenet.ir.CameraAngle

.. py:class:: CastMember

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

One character present in the panel.

.. attribute:: reference

Name of a puppet in the library. This is what gets drawn; the key this member is filed under in cast is the actor id used everywhere else.

.. attribute:: pose

Named pose from that puppet’s declared set.

.. attribute:: at

Preferred horizontal anchor.

.. attribute:: facing

Which way the figure is turned.

The split between actor id and reference is what lets one puppet appear twice in a panel as two different people:

cast:
  guard_left:  {reference: bob, pose: arms_crossed}
  guard_right: {reference: bob, pose: standing_neutral, facing: left}

.. py:attribute:: CastMember.reference

module:

scenet.ir

type:

str

.. py:attribute:: CastMember.pose

module:

scenet.ir

type:

str

.. py:attribute:: CastMember.at

module:

scenet.ir

type:

~scenet.ir.AnchorX

.. py:attribute:: CastMember.facing

module:

scenet.ir

type:

~scenet.ir.Facing

.. py:class:: Facing

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

Which way an actor is turned.

Mirroring the whole puppet, gaze vector included. Defaults to right, so a cast written left to right ends up looking into the panel rather than out of it.

.. py:attribute:: Facing.LEFT

module:

scenet.ir

value:

‘left’

.. py:attribute:: Facing.RIGHT

module:

scenet.ir

value:

‘right’

.. py:method:: Facing.new(value)

module:

scenet.ir

.. py:class:: PanelIR

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

A complete, validated panel: the language’s real definition.

Every frontend produces one of these and nothing else, which is what lets the YAML syntax and the comic-script syntax coexist without the solver knowing either exists. Nothing here carries a coordinate – computing those is the solver’s job, and keeping them out is what makes a panel reusable at any size.

.. attribute:: panel

Dimensions and margin.

.. attribute:: camera

Framing and angle.

.. attribute:: cast

Actor id to character. Declaration order is not significant; staging decides left-to-right order.

.. attribute:: staging

Spatial and attentional relations between actors.

.. attribute:: script

Dialogue, in reading order.

Validation is strict and total: unknown keys are rejected, every actor id mentioned in staging or script must exist in cast, and the ordering relations must not contain a cycle. A misspelled key that was silently ignored would produce a panel that is subtly wrong with no indication of why, which for a language meant to be precise is the worst possible failure.

.. admonition:: Example

from scenet import parse_panel panel = parse_panel(“panel: {size: [800.0, 600.0]}”) panel.panel.width, panel.camera.shot.value (800.0, ‘medium_shot’)

.. seealso:: :func:compile_ir <scenet.pipeline.compile_ir>, to turn one of these into geometry.

.. py:attribute:: PanelIR.panel

module:

scenet.ir

type:

~scenet.ir.PanelSpec

.. py:attribute:: PanelIR.camera

module:

scenet.ir

type:

~scenet.ir.CameraSpec

.. py:attribute:: PanelIR.cast

module:

scenet.ir

type:

dict[str, ~scenet.ir.CastMember]

.. py:attribute:: PanelIR.staging

module:

scenet.ir

type:

tuple[~scenet.ir.Relation, …]

.. py:attribute:: PanelIR.script

module:

scenet.ir

type:

tuple[~scenet.ir.SayEvent, …]

.. py:method:: PanelIR.check_references_resolve()

module:

scenet.ir

Every actor id mentioned anywhere must exist in the cast.

Caught here rather than in the solver so the error names the offending identifier while the source is still in view.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~typing.Self``

.. py:method:: PanelIR.check_ordering_is_consistent()

module:

scenet.ir

Horizontal ordering must not contain a cycle.

The layout engine is a linear constraint solver, so ordering has to be decided before it runs – see docs/reference/language.md. A cycle such as ‘a left_of b, b left_of a’ has no solution, and detecting it here produces a comprehensible message instead of an opaque solver failure.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~typing.Self``

.. py:method:: PanelIR.ordering_constraints()

module:

scenet.ir

Normalised (left, right) pairs from both left_of and right_of relations.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`tuple`\ \[:py:class:`str`, :py:class:`str`], :py:data:`…<Ellipsis>`]`

.. py:method:: PanelIR.gaze_targets()

module:

scenet.ir

Who is looking at whom.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\dict`\ \[:py:class:`str`, :py:class:`str`]`

returns:

A mapping from each looking actor to the actor they are looking at. An actor may look at only one target, so a later looking_at relation for the same subject replaces an earlier one.

.. admonition:: Example

from scenet import parse_panel panel = parse_panel( … “{cast: {a: {reference: alice}, b: {reference: bob}},” … “ staging: [a looking_at b]}” … ) panel.gaze_targets() {‘a’: ‘b’}

.. py:method:: PanelIR.ground_groups()

module:

scenet.ir

Actors joined by ground_shared_with, as connected components.

Union-find rather than pairwise handling, so that ‘a with b’ plus ‘b with c’ puts all three on one ground line without the author saying ‘a with c’.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\tuple`\ \[:py:class:`frozenset`\ \[:py:class:`str`], :py:data:`…<Ellipsis>`]`

.. py:class:: PanelSpec

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

The panel’s own dimensions.

.. attribute:: size

(width, height) in panel units. Everything else in the language is expressed relative to these, so they set what a unit means.

.. attribute:: margin

Inset on all four sides. Balloons are kept inside it; actors may bleed past it, which is ordinary comics practice.

.. admonition:: Example

from scenet import PanelSpec PanelSpec(size=(1200.0, 600.0)).width 1200.0

.. py:attribute:: PanelSpec.size

module:

scenet.ir

type:

tuple[float, float]

.. py:attribute:: PanelSpec.margin

module:

scenet.ir

type:

float

.. py:property:: PanelSpec.width

module:

scenet.ir

type:

float

Panel width in panel units.

.. py:property:: PanelSpec.height

module:

scenet.ir

type:

float

Panel height in panel units.

.. py:method:: PanelSpec.check_positive()

module:

scenet.ir

Reject a panel with no usable area.

rtype:

:sphinx_autodoc_typehints_type:\:py\:class\:\~typing.Self``

returns:

The validated spec.

raises ValueError:

A dimension is zero or negative, or the margins meet in the middle leaving nothing to compose in.

.. py:class:: PlacementZone

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

Where in the panel a balloon would prefer to sit.

Two-dimensional, unlike AnchorX: an actor is placed along the ground line and so only needs a horizontal anchor, whereas a balloon floats and needs both axes. These are hints of the weakest priority – occlusion and reading order override them freely.

.. py:attribute:: PlacementZone.TOP_LEFT

module:

scenet.ir

value:

‘top_left’

.. py:attribute:: PlacementZone.TOP_CENTRE

module:

scenet.ir

value:

‘top_center’

.. py:attribute:: PlacementZone.TOP_RIGHT

module:

scenet.ir

value:

‘top_right’

.. py:attribute:: PlacementZone.MIDDLE_LEFT

module:

scenet.ir

value:

‘middle_left’

.. py:attribute:: PlacementZone.MIDDLE_CENTRE

module:

scenet.ir

value:

‘middle_center’

.. py:attribute:: PlacementZone.MIDDLE_RIGHT

module:

scenet.ir

value:

‘middle_right’

.. py:attribute:: PlacementZone.BOTTOM_LEFT

module:

scenet.ir

value:

‘bottom_left’

.. py:attribute:: PlacementZone.BOTTOM_CENTRE

module:

scenet.ir

value:

‘bottom_center’

.. py:attribute:: PlacementZone.BOTTOM_RIGHT

module:

scenet.ir

value:

‘bottom_right’

.. py:property:: PlacementZone.fractions

module:

scenet.ir

type:

tuple[float, float]

The zone’s centre as a fraction of panel width and height.

.. py:method:: PlacementZone.new(value)

module:

scenet.ir

.. py:class:: Predicate

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

How one actor stands in relation to another.

Drawn from the spatial subset of the Visual Genome vocabulary rather than invented, so a scene stays convertible to and from the scene-graph representations used elsewhere in computer vision.

left_of and right_of are the load-bearing ones: they are resolved at parse time into a linear ordering, because Cassowary cannot express the disjunction “A left of B or B left of A”.

.. py:attribute:: Predicate.LEFT_OF

module:

scenet.ir

value:

‘left_of’

.. py:attribute:: Predicate.RIGHT_OF

module:

scenet.ir

value:

‘right_of’

.. py:attribute:: Predicate.IN_FRONT_OF

module:

scenet.ir

value:

‘in_front_of’

.. py:attribute:: Predicate.BEHIND

module:

scenet.ir

value:

‘behind’

.. py:attribute:: Predicate.LOOKING_AT

module:

scenet.ir

value:

‘looking_at’

.. py:attribute:: Predicate.GROUND_SHARED_WITH

module:

scenet.ir

value:

‘ground_shared_with’

.. py:method:: Predicate.new(value)

module:

scenet.ir

.. py:class:: Relation

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

One staging fact, written as a sentence.

.. attribute:: subject

Actor id the sentence is about.

.. attribute:: predicate

What relation holds.

.. attribute:: object

The other actor id.

Authored as alice left_of bob rather than a three-key mapping because staging is read far more often than it is written, and a sentence is legible at a glance.

raises pydantic.ValidationError:

The subject and object are the same actor. No predicate here is meaningful reflexively.

.. py:attribute:: Relation.subject

module:

scenet.ir

type:

str

.. py:attribute:: Relation.predicate

module:

scenet.ir

type:

~scenet.ir.Predicate

.. py:attribute:: Relation.object

module:

scenet.ir

type:

str

.. py:method:: Relation.check_not_reflexive()

module:

scenet.ir

Reject a relation between an actor and itself.

rtype:

:sphinx_autodoc_typehints_type:\:py\:class\:\~typing.Self``

returns:

The validated relation.

raises ValueError:

Subject and object are the same actor id. No predicate in the language means anything reflexively, so this is always a typo.

.. py:class:: SayEvent

module:

scenet.ir

Bases: :py:class:~scenet.ir.Strict

One line of dialogue.

.. attribute:: by

Actor id of the speaker; must be in the cast.

.. attribute:: text

What is said. Line breaking is the compiler’s job, so write it as one string and do not insert newlines yourself.

.. attribute:: prefer

Optional hint about where the balloon should sit. The weakest of all the placement terms – face avoidance and reading order override it.

.. attribute:: kind

Which sort of balloon carries it.

Script order is reading order, and reading order is a hard constraint. Reorder these and you reorder the panel.

.. py:attribute:: SayEvent.by

module:

scenet.ir

type:

str

.. py:attribute:: SayEvent.text

module:

scenet.ir

type:

str

.. py:attribute:: SayEvent.prefer

module:

scenet.ir

type:

~scenet.ir.PlacementZone | None

.. py:attribute:: SayEvent.kind

module:

scenet.ir

type:

~scenet.ir.BalloonKind

.. py:class:: ShotType

module:

scenet.ir

Bases: :py:class:~enum.StrEnum

How tightly the camera frames the cast.

Ordered from widest to tightest, and the order is enforced by a test: reading down the ladder, the figure never gets smaller.

A shot type is defined by two things in two different units. The crop landmark is anatomical – the waist, the chest, the shoulders – which is what stops a shot type baking in one body and one pose; naming a fraction of panel height instead would do exactly that. The headroom is a plain fraction of panel height, because it is about composition within the frame rather than anatomy. docs/reference/shot_types.md is normative.

The requested shot is an upper bound on tightness, not a promise. If the cast cannot fit across the panel at that framing the camera retreats, and says so in

attr:

CompileResult.notes <scenet.pipeline.CompileResult.notes>.

.. admonition:: Example

from scenet import ShotType ShotType(“close_up”) <ShotType.CLOSE_UP: ‘close_up’>

.. py:attribute:: ShotType.LONG_SHOT

module:

scenet.ir

value:

‘long_shot’

.. py:attribute:: ShotType.WIDE

module:

scenet.ir

value:

‘wide’

.. py:attribute:: ShotType.FULL_SHOT

module:

scenet.ir

value:

‘full_shot’

.. py:attribute:: ShotType.MEDIUM_FULL

module:

scenet.ir

value:

‘medium_full’

.. py:attribute:: ShotType.COWBOY

module:

scenet.ir

value:

‘cowboy’

.. py:attribute:: ShotType.MEDIUM_SHOT

module:

scenet.ir

value:

‘medium_shot’

.. py:attribute:: ShotType.MEDIUM_CLOSE_UP

module:

scenet.ir

value:

‘medium_close_up’

.. py:attribute:: ShotType.CLOSE_UP

module:

scenet.ir

value:

‘close_up’

.. py:attribute:: ShotType.BIG_CLOSE_UP

module:

scenet.ir

value:

‘big_close_up’

.. py:attribute:: ShotType.EXTREME_CLOSE_UP

module:

scenet.ir

value:

‘extreme_close_up’

.. py:method:: ShotType.new(value)

module:

scenet.ir

.. py:class:: Strict

module:

scenet.ir

Bases: :py:class:~pydantic.main.BaseModel

Reject unknown keys everywhere.

A misspelled key that is silently ignored produces a panel that is subtly wrong with no indication of why, which is the worst possible failure for a language meant to be precise.