@phont-ai/subtitles-core 0.1.10 → 0.2.1

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