Documentation
¶
Overview ¶
Package reel holds the deterministic core of the reel pipeline: the storyboard schema and validation, the VO-driven timing maths, orphan-controlled text wrapping, and accent-word parsing. It is a faithful port of the blog gen-reel.py and is pure (no I/O, no ffmpeg, no fonts) so it unit-tests directly (spec 0001 §3.1, §8). The card rendering and ffmpeg assembly live in the renderer adapter (a later phase).
Index ¶
- Constants
- func AccentWords(text string) map[string]bool
- func AspectDims(aspect string) (w, h int, err error)
- func CardDurationFromVO(voSec float64, p Params) float64
- func CleanWord(w string) string
- func CountMarkers(text string) int
- func FormatIssues(errs []Issue) string
- func Marshal(sb Storyboard) ([]byte, error)
- func MediaIgnored(c Card, themeDefault CardMode) bool
- func Narration(c Card) string
- func StripAccents(text string) string
- func ValidScrimDirection(d string) bool
- func Wrap(text string, maxWidth float64, measure Measurer) []string
- type Align
- type Anchor
- type Card
- type CardMode
- type Issue
- type Measurer
- type Media
- type MediaKind
- type MediaSource
- type Migration
- type MigrationResult
- type Params
- type Result
- type Scrim
- type Storyboard
- type TextBlock
- type Timeline
- type VoiceOverride
Constants ¶
const ( // ScrimUp ramps to full opacity at the BOTTOM — the default, and what every // pre-0050 card does. ScrimUp = "up" // ScrimDown ramps to full opacity at the TOP. ScrimDown = "down" )
Scrim directions.
const ( MediaImage MediaKind = "image" MediaVideo MediaKind = "video" SourceGenerated MediaSource = "generated" SourceUploaded MediaSource = "uploaded" // SourceCover reuses the reel's cover art as the card's overlay panel (path // "cover.png", workspace-relative) — the natural fill for opening/closing // cards, usable on any card. Theme staleness is tracked on the cover itself, // not per card. SourceCover MediaSource = "cover" )
const ( Width = 1080 Height = 1920 FPS = 30 )
Geometry / timing defaults (parity with gen-reel.py).
const DefaultAspect = "9:16"
DefaultAspect is the reel's default frame shape (spec 0001 §4 — the 1080×1920 vertical). A workspace may override it per reel (workspace.Meta.Aspect, 0029 §9 note 27); the empty string always means this default, so existing reels render pixel-identically.
Variables ¶
This section is empty.
Functions ¶
func AccentWords ¶
AccentWords returns the set of words marked for the accent colour. A word is accented when its token carries a '*' marker. Faithful to gen-reel.py: in a multi-word emphasis like "*no one safer*" only the first and last tokens carry a marker, so the middle word is not accented (a known parity quirk).
func AspectDims ¶ added in v0.9.0
AspectDims resolves an "W:H" aspect keyword to output pixel dimensions. The short side is always 1080; the long side scales and rounds to even (codec requirement). "" resolves to the 9:16 default.
func CardDurationFromVO ¶
CardDurationFromVO returns a card's on-screen duration given its narration clip length: VO + lead + tail (spec §3.1).
func CleanWord ¶
CleanWord normalises a word for accent comparison: drop any '*' markers and surrounding punctuation.
func CountMarkers ¶
CountMarkers returns the number of '*' accent markers in the text (used by validation to require balanced markers).
func FormatIssues ¶
FormatIssues renders a list of issues as an indented, one-per-line block — the shared form the CLI build/make error messages use.
func Marshal ¶
func Marshal(sb Storyboard) ([]byte, error)
Marshal serialises a storyboard to indented JSON (for drafts / round-trips).
func MediaIgnored ¶ added in v0.9.0
MediaIgnored reports whether a card carries an illustration its effective mode will not draw — a block card with media. That is incoherent rather than wrong, so callers warn (0044 D2): erroring would reject boards that only reached this state because picking an illustration never set the mode.
func Narration ¶
Narration is a card's spoken line: its explicit VO, else the accent-stripped on-screen text. The canonical definition shared by VO generation (the timing driver) and any UI that needs "what would this card say".
The fallback reads BLOCKS, not Text (spec 0038): a card may carry its words in texts[], and reading Text alone would yield an empty narration — no VO, and since VO drives duration, a card with no time on screen. Blocks are joined in render order, because they are one spoken line however they are laid out.
func StripAccents ¶
StripAccents removes the *accent* markers, yielding the text as rendered.
func ValidScrimDirection ¶ added in v0.10.0
ValidScrimDirection reports whether d is a direction the renderer understands. Empty is valid and means the default.
func Wrap ¶
Wrap breaks text into display lines no wider than maxWidth, preserving explicit paragraph breaks ("\n") and applying orphan control: never leave a lone trailing word — pull one word down from the previous line. Faithful port of gen-reel.py's wrap(). The caller is expected to have stripped accent markers (see StripAccents) before measuring.
Types ¶
type Align ¶ added in v0.10.0
type Align string
Align is how a block's text sits inside its box — NOT where the box goes (D7). Changing it therefore never moves the block.
const ( // AlignCentre centres each line in the box — the default, and what every // pre-0038 card does. AlignCentre Align = "centre" // AlignLeft ranges each line against the box's left edge. AlignLeft Align = "left" // AlignRight ranges each line against the box's right edge. AlignRight Align = "right" )
type Anchor ¶ added in v0.10.0
type Anchor struct {
X float64 `json:"x" mapstructure:"x" yaml:"x"`
Y float64 `json:"y" mapstructure:"y" yaml:"y"`
}
Anchor is a position in the frame as a fraction of its width and height, with (0,0) top-left and (1,1) bottom-right. It is the block box's CENTRE on both axes (D5, D7) — so alignment never moves a block, it only moves the text inside it.
Frame fractions rather than pixels because a board must survive a change of output size: the same reel renders at 1080×1920 today and could render larger tomorrow, and a pixel anchor would drift. Fractions also make dragging in the studio the same arithmetic as rendering (D1).
type Card ¶
type Card struct {
// Text is omitted when empty so a card that carries its words in Texts does
// not also write `"text": ""`, which reads like a field someone forgot to
// fill in. Every pre-0038 board has a non-empty Text, so nothing existing
// serialises differently.
Text string `json:"text,omitempty" yaml:"text,omitempty"`
// Lead / Tail override the theme's padding for this scene — the "air" before
// and after its narration, in seconds (spec 0050 D4). An override REPLACES
// rather than adds: lead 1.2 means 1.2s, not 1.2s on top of the theme's.
//
// Pointers, because zero is a real value: `lead: 0` means this beat starts
// speaking immediately, which a plain float64 could not distinguish from
// "unset".
Lead *float64 `json:"lead,omitempty" yaml:"lead,omitempty"`
Tail *float64 `json:"tail,omitempty" yaml:"tail,omitempty"`
// Scrim overrides the theme's scrim for this card (spec 0050 D6) — for a
// scene whose illustration fights the text. Absent means the theme's.
Scrim *Scrim `json:"scrim,omitempty" yaml:"scrim,omitempty"`
// Texts supersedes Text when present (spec 0038): several positioned blocks
// on one scene. Absent means the single Text laid out by the mode default,
// so every pre-0038 board renders identically — see Card.Blocks, which is the
// only place that choice is made.
Texts []TextBlock `json:"texts,omitempty" yaml:"texts,omitempty"`
VO string `json:"vo,omitempty" yaml:"vo,omitempty"`
Bg string `json:"bg,omitempty" yaml:"bg,omitempty"`
Fg string `json:"fg,omitempty" yaml:"fg,omitempty"`
Accent string `json:"accent,omitempty" yaml:"accent,omitempty"`
// Dur is the fallback on-screen duration (seconds) used only when no VO
// drives timing.
Dur float64 `json:"dur,omitempty" yaml:"dur,omitempty"`
// Cover is RETIRED (spec 0050 D1) and renders nothing. The block-mode bookend
// scaled the reel's cover art into a box above the text, throwing most of it
// away; an overlay card whose media is the cover shows it full bleed instead.
//
// The field survives so `storyboard migrate` can still recognise and convert a
// board written before that change. Removing it would make those boards
// unmigratable — the flag would be dropped silently on the next save, with
// nothing left for the migration to find. It is omitempty, so a migrated board
// carries no trace of it.
Cover bool `json:"cover,omitempty" yaml:"cover,omitempty"`
Mono bool `json:"mono,omitempty" yaml:"mono,omitempty"`
// Overlay-mode fields.
Mode CardMode `json:"mode,omitempty" yaml:"mode,omitempty"`
Scene string `json:"scene,omitempty" yaml:"scene,omitempty"`
Media *Media `json:"media,omitempty" yaml:"media,omitempty"`
// Avatars is the subset of the reel's cast that appears in this scene (spec 0034
// D4) — a checklist, not a picker. Empty/absent = NOBODY: a scene with no ticks
// is a scene with no one in it, and renders as plain text-to-image.
Avatars []string `json:"avatars,omitempty" yaml:"avatars,omitempty"`
// Voice is an optional per-card voice override.
Voice *VoiceOverride `json:"voice,omitempty" yaml:"voice,omitempty"`
}
Card is one storyboard card. The on-screen `Text` (a tight distillation) and the `VO` narration are distinct inputs: VO may be fuller and carry provider control tags (SSML <break>, phonetic spellings). Palette-role applicability is mode-dependent (block uses Bg/Fg/Accent; overlay ignores Bg).
func (Card) Blocks ¶ added in v0.10.0
Blocks returns the card's text blocks in render order.
This is the compatibility seam, and the only place that decides it: a card with Texts uses them; a card without gets exactly one unpositioned block from Text, which the renderer lays out by its mode default. Every pre-0038 board therefore renders unchanged (D4).
type CardMode ¶
type CardMode string
CardMode is a card's visual treatment.
func RenderMode ¶ added in v0.9.0
RenderMode returns the mode a card actually renders in, and whether it was degraded for want of an illustration.
Precedence: the card's own mode wins, then the theme's default, then block. An overlay card with no media degrades to block rather than failing — that is the normal state of a board mid-authoring, and honouring a theme's overlay default without this would turn every un-illustrated card into a build error.
type Measurer ¶
Measurer reports the rendered width of a string in a given font. The renderer supplies a font-metrics measurer; tests supply a deterministic one. Keeping width measurement injected is what makes wrapping pure and unit-testable.
type Media ¶
type Media struct {
Kind MediaKind `json:"kind" yaml:"kind"`
Source MediaSource `json:"source" yaml:"source"`
Path string `json:"path" yaml:"path"`
// Theme records the reel theme a *generated* illustration was made under, so the
// editor can flag "this media predates a theme change — re-roll?" (R-UI-22).
// Empty for uploaded media (theme-independent) and pre-existing storyboards.
Theme string `json:"theme,omitempty" yaml:"theme,omitempty"`
}
Media is a resolved card media panel (overlay mode).
type MediaKind ¶
type MediaKind string
MediaKind / MediaSource describe a resolved card media panel.
type MediaSource ¶
type MediaSource string
MediaKind / MediaSource describe a resolved card media panel.
type Migration ¶ added in v0.10.0
type Migration struct {
// Name identifies the migration in output and in tests.
Name string
// Why is the one-line reason, shown to whoever runs it — a migration nobody
// can explain is a migration nobody trusts.
Why string
// Apply rewrites one card in place, returning true if it changed anything.
Apply func(*Card) bool
}
Migration is one named rewrite. Apply reports whether it changed the card.
func Migrations ¶ added in v0.10.0
func Migrations() []Migration
Migrations are every pending rewrite, in the order they must run.
type MigrationResult ¶ added in v0.10.0
type MigrationResult struct {
// Name is the migration's.
Name string
// Cards are the 1-based indices it changed.
Cards []int
}
MigrationResult records what a migration did to a board.
func Migrate ¶ added in v0.10.0
func Migrate(sb Storyboard) []MigrationResult
Migrate applies every pending migration to sb IN PLACE and reports what each one changed. A board already on the current schema comes back untouched with an empty result, so running it twice is safe.
func (MigrationResult) Changed ¶ added in v0.10.0
func (r MigrationResult) Changed() bool
Changed reports whether anything was rewritten.
type Params ¶
Params are the timing knobs. Zero values are replaced by the parity defaults via WithDefaults, so the zero Params behaves like the Python script.
func (Params) WithDefaults ¶
WithDefaults fills any zero field from the parity defaults.
type Result ¶
Result is the outcome of validating a storyboard: MUST failures are Errors, SHOULD findings are Warnings. The same set backs the CLI exit 2, the API 422, and the studio inline validation (0002 §3.1).
func Validate ¶
func Validate(sb Storyboard, palette map[string]bool) Result
Validate checks a storyboard's semantic rules (R-WS-9..13). palette is the set of defined palette role keys from the resolved theme; when empty, palette-role checks (R-WS-12) are skipped (no theme to check against). R-WS-14 (cover cards reference an available cover) is a render-time check, not a load-time one.
type Scrim ¶ added in v0.10.0
type Scrim struct {
From *float64 `json:"from,omitempty" mapstructure:"from" yaml:"from,omitempty"`
Color string `json:"color,omitempty" mapstructure:"color" yaml:"color,omitempty"`
Opacity *float64 `json:"opacity,omitempty" mapstructure:"opacity" yaml:"opacity,omitempty"`
Direction string `json:"direction,omitempty" mapstructure:"direction" yaml:"direction,omitempty"`
}
Scrim overrides part of the theme's scrim for one card. Every field is optional; an unset field keeps the theme's value.
The float fields are POINTERS because zero is a meaningful value for both — `from: 0` is a scrim over the whole frame, and `opacity: 0` turns it off. A plain float64 could not tell "off" from "unset".
type Storyboard ¶
type Storyboard []Card
Storyboard is the ordered set of cards (the storyboard.json is a bare array, parity with gen-reel.py).
func Parse ¶
func Parse(data []byte) (Storyboard, error)
Parse decodes a storyboard.json (a JSON array of cards), rejecting malformed JSON with an actionable error. Semantic validation is Validate's job.
type TextBlock ¶ added in v0.10.0
type TextBlock struct {
Text string `json:"text" mapstructure:"text" yaml:"text"`
// Pos anchors the block in the frame. Absent (nil) means the renderer's
// mode-default placement, so a block can be added without positioning it.
Pos *Anchor `json:"pos,omitempty" mapstructure:"pos" yaml:"pos,omitempty"`
// Align is horizontal alignment about Pos; empty means centre.
Align Align `json:"align,omitempty" mapstructure:"align" yaml:"align,omitempty"`
// Size scales the theme's base text size — 1.4 is forty percent larger.
// Zero means the theme's size unchanged.
Size float64 `json:"size,omitempty" mapstructure:"size" yaml:"size,omitempty"`
// Width is the block's box width as a fraction of the frame width. Zero means
// the mode's default (the old margin-derived width).
//
// Explicit rather than derived from Pos (D7). Deriving it — the original D6 —
// meant a block changed shape as it moved, so the thing under the cursor kept
// re-wrapping while being dragged. A box you set and then place is both the
// familiar model and a stable one.
Width float64 `json:"width,omitempty" mapstructure:"width" yaml:"width,omitempty"`
// Accent is the palette role for this block's *marked* words, overriding the
// card's. A highlight belongs to the words it highlights (D13) — one scene can
// carry a headline marked in amber and a footnote marked in cream.
Accent string `json:"accent,omitempty" mapstructure:"accent" yaml:"accent,omitempty"`
// Role names a PALETTE ROLE ("amber", "cream"), never a hex value (D2).
// A colour written into a board would not follow a theme change, which is
// the thing themes exist to make possible. Empty means the theme's
// foreground.
Role string `json:"role,omitempty" mapstructure:"role" yaml:"role,omitempty"`
}
TextBlock is one positioned run of text on a card.
func (TextBlock) AccentOr ¶ added in v0.10.0
AccentOr returns the block's accent role, falling back to the card's.
Resolution is theme → card → block: a card-level accent still applies to every block that declares none, so boards written before per-block accents render unchanged.
func (TextBlock) AlignOr ¶ added in v0.10.0
AlignOr returns the block's alignment, defaulting to centre.
func (TextBlock) Positioned ¶ added in v0.10.0
Positioned reports whether a block carries an explicit anchor.
func (TextBlock) SizeOr ¶ added in v0.10.0
SizeOr returns the block's size scale, or base when it declares none.
func (TextBlock) WrapFraction ¶ added in v0.10.0
WrapFraction is the block box's width as a fraction of the frame width (D7).
marginFrac is the mode's TOTAL side allowance, so a block that declares no width of its own gets the mode default and behaves exactly as it always has.
This is deliberately pure arithmetic with no font metrics in it, because it is the one part of layout the studio preview can reproduce EXACTLY. Everything else about the preview is an approximation (browser fonts, no ffmpeg); this is not, and layout-cases.json pins the Go and JS implementations to each other.
Superseded D6, which derived this from Pos. See D7 for why that had to go.
type Timeline ¶
type Timeline struct {
// Durations is the per-card on-screen duration.
Durations []float64
// Starts[i] is card i's start time on the assembled timeline (xfade-adjusted):
// sum(durs[:i]) - i*xfade.
Starts []float64
// VODelaysMS[i] is the millisecond delay at which card i's VO is placed
// (start + lead), clamped at zero.
VODelaysMS []int
// XFadeOffsets[k] is the ffmpeg xfade offset for the transition into card
// k+1 (len = n-1); equal to Starts[k+1].
XFadeOffsets []float64
// Total is the finished reel length: sum(durs) - (n-1)*xfade.
Total float64
}
Timeline is the computed reel timeline.
func BuildTimeline ¶
BuildTimeline computes the timeline from per-card on-screen durations. Faithful port of gen-reel.py's offset/starts/total maths.
leads, when non-empty, is each card's own lead — the delay from the card appearing to its narration starting (spec 0050 D4). A shorter or absent slice falls back to p.Lead, which is every pre-0050 board.
type VoiceOverride ¶
type VoiceOverride struct {
// Speaker selects a registered voice (config voices.<name>) as this line's
// base voice, overriding the reel theme's voice (multi-author narration).
Speaker string `json:"speaker,omitempty" yaml:"speaker,omitempty"`
Stability float64 `json:"stability,omitempty" yaml:"stability,omitempty"`
Similarity float64 `json:"similarity,omitempty" yaml:"similarity,omitempty"`
Style float64 `json:"style,omitempty" yaml:"style,omitempty"`
Speed float64 `json:"speed,omitempty" yaml:"speed,omitempty"`
// Model selects the TTS model for this line only (empty ⇒ theme/default). Pronounce
// is a respelling used as this line's narration on any model; IPA is a phonetic
// rendering used instead on a phoneme-capable model (ignored on multilingual_v2).
// Spec 0022 H/I.
Model string `json:"model,omitempty" yaml:"model,omitempty"`
Pronounce string `json:"pronounce,omitempty" yaml:"pronounce,omitempty"`
IPA string `json:"ipa,omitempty" yaml:"ipa,omitempty"`
}
VoiceOverride lets a single line tune its narration voice (e.g. steadying a wobbly line by bumping stability, or slowing a line that reads too fast), or select a different speaker for multi-author reels.