📦 Kahani Magic Assets
Complete asset lifecycle documentation
Last Updated: January 21, 2026
Asset Capability Matrix
| Asset | Create | Generate | Regenerate | Edit | Style | Clear | |-------|--------|----------|------------|------|-------|-------| | IMG | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | | TXT | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | AUD | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | | HIGH | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | | CAP | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | | KIN | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | | BGM | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | | SFX | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | | VID | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
1. IMAGE (IMG)
Description
Visual illustration per page generated from story context.
Fields in StoryPage
imagePrompt: string; // AI-generated prompt
imageUrl?: string; // Alias for generatedImageUrl
generatedImageUrl?: string; // Hostinger CDN URL
isGeneratingImage?: boolean; // Loading state
imageGenFailed?: boolean; // Failure flag
Lifecycle
| Stage | Method | Details |
|-------|--------|---------|
| Generate | POST /generate-image | Uses imagePrompt + style modifiers |
| Regenerate | Same API | Triggered manually |
| Clear | UI action | Sets URL to null |
API Request
{
text: string; // Page text for context
imagePrompt: string; // Visual description
imageStyleId?: string; // "3d-animated", "watercolor", etc.
characterAppearance?: string; // Character consistency
}
Issues
| Severity | Issue | Fix | |----------|-------|-----| | 🔴 High | No inpainting/editing | Add edit API | | 🟡 Medium | No custom upload | Add upload option | | 🟢 Low | No filters | Add post-processing |
2. TEXT (TXT)
Description
Story narration text per page.
Fields in StoryPage
text: string; // Main narration
pageNumber: number; // 1-indexed
textStyle?: TextStyle; // Typography options
Lifecycle
| Stage | Method | Details |
|-------|--------|---------|
| Generate | POST /generate-story | Initial story creation |
| Regenerate | POST /regenerate-text | AI rewrites page |
| Edit | useEditMode.ts | Inline user editing |
| Style | StyleToolbar | Font, size, effects |
Issues
| Severity | Issue | Fix | |----------|-------|-----| | 🔴 High | Edit clears animations | Auto-regenerate KIN/CAP | | 🟡 Medium | No undo for text | Add text history |
3. AUDIO (AUD)
Description
TTS narration audio per page.
Fields in StoryPage
audioUrl?: string; // Hostinger CDN URL
audioBase64?: string; // Temp before upload
audioGenerationStatus?: 'pending' | 'generating' | 'ready' | 'failed';
audioDuration?: number; // Duration in seconds
Lifecycle
| Stage | Method | Details |
|-------|--------|---------|
| Generate | POST /generate-audio | Gemini TTS → ElevenLabs fallback |
| Regenerate | Same API | Manual trigger |
| Style | Voice presets | voiceStyleId, emotion tags |
| Clear | UI action | Clears audio + dependent assets |
Data Flow
text + languageCode + voiceStyleId
→ Emotion analysis
→ Gemini TTS (primary)
→ ElevenLabs TTS (fallback)
→ audioBase64
→ Hostinger upload
→ audioUrl
Issues
| Severity | Issue | Fix | |----------|-------|-----| | 🔴 Critical | No word timestamps | ElevenLabs with_timestamps | | 🔴 Critical | languageCode missing | Fix flow from UI | | 🟡 Medium | No waveform UI | Add visualization |
4. WORD TIMINGS / HIGHLIGHTS (HIGH)
Description
Word-level timestamps for karaoke-style highlighting.
Fields in StoryPage
wordTimings?: WordTiming[]; // Primary field
timings?: WordTiming[]; // Alias
interface WordTiming {
word: string;
start: number; // seconds
end: number; // seconds
}
Lifecycle
| Stage | Method | Details |
|-------|--------|---------|
| Generate | POST /align-text or heuristic | From audio analysis |
| Regenerate | Triggered by audio regen | Automatic |
| Clear | Triggered by audio clear | Automatic |
Current Implementation
// HEURISTIC (inaccurate) - src/app/api/kahani-magic/align-text/route.ts
const avgWordDuration = audioDuration / words.length;
words.forEach((word, i) => {
timings.push({
word,
start: i * avgWordDuration,
end: (i + 1) * avgWordDuration
});
});
Issues
| Severity | Issue | Fix | |----------|-------|-----| | 🔴 Critical | Heuristic-only | Use TTS timestamps | | 🔴 Critical | Drift accumulates | Real word boundaries | | 🟡 Medium | Manual adjustment | Add timing editor |
5. CAPTIONS (CAP)
Description
Phrase-based typography with emotion and layout analysis.
Fields in StoryPage
phraseAnalysis?: PhraseAnalysis[];
typographyPreset?: TypographyPreset;
interface PhraseAnalysis {
words: string[];
powerWordIndices: number[];
powerWordTypes: PowerWordType[];
layout: 'stacked' | 'diagonal' | 'centered' | 'split';
emotion: 'happy' | 'sad' | 'exciting' | 'dramatic' | 'calm';
sfx: SFXTrigger | null;
start: number;
end: number;
}
Lifecycle
| Stage | Method | Details |
|-------|--------|---------|
| Generate | POST /analyze-phrases | AI phrase analysis |
| Style | Animation presets | Caption mode options |
Issues
| Severity | Issue | Fix | |----------|-------|-----| | 🔴 High | languageCode not passed | Add to API call | | 🟡 Medium | Timing sync | Use audio timestamps | | 🟡 Medium | Layout collisions | Add collision detection |
6. KINETIC ANIMATIONS (KIN)
Description
Word-level animation effects for dynamic text display.
Fields in StoryPage
kineticAnimations?: WordAnimation[];
animationMode?: TextAnimationMode;
interface WordAnimation {
word: string;
effect: KineticEffect;
direction?: 'left' | 'right' | 'up' | 'down';
isPowerWord?: boolean;
}
type KineticEffect = 'zoom' | 'slide' | 'bounce' | 'fade' | 'pop' |
'wave' | 'shake' | 'flip' | 'typewriter' | 'morph';
Lifecycle
| Stage | Method | Details |
|-------|--------|---------|
| Generate | POST /generate-kinetic | AI assigns effects |
| Regenerate | Same API | Manual trigger |
| Style | StyleToolbar | Mode selection |
Issues
| Severity | Issue | Fix | |----------|-------|-----| | 🔴 Critical | Array mismatch | Sync with word count | | 🔴 Critical | Cleared on edit | Preserve + auto-regen | | 🟢 Low | No per-word edit | Add animation editor |
7. BACKGROUND MUSIC (BGM)
Description
Style-aware ambient music with narration ducking.
Fields in StoryPage
audioCues?: AudioCues;
interface AudioCues {
bgmMood: BGMMood; // 'peaceful' | 'tense' | 'joyful' | etc.
ambientScene: AmbientScene;
sfxTriggers: SFXTriggerCue[];
}
Service: bgmService.ts
// Key methods
initialize(storyStyle: string): Promise<void>
play(): void
pause(): void
duckForNarration(): void
restoreAfterNarration(): void
setVolume(volume: number): void
Features
- ✅ Mood-based selection (8 moods)
- ✅ Auto-ducking during narration
- ✅ Volume control
- ✅ Toggle on/off
Issues
| Severity | Issue | Fix | |----------|-------|-----| | 🟡 Medium | No preloading | Preload on story load | | 🟢 Low | Limited moods | Expand library |
8. SOUND EFFECTS (SFX)
Description
Word-triggered contextual sound effects.
Fields in StoryPage
interface SFXTriggerCue {
word: string;
wordIndex: number;
sound: string; // "thunder", "laughter"
soundPrompt?: string; // AI generation prompt
volume?: number;
}
Service: sfxService.ts
// Key methods
playSFX(trigger: SFXTrigger, volume: number): void
playEmotionalSFX(emotion: EmotionalSFX): void
onWordHighlight(wordIndex: number): void
Features
- ✅ AI-generated triggers
- ✅ ElevenLabs sound generation
- ✅ Word-synced playback
- ✅ Volume control
Issues
| Severity | Issue | Fix | |----------|-------|-----| | 🟡 Medium | Basic caching | Improve cache | | 🟡 Medium | Timing imprecise | Link to timestamps |
9. VIDEO (VID)
Description
AI-generated video segments per page.
Fields in StoryPage
videoSegments?: VideoSegment[];
videoUrl?: string;
videoStatus?: 'pending' | 'planning' | 'generating' | 'ready' | 'partial' | 'failed';
interface VideoSegment {
order: number;
duration: number;
prompt: string;
videoBase64?: string;
videoUrl?: string;
status: 'pending' | 'generating' | 'ready' | 'failed';
loopEffect?: 'loop' | 'pingpong' | 'once' | 'slowmo' | 'reverse';
}
Lifecycle
| Stage | Method | Details |
|-------|--------|---------|
| Plan | POST /plan-segments | AI plans video structure |
| Generate | POST /generate-segment | Veo generates video |
| Edit | Timeline editor | Trim, effects |
| Style | Loop effects | pingpong, slowmo, etc. |
Features
- ✅ Multi-segment planning
- ✅ Individual segment generation
- ✅ Loop effects
- ✅ Timeline editing
Export Behavior (storyToCombo)
When exporting to Combo editor, video is preferred over image if available:
- Check
page.videoUrl(direct video URL) - Check
page.videoSegments[].videoUrlwherestatus === 'ready' - Falls back to image (
page.imageUrlorpage.generatedImageUrl) if no video
// Logic in src/lib/storyToCombo.ts
const videoUrl = page.videoUrl ||
(page.videoSegments?.find(s => s.status === 'ready')?.videoUrl);
if (videoUrl) { /* export as Video clip */ }
else if (imageUrl) { /* export as Image clip (fallback) */ }
Asset Dependencies
graph TD
TXT[TEXT] --> AUD[AUDIO]
AUD --> HIGH[WORD TIMINGS]
TXT --> CAP[CAPTIONS]
TXT --> KIN[KINETIC]
HIGH --> CAP
HIGH --> SFX[SFX]
TXT --> IMG[IMAGE]
TXT --> VID[VIDEO]
style AUD fill:#ff6b6b
style HIGH fill:#ff6b6b
style CAP fill:#ffd93d
style KIN fill:#ff6b6b
Legend: 🔴 Critical issues | 🟡 Medium issues | ✅ Working
Language Data Architecture (V3)
Unified Content Format
As of January 2026, all language-specific data is stored in unified page.content[lang]:
// V3 Format - Single source of truth
interface LanguageContent {
text?: string; // Translated text
audio?: {
base64?: string; // Temp before upload
url?: string; // Hostinger CDN URL
duration?: number; // Duration in seconds
status: 'pending' | 'generating' | 'ready' | 'failed';
};
wordTimings?: WordTiming[]; // Karaoke highlights
phrases?: PhraseAnalysis[]; // Caption mode
kinetic?: WordAnimation[]; // Animation effects
}
// In StoryPage
content?: { [langCode: string]: LanguageContent };
Access via pageUtils
All consumers use unified access layer:
import { getPageText, getPageAudio, getWordTimings } from './pageUtils';
// Reads V3 format with fallback to root fields for original language
const text = getPageText(page, langCode, story.originalLanguage);
const audio = getPageAudio(page, langCode, story.originalLanguage);
Migration from Legacy Formats
Old data is migrated on-the-fly:
story.translations[lang]→page.content[lang].textpage.localizedAssets[lang]→page.content[lang](deprecated)
Storage Flow
page.content[lang].audio.base64
→ storageService.ts
→ Hostinger upload
→ page.content[lang].audio.url
→ Firebase (base64 deleted, URL persisted)
Legacy Format (Deprecated)
⚠️ DEPRECATED: Use
page.content[lang]instead
// OLD - Do not use for new code
interface LocalizedPageAssets {
text?: string;
audioBase64?: string;
audioUrl?: string;
audioDuration?: number;
wordTimings?: WordTiming[];
phraseAnalysis?: PhraseAnalysis[];
kineticAnimations?: WordAnimation[];
audioGenerationStatus?: 'pending' | 'generating' | 'ready' | 'failed';
}
localizedAssets?: { [langCode: string]: LocalizedPageAssets };
See individual asset docs in ./assets/ for more details