@melite/formats 0.0.0-stage → 0.1.4-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +557 -0
  2. package/lib/b64.d.ts +2 -0
  3. package/lib/b64.js +7 -0
  4. package/lib/bodyPresets.d.ts +40 -0
  5. package/lib/bodyPresets.js +77 -0
  6. package/lib/capabilityCards.d.ts +54 -0
  7. package/lib/capabilityCards.js +127 -0
  8. package/lib/captionSplitter.d.ts +49 -0
  9. package/lib/captionSplitter.js +172 -0
  10. package/lib/cardMutations.d.ts +52 -0
  11. package/lib/cardMutations.js +67 -0
  12. package/lib/downloads.d.ts +18 -0
  13. package/lib/downloads.js +64 -0
  14. package/lib/extractVideoFrame.d.ts +23 -0
  15. package/lib/extractVideoFrame.js +114 -0
  16. package/lib/glbLoad.d.ts +54 -0
  17. package/lib/glbLoad.js +96 -0
  18. package/lib/glbSkin.d.ts +14 -0
  19. package/lib/glbSkin.js +124 -0
  20. package/lib/handPose.d.ts +46 -0
  21. package/lib/handPose.js +122 -0
  22. package/lib/index.d.ts +26 -0
  23. package/lib/index.js +26 -0
  24. package/lib/keyframeExtract.d.ts +46 -0
  25. package/lib/keyframeExtract.js +89 -0
  26. package/lib/maskPrep.d.ts +88 -0
  27. package/lib/maskPrep.js +386 -0
  28. package/lib/motionConstraints.d.ts +237 -0
  29. package/lib/motionConstraints.js +369 -0
  30. package/lib/motionEnvelope.d.ts +56 -0
  31. package/lib/motionEnvelope.js +59 -0
  32. package/lib/motionNpz.d.ts +120 -0
  33. package/lib/motionNpz.js +497 -0
  34. package/lib/npz.d.ts +24 -0
  35. package/lib/npz.js +158 -0
  36. package/lib/openPoseCanvas.d.ts +17 -0
  37. package/lib/openPoseCanvas.js +90 -0
  38. package/lib/pngMeta.d.ts +42 -0
  39. package/lib/pngMeta.js +212 -0
  40. package/lib/pngMetaInfo.d.ts +14 -0
  41. package/lib/pngMetaInfo.js +127 -0
  42. package/lib/poseCodec.d.ts +20 -0
  43. package/lib/poseCodec.js +61 -0
  44. package/lib/poseTransforms.d.ts +73 -0
  45. package/lib/poseTransforms.js +237 -0
  46. package/lib/poserSkin.d.ts +75 -0
  47. package/lib/poserSkin.js +126 -0
  48. package/lib/prompt-tags.json +132 -0
  49. package/lib/promptTags.d.ts +42 -0
  50. package/lib/promptTags.js +116 -0
  51. package/lib/psdExport.d.ts +28 -0
  52. package/lib/psdExport.js +272 -0
  53. package/lib/readFile.d.ts +9 -0
  54. package/lib/readFile.js +16 -0
  55. package/lib/renderSnapshot.d.ts +34 -0
  56. package/lib/renderSnapshot.js +55 -0
  57. package/lib/somaSkin.d.ts +82 -0
  58. package/lib/somaSkin.js +267 -0
  59. package/lib/textToPose.d.ts +388 -0
  60. package/lib/textToPose.js +604 -0
  61. package/lib/uploadToEstate.d.ts +25 -0
  62. package/lib/uploadToEstate.js +97 -0
  63. package/package.json +148 -4
  64. package/README.md +0 -3
@@ -0,0 +1,77 @@
1
+ // Anny's 11 phenotype dims in order:
2
+ // [gender, age, muscle, weight, height, proportions, cupsize, firmness,
3
+ // african, asian, caucasian]
4
+ // gender: 0.0 = masculine, 1.0 = feminine (verified against the live mesh:
5
+ // gender=0 widens jaw and shoulders; gender=1 narrows them and widens hips).
6
+ // age/muscle/weight: 0.5 = average adult. height/proportions: 0.5 = standard.
7
+ // race dims are left at neutral 0.5 so race-specific features stay default.
8
+ const NEUTRAL_RACE = [0.5, 0.5, 0.5]; // african, asian, caucasian
9
+ /**
10
+ * Neutral - reset to the Anny default with no stylization.
11
+ *
12
+ * Provides a quick "back to neutral" affordance in the preset bar without
13
+ * the user having to hit the Reset button. Coeffs all 0.5 = Anny's neutral
14
+ * adult, no local changes, no custom target blends.
15
+ */
16
+ const NEUTRAL = {
17
+ id: "neutral",
18
+ name: "Neutral",
19
+ description: "Default Anny - no stylization, no morphs",
20
+ identityModel: "anny",
21
+ coeffs: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, ...NEUTRAL_RACE],
22
+ localChanges: null,
23
+ customTargets: null,
24
+ };
25
+ /**
26
+ * Male - basic masculine baseline with no stylization.
27
+ *
28
+ * Gender 0.0 (fully masculine → wider jaw, broader shoulders, narrower
29
+ * hips). Age/muscle/weight tuned to a healthy adult male build. No
30
+ * local_changes — this is a clean starting point the user can sculpt
31
+ * further. Height slightly above average.
32
+ */
33
+ const MALE = {
34
+ id: "male",
35
+ name: "Male",
36
+ description: "Basic masculine baseline - no stylization",
37
+ identityModel: "anny",
38
+ coeffs: [0.0, 0.45, 0.55, 0.50, 0.55, 0.50, 0.40, 0.55, ...NEUTRAL_RACE],
39
+ // gen age mus wt ht prop cup firm
40
+ localChanges: null,
41
+ customTargets: null,
42
+ };
43
+ /**
44
+ * Female - basic feminine baseline with no stylization.
45
+ *
46
+ * Gender 1.0 (fully feminine → narrower jaw, defined waist, wider hips).
47
+ * Age/muscle/weight tuned to a healthy adult female build. No
48
+ * local_changes — clean starting point for further sculpting.
49
+ */
50
+ const FEMALE = {
51
+ id: "female",
52
+ name: "Female",
53
+ description: "Basic feminine baseline - no stylization",
54
+ identityModel: "anny",
55
+ coeffs: [1.0, 0.40, 0.35, 0.38, 0.52, 0.52, 0.45, 0.55, ...NEUTRAL_RACE],
56
+ // gen age mus wt ht prop cup firm
57
+ localChanges: null,
58
+ customTargets: null,
59
+ };
60
+ export const BODY_PRESETS = [
61
+ NEUTRAL, MALE, FEMALE,
62
+ ];
63
+ /**
64
+ * Find a preset by id (returns undefined if not found).
65
+ *
66
+ * Used by tests and any future "load preset by URL hash" affordances.
67
+ */
68
+ export function getPresetById(id) {
69
+ return BODY_PRESETS.find((p) => p.id === id);
70
+ }
71
+ /**
72
+ * Default preset (the one applied to the very first Body Studio mount).
73
+ * Returns null so we DON'T clobber the user's saved edits on subsequent
74
+ * loads - the default is the un-morphed Anny.
75
+ */
76
+ export const DEFAULT_PRESET_ID = null;
77
+ //# sourceMappingURL=bodyPresets.js.map
@@ -0,0 +1,54 @@
1
+ import { type CapabilityEntry, type CapabilityRoute, type Catalog, type FamilyEntry } from "@melite/catalog";
2
+ import type { CardGenre } from "@melite/contracts";
3
+ /** A card ready to land on the canvas (what onPick hands the store).
4
+ *
5
+ * ``kind`` is optional — capabilities/families always produce
6
+ * ``"generation"`` (the default), so the existing helpers don't set it
7
+ * and the canvas defaults to "generation". The Input / Media Prep tile
8
+ * sets ``kind: "input"`` so the canvas routes the stage through the
9
+ * doc-driven input family (POST /v1/flows/start, 1-step inline
10
+ * doc) — NOT the generation wire. This is rule 4: the shape carries
11
+ * the kind, so pick → addStage never silently misroutes. */
12
+ export interface PickedCard {
13
+ service: string;
14
+ title: string;
15
+ genre?: CardGenre;
16
+ params: Record<string, unknown>;
17
+ kind?: string;
18
+ }
19
+ /**
20
+ * Seed params for a route: the backend-computed defaults + an empty prompt
21
+ * ONLY when the model's form actually serves a prompt field
22
+ * (prompt_in_form — backend-computed from the builder schema) + a null
23
+ * seed for EVERY input slot the model declares (input_slots — multi-slot
24
+ * forms like edit's three images / video's two frames get one null per
25
+ * slot, so the card renders all its squares from the start). Unknown
26
+ * prompt_in_form → seed (old behavior).
27
+ */
28
+ export declare function seededParams(cat: Catalog | null, route: string, base?: Record<string, unknown>): Record<string, unknown>;
29
+ /**
30
+ * Resolve a capability tile (+ optional route override) to a card. The
31
+ * default tile click uses cap.resolved; the tile's model switcher passes a
32
+ * different CapabilityRoute — same capability intent, model switched
33
+ * underneath. The route's params are backend-precomputed (capability
34
+ * default_params as fallback, model's own defaults winning), so switching
35
+ * the model correctly swaps the per-model recipe (steps / cfg / sampler).
36
+ */
37
+ export declare function capabilityToCard(cat: Catalog | null, cap: CapabilityEntry, route: CapabilityRoute): PickedCard;
38
+ /** Resolve a specific family entry (the "All models" list) to a card. */
39
+ export declare function familyEntryToCard(cat: Catalog | null, familyId: string, fam: FamilyEntry): PickedCard;
40
+ export declare const CAPABILITY_DRAG_MIME = "application/tech-noir-capability";
41
+ /** What a capability tile writes to the drag data store. Carries the fully
42
+ * resolved card (service + title + params + kind) so the drop target can
43
+ * apply it WITHOUT a catalog lookup — the sidebar already paid that cost
44
+ * via capabilityToCard. ``capId`` is diagnostics-only (not read by the
45
+ * drop target; useful for e2e traces). */
46
+ export type CapabilityDropPayload = PickedCard & {
47
+ capId?: string;
48
+ };
49
+ /** Serialize a picked card onto the drag data store. */
50
+ export declare function capabilityDragPayload(capId: string, card: PickedCard): string;
51
+ /** Parse the capability MIME off a drop event. null = absent or malformed
52
+ * (the drop target falls through to the asset/file path). */
53
+ export declare function parseCapabilityDrop(raw: string): CapabilityDropPayload | null;
54
+ //# sourceMappingURL=capabilityCards.d.ts.map
@@ -0,0 +1,127 @@
1
+ // capabilityCards — the ONE capability→card derivation path.
2
+ //
3
+ // A capability tile (Generate / Edit / Upscale / ...) resolves to a card
4
+ // descriptor: the backend-precomputed default model route + merged params
5
+ // (capabilities.<id>.resolved). This module is the single implementation —
6
+ // the ServicePicker dialog AND the Library's right sidebar (LibrarySidebar)
7
+ // both derive their "what lands on the canvas" from these helpers, so a
8
+ // catalog or schema change cannot make the two surfaces drift.
9
+ //
10
+
11
+ // • an empty prompt is seeded ONLY when the model's form actually serves
12
+ // a prompt field (prompt_in_form — backend-computed from the builder
13
+ // schema); a prompt-less card (upscale) must never carry one
14
+ // • a null input slot is seeded when the model's schema has an input
15
+ // (the card modal renders the attach control for it)
16
+ import { familyGenre, narrowGenre } from "@melite/catalog";
17
+ /**
18
+ * Seed params for a route: the backend-computed defaults + an empty prompt
19
+ * ONLY when the model's form actually serves a prompt field
20
+ * (prompt_in_form — backend-computed from the builder schema) + a null
21
+ * seed for EVERY input slot the model declares (input_slots — multi-slot
22
+ * forms like edit's three images / video's two frames get one null per
23
+ * slot, so the card renders all its squares from the start). Unknown
24
+ * prompt_in_form → seed (old behavior).
25
+ */
26
+ export function seededParams(cat, route, base = {}) {
27
+ const model = cat?.models[route];
28
+ const slots = model?.input_slots?.length
29
+ ? model.input_slots
30
+ : (model?.input ? [model.input] : []);
31
+ const hasPrompt = model?.prompt_in_form ?? true; // unknown → seed (old behavior)
32
+ return {
33
+ ...base,
34
+ ...(hasPrompt ? { prompt: "" } : {}),
35
+ ...(slots.length ? Object.fromEntries(slots.map((k) => [k, null])) : {}),
36
+ };
37
+ }
38
+ /**
39
+ * Resolve a capability tile (+ optional route override) to a card. The
40
+ * default tile click uses cap.resolved; the tile's model switcher passes a
41
+ * different CapabilityRoute — same capability intent, model switched
42
+ * underneath. The route's params are backend-precomputed (capability
43
+ * default_params as fallback, model's own defaults winning), so switching
44
+ * the model correctly swaps the per-model recipe (steps / cfg / sampler).
45
+ */
46
+ export function capabilityToCard(cat, cap, route) {
47
+ // Model-less capabilities (kind:"input") carry their params VERBATIM — no
48
+ // prompt/slot seeding. Those are generation-form concerns (prompt_in_form,
49
+ // input_slots); input's editor is the fixed-field InputCardEditor, which
50
+ // owns crop/scale/trim/volume/mask. Seeding a prompt here would give the
51
+
52
+ // regression, generalized). The kind rides through to PickedCard so the
53
+ // canvas routes the stage through executeInput, not executeGeneration.
54
+ const isModelLess = cap.kind !== undefined && cap.kind !== "generation";
55
+ return {
56
+ service: route.model,
57
+ title: cap.label,
58
+ genre: genreForRoute(cat, route.model),
59
+ params: isModelLess ? { ...route.params } : seededParams(cat, route.model, route.params),
60
+ ...(isModelLess ? { kind: cap.kind } : {}),
61
+ };
62
+ }
63
+ /** Resolve a specific family entry (the "All models" list) to a card. */
64
+ export function familyEntryToCard(cat, familyId, fam) {
65
+ // Route resolution — the SAME chain the backend's capability resolver
66
+ // and familiesForForm use: the catalog-declared ``default_model`` (when
67
+ // it is a member), else the member whose key equals the family id, else
68
+ // the FIRST member. NEVER the bare familyId: mask_studio's family id
69
+ // and model key differ (mask_studio vs mask-studio), and a family-id
70
+ // service is a PHANTOM ROUTE — no models[] entry, no familyGenre, and
71
+ // no studio registry match (the "mask studio does not appear" bug,
72
+
73
+ // card zoomed as a plain form card). The catalog now declares
74
+ // default_model for every id-divergent family; this chain is the
75
+ // backstop for the next one.
76
+ const declared = fam.default_model;
77
+ const members = Object.entries(cat?.models ?? {})
78
+ .filter(([, m]) => m.family === familyId)
79
+ .map(([k]) => k);
80
+ const route = (declared && members.includes(declared) ? declared : undefined) ??
81
+ members.find((k) => k === familyId) ??
82
+ members[0] ??
83
+ familyId;
84
+ return {
85
+ service: route,
86
+ title: fam.label ?? familyId,
87
+ genre: narrowGenre(fam.genre) ?? genreForRoute(cat, route),
88
+ params: seededParams(cat, route, cat?.models[route]?.defaults ?? {}),
89
+ };
90
+ }
91
+ /** Genre for a model route — the catalog's familyGenre derivation. */
92
+ function genreForRoute(cat, route) {
93
+ return cat ? familyGenre(route, cat) : undefined;
94
+ }
95
+
96
+ // A capability tile in the LibrarySidebar is DRAGGABLE onto a card in the
97
+ // canvas — "clicking and dragging from the right sidebar onto the middle
98
+ // part should populate the card". The drop REPURPOSES the target card: its
99
+ // service + params + title become the dragged capability's resolved card
100
+ // (the same shape a click would `addStage`, applied to an EXISTING stage).
101
+ //
102
+ // The MIME is deliberately distinct from `application/tech-noir-asset`
103
+ // (AppSidebar's asset drag) so a card's drop handler can tell "populate
104
+ // this card's identity" (capability) from "attach an asset to a slot"
105
+ // (asset) — one drop target, two contracts, no ambiguity.
106
+ export const CAPABILITY_DRAG_MIME = "application/tech-noir-capability";
107
+ /** Serialize a picked card onto the drag data store. */
108
+ export function capabilityDragPayload(capId, card) {
109
+ return JSON.stringify({ ...card, capId });
110
+ }
111
+ /** Parse the capability MIME off a drop event. null = absent or malformed
112
+ * (the drop target falls through to the asset/file path). */
113
+ export function parseCapabilityDrop(raw) {
114
+ if (!raw)
115
+ return null;
116
+ try {
117
+ const p = JSON.parse(raw);
118
+ if (typeof p.service === "string" && typeof p.title === "string" && p.params && typeof p.params === "object") {
119
+ return p;
120
+ }
121
+ return null;
122
+ }
123
+ catch { /* not our payload — the JSON probe answers absent (row 28) */
124
+ return null;
125
+ }
126
+ }
127
+ //# sourceMappingURL=capabilityCards.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * E3.7 — Caption text splitter.
3
+ *
4
+ * Converts a plain-text transcription into a sequence of CaptionCues with
5
+ * proportional timing. When the ASR backend provides word-level timestamps
6
+ * (via `tokens`), we use those directly. Otherwise we fall back to
7
+ * sentence-aware splitting with proportional duration allocation (by
8
+ * character count — longer sentences get proportionally more time).
9
+ *
10
+ * Caption length targets ~42 characters per cue (the industry standard
11
+ * for readable subtitles, per the BBC/NBC style guides). Sentences longer
12
+ * than that are split at clause boundaries (commas, semicolons, dashes)
13
+ * or at word boundaries as a last resort.
14
+ *
15
+ * Timing model: cue.start/end are SECONDS RELATIVE TO THE SEGMENT START
16
+ * (not the global timeline). The program monitor maps global time →
17
+ * segment-relative time before looking up the active cue.
18
+ */
19
+ import type { CaptionCue } from "@melite/contracts";
20
+ export interface SplitArgs {
21
+ /** Full transcription text. */
22
+ text: string;
23
+ /** Segment duration in seconds (the cues' timing window). */
24
+ duration: number;
25
+ /** Optional word-level timestamps from the ASR backend. When provided,
26
+ * cues are built from the actual word timings instead of proportional
27
+ * splitting. Each token: { text, start, end } in seconds. */
28
+ tokens?: Array<{
29
+ text: string;
30
+ start: number;
31
+ end: number;
32
+ }> | null;
33
+ /** Max characters per cue (default 42). */
34
+ maxChars?: number;
35
+ }
36
+ /**
37
+ * Build CaptionCues from a transcription. Returns an empty array if the
38
+ * text is empty/whitespace or the duration is <= 0.
39
+ */
40
+ export declare function splitCaptions(args: SplitArgs): CaptionCue[];
41
+ /**
42
+ * Split text into caption-length chunks. Strategy (in priority order):
43
+ * 1. Split into sentences (terminators . ! ? …).
44
+ * 2. If a sentence exceeds maxChars, split at clause boundaries
45
+ * (, ; : — –) then at word boundaries as a last resort.
46
+ * 3. Merge short consecutive fragments up to maxChars.
47
+ */
48
+ export declare function splitIntoChunks(text: string, maxChars?: number): string[];
49
+ //# sourceMappingURL=captionSplitter.d.ts.map
@@ -0,0 +1,172 @@
1
+ const DEFAULT_MAX_CHARS = 42;
2
+ /**
3
+ * Build CaptionCues from a transcription. Returns an empty array if the
4
+ * text is empty/whitespace or the duration is <= 0.
5
+ */
6
+ export function splitCaptions(args) {
7
+ const { text, duration, tokens = null, maxChars = DEFAULT_MAX_CHARS } = args;
8
+ if (duration <= 0)
9
+ return [];
10
+ // ── Timestamped path takes precedence — tokens can build cues even
11
+ // with empty text (the text is derived from the tokens). ─────────────
12
+ if (tokens && tokens.length > 0) {
13
+ return buildFromTokens(tokens, maxChars, duration);
14
+ }
15
+ const trimmed = text.trim();
16
+ if (!trimmed)
17
+ return [];
18
+ // ── Proportional path: split text + distribute by character count ──────
19
+ const chunks = splitIntoChunks(trimmed, maxChars);
20
+
21
+
22
+
23
+ if (chunks.length === 0)
24
+ return [];
25
+ const totalChars = chunks.reduce((s, c) => s + c.length, 0);
26
+ let elapsed = 0;
27
+ return chunks.map((chunk, i) => {
28
+
29
+ // chunks.length guard above), so totalChars is positive — no fallback.
30
+ const proportion = chunk.length / totalChars;
31
+ const start = elapsed;
32
+ const segDur = proportion * duration;
33
+ // Last cue gets the remaining time (avoids float drift at the end).
34
+ const end = i === chunks.length - 1 ? duration : Math.min(duration, start + segDur);
35
+ elapsed = end;
36
+ return {
37
+ id: `cap_${i}_${Math.random().toString(36).slice(2, 8)}`,
38
+ start: Number(start.toFixed(3)),
39
+ end: Number(end.toFixed(3)),
40
+ text: chunk,
41
+ };
42
+ });
43
+ }
44
+ /**
45
+ * Split text into caption-length chunks. Strategy (in priority order):
46
+ * 1. Split into sentences (terminators . ! ? …).
47
+ * 2. If a sentence exceeds maxChars, split at clause boundaries
48
+ * (, ; : — –) then at word boundaries as a last resort.
49
+ * 3. Merge short consecutive fragments up to maxChars.
50
+ */
51
+ export function splitIntoChunks(text, maxChars = DEFAULT_MAX_CHARS) {
52
+ // Normalize whitespace.
53
+ const clean = text.replace(/\s+/g, ' ').trim();
54
+ if (!clean)
55
+ return [];
56
+ // Step 1: split into sentences, keeping the terminator.
57
+ // The second alternative matches any trailing non-space run, so a
58
+ // non-empty input always yields >=1 match (the `as` is proven, not blind:
59
+ // `clean` is non-empty per the guard above).
60
+ const sentences = clean.match(/[^.!?…]+[.!?…]+|\S[^.!?…]*$/g);
61
+ // Step 2: break long sentences at clause / word boundaries.
62
+ const fragments = [];
63
+ for (const s of sentences) {
64
+ const trimmed = s.trim();
65
+
66
+
67
+
68
+ if (!trimmed)
69
+ continue;
70
+ if (trimmed.length <= maxChars) {
71
+ fragments.push(trimmed);
72
+ }
73
+ else {
74
+ fragments.push(...splitLongSentence(trimmed, maxChars));
75
+ }
76
+ }
77
+ // Step 3: greedily merge short fragments up to maxChars.
78
+ return mergeFragments(fragments, maxChars);
79
+ }
80
+ /** Split a long sentence at clause boundaries, then word boundaries. */
81
+ function splitLongSentence(s, maxChars) {
82
+ // Try clause boundaries first: , ; : — –
83
+ const clauseParts = s.split(/(?<=[,;:—–])\s+/).map((p) => p.trim()).filter(Boolean);
84
+ if (clauseParts.length > 1) {
85
+ return clauseParts.flatMap((p) => p.length <= maxChars ? [p] : splitAtWordBoundary(p, maxChars));
86
+ }
87
+ // No clause boundaries — split at word boundaries.
88
+ return splitAtWordBoundary(s, maxChars);
89
+ }
90
+ /** Split at word boundaries, each chunk <= maxChars. */
91
+ function splitAtWordBoundary(s, maxChars) {
92
+ const words = s.split(/\s+/);
93
+ const chunks = [];
94
+ let cur = '';
95
+ for (const w of words) {
96
+ if ((cur + ' ' + w).trim().length <= maxChars) {
97
+ cur = (cur + ' ' + w).trim();
98
+ }
99
+ else {
100
+ if (cur)
101
+ chunks.push(cur);
102
+ cur = w;
103
+ }
104
+ }
105
+
106
+ if (cur)
107
+ chunks.push(cur);
108
+ return chunks;
109
+ }
110
+ /** Greedily merge short fragments that fit within maxChars. */
111
+ function mergeFragments(frags, maxChars) {
112
+ const merged = [];
113
+ let cur = '';
114
+ for (const f of frags) {
115
+ const candidate = cur ? `${cur} ${f}` : f;
116
+ if (candidate.length <= maxChars) {
117
+ cur = candidate;
118
+ }
119
+ else {
120
+ if (cur)
121
+ merged.push(cur);
122
+ cur = f;
123
+ }
124
+ }
125
+
126
+
127
+ if (cur)
128
+ merged.push(cur);
129
+ return merged;
130
+ }
131
+ /** Build cues from timestamped word tokens, grouping words into cues. */
132
+ function buildFromTokens(tokens, maxChars, fallbackDuration) {
133
+ const cues = [];
134
+ let curWords = [];
135
+ let curLen = 0;
136
+ for (const tok of tokens) {
137
+ const addedLen = (curLen > 0 ? 1 : 0) + tok.text.length;
138
+ if (curLen + addedLen > maxChars && curWords.length > 0) {
139
+ // Flush the current group.
140
+ cues.push(makeTokenCue(curWords, cues.length));
141
+ curWords = [];
142
+ curLen = 0;
143
+ }
144
+ curWords.push(tok);
145
+ curLen += (curLen > 0 ? 1 : 0) + tok.text.length;
146
+ }
147
+
148
+
149
+
150
+ if (curWords.length > 0) {
151
+ cues.push(makeTokenCue(curWords, cues.length));
152
+ }
153
+ // Clamp the last cue's end to the segment duration if tokens overshoot.
154
+
155
+
156
+ if (cues.length > 0) {
157
+ const last = cues[cues.length - 1];
158
+ if (last.end > fallbackDuration) {
159
+ last.end = fallbackDuration;
160
+ }
161
+ }
162
+ return cues;
163
+ }
164
+ function makeTokenCue(words, idx) {
165
+ return {
166
+ id: `cap_${idx}_${Math.random().toString(36).slice(2, 8)}`,
167
+ start: Number(words[0].start.toFixed(3)),
168
+ end: Number(words[words.length - 1].end.toFixed(3)),
169
+ text: words.map((w) => w.text).join(' '),
170
+ };
171
+ }
172
+ //# sourceMappingURL=captionSplitter.js.map
@@ -0,0 +1,52 @@
1
+ /** Max length of a card title / flow name, enforced by this gate at edit time. The wire types carry titles as bare strings — the gate is the bound's single home. */
2
+ export declare const TITLE_MAX_LEN = 120;
3
+ export interface MutationSpec {
4
+ /** The field this mutation writes (the patch key). */
5
+ field: string;
6
+ /** Coerce the raw value before validation (trim). */
7
+ coerce: (v: unknown) => unknown;
8
+ /** Type-guard gate: string, non-empty after trim, <= TITLE_MAX_LEN. */
9
+ validate: (v: unknown) => v is string;
10
+ }
11
+ /** The closed set of stage mutations. Add a member + a CARD_MUTATIONS entry
12
+ * to extend the gate. */
13
+ export type StageMutationKind = "rename";
14
+ export type StageMutation = {
15
+ kind: "rename";
16
+ value: string;
17
+ };
18
+ export declare const CARD_MUTATIONS: Record<StageMutationKind, MutationSpec>;
19
+ export type StageMutationResult = {
20
+ ok: true;
21
+ patch: {
22
+ title: string;
23
+ };
24
+ } | {
25
+ ok: false;
26
+ error: string;
27
+ };
28
+ /** Pure gate. Unknown kinds error at runtime (defense in depth beyond the
29
+ * tsc union). Every card mutation must pass through here. */
30
+ export declare function applyStageMutation(mutation: StageMutation): StageMutationResult;
31
+
32
+
33
+ export type DocMutationKind = "rename";
34
+ export type DocMutation = {
35
+ kind: "rename";
36
+ value: string;
37
+ };
38
+ export declare const DOC_MUTATIONS: Record<DocMutationKind, MutationSpec>;
39
+ export type DocMutationResult = {
40
+ ok: true;
41
+ patch: {
42
+ name: string;
43
+ };
44
+ } | {
45
+ ok: false;
46
+ error: string;
47
+ };
48
+ /** Pure gate for flow-doc mutations — the flow-level sibling of
49
+ * applyStageMutation. Cosmetic: the store action must NOT mark stale or
50
+ * recompute keys (a rename is not a semantic change). */
51
+ export declare function applyDocMutation(mutation: DocMutation): DocMutationResult;
52
+ //# sourceMappingURL=cardMutations.d.ts.map
@@ -0,0 +1,67 @@
1
+
2
+ //
3
+ // DECLARATIVE way. One table, one gate — no per-kind imperative store code.
4
+ // Each row declares the field a mutation kind writes + its validation
5
+ // + coercion; extending the gate = adding a union member + a table row
6
+ // (same discipline as _FORM_CONFIGS: one table the gate mirrors).
7
+ //
8
+ // The enforcement is two-layer, mirroring the catalog schema's pattern:
9
+ // • HERE (frontend, edit time): the typed unions (tsc) + this runtime gate —
10
+ // every card/doc mutation passes through applyStageMutation /
11
+ // applyDocMutation (the shared runMutation core).
12
+ // • WIRE (run start): FlowDoc.title AND BackendStage.title in FlowTypes.ts are bare strings — this gate is the bound's single home; a title that passes here fits the wire.
13
+ // Bump all together.
14
+ //
15
+
16
+ // name gets a row in one of these tables + a bound in the owning layer
17
+ // (this gate where the card owns the name, the wire contract where the
18
+ // run owns it). No ad-hoc name writes — that's how drift and breakage
19
+ // creep in.
20
+ /** Max length of a card title / flow name, enforced by this gate at edit time. The wire types carry titles as bare strings — the gate is the bound's single home. */
21
+ export const TITLE_MAX_LEN = 120;
22
+ /** Shared core: coerce → validate → patch. Every table's apply* entry is a
23
+ * thin wrapper over this — one gate, one error language across targets. */
24
+ function runMutation(spec, value) {
25
+ const v = spec.coerce(value);
26
+ if (!spec.validate(v)) {
27
+ return {
28
+ ok: false,
29
+ error: `Invalid ${spec.field}: must be 1-${TITLE_MAX_LEN} non-empty characters`,
30
+ };
31
+ }
32
+ return { ok: true, patch: { [spec.field]: v } };
33
+ }
34
+ export const CARD_MUTATIONS = {
35
+ rename: {
36
+ field: "title",
37
+ coerce: (v) => (typeof v === "string" ? v.trim() : v),
38
+ validate: (v) => typeof v === "string" && v.trim().length > 0 && v.trim().length <= TITLE_MAX_LEN,
39
+ },
40
+ };
41
+ /** Pure gate. Unknown kinds error at runtime (defense in depth beyond the
42
+ * tsc union). Every card mutation must pass through here. */
43
+ export function applyStageMutation(mutation) {
44
+ const spec = CARD_MUTATIONS[mutation.kind];
45
+ if (!spec) {
46
+ return { ok: false, error: `Unknown stage mutation kind: ${mutation.kind}` };
47
+ }
48
+ return runMutation(spec, mutation.value);
49
+ }
50
+ export const DOC_MUTATIONS = {
51
+ rename: {
52
+ field: "name",
53
+ coerce: (v) => (typeof v === "string" ? v.trim() : v),
54
+ validate: (v) => typeof v === "string" && v.trim().length > 0 && v.trim().length <= TITLE_MAX_LEN,
55
+ },
56
+ };
57
+ /** Pure gate for flow-doc mutations — the flow-level sibling of
58
+ * applyStageMutation. Cosmetic: the store action must NOT mark stale or
59
+ * recompute keys (a rename is not a semantic change). */
60
+ export function applyDocMutation(mutation) {
61
+ const spec = DOC_MUTATIONS[mutation.kind];
62
+ if (!spec) {
63
+ return { ok: false, error: `Unknown doc mutation kind: ${mutation.kind}` };
64
+ }
65
+ return runMutation(spec, mutation.value);
66
+ }
67
+ //# sourceMappingURL=cardMutations.js.map
@@ -0,0 +1,18 @@
1
+ /** downloads - browser download triggers for pose artifacts
2
+ * (ex-lib/text-to-pose export helpers). */
3
+ import type { PoseMotion } from "./textToPose.js";
4
+ /**
5
+ * Trigger a browser download from a base64-encoded payload.
6
+ * Mirrors Kimodo demo's _download_bytes_to_browser (ui.py).
7
+ */
8
+ export declare function downloadBase64Payload(b64: string, filename: string, mimeType?: string): void;
9
+ /**
10
+ * Download the SOMA-77 motion as an .npz file (poses + rotmats + deformed verts).
11
+ */
12
+ export declare function downloadMotionNpz(motion: PoseMotion, filename?: string): void;
13
+ /**
14
+ * Capture the largest <canvas> on the page as a PNG download.
15
+ * Used for screenshots of the 3D viewer.
16
+ */
17
+ export declare function downloadCanvasScreenshot(filename?: string): void;
18
+ //# sourceMappingURL=downloads.d.ts.map