@phont-ai/subtitles-core 0.1.10

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.
@@ -0,0 +1,1346 @@
1
+ /**
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
+ * ============================================
50
+ * SUBTITLE STATE MANAGER
51
+ * ============================================
52
+ * Ported from app/components/subtitle/state/SubtitleStateManager.js
53
+ */
54
+ interface SubtitleState$1 {
55
+ videoTimeSeed: number | null;
56
+ isVisible: boolean;
57
+ isKaraokeMode: boolean;
58
+ isSpeaker: boolean;
59
+ animationMode: "Sentence" | "Word";
60
+ currentSubtitle: string;
61
+ currentSequence: any | null;
62
+ subtitles: any[];
63
+ currentSubtitleData: any | null;
64
+ lastEndTime: number;
65
+ processedSequences: Set<number>;
66
+ initialSequenceOffset: number | null;
67
+ segmentDurations: Map<number, number>;
68
+ soundByteAll: any[];
69
+ soundByteImportant: any[];
70
+ soundByteFiltered: any[];
71
+ showAllSoundbites: boolean;
72
+ showImportantSoundbites: boolean;
73
+ currentTime: number;
74
+ liveStartTime: number | null;
75
+ liveCurrentTime: number;
76
+ soundbiteAnimations: any[];
77
+ showSoundbiteAnimations: boolean;
78
+ isClosedCaptionsEnabled: boolean;
79
+ }
80
+ interface VodState$1 {
81
+ subtitles: any[] | null;
82
+ soundByteAll: any[];
83
+ soundByteImportant: any[];
84
+ soundByteFiltered: any[];
85
+ currentLanguage: string | null;
86
+ videoId: string | null;
87
+ }
88
+ type StateCallback = (state: SubtitleState$1) => void;
89
+ declare class SubtitleStateManager {
90
+ subscribers: Set<StateCallback>;
91
+ state: SubtitleState$1;
92
+ vodState: VodState$1;
93
+ constructor();
94
+ /**
95
+ * Subscribe to state changes
96
+ */
97
+ subscribe(callback: StateCallback): () => void;
98
+ /**
99
+ * Notify all subscribers of state changes
100
+ */
101
+ notify(): void;
102
+ /**
103
+ * Get current state (immutable copy)
104
+ */
105
+ getState(): SubtitleState$1;
106
+ /**
107
+ * Get VOD state (immutable copy)
108
+ */
109
+ getVodState(): VodState$1;
110
+ toggleSubtitles(isVisible: boolean): void;
111
+ toggleClosedCaptions(isEnabled: boolean): void;
112
+ toggleKaraoke(isKaraokeMode: boolean): void;
113
+ toggleSpeaker(isSpeaker: boolean): void;
114
+ toggleSoundbites(type: "all" | "important", isVisible: boolean): void;
115
+ toggleSoundbiteAnimations(isVisible: boolean): void;
116
+ setAnimationMode(mode: "Sentence" | "Word"): void;
117
+ updateSubtitle(text: string): void;
118
+ updateCurrentTime(time: number): void;
119
+ updateSubtitles(subtitles: any[]): void;
120
+ updateCurrentSubtitleData(data: any): void;
121
+ updateSoundbites(all?: any[], important?: any[], filtered?: any[]): void;
122
+ updateSoundbiteAnimations(animations: any[]): void;
123
+ updateVodSubtitles(subtitles: any[]): void;
124
+ updateVodLanguage(language: string | null): void;
125
+ updateVodVideoId(videoId: string | null): void;
126
+ updateVodSoundbites(all?: any[], important?: any[], filtered?: any[]): void;
127
+ updateLiveStartTime(time: number | null): void;
128
+ updateLiveCurrentTime(time: number): void;
129
+ updateVideoTimeSeed(seed: number | null): void;
130
+ markSequenceProcessed(sequenceNumber: number): void;
131
+ isSequenceProcessed(sequenceNumber: number): boolean;
132
+ setInitialSequenceOffset(offset: number): void;
133
+ setSegmentDuration(sequenceNumber: number, duration: number): void;
134
+ getSegmentDuration(sequenceNumber: number): number | undefined;
135
+ updateCurrentSequence(sequence: any): void;
136
+ reset(): void;
137
+ resetControls(): void;
138
+ clearLiveState(): void;
139
+ clearVodState(): void;
140
+ }
141
+
142
+ /**
143
+ * ============================================
144
+ * SUBTITLE MANAGER (Orchestrator)
145
+ * ============================================
146
+ *
147
+ * PURPOSE:
148
+ * Central coordinator for subtitle system. Delegates to specialized classes.
149
+ *
150
+ * USED BY:
151
+ * - PhontSubtitles.jsx: Gets subtitle state and data
152
+ * - Player components: Controls playback and timing
153
+ * - PhontControls.jsx: UI toggles and settings
154
+ *
155
+ * DELEGATES TO:
156
+ * - SubtitleStateManager: State management and subscriptions
157
+ * - SubtitleCache: Language caching and detection
158
+ * - SubtitleFetcher: API requests & data normalization
159
+ * - SoundbiteProcessor: Soundbite/animation processing & laughter merging
160
+ * - VodDataProcessor: VOD data processing, deduplication, soundbite coordination
161
+ * - LiveDataProcessor: LIVE data processing, timing offsets, soundbite coordination
162
+ * - LiveStreamCumulativeTimeGenerator: Generates cumulative time for LIVE streams
163
+ * - CurrentSubtitle: Gets subtitle for time (BOTH VOD & LIVE) + karaoke
164
+ *
165
+ * STATUS: Phase 2 Complete - All 8 classes extracted, clean architecture, no duplication
166
+ */
167
+ type AnimationMode = "Sentence" | "Word" | string;
168
+ interface SubtitleState {
169
+ isVisible: boolean;
170
+ isKaraokeMode: boolean;
171
+ isSpeaker: boolean;
172
+ animationMode: AnimationMode;
173
+ currentSubtitle: string;
174
+ currentSequence: number | null;
175
+ subtitles: any[];
176
+ currentSubtitleData: any | null;
177
+ videoTimeSeed: number | null;
178
+ currentTime: number;
179
+ liveStartTime: number | null;
180
+ liveCurrentTime: number;
181
+ lastEndTime: number;
182
+ processedSequences: Set<number>;
183
+ initialSequenceOffset: number | null;
184
+ segmentDurations: Map<number, number>;
185
+ soundByteAll: any[];
186
+ soundByteImportant: any[];
187
+ soundByteFiltered: any[];
188
+ showAllSoundbites: boolean;
189
+ showImportantSoundbites: boolean;
190
+ soundbiteAnimations: any[];
191
+ showSoundbiteAnimations: boolean;
192
+ isClosedCaptionsEnabled: boolean;
193
+ isBackgroundEffectEnabled: boolean;
194
+ backgroundEffect: string;
195
+ backgroundOpacity: number;
196
+ transition: boolean;
197
+ [key: string]: any;
198
+ }
199
+ interface VodState {
200
+ subtitles: any[] | null;
201
+ url: string | null;
202
+ currentSubtitle: string;
203
+ currentSubtitleData: any | null;
204
+ currentSequence: number | null;
205
+ soundByteAll: any[];
206
+ soundByteImportant: any[];
207
+ soundByteFiltered: any[];
208
+ currentLanguage?: string | null;
209
+ videoId?: string | null;
210
+ translation_provider?: string | null;
211
+ translation_model?: string | null;
212
+ }
213
+ type SubtitleSubscriber = (state: SubtitleState) => void;
214
+ declare class SubtitleManager {
215
+ private stateManager;
216
+ private cache;
217
+ private fetcher;
218
+ private soundbiteProcessor;
219
+ private vodDataProcessor;
220
+ private liveTimeGenerator;
221
+ private liveDataProcessor;
222
+ private currentSubtitle;
223
+ constructor();
224
+ /**
225
+ * Set PhontClient for authenticated API requests
226
+ * This will be passed to SubtitleFetcher for use in API calls
227
+ */
228
+ setPhontClient(phontClient: any): void;
229
+ get state(): SubtitleState;
230
+ set state(value: SubtitleState);
231
+ get vodState(): VodState;
232
+ set vodState(value: VodState);
233
+ get subscribers(): Set<SubtitleSubscriber>;
234
+ subscribe(callback: SubtitleSubscriber): () => void;
235
+ notify(): void;
236
+ toggleSubtitles(isVisible: boolean): void;
237
+ toggleClosedCaptions(isEnabled: boolean): void;
238
+ toggleKaraoke(isKaraokeMode: boolean): void;
239
+ toggleSpeaker(isSpeaker: boolean): void;
240
+ toggleBackgroundEffect(isEnabled: boolean, effect?: string): void;
241
+ toggleSoundbites(type: "all" | "important", isVisible: boolean): void;
242
+ toggleSoundbiteAnimations(isVisible: boolean): void;
243
+ mapEventLabelToAnimation(eventLabel: string): string | null;
244
+ processSoundbiteAnimations(soundbites: any[]): any[];
245
+ setAnimationMode(mode: AnimationMode): void;
246
+ toggleTransition(enabled: boolean): void;
247
+ updateSubtitle(text: string): void;
248
+ updateLiveTime(videoTime?: number | null): number;
249
+ startLiveTiming(): void;
250
+ stopLiveTiming(): void;
251
+ pauseLiveTiming(): void;
252
+ resumeLiveTiming(): void;
253
+ resetLiveStream(videoElement: HTMLVideoElement): void;
254
+ getLiveTime(): number;
255
+ getState(): SubtitleState;
256
+ reset(): void;
257
+ resetControls(): void;
258
+ getCumulativeDuration(upToSequenceNumber: number): number;
259
+ normalizeSubtitles(data: any): any;
260
+ normalizeSoundbites(arr: any[]): any[];
261
+ getAudioSegmentDuration(sequenceNumber: number): Promise<number>;
262
+ getInitialLiveSequenceNumber(): Promise<number>;
263
+ fetchSubtitles(sequenceNumber: number): Promise<any | null>;
264
+ loadVodSubtitles(urlOrVodId: string, languageCode?: string | null): Promise<void>;
265
+ _processVodData(data: any, url: string, languageCode: string | null): void;
266
+ getSubtitleForTime(time: number): any;
267
+ getLiveSubtitleForTime(): any;
268
+ getCurrentSubtitleData(): any | null;
269
+ updateBackgroundOpacity(opacity: number): void;
270
+ clearOldSubtitles(currentTime: number, bufferSeconds?: number): void;
271
+ getQueueStatus(): {
272
+ totalSubtitles: number;
273
+ lastEndTime: number;
274
+ processedSequences: number[];
275
+ currentSequence: number | null;
276
+ };
277
+ setCurrentTime(time: number): void;
278
+ getActiveSoundbites(isVodMode?: boolean, vodTime?: number | null): any[];
279
+ getAllSoundbiteData(isVodMode?: boolean): any;
280
+ getActiveSoundbiteAnimations(isVodMode?: boolean): any[];
281
+ hasActiveAnimations(): boolean;
282
+ clearSoundbiteAnimations(): void;
283
+ clearOldAnimations(currentTime: number, bufferSeconds?: number): void;
284
+ getCurrentSpeaker(isVodMode?: boolean, vodTime?: number | null): string | null;
285
+ fetchSubtitleData(url: string, languageCode?: string | null, model?: string | null): Promise<void>;
286
+ switchSubtitleLanguage(languageCode: string | null, videoId: string, model?: string | null): Promise<void>;
287
+ _loadCachedSubtitles(data: any, url: string, languageCode: string | null): void;
288
+ getCurrentSubtitleLanguage(): string | null;
289
+ clearSubtitleCache(): void;
290
+ clearAudioPlaylistCache(): void;
291
+ updateState(): void;
292
+ }
293
+ declare const subtitleManager: SubtitleManager;
294
+
295
+ /**
296
+ * Animation Types
297
+ *
298
+ * Type definitions for animation parameter states, curves, and animation state
299
+ */
300
+ interface CurvePoint {
301
+ time: number;
302
+ value: number | string;
303
+ }
304
+ interface ParameterState {
305
+ points: CurvePoint[];
306
+ currentValue?: number | string;
307
+ }
308
+ interface ParameterStates {
309
+ [key: string]: ParameterState;
310
+ }
311
+ interface AnimationParameterStates {
312
+ parameterStates: ParameterStates;
313
+ duration?: number;
314
+ format?: 'normalized' | 'absolute';
315
+ }
316
+ interface AnimationState {
317
+ fontSize: number;
318
+ scale: number;
319
+ opacity: number;
320
+ slant: number;
321
+ weight: number;
322
+ width: number;
323
+ textColor: string;
324
+ translateX: number;
325
+ translateY: number;
326
+ stretchY: number;
327
+ rotation?: number;
328
+ skewX?: number;
329
+ fontVariationAxes?: Record<string, number>;
330
+ letterShake?: number;
331
+ letterSpacingOffsets?: number[];
332
+ shadowColor?: string;
333
+ }
334
+ interface SentenceAnimationState extends AnimationState {
335
+ text: string;
336
+ emotion: string;
337
+ emotionIntensity: number;
338
+ fontFamily: string;
339
+ startTime: number;
340
+ endTime: number;
341
+ duration: number;
342
+ isActive: boolean;
343
+ }
344
+ interface WordAnimationState {
345
+ text: string;
346
+ words: Array<{
347
+ word: string;
348
+ start: number;
349
+ end: number;
350
+ animationEnd: number;
351
+ isActive: boolean;
352
+ emotion: string;
353
+ emotionIntensity: number;
354
+ originalEmotionIntensity?: number;
355
+ fontFamily: string;
356
+ emphasis: number;
357
+ emphasisEffect?: any;
358
+ strongEmphasis?: any;
359
+ animationParameters: AnimationState;
360
+ joyAnimationMode?: 'letter' | 'letter-bulge' | 'word' | null;
361
+ joyProgress?: number | null;
362
+ }>;
363
+ currentWordIndex: number;
364
+ emotion: string;
365
+ emotionIntensity: number;
366
+ fontFamily: string;
367
+ startTime: number;
368
+ endTime: number;
369
+ isActive: boolean;
370
+ }
371
+ interface SubtitleSegment {
372
+ start_time: number;
373
+ end_time: number;
374
+ text?: string;
375
+ subtitle?: string;
376
+ words?: Array<{
377
+ word: string;
378
+ start: number;
379
+ end: number;
380
+ emphasis?: number;
381
+ }>;
382
+ animations?: Record<string, number>;
383
+ volume?: number;
384
+ speech_rate?: number;
385
+ merged_subtitles?: SubtitleSegment[];
386
+ }
387
+ interface AnimationOptions {
388
+ maxExpression?: number;
389
+ threshold?: number;
390
+ isRTL?: boolean;
391
+ volume?: number;
392
+ speechRate?: number;
393
+ nextSegmentStartTime?: number;
394
+ extendedEndTime?: number;
395
+ }
396
+
397
+ /**
398
+ * Emotion Selector
399
+ *
400
+ * Pure function version of useEmotionSelector (no React)
401
+ * Selects the highest-value emotion from backend animations object
402
+ * Core emotions (anger, fear, sadness, joy) receive ×1.5 importance multiplier
403
+ */
404
+ interface EmotionSelectionResult {
405
+ emotion: string;
406
+ intensity: number;
407
+ allEmotions: Array<[string, number, number, number, boolean]>;
408
+ importanceFactor: number;
409
+ isCore: boolean;
410
+ }
411
+ /**
412
+ * Find the highest emotion value from animations object
413
+ *
414
+ * Core emotions (anger, fear, sadness, joy, disgust) are multiplied by 1.5 before selection
415
+ * to prioritize fundamental emotions over secondary ones.
416
+ *
417
+ * @param animationsObject - Object with emotion keys and values (0-1)
418
+ * @param threshold - Minimum value to consider (GTS-controlled: 0.7 at GTS=0, 0.2 at GTS=1)
419
+ * @returns Emotion selection result
420
+ */
421
+ declare function selectEmotion(animationsObject: Record<string, number> | null | undefined, threshold?: number): EmotionSelectionResult;
422
+
423
+ /**
424
+ * Animation Engine
425
+ *
426
+ * Pure functions that calculate animation state from emotion curves
427
+ * Uses function-based EMOTION_ANIMATIONS (identical to monorepo)
428
+ */
429
+
430
+ /**
431
+ * Calculate sentence animation state
432
+ *
433
+ * Pure function version of useSentenceAnimation - uses function-based EMOTION_ANIMATIONS (identical to monorepo)
434
+ */
435
+ declare function calculateSentenceAnimation(segment: SubtitleSegment | null, currentTime: number, options?: AnimationOptions): SentenceAnimationState;
436
+ /**
437
+ * Calculate word animation state
438
+ *
439
+ * Pure function version of useWordAnimation - uses function-based EMOTION_ANIMATIONS (identical to monorepo)
440
+ */
441
+ declare function calculateWordAnimation(segment: SubtitleSegment | null, currentTime: number, options?: AnimationOptions): WordAnimationState;
442
+ /**
443
+ * Main function: Calculate animation state from segment and emotion
444
+ *
445
+ * This is the main entry point that uses function-based EMOTION_ANIMATIONS (identical to monorepo)
446
+ */
447
+ declare function calculateAnimationState(segment: SubtitleSegment | null, currentTime: number, mode: 'sentence' | 'word', options?: AnimationOptions): SentenceAnimationState | WordAnimationState;
448
+
449
+ /**
450
+ * Timing Utilities
451
+ *
452
+ * Pure functions for calculating subtitle timing
453
+ */
454
+
455
+ /**
456
+ * Get subtitle timing information
457
+ */
458
+ declare function getSubtitleTiming(subtitle: SubtitleSegment | null): {
459
+ startTime: number;
460
+ endTime: number;
461
+ duration: number;
462
+ };
463
+ /**
464
+ * Get subtitle text content
465
+ */
466
+ declare function getSubtitleText(subtitle: SubtitleSegment | null): string;
467
+ /**
468
+ * Calculate normalized time (t: 0→1) through subtitle duration
469
+ */
470
+ declare function calculateNormalizedTime(currentTime: number, startTime: number, duration: number): number;
471
+
472
+ /**
473
+ * **EMOTION_CURVES_DEFINITION** - Animation DNA
474
+ *
475
+ * Emotion Curves - Animation Parameter Definitions
476
+ *
477
+ * These curves define how subtitle text animates over time based on emotion.
478
+ * Each emotion has time-based functions that return values for visual parameters.
479
+ *
480
+ * PARAMETERS:
481
+ * - fontSize: Always constant (ANIMATION_STYLING.fontSize) for layout stability
482
+ * - scale: Size multiplier applied via CSS transform (prevents text wrapping)
483
+ * - slant: Italic/skew effect in degrees (applied as CSS skewX)
484
+ * - weight: Font weight (100-900) - overridden by VariableFontContext in practice
485
+ * - translateX/Y: Movement in pixels (direct CSS values)
486
+ * - textColor: Text color (rgba)
487
+ * - stretchY: Vertical stretch multiplier (CSS scaleY)
488
+ * - letterShakeIntensity: Per-letter shake amount in pixels (fear)
489
+ *
490
+ * WHY SCALE INSTEAD OF FONTSIZE:
491
+ * Using transform: scale() instead of fontSize changes allows the layout engine
492
+ * to calculate word positions ONCE at the base fontSize, preventing words from
493
+ * wrapping to new lines mid-animation. Scale is GPU-accelerated and doesn't trigger reflow.
494
+ *
495
+ * ⚠️ Parameters NOT included (handled elsewhere):
496
+ * - opacity: Always 1.0 (fade in/out handled by controllers)
497
+ * - shadows: Handled by styling.js ANIMATION_STYLING.shadow (emotion-specific for disgust/angry)
498
+ * - font family: Handled by fontSelection.js based on emotion + intensity
499
+ * - transitions: Handled by styling.js ANIMATION_STYLING.transition
500
+ * - marginRight: Handled by calculateDynamicWordSpacing() based on current scale
501
+ */
502
+ declare const EMOTION_ANIMATIONS: Record<string, any>;
503
+
504
+ /**
505
+ * Animation Defaults - Visual Styling Layer
506
+ *
507
+ * All subtitle animation VISUAL defaults in one place
508
+ * This is where you edit colors, shadows, font names, and CSS values
509
+ *
510
+ * 🎯 HOW TO CONFIGURE:
511
+ * - Change default font size: Edit ANIMATION_STYLING.fontSize
512
+ * - Change default colors/shadows: Edit ANIMATION_STYLING.textColor, shadow, etc.
513
+ *
514
+ * 🔧 FOR BEHAVIOR/TIMING:
515
+ * - Change when Rakkas (angry) font appears: Edit RENDERING_CONTROL.emotion.angryFontThreshold
516
+ * - Change effect timing/intensity: Edit renderingControl.ts
517
+ */
518
+ declare const ANIMATION_STYLING: {
519
+ fontSize: number;
520
+ opacity: number;
521
+ weight: number;
522
+ slant: number;
523
+ textColor: string;
524
+ translateX: number;
525
+ translateY: number;
526
+ defaultFont: string;
527
+ angryFont: string;
528
+ joyFont: string;
529
+ sadnessFont: string;
530
+ shadow: {
531
+ offsetX: number;
532
+ offsetY: number;
533
+ blur: number;
534
+ color: string;
535
+ };
536
+ angryFontStyle: {
537
+ color: string;
538
+ strokeColor: string;
539
+ strokeWidth: number;
540
+ shadowOffsetX: number;
541
+ shadowOffsetY: number;
542
+ shadowBlur: number;
543
+ shadowBlurMin: number;
544
+ shadowBlurMax: number;
545
+ shadowOpacityMin: number;
546
+ shadowOpacityMax: number;
547
+ shadowColor: string;
548
+ };
549
+ disgustEmotion: {
550
+ shadowOffsetX: number;
551
+ shadowOffsetY: number;
552
+ shadowBlur: number;
553
+ shadowColor: string;
554
+ };
555
+ transition: {
556
+ duration: number;
557
+ easing: string;
558
+ };
559
+ lineHeight: number;
560
+ letterSpacing: string;
561
+ wordSpacing: {
562
+ min: string;
563
+ max: string;
564
+ };
565
+ fontSpacingAdjustments: {
566
+ 'Roboto Flex': {
567
+ letterSpacing: string;
568
+ wordSpacingMultiplier: number;
569
+ };
570
+ Parkinsans: {
571
+ letterSpacing: string;
572
+ wordSpacingMultiplier: number;
573
+ fontSizeOffset: number;
574
+ };
575
+ Rakkas: {
576
+ letterSpacing: string;
577
+ wordSpacingMultiplier: number;
578
+ };
579
+ Cormorant: {
580
+ letterSpacing: string;
581
+ wordSpacingMultiplier: number;
582
+ };
583
+ };
584
+ position: {
585
+ bottom: string;
586
+ horizontalAlign: string;
587
+ };
588
+ wordAnimation: {
589
+ minDisplayTime: number;
590
+ allowOverlap: boolean;
591
+ };
592
+ };
593
+
594
+ /**
595
+ * Rendering Configuration
596
+ *
597
+ * Central configuration for animation system timing, thresholds, and behavior
598
+ * This is the single source of truth for non-visual animation parameters
599
+ */
600
+ declare const RENDERING_CONTROL: {
601
+ modes: {
602
+ word: string;
603
+ sentence: string;
604
+ };
605
+ timing: {
606
+ minWordDisplayTime: number;
607
+ breathingRoomDuration: number;
608
+ breathingRoomMinGap: number;
609
+ breathingRoomLargeGap: number;
610
+ timeRangeTolerance: number;
611
+ };
612
+ emotion: {
613
+ defaultThreshold: number;
614
+ maxThreshold: number;
615
+ angryFontThreshold: number;
616
+ angryEmotions: string[];
617
+ joyFontThreshold: number;
618
+ joyEmotions: string[];
619
+ sadnessFontThreshold: number;
620
+ sadnessEmotions: string[];
621
+ };
622
+ emphasis: {
623
+ minThreshold: number;
624
+ maxThreshold: number;
625
+ scaleMultiplier: number;
626
+ scaleUpFrames: number;
627
+ scaleDownFrames: number;
628
+ fps: number;
629
+ };
630
+ strongEmphasis: {
631
+ minThreshold: number;
632
+ maxScale: number;
633
+ maxRotation: number;
634
+ maxTranslateY: number;
635
+ maxTranslateX: number;
636
+ maxSkew: number;
637
+ maxBrightness: number;
638
+ maxContrast: number;
639
+ maxShadowBlur: number;
640
+ shadowOpacity: number;
641
+ shadowOffsetX: number;
642
+ shadowOffsetY: number;
643
+ maxZIndex: number;
644
+ outwardPushTranslateX: number;
645
+ outwardPushRotation: number;
646
+ outwardPushSkew: number;
647
+ fadeOutTailDuration: number;
648
+ };
649
+ fear: {};
650
+ joy: {
651
+ maxWordsForLetterAnimation: number;
652
+ minWordsForWordAnimation: number;
653
+ letterWave: {
654
+ innerWidthRatio: number;
655
+ outerWidthRatio: number;
656
+ maxScale: number;
657
+ maxRotation: number;
658
+ maxTranslateY: number;
659
+ maxTranslateX: number;
660
+ maxSkew: number;
661
+ maxBrightness: number;
662
+ maxContrast: number;
663
+ maxShadowBlur: number;
664
+ shadowOpacity: number;
665
+ maxZIndex: number;
666
+ };
667
+ letterBulge: {
668
+ innerWidthRatio: number;
669
+ outerWidthRatio: number;
670
+ maxScale: number;
671
+ maxStretchY: number;
672
+ maxCompressionX: number;
673
+ maxTranslateY: number;
674
+ maxTranslateX: number;
675
+ maxSkew: number;
676
+ maxRotation: number;
677
+ maxBrightness: number;
678
+ maxContrast: number;
679
+ maxShadowBlur: number;
680
+ shadowOpacity: number;
681
+ maxZIndex: number;
682
+ };
683
+ wordLevel: {
684
+ duration: number;
685
+ easeType: string;
686
+ maxScale: number;
687
+ maxRotation: number;
688
+ maxTranslateY: number;
689
+ maxTranslateX: number;
690
+ maxSkew: number;
691
+ maxBrightness: number;
692
+ maxContrast: number;
693
+ maxShadowBlur: number;
694
+ shadowOpacity: number;
695
+ maxZIndex: number;
696
+ transitionDuration: number;
697
+ };
698
+ };
699
+ debug: {
700
+ showBorders: boolean;
701
+ showTimings: boolean;
702
+ logRendering: boolean;
703
+ };
704
+ };
705
+
706
+ /**
707
+ * Shared Animation Parameter Calculation Utility
708
+ *
709
+ * Calculates animation parameters from emotion curves.
710
+ * Shared by calculateWordAnimation and calculateSentenceAnimation to eliminate duplication.
711
+ *
712
+ * This utility extracts all animation parameters defined in EMOTION_ANIMATIONS
713
+ * including variation axes (YOPQ, YTAS) that are separate from voice axes (wght, wdth).
714
+ *
715
+ * TERMINOLOGY:
716
+ * - **Voice Axes**: Font axes driven by audio analysis (volume→wght, speech_rate→wdth)
717
+ * - **Variation Axes**: Font axes driven by emotion animations (YOPQ, YTAS from emotion curves)
718
+ */
719
+ interface VariableFontStyle {
720
+ fontWeight?: number;
721
+ fontStretch?: string;
722
+ }
723
+ interface AnimationParameters {
724
+ fontSize: number;
725
+ scale: number;
726
+ opacity: number;
727
+ slant: number;
728
+ weight: number;
729
+ width: number;
730
+ textColor: string;
731
+ translateX: number;
732
+ translateY: number;
733
+ stretchY: number;
734
+ letterShake?: number;
735
+ fontVariationAxes?: Record<string, number>;
736
+ shadowColor?: string | ((t: number, intensity: number) => string);
737
+ }
738
+ interface EmotionCurve {
739
+ fontSize?: number | ((t: number, intensity: number) => number);
740
+ scale?: number | ((t: number, intensity: number) => number);
741
+ opacity?: number | ((t: number, intensity: number) => number);
742
+ slant?: number | ((t: number, intensity?: number) => number);
743
+ weight?: number | ((t: number, intensity?: number) => number);
744
+ translateX?: number | ((t: number, intensity: number) => number);
745
+ translateY?: number | ((t: number, intensity: number) => number);
746
+ stretchY?: number | ((t: number, intensity: number) => number);
747
+ textColor?: string | ((t: number, intensity: number) => string);
748
+ shadowColor?: string | ((t: number, intensity: number) => string);
749
+ thinStroke?: (t: number, intensity: number, gtsValue: number) => number;
750
+ ascenderHeight?: (t: number, intensity: number, gtsValue: number) => number;
751
+ letterShake?: (t: number, intensity: number, gtsValue: number) => number;
752
+ }
753
+ /**
754
+ * Calculate animation parameters from emotion curve
755
+ *
756
+ * @param emotionCurve - Emotion curve from EMOTION_ANIMATIONS
757
+ * @param t - Normalized time (0-1) through animation
758
+ * @param intensity - Emotion intensity (0-1), capped by GTS (maxExpression)
759
+ * @param variableFontStyle - Font style from variable font calculations
760
+ * @param gtsValue - GTS maxExpression value (0.2-1.0) for scaling ranges
761
+ * @param selectedFontFamily - The selected font family (e.g., 'Cormorant', 'Roboto Flex')
762
+ * @returns Complete animation parameters including fontVariationAxes (variation axes, not voice axes)
763
+ */
764
+ declare function calculateAnimationParameters(emotionCurve: EmotionCurve, t: number, intensity: number, variableFontStyle?: VariableFontStyle | null, gtsValue?: number, selectedFontFamily?: string): AnimationParameters;
765
+
766
+ /**
767
+ * Shadow Generator
768
+ *
769
+ * Generates CSS text-shadow strings from shadow colors and emotion-specific styling
770
+ */
771
+ /**
772
+ * Generate CSS text-shadow string from shadow color
773
+ *
774
+ * @param shadowColor - Shadow color in hex format (e.g., '#ff3232')
775
+ * @param fontFamily - Font family to determine shadow style
776
+ * @param emotion - Current emotion to determine shadow style
777
+ * @param intensity - Emotion intensity (0-1) to scale glow effects
778
+ * @returns CSS text-shadow value
779
+ */
780
+ declare function generateTextShadow(shadowColor: string | undefined, fontFamily?: string, emotion?: string, intensity?: number): string;
781
+
782
+ /**
783
+ * Dynamic Word Spacing Calculator
784
+ *
785
+ * Calculates word spacing that grows with animation scale and GTS intensity
786
+ * Ported from app/config/animation/styling.js
787
+ */
788
+ /**
789
+ * Calculate dynamic word spacing that grows with scale
790
+ *
791
+ * @param currentScale - Current scale value from animation (e.g., 1.0 to 1.5)
792
+ * @param fontFamily - Font family name for spacing adjustments
793
+ * @param gtsIntensity - GTS intensity (0-1) - currently unused for layout preservation
794
+ * @param emotion - Current emotion type (for anger-specific spacing)
795
+ * @returns CSS em value for marginRight
796
+ */
797
+ declare function calculateDynamicWordSpacing(currentScale?: number, fontFamily?: string, gtsIntensity?: number, emotion?: string): string;
798
+
799
+ /**
800
+ * Joy Animation Effects
801
+ *
802
+ * Per-letter joy pressure effects for wave and bulge animations
803
+ * Ported from app/utils/animation/joy.js
804
+ */
805
+ /**
806
+ * Determine which joy animation mode to use based on word count
807
+ */
808
+ declare function getJoyAnimationMode(wordCount: number): 'letter' | 'letter-bulge' | 'word';
809
+ interface JoyEffect {
810
+ pressure: number;
811
+ scale: number;
812
+ rotation: number;
813
+ translateX: number;
814
+ translateY: number;
815
+ skewX: number;
816
+ stretchY: number;
817
+ stretchX?: number;
818
+ brightness: number;
819
+ contrast: number;
820
+ shadowBlur: number;
821
+ shadowOpacity: number;
822
+ zIndex: number;
823
+ shadowOffsetX?: number;
824
+ shadowOffsetY?: number;
825
+ bulgeIntensity?: number;
826
+ }
827
+ /**
828
+ * Calculate per-letter joy pressure based on word progress and letter position
829
+ */
830
+ declare function calculateJoy(progress: number, letterIndex: number, totalLetters: number, gtsMultiplier?: number): JoyEffect | null;
831
+ /**
832
+ * Calculate joy bulge effect (for multi-word subtitles)
833
+ */
834
+ declare function calculateJoyBulge(progress: number, letterIndex: number, totalLetters: number, gtsMultiplier?: number): JoyEffect | null;
835
+ /**
836
+ * Generate CSS transform string from joy effect
837
+ */
838
+ declare function getJoyTransform(joyEffect: JoyEffect | null): string;
839
+ /**
840
+ * Generate CSS filter string from joy effect
841
+ */
842
+ declare function getJoyFilter(joyEffect: JoyEffect | null): string;
843
+
844
+ /**
845
+ * GTS (Global Text Style) Calculations
846
+ *
847
+ * Centralized logic for GTS-derived values and emotion threshold calculations
848
+ */
849
+ /**
850
+ * Calculate all GTS-derived values from slider position
851
+ *
852
+ * GTS slider range: 0 to 1
853
+ * - GTS = 0 → maxExpression = 0.2, threshold = 0.7 (conservative)
854
+ * - GTS = 1 → maxExpression = 1.0, threshold = 0.2 (expressive)
855
+ *
856
+ * @param gts - GTS slider value (0-1)
857
+ * @returns All derived GTS values
858
+ */
859
+ declare function calculateGTSValues(gts: number): {
860
+ gts: number;
861
+ maxExpression: number;
862
+ threshold: number;
863
+ shouldEnableAngryFont: boolean;
864
+ };
865
+ /**
866
+ * Check if emotion intensity meets the threshold
867
+ *
868
+ * @param intensity - Raw emotion intensity from backend (0-1)
869
+ * @param threshold - GTS-calculated threshold (0-1)
870
+ * @returns True if intensity is above threshold
871
+ */
872
+ declare function meetsEmotionThreshold(intensity: number, threshold: number): boolean;
873
+ /**
874
+ * Calculate capped emotion intensity based on GTS maxExpression
875
+ *
876
+ * @param rawIntensity - Raw emotion intensity from backend (0-1)
877
+ * @param maxExpression - GTS-derived max expression cap (0-1)
878
+ * @returns Capped emotion intensity (0-1)
879
+ */
880
+ declare function capEmotionIntensity(rawIntensity: number, maxExpression: number): number;
881
+ /**
882
+ * Calculate emphasis multiplier within valid emphasis range
883
+ *
884
+ * Maps emphasis value within [minThreshold, maxThreshold] to [0, 1]
885
+ * Used for scaling emphasis effects based on position in range
886
+ *
887
+ * @param emphasis - Word emphasis value (0-1)
888
+ * @returns Emphasis multiplier (0-1), or 0 if outside range
889
+ */
890
+ declare function calculateEmphasisMultiplier(emphasis: number): number;
891
+ /**
892
+ * Check if emphasis value is in valid range for emphasis animation
893
+ *
894
+ * @param emphasis - Word emphasis value (0-1)
895
+ * @returns True if in emphasis range [0.5, 0.7)
896
+ */
897
+ declare function isInEmphasisRange(emphasis: number): boolean;
898
+ /**
899
+ * Check if emphasis value is in valid range for strong emphasis animation
900
+ *
901
+ * @param emphasis - Word emphasis value (0-1)
902
+ * @returns True if in strong emphasis range [0.7, 1.0]
903
+ */
904
+ declare function isInStrongEmphasisRange(emphasis: number): boolean;
905
+
906
+ /**
907
+ * Emotion Animation URLs
908
+ *
909
+ * Config mapping emotion → animation URL
910
+ * Replaces hardcoded EMOTION_ANIMATIONS config with URL-based parameters
911
+ */
912
+ declare const EMOTION_ANIMATION_URLS: Record<string, string>;
913
+
914
+ /**
915
+ * Font Selection Utilities
916
+ *
917
+ * Centralized logic for selecting fonts based on emotion and intensity
918
+ * Eliminates duplicate font selection code across controllers
919
+ */
920
+ interface Stylesheet {
921
+ name?: string;
922
+ fonts?: Record<string, string>;
923
+ }
924
+ /**
925
+ * Select font family based on emotion and GTS intensity
926
+ *
927
+ * @param emotion - Emotion name (lowercase, e.g., 'anger', 'joy', 'sadness')
928
+ * @param maxExpression - GTS-derived max expression (0-1)
929
+ * @param emotionCategory - Emotion category for children stylesheet (shout, whisper, anger, fear, neutral)
930
+ * @param stylesheet - Active stylesheet config (optional)
931
+ * @returns Font family name
932
+ */
933
+ declare function selectFontFamily(emotion: string, maxExpression: number, emotionCategory?: string | null, stylesheet?: Stylesheet | null): string;
934
+ /**
935
+ * Get CSS-ready font-family value
936
+ *
937
+ * @param emotion - Emotion name (lowercase)
938
+ * @param maxExpression - GTS-derived max expression (0-1)
939
+ * @param emotionCategory - Emotion category for children stylesheet
940
+ * @param stylesheet - Active stylesheet config (optional)
941
+ * @returns CSS font-family value
942
+ */
943
+ declare function getFontFamilyCSS(emotion: string, maxExpression: number, emotionCategory?: string | null, stylesheet?: Stylesheet | null): string;
944
+ /**
945
+ * Check if a given emotion and intensity should use Rakkas (angry) font
946
+ *
947
+ * @param emotion - Emotion name (lowercase)
948
+ * @param maxExpression - GTS-derived max expression (0-1)
949
+ * @returns True if Rakkas font should be used
950
+ */
951
+ declare function shouldUseAngryFont(emotion: string, maxExpression: number): boolean;
952
+ /**
953
+ * Check if a given emotion and intensity should use Parkinsans (joy) font
954
+ *
955
+ * @param emotion - Emotion name (lowercase)
956
+ * @param maxExpression - GTS-derived max expression (0-1)
957
+ * @returns True if Parkinsans font should be used
958
+ */
959
+ declare function shouldUseJoyFont(emotion: string, maxExpression: number): boolean;
960
+ /**
961
+ * Check if a given emotion and intensity should use Cormorant (sadness) font
962
+ *
963
+ * @param emotion - Emotion name (lowercase)
964
+ * @param maxExpression - GTS-derived max expression (0-1)
965
+ * @returns True if Cormorant font should be used
966
+ */
967
+ declare function shouldUseSadnessFont(emotion: string, maxExpression: number): boolean;
968
+
969
+ /**
970
+ * Letter Splitting Utilities
971
+ *
972
+ * Reusable utilities for splitting text into individual letters for per-letter animations
973
+ */
974
+ interface Letter {
975
+ char: string;
976
+ isSpace: boolean;
977
+ index: number;
978
+ }
979
+ /**
980
+ * Split text into individual letters for per-letter animation
981
+ *
982
+ * Preserves spaces as non-breaking spaces to maintain layout.
983
+ * Each letter gets metadata for animation calculations.
984
+ */
985
+ declare function splitTextIntoLetters(text: string): Letter[];
986
+
987
+ /**
988
+ * Word Emphasis Effects
989
+ *
990
+ * Calculates emphasis effects (standard and strong) for words based on confidence levels.
991
+ *
992
+ * USED BY:
993
+ * - calculateWordAnimation: Main word animation orchestrator
994
+ *
995
+ * HOW IT WORKS:
996
+ * 1. Standard Emphasis (50-70%): Quick "pop" effect at word start
997
+ * 2. Strong Emphasis (70%+): Per-letter dramatic effects throughout word
998
+ * 3. Both scale with GTS (expressiveness level) and word confidence
999
+ *
1000
+ * KEY FEATURES:
1001
+ * - Confidence-based thresholds (50-70% vs 70%+)
1002
+ * - GTS-scaled intensity
1003
+ * - Emotion slant inheritance
1004
+ * - Smooth transitions
1005
+ */
1006
+ interface Word {
1007
+ emphasis?: number;
1008
+ start: number;
1009
+ end: number;
1010
+ animationEnd?: number;
1011
+ }
1012
+ interface EmphasisEffect {
1013
+ scaleMultiplier: number;
1014
+ slantAdditive: number;
1015
+ weightBoost: number;
1016
+ scaleProgress: number;
1017
+ gtsMultiplier: number;
1018
+ emphasis: number;
1019
+ emphasisMultiplier: number;
1020
+ elapsed: number;
1021
+ scaleActive: boolean;
1022
+ }
1023
+ interface StrongEmphasisData {
1024
+ emphasis: number;
1025
+ rawEmphasis: number;
1026
+ scale: number;
1027
+ rotation: number;
1028
+ translateX: number;
1029
+ translateY: number;
1030
+ skewX: number;
1031
+ stretchY: number;
1032
+ brightness: number;
1033
+ contrast: number;
1034
+ shadowBlur: number;
1035
+ shadowOpacity: number;
1036
+ zIndex: number;
1037
+ weightBoost: number;
1038
+ shadowOffsetX: number;
1039
+ shadowOffsetY: number;
1040
+ gtsMultiplier: number;
1041
+ emphasisMultiplier: number;
1042
+ fadeFactor: number;
1043
+ isInTail: boolean;
1044
+ }
1045
+ /**
1046
+ * Calculate emphasis effects for a word
1047
+ *
1048
+ * @param params - Emphasis calculation parameters
1049
+ * @returns Emphasis effects { emphasisEffect, strongEmphasisData }
1050
+ */
1051
+ declare function calculateWordEmphasisEffects({ word, currentTime, maxExpression, emotionAnimationDuration, wordBaseSlant, isActive, wordEmotion, }: {
1052
+ word: Word;
1053
+ currentTime: number;
1054
+ maxExpression: number;
1055
+ emotionAnimationDuration: number;
1056
+ wordBaseSlant: number;
1057
+ isActive: boolean;
1058
+ wordEmotion?: string;
1059
+ }): {
1060
+ emphasisEffect: EmphasisEffect | null;
1061
+ strongEmphasisData: StrongEmphasisData | null;
1062
+ };
1063
+
1064
+ /**
1065
+ * ============================================
1066
+ * WORD LETTER SPACING
1067
+ * ============================================
1068
+ *
1069
+ * PURPOSE:
1070
+ * Calculates per-letter translateX offsets for GPU-accelerated spacing (no layout reflow).
1071
+ * Extracted from animationEngine for better maintainability.
1072
+ *
1073
+ * USED BY:
1074
+ * - animationEngine: Main word animation orchestrator
1075
+ *
1076
+ * HOW IT WORKS:
1077
+ * 1. Detects active effects (fear shake only - emphasis spacing is disabled)
1078
+ * 2. Calculates per-letter translateX offsets to create spacing between letters
1079
+ * 3. Returns array of translateX values in pixels (one per letter)
1080
+ *
1081
+ * KEY FEATURES:
1082
+ * - Transform-based spacing (GPU-accelerated, no layout reflow)
1083
+ * - Letter spacing disabled for emphasis effects (standard and strong)
1084
+ * - Only fear shake effects get letter spacing (if needed)
1085
+ * - Zero baseline for clean transitions
1086
+ *
1087
+ * PERFORMANCE BENEFITS:
1088
+ * - Uses translateX (compositor property) instead of letter-spacing (layout property)
1089
+ * - Smooth CSS transitions without triggering layout reflow
1090
+ * - All spacing animations run on GPU compositor layer
1091
+ */
1092
+ interface WordLetterSpacingParams {
1093
+ strongEmphasisData?: any;
1094
+ emphasisEffect?: any;
1095
+ wordLetterShakeIntensity: number | null;
1096
+ wordFontFamily: string;
1097
+ currentTime: number;
1098
+ word: any;
1099
+ emotionAnimationDuration: number;
1100
+ wordEmotion?: string;
1101
+ totalLetters: number;
1102
+ fontSize: number;
1103
+ }
1104
+ /**
1105
+ * Calculate per-letter translateX offsets for GPU-accelerated spacing
1106
+ *
1107
+ * @param params - Spacing calculation parameters
1108
+ * @returns Array of translateX offsets in pixels (one per letter)
1109
+ */
1110
+ declare function calculateWordLetterSpacing({ strongEmphasisData, emphasisEffect, wordLetterShakeIntensity, wordFontFamily, currentTime, word, emotionAnimationDuration, wordEmotion, totalLetters, fontSize, }: WordLetterSpacingParams): number[];
1111
+
1112
+ /**
1113
+ * Backend Data Accessors
1114
+ *
1115
+ * Type-safe helpers for accessing backend subtitle data
1116
+ * Provides consistent interface and handles edge cases
1117
+ * Identical to monorepo app/utils/data/backendDataAccessors.js
1118
+ */
1119
+
1120
+ /**
1121
+ * Get subtitle words array
1122
+ */
1123
+ declare function getSubtitleWords(subtitle: SubtitleSegment | null): any[];
1124
+ /**
1125
+ * Get subtitle animations/emotions
1126
+ */
1127
+ declare function getSubtitleAnimations(subtitle: SubtitleSegment | null): Record<string, number>;
1128
+ /**
1129
+ * Check if subtitle has merged subtitles
1130
+ */
1131
+ declare function hasMergedSubtitles(subtitle: SubtitleSegment | null): boolean;
1132
+ /**
1133
+ * Get merged subtitles array
1134
+ */
1135
+ declare function getMergedSubtitles(subtitle: SubtitleSegment | null): SubtitleSegment[];
1136
+ /**
1137
+ * Find subtitle containing a specific word based on timing
1138
+ *
1139
+ * Used for merged_subtitles to find which subtitle a word belongs to
1140
+ */
1141
+ declare function findSubtitleForWord(subtitles: SubtitleSegment[], word: any): SubtitleSegment | null;
1142
+
1143
+ /**
1144
+ * Emotion Importance Processing
1145
+ *
1146
+ * Core emotions (anger, fear, sadness, joy) are prioritized over secondary emotions
1147
+ * by multiplying their intensity by 1.5 before selection.
1148
+ * Identical to monorepo app/utils/data/emotionImportance.js
1149
+ */
1150
+ /**
1151
+ * Process emotions with importance factors
1152
+ *
1153
+ * Core emotions (anger, fear, sadness, joy) get ×1.5 multiplier
1154
+ * Secondary emotions get ×1.0 multiplier
1155
+ *
1156
+ * @param emotions - Array of [emotion, intensity] tuples
1157
+ * @returns Array of [emotion, originalIntensity, adjustedIntensity, importanceFactor, isCore] tuples
1158
+ */
1159
+ declare function processEmotionsWithImportance(emotions: Array<[string, number]>): Array<[string, number, number, number, boolean]>;
1160
+
1161
+ /**
1162
+ * Variable Font Calculations
1163
+ *
1164
+ * Pure functions for calculating font weight/width from volume/speechRate
1165
+ * Extracted from app/utils/font/variableFontCalculations.js
1166
+ */
1167
+ interface FontAxisConfig {
1168
+ min: number;
1169
+ max: number;
1170
+ default: number;
1171
+ }
1172
+ interface FontConfig {
1173
+ axes?: {
1174
+ weight?: FontAxisConfig;
1175
+ width?: FontAxisConfig;
1176
+ };
1177
+ mapping?: {
1178
+ volume?: {
1179
+ min: number;
1180
+ max: number;
1181
+ };
1182
+ };
1183
+ }
1184
+ /**
1185
+ * Calculate font weight from volume
1186
+ * Maps volume (0-1) to weight range
1187
+ *
1188
+ * @param volume - Volume value (0-1)
1189
+ * @param weightAxis - Weight axis config from font
1190
+ * @param fontConfig - Font configuration object (optional)
1191
+ * @param baseWeight - Optional base weight from URL (if provided, volume variation is applied on top)
1192
+ * @returns Calculated font weight
1193
+ */
1194
+ declare function calculateWeightFromVolume(volume: number | undefined, weightAxis: FontAxisConfig | undefined, fontConfig?: FontConfig | null, baseWeight?: number): number;
1195
+ /**
1196
+ * Calculate font width from speech rate
1197
+ * Uses inverse relationship: slower speech = wider font
1198
+ * Maps speech rate (0-1) to width range
1199
+ *
1200
+ * @param speechRate - Speech rate value (0-1)
1201
+ * @param widthAxis - Width axis config from font
1202
+ * @param baseWidth - Optional base width from URL (if provided, speechRate variation is applied on top)
1203
+ * @returns Calculated font width
1204
+ */
1205
+ declare function calculateWidthFromSpeechRate(speechRate: number | undefined, widthAxis: FontAxisConfig | undefined, baseWidth?: number): number;
1206
+
1207
+ /**
1208
+ * Subtitle Engine
1209
+ *
1210
+ * TODO: Extract from app/hooks/subtitle/useSentenceAnimation.js and useWordAnimation.js
1211
+ *
1212
+ * This module should provide:
1213
+ * - createSubtitleEngine(track) - creates engine instance
1214
+ * - engine.at(time) - returns SubtitleEngineState for given time
1215
+ * - Handle karaoke mode (word-by-word) and sentence mode
1216
+ * - Handle edge cases: gaps, overlaps, end-of-video
1217
+ */
1218
+
1219
+ declare function createSubtitleEngine(track: SubtitleTrack): {
1220
+ at(time: number): SubtitleEngineState;
1221
+ };
1222
+
1223
+ /**
1224
+ * Emotion → Style Mapper
1225
+ *
1226
+ * TODO: Extract from app/config/animation/ directory
1227
+ *
1228
+ * This module should provide:
1229
+ * - getEmotionStyle(emotion, intensity) - maps emotion to CSS styles
1230
+ * - Handle all 6 emotions: joy, anger, sadness, fear, disgust, surprise
1231
+ * - Support intensity scaling (0-1)
1232
+ */
1233
+
1234
+ declare function getEmotionStyle(emotion: Emotion, intensity: number): EmotionStyle;
1235
+
1236
+ /**
1237
+ * PHONT API Client
1238
+ *
1239
+ * Supports both authentication methods:
1240
+ * - API Keys: For server-side/programmatic access (POST/DELETE /vods, GET /vods/status)
1241
+ * - Session Tokens: For frontend/user access (GET /vods, GET /vods/{id}, GET /vod-subtitles/{id})
1242
+ *
1243
+ * Tenant isolation is handled automatically by the backend based on credentials.
1244
+ */
1245
+
1246
+ interface LoginResponse {
1247
+ tenant_id: string;
1248
+ username: string;
1249
+ email: string;
1250
+ session_id: string;
1251
+ auth_token: string;
1252
+ }
1253
+ interface RegisterResponse {
1254
+ tenant_id: string;
1255
+ username: string;
1256
+ email: string;
1257
+ }
1258
+ interface VodMetadata {
1259
+ _id: string;
1260
+ tenant_id: string;
1261
+ manifest: string;
1262
+ thumbnail?: string;
1263
+ audio_url?: string;
1264
+ name: string;
1265
+ subtitle_available: boolean;
1266
+ created_at: string;
1267
+ }
1268
+ declare class PhontClient {
1269
+ private apiKey?;
1270
+ private authToken?;
1271
+ private sessionId?;
1272
+ readonly apiBaseUrl: string;
1273
+ constructor(options: PhontClientOptions);
1274
+ /**
1275
+ * Get authentication headers based on configured auth method
1276
+ */
1277
+ getAuthHeaders(): Record<string, string>;
1278
+ /**
1279
+ * Make an authenticated request
1280
+ */
1281
+ private request;
1282
+ /**
1283
+ * Login with credentials (for frontend/user access)
1284
+ * Returns session_id and auth_token for subsequent requests
1285
+ */
1286
+ login(identifier: string, password: string): Promise<LoginResponse>;
1287
+ /**
1288
+ * Register a new user
1289
+ */
1290
+ register(username: string, email: string, password: string): Promise<RegisterResponse>;
1291
+ /**
1292
+ * Get subtitle track for a VOD
1293
+ * Requires: Session token authentication
1294
+ * @param vodId - VOD ID
1295
+ * @param lang - Language code (optional). If not provided, returns original language subtitles.
1296
+ * Use null/undefined for original, or a valid language code for translations.
1297
+ * @param model - Model name for LLM translation (optional)
1298
+ */
1299
+ getSubtitleTrack(vodId: string, lang?: string | null, model?: string | null): Promise<any>;
1300
+ /**
1301
+ * Get VOD metadata
1302
+ * Requires: Session token authentication
1303
+ */
1304
+ getVod(vodId: string): Promise<VodMetadata>;
1305
+ /**
1306
+ * List user's VODs (tenant-isolated)
1307
+ * Requires: Session token authentication
1308
+ */
1309
+ listVods(skip?: number, limit?: number): Promise<VodMetadata[]>;
1310
+ /**
1311
+ * Create a new VOD
1312
+ * Requires: API key authentication
1313
+ */
1314
+ createVod(data: {
1315
+ manifest: string;
1316
+ thumbnail?: string;
1317
+ name: string;
1318
+ }): Promise<VodMetadata>;
1319
+ /**
1320
+ * Delete a VOD
1321
+ * Requires: API key authentication
1322
+ */
1323
+ deleteVod(vodId: string): Promise<void>;
1324
+ /**
1325
+ * Get VOD processing status
1326
+ * Requires: API key authentication
1327
+ */
1328
+ getVodStatus(vodId: string): Promise<{
1329
+ vod_id: string;
1330
+ status: string;
1331
+ }>;
1332
+ /**
1333
+ * Update credentials (useful after login)
1334
+ */
1335
+ setCredentials(options: {
1336
+ apiKey?: string;
1337
+ authToken?: string;
1338
+ sessionId?: string;
1339
+ }): void;
1340
+ /**
1341
+ * Clear credentials (for logout)
1342
+ */
1343
+ clearCredentials(): void;
1344
+ }
1345
+
1346
+ export { ANIMATION_STYLING, type AnimationOptions, type AnimationParameterStates, type AnimationParameters, type AnimationState, type CurvePoint, EMOTION_ANIMATIONS, EMOTION_ANIMATION_URLS, type Emotion, type EmotionCurve, type EmotionSelectionResult, type EmotionStyle, type EmphasisEffect, type FontAxisConfig, type FontConfig, type JoyEffect, type Letter, type LoginResponse, type ParameterState, type ParameterStates, PhontClient, type PhontClientOptions, 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 VariableFontStyle, type VodMetadata, type VodState$1 as VodState, type Word, type WordAnimationState, type WordLetterSpacingParams, calculateAnimationParameters, calculateAnimationState, calculateDynamicWordSpacing, calculateEmphasisMultiplier, calculateGTSValues, calculateJoy, calculateJoyBulge, calculateNormalizedTime, calculateSentenceAnimation, calculateWeightFromVolume, calculateWidthFromSpeechRate, calculateWordAnimation, calculateWordEmphasisEffects, calculateWordLetterSpacing, capEmotionIntensity, createSubtitleEngine, findSubtitleForWord, generateTextShadow, getEmotionStyle, getFontFamilyCSS, getJoyAnimationMode, getJoyFilter, getJoyTransform, getMergedSubtitles, getSubtitleAnimations, getSubtitleText, getSubtitleTiming, getSubtitleWords, hasMergedSubtitles, isInEmphasisRange, isInStrongEmphasisRange, meetsEmotionThreshold, processEmotionsWithImportance, selectEmotion, selectFontFamily, shouldUseAngryFont, shouldUseJoyFont, shouldUseSadnessFont, splitTextIntoLetters, subtitleManager };