import { aiApiClient, pollingApiClient } from "../api/client"; /** * Story Writer API Service * * Provides TypeScript-typed API calls for story generation endpoints. */ export interface StoryGenerationRequest { persona: string; story_setting: string; character_input: string; plot_elements: string; writing_style: string; story_tone: string; narrative_pov: string; audience_age_group: string; content_rating: string; ending_preference: string; story_length?: string; enable_explainer?: boolean; enable_illustration?: boolean; enable_narration?: boolean; enable_video_narration?: boolean; // Image generation settings image_provider?: string; image_width?: number; image_height?: number; image_model?: string; // Video generation settings video_fps?: number; video_transition_duration?: number; // Audio generation settings audio_provider?: string; audio_lang?: string; audio_slow?: boolean; audio_rate?: number; anime_bible?: AnimeStoryBible | null; } export interface StorySetupGenerationRequest { story_idea: string; story_mode?: 'marketing' | 'pure'; story_template?: string | null; brand_context?: { brand_name?: string | null; writing_tone?: string | null; audience_description?: string | null; } | null; } export interface StoryIdeaEnhanceRequest { story_idea: string; story_mode?: 'marketing' | 'pure'; story_template?: string | null; brand_context?: { brand_name?: string | null; writing_tone?: string | null; audience_description?: string | null; } | null; fiction_variant?: string | null; narrative_energy?: string | null; } export interface StoryIdeaEnhanceSuggestion { idea: string; whats_missing: string; why_choose: string; } export interface StoryIdeaEnhanceResponse { suggestions: StoryIdeaEnhanceSuggestion[]; success: boolean; } export interface StoryContextResponse { canonical_profile: Record | null; website_url?: string | null; research_preferences?: Record | null; brand_context?: { brand_name?: string | null; writing_tone?: string | null; audience_description?: string | null; } | null; brand_assets?: { avatar_url?: string | null; voice_preview_url?: string | null; custom_voice_id?: string | null; } | null; persona_enabled?: boolean; has_persona_context?: boolean; } export interface AnimeCharacter { id: string; name: string; age_range: string; role: string; look: string; outfit_palette: string; personality_tags: string[]; } export interface AnimeWorld { setting: string; era: string; tech_or_magic_level: string; core_rules: string[]; } export interface AnimeVisualStyle { style_preset: string; camera_style: string; color_mood: string; lighting: string; line_style: string; extra_tags: string[]; } export interface AnimeStoryBible { story_id?: string; main_cast: AnimeCharacter[]; world: AnimeWorld; visual_style: AnimeVisualStyle; } export interface StorySetupOption { persona: string; story_setting: string; character_input: string; plot_elements: string; writing_style: string; story_tone: string; narrative_pov: string; audience_age_group: string; content_rating: string; ending_preference: string; story_length?: string; premise: string; reasoning: string; // Image generation settings image_provider?: string; image_width?: number; image_height?: number; image_model?: string; // Video generation settings video_fps?: number; video_transition_duration?: number; // Audio generation settings audio_provider?: string; audio_lang?: string; audio_slow?: boolean; audio_rate?: number; } export interface StorySetupGenerationResponse { options: StorySetupOption[]; success: boolean; } export interface StoryPremiseResponse { premise: string; success: boolean; task_id?: string; } export interface StoryScene { scene_number: number; title: string; description: string; image_prompt: string; audio_narration: string; character_descriptions?: string[]; key_events?: string[]; } export interface StoryOutlineResponse { outline: string | StoryScene[]; success: boolean; task_id?: string; is_structured?: boolean; anime_bible?: AnimeStoryBible; } export interface StoryContentResponse { story: string; premise?: string; outline?: string; is_complete: boolean; iterations: number; success: boolean; task_id?: string; } export interface StoryFullGenerationResponse { premise: string; outline: string; story: string; is_complete: boolean; iterations: number; success: boolean; task_id?: string; } export interface StoryStartRequest extends StoryGenerationRequest { premise: string; outline: string | StoryScene[]; } export interface StoryContinueRequest extends StoryGenerationRequest { premise: string; outline: string | StoryScene[]; story_text: string; } export interface StoryContinueResponse { continuation: string; is_complete: boolean; success: boolean; } export interface TaskStatus { task_id: string; status: "pending" | "processing" | "completed" | "failed"; progress?: number; message?: string; result?: any; error?: string; created_at?: string; updated_at?: string; } export interface CacheStats { total_entries: number; cache_keys: string[]; } export interface StoryImageGenerationRequest { scenes: StoryScene[]; provider?: string; width?: number; height?: number; model?: string; } export interface StoryImageResult { scene_number: number; scene_title: string; image_filename: string; image_url: string; width: number; height: number; provider: string; model?: string; seed?: number; error?: string; } export interface StoryImageGenerationResponse { images: StoryImageResult[]; success: boolean; task_id?: string; } export interface StoryAudioGenerationRequest { scenes: StoryScene[]; provider?: string; lang?: string; slow?: boolean; rate?: number; } export interface StoryAudioResult { scene_number: number; scene_title: string; audio_filename: string; audio_url: string; provider: string; file_size: number; error?: string; } export interface StoryAudioGenerationResponse { audio_files: StoryAudioResult[]; success: boolean; task_id?: string; } export interface StoryVideoGenerationRequest { scenes: StoryScene[]; image_urls: (string | null)[]; audio_urls: string[]; video_urls?: (string | null)[] | null; ai_audio_urls?: (string | null)[] | null; story_title?: string; fps?: number; transition_duration?: number; } export interface StoryVideoResult { video_filename: string; video_url: string; duration: number; fps: number; file_size: number; num_scenes: number; error?: string; } export interface StoryVideoGenerationResponse { video: StoryVideoResult; success: boolean; task_id?: string; } export interface AnimateSceneRequest { scene_number: number; scene_data: StoryScene; story_context: Record; image_url: string; duration?: 5 | 10; } export interface AnimateSceneVoiceoverRequest extends AnimateSceneRequest { audio_url: string; resolution?: '480p' | '720p'; prompt?: string; } export interface AnimateSceneResponse { success: boolean; scene_number: number; video_filename: string; video_url: string; duration: number; cost: number; prompt_used: string; provider: string; prediction_id?: string; } export interface ResumeAnimateSceneRequest { prediction_id: string; scene_number: number; duration?: 5 | 10; } export interface AnimeSceneTextRequest { scene: StoryScene; persona: string; story_setting: string; character_input: string; plot_elements: string; writing_style: string; story_tone: string; narrative_pov: string; audience_age_group: string; content_rating: string; anime_bible?: AnimeStoryBible | null; } export interface AnimeSceneTextResponse { scene: StoryScene; success: boolean; } export interface AnimeSceneGenerateRequest { premise: string; persona: string; story_setting: string; character_input: string; plot_elements: string; writing_style: string; story_tone: string; narrative_pov: string; audience_age_group: string; content_rating: string; anime_bible: AnimeStoryBible; previous_scenes?: StoryScene[] | null; target_scene_number?: number | null; } export interface AnimeSceneGenerateResponse { scene: StoryScene; success: boolean; } export interface StoryProjectSummary { id: number; project_id: string; user_id: string; title?: string | null; story_mode?: string | null; story_template?: string | null; setup?: Record | null; outline?: Record | null; scenes?: Record[] | null; story_content?: Record | null; anime_bible?: AnimeStoryBible | null; media_state?: Record | null; current_phase?: string | null; status: string; is_favorite: boolean; is_complete: boolean; created_at: string; updated_at: string; } export interface StoryProjectListResponse { projects: StoryProjectSummary[]; total: number; limit: number; offset: number; } export interface CreateStoryProjectRequest { project_id: string; title?: string | null; story_mode?: string | null; story_template?: string | null; setup?: Record | null; } export interface UpdateStoryProjectRequest { title?: string | null; story_mode?: string | null; story_template?: string | null; setup?: Record | null; outline?: Record | null; scenes?: Record[] | null; story_content?: Record | null; anime_bible?: AnimeStoryBible | null; media_state?: Record | null; current_phase?: string | null; status?: string | null; is_complete?: boolean | null; } class StoryWriterApi { /** * Generate 3 story setup options from a user's story idea */ async generateStorySetup( request: StorySetupGenerationRequest ): Promise { const response = await aiApiClient.post( "/api/story/generate-setup", request ); return response.data; } async enhanceStoryIdea( request: StoryIdeaEnhanceRequest ): Promise { const response = await aiApiClient.post( "/api/story/enhance-idea", request ); return response.data; } /** * Get onboarding-based story context for Story Studio */ async getStoryContext(): Promise { const response = await aiApiClient.get("/api/story/context"); return response.data; } /** * Generate a story premise */ async generatePremise(request: StoryGenerationRequest): Promise { const response = await aiApiClient.post( "/api/story/generate-premise", request ); return response.data; } /** * Generate a story outline from a premise */ async generateOutline( premise: string, request: StoryGenerationRequest ): Promise { // Create StoryStartRequest with premise included const outlineRequest: StoryStartRequest = { ...request, premise: premise, outline: [], // Empty outline for outline generation }; const response = await aiApiClient.post( `/api/story/generate-outline`, outlineRequest ); return response.data; } /** * Generate the starting section of a story */ async generateStoryStart( premise: string, outline: string | StoryScene[], request: StoryGenerationRequest ): Promise { // Create request body with premise and outline const requestBody = { ...request, premise, outline, }; const response = await aiApiClient.post( `/api/story/generate-start`, requestBody ); return response.data; } /** * Continue writing a story */ async continueStory(request: StoryContinueRequest): Promise { const response = await aiApiClient.post( "/api/story/continue", request ); return response.data; } /** * Generate a complete story asynchronously * Returns a task_id for polling */ async generateFullStory( request: StoryGenerationRequest, maxIterations: number = 10 ): Promise<{ task_id: string; status: string; message: string }> { const response = await aiApiClient.post<{ task_id: string; status: string; message: string }>( "/api/story/generate-full", request, { params: { max_iterations: maxIterations }, } ); return response.data; } /** * Get the status of a story generation task */ async getTaskStatus(taskId: string): Promise { const response = await pollingApiClient.get( `/api/story/task/${taskId}/status` ); return response.data; } /** * Get the result of a completed story generation task */ async getTaskResult(taskId: string): Promise { const response = await aiApiClient.get( `/api/story/task/${taskId}/result` ); return response.data; } /** * Get cache statistics */ async getCacheStats(): Promise<{ success: boolean; stats: CacheStats }> { const response = await pollingApiClient.get<{ success: boolean; stats: CacheStats }>( "/api/story/cache/stats" ); return response.data; } /** * Clear the story generation cache */ async clearCache(): Promise<{ success: boolean; status: string; message: string }> { const response = await pollingApiClient.post<{ success: boolean; status: string; message: string }>( "/api/story/cache/clear" ); return response.data; } /** * Generate images for story scenes */ async generateSceneImages(request: StoryImageGenerationRequest): Promise { const response = await aiApiClient.post( "/api/story/generate-images", request ); return response.data; } /** * Animate a single scene image into a short video preview */ async animateScene(request: AnimateSceneRequest): Promise { const response = await aiApiClient.post( "/api/story/animate-scene-preview", request ); return response.data; } /** * Animate a scene image using WaveSpeed InfiniteTalk with voiceover (async) * Returns task_id for polling since InfiniteTalk can take up to 10 minutes. */ async animateSceneVoiceover(request: AnimateSceneVoiceoverRequest): Promise<{ task_id: string; status: string; message: string }> { const response = await aiApiClient.post<{ task_id: string; status: string; message: string }>( "/api/story/animate-scene-voiceover", request ); return response.data; } /** * Resume a timed-out scene animation download using the prediction id */ async resumeAnimateScene(request: ResumeAnimateSceneRequest): Promise { const response = await aiApiClient.post( "/api/story/animate-scene-resume", request ); return response.data; } async refineAnimeSceneText(request: AnimeSceneTextRequest): Promise { const response = await aiApiClient.post( "/api/story/anime/scene-text", request ); return response.data; } async generateAnimeSceneFromBible( request: AnimeSceneGenerateRequest ): Promise { const response = await aiApiClient.post( "/api/story/anime/scene-generate", request ); return response.data; } async createStoryProject( payload: CreateStoryProjectRequest ): Promise { const response = await aiApiClient.post("/api/story/projects", payload); return response.data; } async loadStoryProject(projectId: string): Promise { const response = await aiApiClient.get(`/api/story/projects/${projectId}`); return response.data; } async updateStoryProject( projectId: string, payload: UpdateStoryProjectRequest ): Promise { const response = await aiApiClient.put( `/api/story/projects/${projectId}`, payload ); return response.data; } async listStoryProjects(params?: { status?: string; favorites_only?: boolean; limit?: number; offset?: number; order_by?: "updated_at" | "created_at"; }): Promise { const response = await aiApiClient.get("/api/story/projects", { params, }); return response.data; } async deleteStoryProject(projectId: string): Promise { await aiApiClient.delete(`/api/story/projects/${projectId}`); } async toggleStoryProjectFavorite(projectId: string): Promise { const response = await aiApiClient.post( `/api/story/projects/${projectId}/favorite` ); return response.data; } private buildAbsoluteUrl(path: string): string { if (!path) return path; if (path.startsWith('http://') || path.startsWith('https://')) { return path; } const baseURL = aiApiClient.defaults.baseURL || ''; const cleanBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL; const cleanPath = path.startsWith('/') ? path : `/${path}`; return `${cleanBaseURL}${cleanPath}`; } /** * Get image URL for a scene image */ getImageUrl(imageUrl: string): string { return this.buildAbsoluteUrl(imageUrl); } /** * Convert any relative media URL to absolute */ getMediaUrl(path: string): string { return this.buildAbsoluteUrl(path); } /** * Generate audio narration for story scenes */ async generateSceneAudio(request: StoryAudioGenerationRequest): Promise { const response = await aiApiClient.post( "/api/story/generate-audio", request ); return response.data; } /** * Optimize an image prompt using WaveSpeed prompt optimizer */ async optimizePrompt(request: { text: string; mode?: 'image' | 'video'; style?: 'default' | 'artistic' | 'photographic' | 'technical' | 'anime' | 'realistic'; image?: string; }): Promise<{ optimized_prompt: string; success: boolean }> { const response = await aiApiClient.post<{ optimized_prompt: string; success: boolean }>( "/api/story/optimize-prompt", request ); return response.data; } /** * Regenerate a scene image using a direct prompt (no AI prompt generation) */ async regenerateSceneImage(request: { scene_number: number; scene_title: string; prompt: string; provider?: string; width?: number; height?: number; model?: string; }): Promise<{ scene_number: number; scene_title: string; image_filename: string; image_url: string; width: number; height: number; provider: string; model?: string; seed?: number; success: boolean; error?: string; }> { const response = await aiApiClient.post<{ scene_number: number; scene_title: string; image_filename: string; image_url: string; width: number; height: number; provider: string; model?: string; seed?: number; success: boolean; error?: string; }>( "/api/story/regenerate-images", request ); return response.data; } /** * Generate AI audio for a single scene using WaveSpeed Minimax Speech 02 HD */ async generateAIAudio(request: { scene_number: number; scene_title: string; text: string; voice_id?: string; speed?: number; volume?: number; pitch?: number; emotion?: string; }): Promise<{ scene_number: number; scene_title: string; audio_filename: string; audio_url: string; provider: string; model: string; voice_id: string; text_length: number; file_size: number; cost: number; success: boolean; error?: string; }> { const response = await aiApiClient.post<{ scene_number: number; scene_title: string; audio_filename: string; audio_url: string; provider: string; model: string; voice_id: string; text_length: number; file_size: number; cost: number; success: boolean; error?: string; }>( "/api/story/generate-ai-audio", request ); return response.data; } /** * Generate free audio for a single scene using gTTS */ async generateFreeAudio(request: { scene_number: number; scene_title: string; text: string; provider?: string; lang?: string; slow?: boolean; rate?: number; }): Promise<{ scene_number: number; scene_title: string; audio_filename: string; audio_url: string; provider: string; file_size: number; success: boolean; error?: string; }> { // Use existing generateSceneAudio endpoint but for a single scene const response = await aiApiClient.post( "/api/story/generate-audio", { scenes: [{ scene_number: request.scene_number, title: request.scene_title, audio_narration: request.text, }], provider: request.provider || 'gtts', lang: request.lang || 'en', slow: request.slow || false, rate: request.rate || 150, } ); const result = response.data; if (result.success && result.audio_files && result.audio_files.length > 0) { const audio = result.audio_files[0]; return { scene_number: audio.scene_number, scene_title: audio.scene_title, audio_filename: audio.audio_filename, audio_url: audio.audio_url, provider: audio.provider, file_size: audio.file_size, success: true, error: audio.error, }; } else { throw new Error(result.audio_files?.[0]?.error || 'Failed to generate audio'); } } /** * Get audio URL for a scene audio file */ getAudioUrl(audioUrl: string): string { // If audioUrl is already a full URL, return it as-is if (audioUrl.startsWith('http://') || audioUrl.startsWith('https://')) { return audioUrl; } // Otherwise, prepend the base URL const baseURL = aiApiClient.defaults.baseURL || ''; // Remove trailing slash from baseURL if present, and leading slash from audioUrl if present const cleanBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL; const cleanAudioUrl = audioUrl.startsWith('/') ? audioUrl : `/${audioUrl}`; return `${cleanBaseURL}${cleanAudioUrl}`; } /** * Generate video from story scenes, images, and audio */ async generateStoryVideo(request: StoryVideoGenerationRequest): Promise { const response = await aiApiClient.post( "/api/story/generate-video", request ); return response.data; } /** * Generate video asynchronously (returns task_id for polling) */ async generateStoryVideoAsync( request: StoryVideoGenerationRequest ): Promise<{ task_id: string; status: string; message: string }> { const response = await aiApiClient.post<{ task_id: string; status: string; message: string }>( "/api/story/generate-video-async", request ); return response.data; } /** * Generate HD AI animation via Hugging Face text-to-video (server saves and returns url) */ async generateHdVideo(payload: { prompt: string; provider?: string; model?: string; num_frames?: number; guidance_scale?: number; num_inference_steps?: number; negative_prompt?: string; seed?: number; }): Promise<{ success: boolean; video_filename: string; video_url: string; provider: string; model: string }> { // Long-running request - use longRunningApiClient to allow more time const { longRunningApiClient } = await import("../api/client"); const response = await longRunningApiClient.post( "/api/story/hd-video", { provider: "huggingface", ...payload, } ); return response.data; } /** * Generate HD AI animation asynchronously (returns task_id for polling) */ async generateHdVideoAsync(payload: { prompt: string; provider?: string; model?: string; num_frames?: number; guidance_scale?: number; num_inference_steps?: number; negative_prompt?: string; seed?: number; }): Promise<{ task_id: string; status: string; message: string }> { const response = await aiApiClient.post<{ task_id: string; status: string; message: string }>( "/api/story/hd-video-async", { provider: "huggingface", ...payload, } ); return response.data; } /** * Generate HD AI video for a single scene with AI-enhanced prompt */ async generateHdVideoScene(payload: { scene_number: number; scene_data: StoryScene; story_context: Record; all_scenes: StoryScene[]; provider?: string; model?: string; num_frames?: number; guidance_scale?: number; num_inference_steps?: number; negative_prompt?: string; seed?: number; }): Promise<{ success: boolean; scene_number: number; video_filename: string; video_url: string; prompt_used: string; provider: string; model: string; }> { // Long-running request - use longRunningApiClient to allow more time const { longRunningApiClient } = await import("../api/client"); const response = await longRunningApiClient.post( "/api/story/hd-video-scene", { provider: "huggingface", ...payload, } ); return response.data; } /** * Get video URL for a story video file */ getVideoUrl(videoUrl: string): string { // If videoUrl is already a full URL, return it as-is if (videoUrl.startsWith('http://') || videoUrl.startsWith('https://')) { return videoUrl; } // Otherwise, prepend the base URL const baseURL = aiApiClient.defaults.baseURL || ''; // Remove trailing slash from baseURL if present, and leading slash from videoUrl if present const cleanBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL; const cleanVideoUrl = videoUrl.startsWith('/') ? videoUrl : `/${videoUrl}`; return `${cleanBaseURL}${cleanVideoUrl}`; } } export const storyWriterApi = new StoryWriterApi();