/** * USV packer core — turns corresponded per-frame splat arrays (or an STG-trained coefficient * set) into the USV v1 file tree: `manifest.json` + `static.usc` + `tracks/clip.ust` + * `windows/N.usw`. Pure JavaScript (typed arrays in, `Map` out): the Node * CLI (`tools/splat-video-export.mjs`) adds file I/O and PLY parsing on top. This module is * authoring-only and intentionally remains outside every browser-facing package facade. * * Every pack ends with a built-in ROUND-TRIP GATE: the emitted artifacts are decoded with the * runtime's own codecs and re-evaluated with the CPU twin (`SpacetimeMotion.js`) at every * frame, producing a report of reconstruction errors + the size ledger — the format's * compression claims are measured per clip, never assumed. * * Format specification: packages/core/.ai/SPLAT_VIDEO_FORMAT.md (§9 packer contract). * * @module SplatVideoPacker */ import type { SplatVideoKeyframeTrack, SplatVideoSTGTrack, SplatVideoTracksManifest } from './SplatVideoManifest.js'; /** One corresponded, expanded Gaussian-splat frame accepted by the keyframe packer. */ export interface SplatVideoPackerFrame { positions: Float32Array; scales: Float32Array; rotations: Float32Array; colors: Float32Array; count: number; } /** Validated dimensions shared by every frame in a corresponded sequence. */ export interface SplatVideoSequenceAnalysis { count: number; frameCount: number; } /** Motion thresholds used for static/dynamic partitioning and lifespan fitting. */ export interface SplatVideoPartitionOptions { positionEpsilon?: number | undefined; rotationEpsilon?: number | undefined; alphaEpsilon?: number | undefined; } /** Stable output-to-input order and the resulting partition sizes. */ export interface SplatVideoPartitionResult { permutation: Uint32Array; staticCount: number; dynamicCount: number; } /** Flat-top lifespan tracks expressed in source-frame units. */ export interface SplatVideoDetectedLifespans { birth: Float32Array; death: Float32Array; ramp: Float32Array; } /** Lifespan analysis plus the plateau color source selected for every dynamic splat. */ export interface SplatVideoLifespanResult { lifespans: SplatVideoDetectedLifespans | null; baseAlphas: Float32Array; baseColorFrames: Uint32Array; } /** Controls keyframe sampling, partition thresholds, and emitted manifest metadata. */ export interface SplatVideoPackerOptions extends SplatVideoPartitionOptions { frameRate?: number | undefined; windowFrames?: number | undefined; knotStride?: number | undefined; generator?: string | undefined; rotation?: 'auto' | boolean | undefined; } /** Analytic Spacetime-Gaussian coefficient set accepted by {@link packSTGClip}. */ export interface SplatVideoSTGInput extends SplatVideoPackerFrame { motion: Float32Array; omega: Float32Array; trbfCenter: Float32Array; trbfScaleLog: Float32Array; } /** Controls normalized-time conversion and STG static/dynamic partitioning. */ export interface SplatVideoSTGPackerOptions { duration?: number | undefined; frameRate?: number | undefined; generator?: string | undefined; motionEpsilon?: number | undefined; /** * 'high' (DEFAULT) widens the base to 16-bit-per-axis positions and the STG records to * f32 (~2.4× file size) — measured transport-transparent at room scale (bench-n3dv: * 30.73 dB vs the 30.75 full-float ceiling). 'compact' halves the file for object-scale * assets where 11-bit axes are already below quantization visibility. */ precision?: 'compact' | 'high' | undefined; } /** Emitted file tree shared by both packer modes. */ export type SplatVideoPackedFiles = Map; /** Normalized keyframe-tracks manifest returned by the corresponded-frame packer. */ export type SplatVideoKeyframeManifest = SplatVideoTracksManifest & { tracks: SplatVideoKeyframeTrack; }; /** Normalized STG-tracks manifest returned by the analytic coefficient packer. */ export type SplatVideoSTGManifest = SplatVideoTracksManifest & { tracks: SplatVideoSTGTrack; }; /** Per-file and aggregate size facts emitted by the keyframe round-trip gate. */ export interface SplatVideoRoundTripReport { reconstruction: { positionMax: number; positionRmse: number; positionQuantizationBound: number; rotationMax: number; alphaMax: number; staticDriftMax: number; }; sizes: { files: Record; totalBytes: number; temporalBytes: number; bytesPerSplat: number; temporalBytesPerDynamicSplatPerFrame: number; temporalBytesPerDynamicSplatPerSecond: number; naive: { perFrameUscBytes: number; perFrameSogEstimateBytes: number; ratioVersusPerFrameUsc: number; ratioVersusPerFrameSogEstimate: number; }; }; facts: { count: number; staticCount: number; dynamicCount: number; frameCount: number; windows: number; hasLifespans: boolean; }; } /** Round-trip and size facts emitted for analytic STG records. */ export interface SplatVideoSTGRoundTripReport { reconstruction: { positionMax: number; note: string; }; sizes: { files: Record; totalBytes: number; bytesPerSplat: number; temporalBytesPerDynamicSplat: number; }; facts: { count: number; staticCount: number; dynamicCount: number; frameCount: number; duration: number; kind: 'stg'; }; } interface SplatVideoPackResultBase { files: SplatVideoPackedFiles; manifest: TManifest; report: TReport; permutation: Uint32Array; staticCount: number; dynamicCount: number; } /** Complete result of {@link packSplatVideo}. */ export type SplatVideoPackResult = SplatVideoPackResultBase; /** Complete result of {@link packSTGClip}. */ export type SplatVideoSTGPackResult = SplatVideoPackResultBase; /** * Validate a corresponded frame sequence: equal counts (index identity is the v1 * correspondence model) and finite attribute arrays of the expected shapes. * * @param {Object[]} frames - Per-frame `{ positions, scales, rotations, colors, count }`. * @returns {{ count:number, frameCount:number }} */ export declare function analyzeSequenceFrames(frames: readonly SplatVideoPackerFrame[]): SplatVideoSequenceAnalysis; /** * Split splats into `[static prefix | dynamic tail]` by motion energy across frames. A splat * is dynamic when its position, rotation, or opacity moves beyond the epsilons over the clip * (opacity-only animation counts — alpha changes cross the contribution cull). The returned * permutation is stable (relative Morton order preserved inside each part). * * @param {Object[]} frames - Corresponded frames (any consistent order). * @param {Object} [options] - { positionEpsilon, rotationEpsilon, alphaEpsilon }. * @returns {{ permutation:Uint32Array, staticCount:number, dynamicCount:number }} */ export declare function partitionSplats(frames: readonly SplatVideoPackerFrame[], options?: SplatVideoPartitionOptions): SplatVideoPartitionResult; /** * Fit flat-top lifespans from the tail splats' opacity envelopes. Base alpha becomes the * envelope's PLATEAU (max) — a splat born mid-clip must not inherit its frame-0 (near-zero) * alpha as the base. Returns null when no tail splat's alpha varies. * * @param {Object[]} frames - Permuted frames. * @param {number} staticCount - Prefix size. * @param {Object} [options] - { alphaEpsilon }. * @returns {{ lifespans:{birth:Float32Array, death:Float32Array, ramp:Float32Array}|null, * baseAlphas:Float32Array, baseColorFrames:Uint32Array }} Tail-indexed arrays. */ export declare function detectLifespans(frames: readonly SplatVideoPackerFrame[], staticCount: number, options?: Pick): SplatVideoLifespanResult; /** * Pack a corresponded frame sequence into the USV `tracks` (keyframes) layout, then run the * round-trip gate against the emitted artifacts. * * @param {Object[]} inputFrames - Per-frame `{ positions, scales, rotations, colors, count }` * in a SHARED order (splat i = same Gaussian in every frame; the packer re-orders internally). * @param {Object} [options] - { frameRate=30, windowFrames=50, knotStride=10, generator, * positionEpsilon, rotationEpsilon, alphaEpsilon, rotation:'auto'|true|false }. * @returns {{ files:Map, manifest:Object, report:Object, permutation:Uint32Array, * staticCount:number, dynamicCount:number }} */ export declare function packSplatVideo(inputFrames: readonly SplatVideoPackerFrame[], options?: SplatVideoPackerOptions): SplatVideoPackResult; /** * Pack an STG-trained coefficient set (Spacetime Gaussians / splaTV lineage) into the USV * `stg` track kind — analytic conversion, no fitting. Input coefficients use the training * clip's NORMALIZED time (t ∈ [0, 1]); `duration` maps them onto seconds (spec §6): * m1/dur, m2/dur², m3/dur³, ω/dur, trbfCenter·dur, exp(trbfScaleLog)·dur. * * @param {Object} stg - { positions, scales(log), rotations, colors(straight rgba), count, * motion(9/splat), omega(4/splat), trbfCenter(normalized), trbfScaleLog(log-space) }. * @param {Object} [options] - { duration=1 (seconds), frameRate=30, generator, * motionEpsilon=1e-6 (partition threshold on converted coefficients) }. * @returns {{ files:Map, manifest:Object, report:Object, permutation:Uint32Array, * staticCount:number, dynamicCount:number }} */ export declare function packSTGClip(stg: SplatVideoSTGInput, options?: SplatVideoSTGPackerOptions): SplatVideoSTGPackResult; export {};