@phont-ai/subtitles-core 0.1.10 → 0.2.2

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.
package/dist/index.d.ts CHANGED
@@ -1,10 +1,1466 @@
1
1
  /**
2
- * Core Package Exports
2
+ * Core types for PHONT expressive subtitles
3
+ * Framework-agnostic, pure TypeScript
4
+ */
5
+ type Emotion = "joy" | "anger" | "sadness" | "fear" | "disgust" | "surprise" | "none";
6
+ interface SubtitleWord {
7
+ text: string;
8
+ start: number;
9
+ end: number;
10
+ emotion?: Emotion;
11
+ emphasis?: number;
12
+ }
13
+ interface SubtitleLine {
14
+ speaker?: string;
15
+ start: number;
16
+ end: number;
17
+ words: SubtitleWord[];
18
+ emotion?: Emotion;
19
+ emotion_intensity?: number;
20
+ }
21
+ interface SubtitleTrack {
22
+ vodId: string;
23
+ language: string;
24
+ lines: SubtitleLine[];
25
+ }
26
+ interface EmotionStyle {
27
+ fontWeight?: number;
28
+ skew?: number;
29
+ jitter?: number;
30
+ color?: string;
31
+ glow?: boolean;
32
+ scale?: number;
33
+ }
34
+ interface SubtitleEngineState {
35
+ activeLine: SubtitleLine | null;
36
+ activeWords: SubtitleWord[];
37
+ currentWordIndex: number;
38
+ emotion: Emotion;
39
+ emotionIntensity: number;
40
+ }
41
+ interface PhontClientOptions {
42
+ apiKey?: string;
43
+ authToken?: string;
44
+ sessionId?: string;
45
+ apiBaseUrl?: string;
46
+ }
47
+
48
+ /**
49
+ * Animation Types
50
+ *
51
+ * Type definitions for animation parameter states, curves, and animation state
52
+ */
53
+ interface CurvePoint {
54
+ time: number;
55
+ value: number | string;
56
+ }
57
+ interface ParameterState {
58
+ points: CurvePoint[];
59
+ currentValue?: number | string;
60
+ }
61
+ interface ParameterStates {
62
+ [key: string]: ParameterState;
63
+ }
64
+ interface AnimationParameterStates {
65
+ parameterStates: ParameterStates;
66
+ duration?: number;
67
+ format?: 'normalized' | 'absolute';
68
+ fontData?: {
69
+ style?: {
70
+ fontFamily?: string;
71
+ [key: string]: unknown;
72
+ };
73
+ config?: unknown;
74
+ fontId?: string | null;
75
+ fontName?: string | null;
76
+ };
77
+ text?: string;
78
+ name?: string;
79
+ }
80
+ interface AnimationState {
81
+ fontSize: number;
82
+ scale: number;
83
+ opacity: number;
84
+ slant: number;
85
+ weight: number;
86
+ width: number;
87
+ textColor: string;
88
+ translateX: number;
89
+ translateY: number;
90
+ stretchY: number;
91
+ rotation?: number;
92
+ skewX?: number;
93
+ fontVariationAxes?: Record<string, number>;
94
+ letterShake?: number;
95
+ shadowColor?: string;
96
+ }
97
+ interface SentenceAnimationState extends AnimationState {
98
+ text: string;
99
+ emotion: string;
100
+ emotionIntensity: number;
101
+ fontFamily: string;
102
+ startTime: number;
103
+ endTime: number;
104
+ duration: number;
105
+ isActive: boolean;
106
+ }
107
+ interface WordAnimationState {
108
+ text: string;
109
+ words: Array<{
110
+ word: string;
111
+ start: number;
112
+ end: number;
113
+ animationEnd: number;
114
+ isActive: boolean;
115
+ emotion: string;
116
+ emotionIntensity: number;
117
+ originalEmotionIntensity?: number;
118
+ fontFamily: string;
119
+ emphasis: number;
120
+ emphasisEffect?: any;
121
+ strongEmphasis?: any;
122
+ animationParameters: AnimationState;
123
+ joyAnimationMode?: 'letter' | 'letter-bulge' | 'word' | null;
124
+ joyProgress?: number | null;
125
+ }>;
126
+ currentWordIndex: number;
127
+ emotion: string;
128
+ emotionIntensity: number;
129
+ fontFamily: string;
130
+ startTime: number;
131
+ endTime: number;
132
+ isActive: boolean;
133
+ }
134
+ interface SubtitleSegment {
135
+ start_time: number;
136
+ end_time: number;
137
+ text?: string;
138
+ subtitle?: string;
139
+ emotion?: string;
140
+ emotion_intensity?: number;
141
+ animation_url?: string;
142
+ /** Per-cue expressiveness (0–1); combined with global GTS slider at playback. */
143
+ gts?: number;
144
+ words?: Array<{
145
+ word: string;
146
+ start: number;
147
+ end: number;
148
+ emphasis?: number;
149
+ emotion?: string;
150
+ emotion_intensity?: number;
151
+ animation_url?: string;
152
+ animations?: Record<string, number>;
153
+ gts?: number;
154
+ }>;
155
+ animations?: Record<string, number>;
156
+ volume?: number;
157
+ speech_rate?: number;
158
+ merged_subtitles?: SubtitleSegment[];
159
+ }
160
+ interface AnimationOptions {
161
+ maxExpression?: number;
162
+ threshold?: number;
163
+ isRTL?: boolean;
164
+ volume?: number;
165
+ speechRate?: number;
166
+ nextSegmentStartTime?: number;
167
+ extendedEndTime?: number;
168
+ }
169
+
170
+ /**
171
+ * ============================================
172
+ * SUBTITLE STATE MANAGER
173
+ * ============================================
174
+ * Ported from app/components/subtitle/state/SubtitleStateManager.js
175
+ */
176
+ interface SubtitleState$1 {
177
+ videoTimeSeed: number | null;
178
+ isVisible: boolean;
179
+ isKaraokeMode: boolean;
180
+ isSpeaker: boolean;
181
+ animationMode: "Sentence" | "Word";
182
+ currentSubtitle: string;
183
+ currentSequence: any | null;
184
+ subtitles: any[];
185
+ currentSubtitleData: any | null;
186
+ lastEndTime: number;
187
+ processedSequences: Set<number>;
188
+ initialSequenceOffset: number | null;
189
+ segmentDurations: Map<number, number>;
190
+ soundByteAll: any[];
191
+ soundByteImportant: any[];
192
+ soundByteFiltered: any[];
193
+ showAllSoundbites: boolean;
194
+ showImportantSoundbites: boolean;
195
+ currentTime: number;
196
+ liveStartTime: number | null;
197
+ liveCurrentTime: number;
198
+ soundbiteAnimations: any[];
199
+ showSoundbiteAnimations: boolean;
200
+ isClosedCaptionsEnabled: boolean;
201
+ }
202
+ interface VodState$1 {
203
+ subtitles: any[] | null;
204
+ soundByteAll: any[];
205
+ soundByteImportant: any[];
206
+ soundByteFiltered: any[];
207
+ currentLanguage: string | null;
208
+ videoId: string | null;
209
+ }
210
+ type StateCallback = (state: SubtitleState$1) => void;
211
+ declare class SubtitleStateManager {
212
+ subscribers: Set<StateCallback>;
213
+ state: SubtitleState$1;
214
+ vodState: VodState$1;
215
+ constructor();
216
+ /**
217
+ * Subscribe to state changes
218
+ */
219
+ subscribe(callback: StateCallback): () => void;
220
+ /**
221
+ * Notify all subscribers of state changes
222
+ */
223
+ notify(): void;
224
+ /**
225
+ * Get current state (immutable copy)
226
+ */
227
+ getState(): SubtitleState$1;
228
+ /**
229
+ * Get VOD state (immutable copy)
230
+ */
231
+ getVodState(): VodState$1;
232
+ toggleSubtitles(isVisible: boolean): void;
233
+ toggleClosedCaptions(isEnabled: boolean): void;
234
+ toggleKaraoke(isKaraokeMode: boolean): void;
235
+ toggleSpeaker(isSpeaker: boolean): void;
236
+ toggleSoundbites(type: "all" | "important", isVisible: boolean): void;
237
+ toggleSoundbiteAnimations(isVisible: boolean): void;
238
+ setAnimationMode(mode: "Sentence" | "Word"): void;
239
+ updateSubtitle(text: string): void;
240
+ updateCurrentTime(time: number): void;
241
+ updateSubtitles(subtitles: any[]): void;
242
+ updateCurrentSubtitleData(data: any): void;
243
+ updateSoundbites(all?: any[], important?: any[], filtered?: any[]): void;
244
+ updateSoundbiteAnimations(animations: any[]): void;
245
+ updateVodSubtitles(subtitles: any[]): void;
246
+ updateVodLanguage(language: string | null): void;
247
+ updateVodVideoId(videoId: string | null): void;
248
+ updateVodSoundbites(all?: any[], important?: any[], filtered?: any[]): void;
249
+ updateLiveStartTime(time: number | null): void;
250
+ updateLiveCurrentTime(time: number): void;
251
+ updateVideoTimeSeed(seed: number | null): void;
252
+ markSequenceProcessed(sequenceNumber: number): void;
253
+ isSequenceProcessed(sequenceNumber: number): boolean;
254
+ setInitialSequenceOffset(offset: number): void;
255
+ setSegmentDuration(sequenceNumber: number, duration: number): void;
256
+ getSegmentDuration(sequenceNumber: number): number | undefined;
257
+ updateCurrentSequence(sequence: any): void;
258
+ reset(): void;
259
+ resetControls(): void;
260
+ clearLiveState(): void;
261
+ clearVodState(): void;
262
+ }
263
+
264
+ /**
265
+ * ============================================
266
+ * SUBTITLE MANAGER (Orchestrator)
267
+ * ============================================
268
+ *
269
+ * PURPOSE:
270
+ * Central coordinator for subtitle system. Delegates to specialized classes.
271
+ *
272
+ * USED BY:
273
+ * - PhontSubtitles.jsx: Gets subtitle state and data
274
+ * - Player components: Controls playback and timing
275
+ * - PhontControls.jsx: UI toggles and settings
276
+ *
277
+ * DELEGATES TO:
278
+ * - SubtitleStateManager: State management and subscriptions
279
+ * - SubtitleCache: Language caching and detection
280
+ * - SubtitleFetcher: API requests & data normalization
281
+ * - SoundbiteProcessor: Soundbite/animation processing & laughter merging
282
+ * - VodDataProcessor: VOD data processing, deduplication, soundbite coordination
283
+ * - LiveDataProcessor: LIVE data processing, timing offsets, soundbite coordination
284
+ * - LiveStreamCumulativeTimeGenerator: Generates cumulative time for LIVE streams
285
+ * - CurrentSubtitle: Gets subtitle for time (BOTH VOD & LIVE) + karaoke
286
+ *
287
+ * STATUS: Phase 2 Complete - All 8 classes extracted, clean architecture, no duplication
288
+ */
289
+ type AnimationMode$1 = "Sentence" | "Word" | string;
290
+ interface SubtitleState {
291
+ isVisible: boolean;
292
+ isKaraokeMode: boolean;
293
+ isSpeaker: boolean;
294
+ animationMode: AnimationMode$1;
295
+ currentSubtitle: string;
296
+ currentSequence: number | null;
297
+ subtitles: any[];
298
+ currentSubtitleData: any | null;
299
+ videoTimeSeed: number | null;
300
+ currentTime: number;
301
+ liveStartTime: number | null;
302
+ liveCurrentTime: number;
303
+ lastEndTime: number;
304
+ processedSequences: Set<number>;
305
+ initialSequenceOffset: number | null;
306
+ segmentDurations: Map<number, number>;
307
+ soundByteAll: any[];
308
+ soundByteImportant: any[];
309
+ soundByteFiltered: any[];
310
+ showAllSoundbites: boolean;
311
+ showImportantSoundbites: boolean;
312
+ soundbiteAnimations: any[];
313
+ showSoundbiteAnimations: boolean;
314
+ isClosedCaptionsEnabled: boolean;
315
+ isBackgroundEffectEnabled: boolean;
316
+ backgroundEffect: string;
317
+ backgroundOpacity: number;
318
+ transition: boolean;
319
+ [key: string]: any;
320
+ }
321
+ interface VodState {
322
+ subtitles: any[] | null;
323
+ url: string | null;
324
+ currentSubtitle: string;
325
+ currentSubtitleData: any | null;
326
+ currentSequence: number | null;
327
+ soundByteAll: any[];
328
+ soundByteImportant: any[];
329
+ soundByteFiltered: any[];
330
+ currentLanguage?: string | null;
331
+ videoId?: string | null;
332
+ translation_provider?: string | null;
333
+ translation_model?: string | null;
334
+ }
335
+ type SubtitleSubscriber = (state: SubtitleState) => void;
336
+ declare class SubtitleManager {
337
+ private stateManager;
338
+ private cache;
339
+ private fetcher;
340
+ private soundbiteProcessor;
341
+ private vodDataProcessor;
342
+ private liveTimeGenerator;
343
+ private liveDataProcessor;
344
+ private currentSubtitle;
345
+ constructor();
346
+ /**
347
+ * Set PhontClient for authenticated API requests
348
+ * This will be passed to SubtitleFetcher for use in API calls
349
+ */
350
+ setPhontClient(phontClient: any): void;
351
+ get state(): SubtitleState;
352
+ set state(value: SubtitleState);
353
+ get vodState(): VodState;
354
+ set vodState(value: VodState);
355
+ get subscribers(): Set<SubtitleSubscriber>;
356
+ subscribe(callback: SubtitleSubscriber): () => void;
357
+ notify(): void;
358
+ toggleSubtitles(isVisible: boolean): void;
359
+ toggleClosedCaptions(isEnabled: boolean): void;
360
+ toggleKaraoke(isKaraokeMode: boolean): void;
361
+ toggleSpeaker(isSpeaker: boolean): void;
362
+ toggleBackgroundEffect(isEnabled: boolean, effect?: string): void;
363
+ toggleSoundbites(type: "all" | "important", isVisible: boolean): void;
364
+ toggleSoundbiteAnimations(isVisible: boolean): void;
365
+ mapEventLabelToAnimation(eventLabel: string): string | null;
366
+ processSoundbiteAnimations(soundbites: any[]): any[];
367
+ setAnimationMode(mode: AnimationMode$1): void;
368
+ toggleTransition(enabled: boolean): void;
369
+ updateSubtitle(text: string): void;
370
+ updateLiveTime(videoTime?: number | null): number;
371
+ startLiveTiming(): void;
372
+ stopLiveTiming(): void;
373
+ pauseLiveTiming(): void;
374
+ resumeLiveTiming(): void;
375
+ resetLiveStream(videoElement: HTMLVideoElement): void;
376
+ getLiveTime(): number;
377
+ getState(): SubtitleState;
378
+ reset(): void;
379
+ resetControls(): void;
380
+ getCumulativeDuration(upToSequenceNumber: number): number;
381
+ normalizeSubtitles(data: any): any;
382
+ normalizeSoundbites(arr: any[]): any[];
383
+ getAudioSegmentDuration(sequenceNumber: number): Promise<number>;
384
+ getInitialLiveSequenceNumber(): Promise<number>;
385
+ fetchSubtitles(sequenceNumber: number): Promise<any | null>;
386
+ loadVodSubtitles(urlOrVodId: string, languageCode?: string | null): Promise<void>;
387
+ _processVodData(data: any, url: string, languageCode: string | null, originalSubtitles?: any[]): void;
388
+ getSubtitleForTime(time: number): any;
389
+ getLiveSubtitleForTime(): any;
390
+ getCurrentSubtitleData(): any | null;
391
+ updateBackgroundOpacity(opacity: number): void;
392
+ clearOldSubtitles(currentTime: number, bufferSeconds?: number): void;
393
+ getQueueStatus(): {
394
+ totalSubtitles: number;
395
+ lastEndTime: number;
396
+ processedSequences: number[];
397
+ currentSequence: number | null;
398
+ };
399
+ setCurrentTime(time: number): void;
400
+ getActiveSoundbites(isVodMode?: boolean, vodTime?: number | null): any[];
401
+ getAllSoundbiteData(isVodMode?: boolean): any;
402
+ getActiveSoundbiteAnimations(isVodMode?: boolean): any[];
403
+ hasActiveAnimations(): boolean;
404
+ clearSoundbiteAnimations(): void;
405
+ clearOldAnimations(currentTime: number, bufferSeconds?: number): void;
406
+ getCurrentSpeaker(isVodMode?: boolean, vodTime?: number | null): string | null;
407
+ fetchSubtitleData(url: string, languageCode?: string | null, model?: string | null): Promise<void>;
408
+ switchSubtitleLanguage(languageCode: string | null, videoId: string, model?: string | null): Promise<void>;
409
+ _loadCachedSubtitles(data: any, url: string, languageCode: string | null): void;
410
+ getCurrentSubtitleLanguage(): string | null;
411
+ clearSubtitleCache(): void;
412
+ clearAudioPlaylistCache(): void;
413
+ updateState(): void;
414
+ }
415
+ declare const subtitleManager: SubtitleManager;
416
+
417
+ /**
418
+ * URL Decoder for Animation Parameters
419
+ *
420
+ * Extracted from app/playground/utils/urlEncoding.js
421
+ * Decodes animation URLs into parameter states
422
+ *
423
+ * Supports both full URLs and query strings:
424
+ * - Full URL: https://stage-dev.phont.ai/playground/shared?name=...&scale=0,1|1,1.2
425
+ * - Query string: ?name=...&scale=0,1|1,1.2
426
+ */
427
+
428
+ /**
429
+ * Decode animation data from URL or query string
430
+ * @param urlOrQuery - Full URL (https://...) or query string (?name=... or name=...)
431
+ * @returns Decoded animation data object with parameterStates
432
+ */
433
+ declare function decodeAnimationUrl(urlOrQuery: string): AnimationParameterStates;
434
+
435
+ /**
436
+ * Animation parameter calculation -- the unified rendering pipeline.
437
+ * TypeScript port of app/utils/animation/animationParameters.js.
438
+ *
439
+ * Samples authored URL curves with linear interpolation, then scales deviations
440
+ * proportionally from a reference point (REFERENCE_INTENSITY × REFERENCE_GTS).
441
+ * At the reference point the authored curve is rendered 1:1.
442
+ */
443
+
444
+ /**
445
+ * Exponential variance curve (hockey stick)
446
+ *
447
+ * Creates an exponential curve that accelerates dramatically after 0.7 intensity.
448
+ * Low intensities (0.0-0.7): Subtle effects (gradual rise)
449
+ * High intensities (0.7-1.0): Dramatic effects (exponential acceleration)
450
+ *
451
+ * @param i - Input intensity (0-1)
452
+ * @returns Transformed intensity (0-1) with exponential variance
453
+ */
454
+ declare function hockeyStickIntensity(i: number): number;
455
+ /**
456
+ * Sample a parameter curve at a given time
457
+ *
458
+ * Interpolates between keyframe points to get the value at a specific time.
459
+ * Supports both normalized (0-1) and absolute time formats.
460
+ * For color parameters, uses RGB interpolation for smooth color transitions.
461
+ *
462
+ * @param points - Array of {time, value} points or [time, value] arrays
463
+ * @param t - Normalized time (0-1)
464
+ * @param duration - Animation duration in seconds
465
+ * @param isNormalized - Whether points are in normalized (0-1) or absolute time
466
+ * @param parameterName - Name of the parameter (e.g., 'color', 'shadowColor', 'outlineColor') for color interpolation
467
+ * @returns Sampled value or null if no points
468
+ */
469
+ declare function sampleParameterCurve(points: Array<{
470
+ time?: number;
471
+ value?: number | string;
472
+ } | [number, number | string]> | undefined | null, t: number, duration: number, isNormalized?: boolean, parameterName?: string | null): number | string | null;
473
+ interface AnimationParameterState {
474
+ rawIntensity: number;
475
+ scaleFactor: number;
476
+ scale: number;
477
+ slant: number;
478
+ stretchY: number;
479
+ translateX: number;
480
+ translateY: number;
481
+ weight: number;
482
+ width: number;
483
+ fontFamily: string;
484
+ textColor: string;
485
+ opacity: number;
486
+ textShadow: string;
487
+ blur: number;
488
+ outlineWidth: number;
489
+ outlineColor: string;
490
+ outline2Width?: number;
491
+ outline2Color?: string;
492
+ outline3Width?: number;
493
+ outline3Color?: string;
494
+ outlineShadowWidth?: number;
495
+ outlineShadowColor?: string;
496
+ outlineShadowBlur?: number;
497
+ glowIntensity?: number;
498
+ glowColor?: string;
499
+ snakeWave?: number;
500
+ karaoke?: number;
501
+ stretchX?: number;
502
+ letterSpacing: string;
503
+ fontVariationAxes: Record<string, number>;
504
+ text: string;
505
+ joy: number;
506
+ fear: number;
507
+ standardEmphasis: number;
508
+ strongEmphasis: number;
509
+ joyWaveProgress: number;
510
+ }
511
+ declare const REFERENCE_INTENSITY = 0.7;
512
+ declare const REFERENCE_GTS = 0.7;
513
+ /** Play authored URL curves 1:1 — matches main playground preview (no intensity/GTS scaling). */
514
+ /** Playground / converter preview only — bypasses GTS scaling (1:1 at reference). */
515
+ declare const AUTHORED_PLAYBACK_OPTIONS: {
516
+ rawMode: boolean;
517
+ };
518
+ /**
519
+ * Sample all animation parameters at time `t` with proportional intensity scaling.
520
+ *
521
+ * The authored curve represents the animation at REFERENCE_INTENSITY × REFERENCE_GTS.
522
+ * At those values you see exactly what was authored. Other intensity × GTS values
523
+ * scale linearly from there.
524
+ */
525
+ declare function calculateAnimationParameters(animationData: AnimationParameterStates | null, rawIntensity: number, t: number, gts: number, subtitleDuration?: number | null, { rawMode }?: {
526
+ rawMode?: boolean;
527
+ }): AnimationParameterState | null;
528
+
529
+ type AnimationMode = 'sentence' | 'word';
530
+ type AnimationStateOptions = AnimationOptions & {
531
+ transparentUntilSpoken?: boolean;
532
+ };
533
+ declare function calculateAnimationState(segment: SubtitleSegment | null, currentTime: number, modeOrOptions?: AnimationMode | AnimationStateOptions, maybeOptions?: AnimationStateOptions): SentenceAnimationState | WordAnimationState;
534
+
535
+ /**
536
+ * Emotion Animation URLs
537
+ *
538
+ * Config mapping emotion → animation URL
539
+ * Uses full playground URLs from app/gallery/config/animations.js
540
+ *
541
+ * SYNCED FROM: app/gallery/config/emotionMapping.js
542
+ *
543
+ * To update these URLs:
544
+ * 1. Update app/gallery/config/animations.js with new URLs
545
+ * 2. Update app/gallery/config/emotionMapping.js to map emotions to indices
546
+ * 3. Run: node scripts/sync-animation-urls.js
547
+ * OR manually copy the URLs based on the mapping
548
+ *
549
+ * The decodeAnimationUrl() function now supports both full URLs and query strings.
550
+ */
551
+ declare const ANIMATION_URLS: string[];
552
+ /**
553
+ * Emotion to Animation URL mapping
554
+ * Based on app/gallery/config/emotionMapping.js
555
+ *
556
+ * Maps each emotion to an index in ANIMATION_URLS array
557
+ */
558
+ declare const EMOTION_ANIMATION_URLS: Record<string, string>;
559
+ /**
560
+ * Get animation URL for an emotion
561
+ * @param emotion - Emotion name (case-insensitive)
562
+ * @returns Full animation URL or query string fallback
563
+ */
564
+ declare function getAnimationUrlForEmotion(emotion: string): string;
565
+
566
+ /**
567
+ * Published Animation Resolver (framework-agnostic)
568
+ *
569
+ * Fetches the active published animation URL for each emotion from the backend,
570
+ * caches the results, and provides sync/async accessors.
571
+ *
572
+ * Any consumer (app, SDK, extension) creates an instance by supplying a fetcher
573
+ * that returns an array of animation objects for a given stylesheet.
574
+ *
575
+ * USAGE:
576
+ * import { createPublishedAnimationResolver } from '@phont-ai/subtitles-core';
577
+ *
578
+ * const resolver = createPublishedAnimationResolver(
579
+ * async (stylesheet) => fetch(`/animations?stylesheet_name=${stylesheet}&is_active=true`).then(r => r.json())
580
+ * );
581
+ *
582
+ * await resolver.prefetch('default'); // warm the cache
583
+ * const url = resolver.getSync('lust'); // sync (returns null if not cached)
584
+ * const url = await resolver.get('lust'); // async (fetches if needed)
585
+ * resolver.invalidate(); // bust after publishing
586
+ */
587
+ interface AnimationRecord {
588
+ emotion: string;
589
+ url: string;
590
+ is_active: boolean;
591
+ [key: string]: unknown;
592
+ }
593
+ type AnimationFetcher = (stylesheetName: string) => Promise<AnimationRecord[]>;
594
+ interface PublishedAnimationResolver {
595
+ prefetch: (stylesheetName?: string) => Promise<Map<string, string>>;
596
+ get: (emotion: string, stylesheetName?: string) => Promise<string | null>;
597
+ getSync: (emotion: string, stylesheetName?: string) => string | null;
598
+ invalidate: () => void;
599
+ }
600
+ declare function createPublishedAnimationResolver(fetcher: AnimationFetcher, { ttlMs }?: {
601
+ ttlMs?: number;
602
+ }): PublishedAnimationResolver;
603
+
604
+ /**
605
+ * Emotion Selector
606
+ *
607
+ * Pure function version of useEmotionSelector (no React)
608
+ * Selects the highest-value emotion from backend animations object
609
+ * Core emotions (anger, fear, sadness, joy) receive ×1.5 importance multiplier
610
+ */
611
+ interface EmotionSelectionResult {
612
+ emotion: string;
613
+ intensity: number;
614
+ allEmotions: Array<[string, number, number, number, boolean]>;
615
+ importanceFactor: number;
616
+ isCore: boolean;
617
+ }
618
+ /**
619
+ * Find the highest emotion value from animations object
620
+ *
621
+ * Core emotions (anger, fear, sadness, joy, disgust) are multiplied by 1.5 before selection
622
+ * to prioritize fundamental emotions over secondary ones.
623
+ *
624
+ * @param animationsObject - Object with emotion keys and values (0-1)
625
+ * @param threshold - Minimum value to consider (GTS-controlled: 0.7 at GTS=0, 0.2 at GTS=1)
626
+ * @returns Emotion selection result
627
+ */
628
+ declare function selectEmotion(animationsObject: Record<string, number> | null | undefined, threshold?: number): EmotionSelectionResult;
629
+
630
+ /**
631
+ * Timing Utilities
632
+ *
633
+ * Pure functions for calculating subtitle timing
634
+ */
635
+
636
+ /**
637
+ * Get subtitle timing information
638
+ */
639
+ declare function getSubtitleTiming(subtitle: SubtitleSegment | null): {
640
+ startTime: number;
641
+ endTime: number;
642
+ duration: number;
643
+ };
644
+ /**
645
+ * Calculate normalized time (t: 0→1) through subtitle duration
646
+ */
647
+ declare function calculateNormalizedTime(currentTime: number, startTime: number, duration: number): number;
648
+
649
+ /**
650
+ * Animation Defaults - Visual Styling Layer
651
+ *
652
+ * All subtitle animation VISUAL defaults in one place
653
+ * This is where you edit colors, shadows, font names, and CSS values
654
+ *
655
+ * 🎯 HOW TO CONFIGURE:
656
+ * - Change default font size: Edit ANIMATION_STYLING.fontSize
657
+ * - Change default colors/shadows: Edit ANIMATION_STYLING.textColor, shadow, etc.
658
+ *
659
+ * 🔧 FOR BEHAVIOR/TIMING:
660
+ * - Change when Rakkas (angry) font appears: Edit RENDERING_CONTROL.emotion.angryFontThreshold
661
+ * - Change effect timing/intensity: Edit renderingControl.ts
662
+ */
663
+ declare const ANIMATION_STYLING: {
664
+ fontSize: number;
665
+ opacity: number;
666
+ weight: number;
667
+ slant: number;
668
+ textColor: string;
669
+ translateX: number;
670
+ translateY: number;
671
+ defaultFont: string;
672
+ angryFont: string;
673
+ joyFont: string;
674
+ sadnessFont: string;
675
+ shadow: {
676
+ offsetX: number;
677
+ offsetY: number;
678
+ blur: number;
679
+ color: string;
680
+ };
681
+ angryFontStyle: {
682
+ color: string;
683
+ strokeColor: string;
684
+ strokeWidth: number;
685
+ shadowOffsetX: number;
686
+ shadowOffsetY: number;
687
+ shadowBlur: number;
688
+ shadowBlurMin: number;
689
+ shadowBlurMax: number;
690
+ shadowOpacityMin: number;
691
+ shadowOpacityMax: number;
692
+ shadowColor: string;
693
+ };
694
+ disgustEmotion: {
695
+ shadowOffsetX: number;
696
+ shadowOffsetY: number;
697
+ shadowBlur: number;
698
+ shadowColor: string;
699
+ };
700
+ transition: {
701
+ duration: number;
702
+ easing: string;
703
+ };
704
+ lineHeight: number;
705
+ letterSpacing: string;
706
+ wordSpacing: {
707
+ min: string;
708
+ max: string;
709
+ };
710
+ fontSpacingAdjustments: {
711
+ 'Roboto Flex': {
712
+ letterSpacing: string;
713
+ wordSpacingMultiplier: number;
714
+ };
715
+ Parkinsans: {
716
+ letterSpacing: string;
717
+ wordSpacingMultiplier: number;
718
+ fontSizeOffset: number;
719
+ };
720
+ 'Bricolage Grotesque': {
721
+ letterSpacing: string;
722
+ wordSpacingMultiplier: number;
723
+ fontSizeOffset: number;
724
+ };
725
+ Rakkas: {
726
+ letterSpacing: string;
727
+ wordSpacingMultiplier: number;
728
+ };
729
+ Cormorant: {
730
+ letterSpacing: string;
731
+ wordSpacingMultiplier: number;
732
+ };
733
+ };
734
+ position: {
735
+ bottom: string;
736
+ horizontalAlign: string;
737
+ };
738
+ wordAnimation: {
739
+ minDisplayTime: number;
740
+ allowOverlap: boolean;
741
+ };
742
+ };
743
+
744
+ /**
745
+ * Rendering Configuration
746
+ *
747
+ * Central configuration for animation system timing, thresholds, and behavior
748
+ * This is the single source of truth for non-visual animation parameters
749
+ */
750
+ declare const RENDERING_CONTROL: {
751
+ modes: {
752
+ word: string;
753
+ sentence: string;
754
+ };
755
+ timing: {
756
+ minWordDisplayTime: number;
757
+ breathingRoomDuration: number;
758
+ breathingRoomMinGap: number;
759
+ breathingRoomLargeGap: number;
760
+ breathingRoomSafetyBuffer: number;
761
+ breathingRoomNoNextHold: number;
762
+ fadeOutDuration: number;
763
+ timeRangeTolerance: number;
764
+ };
765
+ emotion: {
766
+ defaultThreshold: number;
767
+ maxThreshold: number;
768
+ angryFontThreshold: number;
769
+ angryEmotions: string[];
770
+ joyFontThreshold: number;
771
+ joyEmotions: string[];
772
+ sadnessFontThreshold: number;
773
+ sadnessEmotions: string[];
774
+ };
775
+ emphasis: {
776
+ minThreshold: number;
777
+ maxThreshold: number;
778
+ scaleMultiplier: number;
779
+ scaleUpFrames: number;
780
+ scaleDownFrames: number;
781
+ fps: number;
782
+ };
783
+ strongEmphasis: {
784
+ minThreshold: number;
785
+ maxScale: number;
786
+ maxRotation: number;
787
+ maxTranslateY: number;
788
+ maxTranslateX: number;
789
+ maxSkew: number;
790
+ maxBrightness: number;
791
+ maxContrast: number;
792
+ maxShadowBlur: number;
793
+ shadowOpacity: number;
794
+ shadowOffsetX: number;
795
+ shadowOffsetY: number;
796
+ maxZIndex: number;
797
+ outwardPushTranslateX: number;
798
+ outwardPushRotation: number;
799
+ outwardPushSkew: number;
800
+ fadeOutTailDuration: number;
801
+ };
802
+ fear: {};
803
+ joy: {
804
+ maxWordsForLetterAnimation: number;
805
+ minWordsForWordAnimation: number;
806
+ letterWave: {
807
+ innerWidthRatio: number;
808
+ outerWidthRatio: number;
809
+ maxScale: number;
810
+ maxRotation: number;
811
+ maxTranslateY: number;
812
+ maxTranslateX: number;
813
+ maxSkew: number;
814
+ maxBrightness: number;
815
+ maxContrast: number;
816
+ maxShadowBlur: number;
817
+ shadowOpacity: number;
818
+ maxZIndex: number;
819
+ };
820
+ letterBulge: {
821
+ innerWidthRatio: number;
822
+ outerWidthRatio: number;
823
+ maxScale: number;
824
+ maxStretchY: number;
825
+ maxCompressionX: number;
826
+ maxTranslateY: number;
827
+ maxTranslateX: number;
828
+ maxSkew: number;
829
+ maxRotation: number;
830
+ maxBrightness: number;
831
+ maxContrast: number;
832
+ maxShadowBlur: number;
833
+ shadowOpacity: number;
834
+ maxZIndex: number;
835
+ };
836
+ wordLevel: {
837
+ duration: number;
838
+ easeType: string;
839
+ maxScale: number;
840
+ maxRotation: number;
841
+ maxTranslateY: number;
842
+ maxTranslateX: number;
843
+ maxSkew: number;
844
+ maxBrightness: number;
845
+ maxContrast: number;
846
+ maxShadowBlur: number;
847
+ shadowOpacity: number;
848
+ maxZIndex: number;
849
+ transitionDuration: number;
850
+ };
851
+ };
852
+ debug: {
853
+ showBorders: boolean;
854
+ showTimings: boolean;
855
+ logRendering: boolean;
856
+ };
857
+ };
858
+
859
+ /**
860
+ * Shadow Generator
861
+ *
862
+ * Generates CSS text-shadow strings from shadow colors
863
+ * All emotion-specific shadows are now controlled by URL animations
864
+ */
865
+ /**
866
+ * Generate CSS text-shadow string from shadow color
867
+ *
868
+ * @param shadowColor - Shadow color in hex format (e.g., '#ff3232')
869
+ * @param _fontFamily - Unused (kept for API compatibility)
870
+ * @param _emotion - Unused (kept for API compatibility)
871
+ * @param _intensity - Unused (kept for API compatibility)
872
+ * @returns CSS text-shadow value or empty string if no shadow
873
+ */
874
+ declare function generateTextShadow(shadowColor: string | undefined, _fontFamily?: string, _emotion?: string, _intensity?: number): string;
875
+
876
+ /**
877
+ * Dynamic Word Spacing Calculator
878
+ *
879
+ * Calculates word spacing that grows with animation scale and GTS intensity
880
+ * Ported from app/config/animation/styling.js
881
+ */
882
+ /**
883
+ * Calculate dynamic word spacing that grows with scale
884
+ *
885
+ * @param currentScale - Current scale value from animation (e.g., 1.0 to 1.5)
886
+ * @param fontFamily - Font family name for spacing adjustments
887
+ * @param gtsIntensity - GTS intensity (0-1) - currently unused for layout preservation
888
+ * @param emotion - Current emotion type (for anger-specific spacing)
889
+ * @returns CSS em value for marginRight
890
+ */
891
+ declare function calculateDynamicWordSpacing(currentScale?: number, fontFamily?: string, gtsIntensity?: number, emotion?: string): string;
892
+
893
+ /**
894
+ * Prosody Weight — Natural Voice Intonation for Animations
895
+ *
896
+ * Only ~20% of words get motion animation, mimicking how natural speech
897
+ * stresses only a few key words per phrase. Font styling (color, weight,
898
+ * slant) is unaffected — this only gates movement.
899
+ *
900
+ * Selection criteria:
901
+ * 1. Function words (the, a, is, and…) → always quiet
902
+ * 2. Content words are scored by length + deterministic hash
903
+ * 3. Only the top ~35% of content words animate
904
+ * (since function/short words are ~55% of text,
905
+ * 35% of remaining 45% ≈ 16-20% of all words)
906
+ */
907
+ /**
908
+ * Compute a prosody importance weight for a single word.
909
+ *
910
+ * @returns 0 (no motion) or 1 (full motion).
911
+ */
912
+ declare function computeProsodyWeight(word: any, _emotionIntensity: number, _gts: number): number;
913
+
914
+ /**
915
+ * Joy Animation Effects
916
+ *
917
+ * Per-letter joy pressure effects for wave and bulge animations
918
+ * Ported from app/utils/animation/joy.js
919
+ */
920
+ /**
921
+ * Determine which joy animation mode to use based on word count
922
+ */
923
+ declare function getJoyAnimationMode(wordCount: number): 'letter' | 'letter-bulge' | 'word';
924
+ interface JoyEffect {
925
+ pressure: number;
926
+ scale: number;
927
+ rotation: number;
928
+ translateX: number;
929
+ translateY: number;
930
+ skewX: number;
931
+ stretchY: number;
932
+ stretchX?: number;
933
+ brightness: number;
934
+ contrast: number;
935
+ shadowBlur: number;
936
+ shadowOpacity: number;
937
+ zIndex: number;
938
+ shadowOffsetX?: number;
939
+ shadowOffsetY?: number;
940
+ bulgeIntensity?: number;
941
+ }
942
+ /**
943
+ * Calculate per-letter joy pressure based on word progress and letter position
944
+ */
945
+ declare function calculateJoy(progress: number, letterIndex: number, totalLetters: number, gtsMultiplier?: number): JoyEffect | null;
946
+ /**
947
+ * Calculate joy bulge effect (for multi-word subtitles)
948
+ */
949
+ declare function calculateJoyBulge(progress: number, letterIndex: number, totalLetters: number, gtsMultiplier?: number): JoyEffect | null;
950
+ /**
951
+ * Generate CSS transform string from joy effect
952
+ */
953
+ declare function getJoyTransform(joyEffect: JoyEffect | null): string;
954
+ /**
955
+ * Generate CSS filter string from joy effect
956
+ */
957
+ declare function getJoyFilter(joyEffect: JoyEffect | null): string;
958
+
959
+ /**
960
+ * Word Emphasis Effects
961
+ *
962
+ * Calculates emphasis effects (standard and strong) for words based on confidence levels.
963
+ *
964
+ * USED BY:
965
+ * - calculateWordAnimation: Main word animation orchestrator
966
+ *
967
+ * HOW IT WORKS:
968
+ * 1. Standard Emphasis (50-70%): Quick "pop" effect at word start
969
+ * 2. Strong Emphasis (70%+): Per-letter dramatic effects throughout word
970
+ * 3. Both scale with GTS (expressiveness level) and word confidence
971
+ *
972
+ * KEY FEATURES:
973
+ * - Confidence-based thresholds (50-70% vs 70%+)
974
+ * - GTS-scaled intensity
975
+ * - Emotion slant inheritance
976
+ * - Smooth transitions
977
+ */
978
+ interface Word {
979
+ emphasis?: number;
980
+ start: number;
981
+ end: number;
982
+ animationEnd?: number;
983
+ }
984
+ interface EmphasisEffect {
985
+ scaleMultiplier: number;
986
+ slantAdditive: number;
987
+ weightBoost: number;
988
+ scaleProgress: number;
989
+ gtsMultiplier: number;
990
+ emphasis: number;
991
+ emphasisMultiplier: number;
992
+ elapsed: number;
993
+ scaleActive: boolean;
994
+ }
995
+ interface StrongEmphasisData {
996
+ emphasis: number;
997
+ rawEmphasis: number;
998
+ scale: number;
999
+ rotation: number;
1000
+ translateX: number;
1001
+ translateY: number;
1002
+ skewX: number;
1003
+ stretchY: number;
1004
+ brightness: number;
1005
+ contrast: number;
1006
+ shadowBlur: number;
1007
+ shadowOpacity: number;
1008
+ zIndex: number;
1009
+ weightBoost: number;
1010
+ shadowOffsetX: number;
1011
+ shadowOffsetY: number;
1012
+ gtsMultiplier: number;
1013
+ emphasisMultiplier: number;
1014
+ fadeFactor: number;
1015
+ isInTail: boolean;
1016
+ }
1017
+ /**
1018
+ * Calculate emphasis effects for a word
1019
+ *
1020
+ * @param params - Emphasis calculation parameters
1021
+ * @returns Emphasis effects { emphasisEffect, strongEmphasisData }
1022
+ */
1023
+ declare function calculateWordEmphasisEffects({ word, currentTime, maxExpression, emotionAnimationDuration, wordBaseSlant, isActive, wordEmotion, }: {
1024
+ word: Word;
1025
+ currentTime: number;
1026
+ maxExpression: number;
1027
+ emotionAnimationDuration: number;
1028
+ wordBaseSlant: number;
1029
+ isActive: boolean;
1030
+ wordEmotion?: string;
1031
+ }): {
1032
+ emphasisEffect: EmphasisEffect | null;
1033
+ strongEmphasisData: StrongEmphasisData | null;
1034
+ };
1035
+
1036
+ /**
1037
+ * ============================================
1038
+ * WORD LETTER SPACING
1039
+ * ============================================
1040
+ *
1041
+ * PURPOSE:
1042
+ * Calculates per-letter translateX offsets for GPU-accelerated spacing (no layout reflow).
1043
+ * Extracted from animationEngine for better maintainability.
1044
+ *
1045
+ * USED BY:
1046
+ * - animationEngine: Main word animation orchestrator
1047
+ *
1048
+ * HOW IT WORKS:
1049
+ * 1. Detects active effects (fear shake only - emphasis spacing is disabled)
1050
+ * 2. Calculates per-letter translateX offsets to create spacing between letters
1051
+ * 3. Returns array of translateX values in pixels (one per letter)
1052
+ *
1053
+ * KEY FEATURES:
1054
+ * - Transform-based spacing (GPU-accelerated, no layout reflow)
1055
+ * - Letter spacing disabled for emphasis effects (standard and strong)
1056
+ * - Only fear shake effects get letter spacing (if needed)
1057
+ * - Zero baseline for clean transitions
1058
+ *
1059
+ * PERFORMANCE BENEFITS:
1060
+ * - Uses translateX (compositor property) instead of letter-spacing (layout property)
1061
+ * - Smooth CSS transitions without triggering layout reflow
1062
+ * - All spacing animations run on GPU compositor layer
1063
+ */
1064
+ interface WordLetterSpacingParams {
1065
+ strongEmphasisData?: any;
1066
+ emphasisEffect?: any;
1067
+ wordLetterShakeIntensity: number | null;
1068
+ wordFontFamily: string;
1069
+ currentTime: number;
1070
+ word: any;
1071
+ emotionAnimationDuration: number;
1072
+ wordEmotion?: string;
1073
+ totalLetters: number;
1074
+ fontSize: number;
1075
+ }
1076
+ /**
1077
+ * Calculate per-letter translateX offsets for GPU-accelerated spacing
1078
+ *
1079
+ * @param params - Spacing calculation parameters
1080
+ * @returns Array of translateX offsets in pixels (one per letter)
1081
+ */
1082
+ declare function calculateWordLetterSpacing({ strongEmphasisData, emphasisEffect, wordLetterShakeIntensity, wordFontFamily, currentTime, word, emotionAnimationDuration, wordEmotion, totalLetters, fontSize, }: WordLetterSpacingParams): number[];
1083
+
1084
+ /**
1085
+ * GTS (Global Text Style) Calculations
1086
+ *
1087
+ * Centralized logic for GTS-derived values and emotion threshold calculations
1088
+ */
1089
+ /**
1090
+ * Combine global GTS slider with optional per-segment / per-word authored gts.
1091
+ */
1092
+ declare function resolveEffectiveGts(sliderGts: number, segmentGts?: number, wordGts?: number): number;
1093
+ /**
1094
+ * Calculate all GTS-derived values from slider position
1095
+ *
1096
+ * GTS slider range: 0 to 1
1097
+ * - GTS = 0 → maxExpression = 0.2, threshold = 0.7 (conservative)
1098
+ * - GTS = 1 → maxExpression = 1.0, threshold = 0.2 (expressive)
1099
+ *
1100
+ * @param gts - GTS slider value (0-1)
1101
+ * @returns All derived GTS values
1102
+ */
1103
+ declare function calculateGTSValues(gts: number): {
1104
+ gts: number;
1105
+ maxExpression: number;
1106
+ threshold: number;
1107
+ shouldEnableAngryFont: boolean;
1108
+ };
1109
+ /**
1110
+ * Check if emotion intensity meets the threshold
1111
+ *
1112
+ * @param intensity - Raw emotion intensity from backend (0-1)
1113
+ * @param threshold - GTS-calculated threshold (0-1)
1114
+ * @returns True if intensity is above threshold
1115
+ */
1116
+ declare function meetsEmotionThreshold(intensity: number, threshold: number): boolean;
1117
+ /**
1118
+ * Calculate scaled emotion intensity based on GTS maxExpression
1119
+ *
1120
+ * GTS acts as a scaling multiplier (final layer) to preserve variance in emotion values
1121
+ * while controlling overall expressiveness.
1122
+ *
1123
+ * When GTS=0.0 (maxExpression=0.2), animations are completely disabled.
1124
+ *
1125
+ * @param rawIntensity - Raw emotion intensity from backend (0-1)
1126
+ * @param maxExpression - GTS-derived max expression multiplier (0.2-1.0)
1127
+ * @returns Scaled emotion intensity (0-1), or 0 if GTS=0.0
1128
+ */
1129
+ declare function capEmotionIntensity(rawIntensity: number, maxExpression: number): number;
1130
+ /**
1131
+ * Calculate emphasis multiplier within valid emphasis range
1132
+ *
1133
+ * Maps emphasis value within [minThreshold, maxThreshold] to [0, 1]
1134
+ * Used for scaling emphasis effects based on position in range
1135
+ *
1136
+ * @param emphasis - Word emphasis value (0-1)
1137
+ * @returns Emphasis multiplier (0-1), or 0 if outside range
1138
+ */
1139
+ declare function calculateEmphasisMultiplier(emphasis: number): number;
1140
+ /**
1141
+ * Check if emphasis value is in valid range for emphasis animation
1142
+ *
1143
+ * @param emphasis - Word emphasis value (0-1)
1144
+ * @returns True if in emphasis range [0.5, 0.7)
1145
+ */
1146
+ declare function isInEmphasisRange(emphasis: number): boolean;
1147
+ /**
1148
+ * Check if emphasis value is in valid range for strong emphasis animation
1149
+ *
1150
+ * @param emphasis - Word emphasis value (0-1)
1151
+ * @returns True if in strong emphasis range [0.7, 1.0]
1152
+ */
1153
+ declare function isInStrongEmphasisRange(emphasis: number): boolean;
1154
+
1155
+ /**
1156
+ * Font Selection Utilities
1157
+ *
1158
+ * Centralized logic for selecting fonts based on emotion and intensity
1159
+ * Eliminates duplicate font selection code across controllers
1160
+ */
1161
+ interface Stylesheet {
1162
+ name?: string;
1163
+ fonts?: Record<string, string>;
1164
+ }
1165
+ /**
1166
+ * Select font family based on emotion and GTS intensity
1167
+ *
1168
+ * @param emotion - Emotion name (lowercase, e.g., 'anger', 'joy', 'sadness')
1169
+ * @param maxExpression - GTS-derived max expression (0-1)
1170
+ * @param emotionCategory - Emotion category for children stylesheet (shout, whisper, anger, fear, neutral)
1171
+ * @param stylesheet - Active stylesheet config (optional)
1172
+ * @returns Font family name
1173
+ */
1174
+ declare function selectFontFamily(emotion: string, maxExpression: number, emotionCategory?: string | null, stylesheet?: Stylesheet | null): string;
1175
+ /**
1176
+ * Get CSS-ready font-family value
1177
+ *
1178
+ * @param emotion - Emotion name (lowercase)
1179
+ * @param maxExpression - GTS-derived max expression (0-1)
1180
+ * @param emotionCategory - Emotion category for children stylesheet
1181
+ * @param stylesheet - Active stylesheet config (optional)
1182
+ * @returns CSS font-family value
1183
+ */
1184
+ declare function getFontFamilyCSS(emotion: string, maxExpression: number, emotionCategory?: string | null, stylesheet?: Stylesheet | null): string;
1185
+ /**
1186
+ * Check if a given emotion and intensity should use Rakkas (angry) font
1187
+ *
1188
+ * @param emotion - Emotion name (lowercase)
1189
+ * @param maxExpression - GTS-derived max expression (0-1)
1190
+ * @returns True if Rakkas font should be used
1191
+ */
1192
+ declare function shouldUseAngryFont(emotion: string, maxExpression: number): boolean;
1193
+ /**
1194
+ * Check if a given emotion and intensity should use Parkinsans (joy) font
1195
+ *
1196
+ * @param emotion - Emotion name (lowercase)
1197
+ * @param maxExpression - GTS-derived max expression (0-1)
1198
+ * @returns True if Parkinsans font should be used
1199
+ */
1200
+ declare function shouldUseJoyFont(emotion: string, maxExpression: number): boolean;
1201
+ /**
1202
+ * Check if a given emotion and intensity should use Cormorant (sadness) font
1203
+ *
1204
+ * @param emotion - Emotion name (lowercase)
1205
+ * @param maxExpression - GTS-derived max expression (0-1)
1206
+ * @returns True if Cormorant font should be used
1207
+ */
1208
+ declare function shouldUseSadnessFont(emotion: string, maxExpression: number): boolean;
1209
+
1210
+ /**
1211
+ * Letter Splitting Utilities
1212
+ *
1213
+ * Reusable utilities for splitting text into individual letters for per-letter animations
1214
+ */
1215
+ interface Letter {
1216
+ char: string;
1217
+ isSpace: boolean;
1218
+ index: number;
1219
+ }
1220
+ /**
1221
+ * Split text into individual letters for per-letter animation
1222
+ *
1223
+ * Preserves spaces as non-breaking spaces to maintain layout.
1224
+ * Each letter gets metadata for animation calculations.
1225
+ */
1226
+ declare function splitTextIntoLetters(text: string): Letter[];
1227
+
1228
+ /**
1229
+ * Motion blur from scale deviation — matches main app WordRenderer during movement.
1230
+ * Authored `blur` from animation curves is separate.
1231
+ */
1232
+ declare function getMotionBlurAmount(scale: number, { minDeviation, maxBlur, multiplier }?: {
1233
+ minDeviation?: number | undefined;
1234
+ maxBlur?: number | undefined;
1235
+ multiplier?: number | undefined;
1236
+ }): number;
1237
+ declare function getCombinedBlurAmount(authoredBlur?: number, scale?: number, gts?: number): number;
1238
+
1239
+ /**
1240
+ * Emotion Importance Processing
1241
+ *
1242
+ * Core emotions (anger, fear, sadness, joy) are prioritized over secondary emotions
1243
+ * by multiplying their intensity by 1.5 before selection.
1244
+ * Identical to monorepo app/utils/data/emotionImportance.js
1245
+ */
1246
+ /**
1247
+ * Process emotions with importance factors
1248
+ *
1249
+ * Core emotions (anger, fear, sadness, joy) get ×1.5 multiplier
1250
+ * Secondary emotions get ×1.0 multiplier
1251
+ *
1252
+ * @param emotions - Array of [emotion, intensity] tuples
1253
+ * @returns Array of [emotion, originalIntensity, adjustedIntensity, importanceFactor, isCore] tuples
1254
+ */
1255
+ declare function processEmotionsWithImportance(emotions: Array<[string, number]>): Array<[string, number, number, number, boolean]>;
1256
+
1257
+ /**
1258
+ * Backend Data Accessors
1259
+ *
1260
+ * Type-safe helpers for accessing backend subtitle data
1261
+ * Provides consistent interface and handles edge cases
1262
+ * Identical to monorepo app/utils/data/backendDataAccessors.js
1263
+ */
1264
+
1265
+ /**
1266
+ * Get word emphasis data
1267
+ */
1268
+ declare function getWordEmphasis(word: any): {
1269
+ confidence: any;
1270
+ emphasis: any;
1271
+ hasEmphasis: boolean;
1272
+ hasStrongEmphasis: boolean;
1273
+ };
1274
+ /**
1275
+ * Get subtitle text content
1276
+ *
1277
+ * Handles both 'subtitle' and 'text' properties
1278
+ */
1279
+ declare function getSubtitleText(subtitle: SubtitleSegment | null): string;
1280
+ /**
1281
+ * Get subtitle words array
1282
+ */
1283
+ declare function getSubtitleWords(subtitle: SubtitleSegment | null): any[];
1284
+ /**
1285
+ * Get subtitle animations/emotions
1286
+ */
1287
+ declare function getSubtitleAnimations(subtitle: SubtitleSegment | null): Record<string, number>;
1288
+ /**
1289
+ * Check if subtitle has merged subtitles
1290
+ */
1291
+ declare function hasMergedSubtitles(subtitle: SubtitleSegment | null): boolean;
1292
+ /**
1293
+ * Get merged subtitles array
1294
+ */
1295
+ declare function getMergedSubtitles(subtitle: SubtitleSegment | null): SubtitleSegment[];
1296
+ /**
1297
+ * Find subtitle containing a specific word based on timing
1298
+ *
1299
+ * Used for merged_subtitles to find which subtitle a word belongs to
1300
+ */
1301
+ declare function findSubtitleForWord(subtitles: SubtitleSegment[], word: any): SubtitleSegment | null;
1302
+
1303
+ /**
1304
+ * Variable Font Calculations
1305
+ *
1306
+ * Pure functions for calculating font weight/width from volume/speechRate
1307
+ * Extracted from app/utils/font/variableFontCalculations.js
1308
+ */
1309
+ interface FontAxisConfig {
1310
+ min: number;
1311
+ max: number;
1312
+ default: number;
1313
+ }
1314
+ interface FontConfig {
1315
+ axes?: {
1316
+ weight?: FontAxisConfig;
1317
+ width?: FontAxisConfig;
1318
+ };
1319
+ mapping?: {
1320
+ volume?: {
1321
+ min: number;
1322
+ max: number;
1323
+ };
1324
+ };
1325
+ }
1326
+ /**
1327
+ * Calculate font weight from volume
1328
+ * Maps volume (0-1) to weight range
1329
+ *
1330
+ * @param volume - Volume value (0-1)
1331
+ * @param weightAxis - Weight axis config from font
1332
+ * @param fontConfig - Font configuration object (optional)
1333
+ * @param baseWeight - Optional base weight from URL (if provided, volume variation is applied on top)
1334
+ * @returns Calculated font weight
1335
+ */
1336
+ declare function calculateWeightFromVolume(volume: number | undefined, weightAxis: FontAxisConfig | undefined, fontConfig?: FontConfig | null, baseWeight?: number): number;
1337
+ /**
1338
+ * Calculate font width from speech rate
1339
+ * Uses inverse relationship: slower speech = wider font
1340
+ * Maps speech rate (0-1) to width range
1341
+ *
1342
+ * @param speechRate - Speech rate value (0-1)
1343
+ * @param widthAxis - Width axis config from font
1344
+ * @param baseWidth - Optional base width from URL (if provided, speechRate variation is applied on top)
1345
+ * @returns Calculated font width
1346
+ */
1347
+ declare function calculateWidthFromSpeechRate(speechRate: number | undefined, widthAxis: FontAxisConfig | undefined, baseWidth?: number): number;
1348
+
1349
+ declare function ensureFont(fontFamily: string | undefined | null): void;
1350
+ declare function preloadAnimationFonts(): void;
1351
+
1352
+ /**
1353
+ * PHONT API Client
1354
+ *
1355
+ * Supports both authentication methods:
1356
+ * - API Keys: For server-side/programmatic access (POST/DELETE /vods, GET /vods/status)
1357
+ * - Session Tokens: For frontend/user access (GET /vods, GET /vods/{id}, GET /vod-subtitles/{id})
3
1358
  *
4
- * This file exports all utilities from the core package.
5
- * Add new exports here as you create new modules.
1359
+ * Tenant isolation is handled automatically by the backend based on credentials.
6
1360
  */
7
- export * from './validation/registration';
8
- export * from './types/registration';
9
- export * from './utils/video-handling';
10
- export { registerWithValidation } from './client/PhontClient';
1361
+
1362
+ interface LoginResponse {
1363
+ tenant_id: string;
1364
+ username: string;
1365
+ email: string;
1366
+ session_id: string;
1367
+ auth_token: string;
1368
+ }
1369
+ interface RegisterResponse {
1370
+ tenant_id: string;
1371
+ username: string;
1372
+ email: string;
1373
+ }
1374
+ interface VodMetadata {
1375
+ _id: string;
1376
+ tenant_id: string;
1377
+ manifest: string;
1378
+ thumbnail?: string;
1379
+ audio_url?: string;
1380
+ name: string;
1381
+ subtitle_available: boolean;
1382
+ created_at: string;
1383
+ }
1384
+ declare class PhontClient {
1385
+ private apiKey?;
1386
+ private authToken?;
1387
+ private sessionId?;
1388
+ readonly apiBaseUrl: string;
1389
+ constructor(options: PhontClientOptions);
1390
+ /**
1391
+ * Get authentication headers based on configured auth method
1392
+ */
1393
+ getAuthHeaders(): Record<string, string>;
1394
+ /**
1395
+ * Make an authenticated request
1396
+ */
1397
+ private request;
1398
+ /**
1399
+ * Login with credentials (for frontend/user access)
1400
+ * Returns session_id and auth_token for subsequent requests
1401
+ */
1402
+ login(identifier: string, password: string): Promise<LoginResponse>;
1403
+ /**
1404
+ * Register a new user
1405
+ */
1406
+ register(username: string, email: string, password: string, profile: {
1407
+ company: string;
1408
+ first_name: string;
1409
+ last_name: string;
1410
+ }): Promise<RegisterResponse>;
1411
+ /**
1412
+ * Get subtitle track for a VOD
1413
+ * Requires: Session token authentication
1414
+ * @param vodId - VOD ID
1415
+ * @param lang - Language code (optional). If not provided, returns original language subtitles.
1416
+ * Use null/undefined for original, or a valid language code for translations.
1417
+ * @param model - Model name for LLM translation (optional)
1418
+ */
1419
+ getSubtitleTrack(vodId: string, lang?: string | null, model?: string | null): Promise<any>;
1420
+ /**
1421
+ * Get VOD metadata
1422
+ * Requires: Session token authentication
1423
+ */
1424
+ getVod(vodId: string): Promise<VodMetadata>;
1425
+ /**
1426
+ * List user's VODs (tenant-isolated)
1427
+ * Requires: Session token authentication
1428
+ */
1429
+ listVods(skip?: number, limit?: number): Promise<VodMetadata[]>;
1430
+ /**
1431
+ * Create a new VOD
1432
+ * Requires: API key authentication
1433
+ */
1434
+ createVod(data: {
1435
+ manifest: string;
1436
+ thumbnail?: string;
1437
+ name: string;
1438
+ }): Promise<VodMetadata>;
1439
+ /**
1440
+ * Delete a VOD
1441
+ * Requires: API key authentication
1442
+ */
1443
+ deleteVod(vodId: string): Promise<void>;
1444
+ /**
1445
+ * Get VOD processing status
1446
+ * Requires: API key authentication
1447
+ */
1448
+ getVodStatus(vodId: string): Promise<{
1449
+ vod_id: string;
1450
+ status: string;
1451
+ }>;
1452
+ /**
1453
+ * Update credentials (useful after login)
1454
+ */
1455
+ setCredentials(options: {
1456
+ apiKey?: string;
1457
+ authToken?: string;
1458
+ sessionId?: string;
1459
+ }): void;
1460
+ /**
1461
+ * Clear credentials (for logout)
1462
+ */
1463
+ clearCredentials(): void;
1464
+ }
1465
+
1466
+ export { ANIMATION_STYLING, ANIMATION_URLS, AUTHORED_PLAYBACK_OPTIONS, type AnimationFetcher, type AnimationOptions, type AnimationParameterState, type AnimationParameterStates, type AnimationRecord, type AnimationState, type CurvePoint, EMOTION_ANIMATION_URLS, type Emotion, type EmotionSelectionResult, type EmotionStyle, type EmphasisEffect, type FontAxisConfig, type FontConfig, type JoyEffect, type Letter, type LoginResponse, type ParameterState, type ParameterStates, PhontClient, type PhontClientOptions, type PublishedAnimationResolver, REFERENCE_GTS, REFERENCE_INTENSITY, RENDERING_CONTROL, type RegisterResponse, type SentenceAnimationState, type StateCallback, type StrongEmphasisData, type Stylesheet, type SubtitleEngineState, type SubtitleLine, SubtitleManager, type SubtitleSegment, type SubtitleState$1 as SubtitleState, SubtitleStateManager, type SubtitleTrack, type SubtitleWord, type VodMetadata, type VodState$1 as VodState, type Word, type WordAnimationState, type WordLetterSpacingParams, calculateAnimationParameters, calculateAnimationState, calculateDynamicWordSpacing, calculateEmphasisMultiplier, calculateGTSValues, calculateJoy, calculateJoyBulge, calculateNormalizedTime, calculateWeightFromVolume, calculateWidthFromSpeechRate, calculateWordEmphasisEffects, calculateWordLetterSpacing, capEmotionIntensity, computeProsodyWeight, createPublishedAnimationResolver, decodeAnimationUrl, ensureFont, findSubtitleForWord, generateTextShadow, getAnimationUrlForEmotion, getCombinedBlurAmount, getFontFamilyCSS, getJoyAnimationMode, getJoyFilter, getJoyTransform, getMergedSubtitles, getMotionBlurAmount, getSubtitleAnimations, getSubtitleText, getSubtitleTiming, getSubtitleWords, getWordEmphasis, hasMergedSubtitles, hockeyStickIntensity, isInEmphasisRange, isInStrongEmphasisRange, meetsEmotionThreshold, preloadAnimationFonts, processEmotionsWithImportance, resolveEffectiveGts, sampleParameterCurve, selectEmotion, selectFontFamily, shouldUseAngryFont, shouldUseJoyFont, shouldUseSadnessFont, splitTextIntoLetters, subtitleManager };