"""A lacing body schema for a rendered caption (``pip install tituli[lacing]``).
URI: ``annot://schema/text-overlay/v1``. A caption is an annotation **on the
image** (``MediaRef(asset_id=<image hash>)``), and when that image sits under an
audio segment the pair is expressed the way ``artful.PanelBody`` does it: the
annotation's ``reference`` is the interval on the segment and
``provenance.was_derived_from`` lists both the image ``asset_id`` and the
caption annotation id. No N-ary reference type is invented; the timing lives
on the reference, not in this body.
Registration is lazy and idempotent (:func:`register`), so ``import tituli``
never touches lacing.
"""
from __future__ import annotations
from typing import Any
TEXT_OVERLAY_V1 = "annot://schema/text-overlay/v1"
_REGISTERED = False
def _model():
from pydantic import BaseModel, Field
class TextOverlayBodyV1(BaseModel):
"""What was written over the picture, and how. Placement is normalised."""
model_config = {"frozen": True, "extra": "forbid"}
text: str = Field(
..., description="The caption as displayed (after wrapping/truncation)."
)
attribution: str = Field(
"", description="The small credit line under the caption, if any."
)
kind: str = Field(
"caption",
description="Free string: 'caption', 'lower_third', 'title', 'intertitle', ...",
)
anchor: str = Field(
"", description="Grid position the block was placed at ('top-left', ...)."
)
box: tuple[float, float, float, float] | None = Field(
None, description="Normalised (x, y, w, h) of the text block in the frame."
)
ink: str | None = Field(None, description="Ink colour as #rrggbb, if recorded.")
scrim: bool | None = Field(
None, description="Whether a scrim was drawn under the text."
)
reason: str | None = Field(
None, description="The ink/scrim decision the frame's knowledge led to."
)
style: dict[str, Any] | None = Field(
None, description="The TextStyle fields used, for re-rendering."
)
unlabelled: bool = Field(
False,
description="True when the still was deliberately shown without a label (an explicit choice, not an omission).",
)
overlay_asset_id: str | None = Field(
None, description="asset_id of the rendered transparent PNG, if stored."
)
return TextOverlayBodyV1
[docs]
def register() -> str:
"""Register the schema with lacing (once). Returns the URI."""
global _REGISTERED
if _REGISTERED:
return TEXT_OVERLAY_V1
from lacing import register_body_schema
register_body_schema(TEXT_OVERLAY_V1, _model())
_REGISTERED = True
return TEXT_OVERLAY_V1
[docs]
def body_for(
layout,
frame,
*,
text: str,
attribution: str = "",
kind: str = "caption",
unlabelled: bool = False,
) -> dict:
"""The body dict for a rendered layout — plain data, no lacing needed.
>>> from tituli.frame import Frame
>>> from tituli.compose import caption
>>> f = Frame.blank((1920, 1080))
>>> b = body_for(caption("A still", "PD", frame=f), f, text="A still", attribution="PD")
>>> b["kind"], b["anchor"], len(b["box"])
('caption', 'bottom-left', 4)
"""
bb = layout.bbox()
ink = None
if layout.runs:
r, g, b, _ = layout.runs[0].color
ink = f"#{r:02x}{g:02x}{b:02x}"
return {
"text": text,
"attribution": attribution,
"kind": kind,
"anchor": str(layout.meta.get("anchor", "")),
"box": tuple(round(v, 4) for v in bb.to_norm(frame.width, frame.height)),
"ink": ink,
"scrim": any(p.kind != "box" or p.color[3] < 255 for p in layout.plates)
or None,
"reason": layout.meta.get("ink"),
"style": None,
"unlabelled": unlabelled,
"overlay_asset_id": None,
}
__all__ = ["TEXT_OVERLAY_V1", "register", "body_for"]