Files
ALwrity/frontend/src/services/youtubeApi.ts
ajaysi b134e9dc7e Added video studio router and endpoints. Added research router and endpoints. Added youtube router and endpoints. Added onboarding utils router and endpoints. Added onboarding utils service. Added onboarding utils models. Added onboarding utils routes. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils. Added onboarding utils utils.
2026-01-01 17:56:25 +05:30

574 lines
18 KiB
TypeScript

// YouTube Creator Studio API Client
import { apiClient, aiApiClient } from '../api/client';
const API_BASE = '/api/youtube';
export interface VideoPlanRequest {
user_idea: string;
duration_type: 'shorts' | 'medium' | 'long';
video_type?: 'tutorial' | 'review' | 'educational' | 'entertainment' | 'vlog' | 'product_demo' | 'reaction' | 'storytelling';
target_audience?: string;
video_goal?: string;
brand_style?: string;
reference_image_description?: string;
source_content_id?: string;
source_content_type?: 'blog' | 'story';
avatar_url?: string;
}
export interface VideoPlan {
video_summary: string;
target_audience: string;
video_goal?: string;
key_message?: string;
content_outline: Array<{
section: string;
description: string;
duration_estimate: number;
}>;
hook_strategy: string;
call_to_action?: string;
cta_ideas?: string[];
visual_style: string;
tone?: string;
seo_keywords: string[];
duration_type: string;
estimated_duration?: string;
auto_generated_avatar_url?: string;
avatar_reused?: boolean; // Flag indicating if avatar was reused from asset library
avatar_recommendations?: {
description?: string;
style?: string;
energy?: string;
};
avatar_prompt?: string; // AI prompt used to generate the avatar
}
export interface Scene {
scene_number: number;
title: string;
narration: string;
visual_prompt: string;
enhanced_visual_prompt?: string;
duration_estimate: number;
visual_cues: string[];
emphasis_tags: string[];
enabled?: boolean;
imageUrl?: string; // Per-scene generated image URL
audioUrl?: string; // Per-scene generated audio URL
videoUrl?: string; // Per-scene generated video URL
}
export interface VideoRenderRequest {
scenes: Scene[];
video_plan: VideoPlan;
resolution?: '480p' | '720p' | '1080p';
combine_scenes?: boolean;
voice_id?: string;
}
export interface SceneVideoRenderRequest {
scene: Scene;
video_plan: VideoPlan;
resolution?: '480p' | '720p' | '1080p';
voice_id?: string;
generate_audio_enabled?: boolean;
}
export interface SceneVideoRenderResponse {
success: boolean;
task_id?: string;
message: string;
scene_number?: number;
}
export interface CombineVideosRequest {
scene_video_urls: string[];
resolution?: '480p' | '720p' | '1080p';
title?: string;
video_plan?: VideoPlan;
}
export interface VideoListItem {
scene_number?: number | null;
video_url: string;
filename: string;
created_at?: string;
resolution?: string;
}
export interface VideoListResponse {
success: boolean;
videos: VideoListItem[];
message: string;
}
export interface TaskStatus {
task_id: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
message?: string;
result?: any;
error?: string;
}
export interface CostEstimateRequest {
scenes: Scene[];
resolution: '480p' | '720p' | '1080p';
imageModel?: 'ideogram-v3-turbo' | 'qwen-image';
}
export interface CostEstimate {
resolution: string;
price_per_second: number;
num_scenes: number;
total_duration_seconds: number;
scene_costs: Array<{
scene_number: number;
duration_estimate: number;
actual_duration: number;
cost: number;
}>;
total_cost: number;
estimated_cost_range: {
min: number;
max: number;
};
image_model?: string;
image_cost_per_scene?: number;
total_image_cost?: number;
}
export interface CostEstimateResponse {
success: boolean;
estimate?: CostEstimate;
message: string;
}
export interface AvatarUploadResponse {
avatar_url: string;
avatar_filename: string;
message: string;
}
export interface AvatarTransformResponse {
avatar_url: string;
avatar_filename: string;
avatar_prompt?: string;
message: string;
}
export interface SceneImageRequest {
sceneId: string;
sceneTitle?: string;
sceneContent?: string;
baseAvatarUrl?: string;
idea?: string;
width?: number;
height?: number;
customPrompt?: string;
style?: string;
renderingSpeed?: string;
aspectRatio?: string;
model?: string;
}
export interface SceneImageTaskResponse {
success: boolean;
task_id: string;
message: string;
}
export interface SceneImageResponse {
scene_id: string;
scene_title?: string;
image_filename: string;
image_url: string;
width: number;
height: number;
}
export interface SceneAudioRequest {
sceneId: string;
sceneTitle: string;
text: string;
voiceId?: string;
language?: string;
speed?: number;
volume?: number;
pitch?: number;
emotion?: string;
englishNormalization?: boolean;
sampleRate?: number;
bitrate?: number;
channel?: string;
format?: string;
languageBoost?: string;
enableSyncMode?: boolean;
videoPlanContext?: any; // Context for intelligent voice/emotion selection
}
export interface SceneAudioResponse {
scene_id: string;
scene_title: string;
audio_filename: string;
audio_url: string;
provider: string;
model: string;
voice_id: string;
text_length: number;
file_size: number;
cost: number;
}
export const youtubeApi = {
/**
* Generate a video plan from user input.
*/
async createPlan(request: VideoPlanRequest): Promise<{ success: boolean; plan?: VideoPlan; message: string }> {
try {
const response = await apiClient.post(`${API_BASE}/plan`, request);
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to create video plan';
throw new Error(errorMessage);
}
},
/**
* Build scenes from a video plan.
*/
async buildScenes(videoPlan: VideoPlan, customScript?: string): Promise<{ success: boolean; scenes?: Scene[]; message: string }> {
try {
const response = await apiClient.post(`${API_BASE}/scenes`, {
video_plan: videoPlan,
custom_script: customScript || undefined,
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to build scenes';
throw new Error(errorMessage);
}
},
/**
* Update a single scene.
*/
async updateScene(
sceneId: number,
updates: {
narration?: string;
visual_description?: string;
duration_estimate?: number;
enabled?: boolean;
}
): Promise<{ success: boolean; scene?: Scene; message: string }> {
try {
const response = await apiClient.post(`${API_BASE}/scenes/${sceneId}/update`, updates);
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to update scene';
throw new Error(errorMessage);
}
},
/**
* Start rendering a video.
*/
async startRender(request: VideoRenderRequest): Promise<{ success: boolean; task_id?: string; message: string }> {
try {
const response = await apiClient.post(`${API_BASE}/render`, request);
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to start render';
throw new Error(errorMessage);
}
},
/**
* Get render task status.
* Returns null if task not found (matches podcast pattern for graceful handling).
*/
async getRenderStatus(taskId: string): Promise<TaskStatus | null> {
try {
const response = await apiClient.get(`${API_BASE}/render/${taskId}`);
// Backend returns null if task not found
return response.data || null;
} catch (error: any) {
// If 404, return null instead of throwing (matches podcast pattern)
if (error.response?.status === 404) {
return null;
}
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to get render status';
throw new Error(errorMessage);
}
},
/**
* Render a single scene video (scene-wise generation).
*/
async generateSceneVideo(params: SceneVideoRenderRequest): Promise<SceneVideoRenderResponse> {
try {
const response = await apiClient.post(`${API_BASE}/render/scene`, {
scene: params.scene,
video_plan: params.video_plan,
resolution: params.resolution || '720p',
voice_id: params.voice_id || 'Wise_Woman',
generate_audio_enabled: params.generate_audio_enabled ?? false,
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to start scene video render';
throw new Error(errorMessage);
}
},
/**
* Combine multiple scene videos into a final video.
*/
async combineVideos(params: CombineVideosRequest): Promise<{ success: boolean; task_id?: string; message: string }> {
try {
const response = await apiClient.post(`${API_BASE}/render/combine`, {
video_urls: params.scene_video_urls,
video_plan: params.video_plan,
resolution: params.resolution || '720p',
title: params.title,
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to start video combination';
throw new Error(errorMessage);
}
},
async listVideos(): Promise<VideoListResponse> {
try {
const response = await apiClient.get(`${API_BASE}/videos`);
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to list videos';
throw new Error(errorMessage);
}
},
/**
* Estimate the cost of rendering a video before rendering.
*/
async estimateCost(request: CostEstimateRequest): Promise<CostEstimateResponse> {
try {
const response = await apiClient.post(`${API_BASE}/estimate-cost`, request);
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to estimate cost';
throw new Error(errorMessage);
}
},
/**
* Get the status of an image generation task.
*/
async getImageGenerationStatus(taskId: string): Promise<TaskStatus> {
try {
const response = await apiClient.get(`${API_BASE}/image/status/${taskId}`);
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to get task status';
throw new Error(errorMessage);
}
},
/**
* Get video URL for a generated video.
*/
getVideoUrl(filename: string): string {
return `${API_BASE}/videos/${filename}`;
},
/**
* Upload a YouTube avatar image.
*/
async uploadAvatar(file: File): Promise<AvatarUploadResponse> {
try {
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post(`${API_BASE}/avatar/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to upload avatar';
throw new Error(errorMessage);
}
},
/**
* Make an uploaded avatar presentable for YouTube.
*/
async makeAvatarPresentable(
avatarUrl: string,
projectId?: string,
videoType?: string,
targetAudience?: string,
videoGoal?: string,
brandStyle?: string
): Promise<AvatarTransformResponse> {
try {
const formData = new FormData();
formData.append('avatar_url', avatarUrl);
if (projectId) formData.append('project_id', projectId);
if (videoType) formData.append('video_type', videoType);
if (targetAudience) formData.append('target_audience', targetAudience);
if (videoGoal) formData.append('video_goal', videoGoal);
if (brandStyle) formData.append('brand_style', brandStyle);
// Use aiApiClient for longer timeout (image editing takes ~30 seconds)
const response = await aiApiClient.post(`${API_BASE}/avatar/make-presentable`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to optimize avatar';
throw new Error(errorMessage);
}
},
/**
* Auto-generate a YouTube creator avatar.
*/
async generateCreatorAvatar(params: { projectId?: string; audience?: string; contentType?: string }): Promise<AvatarTransformResponse> {
try {
const formData = new FormData();
if (params.projectId) formData.append('project_id', params.projectId);
if (params.audience) formData.append('audience', params.audience);
if (params.contentType) formData.append('content_type', params.contentType);
const response = await apiClient.post(`${API_BASE}/avatar/generate`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to generate avatar';
throw new Error(errorMessage);
}
},
/**
* Regenerate a YouTube creator avatar using video plan context.
*/
async regenerateCreatorAvatar(videoPlan: VideoPlan, projectId?: string): Promise<AvatarTransformResponse> {
try {
const formData = new FormData();
formData.append('video_plan_json', JSON.stringify(videoPlan));
if (projectId) formData.append('project_id', projectId);
const response = await aiApiClient.post(`${API_BASE}/avatar/regenerate`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to regenerate avatar';
throw new Error(errorMessage);
}
},
/**
* Generate a YouTube scene image (with optional avatar consistency).
* Returns a task ID for polling status.
*/
async generateSceneImage(params: SceneImageRequest): Promise<SceneImageTaskResponse> {
try {
// Use aiApiClient for longer timeout (image generation can take 30-60 seconds)
const response = await apiClient.post(`${API_BASE}/image`, {
scene_id: params.sceneId,
scene_title: params.sceneTitle,
scene_content: params.sceneContent,
base_avatar_url: params.baseAvatarUrl || null,
idea: params.idea || null,
width: params.width || 1024,
height: params.height || 576,
custom_prompt: params.customPrompt || null,
style: params.style || null,
rendering_speed: params.renderingSpeed || null,
aspect_ratio: params.aspectRatio || null,
model: params.model,
});
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to generate scene image';
throw new Error(errorMessage);
}
},
/**
* Get avatar URL for display.
*/
getAvatarUrl(filename: string): string {
return `${API_BASE}/images/avatars/${filename}`;
},
/**
* Get scene image URL for display.
*/
getSceneImageUrl(filename: string): string {
return `${API_BASE}/images/scenes/${filename}`;
},
/**
* Generate a YouTube scene audio (narration).
*/
async generateSceneAudio(params: SceneAudioRequest): Promise<SceneAudioResponse> {
try {
// Use aiApiClient for longer timeout (audio generation can take 10-30 seconds)
const requestBody: any = {
scene_id: params.sceneId,
scene_title: params.sceneTitle,
text: params.text,
// Only send voice_id if explicitly set; otherwise backend will auto-select
speed: params.speed ?? 1.0,
volume: params.volume ?? 1.0,
pitch: params.pitch ?? 0.0,
emotion: params.emotion || 'neutral',
english_normalization: params.englishNormalization ?? false,
enable_sync_mode: params.enableSyncMode !== false,
};
if (params.voiceId !== undefined && params.voiceId !== null && String(params.voiceId).trim() !== '') {
requestBody.voice_id = params.voiceId;
}
if (params.language !== undefined && params.language !== null && String(params.language).trim() !== '') {
requestBody.language = params.language;
}
// Only include optional fields if they are defined and valid
// WaveSpeed has strict validation for these parameters
if (params.sampleRate !== undefined && params.sampleRate !== null) {
requestBody.sample_rate = params.sampleRate;
}
if (params.bitrate !== undefined && params.bitrate !== null) {
requestBody.bitrate = params.bitrate;
}
// Channel must be "1" or "2" (strings) - only include if valid
if (params.channel !== undefined && params.channel !== null && (params.channel === "1" || params.channel === "2")) {
requestBody.channel = params.channel;
}
if (params.format !== undefined && params.format !== null) {
requestBody.format = params.format;
}
if (params.languageBoost !== undefined && params.languageBoost !== null) {
requestBody.language_boost = params.languageBoost;
}
// Include video plan context for intelligent voice/emotion selection
if (params.videoPlanContext !== undefined && params.videoPlanContext !== null) {
requestBody.video_plan_context = params.videoPlanContext;
}
const response = await aiApiClient.post(`${API_BASE}/audio`, requestBody);
return response.data;
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.response?.data?.detail || error.message || 'Failed to generate scene audio';
throw new Error(errorMessage);
}
},
};