1039 lines
26 KiB
TypeScript
1039 lines
26 KiB
TypeScript
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<string, any> | null;
|
|
website_url?: string | null;
|
|
research_preferences?: Record<string, any> | 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<string, any>;
|
|
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<string, any> | null;
|
|
outline?: Record<string, any> | null;
|
|
scenes?: Record<string, any>[] | null;
|
|
story_content?: Record<string, any> | null;
|
|
anime_bible?: AnimeStoryBible | null;
|
|
media_state?: Record<string, any> | 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<string, any> | null;
|
|
}
|
|
|
|
export interface UpdateStoryProjectRequest {
|
|
title?: string | null;
|
|
story_mode?: string | null;
|
|
story_template?: string | null;
|
|
setup?: Record<string, any> | null;
|
|
outline?: Record<string, any> | null;
|
|
scenes?: Record<string, any>[] | null;
|
|
story_content?: Record<string, any> | null;
|
|
anime_bible?: AnimeStoryBible | null;
|
|
media_state?: Record<string, any> | 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<StorySetupGenerationResponse> {
|
|
const response = await aiApiClient.post<StorySetupGenerationResponse>(
|
|
"/api/story/generate-setup",
|
|
request
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
async enhanceStoryIdea(
|
|
request: StoryIdeaEnhanceRequest
|
|
): Promise<StoryIdeaEnhanceResponse> {
|
|
const response = await aiApiClient.post<StoryIdeaEnhanceResponse>(
|
|
"/api/story/enhance-idea",
|
|
request
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Get onboarding-based story context for Story Studio
|
|
*/
|
|
async getStoryContext(): Promise<StoryContextResponse> {
|
|
const response = await aiApiClient.get<StoryContextResponse>("/api/story/context");
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Generate a story premise
|
|
*/
|
|
async generatePremise(request: StoryGenerationRequest): Promise<StoryPremiseResponse> {
|
|
const response = await aiApiClient.post<StoryPremiseResponse>(
|
|
"/api/story/generate-premise",
|
|
request
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Generate a story outline from a premise
|
|
*/
|
|
async generateOutline(
|
|
premise: string,
|
|
request: StoryGenerationRequest
|
|
): Promise<StoryOutlineResponse> {
|
|
// Create StoryStartRequest with premise included
|
|
const outlineRequest: StoryStartRequest = {
|
|
...request,
|
|
premise: premise,
|
|
outline: [], // Empty outline for outline generation
|
|
};
|
|
const response = await aiApiClient.post<StoryOutlineResponse>(
|
|
`/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<StoryContentResponse> {
|
|
// Create request body with premise and outline
|
|
const requestBody = {
|
|
...request,
|
|
premise,
|
|
outline,
|
|
};
|
|
const response = await aiApiClient.post<StoryContentResponse>(
|
|
`/api/story/generate-start`,
|
|
requestBody
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Continue writing a story
|
|
*/
|
|
async continueStory(request: StoryContinueRequest): Promise<StoryContinueResponse> {
|
|
const response = await aiApiClient.post<StoryContinueResponse>(
|
|
"/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<TaskStatus> {
|
|
const response = await pollingApiClient.get<TaskStatus>(
|
|
`/api/story/task/${taskId}/status`
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Get the result of a completed story generation task
|
|
*/
|
|
async getTaskResult(taskId: string): Promise<StoryFullGenerationResponse> {
|
|
const response = await aiApiClient.get<StoryFullGenerationResponse>(
|
|
`/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<StoryImageGenerationResponse> {
|
|
const response = await aiApiClient.post<StoryImageGenerationResponse>(
|
|
"/api/story/generate-images",
|
|
request
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Animate a single scene image into a short video preview
|
|
*/
|
|
async animateScene(request: AnimateSceneRequest): Promise<AnimateSceneResponse> {
|
|
const response = await aiApiClient.post<AnimateSceneResponse>(
|
|
"/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<AnimateSceneResponse> {
|
|
const response = await aiApiClient.post<AnimateSceneResponse>(
|
|
"/api/story/animate-scene-resume",
|
|
request
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
async refineAnimeSceneText(request: AnimeSceneTextRequest): Promise<AnimeSceneTextResponse> {
|
|
const response = await aiApiClient.post<AnimeSceneTextResponse>(
|
|
"/api/story/anime/scene-text",
|
|
request
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
async generateAnimeSceneFromBible(
|
|
request: AnimeSceneGenerateRequest
|
|
): Promise<AnimeSceneGenerateResponse> {
|
|
const response = await aiApiClient.post<AnimeSceneGenerateResponse>(
|
|
"/api/story/anime/scene-generate",
|
|
request
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
async createStoryProject(
|
|
payload: CreateStoryProjectRequest
|
|
): Promise<StoryProjectSummary> {
|
|
const response = await aiApiClient.post<StoryProjectSummary>("/api/story/projects", payload);
|
|
return response.data;
|
|
}
|
|
|
|
async loadStoryProject(projectId: string): Promise<StoryProjectSummary> {
|
|
const response = await aiApiClient.get<StoryProjectSummary>(`/api/story/projects/${projectId}`);
|
|
return response.data;
|
|
}
|
|
|
|
async updateStoryProject(
|
|
projectId: string,
|
|
payload: UpdateStoryProjectRequest
|
|
): Promise<StoryProjectSummary> {
|
|
const response = await aiApiClient.put<StoryProjectSummary>(
|
|
`/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<StoryProjectListResponse> {
|
|
const response = await aiApiClient.get<StoryProjectListResponse>("/api/story/projects", {
|
|
params,
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
async deleteStoryProject(projectId: string): Promise<void> {
|
|
await aiApiClient.delete(`/api/story/projects/${projectId}`);
|
|
}
|
|
|
|
async toggleStoryProjectFavorite(projectId: string): Promise<StoryProjectSummary> {
|
|
const response = await aiApiClient.post<StoryProjectSummary>(
|
|
`/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<StoryAudioGenerationResponse> {
|
|
const response = await aiApiClient.post<StoryAudioGenerationResponse>(
|
|
"/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<StoryAudioGenerationResponse>(
|
|
"/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<StoryVideoGenerationResponse> {
|
|
const response = await aiApiClient.post<StoryVideoGenerationResponse>(
|
|
"/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<string, any>;
|
|
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();
|