# wity-scene — API Quick Reference Pure API surface. No prose. Signatures, types, constants, and schema attributes. Version: scene-core v1.0.0 · scene-headless v1.0.0 · scene-player v1.0.0 · scene-to-video v1.0.0 · scene-compose v1.0.0 · scene-to-pdf v1.0.0 · scene-to-pptx v1.0.0 --- ## IMPORT PATHS ```js // Core — parse/evaluate/serialize/validate import { parse, evaluate, serialize, validate, resolveUnit, SCHEMA_VERSION, ANIMATE_IN_VALUES, ANIMATE_OUT_VALUES, ANCHOR_VALUES, ELEMENT_TAGS, MEDIA_FIT_VALUES, } from '@wity/scene-core'; // Headless player — drives evaluate() at playback rate import { HeadlessPlayer } from '@wity/scene-player'; // Server-side: graphics compiler (Node.js / Lambda only) // Renders ws-rect/ws-text/ws-image → silent MP4. ws-video/ws-audio ignored. import { compile } from '@wity/scene-to-video'; // Server-side: full compositor (Node.js / Lambda only) // Blends ws-video + ws-audio + graphics overlay → final MP4 via FFmpeg filter_complex import { compose } from '@wity/scene-compose'; // Convenience re-export of scene-core import { parse, evaluate, serialize, validate } from '@wity/scene'; ``` --- ## Rendering a scene `witySceneRender` is the single entry point for all scene rendering. Send your scene XML — routing to the correct internal pipeline is automatic. There are no other services to call. ### Input | Field | Type | Default | Description | |-------|------|---------|-------------| | `sceneXml` | string | required | The `` XML document | | `outputFormat` | string | `"mp4"` | Output format: `"mp4"`, `"pdf"`, `"pptx"` | | `options.fps` | number | `30` | Frame rate (mp4 only) | | `options.sceneWidth` | number | `1280` | Canvas width px (mp4 only) | | `options.sceneHeight` | number | `720` | Canvas height px (mp4 only) | | `options.fontManifest` | object | `{}` | Custom fonts: `{ "FontName": "https://url/to/font.ttf" }` | | `options.ts` | number | `0` | Snapshot timestamp in seconds (pdf/pptx) | | `options.timestamps` | number[] | — | Multi-page/slide timestamps (pdf/pptx); overrides `ts` | | `options.variant` | string | `"standard"` | `"standard"` or `"print"` (pdf only) | | `options.dpi` | number | 96/300 | Render DPI override (pdf only) | ```json { "sceneXml": "...", "outputFormat": "mp4", "options": { "fps": 30, "sceneWidth": 1920, "sceneHeight": 1080, "fontManifest": {} } } ``` ```json { "sceneXml": "...", "outputFormat": "pdf", "options": { "ts": 2.0, "variant": "print", "dpi": 300 } } ``` ```json { "sceneXml": "...", "outputFormat": "pptx", "options": { "timestamps": [0, 5.0, 10.0] } } ``` ### Output | Field | Type | Description | |-------|------|-------------| | `url` | string | Public S3 URL of the rendered output | | `fileSize` | number | Output file size in bytes | | `pipeline` | string[] | Which internal Lambdas ran (for observability) | | `pages` | number | Number of pages (pdf only) | | `slides` | number | Number of slides (pptx only) | ```json { "url": "https://...", "fileSize": 9876543, "pipeline": ["witySceneToVideo", "witySceneCompose"] } ``` ### Routing (automatic) **mp4:** Gateway inspects element tags and picks minimal pipeline: - Graphics only (`ws-rect`/`ws-text`/`ws-image`) → `witySceneToVideo` only - Mixed (graphics + `ws-video`/`ws-audio`) → `witySceneToVideo` → `witySceneCompose` - Media only (`ws-video`/`ws-audio`) → `witySceneCompose` only **pdf:** → `witySceneToPdf` (single call). **pptx:** → `witySceneToPptx` (single call). --- ## Analyzing a scene — wityAudioProfile Standalone analysis Lambda. **Not routed through `witySceneRender`** — call directly. ### Input | Field | Type | Default | Description | |-------|------|---------|-------------| | `sceneXml` | string | required | The `` XML document | | `options.windowMs` | number | `100` | RMS window size in milliseconds | | `options.sampleRate` | number | `16000` | PCM decode sample rate (Hz) | ```json { "sceneXml": "...", "options": { "windowMs": 100, "sampleRate": 16000 } } ``` ### Output — AudioProfileResult | Field | Type | Description | |-------|------|-------------| | `sceneDuration` | number | Resolved scene duration (seconds) | | `windowMs` | number | Window size used | | `sampleRate` | number | Sample rate used | | `elements` | ElementProfile[] | Per-element loudness data | | `mixed` | LoudnessProfile | Power-sum mix across all elements | ```ts ElementProfile: { id, tag: 'ws-video'|'ws-audio', src, begin, dur, volume, hasAudio: boolean, // false if muted or no audio track profile: LoudnessProfile | null, // null if hasAudio false or decode failed } LoudnessProfile: { startTime: number, // scene timeline offset of window[0] (seconds) windowCount: number, windowMs: number, rmsLinear: number[], // volume-adjusted RMS per window, 0.0–1.0 rmsDbfs: number[], // 20*log10(rmsLinear); -Infinity for silence peakDbfs: number, avgDbfs: number, } ``` --- ## compile(sceneXml, fontManifest, options) [@wity/scene-to-video] ```js compile( sceneXml: string, fontManifest: Record, // { 'FontName': 'https://url/to/font.ttf' } options: { fps?: number } // default fps=30 ): Promise<{ videoPath: string, cleanup: () => Promise }> ``` - Renders graphic elements only: ws-rect, ws-text, ws-image - ws-video and ws-audio elements are silently ignored - `videoPath` is a local /tmp path — upload to S3, then call cleanup() - Requires FFmpeg in PATH or FFMPEG_PATH env var - Node.js / Lambda only (uses node-canvas, not browser-safe) Lambda (`witySceneToVideo`) input: `{ sceneXml, fontManifest?, fps? }` Lambda response: `{ url: string, fileSize: number }` --- ## compose(sceneXml, graphicsMp4Url, options) [@wity/scene-compose] ```js compose( sceneXml: string, graphicsMp4Url: string | null, options: { outputBucket?: string, // S3 bucket (or OUTPUT_BUCKET env) outputPrefix?: string, // default "scene-composed/" fps?: number, // default 30 sceneWidth?: number, // default 1280 sceneHeight?: number, // default 720 } ): Promise<{ url: string, fileSize: number }> ``` - Composites ws-video clips + ws-audio tracks + optional graphics overlay - `graphicsMp4Url`: URL of graphics MP4 from `compile()` / witySceneToVideo; pass null if no graphics - Downloads all media in parallel, runs FFmpeg filter_complex, streams output to S3 - Audio pipeline: aresample=48000, adelay:all=1, volume, amix normalize=0 dropout_transition=0 - Video clips: scale/fit → setpts offset → overlay=x:y:enable='between(t,begin,end)' - Node.js / Lambda only - Requires FFmpeg in PATH or FFMPEG_PATH env var Lambda (`witySceneCompose`) input: `{ sceneXml, graphicsMp4Url?, fps?, sceneWidth?, sceneHeight? }` Lambda response: `{ url: string, fileSize: number }` ### Two-step pipeline ``` sceneXml → witySceneToVideo → graphicsMp4Url sceneXml + graphicsMp4Url → witySceneCompose → final MP4 url ``` Both Lambdas receive the same sceneXml. Step 2 re-parses to extract ws-video/ws-audio independently. --- ## HeadlessPlayer ```js // Construction const player = new HeadlessPlayer({ fps?: number }) // default fps=60 // Loading player.loadXml(xml: string): void // parse + load player.loadScene(scene: WityScene): void // load pre-parsed scene player.replaceXml(xml: string): void // hot-swap; retains play/pause state // Playback player.play(): void player.pause(): void player.stop(): void // pause + seek to 0 player.seek(t: number): void // emits 'frame' immediately // State (readonly) player.currentTime: number player.duration: number player.isPlaying: boolean player.isLoaded: boolean player.progress: number // 0–1 // Mutation — takes effect next frame (or immediately if paused) player.addLayer(data): string → layerId player.updateLayer(id, patch): void player.removeLayer(id): void player.addElement(layerId, data): string → elementId player.updateElement(id, patch): void player.removeElement(id): void // Serialization player.getXml(): string | null // serialize current (mutated) scene // Advanced player.getStore(): SceneStore | null // access authoring primitives // EventBus (inherited) player.on(event, handler): unsubFn player.once(event, handler): unsubFn player.off(event, handler): void player.emit(event, payload): void player.destroy(): void ``` ### HeadlessPlayer events | Event | Payload | |---|---| | `'frame'` | `{ frame: ComputedFrame, t: number }` | | `'playback:started'` | `{ t: number }` | | `'playback:paused'` | `{ t: number }` | | `'playback:stopped'` | `{}` | | `'playback:ended'` | `{ t: number }` | | `'time:changed'` | `{ t: number }` | | `'scene:loaded'` | `{ scene: WityScene }` | | `'scene:mutated'` | `{ scene: WityScene }` | --- ## parse(xml) ```js parse(xml: string): WityScene ``` - Throws if XML is malformed or root element is not `` - Resets internal element ID counter per call - Browser: uses native `DOMParser` - Node.js: requires `@xmldom/xmldom` peer dep --- ## evaluate(scene, t) ```js evaluate(scene: WityScene, t: number): ComputedFrame ``` - `t` clamped to `[0, scene.dur]` - Pure function — no mutation, no side effects - Elements sorted by effective z ascending (painters algorithm) - Effective z = `layer.z * 1000 + element.z` - `ws-audio` elements appear in `frame.elements` with temporal visibility but no pixel output - `ws-character` entities are NOT in `frame.elements` — access via `scene.cast` --- ## serialize(scene) ```js serialize(scene: WityScene): string ``` - Returns `\n` - Omits attributes equal to their defaults - Round-trips cleanly with `parse()` - `` section serialized before layers when `scene.cast` is non-empty --- ## validate(scene) ```js validate(scene: WityScene): { valid: boolean, errors: string[], warnings: string[] } ``` - Never throws — returns result object - Checks: root types, cast array, layer types, element types, unit values, opacity/volume range, enum membership, id uniqueness --- ## resolveUnit(value, containerSize) ```js resolveUnit(value: string | number, containerSize: number): number ``` ```js resolveUnit('50%', 1920) // → 960 resolveUnit('120px', 0) // → 120 resolveUnit(80, 0) // → 80 resolveUnit('80', 0) // → 80 ``` --- ## Constants ```js SCHEMA_VERSION // '1.0' ANIMATE_IN_VALUES // ['none', 'fade', 'fade-up', 'fade-down', 'slide-left', 'slide-right'] ANIMATE_OUT_VALUES // ['none', 'fade', 'fade-up', 'fade-down', 'slide-left', 'slide-right'] ANCHOR_VALUES // ['top-left', 'top', 'top-right', 'left', 'center', 'right', // 'bottom-left', 'bottom', 'bottom-right'] ELEMENT_TAGS // ['ws-text', 'ws-rect', 'ws-image', 'ws-video', 'ws-audio'] MEDIA_FIT_VALUES // ['cover', 'contain', 'fill', 'none'] ``` --- ## Types ### WityScene ```js { version: string, // '1.0' width: number, // canvas width px height: number, // canvas height px dur: number, // total duration seconds layers: WsLayer[], cast: WsCharacter[], // semantic entities (non-rendered) } ``` ### WsLayer ```js { id: string, z: number, // layer z-index opacity: number, // 0–1, multiplied with element opacity elements: WsElement[], } ``` ### WsElementBase (visual elements only) ```js { id: string, x: string | number, // unit value y: string | number, anchor: AnchorValue, begin: number, // start time seconds (default 0) dur: number, // duration seconds; Infinity = full scene z: number, opacity: number, // 0–1 animateIn: AnimateValue, animateOut: AnimateValue, animateDur: number, // seconds (default 0.4) animateEasing?: string, // custom cubic bezier: "x1,y1,x2,y2" name?: string, // optional human-readable display name } ``` ### WsKeyframe ```js { t: number, // element-relative time in seconds x?: string | number, // unit value y?: string | number, // unit value opacity?: number, // 0–1 easing?: string, // cubic bezier for segment from this keyframe: "x1,y1,x2,y2" } ``` ### WsText ```js WsElementBase & { tag: 'ws-text', content: string, fontSize: string | number, fontFamily: string, fontWeight: string, color: string, textAlign: 'left' | 'center' | 'right', lineHeight: number, maxWidth: string | number | null, letterSpacing: string | number, keyframes?: WsKeyframe[], } ``` ### WsRect ```js WsElementBase & { tag: 'ws-rect', width: string | number, height: string | number, fill: string, stroke: string | null, strokeWidth: number, rx: number, keyframes?: WsKeyframe[], } ``` ### WsImage ```js WsElementBase & { tag: 'ws-image', src: string, width: string | number, height: string | number, fit: 'cover' | 'contain' | 'fill' | 'none', keyframes?: WsKeyframe[], } ``` ### WsVideo ```js WsElementBase & { tag: 'ws-video', src: string, width: string | number, height: string | number, fit: 'cover' | 'contain' | 'fill' | 'none', volume: number, // 0–1 trimIn: number, // seconds into source trimOut: number | null, // seconds into source; null = no trim muted: boolean, cues?: WsCue[], // optional timed speech/subtitle cues } ``` ### WsAudio ```js { tag: 'ws-audio', id: string, begin: number, dur: number, src: string, volume: number, // 0–1 loop: boolean, trimIn: number, trimOut: number | null, name?: string, // optional human-readable display name cues?: WsCue[], // optional timed speech/subtitle cues } ``` ### WsCue (inside ws-video or ws-audio) Non-rendered metadata — consumed by analysis services, AI agents, accessibility tools. Timestamps relative to source media file. ```js { begin: number, // start time within source media (seconds) end: number, // end time within source media (seconds) text: string, // speech/subtitle text content speaker?: string, // optional ws-character id (links to scene.cast) } ``` ### WsCharacter ```js { id: string, // unique within scene.cast name: string, role?: string, description?: string, avatarUrl?: string, } ``` ### ComputedFrame ```js { t: number, width: number, height: number, elements: ComputedElement[], // sorted by z ascending } ``` ### ComputedElement ```js { id: string, tag: 'ws-text' | 'ws-rect' | 'ws-image' | 'ws-video' | 'ws-audio', x: number, // pixels; anchor-adjusted + animation translate offset y: number, opacity: number, // element.opacity × layer.opacity × animation opacity z: number, // layer.z * 1000 + element.z visible: boolean, // false outside [begin, begin+dur] props: object, // tag-specific resolved props (all units → px) content: string | null, // text content (ws-text only) } ``` ### ComputedElement.props by tag **ws-text** ```js { fontSize, fontFamily, fontWeight, color, textAlign, lineHeight, maxWidth, letterSpacing } ``` **ws-rect** ```js { width, height, fill, stroke, strokeWidth, rx } ``` **ws-image** ```js { src, width, height, fit } ``` **ws-video** ```js { src, width, height, fit, volume, trimIn, trimOut, muted } ``` **ws-audio** ```js { src, volume, loop, trimIn, trimOut } ``` --- ## XML Schema — attribute defaults ### `` | Attribute | Type | Required | |-----------|--------|----------| | version | string | yes — "1.0" | | width | number | yes | | height | number | yes | | dur | number | yes | ### `` | Attribute | Default | |-----------|---------| | id | required | | z | 0 | | opacity | 1 | ### Common element attributes (visual elements) | Attribute | Default | |------------------|------------| | id | auto-assigned | | x | 0 | | y | 0 | | anchor | top-left | | begin | 0 | | dur | Infinity | | z | 0 | | opacity | 1 | | animate-in | none | | animate-out | none | | animate-dur | 0.4 | | animate-easing | (none) — custom cubic bezier: "x1,y1,x2,y2"; overrides built-in preset easing | | name | (none) | ### `` attributes | Attribute | Default | |----------------|--------------| | font-size | 3% | | font-family | sans-serif | | font-weight | normal | | color | #ffffff | | text-align | center | | line-height | 1.4 | | max-width | (none) | | letter-spacing | 0 | ### `` attributes | Attribute | Default | |--------------|-------------| | width | 100% | | height | 100% | | fill | transparent | | stroke | (none) | | stroke-width | 1 | | rx | 0 | ### `` attributes | Attribute | Default | |-----------|---------| | src | required | | width | 100% | | height | 100% | | fit | cover | ### `` attributes | Attribute | Default | |-----------|---------| | src | required | | width | 100% | | height | 100% | | fit | cover | | volume | 1 | | trim-in | 0 | | trim-out | (none) | | muted | false | Both `` and `` may contain optional `` children (see below). ### `` attributes (inside ``, ``, ``) Optional children for animating position and/or opacity over element-relative time. Multiple keyframes are sorted by `t` and interpolated. | Attribute | Type | Required | Description | |-----------|--------|----------|-------------| | t | number | yes | Element-relative time in seconds (from element's own `begin`) | | x | unit | no | Horizontal position at this keyframe | | y | unit | no | Vertical position at this keyframe | | opacity | number | no | Opacity 0–1 at this keyframe | | easing | string | no | Cubic bezier for segment FROM this keyframe: `"x1,y1,x2,y2"` | Text content is the cue's text node. Only specified properties are animated; unspecified fall back to the element's static values. ### `` attributes | Attribute | Default | |-----------|----------| | id | auto | | begin | 0 | | dur | Infinity | | src | required | | volume | 1 | | loop | false | | trim-in | 0 | | trim-out | (none) | | name | (none) | ### `` attributes (inside `` or ``) Non-rendered metadata — timed speech/subtitle cues. Timestamps relative to source media file. | Attribute | Type | Required | Description | |-----------|--------|----------|-------------| | begin | number | yes | Start time within source media (seconds) | | end | number | yes | End time within source media (seconds) | | speaker | string | no | ws-character id (links to scene.cast) | Text content is the cue's text node. ### `` attributes (inside ``) | Attribute | Required | |-------------|----------| | id | yes | | name | yes | | role | no | | description | no | | avatar-url | no | --- ## Animation Slide animations (`slide-left`, `slide-right`, `fade-up`, `fade-down`) translate by **40px**. Default easing: `easeOutCubic` (entrance) / `easeInCubic` (exit). Override with `animate-easing="x1,y1,x2,y2"` (CSS cubic-bezier format; same easing applied to both in and out).