AI story writer enhancements, text to video and voice generation, subscription management, and more.

This commit is contained in:
ajaysi
2025-11-19 09:55:32 +05:30
parent bf7493c366
commit e96525347b
64 changed files with 10367 additions and 400 deletions

View File

@@ -587,6 +587,127 @@ export const formatCurrency = (amount: number): string => {
}).format(amount);
};
// Pre-flight check interfaces
export interface PreflightOperation {
provider: string;
model?: string;
tokens_requested?: number;
operation_type: string;
actual_provider_name?: string;
}
export interface PreflightLimitInfo {
current_usage: number;
limit: number;
remaining: number;
}
export interface PreflightOperationResult {
provider: string;
operation_type: string;
cost: number;
allowed: boolean;
limit_info: PreflightLimitInfo | null;
message: string | null;
}
export interface PreflightCheckResponse {
can_proceed: boolean;
estimated_cost: number;
operations: PreflightOperationResult[];
total_cost: number;
usage_summary: {
current_calls: number;
limit: number;
remaining: number;
} | null;
cached: boolean;
}
/**
* Check pre-flight validation for a single operation.
* Returns cost estimation, limits check, and usage information.
*/
export const checkPreflight = async (
operation: PreflightOperation
): Promise<PreflightCheckResponse> => {
try {
const response = await billingAPI.post<{ success: boolean; data: PreflightCheckResponse }>(
'/preflight-check',
{
operations: [operation]
}
);
if (!response.data.success) {
throw new Error('Pre-flight check failed');
}
return response.data.data;
} catch (error: any) {
console.error('[BillingService] Pre-flight check error:', error);
// Return a safe default response on error
return {
can_proceed: false,
estimated_cost: 0,
operations: [{
provider: operation.provider,
operation_type: operation.operation_type,
cost: 0,
allowed: false,
limit_info: null,
message: error?.response?.data?.detail || 'Pre-flight check failed'
}],
total_cost: 0,
usage_summary: null,
cached: false
};
}
};
/**
* Check pre-flight validation for multiple operations in a single request.
* Useful for pages with many buttons to reduce API calls.
*/
export const checkPreflightBatch = async (
operations: PreflightOperation[]
): Promise<PreflightCheckResponse> => {
try {
const response = await billingAPI.post<{ success: boolean; data: PreflightCheckResponse }>(
'/preflight-check',
{
operations
}
);
if (!response.data.success) {
throw new Error('Pre-flight check failed');
}
return response.data.data;
} catch (error: any) {
console.error('[BillingService] Pre-flight batch check error:', error);
// Return a safe default response on error
return {
can_proceed: false,
estimated_cost: 0,
operations: operations.map(op => ({
provider: op.provider,
operation_type: op.operation_type,
cost: 0,
allowed: false,
limit_info: null,
message: error?.response?.data?.detail || 'Pre-flight check failed'
})),
total_cost: 0,
usage_summary: null,
cached: false
};
}
};
export const formatNumber = (num: number): string => {
return new Intl.NumberFormat('en-US').format(num);
};

View File

@@ -204,8 +204,10 @@ export interface StoryAudioGenerationResponse {
export interface StoryVideoGenerationRequest {
scenes: StoryScene[];
image_urls: string[];
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;
@@ -227,6 +229,38 @@ export interface StoryVideoGenerationResponse {
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;
}
class StoryWriterApi {
/**
* Generate 3 story setup options from a user's story idea
@@ -373,20 +407,63 @@ class StoryWriterApi {
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;
}
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 {
// If imageUrl is already a full URL, return it as-is
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
return this.buildAbsoluteUrl(imageUrl);
}
// Otherwise, prepend the base URL
const baseURL = aiApiClient.defaults.baseURL || '';
// Remove trailing slash from baseURL if present, and leading slash from imageUrl if present
const cleanBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
const cleanImageUrl = imageUrl.startsWith('/') ? imageUrl : `/${imageUrl}`;
return `${cleanBaseURL}${cleanImageUrl}`;
/**
* Convert any relative media URL to absolute
*/
getMediaUrl(path: string): string {
return this.buildAbsoluteUrl(path);
}
/**
@@ -400,6 +477,165 @@ class StoryWriterApi {
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
*/
@@ -496,7 +732,6 @@ class StoryWriterApi {
scene_data: StoryScene;
story_context: Record<string, any>;
all_scenes: StoryScene[];
scene_image_url?: string;
provider?: string;
model?: string;
num_frames?: number;