{
+export interface PersonaPlaceholderData {
+ research_angles?: string[];
+ recommended_presets?: Array<{
+ name: string;
+ keywords: string | string[];
+ description?: string;
+ }>;
+ industry?: string;
+ target_audience?: string;
+}
+
+export const getIndustryPlaceholders = (
+ industry: string,
+ personaData?: PersonaPlaceholderData
+): string[] => {
+ // If we have research persona data, use it to generate personalized placeholders
+ if (personaData) {
+ const personalizedPlaceholders: string[] = [];
+
+ // Priority 1: Use recommended presets (most actionable)
+ if (personaData.recommended_presets && personaData.recommended_presets.length > 0) {
+ const presets = personaData.recommended_presets.slice(0, 4); // Use first 4 presets
+ presets.forEach((preset) => {
+ const keywords = typeof preset.keywords === 'string'
+ ? preset.keywords
+ : Array.isArray(preset.keywords)
+ ? preset.keywords.join(', ')
+ : '';
+
+ if (keywords && keywords.trim().length > 0) {
+ // Make placeholders concise and actionable
+ personalizedPlaceholders.push(keywords.trim());
+ }
+ });
+ }
+
+ // Priority 2: Use research angles (formatted as actionable queries)
+ if (personaData.research_angles && personaData.research_angles.length > 0 && personalizedPlaceholders.length < 4) {
+ const angles = personaData.research_angles.slice(0, 4 - personalizedPlaceholders.length);
+ angles.forEach((angle) => {
+ // Format angle as a concise research query
+ let placeholder = angle;
+
+ // Replace topic placeholders with industry if available
+ if (placeholder.includes('{topic}') || placeholder.includes('{{topic}}')) {
+ placeholder = placeholder.replace(/\{topic\}/g, industry || 'your topic')
+ .replace(/\{\{topic\}\}/g, industry || 'your topic');
+ }
+
+ // Make it concise - remove "Research:" prefix if present, keep it natural
+ placeholder = placeholder.replace(/^Research:\s*/i, '').trim();
+
+ if (placeholder && placeholder.length > 10) { // Only add meaningful angles
+ personalizedPlaceholders.push(placeholder);
+ }
+ });
+ }
+
+ // If we have personalized placeholders, return them (with fallback to industry defaults)
+ if (personalizedPlaceholders.length > 0) {
+ // Add 1-2 industry-specific ones as backup for variety
+ const industryDefaults = getIndustryDefaults(industry);
+ const needed = Math.max(0, 5 - personalizedPlaceholders.length);
+ return [...personalizedPlaceholders, ...industryDefaults.slice(0, needed)];
+ }
+ }
+
+ // Fallback to industry-specific defaults
+ return getIndustryDefaults(industry);
+};
+
+/**
+ * Get industry-specific default placeholders (original logic)
+ */
+const getIndustryDefaults = (industry: string): string[] => {
const industryExamples: Record
= {
Healthcare: [
- "Research: AI-powered diagnostic tools in clinical practice\n\n💡 What you'll get:\n• FDA-approved AI medical devices\n• Clinical accuracy and patient outcomes\n• Implementation costs and ROI",
- "Analyze: Telemedicine adoption trends and patient satisfaction\n\n💡 Research includes:\n• Post-pandemic telehealth growth\n• Remote patient monitoring technologies\n• Insurance coverage and reimbursement",
- "Investigate: Personalized medicine and genomic testing advances\n\n💡 You'll discover:\n• Latest genomic sequencing technologies\n• Precision therapy success rates\n• Ethical considerations and regulations"
+ "AI diagnostic tools and clinical applications",
+ "Telemedicine adoption and patient outcomes",
+ "Personalized medicine and genomic testing",
+ "Healthcare automation and workflow optimization"
],
Technology: [
- "Investigate: Latest developments in edge computing and IoT\n\n💡 What you'll get:\n• Edge AI deployment strategies\n• 5G integration and performance\n• Industry use cases and benchmarks",
- "Compare: Cloud providers for enterprise SaaS applications\n\n💡 Research includes:\n• AWS vs Azure vs GCP feature comparison\n• Cost optimization strategies\n• Security and compliance certifications",
- "Analyze: Quantum computing breakthroughs and commercial applications\n\n💡 You'll discover:\n• Latest quantum hardware developments\n• Real-world problem solving examples\n• Investment landscape and timeline"
+ "Edge computing and IoT deployment strategies",
+ "Cloud provider comparison and cost optimization",
+ "Quantum computing breakthroughs and applications",
+ "AI and machine learning industry trends"
],
Finance: [
- "Research: DeFi regulatory landscape and compliance challenges\n\n💡 What you'll get:\n• Global regulatory frameworks\n• Compliance best practices\n• Risk management strategies",
- "Analyze: Digital banking customer retention strategies\n\n💡 Research includes:\n• Neobank growth and market share\n• Customer acquisition costs and LTV\n• Personalization and UX innovations",
- "Investigate: ESG investing trends and impact measurement\n\n💡 You'll discover:\n• ESG rating methodologies\n• Fund performance and returns\n• Regulatory requirements and reporting"
+ "DeFi regulations and compliance strategies",
+ "Digital banking and customer retention",
+ "ESG investing trends and performance",
+ "Fintech innovations and market analysis"
],
Marketing: [
- "Research: AI-powered marketing automation and personalization\n\n💡 What you'll get:\n• Top marketing AI platforms and features\n• ROI and conversion rate improvements\n• Implementation case studies",
- "Analyze: Influencer marketing ROI and authenticity trends\n\n💡 Research includes:\n• Micro vs macro influencer effectiveness\n• Platform-specific engagement rates\n• Brand partnership best practices",
- "Investigate: Privacy-first marketing in a cookieless world\n\n💡 You'll discover:\n• First-party data strategies\n• Contextual targeting innovations\n• Compliance with privacy regulations"
+ "AI marketing automation and personalization",
+ "Influencer marketing ROI and best practices",
+ "Privacy-first marketing in cookieless world",
+ "Content marketing strategies and trends"
],
Business: [
- "Research: Remote work policies and hybrid workplace models\n\n💡 What you'll get:\n• Productivity metrics and employee satisfaction\n• Technology infrastructure requirements\n• Cultural impact and change management",
- "Analyze: Supply chain resilience and diversification strategies\n\n💡 Research includes:\n• Nearshoring and reshoring trends\n• Technology solutions for visibility\n• Risk mitigation frameworks",
- "Investigate: Sustainability initiatives and corporate ESG programs\n\n💡 You'll discover:\n• Industry-specific sustainability benchmarks\n• Cost-benefit analysis of green initiatives\n• Stakeholder communication strategies"
+ "Remote work policies and hybrid models",
+ "Supply chain resilience and diversification",
+ "Sustainability initiatives and ESG programs",
+ "Business automation and efficiency"
],
Education: [
- "Research: EdTech tools for personalized learning experiences\n\n💡 What you'll get:\n• Adaptive learning platform comparisons\n• Student engagement and outcomes data\n• Implementation costs and training needs",
- "Analyze: Microlearning and skill-based education trends\n\n💡 Research includes:\n• Corporate training effectiveness\n• Platform and content recommendations\n• ROI and completion rates",
- "Investigate: AI tutoring systems and student support tools\n\n💡 You'll discover:\n• Natural language processing advances\n• Student performance improvements\n• Accessibility and inclusion features"
+ "EdTech tools and personalized learning",
+ "Microlearning and skill-based education",
+ "AI tutoring systems and student support",
+ "Online learning platforms and outcomes"
],
'Real Estate': [
- "Research: PropTech innovations transforming property management\n\n💡 What you'll get:\n• Smart building technologies and IoT\n• Tenant experience platforms\n• Operational efficiency gains",
- "Analyze: Virtual staging and 3D property tours adoption\n\n💡 Research includes:\n• Technology provider comparisons\n• Impact on sales velocity and pricing\n• Cost vs traditional staging",
- "Investigate: Real estate tokenization and fractional ownership\n\n💡 You'll discover:\n• Blockchain platforms and regulations\n• Investor demographics and demand\n• Liquidity and exit strategies"
+ "PropTech innovations and property management",
+ "Virtual staging and 3D property tours",
+ "Real estate tokenization and fractional ownership",
+ "Smart building technologies and IoT"
],
Travel: [
- "Research: Sustainable tourism trends and eco-travel preferences\n\n💡 What you'll get:\n• Green certification programs\n• Traveler willingness to pay premium\n• Destination best practices",
- "Analyze: AI-powered travel personalization and recommendations\n\n💡 Research includes:\n• Recommendation engine technologies\n• Booking conversion rate improvements\n• Customer lifetime value impact",
- "Investigate: Bleisure travel and workation destination trends\n\n💡 You'll discover:\n• Remote work-friendly destinations\n• Co-working and accommodation options\n• Digital nomad demographics"
+ "Sustainable tourism and eco-travel trends",
+ "AI travel personalization and recommendations",
+ "Bleisure travel and workation destinations",
+ "Travel technology and booking platforms"
]
};
+ // Default placeholders - concise and actionable
return industryExamples[industry] || [
- "Research: Latest AI advancements in your industry\n\n💡 What you'll get:\n• Recent breakthroughs and innovations\n• Key companies and technologies\n• Expert insights and market trends",
-
- "Write a blog on: Emerging trends shaping your industry in 2025\n\n💡 This will research:\n• Technology disruptions and innovations\n• Regulatory changes and compliance\n• Consumer behavior shifts",
-
- "Analyze: Best practices and success stories in your field\n\n💡 Research includes:\n• Industry leader strategies\n• Implementation case studies\n• ROI and performance metrics",
-
- "https://example.com/article\n\n💡 URL detected! Research will:\n• Extract key insights from the article\n• Find related sources and updates\n• Provide comprehensive context"
+ "Latest AI trends and innovations",
+ "Best practices and case studies",
+ "Market analysis and competitor insights",
+ "Emerging technologies and future predictions"
];
};
diff --git a/frontend/src/components/Research/types/intent.types.ts b/frontend/src/components/Research/types/intent.types.ts
new file mode 100644
index 00000000..39996e62
--- /dev/null
+++ b/frontend/src/components/Research/types/intent.types.ts
@@ -0,0 +1,328 @@
+/**
+ * Intent-Driven Research Types
+ *
+ * Types for the new intent-driven research system that:
+ * - Infers user intent from minimal input
+ * - Generates targeted queries
+ * - Analyzes results based on what user needs
+ */
+
+// ============================================================================
+// Enums
+// ============================================================================
+
+export type ResearchPurpose =
+ | 'learn'
+ | 'create_content'
+ | 'make_decision'
+ | 'compare'
+ | 'solve_problem'
+ | 'find_data'
+ | 'explore_trends'
+ | 'validate'
+ | 'generate_ideas';
+
+export type ContentOutput =
+ | 'blog'
+ | 'podcast'
+ | 'video'
+ | 'social_post'
+ | 'newsletter'
+ | 'presentation'
+ | 'report'
+ | 'whitepaper'
+ | 'email'
+ | 'general';
+
+export type ExpectedDeliverable =
+ | 'key_statistics'
+ | 'expert_quotes'
+ | 'case_studies'
+ | 'comparisons'
+ | 'trends'
+ | 'best_practices'
+ | 'step_by_step'
+ | 'pros_cons'
+ | 'definitions'
+ | 'citations'
+ | 'examples'
+ | 'predictions';
+
+export type ResearchDepthLevel = 'overview' | 'detailed' | 'expert';
+
+export type InputType = 'keywords' | 'question' | 'goal' | 'mixed';
+
+// ============================================================================
+// Core Intent Types
+// ============================================================================
+
+export interface ResearchIntent {
+ primary_question: string;
+ secondary_questions: string[];
+ purpose: ResearchPurpose;
+ content_output: ContentOutput;
+ expected_deliverables: ExpectedDeliverable[];
+ depth: ResearchDepthLevel;
+ focus_areas: string[];
+ perspective: string | null;
+ time_sensitivity: string | null;
+ input_type: InputType;
+ original_input: string;
+ confidence: number;
+ needs_clarification: boolean;
+ clarifying_questions: string[];
+}
+
+export interface ResearchQuery {
+ query: string;
+ purpose: ExpectedDeliverable;
+ provider: 'exa' | 'tavily' | 'google';
+ priority: number;
+ expected_results: string;
+}
+
+// ============================================================================
+// Deliverable Types
+// ============================================================================
+
+export interface StatisticWithCitation {
+ statistic: string;
+ value: string | null;
+ context: string;
+ source: string;
+ url: string;
+ credibility: number;
+ recency: string | null;
+}
+
+export interface ExpertQuote {
+ quote: string;
+ speaker: string;
+ title: string | null;
+ organization: string | null;
+ context: string | null;
+ source: string;
+ url: string;
+}
+
+export interface CaseStudySummary {
+ title: string;
+ organization: string;
+ challenge: string;
+ solution: string;
+ outcome: string;
+ key_metrics: string[];
+ source: string;
+ url: string;
+}
+
+export interface TrendAnalysis {
+ trend: string;
+ direction: 'growing' | 'declining' | 'emerging' | 'stable';
+ evidence: string[];
+ impact: string | null;
+ timeline: string | null;
+ sources: string[];
+}
+
+export interface ComparisonItem {
+ name: string;
+ description: string | null;
+ pros: string[];
+ cons: string[];
+ features: Record;
+ rating: number | null;
+ source: string | null;
+}
+
+export interface ComparisonTable {
+ title: string;
+ criteria: string[];
+ items: ComparisonItem[];
+ winner: string | null;
+ verdict: string | null;
+}
+
+export interface ProsCons {
+ subject: string;
+ pros: string[];
+ cons: string[];
+ balanced_verdict: string;
+}
+
+export interface SourceWithRelevance {
+ title: string;
+ url: string;
+ excerpt: string | null;
+ relevance_score: number;
+ relevance_reason: string | null;
+ content_type: string | null;
+ published_date: string | null;
+ credibility_score: number;
+}
+
+// ============================================================================
+// API Request/Response Types
+// ============================================================================
+
+export interface AnalyzeIntentRequest {
+ user_input: string;
+ keywords: string[];
+ use_persona: boolean;
+ use_competitor_data: boolean;
+}
+
+export interface AnalyzeIntentResponse {
+ success: boolean;
+ intent: ResearchIntent;
+ analysis_summary: string;
+ suggested_queries: ResearchQuery[];
+ suggested_keywords: string[];
+ suggested_angles: string[];
+ quick_options: QuickOption[];
+ error_message: string | null;
+}
+
+export interface QuickOption {
+ id: string;
+ label: string;
+ value: string | string[];
+ display: string | string[];
+ alternatives: string[];
+ confidence: number;
+ multi_select?: boolean;
+}
+
+export interface IntentDrivenResearchRequest {
+ user_input: string;
+ confirmed_intent?: ResearchIntent;
+ selected_queries?: ResearchQuery[];
+ max_sources: number;
+ include_domains: string[];
+ exclude_domains: string[];
+ skip_inference: boolean;
+}
+
+export interface IntentDrivenResearchResponse {
+ success: boolean;
+
+ // Direct answers
+ primary_answer: string;
+ secondary_answers: Record;
+
+ // Deliverables
+ statistics: StatisticWithCitation[];
+ expert_quotes: ExpertQuote[];
+ case_studies: CaseStudySummary[];
+ trends: TrendAnalysis[];
+ comparisons: ComparisonTable[];
+ best_practices: string[];
+ step_by_step: string[];
+ pros_cons: ProsCons | null;
+ definitions: Record;
+ examples: string[];
+ predictions: string[];
+
+ // Content-ready outputs
+ executive_summary: string;
+ key_takeaways: string[];
+ suggested_outline: string[];
+
+ // Sources and metadata
+ sources: SourceWithRelevance[];
+ confidence: number;
+ gaps_identified: string[];
+ follow_up_queries: string[];
+
+ // The intent used
+ intent: ResearchIntent | null;
+
+ // Error
+ error_message: string | null;
+}
+
+// ============================================================================
+// UI State Types
+// ============================================================================
+
+export interface IntentWizardState {
+ // User input
+ userInput: string;
+ keywords: string[];
+
+ // Inferred/confirmed intent
+ intent: ResearchIntent | null;
+
+ // Suggested queries
+ suggestedQueries: ResearchQuery[];
+ selectedQueries: ResearchQuery[];
+
+ // Quick options for confirmation
+ quickOptions: QuickOption[];
+
+ // Analysis
+ analysisSummary: string;
+ suggestedKeywords: string[];
+ suggestedAngles: string[];
+
+ // State
+ isAnalyzing: boolean;
+ isResearching: boolean;
+ hasConfirmedIntent: boolean;
+
+ // Results
+ result: IntentDrivenResearchResponse | null;
+
+ // Errors
+ error: string | null;
+}
+
+// ============================================================================
+// Display Helpers
+// ============================================================================
+
+export const PURPOSE_DISPLAY: Record = {
+ learn: 'Understand this topic',
+ create_content: 'Create content about this',
+ make_decision: 'Make a decision',
+ compare: 'Compare options',
+ solve_problem: 'Solve a problem',
+ find_data: 'Find specific data',
+ explore_trends: 'Explore trends',
+ validate: 'Validate information',
+ generate_ideas: 'Generate ideas',
+};
+
+export const CONTENT_OUTPUT_DISPLAY: Record = {
+ blog: 'Blog Post',
+ podcast: 'Podcast',
+ video: 'Video',
+ social_post: 'Social Post',
+ newsletter: 'Newsletter',
+ presentation: 'Presentation',
+ report: 'Report',
+ whitepaper: 'Whitepaper',
+ email: 'Email',
+ general: 'General Research',
+};
+
+export const DELIVERABLE_DISPLAY: Record = {
+ key_statistics: 'Key Statistics',
+ expert_quotes: 'Expert Quotes',
+ case_studies: 'Case Studies',
+ comparisons: 'Comparisons',
+ trends: 'Trends',
+ best_practices: 'Best Practices',
+ step_by_step: 'Step-by-Step Guide',
+ pros_cons: 'Pros & Cons',
+ definitions: 'Definitions',
+ citations: 'Citations',
+ examples: 'Examples',
+ predictions: 'Predictions',
+};
+
+export const DEPTH_DISPLAY: Record = {
+ overview: 'Quick Overview',
+ detailed: 'Detailed Analysis',
+ expert: 'Expert-Level Deep Dive',
+};
diff --git a/frontend/src/components/Research/types/research.types.ts b/frontend/src/components/Research/types/research.types.ts
index 394dde9e..2cd8c9b8 100644
--- a/frontend/src/components/Research/types/research.types.ts
+++ b/frontend/src/components/Research/types/research.types.ts
@@ -1,4 +1,9 @@
import { BlogResearchResponse, ResearchMode, ResearchConfig } from '../../../services/blogWriterApi';
+import {
+ ResearchIntent,
+ AnalyzeIntentResponse,
+ IntentDrivenResearchResponse
+} from './intent.types';
export interface WizardState {
currentStep: number;
@@ -11,6 +16,7 @@ export interface WizardState {
}
export interface ResearchExecution {
+ // Legacy API
executeResearch: (state: WizardState) => Promise;
stopExecution: () => void;
isExecuting: boolean;
@@ -18,6 +24,19 @@ export interface ResearchExecution {
progressMessages: Array<{ timestamp: string; message: string }>;
currentStatus: string;
result: any;
+
+ // Intent-driven API
+ useIntentMode: boolean;
+ setUseIntentMode: (enabled: boolean) => void;
+ isAnalyzingIntent: boolean;
+ intentAnalysis: AnalyzeIntentResponse | null;
+ confirmedIntent: ResearchIntent | null;
+ intentResult: IntentDrivenResearchResponse | null;
+ analyzeIntent: (state: WizardState) => Promise;
+ confirmIntent: (intent: ResearchIntent) => void;
+ updateIntentField: (field: K, value: ResearchIntent[K]) => void;
+ executeIntentResearch: (state: WizardState) => Promise;
+ clearIntent: () => void;
}
export interface WizardStepProps {
diff --git a/frontend/src/components/VideoStudio/ModulePlaceholder.tsx b/frontend/src/components/VideoStudio/ModulePlaceholder.tsx
new file mode 100644
index 00000000..4c2dacb8
--- /dev/null
+++ b/frontend/src/components/VideoStudio/ModulePlaceholder.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { Box, Paper, Stack, Typography, Chip } from '@mui/material';
+import { VideoStudioLayout } from './VideoStudioLayout';
+
+interface ModulePlaceholderProps {
+ title: string;
+ subtitle: string;
+ status?: 'live' | 'beta' | 'coming soon';
+ description?: string;
+ bullets?: string[];
+}
+
+const statusColor: Record = {
+ live: { bg: 'rgba(16,185,129,0.18)', color: '#10b981' },
+ beta: { bg: 'rgba(59,130,246,0.18)', color: '#3b82f6' },
+ 'coming soon': { bg: 'rgba(249,115,22,0.18)', color: '#f97316' },
+};
+
+export const ModulePlaceholder: React.FC = ({
+ title,
+ subtitle,
+ status = 'coming soon',
+ description,
+ bullets = [],
+}) => {
+ const style = statusColor[status] || statusColor['coming soon'];
+
+ return (
+
+
+
+
+ {description && (
+
+ {description}
+
+ )}
+ {bullets.length > 0 && (
+
+ {bullets.map(item => (
+
+
+ {item}
+
+
+ ))}
+
+ )}
+
+ We’ll surface cost estimates, provider choices, and templates here as the module goes live.
+
+
+
+
+ );
+};
+
+export default ModulePlaceholder;
diff --git a/frontend/src/components/VideoStudio/VideoStudioDashboard.tsx b/frontend/src/components/VideoStudio/VideoStudioDashboard.tsx
new file mode 100644
index 00000000..904fa5c2
--- /dev/null
+++ b/frontend/src/components/VideoStudio/VideoStudioDashboard.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { Grid, Paper, Stack, Typography, Divider } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+import { VideoStudioLayout } from './VideoStudioLayout';
+import { videoStudioModules } from './dashboard/modules';
+import { ModuleCard } from './dashboard/ModuleCard';
+
+export const VideoStudioDashboard: React.FC = () => {
+ const navigate = useNavigate();
+ const [hovered, setHovered] = React.useState('');
+
+ return (
+
+
+
+
+ {videoStudioModules.map(module => (
+
+ setHovered(module.key)}
+ onMouseLeave={() => setHovered('')}
+ onNavigate={navigate}
+ />
+
+ ))}
+
+
+
+ );
+};
+
+export default VideoStudioDashboard;
diff --git a/frontend/src/components/VideoStudio/VideoStudioLayout.tsx b/frontend/src/components/VideoStudio/VideoStudioLayout.tsx
new file mode 100644
index 00000000..05211612
--- /dev/null
+++ b/frontend/src/components/VideoStudio/VideoStudioLayout.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { Box } from '@mui/material';
+import { motion } from 'framer-motion';
+import type { Variants } from 'framer-motion';
+import DashboardHeader from '../shared/DashboardHeader';
+import type { DashboardHeaderProps } from '../shared/types';
+
+const MotionBox = motion(Box);
+
+const sparkleVariants: Variants = {
+ initial: { scale: 0, rotate: 0 },
+ animate: {
+ scale: [0, 1, 0],
+ rotate: [0, 180, 360],
+ transition: { duration: 2, repeat: Infinity, ease: 'easeInOut' },
+ },
+};
+
+interface VideoStudioLayoutProps {
+ children: React.ReactNode;
+ showHeader?: boolean;
+ headerProps?: DashboardHeaderProps;
+}
+
+const defaultHeaderProps: DashboardHeaderProps = {
+ title: 'AI Video Studio',
+ subtitle:
+ 'Provider-agnostic, cost-transparent video creation. Generate, enhance, and optimize videos with guided presets.',
+};
+
+export const VideoStudioLayout: React.FC = ({
+ children,
+ showHeader = true,
+ headerProps,
+}) => {
+ const mergedHeaderProps = { ...defaultHeaderProps, ...headerProps };
+
+ return (
+
+
+ {[...Array(20)].map((_, i) => (
+
+ ))}
+
+
+
+ {showHeader && (
+
+
+
+ )}
+ {children}
+
+
+ );
+};
+
+export default VideoStudioLayout;
diff --git a/frontend/src/components/VideoStudio/dashboard/ModuleCard.tsx b/frontend/src/components/VideoStudio/dashboard/ModuleCard.tsx
new file mode 100644
index 00000000..e1a332c7
--- /dev/null
+++ b/frontend/src/components/VideoStudio/dashboard/ModuleCard.tsx
@@ -0,0 +1,202 @@
+import React from 'react';
+import {
+ Box,
+ Paper,
+ Stack,
+ Typography,
+ Chip,
+ Button,
+ Tooltip,
+ Divider,
+} from '@mui/material';
+import LaunchIcon from '@mui/icons-material/Launch';
+import LockIcon from '@mui/icons-material/Lock';
+import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
+import SavingsIcon from '@mui/icons-material/Savings';
+import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
+import { alpha } from '@mui/material/styles';
+import type { ModuleConfig } from './types';
+import { statusStyles } from './modules';
+import { CreateVideoPreview, AvatarVideoPreview, EnhanceVideoPreview } from './previews';
+
+interface ModuleCardProps {
+ module: ModuleConfig;
+ isHovered: boolean;
+ onMouseEnter: () => void;
+ onMouseLeave: () => void;
+ onNavigate: (route: string) => void;
+}
+
+export const ModuleCard: React.FC = ({
+ module,
+ isHovered,
+ onMouseEnter,
+ onMouseLeave,
+ onNavigate,
+}) => {
+ const status = statusStyles[module.status];
+ const disabled = module.status !== 'live';
+
+ return (
+
+
+
+
+ {module.icon}
+
+
+
+ {module.title}
+
+
+ {module.subtitle}
+
+
+
+
+
+
+
+ {module.description}
+
+
+
+ {module.highlights.map(item => (
+
+ ))}
+
+
+
+
+
+
+
+ {module.help || 'Built for creators: pick a template and we guide duration/aspect and cost.'}
+
+
+
+
+
+
+
+
+ {module.pricingNote || 'Cost shown before run (duration, resolution, provider).'}
+
+
+
+ {module.costDrivers && (
+
+ {module.costDrivers.map(driver => (
+ }
+ label={driver}
+ sx={{
+ backgroundColor: 'rgba(15,118,110,0.25)',
+ color: '#99f6e4',
+ border: '1px solid rgba(34,197,94,0.35)',
+ fontWeight: 600,
+ }}
+ />
+ ))}
+
+ )}
+
+
+
+ ETA: {module.eta || 'TBD'}
+
+
+
+ {/* Visual Preview Component */}
+ {module.status === 'live' && (
+
+ {module.key === 'create' && }
+ {module.key === 'avatar' && }
+ {module.key === 'enhance' && }
+
+ )}
+
+
+ : }
+ disabled={disabled}
+ onClick={() => onNavigate(module.route)}
+ sx={{
+ textTransform: 'none',
+ fontWeight: 700,
+ boxShadow: 'none',
+ background: disabled ? 'rgba(148,163,184,0.25)' : 'linear-gradient(120deg,#6366f1,#8b5cf6)',
+ }}
+ >
+ {disabled ? 'Preview' : 'Open'}
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/dashboard/constants.ts b/frontend/src/components/VideoStudio/dashboard/constants.ts
new file mode 100644
index 00000000..9ba4a062
--- /dev/null
+++ b/frontend/src/components/VideoStudio/dashboard/constants.ts
@@ -0,0 +1,50 @@
+export const createVideoExamples = [
+ {
+ id: 'instagram-reel',
+ label: 'Instagram Reel',
+ prompt: 'A modern coffee shop interior with baristas crafting latte art, warm golden hour lighting streaming through large windows, customers chatting at wooden tables, cozy atmosphere, 9:16 vertical format',
+ description: 'Perfect for Instagram Reels and TikTok. Shows how text descriptions become engaging short-form video content.',
+ price: '$0.50',
+ eta: '~15s',
+ provider: 'Auto-select',
+ video: '/videos/scene_1_user_33Gz1FPI86V_0a5d0d71.mp4',
+ platform: 'Instagram',
+ useCase: 'Social media content',
+ },
+ {
+ id: 'linkedin-post',
+ label: 'LinkedIn Post',
+ prompt: 'Professional workspace with laptop, notebook, and coffee cup on a minimalist desk, soft natural lighting, clean modern office environment, 16:9 format',
+ description: 'Ideal for LinkedIn posts and professional content. Demonstrates how simple descriptions create polished business videos.',
+ price: '$0.75',
+ eta: '~18s',
+ provider: 'Auto-select',
+ video: '/videos/text-video-voiceover.mp4',
+ platform: 'LinkedIn',
+ useCase: 'Professional content',
+ },
+ {
+ id: 'youtube-short',
+ label: 'YouTube Short',
+ prompt: 'Dynamic product showcase with rotating view, vibrant colors, smooth camera movement, energetic music vibe, 9:16 vertical format',
+ description: 'Great for YouTube Shorts and product demos. Shows how product descriptions transform into engaging video content.',
+ price: '$0.60',
+ eta: '~16s',
+ provider: 'Auto-select',
+ video: '/videos/scene_1_user_33Gz1FPI86V_0a5d0d71.mp4',
+ platform: 'YouTube',
+ useCase: 'Product marketing',
+ },
+];
+
+export const enhanceVideoExamples = {
+ before: '/videos/scene_1_user_33Gz1FPI86V_0a5d0d71.mp4',
+ after: '/videos/text-video-voiceover.mp4',
+ description: 'Upscale 480p to 1080p, boost frame rate from 24fps to 60fps, and enhance clarity for professional use.',
+};
+
+export const avatarExamples = {
+ image: '/images/scene_1_Welcome_to_the_Cloud_Kitchen___ae6436d9.png',
+ video: '/videos/text-video-voiceover.mp4',
+ description: 'Upload a photo and audio to create a talking avatar perfect for explainer videos, tutorials, and personalized messages.',
+};
diff --git a/frontend/src/components/VideoStudio/dashboard/modules.tsx b/frontend/src/components/VideoStudio/dashboard/modules.tsx
new file mode 100644
index 00000000..95917787
--- /dev/null
+++ b/frontend/src/components/VideoStudio/dashboard/modules.tsx
@@ -0,0 +1,203 @@
+import React from 'react';
+import MovieCreationIcon from '@mui/icons-material/MovieCreation';
+import FaceRetouchingNaturalIcon from '@mui/icons-material/FaceRetouchingNatural';
+import EditIcon from '@mui/icons-material/Edit';
+import HighQualityIcon from '@mui/icons-material/HighQuality';
+import TimelineIcon from '@mui/icons-material/Timeline';
+import TransformIcon from '@mui/icons-material/Transform';
+import ShareIcon from '@mui/icons-material/Share';
+import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
+import LibraryBooksIcon from '@mui/icons-material/LibraryBooks';
+import TranslateIcon from '@mui/icons-material/Translate';
+import WallpaperIcon from '@mui/icons-material/Wallpaper';
+import MusicNoteIcon from '@mui/icons-material/MusicNote';
+import type { ModuleConfig } from './types';
+
+export const statusStyles = {
+ live: { label: 'Live', color: '#10b981' },
+ beta: { label: 'Beta', color: '#3b82f6' },
+ 'coming soon': { label: 'Coming Soon', color: '#f97316' },
+};
+
+export const videoStudioModules: ModuleConfig[] = [
+ {
+ key: 'create',
+ title: 'Create Studio',
+ subtitle: 'Turn your ideas into videos',
+ description:
+ 'Describe your video idea and we create it for you. Perfect for Instagram Reels, TikTok, YouTube Shorts, LinkedIn posts, and more. We automatically choose the best settings for your platform.',
+ highlights: ['Text to Video', 'Image to Video', 'Platform Ready'],
+ status: 'live',
+ route: '/video-studio/create',
+ pricingNote: 'Cost depends on video length and quality. We show you the price before generating.',
+ eta: 'Now',
+ icon: ,
+ help: 'Perfect for creating engaging social media content. Just describe what you want and we handle the rest. Add background music or voiceover later.',
+ costDrivers: ['Video length (5–10 seconds)', 'Quality (480p/720p/1080p)', 'Platform format'],
+ },
+ {
+ key: 'avatar',
+ title: 'Avatar Studio',
+ subtitle: 'Create talking videos from photos',
+ description:
+ 'Upload a photo and audio to create a talking avatar. Perfect for explainer videos, tutorials, personalized messages, and social media content. Your photo comes to life with perfect lip-sync.',
+ highlights: ['Talking Avatars', 'Lip-sync', 'Translation'],
+ status: 'beta',
+ route: '/video-studio/avatar',
+ pricingNote: 'Cost depends on video length and quality',
+ eta: 'Beta',
+ icon: ,
+ help: 'Great for creating personalized video messages, explainer videos, and tutorials. Upload your photo and audio, and we create a talking video.',
+ costDrivers: ['Video length', 'Quality'],
+ },
+ {
+ key: 'enhance',
+ title: 'Enhance Studio',
+ subtitle: 'Upgrade your video quality',
+ description:
+ 'Transform low-resolution videos into professional-quality content. Upscale from 480p to 1080p or 4K, boost frame rate, and improve clarity. Perfect for upgrading social media content or preparing videos for YouTube.',
+ highlights: ['Upscale Quality', 'Smooth Motion', 'Frame Rate Boost'],
+ status: 'live',
+ route: '/video-studio/enhance',
+ pricingNote: 'Cost depends on original quality and target quality',
+ eta: 'Now',
+ icon: ,
+ help: 'Perfect for improving videos shot on phones or upgrading old content. Make your videos look professional and ready for any platform.',
+ costDrivers: ['Original quality', 'Target quality', 'Video length'],
+ },
+ {
+ key: 'extend',
+ title: 'Extend Studio',
+ subtitle: 'Extend short clips seamlessly',
+ description:
+ 'Turn short video clips into longer videos with seamless motion and audio continuity. Perfect for extending social media content, creating longer scenes from existing footage, and adding smooth transitions.',
+ highlights: ['Motion Continuity', 'Audio Sync', 'Seamless Extension'],
+ status: 'live',
+ route: '/video-studio/extend',
+ pricingNote: 'Cost depends on extension duration and resolution',
+ eta: 'Now',
+ icon: ,
+ help: 'Great for extending short clips into longer videos. Describe how you want the video to continue, and we create a seamless extension with preserved motion and style.',
+ costDrivers: ['Extension duration', 'Resolution', 'Video length'],
+ },
+ {
+ key: 'edit',
+ title: 'Edit Studio',
+ subtitle: 'Trim, enhance, and customize',
+ description:
+ 'Trim and cut videos, adjust speed, stabilize shaky footage, replace backgrounds, swap faces, add captions and subtitles, and color grade. All the editing tools you need in one place.',
+ highlights: ['Trim & Cut', 'Background Swap', 'Add Captions'],
+ status: 'coming soon',
+ route: '/video-studio/edit',
+ pricingNote: 'Cost depends on video length and number of edits',
+ eta: 'Coming soon',
+ icon: ,
+ help: 'Complete video editing suite for content creators. Make your videos perfect before sharing on social media.',
+ costDrivers: ['Video length', 'Number of edits'],
+ },
+ {
+ key: 'transform',
+ title: 'Transform Studio',
+ subtitle: 'Change format and style',
+ description:
+ 'Convert videos between different formats (MP4, MOV, WebM, GIF), change aspect ratios (16:9, 9:16, 1:1), adjust speed, scale resolution, and compress files. All transformations use fast FFmpeg processing.',
+ highlights: ['Format Conversion', 'Aspect Ratio', 'Speed Control', 'Resolution Scaling', 'Compression'],
+ status: 'live',
+ route: '/video-studio/transform',
+ pricingNote: 'Free (FFmpeg processing)',
+ eta: 'Now',
+ icon: ,
+ help: 'Perfect for adapting one video for multiple platforms. Convert formats, change aspect ratios, adjust speed, scale resolution, and compress files - all for free using FFmpeg.',
+ costDrivers: ['Free processing'],
+ },
+ {
+ key: 'social',
+ title: 'Social Optimizer',
+ subtitle: 'One-click platform optimization',
+ description:
+ 'Create optimized versions of your video for Instagram, TikTok, YouTube, LinkedIn, and Twitter with one click. Includes safe zones, compression, and thumbnails. Make your content platform-ready instantly.',
+ highlights: ['Multi-Platform', 'Safe Zones', 'Auto Thumbnails'],
+ status: 'live',
+ route: '/video-studio/social',
+ pricingNote: 'Free (FFmpeg processing)',
+ eta: 'Now',
+ icon: ,
+ help: 'Save time by creating platform-optimized versions automatically. One video, multiple platforms, perfect formatting for each.',
+ costDrivers: ['Free processing'],
+ },
+ {
+ key: 'faceswap',
+ title: 'Face Swap Studio',
+ subtitle: 'Replace characters in videos',
+ description:
+ 'Swap faces or characters in videos using MoCha AI. Upload a reference image and source video to seamlessly replace characters while preserving motion, emotion, and camera perspective.',
+ highlights: ['Character Replacement', 'Motion Preservation', 'Identity Consistency'],
+ status: 'live',
+ route: '/video-studio/face-swap',
+ pricingNote: '$0.04/s (480p) or $0.08/s (720p), min 5s charge',
+ eta: 'Now',
+ icon: ,
+ help: 'Perfect for film, advertising, digital avatars, and creative character transformation. No pose or depth maps needed.',
+ costDrivers: ['Video duration', 'Resolution (480p/720p)'],
+ },
+ {
+ key: 'video-translate',
+ title: 'Video Translate Studio',
+ subtitle: 'Translate videos to 70+ languages',
+ description:
+ 'Translate videos to 70+ languages and 175+ dialects with AI. Preserves lip-sync and natural voice. Perfect for global content, localization, and reaching international audiences.',
+ highlights: ['70+ Languages', 'Lip-sync Preservation', 'Natural Voice'],
+ status: 'live',
+ route: '/video-studio/video-translate',
+ pricingNote: '$0.0375/second',
+ eta: 'Now',
+ icon: ,
+ help: 'Perfect for global content creators, localization, and reaching international audiences. No voice actors or dubbing needed.',
+ costDrivers: ['Video duration'],
+ },
+ {
+ key: 'video-background-remover',
+ title: 'Background Remover Studio',
+ subtitle: 'Remove or replace video backgrounds',
+ description:
+ 'Remove or replace video backgrounds with clean matting and edge-aware blending. Upload a background image to replace, or leave empty for transparent background. Perfect for product videos, presentations, and creative content.',
+ highlights: ['Clean Matting', 'Edge-Aware Blending', 'Background Replacement'],
+ status: 'live',
+ route: '/video-studio/video-background-remover',
+ pricingNote: '$0.01/second (min $0.05, max $6.00)',
+ eta: 'Now',
+ icon: ,
+ help: 'Perfect for product videos, presentations, and creative content. Remove backgrounds or replace them with custom images.',
+ costDrivers: ['Video duration'],
+ },
+ {
+ key: 'add-audio-to-video',
+ title: 'Add Audio to Video Studio',
+ subtitle: 'Generate realistic Foley and ambient audio',
+ description:
+ 'Generate realistic Foley and ambient audio directly from video using AI. Choose between Hunyuan Video Foley (48 kHz hi-fi, multi-scene sync) or Think Sound (context-aware, flat rate pricing). Perfect for post-production, social content, and prototyping.',
+ highlights: ['2 AI Models', '48 kHz Hi-Fi', 'Context-Aware'],
+ status: 'live',
+ route: '/video-studio/add-audio-to-video',
+ pricingNote: '$0.02/s (Hunyuan) or $0.05/video (Think Sound)',
+ eta: 'Now',
+ icon: ,
+ help: 'Perfect for post-production, social content, and prototyping. Use optional text prompts to guide specific sounds or let AI automatically generate appropriate audio based on visual cues.',
+ costDrivers: ['Video duration'],
+ },
+ {
+ key: 'library',
+ title: 'Asset Library',
+ subtitle: 'Organize and manage your videos',
+ description:
+ 'Keep all your videos organized with AI-powered tagging, version tracking, usage analytics, and secure sharing. Manage your video content library like a pro.',
+ highlights: ['AI Tagging', 'Version Control', 'Usage Analytics'],
+ status: 'beta',
+ route: '/video-studio/library',
+ pricingNote: 'Storage and download costs',
+ eta: 'Beta',
+ icon: ,
+ help: 'Perfect for content creators managing multiple videos. Keep everything organized, track usage, and share securely.',
+ costDrivers: ['Storage space', 'Downloads'],
+ },
+];
diff --git a/frontend/src/components/VideoStudio/dashboard/previews/AvatarVideoPreview.tsx b/frontend/src/components/VideoStudio/dashboard/previews/AvatarVideoPreview.tsx
new file mode 100644
index 00000000..97f2f3f7
--- /dev/null
+++ b/frontend/src/components/VideoStudio/dashboard/previews/AvatarVideoPreview.tsx
@@ -0,0 +1,111 @@
+import React from 'react';
+import { Box, Stack, Typography, Chip } from '@mui/material';
+import { avatarExamples } from '../constants';
+import { OptimizedImage } from '../../../ImageStudio/dashboard/utils/OptimizedImage';
+import { OptimizedVideo } from '../../../ImageStudio/dashboard/utils/OptimizedVideo';
+
+export const AvatarVideoPreview: React.FC = () => {
+ return (
+
+
+
+
+ Step 1: Upload Photo + Audio
+
+ {avatarExamples.description}
+
+ {['Photo upload', 'Audio upload', 'Lip-sync'].map(label => (
+
+ ))}
+
+
+
+
+
+
+
+
+ Result: Talking Avatar
+
+
+
+
+
+
+
+
+
+ Perfect for explainer videos, tutorials, personalized messages, and social media content. Your photo comes to life with perfect lip-sync.
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/dashboard/previews/CreateVideoPreview.tsx b/frontend/src/components/VideoStudio/dashboard/previews/CreateVideoPreview.tsx
new file mode 100644
index 00000000..5a13f87a
--- /dev/null
+++ b/frontend/src/components/VideoStudio/dashboard/previews/CreateVideoPreview.tsx
@@ -0,0 +1,133 @@
+import React from 'react';
+import { Box, Stack, Typography, Chip } from '@mui/material';
+import { createVideoExamples } from '../constants';
+import { OptimizedVideo } from '../../../ImageStudio/dashboard/utils/OptimizedVideo';
+
+export const CreateVideoPreview: React.FC = () => {
+ const [textHovered, setTextHovered] = React.useState(false);
+ const [exampleIndex, setExampleIndex] = React.useState(0);
+ const example = createVideoExamples[exampleIndex];
+ const videoWidth = textHovered ? '20%' : '70%';
+ const textWidth = textHovered ? '80%' : '30%';
+
+ return (
+
+
+
+
+ {createVideoExamples.map((_, idx) => (
+ setExampleIndex(idx)}
+ sx={{
+ width: 32,
+ height: 10,
+ borderRadius: 999,
+ background: idx === exampleIndex ? '#c4b5fd' : 'rgba(255,255,255,0.3)',
+ cursor: 'pointer',
+ transition: 'background 0.2s ease',
+ }}
+ />
+ ))}
+
+
+ setTextHovered(true)}
+ onMouseLeave={() => setTextHovered(false)}
+ >
+
+
+ Step 1: Enter Your Video Requirements
+
+
+ Example Prompt
+
+ {example.prompt}
+
+ {example.description}
+
+
+
+
+
+
+
+ Best for: {example.useCase}
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/dashboard/previews/EnhanceVideoPreview.tsx b/frontend/src/components/VideoStudio/dashboard/previews/EnhanceVideoPreview.tsx
new file mode 100644
index 00000000..8845a75e
--- /dev/null
+++ b/frontend/src/components/VideoStudio/dashboard/previews/EnhanceVideoPreview.tsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import { Box, Stack, Typography, Chip } from '@mui/material';
+import { enhanceVideoExamples } from '../constants';
+import { OptimizedVideo } from '../../../ImageStudio/dashboard/utils/OptimizedVideo';
+
+export const EnhanceVideoPreview: React.FC = () => {
+ return (
+
+
+
+
+ Before: 480p @ 24fps
+
+ {enhanceVideoExamples.description}
+
+ {['480p', '24fps', 'Standard'].map(label => (
+
+ ))}
+
+
+
+
+
+
+
+ After: 1080p @ 60fps
+
+ Enhanced quality ready for professional use
+
+ {['1080p', '60fps', 'Enhanced'].map(label => (
+
+ ))}
+
+
+
+
+
+
+
+ Transform low-resolution videos into professional-quality content. Perfect for upgrading social media content or preparing videos for YouTube and other platforms.
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/dashboard/previews/index.ts b/frontend/src/components/VideoStudio/dashboard/previews/index.ts
new file mode 100644
index 00000000..f01fc0e3
--- /dev/null
+++ b/frontend/src/components/VideoStudio/dashboard/previews/index.ts
@@ -0,0 +1,3 @@
+export { CreateVideoPreview } from './CreateVideoPreview';
+export { AvatarVideoPreview } from './AvatarVideoPreview';
+export { EnhanceVideoPreview } from './EnhanceVideoPreview';
diff --git a/frontend/src/components/VideoStudio/dashboard/types.ts b/frontend/src/components/VideoStudio/dashboard/types.ts
new file mode 100644
index 00000000..563da526
--- /dev/null
+++ b/frontend/src/components/VideoStudio/dashboard/types.ts
@@ -0,0 +1,16 @@
+export type ModuleStatus = 'live' | 'beta' | 'coming soon';
+
+export interface ModuleConfig {
+ key: string;
+ title: string;
+ subtitle: string;
+ description: string;
+ highlights: string[];
+ status: ModuleStatus;
+ route: string;
+ pricingNote?: string;
+ eta?: string;
+ icon?: React.ReactNode;
+ help?: string;
+ costDrivers?: string[];
+}
diff --git a/frontend/src/components/VideoStudio/index.ts b/frontend/src/components/VideoStudio/index.ts
new file mode 100644
index 00000000..9f6362eb
--- /dev/null
+++ b/frontend/src/components/VideoStudio/index.ts
@@ -0,0 +1,14 @@
+export { VideoStudioLayout } from './VideoStudioLayout';
+export { VideoStudioDashboard } from './VideoStudioDashboard';
+export { CreateVideo } from './modules/CreateVideo';
+export { AvatarVideo } from './modules/AvatarVideo';
+export { EnhanceVideo } from './modules/EnhanceVideo';
+export { ExtendVideo } from './modules/ExtendVideo';
+export { EditVideo } from './modules/EditVideo';
+export { TransformVideo } from './modules/TransformVideo/TransformVideo';
+export { SocialVideo } from './modules/SocialVideo/SocialVideo';
+export { FaceSwap } from './modules/FaceSwap';
+export { VideoTranslate } from './modules/VideoTranslate';
+export { VideoBackgroundRemover } from './modules/VideoBackgroundRemover';
+export { AddAudioToVideo } from './modules/AddAudioToVideo';
+export { LibraryVideo } from './modules/LibraryVideo';
diff --git a/frontend/src/components/VideoStudio/modules/AddAudioToVideo/AddAudioToVideo.tsx b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/AddAudioToVideo.tsx
new file mode 100644
index 00000000..3138cf4a
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/AddAudioToVideo.tsx
@@ -0,0 +1,315 @@
+import React from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress, LinearProgress, Alert, Paper } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useAddAudioToVideo } from './hooks/useAddAudioToVideo';
+import { VideoUpload, AudioSettings } from './components';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import ErrorIcon from '@mui/icons-material/Error';
+import MusicNoteIcon from '@mui/icons-material/MusicNote';
+
+const AddAudioToVideo: React.FC = () => {
+ const {
+ videoFile,
+ videoPreview,
+ model,
+ prompt,
+ seed,
+ processing,
+ progress,
+ error,
+ result,
+ setVideoFile,
+ setModel,
+ setPrompt,
+ setSeed,
+ canAddAudio,
+ costHint,
+ addAudio,
+ reset,
+ } = useAddAudioToVideo();
+
+ return (
+
+
+ {/* Left Panel - Upload & Settings */}
+
+
+
+
+
+
+
+ : }
+ onClick={addAudio}
+ disabled={!canAddAudio || processing}
+ sx={{
+ py: 1.5,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ '&:disabled': {
+ backgroundColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {processing ? 'Processing...' : 'Add Audio to Video'}
+
+
+
+ {processing && (
+
+
+
+ Generating audio... This may take a few minutes...
+
+
+
+
+ )}
+
+ {error && (
+ {}} icon={}>
+ {error}
+
+ )}
+
+ {result && (
+ }
+ action={
+
+ }
+ >
+ Audio added successfully! Cost: ${result.cost.toFixed(4)}
+
+ )}
+
+
+
+ {/* Right Panel - Preview & Results */}
+
+
+ {result ? (
+ // Result view
+
+
+ Video with Audio
+
+
+
+
+
+ Audio Added ({result.model_used})
+
+
+
+
+
+
+
+
+
+ ) : videoPreview ? (
+ // Original video preview
+
+
+ Original Video Preview
+
+
+
+
+
+ Upload a video and configure audio settings to get started
+
+
+
+
+ ) : (
+
+
+ Upload a video to see preview
+
+
+ Your video with audio will appear here
+
+
+ )}
+
+ {/* Info Box */}
+
+
+ About Audio Generation Models
+
+
+
+
+ Hunyuan Video Foley:
+
+
+
+ Multi-scene synchronization – Audio aligned to complex, fast-cut visuals
+
+
+ 48 kHz hi-fi output – Professional clarity with low noise
+
+
+ Pricing: $0.02/second
+
+
+
+
+
+
+ Think Sound:
+
+
+
+ Context-aware sound – Analyzes visual elements to generate matching audio
+
+
+ Prompt-guided output with built-in Prompt Enhancer for AI-assisted optimization
+
+
+ High-quality output with clear, realistic audio
+
+
+ Pricing: $0.05 per video (flat rate)
+
+
+
+
+
+ Pro Tips for Best Quality:
+
+
+
+ Use videos with clear visuals and distinct actions for best audio matching
+
+
+ Add prompts to specify the type of sound (e.g., "engine roaring", "footsteps on gravel")
+
+
+ Ensure videos have visible sound-producing elements like movement or impacts
+
+
+ Fix the seed when iterating to compare different prompt variations
+
+
+
+
+
+
+
+ );
+};
+
+export { AddAudioToVideo };
+export default AddAudioToVideo;
diff --git a/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/AudioSettings.tsx b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/AudioSettings.tsx
new file mode 100644
index 00000000..7c80dc6c
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/AudioSettings.tsx
@@ -0,0 +1,190 @@
+import React from 'react';
+import { Box, Stack, Typography, FormControl, InputLabel, Select, MenuItem, TextField, Paper, Chip } from '@mui/material';
+import MusicNoteIcon from '@mui/icons-material/MusicNote';
+import type { AudioModel } from '../hooks/useAddAudioToVideo';
+
+interface AudioSettingsProps {
+ model: AudioModel;
+ prompt: string;
+ seed: number | null;
+ costHint: string;
+ onModelChange: (model: AudioModel) => void;
+ onPromptChange: (prompt: string) => void;
+ onSeedChange: (seed: number | null) => void;
+}
+
+export const AudioSettings: React.FC = ({
+ model,
+ prompt,
+ seed,
+ costHint,
+ onModelChange,
+ onPromptChange,
+ onSeedChange,
+}) => {
+ return (
+
+
+
+
+
+
+ Audio Settings
+
+
+
+
+
+
+ Audio Model
+
+
+
+
+
+ {model === 'hunyuan-video-foley'
+ ? 'Tencent Hunyuan\'s video-to-audio model: Multi-scene synchronization, 48 kHz hi-fi output, SOTA performance'
+ : model === 'think-sound'
+ ? 'Context-aware video-to-audio generation: Analyzes visual elements to generate matching audio. Features built-in Prompt Enhancer for AI-assisted optimization.'
+ : 'Generate audio from video'}
+
+
+
+
+
+ Audio Prompt (Optional)
+
+ onPromptChange(e.target.value)}
+ placeholder={
+ model === 'hunyuan-video-foley'
+ ? "Briefly describe the mood or key sounds (e.g., 'Rainy street ambience, soft footsteps, distant cars' or 'Kitchen ASMR: chopping vegetables, sizzling pan')"
+ : "Describe the type of sound you want (e.g., 'engine roaring', 'footsteps on gravel', 'ocean waves crashing'). The built-in Prompt Enhancer will optimize your prompt for better results."
+ }
+ sx={{
+ backgroundColor: '#fff',
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderColor: '#e2e8f0',
+ },
+ }}
+ />
+
+ {model === 'hunyuan-video-foley'
+ ? 'Optional: Leave empty to let AI automatically generate appropriate sounds based on visual cues'
+ : 'Optional: Add text descriptions to guide the style and type of audio generated. The built-in Prompt Enhancer will optimize your prompt for better results. Use clear, descriptive prompts for best quality.'}
+
+
+
+
+
+ Seed (Optional)
+
+ {
+ const value = e.target.value;
+ onSeedChange(value === '' ? null : parseInt(value, 10));
+ }}
+ placeholder="-1 for random"
+ sx={{
+ backgroundColor: '#fff',
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderColor: '#e2e8f0',
+ },
+ }}
+ />
+
+ Use -1 for random seed, or specify a number for reproducible results. Fix the seed when iterating to compare different prompt variations.
+
+
+
+
+
+
+ Estimated Cost:
+
+
+
+
+ {model === 'think-sound'
+ ? 'Pricing: $0.05 per video (flat rate)'
+ : 'Pricing: $0.02/second (estimated)'}
+
+ {model === 'hunyuan-video-foley' && (
+
+ Minimum charge: 5 seconds | Maximum: 10 minutes (600 seconds)
+
+ )}
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/VideoUpload.tsx b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/VideoUpload.tsx
new file mode 100644
index 00000000..ab02562c
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/VideoUpload.tsx
@@ -0,0 +1,125 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import VideocamIcon from '@mui/icons-material/Videocam';
+
+interface VideoUploadProps {
+ videoPreview: string | null;
+ onVideoSelect: (file: File | null) => void;
+}
+
+export const VideoUpload: React.FC = ({
+ videoPreview,
+ onVideoSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+ onVideoSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onVideoSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Source Video
+
+
+ {videoPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload video
+
+
+ MP4, WebM up to 500MB (max 10 minutes)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/index.ts b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/index.ts
new file mode 100644
index 00000000..6eeb61ed
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/components/index.ts
@@ -0,0 +1,2 @@
+export { VideoUpload } from './VideoUpload';
+export { AudioSettings } from './AudioSettings';
diff --git a/frontend/src/components/VideoStudio/modules/AddAudioToVideo/hooks/useAddAudioToVideo.ts b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/hooks/useAddAudioToVideo.ts
new file mode 100644
index 00000000..b4f0ee46
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/hooks/useAddAudioToVideo.ts
@@ -0,0 +1,193 @@
+import { useState, useMemo, useEffect } from 'react';
+import { aiApiClient } from '../../../../../api/client';
+
+export type AudioModel = 'hunyuan-video-foley' | 'think-sound';
+
+export const useAddAudioToVideo = () => {
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoPreview, setVideoPreview] = useState(null);
+ const [model, setModel] = useState('hunyuan-video-foley');
+ const [prompt, setPrompt] = useState('');
+ const [seed, setSeed] = useState(null);
+ const [processing, setProcessing] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [error, setError] = useState(null);
+ const [result, setResult] = useState<{ video_url: string; cost: number; model_used: string } | null>(null);
+ const [estimatedDuration, setEstimatedDuration] = useState(10.0);
+ const [costEstimate, setCostEstimate] = useState(null);
+
+ // Update preview when file changes
+ useEffect(() => {
+ if (videoFile) {
+ const url = URL.createObjectURL(videoFile);
+ setVideoPreview(url);
+
+ // Rough estimate: 1MB ≈ 1 second at 1080p
+ const estimated = Math.max(5, videoFile.size / (1024 * 1024));
+ setEstimatedDuration(estimated);
+
+ return () => URL.revokeObjectURL(url);
+ } else {
+ setVideoPreview(null);
+ setEstimatedDuration(10.0);
+ }
+ }, [videoFile]);
+
+ // Fetch cost estimate when model or duration changes
+ useEffect(() => {
+ const fetchCostEstimate = async () => {
+ if (!videoFile || estimatedDuration < 5) {
+ setCostEstimate(null);
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('model', model);
+ formData.append('estimated_duration', estimatedDuration.toString());
+
+ const response = await aiApiClient.post('/api/video-studio/add-audio-to-video/estimate-cost', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ if (response.data.estimated_cost) {
+ setCostEstimate(response.data.estimated_cost);
+ }
+ } catch (err) {
+ console.error('Failed to fetch cost estimate:', err);
+ // Fallback to client-side calculation
+ if (model === 'think-sound') {
+ setCostEstimate(0.05); // Flat rate per video
+ } else {
+ const costPerSecond = 0.02;
+ setCostEstimate(Math.max(5.0, estimatedDuration) * costPerSecond);
+ }
+ }
+ };
+
+ fetchCostEstimate();
+ }, [videoFile, model, estimatedDuration]);
+
+ const canAddAudio = useMemo(() => {
+ return videoFile !== null;
+ }, [videoFile]);
+
+ const costHint = useMemo(() => {
+ if (!videoFile) return 'Upload a video to see cost estimate';
+
+ if (costEstimate !== null) {
+ return `Est. ~$${costEstimate.toFixed(2)} (${estimatedDuration.toFixed(0)}s)`;
+ }
+
+ // Fallback calculation
+ if (model === 'think-sound') {
+ return `Est. ~$0.05 (flat rate per video)`;
+ } else {
+ const costPerSecond = 0.02;
+ const estimatedCost = Math.max(5.0, estimatedDuration) * costPerSecond;
+ return `Est. ~$${estimatedCost.toFixed(2)} (${estimatedDuration.toFixed(0)}s)`;
+ }
+ }, [videoFile, estimatedDuration, costEstimate]);
+
+ const addAudio = async () => {
+ if (!videoFile) return;
+
+ setProcessing(true);
+ setError(null);
+ setResult(null);
+ setProgress(0);
+
+ try {
+ const formData = new FormData();
+ formData.append('video_file', videoFile);
+ formData.append('model', model);
+ if (prompt) {
+ formData.append('prompt', prompt);
+ }
+ if (seed !== null) {
+ formData.append('seed', seed.toString());
+ }
+
+ // Submit audio addition request
+ setProgress(10);
+ const response = await aiApiClient.post('/api/video-studio/add-audio-to-video', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const uploadProgress = Math.round((progressEvent.loaded * 30) / progressEvent.total);
+ setProgress(uploadProgress);
+ }
+ },
+ timeout: 600000, // 10 minutes timeout
+ });
+
+ setProgress(40);
+
+ // Simulate progress updates
+ let simulatedProgress = 40;
+ const progressInterval = setInterval(() => {
+ simulatedProgress = Math.min(90, simulatedProgress + 5);
+ setProgress(simulatedProgress);
+ }, 2000);
+
+ try {
+ if (response.data.success) {
+ clearInterval(progressInterval);
+ setProcessing(false);
+ setResult(response.data);
+ setProgress(100);
+ } else {
+ clearInterval(progressInterval);
+ throw new Error(response.data.error || 'Adding audio failed');
+ }
+ } catch (err) {
+ clearInterval(progressInterval);
+ throw err;
+ }
+ } catch (err: any) {
+ setProcessing(false);
+ setProgress(0);
+ setError(err.response?.data?.detail || err.message || 'Failed to add audio');
+ }
+ };
+
+ const reset = () => {
+ setProcessing(false);
+ setProgress(0);
+ setError(null);
+ setResult(null);
+ setVideoFile(null);
+ setPrompt('');
+ setSeed(null);
+ };
+
+ return {
+ // State
+ videoFile,
+ videoPreview,
+ model,
+ prompt,
+ seed,
+ processing,
+ progress,
+ error,
+ result,
+ estimatedDuration,
+ costEstimate,
+ // Setters
+ setVideoFile,
+ setModel,
+ setPrompt,
+ setSeed,
+ // Computed
+ canAddAudio,
+ costHint,
+ // Actions
+ addAudio,
+ reset,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/AddAudioToVideo/index.ts b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/index.ts
new file mode 100644
index 00000000..0a492f37
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AddAudioToVideo/index.ts
@@ -0,0 +1,2 @@
+export { AddAudioToVideo } from './AddAudioToVideo';
+export { default } from './AddAudioToVideo';
diff --git a/frontend/src/components/VideoStudio/modules/AvatarVideo.tsx b/frontend/src/components/VideoStudio/modules/AvatarVideo.tsx
new file mode 100644
index 00000000..163fdb6a
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AvatarVideo.tsx
@@ -0,0 +1,3 @@
+// Re-export from the AvatarVideo component
+export { AvatarVideo } from './AvatarVideo/AvatarVideo';
+export { default } from './AvatarVideo/AvatarVideo';
diff --git a/frontend/src/components/VideoStudio/modules/AvatarVideo/AvatarVideo.tsx b/frontend/src/components/VideoStudio/modules/AvatarVideo/AvatarVideo.tsx
new file mode 100644
index 00000000..adb3a082
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AvatarVideo/AvatarVideo.tsx
@@ -0,0 +1,249 @@
+import React, { useState } from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useAvatarVideo } from './hooks/useAvatarVideo';
+import { ImageUpload, AudioUpload, AvatarSettings } from './components';
+import { aiApiClient } from '../../../../api/client';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+
+export const AvatarVideo: React.FC = () => {
+ const {
+ imageFile,
+ imagePreview,
+ audioFile,
+ audioPreview,
+ resolution,
+ model,
+ prompt,
+ seed,
+ setImageFile,
+ setAudioFile,
+ setResolution,
+ setModel,
+ setPrompt,
+ setSeed,
+ canGenerate,
+ costHint,
+ } = useAvatarVideo();
+
+ const [generating, setGenerating] = useState(false);
+ const [taskId, setTaskId] = useState(null);
+ const [progress, setProgress] = useState(0);
+ const [statusMessage, setStatusMessage] = useState('');
+ const [error, setError] = useState(null);
+ const [result, setResult] = useState<{ video_url: string; cost: number } | null>(null);
+
+ const handleGenerate = async () => {
+ if (!imageFile || !audioFile) return;
+
+ setGenerating(true);
+ setError(null);
+ setResult(null);
+ setProgress(0);
+ setStatusMessage('Starting avatar generation...');
+
+ try {
+ // Create FormData
+ const formData = new FormData();
+ formData.append('image', imageFile);
+ formData.append('audio', audioFile);
+ formData.append('resolution', resolution);
+ formData.append('model', model);
+ if (prompt) {
+ formData.append('prompt', prompt);
+ }
+ if (seed !== null) {
+ formData.append('seed', seed.toString());
+ }
+
+ // Submit generation request
+ const response = await aiApiClient.post('/api/video-studio/avatar/create-async', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ const { task_id } = response.data;
+ setTaskId(task_id);
+ setStatusMessage('Avatar generation started. Polling for updates...');
+
+ // Poll for status
+ const pollInterval = setInterval(async () => {
+ try {
+ const statusResponse = await aiApiClient.get(`/api/video-studio/task/${task_id}/status`);
+ const status = statusResponse.data;
+
+ setProgress(status.progress || 0);
+ setStatusMessage(status.message || 'Processing...');
+
+ if (status.status === 'completed') {
+ clearInterval(pollInterval);
+ setGenerating(false);
+ setResult(status.result);
+ setStatusMessage('Avatar generation complete!');
+ } else if (status.status === 'failed') {
+ clearInterval(pollInterval);
+ setGenerating(false);
+ setError(status.error || 'Avatar generation failed');
+ setStatusMessage('Generation failed');
+ }
+ } catch (err: any) {
+ console.error('Polling error:', err);
+ // Continue polling on transient errors
+ }
+ }, 2000); // Poll every 2 seconds
+
+ // Cleanup on unmount
+ return () => clearInterval(pollInterval);
+ } catch (err: any) {
+ setGenerating(false);
+ setError(err.response?.data?.detail || err.message || 'Failed to start avatar generation');
+ setStatusMessage('Failed to start generation');
+ }
+ };
+
+ return (
+
+
+ {/* Left Panel: Uploads and Settings */}
+
+
+
+
+
+
+
+
+ {/* Cost and Generate */}
+
+
+
+
+ Estimated Cost
+
+
+ {costHint}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {generating && (
+
+
+
+
+ {statusMessage}
+
+
+ {progress > 0 && (
+
+
+ Progress: {progress.toFixed(0)}%
+
+
+ )}
+
+ )}
+
+ : }
+ onClick={handleGenerate}
+ disabled={!canGenerate || generating}
+ sx={{
+ py: 1.5,
+ borderRadius: 2,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ }}
+ >
+ {generating ? 'Generating...' : 'Create Avatar'}
+
+
+
+
+
+
+ {/* Right Panel: Preview/Result */}
+
+
+ {result ? (
+
+
+ Avatar Generated!
+
+
+
+ Cost: ${result.cost.toFixed(2)}
+
+
+ ) : (
+
+ {imagePreview && audioPreview
+ ? 'Upload your photo and audio, then click "Create Avatar" to generate your talking avatar.'
+ : 'Upload a photo and audio to create your talking avatar.'}
+
+ )}
+
+
+
+
+ );
+};
+
+export default AvatarVideo;
diff --git a/frontend/src/components/VideoStudio/modules/AvatarVideo/components/AudioUpload.tsx b/frontend/src/components/VideoStudio/modules/AvatarVideo/components/AudioUpload.tsx
new file mode 100644
index 00000000..47004f5d
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AvatarVideo/components/AudioUpload.tsx
@@ -0,0 +1,122 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import CloudUploadIcon from '@mui/icons-material/CloudUpload';
+import AudioFileIcon from '@mui/icons-material/AudioFile';
+
+interface AudioUploadProps {
+ audioPreview: string | null;
+ onAudioSelect: (file: File | null) => void;
+}
+
+export const AudioUpload: React.FC = ({
+ audioPreview,
+ onAudioSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate audio file
+ if (!file.type.startsWith('audio/')) {
+ alert('Please select an audio file');
+ return;
+ }
+ if (file.size > 50 * 1024 * 1024) {
+ alert('Audio file must be less than 50MB');
+ return;
+ }
+ onAudioSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onAudioSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Upload Audio
+
+
+ {audioPreview ? (
+
+
+
+
+
+ Audio file selected
+
+
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload audio
+
+
+ MP3, WAV up to 50MB (max 10 minutes)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/AvatarVideo/components/AvatarSettings.tsx b/frontend/src/components/VideoStudio/modules/AvatarVideo/components/AvatarSettings.tsx
new file mode 100644
index 00000000..90f81b7f
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AvatarVideo/components/AvatarSettings.tsx
@@ -0,0 +1,206 @@
+import React, { useState } from 'react';
+import { Box, Stack, Typography, FormControl, InputLabel, Select, MenuItem, TextField, Button, CircularProgress, Tooltip } from '@mui/material';
+import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
+import type { AvatarResolution, AvatarModel } from '../hooks/useAvatarVideo';
+import { optimizePrompt } from '../../../../../api/videoStudioApi';
+
+interface AvatarSettingsProps {
+ resolution: AvatarResolution;
+ model: AvatarModel;
+ prompt: string;
+ seed: number | null;
+ onResolutionChange: (value: AvatarResolution) => void;
+ onModelChange: (value: AvatarModel) => void;
+ onPromptChange: (value: string) => void;
+ onSeedChange: (value: number | null) => void;
+}
+
+export const AvatarSettings: React.FC = ({
+ resolution,
+ model,
+ prompt,
+ seed,
+ onResolutionChange,
+ onModelChange,
+ onPromptChange,
+ onSeedChange,
+}) => {
+ const [enhancing, setEnhancing] = useState(false);
+
+ const handleEnhancePrompt = async () => {
+ if (!prompt.trim() || enhancing) return;
+
+ setEnhancing(true);
+ try {
+ const result = await optimizePrompt({
+ text: prompt,
+ mode: 'video', // Use 'video' mode for avatar generation
+ style: 'default',
+ });
+
+ if (result.success && result.optimized_prompt) {
+ onPromptChange(result.optimized_prompt);
+ }
+ } catch (error) {
+ console.error('Failed to enhance prompt:', error);
+ } finally {
+ setEnhancing(false);
+ }
+ };
+
+ return (
+
+
+ AI Model
+
+
+
+
+ Video Quality
+
+
+
+
+
+
+ Expression Prompt (Optional)
+
+
+
+ AI Prompt Optimizer
+
+
+ Enhances your expression prompt for better avatar results by improving:
+
+
+ • Visual clarity & composition
+
+
+ • Expression details & style consistency
+
+
+ }
+ arrow
+ placement="top"
+ >
+ : }
+ onClick={handleEnhancePrompt}
+ disabled={!prompt.trim() || enhancing}
+ sx={{
+ textTransform: 'none',
+ fontSize: '0.75rem',
+ py: 0.5,
+ px: 1.5,
+ borderColor: '#3b82f6',
+ color: '#3b82f6',
+ '&:hover': {
+ borderColor: '#3b82f6',
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ },
+ '&:disabled': {
+ borderColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {enhancing ? 'Enhancing...' : 'Enhance Instructions'}
+
+
+
+ onPromptChange(e.target.value)}
+ helperText="Describe the expression or style you want for your avatar"
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 2,
+ backgroundColor: '#fff',
+ '& fieldset': { borderColor: '#e2e8f0' },
+ },
+ }}
+ />
+
+
+ {
+ const value = e.target.value;
+ onSeedChange(value ? parseInt(value, 10) : null);
+ }}
+ helperText="Use the same seed to generate similar results"
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 2,
+ backgroundColor: '#fff',
+ '& fieldset': { borderColor: '#e2e8f0' },
+ },
+ }}
+ />
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/AvatarVideo/components/ImageUpload.tsx b/frontend/src/components/VideoStudio/modules/AvatarVideo/components/ImageUpload.tsx
new file mode 100644
index 00000000..85b0d436
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AvatarVideo/components/ImageUpload.tsx
@@ -0,0 +1,126 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import CloudUploadIcon from '@mui/icons-material/CloudUpload';
+import ImageIcon from '@mui/icons-material/Image';
+
+interface ImageUploadProps {
+ imagePreview: string | null;
+ onImageSelect: (file: File | null) => void;
+}
+
+export const ImageUpload: React.FC = ({
+ imagePreview,
+ onImageSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate image file
+ if (!file.type.startsWith('image/')) {
+ alert('Please select an image file');
+ return;
+ }
+ if (file.size > 10 * 1024 * 1024) {
+ alert('Image file must be less than 10MB');
+ return;
+ }
+ onImageSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onImageSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Upload Photo
+
+
+ {imagePreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload a photo
+
+
+ PNG, JPG up to 10MB
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/AvatarVideo/components/index.ts b/frontend/src/components/VideoStudio/modules/AvatarVideo/components/index.ts
new file mode 100644
index 00000000..bb47f87c
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AvatarVideo/components/index.ts
@@ -0,0 +1,3 @@
+export { ImageUpload } from './ImageUpload';
+export { AudioUpload } from './AudioUpload';
+export { AvatarSettings } from './AvatarSettings';
diff --git a/frontend/src/components/VideoStudio/modules/AvatarVideo/hooks/useAvatarVideo.ts b/frontend/src/components/VideoStudio/modules/AvatarVideo/hooks/useAvatarVideo.ts
new file mode 100644
index 00000000..0a4a0cf7
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AvatarVideo/hooks/useAvatarVideo.ts
@@ -0,0 +1,92 @@
+import { useState, useMemo, useCallback } from 'react';
+
+export type AvatarResolution = '480p' | '720p';
+export type AvatarModel = 'infinitetalk' | 'hunyuan-avatar';
+
+export const useAvatarVideo = () => {
+ const [imageFile, setImageFile] = useState(null);
+ const [imagePreview, setImagePreview] = useState(null);
+ const [audioFile, setAudioFile] = useState(null);
+ const [audioPreview, setAudioPreview] = useState(null);
+ const [resolution, setResolution] = useState('720p');
+ const [model, setModel] = useState('infinitetalk');
+ const [prompt, setPrompt] = useState('');
+ const [maskImageFile, setMaskImageFile] = useState(null);
+ const [seed, setSeed] = useState(null);
+
+ // Cost estimation
+ const costHint = useMemo(() => {
+ const estimatedDuration = 10; // TODO: Get actual audio duration
+
+ if (model === 'hunyuan-avatar') {
+ // Hunyuan Avatar: $0.15/5s (480p) or $0.30/5s (720p)
+ const costPer5Seconds = resolution === '480p' ? 0.15 : 0.30;
+ const billable5SecondBlocks = Math.ceil(estimatedDuration / 5);
+ const estimate = (costPer5Seconds * billable5SecondBlocks).toFixed(2);
+ return `Est. ~$${estimate}`;
+ } else {
+ // InfiniteTalk: $0.03/s (480p) or $0.06/s (720p)
+ const costPerSecond = resolution === '480p' ? 0.03 : 0.06;
+ const estimate = (costPerSecond * estimatedDuration).toFixed(2);
+ return `Est. ~$${estimate}`;
+ }
+ }, [resolution, model]);
+
+ const canGenerate = useMemo(() => {
+ return imageFile !== null && audioFile !== null;
+ }, [imageFile, audioFile]);
+
+ const handleImageSelect = useCallback((file: File | null) => {
+ setImageFile(file);
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setImagePreview(e.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ } else {
+ setImagePreview(null);
+ }
+ }, []);
+
+ const handleAudioSelect = useCallback((file: File | null) => {
+ setAudioFile(file);
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setAudioPreview(e.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ } else {
+ setAudioPreview(null);
+ }
+ }, []);
+
+ const handleMaskImageSelect = useCallback((file: File | null) => {
+ setMaskImageFile(file);
+ }, []);
+
+ return {
+ // State
+ imageFile,
+ imagePreview,
+ audioFile,
+ audioPreview,
+ resolution,
+ model,
+ prompt,
+ maskImageFile,
+ seed,
+ // Setters
+ setImageFile: handleImageSelect,
+ setAudioFile: handleAudioSelect,
+ setResolution,
+ setModel,
+ setPrompt,
+ setMaskImageFile: handleMaskImageSelect,
+ setSeed,
+ // Computed
+ canGenerate,
+ costHint,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/AvatarVideo/index.ts b/frontend/src/components/VideoStudio/modules/AvatarVideo/index.ts
new file mode 100644
index 00000000..26e7b32f
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/AvatarVideo/index.ts
@@ -0,0 +1,2 @@
+export { AvatarVideo } from './AvatarVideo';
+export { default } from './AvatarVideo';
diff --git a/frontend/src/components/VideoStudio/modules/CarouselPlaceholder.tsx b/frontend/src/components/VideoStudio/modules/CarouselPlaceholder.tsx
new file mode 100644
index 00000000..fc77ace3
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CarouselPlaceholder.tsx
@@ -0,0 +1,108 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Box, Typography } from '@mui/material';
+import { motion, AnimatePresence } from 'framer-motion';
+
+interface CarouselPlaceholderProps {
+ examples: string[];
+ interval?: number;
+ onExampleChange?: (example: string, index: number) => void;
+ paused?: boolean;
+}
+
+export const CarouselPlaceholder: React.FC = ({
+ examples,
+ interval = 4000,
+ onExampleChange,
+ paused = false,
+}) => {
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const intervalRef = useRef(null);
+
+ useEffect(() => {
+ if (examples.length <= 1 || paused) {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+ return;
+ }
+
+ intervalRef.current = setInterval(() => {
+ setCurrentIndex(prev => {
+ const next = (prev + 1) % examples.length;
+ if (onExampleChange) {
+ onExampleChange(examples[next], next);
+ }
+ return next;
+ });
+ }, interval);
+
+ return () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
+ };
+ }, [examples.length, interval, onExampleChange, paused]);
+
+ if (examples.length === 0) return null;
+
+ return (
+
+
+
+
+ {examples[currentIndex]}
+
+
+
+ {examples.length > 1 && (
+
+ {examples.map((_, idx) => (
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/CreateVideo.tsx b/frontend/src/components/VideoStudio/modules/CreateVideo/CreateVideo.tsx
new file mode 100644
index 00000000..084757e4
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/CreateVideo.tsx
@@ -0,0 +1,140 @@
+import React from 'react';
+import { Grid } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useCreateVideo } from './hooks/useCreateVideo';
+import { GenerationSettingsPanel, VideoExamplesPanel } from './components';
+import { handleExampleClick, handleAssetClick } from './utils/exampleHandlers';
+import { createVideoExamples } from '../../dashboard/constants';
+import type { ContentAsset } from '../../../../hooks/useContentAssets';
+
+export const CreateVideo: React.FC = () => {
+ const {
+ mode,
+ setMode,
+ prompt,
+ setPrompt,
+ negativePrompt,
+ setNegativePrompt,
+ duration,
+ setDuration,
+ resolution,
+ setResolution,
+ aspect,
+ setAspect,
+ motion,
+ setMotion,
+ audioAttached,
+ setAudioAttached,
+ selectedModel,
+ setSelectedModel,
+ selectedExample,
+ setSelectedExample,
+ selectedAssetId,
+ setSelectedAssetId,
+ promptPlaceholderIndex,
+ setPromptPlaceholderIndex,
+ negativePlaceholderIndex,
+ setNegativePlaceholderIndex,
+ promptFocused,
+ setPromptFocused,
+ negativeFocused,
+ setNegativeFocused,
+ canGenerate,
+ costHint,
+ libraryVideos,
+ loadingLibraryVideos,
+ handleFileSelect,
+ } = useCreateVideo();
+
+ const handleExampleClickWrapper = (index: number) => {
+ const example = createVideoExamples[index];
+ handleExampleClick(
+ index,
+ example,
+ setPrompt,
+ setAspect,
+ setSelectedExample,
+ setSelectedAssetId
+ );
+ };
+
+ const handleAssetClickWrapper = (asset: ContentAsset) => {
+ handleAssetClick(
+ asset,
+ setPrompt,
+ setAspect,
+ setResolution,
+ setSelectedAssetId,
+ setSelectedExample
+ );
+ };
+
+ const handleGenerate = () => {
+ // Placeholder: hook preflight + job creation later
+ alert('This is a UI preview. Backend generation will be wired in the next step.');
+ };
+
+ return (
+
+
+ {/* Left Panel - Generation Controls */}
+
+ setPromptFocused(true)}
+ onPromptBlur={() => setPromptFocused(false)}
+ onNegativeFocus={() => setNegativeFocused(true)}
+ onNegativeBlur={() => setNegativeFocused(false)}
+ onPromptPlaceholderChange={setPromptPlaceholderIndex}
+ onNegativePlaceholderChange={setNegativePlaceholderIndex}
+ onGenerate={handleGenerate}
+ />
+
+
+ {/* Right Panel - Video Preview & Examples */}
+
+
+
+
+
+ );
+};
+
+export default CreateVideo;
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/components/AssetLibraryVideoCard.tsx b/frontend/src/components/VideoStudio/modules/CreateVideo/components/AssetLibraryVideoCard.tsx
new file mode 100644
index 00000000..84909fc3
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/components/AssetLibraryVideoCard.tsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import { Box, Card, CardContent, Stack, Typography, Chip } from '@mui/material';
+import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
+import { motion as framerMotion } from 'framer-motion';
+import { OptimizedVideo } from '../../../../ImageStudio/dashboard/utils/OptimizedVideo';
+import type { ContentAsset } from '../../../../../hooks/useContentAssets';
+
+interface AssetLibraryVideoCardProps {
+ asset: ContentAsset;
+ isSelected: boolean;
+ onClick: () => void;
+}
+
+export const AssetLibraryVideoCard: React.FC = ({
+ asset,
+ isSelected,
+ onClick,
+}) => {
+ return (
+
+
+
+
+ {isSelected && (
+
+
+
+ )}
+
+
+
+
+
+ {asset.title || asset.filename}
+
+ {asset.source_module && (
+
+ )}
+
+ {asset.description && (
+
+ {asset.description.length > 60
+ ? `${asset.description.substring(0, 60)}...`
+ : asset.description}
+
+ )}
+ {asset.prompt && (
+
+ "{asset.prompt.length > 50 ? `${asset.prompt.substring(0, 50)}...` : asset.prompt}"
+
+ )}
+
+ {asset.cost > 0 && (
+
+ )}
+ {asset.asset_metadata?.resolution && (
+
+ )}
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/components/ExampleVideoCard.tsx b/frontend/src/components/VideoStudio/modules/CreateVideo/components/ExampleVideoCard.tsx
new file mode 100644
index 00000000..49e1ec97
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/components/ExampleVideoCard.tsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import { Box, Card, CardContent, Stack, Typography, Chip } from '@mui/material';
+import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
+import { motion as framerMotion } from 'framer-motion';
+import { OptimizedVideo } from '../../../../ImageStudio/dashboard/utils/OptimizedVideo';
+import type { ExampleVideo } from '../types';
+
+interface ExampleVideoCardProps {
+ example: ExampleVideo;
+ index: number;
+ isSelected: boolean;
+ onClick: () => void;
+}
+
+export const ExampleVideoCard: React.FC = ({
+ example,
+ index,
+ isSelected,
+ onClick,
+}) => {
+ return (
+
+
+
+
+ {isSelected && (
+
+
+
+ )}
+
+
+
+
+
+ {example.label}
+
+
+
+
+ {example.description}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/components/GenerationSettingsPanel.tsx b/frontend/src/components/VideoStudio/modules/CreateVideo/components/GenerationSettingsPanel.tsx
new file mode 100644
index 00000000..8bbd1381
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/components/GenerationSettingsPanel.tsx
@@ -0,0 +1,255 @@
+import React from 'react';
+import {
+ Box,
+ Paper,
+ Stack,
+ Typography,
+ ToggleButtonGroup,
+ ToggleButton,
+ Button,
+ Alert,
+} from '@mui/material';
+import UploadFileIcon from '@mui/icons-material/UploadFile';
+import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
+import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import type { Mode } from '../types';
+import { PromptInput } from './PromptInput';
+import { VideoSettings } from './VideoSettings';
+import { ModelSelector } from './ModelSelector';
+import type { Resolution, AspectPreset, MotionPreset, Duration } from '../types';
+
+interface GenerationSettingsPanelProps {
+ mode: Mode;
+ prompt: string;
+ negativePrompt: string;
+ duration: Duration;
+ resolution: Resolution;
+ aspect: AspectPreset;
+ motion: MotionPreset;
+ audioAttached: boolean;
+ costHint: string;
+ canGenerate: boolean;
+ promptFocused: boolean;
+ negativeFocused: boolean;
+ promptPlaceholderIndex: number;
+ negativePlaceholderIndex: number;
+ selectedModel: string;
+ onModeChange: (mode: Mode) => void;
+ onPromptChange: (value: string) => void;
+ onNegativePromptChange: (value: string) => void;
+ onDurationChange: (value: Duration) => void;
+ onResolutionChange: (value: Resolution) => void;
+ onAspectChange: (value: AspectPreset) => void;
+ onMotionChange: (value: MotionPreset) => void;
+ onModelChange: (modelId: string) => void;
+ onFileSelect: (e: React.ChangeEvent) => void;
+ onPromptFocus: () => void;
+ onPromptBlur: () => void;
+ onNegativeFocus: () => void;
+ onNegativeBlur: () => void;
+ onPromptPlaceholderChange: (index: number) => void;
+ onNegativePlaceholderChange: (index: number) => void;
+ onGenerate: () => void;
+}
+
+export const GenerationSettingsPanel: React.FC = ({
+ mode,
+ prompt,
+ negativePrompt,
+ duration,
+ resolution,
+ aspect,
+ motion,
+ costHint,
+ canGenerate,
+ promptFocused,
+ negativeFocused,
+ promptPlaceholderIndex,
+ negativePlaceholderIndex,
+ selectedModel,
+ onModeChange,
+ onPromptChange,
+ onNegativePromptChange,
+ onDurationChange,
+ onResolutionChange,
+ onAspectChange,
+ onMotionChange,
+ onModelChange,
+ onFileSelect,
+ onPromptFocus,
+ onPromptBlur,
+ onNegativeFocus,
+ onNegativeBlur,
+ onPromptPlaceholderChange,
+ onNegativePlaceholderChange,
+ onGenerate,
+}) => {
+ return (
+
+
+
+ Generation Settings
+
+
+
+ {/* Mode Toggle */}
+ val && onModeChange(val)}
+ size="small"
+ fullWidth
+ sx={{
+ background: 'rgba(255,255,255,0.8)',
+ borderRadius: 2,
+ '& .MuiToggleButton-root': {
+ color: '#475569',
+ '&.Mui-selected': {
+ background: 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)',
+ color: '#fff',
+ fontWeight: 700,
+ },
+ },
+ }}
+ >
+ Text to Video
+ Image to Video
+
+
+ {/* AI Model Selector (only for text-to-video) */}
+ {mode === 't2v' && (
+
+ )}
+
+ {/* Prompt Input */}
+
+
+ {/* Image Upload for i2v */}
+ {mode === 'i2v' && (
+ }
+ fullWidth
+ sx={{
+ borderRadius: 2,
+ borderColor: '#d2d9ee',
+ color: '#0f172a',
+ backgroundColor: 'rgba(255, 255, 255, 0.85)',
+ '&:hover': {
+ borderColor: '#7c3aed',
+ background: 'rgba(124, 58, 237, 0.05)',
+ },
+ }}
+ >
+ Upload Image
+
+
+ )}
+
+ {/* Video Settings */}
+
+
+ {/* Cost Estimate */}
+ }
+ sx={{
+ borderRadius: 2,
+ background: 'rgba(99, 102, 241, 0.08)',
+ color: '#0f172a',
+ '& .MuiAlert-icon': { color: '#6366f1' },
+ }}
+ >
+
+ Estimated Cost: {costHint}
+
+
+ Final cost is confirmed before generation. Lower cost = shorter duration + lower quality.
+
+
+
+ {/* Generate Button */}
+ }
+ disabled={!canGenerate}
+ fullWidth
+ onClick={onGenerate}
+ sx={{
+ py: 2,
+ borderRadius: 2,
+ background: canGenerate
+ ? 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)'
+ : '#e2e8f0',
+ color: canGenerate ? '#fff' : '#94a3b8',
+ fontWeight: 700,
+ fontSize: 16,
+ textTransform: 'none',
+ boxShadow: canGenerate ? '0 8px 24px rgba(102, 126, 234, 0.4)' : 'none',
+ '&:hover': {
+ background: canGenerate
+ ? 'linear-gradient(90deg, #5568d3 0%, #65408b 100%)'
+ : '#e2e8f0',
+ boxShadow: canGenerate ? '0 12px 32px rgba(102, 126, 234, 0.5)' : 'none',
+ },
+ }}
+ >
+ Create Video
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/components/ModelSelector.tsx b/frontend/src/components/VideoStudio/modules/CreateVideo/components/ModelSelector.tsx
new file mode 100644
index 00000000..7d5bde06
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/components/ModelSelector.tsx
@@ -0,0 +1,292 @@
+import React, { useState } from 'react';
+import {
+ Box,
+ Paper,
+ Stack,
+ Typography,
+ FormControl,
+ Select,
+ MenuItem,
+ Chip,
+ Tooltip,
+ IconButton,
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Divider,
+} from '@mui/material';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import InfoIcon from '@mui/icons-material/Info';
+import { VIDEO_MODELS, type VideoModelInfo } from '../models/videoModels';
+
+interface ModelSelectorProps {
+ selectedModel: string;
+ onModelChange: (modelId: string) => void;
+ duration: number;
+ resolution: string;
+}
+
+export const ModelSelector: React.FC = ({
+ selectedModel,
+ onModelChange,
+ duration,
+ resolution,
+}) => {
+ const [expandedModel, setExpandedModel] = useState(false);
+ const selectedModelInfo = VIDEO_MODELS.find(m => m.id === selectedModel);
+
+ const handleAccordionChange = (modelId: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
+ setExpandedModel(isExpanded ? modelId : false);
+ };
+
+ const calculateCost = (model: VideoModelInfo): string => {
+ const costPerSecond = model.costPerSecond[resolution] || model.costPerSecond[Object.keys(model.costPerSecond)[0]];
+ const totalCost = costPerSecond * duration;
+ return `$${totalCost.toFixed(2)}`;
+ };
+
+ const isModelCompatible = (model: VideoModelInfo): { compatible: boolean; reason?: string } => {
+ if (!model.durations.includes(duration)) {
+ return { compatible: false, reason: `Duration ${duration}s not supported. Available: ${model.durations.join(', ')}s` };
+ }
+ if (!model.resolutions.includes(resolution)) {
+ return { compatible: false, reason: `Resolution ${resolution} not supported. Available: ${model.resolutions.join(', ')}` };
+ }
+ return { compatible: true };
+ };
+
+ return (
+
+
+
+ AI Model
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Selected Model Details */}
+ {selectedModelInfo && (
+
+
+
+
+ {selectedModelInfo.name}
+
+
+ {selectedModelInfo.description}
+
+
+
+
+
+ {/* Best For */}
+
+
+ Best For
+
+
+ {selectedModelInfo.bestFor.slice(0, 3).map((useCase) => (
+
+ ))}
+
+
+
+ {/* Cost & Duration Info */}
+
+
+
+
+ Estimated Cost
+
+
+ {calculateCost(selectedModelInfo)}
+
+
+
+
+ Audio Support
+
+
+ {selectedModelInfo.audioSupport ? 'Yes' : 'No'}
+
+
+
+
+
+ {/* Expandable Details */}
+
+ }
+ sx={{ minHeight: 40 }}
+ >
+
+ View Full Details & Tips
+
+
+
+
+ {/* Strengths */}
+
+
+ Strengths
+
+
+ {selectedModelInfo.strengths.map((strength, idx) => (
+
+
+
+
+
+
+ ))}
+
+
+
+ {/* Tips */}
+
+
+ Pro Tips
+
+
+ {selectedModelInfo.tips.map((tip, idx) => (
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ )}
+
+ {/* Model Comparison Link */}
+
+
+
+ Compare all models →
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/components/PromptInput.tsx b/frontend/src/components/VideoStudio/modules/CreateVideo/components/PromptInput.tsx
new file mode 100644
index 00000000..43e4756e
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/components/PromptInput.tsx
@@ -0,0 +1,241 @@
+import React, { useState } from 'react';
+import { Box, TextField, Typography, Stack, Button, CircularProgress, Tooltip } from '@mui/material';
+import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
+import { CarouselPlaceholder } from '../../CarouselPlaceholder';
+import { examplePrompts, exampleNegativePrompts, inputStyles, colors } from '../constants';
+import { optimizePrompt } from '../../../../../api/videoStudioApi';
+
+interface PromptInputProps {
+ prompt: string;
+ negativePrompt: string;
+ promptFocused: boolean;
+ negativeFocused: boolean;
+ promptPlaceholderIndex: number;
+ negativePlaceholderIndex: number;
+ onPromptChange: (value: string) => void;
+ onNegativePromptChange: (value: string) => void;
+ onPromptFocus: () => void;
+ onPromptBlur: () => void;
+ onNegativeFocus: () => void;
+ onNegativeBlur: () => void;
+ onPromptPlaceholderChange: (index: number) => void;
+ onNegativePlaceholderChange: (index: number) => void;
+}
+
+export const PromptInput: React.FC = ({
+ prompt,
+ negativePrompt,
+ promptFocused,
+ negativeFocused,
+ promptPlaceholderIndex,
+ negativePlaceholderIndex,
+ onPromptChange,
+ onNegativePromptChange,
+ onPromptFocus,
+ onPromptBlur,
+ onNegativeFocus,
+ onNegativeBlur,
+ onPromptPlaceholderChange,
+ onNegativePlaceholderChange,
+}) => {
+ const [enhancing, setEnhancing] = useState(false);
+
+ const handleEnhancePrompt = async () => {
+ if (!prompt.trim() || enhancing) return;
+
+ setEnhancing(true);
+ try {
+ const result = await optimizePrompt({
+ text: prompt,
+ mode: 'video', // Always use 'video' mode for Video Studio
+ style: 'default',
+ });
+
+ if (result.success && result.optimized_prompt) {
+ onPromptChange(result.optimized_prompt);
+ }
+ } catch (error) {
+ console.error('Failed to enhance prompt:', error);
+ // Optionally show error toast/notification
+ } finally {
+ setEnhancing(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Describe Your Video
+
+
+
+ AI Prompt Optimizer
+
+
+ Enhances your prompt for better video generation by improving:
+
+
+ • Visual clarity & composition
+
+
+ • Cinematic framing & lighting
+
+
+ • Camera movement & style consistency
+
+
+ }
+ arrow
+ placement="top"
+ >
+ : }
+ onClick={handleEnhancePrompt}
+ disabled={!prompt.trim() || enhancing}
+ sx={{
+ textTransform: 'none',
+ fontSize: '0.75rem',
+ py: 0.5,
+ px: 1.5,
+ borderColor: colors.primary,
+ color: colors.primary,
+ '&:hover': {
+ borderColor: colors.primary,
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ },
+ '&:disabled': {
+ borderColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {enhancing ? 'Enhancing...' : 'Enhance Instructions'}
+
+
+
+
+ onPromptChange(e.target.value)}
+ onFocus={onPromptFocus}
+ onBlur={onPromptBlur}
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ ...inputStyles.outlinedInputBase,
+ minHeight: 140,
+ },
+ '& .MuiInputBase-input': {
+ color: '#0f172a',
+ '&::placeholder': {
+ color: '#64748b',
+ opacity: 1,
+ },
+ },
+ }}
+ />
+ {!prompt && (
+
+ onPromptPlaceholderChange(idx)}
+ />
+
+ )}
+
+
+
+
+
+ What to Avoid (Optional)
+
+
+ onNegativePromptChange(e.target.value)}
+ onFocus={onNegativeFocus}
+ onBlur={onNegativeBlur}
+ fullWidth
+ sx={{
+ '& .MuiOutlinedInput-root': inputStyles.outlinedInputBase,
+ '& .MuiInputBase-input': {
+ color: '#0f172a',
+ '&::placeholder': {
+ color: '#64748b',
+ opacity: 1,
+ },
+ },
+ }}
+ />
+ {!negativePrompt && (
+
+ onNegativePlaceholderChange(idx)}
+ />
+
+ )}
+
+
+ Use this to specify what you don't want in your video (e.g., "blurry, low quality, distorted faces")
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/components/VideoExamplesPanel.tsx b/frontend/src/components/VideoStudio/modules/CreateVideo/components/VideoExamplesPanel.tsx
new file mode 100644
index 00000000..3bd5f960
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/components/VideoExamplesPanel.tsx
@@ -0,0 +1,197 @@
+import React from 'react';
+import {
+ Box,
+ Paper,
+ Stack,
+ Typography,
+ Divider,
+ Grid,
+ Chip,
+} from '@mui/material';
+import MovieCreationIcon from '@mui/icons-material/MovieCreation';
+import type { ExampleVideo } from '../types';
+import type { ContentAsset } from '../../../../../hooks/useContentAssets';
+import { ExampleVideoCard } from './ExampleVideoCard';
+import { AssetLibraryVideoCard } from './AssetLibraryVideoCard';
+
+interface VideoExamplesPanelProps {
+ examples: ExampleVideo[];
+ libraryVideos: ContentAsset[];
+ loadingLibraryVideos: boolean;
+ selectedExample: number | null;
+ selectedAssetId: number | null;
+ prompt: string;
+ onExampleClick: (index: number) => void;
+ onAssetClick: (asset: ContentAsset) => void;
+}
+
+export const VideoExamplesPanel: React.FC = ({
+ examples,
+ libraryVideos,
+ loadingLibraryVideos,
+ selectedExample,
+ selectedAssetId,
+ prompt,
+ onExampleClick,
+ onAssetClick,
+}) => {
+ return (
+
+
+
+ Video Examples & Preview
+
+
+ {/* Example Videos */}
+
+
+ Example Videos
+
+
+ {examples.map((example, index) => (
+
+ onExampleClick(index)}
+ />
+
+ ))}
+
+
+
+ {/* Asset Library Videos */}
+ {libraryVideos.length > 0 && (
+ <>
+
+
+
+
+ Your Videos from Asset Library
+
+
+
+ {loadingLibraryVideos ? (
+
+
+ Loading your videos...
+
+
+ ) : (
+
+ {libraryVideos.map((asset) => (
+
+ onAssetClick(asset)}
+ />
+
+ ))}
+
+ )}
+
+ >
+ )}
+
+
+
+ {/* Empty State / Preview Area */}
+ {!prompt && (
+
+
+
+
+
+ No Video Yet
+
+
+ Enter a prompt and click "Create Video" to generate your video, or click an example above to see what's possible
+
+
+ {['Instagram Reel', 'TikTok Video', 'YouTube Short', 'LinkedIn Post'].map((tag) => (
+
+ ))}
+
+
+ )}
+
+ {/* Generated Video Preview (when available) */}
+ {prompt && (
+
+
+ Your video will appear here
+
+
+ Click "Create Video" to generate your video based on your prompt and settings
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/components/VideoSettings.tsx b/frontend/src/components/VideoStudio/modules/CreateVideo/components/VideoSettings.tsx
new file mode 100644
index 00000000..2e71171c
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/components/VideoSettings.tsx
@@ -0,0 +1,166 @@
+import React from 'react';
+import { Box, Stack, Typography, FormControl, InputLabel, Select, MenuItem, Slider } from '@mui/material';
+import type { Resolution, AspectPreset, MotionPreset, Duration } from '../types';
+import { motionPresets, aspectPresets, inputStyles } from '../constants';
+
+interface VideoSettingsProps {
+ resolution: Resolution;
+ aspect: AspectPreset;
+ motion: MotionPreset;
+ duration: Duration;
+ onResolutionChange: (value: Resolution) => void;
+ onAspectChange: (value: AspectPreset) => void;
+ onMotionChange: (value: MotionPreset) => void;
+ onDurationChange: (value: Duration) => void;
+}
+
+export const VideoSettings: React.FC = ({
+ resolution,
+ aspect,
+ motion,
+ duration,
+ onResolutionChange,
+ onAspectChange,
+ onMotionChange,
+ onDurationChange,
+}) => {
+ return (
+ <>
+ {/* Resolution, Aspect, Motion */}
+
+
+ Video Quality
+
+
+
+
+ Video Format
+
+
+
+
+
+ Movement Style
+
+
+
+ {/* Duration Slider */}
+
+
+ Duration: {duration} seconds
+
+ onDurationChange(val as Duration)}
+ sx={{
+ color: '#667eea',
+ '& .MuiSlider-markLabel': { color: '#475569' },
+ }}
+ />
+
+ Shorter videos cost less. Perfect for testing ideas before investing in longer content.
+
+
+ >
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/components/index.ts b/frontend/src/components/VideoStudio/modules/CreateVideo/components/index.ts
new file mode 100644
index 00000000..7f7e211a
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/components/index.ts
@@ -0,0 +1,7 @@
+export { GenerationSettingsPanel } from './GenerationSettingsPanel';
+export { VideoExamplesPanel } from './VideoExamplesPanel';
+export { PromptInput } from './PromptInput';
+export { VideoSettings } from './VideoSettings';
+export { ExampleVideoCard } from './ExampleVideoCard';
+export { AssetLibraryVideoCard } from './AssetLibraryVideoCard';
+export { ModelSelector } from './ModelSelector';
\ No newline at end of file
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/constants.ts b/frontend/src/components/VideoStudio/modules/CreateVideo/constants.ts
new file mode 100644
index 00000000..8e003979
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/constants.ts
@@ -0,0 +1,43 @@
+import type { MotionPreset, AspectPreset } from './types';
+
+export const motionPresets: readonly MotionPreset[] = ['Subtle', 'Medium', 'Dynamic'] as const;
+export const aspectPresets: readonly AspectPreset[] = ['9:16', '1:1', '16:9'] as const;
+
+// Example prompts for content creators
+export const examplePrompts = [
+ 'A modern coffee shop interior with baristas crafting latte art, warm golden hour lighting streaming through large windows, customers chatting at wooden tables, cozy atmosphere, perfect for Instagram Reels',
+ 'Professional workspace with laptop, notebook, and coffee cup on a minimalist desk, soft natural lighting, clean modern office environment, ideal for LinkedIn posts',
+ 'Dynamic product showcase with rotating view, vibrant colors, smooth camera movement, energetic music vibe, perfect for YouTube Shorts and product demos',
+];
+
+export const exampleNegativePrompts = [
+ 'blurry, low quality, distorted faces, text overlays',
+ 'grainy footage, poor lighting, shaky camera, watermark',
+ 'unprofessional, cluttered background, bad composition',
+];
+
+// Input styles
+export const inputStyles = {
+ outlinedInputBase: {
+ borderRadius: 2,
+ backgroundColor: '#fff',
+ '& fieldset': { borderColor: '#e2e8f0' },
+ '&:hover fieldset': { borderColor: '#cbd5f5' },
+ '&.Mui-focused fieldset': {
+ borderColor: '#7c3aed',
+ boxShadow: '0 0 0 3px rgba(124, 58, 237, 0.15)',
+ },
+ },
+ inputLabel: {
+ color: '#475569',
+ fontWeight: 600,
+ },
+};
+
+// Color constants
+export const colors = {
+ primary: '#0f172a',
+ muted: '#475569',
+ accent: '#667eea',
+ accentSecondary: '#764ba2',
+};
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/hooks/useCreateVideo.ts b/frontend/src/components/VideoStudio/modules/CreateVideo/hooks/useCreateVideo.ts
new file mode 100644
index 00000000..7a1327b2
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/hooks/useCreateVideo.ts
@@ -0,0 +1,91 @@
+import { useState, useMemo, useCallback } from 'react';
+import { useContentAssets, type ContentAsset } from '../../../../../hooks/useContentAssets';
+import { getModelInfo } from '../models/videoModels';
+import type { Mode, Duration, Resolution, AspectPreset, MotionPreset } from '../types';
+
+export const useCreateVideo = () => {
+ const [mode, setMode] = useState('t2v');
+ const [prompt, setPrompt] = useState('');
+ const [negativePrompt, setNegativePrompt] = useState('');
+ const [duration, setDuration] = useState(8);
+ const [resolution, setResolution] = useState('720p');
+ const [aspect, setAspect] = useState('9:16');
+ const [motion, setMotion] = useState('Medium');
+ const [audioAttached, setAudioAttached] = useState(false);
+ const [selectedModel, setSelectedModel] = useState('hunyuan-video-1.5'); // Default model
+ const [selectedExample, setSelectedExample] = useState(null);
+ const [selectedAssetId, setSelectedAssetId] = useState(null);
+ const [promptPlaceholderIndex, setPromptPlaceholderIndex] = useState(0);
+ const [negativePlaceholderIndex, setNegativePlaceholderIndex] = useState(0);
+ const [promptFocused, setPromptFocused] = useState(false);
+ const [negativeFocused, setNegativeFocused] = useState(false);
+
+ // Fetch videos from asset library
+ const { assets: libraryVideos, loading: loadingLibraryVideos } = useContentAssets({
+ asset_type: 'video',
+ limit: 6,
+ });
+
+ const canGenerate = useMemo(() => prompt.trim().length > 5, [prompt]);
+
+ const costHint = useMemo(() => {
+ // Get model-specific pricing
+ const modelInfo = getModelInfo(selectedModel);
+ if (modelInfo) {
+ const costPerSecond = modelInfo.costPerSecond[resolution] || modelInfo.costPerSecond[Object.keys(modelInfo.costPerSecond)[0]];
+ const estimate = (costPerSecond * duration).toFixed(2);
+ return `Est. ~$${estimate}`;
+ }
+ // Fallback to default pricing
+ const base = resolution === '480p' ? 0.02 : resolution === '720p' ? 0.04 : 0.06;
+ const estimate = (base * duration).toFixed(2);
+ return `Est. ~$${estimate}`;
+ }, [duration, resolution, selectedModel]);
+
+ const handleFileSelect = useCallback((e: React.ChangeEvent) => {
+ if (mode === 'i2v' && e.target.files?.length) {
+ // Placeholder: in later phases, we'll upload/preview
+ }
+ }, [mode]);
+
+ return {
+ // State
+ mode,
+ setMode,
+ prompt,
+ setPrompt,
+ negativePrompt,
+ setNegativePrompt,
+ duration,
+ setDuration,
+ resolution,
+ setResolution,
+ aspect,
+ setAspect,
+ motion,
+ setMotion,
+ audioAttached,
+ setAudioAttached,
+ selectedModel,
+ setSelectedModel,
+ selectedExample,
+ setSelectedExample,
+ selectedAssetId,
+ setSelectedAssetId,
+ promptPlaceholderIndex,
+ setPromptPlaceholderIndex,
+ negativePlaceholderIndex,
+ setNegativePlaceholderIndex,
+ promptFocused,
+ setPromptFocused,
+ negativeFocused,
+ setNegativeFocused,
+ // Computed
+ canGenerate,
+ costHint,
+ libraryVideos,
+ loadingLibraryVideos,
+ // Handlers
+ handleFileSelect,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/index.ts b/frontend/src/components/VideoStudio/modules/CreateVideo/index.ts
new file mode 100644
index 00000000..c36100f9
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/index.ts
@@ -0,0 +1,2 @@
+export { CreateVideo } from './CreateVideo';
+export { default } from './CreateVideo';
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/models/videoModels.ts b/frontend/src/components/VideoStudio/modules/CreateVideo/models/videoModels.ts
new file mode 100644
index 00000000..2957fa19
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/models/videoModels.ts
@@ -0,0 +1,207 @@
+/**
+ * Video Model Information for Content Creators
+ *
+ * Non-technical, creator-focused descriptions to help users choose the right AI model
+ * for their video generation needs.
+ */
+
+export interface VideoModelInfo {
+ id: string;
+ name: string;
+ tagline: string;
+ description: string;
+ bestFor: string[];
+ strengths: string[];
+ limitations: string[];
+ durations: number[];
+ resolutions: string[];
+ aspectRatios: string[];
+ audioSupport: boolean;
+ costPerSecond: {
+ [resolution: string]: number;
+ };
+ exampleUseCases: string[];
+ tips: string[];
+ icon?: string;
+}
+
+export const VIDEO_MODELS: VideoModelInfo[] = [
+ {
+ id: 'hunyuan-video-1.5',
+ name: 'HunyuanVideo 1.5',
+ tagline: 'Lightweight & Fast - Perfect for Quick Content',
+ description: 'A lightweight model that generates high-quality videos quickly. Great for social media content, quick iterations, and when you need fast results without breaking the bank.',
+ bestFor: [
+ 'Instagram Reels & Stories',
+ 'TikTok videos',
+ 'Quick social media content',
+ 'Testing ideas and concepts',
+ 'Budget-conscious creators'
+ ],
+ strengths: [
+ 'Fast generation time',
+ 'Affordable pricing',
+ 'Good motion quality',
+ 'Works well for short clips',
+ 'Great for testing prompts'
+ ],
+ limitations: [
+ 'Limited to 5-10 second videos',
+ 'Only 480p or 720p resolution',
+ 'No audio generation',
+ 'Best for shorter content'
+ ],
+ durations: [5, 8, 10],
+ resolutions: ['480p', '720p'],
+ aspectRatios: ['16:9', '9:16'],
+ audioSupport: false,
+ costPerSecond: {
+ '480p': 0.02,
+ '720p': 0.04,
+ },
+ exampleUseCases: [
+ 'Quick product showcases for social media',
+ 'Story highlights and behind-the-scenes',
+ 'Fast-paced social media content',
+ 'Testing video concepts before production'
+ ],
+ tips: [
+ 'Use for 5-8 second clips for best results',
+ 'Describe motion and camera movement clearly',
+ 'Mention style and mood in your prompt',
+ 'Perfect for Instagram and TikTok content'
+ ],
+ },
+ {
+ id: 'lightricks/ltx-2-pro',
+ name: 'LTX-2 Pro',
+ tagline: 'Production Quality with Synchronized Audio',
+ description: 'Professional-grade video generation with perfectly synchronized audio. Designed for real production workflows where quality and audio-video sync matter. Creates cinematic scenes with matching sound.',
+ bestFor: [
+ 'YouTube videos',
+ 'Professional marketing content',
+ 'Music videos',
+ 'Film previsualization',
+ 'Advertising campaigns',
+ 'Production workflows'
+ ],
+ strengths: [
+ 'Synchronized audio generation',
+ 'Cinematic quality',
+ 'Perfect audio-video sync',
+ 'Production-ready output',
+ '1080p native resolution',
+ 'Great for longer content (6-10s)'
+ ],
+ limitations: [
+ 'Fixed at 1080p (no lower resolutions)',
+ 'Higher cost per second',
+ 'Longer generation time',
+ 'Only 6-10 second durations'
+ ],
+ durations: [6, 8, 10],
+ resolutions: ['1080p'],
+ aspectRatios: ['16:9', '9:16'],
+ audioSupport: true,
+ costPerSecond: {
+ '1080p': 0.06,
+ },
+ exampleUseCases: [
+ 'YouTube video intros and outros',
+ 'Product launch videos with music',
+ 'Music video sequences',
+ 'Professional marketing clips',
+ 'Film storyboard visualization'
+ ],
+ tips: [
+ 'Describe camera movements and scene composition',
+ 'Mention emotional tone and atmosphere',
+ 'Audio is automatically generated to match motion',
+ 'Best for 6-8 second clips for optimal quality',
+ 'Perfect for professional content creation'
+ ],
+ },
+ {
+ id: 'google/veo3.1',
+ name: 'Google Veo 3.1',
+ tagline: 'High-Quality with Flexible Options',
+ description: 'Google\'s advanced video generation model that creates high-quality videos with synchronized audio. Offers flexible resolution and aspect ratio options, perfect for various content platforms.',
+ bestFor: [
+ 'YouTube content',
+ 'Professional presentations',
+ 'Multi-platform content',
+ 'High-quality social media',
+ 'Content requiring flexibility'
+ ],
+ strengths: [
+ '720p and 1080p options',
+ 'Synchronized audio generation',
+ 'Negative prompt support',
+ 'Seed control for consistency',
+ 'Flexible aspect ratios',
+ 'High visual quality'
+ ],
+ limitations: [
+ 'Shorter duration options (4-8s)',
+ 'Higher cost for 1080p',
+ 'No 480p option'
+ ],
+ durations: [4, 6, 8],
+ resolutions: ['720p', '1080p'],
+ aspectRatios: ['16:9', '9:16'],
+ audioSupport: true,
+ costPerSecond: {
+ '720p': 0.08,
+ '1080p': 0.12,
+ },
+ exampleUseCases: [
+ 'YouTube Shorts and regular videos',
+ 'Professional social media content',
+ 'Multi-platform content creation',
+ 'High-quality product showcases',
+ 'Content requiring specific aspect ratios'
+ ],
+ tips: [
+ 'Use negative prompts to exclude unwanted elements',
+ 'Use seed values to create consistent variations',
+ '720p is great for social media, 1080p for YouTube',
+ 'Describe scenes with clear visual details',
+ 'Audio automatically matches video motion'
+ ],
+ },
+];
+
+/**
+ * Get model information by ID
+ */
+export function getModelInfo(modelId: string): VideoModelInfo | undefined {
+ return VIDEO_MODELS.find(m => m.id === modelId);
+}
+
+/**
+ * Get recommended model based on use case
+ */
+export function getRecommendedModel(useCase: string): VideoModelInfo | undefined {
+ const useCaseLower = useCase.toLowerCase();
+
+ if (useCaseLower.includes('social') || useCaseLower.includes('instagram') || useCaseLower.includes('tiktok')) {
+ return VIDEO_MODELS.find(m => m.id === 'hunyuan-video-1.5');
+ }
+
+ if (useCaseLower.includes('youtube') || useCaseLower.includes('professional') || useCaseLower.includes('production')) {
+ return VIDEO_MODELS.find(m => m.id === 'lightricks/ltx-2-pro');
+ }
+
+ if (useCaseLower.includes('flexible') || useCaseLower.includes('multi-platform')) {
+ return VIDEO_MODELS.find(m => m.id === 'google/veo3.1');
+ }
+
+ return VIDEO_MODELS[0]; // Default to first model
+}
+
+/**
+ * Compare models side by side
+ */
+export function compareModels(modelIds: string[]): VideoModelInfo[] {
+ return VIDEO_MODELS.filter(m => modelIds.includes(m.id));
+}
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/types.ts b/frontend/src/components/VideoStudio/modules/CreateVideo/types.ts
new file mode 100644
index 00000000..d59887f2
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/types.ts
@@ -0,0 +1,30 @@
+export type Mode = 't2v' | 'i2v';
+
+export type MotionPreset = 'Subtle' | 'Medium' | 'Dynamic';
+export type AspectPreset = '9:16' | '1:1' | '16:9';
+export type Resolution = '480p' | '720p' | '1080p';
+export type Duration = 5 | 8 | 10;
+
+export interface VideoGenerationSettings {
+ mode: Mode;
+ prompt: string;
+ negativePrompt: string;
+ duration: Duration;
+ resolution: Resolution;
+ aspect: AspectPreset;
+ motion: MotionPreset;
+ audioAttached: boolean;
+}
+
+export interface ExampleVideo {
+ id: string;
+ label: string;
+ prompt: string;
+ description: string;
+ price: string;
+ eta: string;
+ provider: string;
+ video: string;
+ platform: string;
+ useCase: string;
+}
diff --git a/frontend/src/components/VideoStudio/modules/CreateVideo/utils/exampleHandlers.ts b/frontend/src/components/VideoStudio/modules/CreateVideo/utils/exampleHandlers.ts
new file mode 100644
index 00000000..2f75885f
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/CreateVideo/utils/exampleHandlers.ts
@@ -0,0 +1,57 @@
+import type { ExampleVideo, AspectPreset } from '../types';
+import type { ContentAsset } from '../../../../../hooks/useContentAssets';
+import { aspectPresets } from '../constants';
+
+export const handleExampleClick = (
+ index: number,
+ example: ExampleVideo,
+ setPrompt: (value: string) => void,
+ setAspect: (value: AspectPreset) => void,
+ setSelectedExample: (index: number | null) => void,
+ setSelectedAssetId: (id: number | null) => void
+) => {
+ setSelectedExample(index);
+ setSelectedAssetId(null);
+ setPrompt(example.prompt);
+ // Set appropriate settings based on example
+ if (example.platform === 'Instagram' || example.platform === 'YouTube') {
+ setAspect('9:16');
+ } else if (example.platform === 'LinkedIn') {
+ setAspect('16:9');
+ }
+};
+
+export const handleAssetClick = (
+ asset: ContentAsset,
+ setPrompt: (value: string) => void,
+ setAspect: (value: AspectPreset) => void,
+ setResolution: (value: '480p' | '720p' | '1080p') => void,
+ setSelectedAssetId: (id: number | null) => void,
+ setSelectedExample: (index: number | null) => void
+) => {
+ setSelectedAssetId(asset.id);
+ setSelectedExample(null);
+ // Use prompt from asset if available, otherwise use title or description
+ if (asset.prompt) {
+ setPrompt(asset.prompt);
+ } else if (asset.title) {
+ setPrompt(asset.title);
+ } else if (asset.description) {
+ setPrompt(asset.description);
+ }
+ // Try to extract settings from metadata
+ if (asset.asset_metadata) {
+ if (asset.asset_metadata.aspect_ratio || asset.asset_metadata.aspect) {
+ const aspectValue = asset.asset_metadata.aspect_ratio || asset.asset_metadata.aspect;
+ if (aspectPresets.includes(aspectValue as any)) {
+ setAspect(aspectValue as AspectPreset);
+ }
+ }
+ if (asset.asset_metadata.resolution) {
+ const res = asset.asset_metadata.resolution.toLowerCase();
+ if (res.includes('480')) setResolution('480p');
+ else if (res.includes('720')) setResolution('720p');
+ else if (res.includes('1080')) setResolution('1080p');
+ }
+ }
+};
diff --git a/frontend/src/components/VideoStudio/modules/EditVideo.tsx b/frontend/src/components/VideoStudio/modules/EditVideo.tsx
new file mode 100644
index 00000000..182cad01
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/EditVideo.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import ModulePlaceholder from '../ModulePlaceholder';
+
+export const EditVideo: React.FC = () => {
+ return (
+
+ );
+};
+
+export default EditVideo;
diff --git a/frontend/src/components/VideoStudio/modules/EnhanceVideo.tsx b/frontend/src/components/VideoStudio/modules/EnhanceVideo.tsx
new file mode 100644
index 00000000..a2fa0ce0
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/EnhanceVideo.tsx
@@ -0,0 +1,3 @@
+// Re-export from the EnhanceVideo component
+export { EnhanceVideo } from './EnhanceVideo/EnhanceVideo';
+export { default } from './EnhanceVideo/EnhanceVideo';
diff --git a/frontend/src/components/VideoStudio/modules/EnhanceVideo/EnhanceVideo.tsx b/frontend/src/components/VideoStudio/modules/EnhanceVideo/EnhanceVideo.tsx
new file mode 100644
index 00000000..1c430cbe
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/EnhanceVideo/EnhanceVideo.tsx
@@ -0,0 +1,407 @@
+import React, { useState, useEffect } from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress, LinearProgress, Alert } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useEnhanceVideo } from './hooks/useEnhanceVideo';
+import { VideoUpload, EnhancementSettings } from './components';
+import { aiApiClient } from '../../../../api/client';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import ErrorIcon from '@mui/icons-material/Error';
+
+const EnhanceVideo: React.FC = () => {
+ const {
+ videoFile,
+ videoPreview,
+ targetResolution,
+ enhancementType,
+ setVideoFile,
+ setTargetResolution,
+ setEnhancementType,
+ canEnhance,
+ costHint,
+ } = useEnhanceVideo();
+
+ const [enhancing, setEnhancing] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [statusMessage, setStatusMessage] = useState('');
+ const [error, setError] = useState(null);
+ const [result, setResult] = useState<{ video_url: string; cost: number } | null>(null);
+ const [progressInterval, setProgressInterval] = useState(null);
+
+ // Cleanup progress interval on unmount
+ useEffect(() => {
+ return () => {
+ if (progressInterval) {
+ clearInterval(progressInterval);
+ }
+ };
+ }, [progressInterval]);
+
+ const handleEnhance = async () => {
+ if (!videoFile) return;
+
+ setEnhancing(true);
+ setError(null);
+ setResult(null);
+ setProgress(0);
+ setStatusMessage('Starting video enhancement...');
+
+ try {
+ // Create FormData
+ const formData = new FormData();
+ formData.append('file', videoFile);
+ formData.append('enhancement_type', enhancementType);
+ formData.append('target_resolution', targetResolution);
+ formData.append('provider', 'wavespeed');
+ formData.append('model', 'flashvsr');
+
+ // Submit enhancement request
+ setStatusMessage('Uploading video...');
+ const response = await aiApiClient.post('/api/video-studio/enhance', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const uploadProgress = Math.round((progressEvent.loaded * 20) / progressEvent.total);
+ setProgress(uploadProgress);
+ setStatusMessage(`Uploading video... ${uploadProgress}%`);
+ }
+ },
+ timeout: 600000, // 10 minutes timeout for long videos
+ });
+
+ setProgress(30);
+ setStatusMessage('Processing video with FlashVSR... This may take a few minutes...');
+
+ // FlashVSR processing can take 3-20 seconds per 1 second of video
+ // Simulate progress updates while waiting for response
+ let simulatedProgress = 30;
+ const interval = setInterval(() => {
+ simulatedProgress = Math.min(90, simulatedProgress + 5);
+ setProgress(simulatedProgress);
+ setStatusMessage(`Processing... ${simulatedProgress}% (This may take several minutes for long videos)`);
+ }, 2000);
+ setProgressInterval(interval);
+
+ try {
+ if (response.data.success) {
+ clearInterval(interval);
+ setProgressInterval(null);
+ setEnhancing(false);
+ setResult(response.data);
+ setProgress(100);
+ setStatusMessage('Video enhancement complete!');
+ } else {
+ clearInterval(interval);
+ setProgressInterval(null);
+ throw new Error(response.data.error || 'Enhancement failed');
+ }
+ } catch (err) {
+ clearInterval(interval);
+ setProgressInterval(null);
+ throw err;
+ }
+ } catch (err: any) {
+ if (progressInterval) {
+ clearInterval(progressInterval);
+ setProgressInterval(null);
+ }
+ setEnhancing(false);
+ setProgress(0);
+ setError(err.response?.data?.detail || err.message || 'Failed to enhance video');
+ setStatusMessage('Enhancement failed');
+ }
+ };
+
+ const handleReset = () => {
+ setEnhancing(false);
+ setProgress(0);
+ setStatusMessage('');
+ setError(null);
+ setResult(null);
+ if (progressInterval) {
+ clearInterval(progressInterval);
+ setProgressInterval(null);
+ }
+ };
+
+ return (
+
+
+ {/* Left Panel - Upload & Settings */}
+
+
+
+
+
+
+
+ : }
+ onClick={handleEnhance}
+ disabled={!canEnhance || enhancing}
+ sx={{
+ py: 1.5,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ '&:disabled': {
+ backgroundColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {enhancing ? 'Enhancing...' : 'Enhance Video'}
+
+
+
+ {enhancing && (
+
+
+
+ {statusMessage}
+
+
+
+
+ )}
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+
+
+
+ {/* Right Panel - Preview & Results */}
+
+
+ {result ? (
+ // Side-by-side comparison view
+
+
+ Comparison
+
+
+
+
+
+
+
+ Original Video
+
+
+
+
+
+
+
+
+
+ Enhanced ({targetResolution.toUpperCase()})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enhancement Complete!
+
+
+ Cost: ${result.cost.toFixed(4)} | Resolution: {targetResolution.toUpperCase()}
+
+
+
+
+ ) : videoPreview ? (
+ // Original video preview
+
+
+ Original Video Preview
+
+
+
+
+
+ Upload a video and select enhancement options to get started
+
+
+
+
+ ) : (
+
+
+ Upload a video to see preview
+
+
+ Your enhanced video will appear here
+
+
+ )}
+
+ {/* Info Box */}
+
+
+ About FlashVSR
+
+
+ FlashVSR is the most advanced video upscaler, delivering:
+
+
+
+ Temporal consistency for stable motion
+
+
+ Detail reconstruction for fine textures
+
+
+ Artifact cleanup for compression blocks
+
+
+ Natural look without overprocessing
+
+
+
+
+
+
+
+ );
+};
+
+export default EnhanceVideo;
+export { EnhanceVideo };
diff --git a/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/EnhancementSettings.tsx b/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/EnhancementSettings.tsx
new file mode 100644
index 00000000..dd2ed1b9
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/EnhancementSettings.tsx
@@ -0,0 +1,147 @@
+import React from 'react';
+import { Box, Stack, Typography, FormControl, InputLabel, Select, MenuItem, Chip, Paper } from '@mui/material';
+import HighQualityIcon from '@mui/icons-material/HighQuality';
+import type { EnhancementResolution, EnhancementType } from '../hooks/useEnhanceVideo';
+
+interface EnhancementSettingsProps {
+ targetResolution: EnhancementResolution;
+ enhancementType: EnhancementType;
+ costHint: string;
+ onTargetResolutionChange: (resolution: EnhancementResolution) => void;
+ onEnhancementTypeChange: (type: EnhancementType) => void;
+}
+
+export const EnhancementSettings: React.FC = ({
+ targetResolution,
+ enhancementType,
+ costHint,
+ onTargetResolutionChange,
+ onEnhancementTypeChange,
+}) => {
+ return (
+
+
+
+
+
+
+ Enhancement Settings
+
+
+
+
+
+
+ Enhancement Type
+
+
+
+
+
+ FlashVSR upscales videos with temporal consistency and detail reconstruction
+
+
+
+
+
+ Target Resolution
+
+
+ Resolution
+
+
+
+ Higher resolution = better quality but higher cost
+
+
+
+
+
+
+ Estimated Cost:
+
+
+
+
+ FlashVSR pricing: $0.012-$0.032/second (based on resolution)
+
+
+ Minimum charge: 5 seconds | Maximum: 10 minutes (600 seconds)
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/VideoUpload.tsx b/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/VideoUpload.tsx
new file mode 100644
index 00000000..65c8e9ba
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/VideoUpload.tsx
@@ -0,0 +1,126 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import CloudUploadIcon from '@mui/icons-material/CloudUpload';
+import VideocamIcon from '@mui/icons-material/Videocam';
+
+interface VideoUploadProps {
+ videoPreview: string | null;
+ onVideoSelect: (file: File | null) => void;
+}
+
+export const VideoUpload: React.FC = ({
+ videoPreview,
+ onVideoSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+ onVideoSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onVideoSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Upload Video
+
+
+ {videoPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload a video
+
+
+ MP4, WebM up to 500MB (max 10 minutes)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/index.ts b/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/index.ts
new file mode 100644
index 00000000..15f0f429
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/EnhanceVideo/components/index.ts
@@ -0,0 +1,2 @@
+export { VideoUpload } from './VideoUpload';
+export { EnhancementSettings } from './EnhancementSettings';
diff --git a/frontend/src/components/VideoStudio/modules/EnhanceVideo/hooks/useEnhanceVideo.ts b/frontend/src/components/VideoStudio/modules/EnhanceVideo/hooks/useEnhanceVideo.ts
new file mode 100644
index 00000000..2078f792
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/EnhanceVideo/hooks/useEnhanceVideo.ts
@@ -0,0 +1,136 @@
+import { useState, useMemo, useCallback, useEffect } from 'react';
+import { aiApiClient } from '../../../../../api/client';
+
+export type EnhancementResolution = '720p' | '1080p' | '2k' | '4k';
+export type EnhancementType = 'upscale';
+
+export const useEnhanceVideo = () => {
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoPreview, setVideoPreview] = useState(null);
+ const [targetResolution, setTargetResolution] = useState('1080p');
+ const [enhancementType, setEnhancementType] = useState('upscale');
+ const [estimatedDuration, setEstimatedDuration] = useState(10.0);
+ const [costEstimate, setCostEstimate] = useState(null);
+
+ // Update preview when file changes
+ useEffect(() => {
+ if (videoFile) {
+ const url = URL.createObjectURL(videoFile);
+ setVideoPreview(url);
+
+ // Rough estimate: 1MB ≈ 1 second at 1080p
+ // In production, you'd parse the video to get actual duration
+ const estimated = Math.max(5, videoFile.size / (1024 * 1024));
+ setEstimatedDuration(estimated);
+
+ return () => URL.revokeObjectURL(url);
+ } else {
+ setVideoPreview(null);
+ setEstimatedDuration(10.0);
+ }
+ }, [videoFile]);
+
+ // Fetch cost estimate when resolution or duration changes
+ useEffect(() => {
+ const fetchCostEstimate = async () => {
+ if (!videoFile || estimatedDuration < 5) {
+ setCostEstimate(null);
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('target_resolution', targetResolution);
+ formData.append('estimated_duration', estimatedDuration.toString());
+
+ const response = await aiApiClient.post('/api/video-studio/enhance/estimate-cost', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ if (response.data.estimated_cost) {
+ setCostEstimate(response.data.estimated_cost);
+ }
+ } catch (err) {
+ console.error('Failed to fetch cost estimate:', err);
+ // Fallback to client-side calculation
+ const pricing = {
+ '720p': 0.06 / 5,
+ '1080p': 0.09 / 5,
+ '2k': 0.12 / 5,
+ '4k': 0.16 / 5,
+ };
+ const costPerSecond = pricing[targetResolution];
+ setCostEstimate(Math.max(5.0, estimatedDuration) * costPerSecond);
+ }
+ };
+
+ fetchCostEstimate();
+ }, [videoFile, targetResolution, estimatedDuration]);
+
+ // Cost hint for display
+ const costHint = useMemo(() => {
+ if (!videoFile) return 'Upload a video to see cost estimate';
+
+ if (costEstimate !== null) {
+ return `Est. ~$${costEstimate.toFixed(2)} (${estimatedDuration.toFixed(0)}s @ ${targetResolution})`;
+ }
+
+ // Fallback calculation
+ const pricing = {
+ '720p': 0.06 / 5,
+ '1080p': 0.09 / 5,
+ '2k': 0.12 / 5,
+ '4k': 0.16 / 5,
+ };
+ const costPerSecond = pricing[targetResolution];
+ const estimatedCost = Math.max(5.0, estimatedDuration) * costPerSecond;
+ return `Est. ~$${estimatedCost.toFixed(2)} (${estimatedDuration.toFixed(0)}s @ ${targetResolution})`;
+ }, [videoFile, targetResolution, estimatedDuration, costEstimate]);
+
+ const canEnhance = useMemo(() => {
+ return videoFile !== null;
+ }, [videoFile]);
+
+ const handleVideoSelect = useCallback((file: File | null) => {
+ setVideoFile(file);
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+
+ // Create preview URL
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setVideoPreview(e.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ } else {
+ setVideoPreview(null);
+ }
+ }, []);
+
+ return {
+ // State
+ videoFile,
+ videoPreview,
+ targetResolution,
+ enhancementType,
+ estimatedDuration,
+ costEstimate,
+ // Setters
+ setVideoFile: handleVideoSelect,
+ setTargetResolution,
+ setEnhancementType,
+ // Computed
+ canEnhance,
+ costHint,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/EnhanceVideo/index.ts b/frontend/src/components/VideoStudio/modules/EnhanceVideo/index.ts
new file mode 100644
index 00000000..5e2c5926
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/EnhanceVideo/index.ts
@@ -0,0 +1,2 @@
+export { EnhanceVideo } from './EnhanceVideo';
+export { default } from './EnhanceVideo';
diff --git a/frontend/src/components/VideoStudio/modules/ExtendVideo.tsx b/frontend/src/components/VideoStudio/modules/ExtendVideo.tsx
new file mode 100644
index 00000000..0f84d70a
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/ExtendVideo.tsx
@@ -0,0 +1,3 @@
+// Re-export from the ExtendVideo component
+export { ExtendVideo } from './ExtendVideo/ExtendVideo';
+export { default } from './ExtendVideo/ExtendVideo';
diff --git a/frontend/src/components/VideoStudio/modules/ExtendVideo/ExtendVideo.tsx b/frontend/src/components/VideoStudio/modules/ExtendVideo/ExtendVideo.tsx
new file mode 100644
index 00000000..52277196
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/ExtendVideo/ExtendVideo.tsx
@@ -0,0 +1,373 @@
+import React, { useState } from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress, LinearProgress, Alert } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useExtendVideo } from './hooks/useExtendVideo';
+import { VideoUpload, AudioUpload, ExtendSettings } from './components';
+import { aiApiClient } from '../../../../api/client';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+
+const ExtendVideo: React.FC = () => {
+ const {
+ videoFile,
+ videoPreview,
+ audioFile,
+ prompt,
+ negativePrompt,
+ model,
+ resolution,
+ duration,
+ enablePromptExpansion,
+ generateAudio,
+ cameraFixed,
+ seed,
+ setVideoFile,
+ setAudioFile,
+ setPrompt,
+ setNegativePrompt,
+ setModel,
+ setResolution,
+ setDuration,
+ setEnablePromptExpansion,
+ setGenerateAudio,
+ setCameraFixed,
+ setSeed,
+ canExtend,
+ costHint,
+ } = useExtendVideo();
+
+ const [extending, setExtending] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [statusMessage, setStatusMessage] = useState('');
+ const [error, setError] = useState(null);
+ const [result, setResult] = useState<{ video_url: string; cost: number; duration: number } | null>(null);
+
+ const handleExtend = async () => {
+ if (!videoFile || !prompt.trim()) return;
+
+ setExtending(true);
+ setError(null);
+ setResult(null);
+ setProgress(0);
+ setStatusMessage('Starting video extension...');
+
+ try {
+ // Create FormData
+ const formData = new FormData();
+ formData.append('file', videoFile);
+ formData.append('prompt', prompt);
+ formData.append('model', model);
+ if (negativePrompt && model === 'wan-2.5') {
+ formData.append('negative_prompt', negativePrompt);
+ }
+ if (audioFile && model === 'wan-2.5') {
+ formData.append('audio', audioFile);
+ }
+ formData.append('resolution', resolution);
+ formData.append('duration', duration.toString());
+ if (model === 'wan-2.5') {
+ formData.append('enable_prompt_expansion', enablePromptExpansion.toString());
+ }
+ if (model === 'seedance-1.5-pro') {
+ formData.append('generate_audio', generateAudio.toString());
+ formData.append('camera_fixed', cameraFixed.toString());
+ }
+ if (seed !== null) {
+ formData.append('seed', seed.toString());
+ }
+
+ // Submit extension request
+ setStatusMessage('Uploading video...');
+ const response = await aiApiClient.post('/api/video-studio/extend', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const uploadProgress = Math.round((progressEvent.loaded * 30) / progressEvent.total);
+ setProgress(uploadProgress);
+ setStatusMessage(`Uploading... ${uploadProgress}%`);
+ }
+ },
+ timeout: 600000, // 10 minutes timeout
+ });
+
+ setProgress(40);
+ setStatusMessage('Extending video with WAN 2.5... This may take a few minutes...');
+
+ if (response.data.success) {
+ setExtending(false);
+ setResult(response.data);
+ setProgress(100);
+ setStatusMessage('Video extension complete!');
+ } else {
+ throw new Error(response.data.error || 'Extension failed');
+ }
+ } catch (err: any) {
+ setExtending(false);
+ setError(err.response?.data?.detail || err.message || 'Failed to extend video');
+ setStatusMessage('Extension failed');
+ }
+ };
+
+ const handleReset = () => {
+ setExtending(false);
+ setProgress(0);
+ setStatusMessage('');
+ setError(null);
+ setResult(null);
+ };
+
+ return (
+
+
+ {/* Left Panel - Upload & Settings */}
+
+
+
+
+ {model === 'wan-2.5' && (
+
+ )}
+
+
+
+
+ : }
+ onClick={handleExtend}
+ disabled={!canExtend || extending}
+ sx={{
+ py: 1.5,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ '&:disabled': {
+ backgroundColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {extending ? 'Extending...' : 'Extend Video'}
+
+
+
+ {extending && (
+
+
+
+ {statusMessage}
+
+
+
+
+ )}
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+ {result && (
+ }
+ action={
+
+ }
+ >
+ Video extended successfully! Cost: ${result.cost.toFixed(2)} ({result.duration}s)
+
+ )}
+
+
+
+ {/* Right Panel - Preview & Results */}
+
+
+
+
+ Preview
+
+
+ {videoPreview && !result && (
+
+
+
+
+ Original Video
+
+
+
+ )}
+
+ {result && (
+
+
+
+
+
+ Extended Video ({result.duration}s @ {resolution.toUpperCase()})
+
+
+
+
+
+
+
+
+
+ )}
+
+ {!videoPreview && !result && (
+
+
+ Upload a video to see preview
+
+
+ )}
+
+
+ {/* Info Box */}
+
+
+ About Video Extension
+
+
+ WAN 2.5 Video-Extend creates seamless extensions of your videos with:
+
+
+
+ Motion continuity for smooth transitions
+
+
+ Audio synchronization when audio is provided (3-30s, ≤15MB)
+
+
+ Natural scene continuation with preserved style
+
+
+ Multilingual support (Chinese and English prompts)
+
+
+ Auto-generated audio if no audio is provided
+
+
+
+ Note: If audio is longer than video duration, only the first segment is used. If audio is shorter, remaining video plays silently.
+
+
+
+
+
+
+ );
+};
+
+export default ExtendVideo;
+export { ExtendVideo };
diff --git a/frontend/src/components/VideoStudio/modules/ExtendVideo/components/AudioUpload.tsx b/frontend/src/components/VideoStudio/modules/ExtendVideo/components/AudioUpload.tsx
new file mode 100644
index 00000000..6ef3b0e9
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/ExtendVideo/components/AudioUpload.tsx
@@ -0,0 +1,122 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import AudioFileIcon from '@mui/icons-material/AudioFile';
+
+interface AudioUploadProps {
+ audioPreview: string | null;
+ onAudioSelect: (file: File | null) => void;
+}
+
+export const AudioUpload: React.FC = ({
+ audioPreview,
+ onAudioSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate audio file
+ if (!file.type.startsWith('audio/')) {
+ alert('Please select an audio file');
+ return;
+ }
+ // Validate audio file size (max 15MB per WAN 2.5 documentation)
+ if (file.size > 15 * 1024 * 1024) {
+ alert('Audio file must be less than 15MB (per WAN 2.5 requirements)');
+ return;
+ }
+ onAudioSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onAudioSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Optional Audio Guide
+
+
+ {audioPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload audio (optional)
+
+
+ MP3, WAV up to 15MB (3-30s recommended)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/ExtendVideo/components/ExtendSettings.tsx b/frontend/src/components/VideoStudio/modules/ExtendVideo/components/ExtendSettings.tsx
new file mode 100644
index 00000000..db0de94e
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/ExtendVideo/components/ExtendSettings.tsx
@@ -0,0 +1,429 @@
+import React, { useState } from 'react';
+import { Box, Stack, Typography, FormControl, InputLabel, Select, MenuItem, TextField, FormControlLabel, Switch, Chip, Button, CircularProgress, Tooltip, Paper } from '@mui/material';
+import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
+import type { ExtendResolution, ExtendModel } from '../hooks/useExtendVideo';
+import { optimizePrompt } from '../../../../../api/videoStudioApi';
+
+interface ExtendSettingsProps {
+ model: ExtendModel;
+ prompt: string;
+ negativePrompt: string;
+ resolution: ExtendResolution;
+ duration: number;
+ enablePromptExpansion: boolean;
+ generateAudio: boolean;
+ cameraFixed: boolean;
+ seed: number | null;
+ costHint: string;
+ onModelChange: (model: ExtendModel) => void;
+ onPromptChange: (value: string) => void;
+ onNegativePromptChange: (value: string) => void;
+ onResolutionChange: (resolution: ExtendResolution) => void;
+ onDurationChange: (duration: number) => void;
+ onEnablePromptExpansionChange: (enabled: boolean) => void;
+ onGenerateAudioChange: (enabled: boolean) => void;
+ onCameraFixedChange: (enabled: boolean) => void;
+ onSeedChange: (seed: number | null) => void;
+}
+
+export const ExtendSettings: React.FC = ({
+ model,
+ prompt,
+ negativePrompt,
+ resolution,
+ duration,
+ enablePromptExpansion,
+ generateAudio,
+ cameraFixed,
+ seed,
+ costHint,
+ onModelChange,
+ onPromptChange,
+ onNegativePromptChange,
+ onResolutionChange,
+ onDurationChange,
+ onEnablePromptExpansionChange,
+ onGenerateAudioChange,
+ onCameraFixedChange,
+ onSeedChange,
+}) => {
+ const [enhancing, setEnhancing] = useState(false);
+
+ const handleEnhancePrompt = async () => {
+ if (!prompt.trim() || enhancing) return;
+
+ setEnhancing(true);
+ try {
+ const result = await optimizePrompt({
+ text: prompt,
+ mode: 'video',
+ style: 'default',
+ });
+
+ if (result.success && result.optimized_prompt) {
+ onPromptChange(result.optimized_prompt);
+ }
+ } catch (error) {
+ console.error('Failed to enhance prompt:', error);
+ } finally {
+ setEnhancing(false);
+ }
+ };
+
+ // Model-specific options
+ const isWan22Spicy = model === 'wan-2.2-spicy';
+ const isSeedance = model === 'seedance-1.5-pro';
+ const isWan25 = model === 'wan-2.5';
+
+ const availableResolutions: ExtendResolution[] = (isWan22Spicy || isSeedance)
+ ? ['480p', '720p']
+ : ['480p', '720p', '1080p'];
+
+ const availableDurations = isWan22Spicy
+ ? [5, 8]
+ : isSeedance
+ ? [4, 5, 6, 7, 8, 9, 10, 11, 12]
+ : [3, 4, 5, 6, 7, 8, 9, 10];
+
+ return (
+
+
+
+ AI Model
+
+
+
+
+
+
+ {isWan22Spicy ? 'WAN 2.2 Spicy' : isSeedance ? 'Seedance 1.5 Pro' : 'WAN 2.5'}
+
+
+ {isWan22Spicy
+ ? 'Fast and affordable: 480p/720p, 5 or 8 seconds. $0.03-0.06/s pricing. Perfect for quick extensions with expressive visuals.'
+ : isSeedance
+ ? `Advanced features: 480p/720p, 4-12 seconds, auto audio generation, camera control. ${generateAudio ? '$0.024-0.052' : '$0.012-0.026'}/s pricing. Ideal for ad creatives and short dramas.`
+ : 'Full featured: 480p/720p/1080p, 3-10 seconds, audio upload, negative prompts, and prompt expansion. $0.05-0.15/s pricing.'}
+
+
+
+
+
+
+
+ Extension Prompt *
+
+
+ : }
+ onClick={handleEnhancePrompt}
+ disabled={!prompt.trim() || enhancing}
+ sx={{
+ textTransform: 'none',
+ fontSize: '0.75rem',
+ py: 0.5,
+ px: 1.5,
+ borderColor: '#3b82f6',
+ color: '#3b82f6',
+ '&:hover': {
+ borderColor: '#3b82f6',
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ },
+ '&:disabled': {
+ borderColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {enhancing ? 'Enhancing...' : 'Enhance Instructions'}
+
+
+
+ onPromptChange(e.target.value)}
+ required
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ backgroundColor: '#fff',
+ '& fieldset': { borderColor: '#e2e8f0' },
+ },
+ '& .MuiInputBase-input': {
+ color: '#0f172a',
+ '&::placeholder': {
+ color: '#64748b',
+ opacity: 1,
+ },
+ },
+ }}
+ />
+
+ Describe the motion, scene, or effect you want for the extended portion. Supports Chinese and English prompts.
+
+
+
+ {isWan25 && (
+
+
+ Negative Prompt (Optional)
+
+ onNegativePromptChange(e.target.value)}
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ backgroundColor: '#fff',
+ '& fieldset': { borderColor: '#e2e8f0' },
+ },
+ }}
+ />
+
+ )}
+
+ {isSeedance && (
+ <>
+
+ onGenerateAudioChange(e.target.checked)}
+ color="primary"
+ />
+ }
+ label="Generate Audio"
+ />
+
+ Automatically generate audio for the extended video
+ {generateAudio
+ ? ' (Adds ~$0.012-0.026/s to cost)'
+ : ' (Saves ~$0.012-0.026/s)'}
+
+
+
+
+ onCameraFixedChange(e.target.checked)}
+ color="primary"
+ />
+ }
+ label="Fix Camera Position"
+ />
+
+ Keep camera position fixed for stable shots
+
+
+ >
+ )}
+
+
+
+ Resolution
+
+
+
+
+
+
+
+
+ Extension Duration
+
+
+
+
+
+ How long should the extended portion be?
+
+
+
+ {isWan25 && (
+
+ onEnablePromptExpansionChange(e.target.checked)}
+ color="primary"
+ />
+ }
+ label="Enable Prompt Expansion"
+ />
+
+ Automatically enhance your prompt for better results
+
+
+ )}
+
+
+
+ Seed (Optional)
+
+ {
+ const value = e.target.value;
+ onSeedChange(value === '' ? null : Number(value));
+ }}
+ sx={{
+ backgroundColor: '#fff',
+ '& .MuiOutlinedInput-root': {
+ '& fieldset': { borderColor: '#e2e8f0' },
+ },
+ }}
+ />
+
+ Use the same seed to reproduce similar results
+
+
+
+
+
+
+ Estimated Cost:
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/ExtendVideo/components/VideoUpload.tsx b/frontend/src/components/VideoStudio/modules/ExtendVideo/components/VideoUpload.tsx
new file mode 100644
index 00000000..38eb54c9
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/ExtendVideo/components/VideoUpload.tsx
@@ -0,0 +1,125 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import VideocamIcon from '@mui/icons-material/Videocam';
+
+interface VideoUploadProps {
+ videoPreview: string | null;
+ onVideoSelect: (file: File | null) => void;
+}
+
+export const VideoUpload: React.FC = ({
+ videoPreview,
+ onVideoSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+ onVideoSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onVideoSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Upload Video to Extend
+
+
+ {videoPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload a video
+
+
+ MP4, WebM up to 500MB
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/ExtendVideo/components/index.ts b/frontend/src/components/VideoStudio/modules/ExtendVideo/components/index.ts
new file mode 100644
index 00000000..8a3d293b
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/ExtendVideo/components/index.ts
@@ -0,0 +1,3 @@
+export { VideoUpload } from './VideoUpload';
+export { AudioUpload } from './AudioUpload';
+export { ExtendSettings } from './ExtendSettings';
diff --git a/frontend/src/components/VideoStudio/modules/ExtendVideo/hooks/useExtendVideo.ts b/frontend/src/components/VideoStudio/modules/ExtendVideo/hooks/useExtendVideo.ts
new file mode 100644
index 00000000..4c68c493
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/ExtendVideo/hooks/useExtendVideo.ts
@@ -0,0 +1,161 @@
+import { useState, useMemo, useCallback } from 'react';
+
+export type ExtendResolution = '480p' | '720p' | '1080p';
+export type ExtendModel = 'wan-2.5' | 'wan-2.2-spicy' | 'seedance-1.5-pro';
+
+export const useExtendVideo = () => {
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoPreview, setVideoPreview] = useState(null);
+ const [audioFile, setAudioFile] = useState(null);
+ const [audioPreview, setAudioPreview] = useState(null);
+ const [prompt, setPrompt] = useState('');
+ const [negativePrompt, setNegativePrompt] = useState('');
+ const [model, setModel] = useState('wan-2.5');
+ const [resolution, setResolution] = useState('720p');
+ const [duration, setDuration] = useState(5);
+ const [enablePromptExpansion, setEnablePromptExpansion] = useState(false);
+ const [generateAudio, setGenerateAudio] = useState(true); // Seedance 1.5 Pro only
+ const [cameraFixed, setCameraFixed] = useState(false); // Seedance 1.5 Pro only
+ const [seed, setSeed] = useState(null);
+
+ // Adjust resolution and duration when model changes
+ const handleModelChange = useCallback((newModel: ExtendModel) => {
+ setModel(newModel);
+ // Adjust resolution if needed
+ if ((newModel === 'wan-2.2-spicy' || newModel === 'seedance-1.5-pro') && resolution === '1080p') {
+ setResolution('720p');
+ }
+ // Adjust duration if needed
+ if (newModel === 'wan-2.2-spicy' && duration !== 5 && duration !== 8) {
+ setDuration(5);
+ } else if (newModel === 'seedance-1.5-pro' && (duration < 4 || duration > 12)) {
+ setDuration(5); // Default to 5s for Seedance
+ }
+ }, [resolution, duration]);
+
+ // Cost estimation (model-specific pricing)
+ const costHint = useMemo(() => {
+ if (!videoFile) return 'Upload a video to see cost estimate';
+
+ // Model-specific pricing
+ let pricing: { [key: string]: number };
+ if (model === 'wan-2.2-spicy') {
+ // WAN 2.2 Spicy: $0.03/s (480p), $0.06/s (720p)
+ pricing = {
+ '480p': 0.03,
+ '720p': 0.06,
+ };
+ } else if (model === 'seedance-1.5-pro') {
+ // Seedance 1.5 Pro pricing varies by audio generation
+ // With audio: $0.024/s (480p), $0.052/s (720p)
+ // Without audio: $0.012/s (480p), $0.026/s (720p)
+ if (generateAudio) {
+ pricing = {
+ '480p': 0.024,
+ '720p': 0.052,
+ };
+ } else {
+ pricing = {
+ '480p': 0.012,
+ '720p': 0.026,
+ };
+ }
+ } else {
+ // WAN 2.5: $0.05/s (480p), $0.10/s (720p), $0.15/s (1080p)
+ pricing = {
+ '480p': 0.05,
+ '720p': 0.10,
+ '1080p': 0.15,
+ };
+ }
+
+ const costPerSecond = pricing[resolution as keyof typeof pricing] || pricing['720p'];
+ const estimatedCost = (costPerSecond * duration).toFixed(2);
+
+ return `Est. ~$${estimatedCost} (${duration}s @ ${resolution})`;
+ }, [videoFile, model, resolution, duration, generateAudio]);
+
+ const canExtend = useMemo(() => {
+ return videoFile !== null && prompt.trim().length > 0;
+ }, [videoFile, prompt]);
+
+ const handleVideoSelect = useCallback((file: File | null) => {
+ setVideoFile(file);
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+
+ // Create preview URL
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setVideoPreview(e.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ } else {
+ setVideoPreview(null);
+ }
+ }, []);
+
+ const handleAudioSelect = useCallback((file: File | null) => {
+ setAudioFile(file);
+ if (file) {
+ // Validate audio file
+ if (!file.type.startsWith('audio/')) {
+ alert('Please select an audio file');
+ return;
+ }
+ if (file.size > 50 * 1024 * 1024) {
+ alert('Audio file must be less than 50MB');
+ return;
+ }
+
+ // Create preview URL
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setAudioPreview(e.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ } else {
+ setAudioPreview(null);
+ }
+ }, []);
+
+ return {
+ // State
+ videoFile,
+ videoPreview,
+ audioFile,
+ audioPreview,
+ prompt,
+ negativePrompt,
+ model,
+ resolution,
+ duration,
+ enablePromptExpansion,
+ generateAudio,
+ cameraFixed,
+ seed,
+ // Setters
+ setVideoFile: handleVideoSelect,
+ setAudioFile: handleAudioSelect,
+ setPrompt,
+ setNegativePrompt,
+ setModel: handleModelChange,
+ setResolution,
+ setDuration,
+ setEnablePromptExpansion,
+ setGenerateAudio,
+ setCameraFixed,
+ setSeed,
+ // Computed
+ canExtend,
+ costHint,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/ExtendVideo/index.ts b/frontend/src/components/VideoStudio/modules/ExtendVideo/index.ts
new file mode 100644
index 00000000..a3b044cf
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/ExtendVideo/index.ts
@@ -0,0 +1,2 @@
+export { ExtendVideo } from './ExtendVideo';
+export { default } from './ExtendVideo';
diff --git a/frontend/src/components/VideoStudio/modules/FaceSwap/FaceSwap.tsx b/frontend/src/components/VideoStudio/modules/FaceSwap/FaceSwap.tsx
new file mode 100644
index 00000000..ed3ee5af
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/FaceSwap/FaceSwap.tsx
@@ -0,0 +1,332 @@
+import React from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress, LinearProgress, Alert, Paper } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useFaceSwap } from './hooks/useFaceSwap';
+import { ImageUpload, VideoUpload, SettingsPanel, ModelSelector } from './components';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import ErrorIcon from '@mui/icons-material/Error';
+
+const FaceSwap: React.FC = () => {
+ const {
+ imageFile,
+ imagePreview,
+ videoFile,
+ videoPreview,
+ model,
+ prompt,
+ resolution,
+ seed,
+ targetGender,
+ targetIndex,
+ swapping,
+ progress,
+ error,
+ result,
+ setImageFile,
+ setVideoFile,
+ setModel,
+ setPrompt,
+ setResolution,
+ setSeed,
+ setTargetGender,
+ setTargetIndex,
+ canSwap,
+ costHint,
+ swapFace,
+ reset,
+ } = useFaceSwap();
+
+ return (
+
+
+ {/* Left Panel - Upload & Settings */}
+
+
+
+
+
+
+
+ {imageFile && videoFile && (
+
+ )}
+
+
+ : }
+ onClick={swapFace}
+ disabled={!canSwap || swapping}
+ sx={{
+ py: 1.5,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ '&:disabled': {
+ backgroundColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {swapping ? 'Swapping Face...' : 'Swap Face'}
+
+
+
+ {imageFile && videoFile && (
+
+
+ Cost: {costHint}
+
+
+ {model === 'mocha'
+ ? 'Minimum charge: 5 seconds | Maximum billed: 120 seconds'
+ : 'Minimum charge: 5 seconds | Maximum billed: 600 seconds (10 minutes)'}
+
+
+ )}
+
+ {swapping && (
+
+
+
+ Processing face swap... This may take a few minutes...
+
+
+
+
+ )}
+
+ {error && (
+ {}} icon={}>
+ {error}
+
+ )}
+
+ {result && (
+ }
+ action={
+
+ }
+ >
+ Face swap successful! Cost: ${result.cost.toFixed(2)}
+
+ )}
+
+
+
+ {/* Right Panel - Preview & Results */}
+
+
+
+
+ Preview
+
+
+ {result ? (
+
+
+
+
+
+ Face-Swapped Video
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ {imagePreview && (
+
+
+ Reference Image:
+
+
+
+
+
+ )}
+
+ {videoPreview && (
+
+
+ Source Video:
+
+
+
+
+
+ )}
+
+ {!imagePreview && !videoPreview && (
+
+
+ Upload image and video to see preview
+
+
+ )}
+
+ )}
+
+
+ {/* Info Box */}
+
+
+ About Face Swap Studio
+
+
+ MoCha performs seamless character replacement in videos:
+
+
+
+ Structure-free replacement - no pose or depth maps needed
+
+
+ Preserves motion, emotion, and camera perspective
+
+
+ Maintains identity consistency across frames
+
+
+ Works with a single reference image and source video
+
+
+
+ Tips: Match pose & composition, keep aspect ratios consistent, limit video length to 60s for best results.
+
+
+
+
+
+
+ );
+};
+
+export default FaceSwap;
+export { FaceSwap };
diff --git a/frontend/src/components/VideoStudio/modules/FaceSwap/components/ImageUpload.tsx b/frontend/src/components/VideoStudio/modules/FaceSwap/components/ImageUpload.tsx
new file mode 100644
index 00000000..18c20c5f
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/FaceSwap/components/ImageUpload.tsx
@@ -0,0 +1,127 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import ImageIcon from '@mui/icons-material/Image';
+
+interface ImageUploadProps {
+ imagePreview: string | null;
+ onImageSelect: (file: File | null) => void;
+}
+
+export const ImageUpload: React.FC = ({
+ imagePreview,
+ onImageSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate image file
+ if (!file.type.startsWith('image/')) {
+ alert('Please select an image file');
+ return;
+ }
+ if (file.size > 10 * 1024 * 1024) {
+ alert('Image file must be less than 10MB');
+ return;
+ }
+ onImageSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onImageSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Reference Image (Character to Swap In)
+
+
+ {imagePreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload reference image
+
+
+ JPG, PNG up to 10MB (avoid WEBP)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/FaceSwap/components/ModelSelector.tsx b/frontend/src/components/VideoStudio/modules/FaceSwap/components/ModelSelector.tsx
new file mode 100644
index 00000000..4fa38cfe
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/FaceSwap/components/ModelSelector.tsx
@@ -0,0 +1,138 @@
+import React from 'react';
+import { Box, Paper, Stack, Typography, FormControl, Select, MenuItem, Chip, Divider } from '@mui/material';
+import { FaceSwapModel } from '../hooks/useFaceSwap';
+
+interface ModelSelectorProps {
+ selectedModel: FaceSwapModel;
+ onModelChange: (model: FaceSwapModel) => void;
+}
+
+const MODEL_INFO = {
+ mocha: {
+ name: 'MoCha',
+ tagline: 'Character Replacement with Motion Preservation',
+ description: 'Advanced character replacement that preserves motion, emotion, and camera perspective. Perfect for film, advertising, and creative character transformation.',
+ pricing: '$0.04/s (480p) or $0.08/s (720p)',
+ maxLength: '120 seconds',
+ features: ['Motion preservation', 'Expression transfer', 'Prompt guidance', 'Seed control', 'High quality output'],
+ },
+ 'video-face-swap': {
+ name: 'Video Face Swap',
+ tagline: 'Simple Face Swap with Multi-Face Support',
+ description: 'Affordable face swap with gender filtering and face index selection. Ideal for content creation, memes, and social media.',
+ pricing: '$0.01/s',
+ maxLength: '10 minutes (600 seconds)',
+ features: ['Multi-face support', 'Gender filter', 'Face index selection', 'Affordable pricing', 'Long video support'],
+ },
+};
+
+export const ModelSelector: React.FC = ({ selectedModel, onModelChange }) => {
+ const selectedInfo = MODEL_INFO[selectedModel];
+
+ return (
+
+
+ AI Model
+
+
+
+
+
+
+
+
+ {selectedInfo.tagline}
+
+
+ {selectedInfo.description}
+
+
+
+
+
+
+
+ Pricing:
+
+
+ {selectedInfo.pricing}
+
+
+
+
+ Max Length:
+
+
+ {selectedInfo.maxLength}
+
+
+
+
+
+
+
+ Features:
+
+
+ {selectedInfo.features.map((feature, idx) => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/FaceSwap/components/SettingsPanel.tsx b/frontend/src/components/VideoStudio/modules/FaceSwap/components/SettingsPanel.tsx
new file mode 100644
index 00000000..502dbd8c
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/FaceSwap/components/SettingsPanel.tsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { Box, Typography, TextField, FormControl, InputLabel, Select, MenuItem, Paper, Stack } from '@mui/material';
+import { Resolution, FaceSwapModel, TargetGender } from '../hooks/useFaceSwap';
+
+interface SettingsPanelProps {
+ model: FaceSwapModel;
+ prompt: string;
+ resolution: Resolution;
+ seed: number | null;
+ targetGender: TargetGender;
+ targetIndex: number;
+ onPromptChange: (value: string) => void;
+ onResolutionChange: (value: Resolution) => void;
+ onSeedChange: (value: number | null) => void;
+ onTargetGenderChange: (value: TargetGender) => void;
+ onTargetIndexChange: (value: number) => void;
+}
+
+export const SettingsPanel: React.FC = ({
+ model,
+ prompt,
+ resolution,
+ seed,
+ targetGender,
+ targetIndex,
+ onPromptChange,
+ onResolutionChange,
+ onSeedChange,
+ onTargetGenderChange,
+ onTargetIndexChange,
+}) => {
+ if (model === 'mocha') {
+ return (
+
+
+ MoCha Settings
+
+
+ onPromptChange(e.target.value)}
+ multiline
+ rows={3}
+ fullWidth
+ helperText="Optional prompt to guide the character replacement"
+ />
+
+
+ Resolution
+
+
+
+ {
+ const value = e.target.value;
+ onSeedChange(value === '' ? null : parseInt(value, 10));
+ }}
+ fullWidth
+ helperText="Random seed for reproducibility (-1 for random, leave empty for random)"
+ inputProps={{ min: -1 }}
+ />
+
+
+ );
+ }
+
+ // video-face-swap settings
+ return (
+
+
+ Video Face Swap Settings
+
+
+
+ Target Gender
+
+
+
+ {
+ const value = parseInt(e.target.value, 10);
+ if (!isNaN(value) && value >= 0 && value <= 10) {
+ onTargetIndexChange(value);
+ }
+ }}
+ fullWidth
+ helperText="0 = largest face, 1 = second largest, etc. (0-10)"
+ inputProps={{ min: 0, max: 10 }}
+ />
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/FaceSwap/components/VideoUpload.tsx b/frontend/src/components/VideoStudio/modules/FaceSwap/components/VideoUpload.tsx
new file mode 100644
index 00000000..8f11c78a
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/FaceSwap/components/VideoUpload.tsx
@@ -0,0 +1,125 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import VideocamIcon from '@mui/icons-material/Videocam';
+
+interface VideoUploadProps {
+ videoPreview: string | null;
+ onVideoSelect: (file: File | null) => void;
+}
+
+export const VideoUpload: React.FC = ({
+ videoPreview,
+ onVideoSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+ onVideoSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onVideoSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Source Video (Character to Replace)
+
+
+ {videoPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload source video
+
+
+ MP4, WebM up to 500MB (max 120 seconds)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/FaceSwap/components/index.ts b/frontend/src/components/VideoStudio/modules/FaceSwap/components/index.ts
new file mode 100644
index 00000000..75d0e6fe
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/FaceSwap/components/index.ts
@@ -0,0 +1,4 @@
+export { ImageUpload } from './ImageUpload';
+export { VideoUpload } from './VideoUpload';
+export { SettingsPanel } from './SettingsPanel';
+export { ModelSelector } from './ModelSelector';
\ No newline at end of file
diff --git a/frontend/src/components/VideoStudio/modules/FaceSwap/hooks/useFaceSwap.ts b/frontend/src/components/VideoStudio/modules/FaceSwap/hooks/useFaceSwap.ts
new file mode 100644
index 00000000..ec9f0c1d
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/FaceSwap/hooks/useFaceSwap.ts
@@ -0,0 +1,168 @@
+import { useState, useMemo, useEffect } from 'react';
+import { aiApiClient } from '../../../../../api/client';
+
+export type Resolution = '480p' | '720p';
+export type FaceSwapModel = 'mocha' | 'video-face-swap';
+export type TargetGender = 'all' | 'female' | 'male';
+
+export const useFaceSwap = () => {
+ const [imageFile, setImageFile] = useState(null);
+ const [imagePreview, setImagePreview] = useState(null);
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoPreview, setVideoPreview] = useState(null);
+ const [model, setModel] = useState('mocha');
+ const [prompt, setPrompt] = useState('');
+ const [resolution, setResolution] = useState('480p');
+ const [seed, setSeed] = useState(null);
+ const [targetGender, setTargetGender] = useState('all');
+ const [targetIndex, setTargetIndex] = useState(0);
+ const [swapping, setSwapping] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [error, setError] = useState(null);
+ const [result, setResult] = useState<{ video_url: string; cost: number; model: string } | null>(null);
+
+ // Update previews when files change
+ useEffect(() => {
+ if (imageFile) {
+ const url = URL.createObjectURL(imageFile);
+ setImagePreview(url);
+ return () => URL.revokeObjectURL(url);
+ } else {
+ setImagePreview(null);
+ }
+ }, [imageFile]);
+
+ useEffect(() => {
+ if (videoFile) {
+ const url = URL.createObjectURL(videoFile);
+ setVideoPreview(url);
+ return () => URL.revokeObjectURL(url);
+ } else {
+ setVideoPreview(null);
+ }
+ }, [videoFile]);
+
+ const canSwap = useMemo(() => {
+ return imageFile !== null && videoFile !== null;
+ }, [imageFile, videoFile]);
+
+ const costHint = useMemo(() => {
+ if (!imageFile || !videoFile) return 'Upload image and video to see cost';
+
+ // MoCha pricing: $0.04/s (480p), $0.08/s (720p)
+ // Video Face Swap pricing: $0.01/s
+ // Minimum charge: 5 seconds for both
+ // We'll estimate based on a default duration (actual cost calculated on backend)
+ let costPerSecond: number;
+ if (model === 'mocha') {
+ costPerSecond = resolution === '480p' ? 0.04 : 0.08;
+ } else {
+ costPerSecond = 0.01;
+ }
+ const estimatedCost = costPerSecond * 10; // Estimate 10 seconds
+ return `~$${estimatedCost.toFixed(2)} (estimated, based on video duration)`;
+ }, [imageFile, videoFile, model, resolution]);
+
+ const swapFace = async (): Promise => {
+ if (!imageFile || !videoFile) return;
+
+ setSwapping(true);
+ setProgress(0);
+ setError(null);
+ setResult(null);
+
+ try {
+ const formData = new FormData();
+ formData.append('image_file', imageFile);
+ formData.append('video_file', videoFile);
+ formData.append('model', model);
+
+ if (model === 'mocha') {
+ if (prompt) {
+ formData.append('prompt', prompt);
+ }
+ formData.append('resolution', resolution);
+ if (seed !== null) {
+ formData.append('seed', seed.toString());
+ }
+ } else {
+ formData.append('target_gender', targetGender);
+ formData.append('target_index', targetIndex.toString());
+ }
+
+ setProgress(10);
+
+ const response = await aiApiClient.post('/api/video-studio/face-swap', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const uploadProgress = Math.round((progressEvent.loaded * 20) / progressEvent.total);
+ setProgress(10 + uploadProgress);
+ }
+ },
+ timeout: 600000, // 10 minutes
+ });
+
+ setProgress(50);
+
+ if (response.data.success) {
+ setResult(response.data);
+ setProgress(100);
+ } else {
+ throw new Error(response.data.error || 'Face swap failed');
+ }
+ } catch (err: any) {
+ setError(err.response?.data?.detail || err.message || 'Failed to swap face');
+ setProgress(0);
+ } finally {
+ setSwapping(false);
+ }
+ };
+
+ const reset = () => {
+ setImageFile(null);
+ setImagePreview(null);
+ setVideoFile(null);
+ setVideoPreview(null);
+ setModel('mocha');
+ setPrompt('');
+ setResolution('480p');
+ setSeed(null);
+ setTargetGender('all');
+ setTargetIndex(0);
+ setResult(null);
+ setError(null);
+ setProgress(0);
+ };
+
+ return {
+ imageFile,
+ imagePreview,
+ videoFile,
+ videoPreview,
+ model,
+ prompt,
+ resolution,
+ seed,
+ targetGender,
+ targetIndex,
+ swapping,
+ progress,
+ error,
+ result,
+ setImageFile,
+ setVideoFile,
+ setModel,
+ setPrompt,
+ setResolution,
+ setSeed,
+ setTargetGender,
+ setTargetIndex,
+ canSwap,
+ costHint,
+ swapFace,
+ reset,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/FaceSwap/index.ts b/frontend/src/components/VideoStudio/modules/FaceSwap/index.ts
new file mode 100644
index 00000000..0cd05a68
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/FaceSwap/index.ts
@@ -0,0 +1,2 @@
+export { FaceSwap } from './FaceSwap';
+export { default } from './FaceSwap';
diff --git a/frontend/src/components/VideoStudio/modules/LibraryVideo.tsx b/frontend/src/components/VideoStudio/modules/LibraryVideo.tsx
new file mode 100644
index 00000000..cdfa01c1
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/LibraryVideo.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import ModulePlaceholder from '../ModulePlaceholder';
+
+export const LibraryVideo: React.FC = () => {
+ return (
+
+ );
+};
+
+export default LibraryVideo;
diff --git a/frontend/src/components/VideoStudio/modules/SocialVideo/SocialVideo.tsx b/frontend/src/components/VideoStudio/modules/SocialVideo/SocialVideo.tsx
new file mode 100644
index 00000000..79a1ce58
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/SocialVideo/SocialVideo.tsx
@@ -0,0 +1,285 @@
+import React from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress, LinearProgress, Alert, Paper } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useSocialVideo } from './hooks/useSocialVideo';
+import { VideoUpload, PlatformSelector, OptimizationOptions, PreviewGrid } from './components';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import ErrorIcon from '@mui/icons-material/Error';
+import DownloadIcon from '@mui/icons-material/Download';
+
+const SocialVideo: React.FC = () => {
+ const {
+ videoFile,
+ videoPreview,
+ selectedPlatforms,
+ autoCrop,
+ generateThumbnails,
+ compress,
+ trimMode,
+ optimizing,
+ progress,
+ results,
+ errors,
+ platformSpecs,
+ setVideoFile,
+ togglePlatform,
+ setAutoCrop,
+ setGenerateThumbnails,
+ setCompress,
+ setTrimMode,
+ canOptimize,
+ costHint,
+ optimize,
+ reset,
+ } = useSocialVideo();
+
+ const handleDownload = (result: any) => {
+ const videoUrl = result.video_url.startsWith('http')
+ ? result.video_url
+ : `${window.location.origin}${result.video_url}`;
+ window.open(videoUrl, '_blank');
+ };
+
+ const handleDownloadAll = () => {
+ results.forEach((result) => {
+ const videoUrl = result.video_url.startsWith('http')
+ ? result.video_url
+ : `${window.location.origin}${result.video_url}`;
+ window.open(videoUrl, '_blank');
+ });
+ };
+
+ return (
+
+
+ {/* Left Panel - Upload & Settings */}
+
+
+
+
+ {videoFile && (
+ <>
+
+
+
+ >
+ )}
+
+
+ : }
+ onClick={optimize}
+ disabled={!canOptimize || optimizing}
+ sx={{
+ py: 1.5,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ '&:disabled': {
+ backgroundColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {optimizing ? 'Optimizing...' : 'Optimize for All Platforms'}
+
+
+
+ {videoFile && (
+
+
+ Cost: {costHint}
+
+
+ )}
+
+ {optimizing && (
+
+
+
+ Optimizing videos for {selectedPlatforms.length} platform{selectedPlatforms.length !== 1 ? 's' : ''}...
+
+
+
+
+ )}
+
+ {errors.length > 0 && (
+ }>
+
+ Optimization Errors:
+
+ {errors.map((error, index) => (
+
+ {error.platform}: {error.error}
+
+ ))}
+
+ )}
+
+ {results.length > 0 && (
+ }
+ action={
+
+ }
+ >
+ Successfully optimized {results.length} video{results.length !== 1 ? 's' : ''}!
+
+ )}
+
+
+
+ {/* Right Panel - Preview & Results */}
+
+
+ {results.length > 0 ? (
+
+ ) : (
+
+
+ Preview
+
+
+ {videoPreview && (
+
+
+
+
+ Original Video
+
+
+
+ )}
+
+ {!videoPreview && (
+
+
+ Upload a video to see preview
+
+
+ )}
+
+ )}
+
+ {/* Info Box */}
+
+
+ About Social Optimizer
+
+
+ Social Optimizer automatically creates platform-ready versions of your video:
+
+
+
+ Aspect ratio conversion (9:16, 16:9, 1:1)
+
+
+ Duration trimming to platform limits
+
+
+ File size compression for platform requirements
+
+
+ Thumbnail generation for each platform
+
+
+
+ All processing is free using FFmpeg.
+
+
+
+
+
+
+ );
+};
+
+export default SocialVideo;
+export { SocialVideo };
diff --git a/frontend/src/components/VideoStudio/modules/SocialVideo/components/OptimizationOptions.tsx b/frontend/src/components/VideoStudio/modules/SocialVideo/components/OptimizationOptions.tsx
new file mode 100644
index 00000000..8537eab4
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/SocialVideo/components/OptimizationOptions.tsx
@@ -0,0 +1,147 @@
+import React from 'react';
+import { Box, Typography, FormControlLabel, Switch, FormControl, RadioGroup, Radio, Stack, Paper } from '@mui/material';
+import { TrimMode } from '../hooks/useSocialVideo';
+
+interface OptimizationOptionsProps {
+ autoCrop: boolean;
+ generateThumbnails: boolean;
+ compress: boolean;
+ trimMode: TrimMode;
+ onAutoCropChange: (value: boolean) => void;
+ onGenerateThumbnailsChange: (value: boolean) => void;
+ onCompressChange: (value: boolean) => void;
+ onTrimModeChange: (value: TrimMode) => void;
+}
+
+export const OptimizationOptions: React.FC = ({
+ autoCrop,
+ generateThumbnails,
+ compress,
+ trimMode,
+ onAutoCropChange,
+ onGenerateThumbnailsChange,
+ onCompressChange,
+ onTrimModeChange,
+}) => {
+ return (
+
+
+ Optimization Options
+
+
+ onAutoCropChange(e.target.checked)}
+ color="primary"
+ />
+ }
+ label={
+
+
+ Auto-crop to platform ratio
+
+
+ Automatically crop video to match platform aspect ratio
+
+
+ }
+ />
+
+ onGenerateThumbnailsChange(e.target.checked)}
+ color="primary"
+ />
+ }
+ label={
+
+
+ Generate thumbnails
+
+
+ Create thumbnail images for each platform
+
+
+ }
+ />
+
+ onCompressChange(e.target.checked)}
+ color="primary"
+ />
+ }
+ label={
+
+
+ Compress for file size limits
+
+
+ Automatically compress videos to meet platform file size requirements
+
+
+ }
+ />
+
+
+
+ Trim Mode (if video exceeds duration)
+
+ onTrimModeChange(e.target.value as TrimMode)}
+ >
+ }
+ label={
+
+ Keep Beginning - Trim from the end
+
+ }
+ />
+ }
+ label={
+
+ Keep Middle - Trim from both ends
+
+ }
+ />
+ }
+ label={
+
+ Keep End - Trim from the beginning
+
+ }
+ />
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/SocialVideo/components/PlatformSelector.tsx b/frontend/src/components/VideoStudio/modules/SocialVideo/components/PlatformSelector.tsx
new file mode 100644
index 00000000..cf7caeb5
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/SocialVideo/components/PlatformSelector.tsx
@@ -0,0 +1,111 @@
+import React from 'react';
+import { Box, Typography, FormControlLabel, Checkbox, Stack, Chip, Paper } from '@mui/material';
+import { Platform } from '../hooks/useSocialVideo';
+
+interface PlatformSelectorProps {
+ selectedPlatforms: Platform[];
+ platformSpecs: Record;
+ onTogglePlatform: (platform: Platform) => void;
+}
+
+const platformInfo: Record = {
+ instagram: { label: 'Instagram Reels', icon: '📷', color: '#E4405F' },
+ tiktok: { label: 'TikTok', icon: '🎵', color: '#000000' },
+ youtube: { label: 'YouTube Shorts', icon: '▶️', color: '#FF0000' },
+ linkedin: { label: 'LinkedIn', icon: '💼', color: '#0077B5' },
+ facebook: { label: 'Facebook', icon: '👥', color: '#1877F2' },
+ twitter: { label: 'Twitter/X', icon: '🐦', color: '#1DA1F2' },
+};
+
+export const PlatformSelector: React.FC = ({
+ selectedPlatforms,
+ platformSpecs,
+ onTogglePlatform,
+}) => {
+ const getPlatformSpec = (platform: Platform) => {
+ const specs = platformSpecs[platform];
+ if (!specs || specs.length === 0) return null;
+ return specs[0]; // Get first format
+ };
+
+ return (
+
+
+ Select Platforms
+
+
+ {(Object.keys(platformInfo) as Platform[]).map((platform) => {
+ const info = platformInfo[platform];
+ const spec = getPlatformSpec(platform);
+ const isSelected = selectedPlatforms.includes(platform);
+
+ return (
+ onTogglePlatform(platform)}
+ >
+
+ onTogglePlatform(platform)}
+ sx={{
+ color: info.color,
+ '&.Mui-checked': {
+ color: info.color,
+ },
+ }}
+ />
+
+
+
+ {info.icon} {info.label}
+
+
+ {spec && (
+
+
+
+
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/SocialVideo/components/PreviewGrid.tsx b/frontend/src/components/VideoStudio/modules/SocialVideo/components/PreviewGrid.tsx
new file mode 100644
index 00000000..0bc7d7da
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/SocialVideo/components/PreviewGrid.tsx
@@ -0,0 +1,198 @@
+import React from 'react';
+import { Grid, Box, Typography, Button, Stack, Chip, Paper, CircularProgress } from '@mui/material';
+import DownloadIcon from '@mui/icons-material/Download';
+import { PlatformResult } from '../hooks/useSocialVideo';
+
+interface PreviewGridProps {
+ results: PlatformResult[];
+ optimizing: boolean;
+ onDownload: (result: PlatformResult) => void;
+ onDownloadAll: () => void;
+}
+
+const platformColors: Record = {
+ instagram: '#E4405F',
+ tiktok: '#000000',
+ youtube: '#FF0000',
+ linkedin: '#0077B5',
+ facebook: '#1877F2',
+ twitter: '#1DA1F2',
+};
+
+export const PreviewGrid: React.FC = ({
+ results,
+ optimizing,
+ onDownload,
+ onDownloadAll,
+}) => {
+ if (optimizing) {
+ return (
+
+
+
+ Optimizing videos for selected platforms...
+
+
+ );
+ }
+
+ if (results.length === 0) {
+ return (
+
+
+ Optimized videos will appear here
+
+
+ );
+ }
+
+ const formatFileSize = (bytes: number): string => {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ };
+
+ return (
+
+
+
+ Optimized Videos ({results.length})
+
+ {results.length > 1 && (
+ }
+ onClick={onDownloadAll}
+ sx={{
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ }}
+ >
+ Download All
+
+ )}
+
+
+
+ {results.map((result, index) => {
+ const color = platformColors[result.platform] || '#3b82f6';
+ const videoUrl = result.video_url.startsWith('http')
+ ? result.video_url
+ : `${window.location.origin}${result.video_url}`;
+
+ return (
+
+
+
+
+
+
+ {result.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {result.thumbnail_url && (
+
+
+ Thumbnail:
+
+
+
+ )}
+
+ }
+ onClick={() => onDownload(result)}
+ href={videoUrl}
+ download
+ sx={{
+ borderColor: color,
+ color: color,
+ '&:hover': {
+ borderColor: color,
+ backgroundColor: `${color}08`,
+ },
+ }}
+ >
+ Download
+
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/SocialVideo/components/VideoUpload.tsx b/frontend/src/components/VideoStudio/modules/SocialVideo/components/VideoUpload.tsx
new file mode 100644
index 00000000..65c8e9ba
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/SocialVideo/components/VideoUpload.tsx
@@ -0,0 +1,126 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import CloudUploadIcon from '@mui/icons-material/CloudUpload';
+import VideocamIcon from '@mui/icons-material/Videocam';
+
+interface VideoUploadProps {
+ videoPreview: string | null;
+ onVideoSelect: (file: File | null) => void;
+}
+
+export const VideoUpload: React.FC = ({
+ videoPreview,
+ onVideoSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+ onVideoSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onVideoSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Upload Video
+
+
+ {videoPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload a video
+
+
+ MP4, WebM up to 500MB (max 10 minutes)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/SocialVideo/components/index.ts b/frontend/src/components/VideoStudio/modules/SocialVideo/components/index.ts
new file mode 100644
index 00000000..db86d45b
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/SocialVideo/components/index.ts
@@ -0,0 +1,4 @@
+export { VideoUpload } from './VideoUpload';
+export { PlatformSelector } from './PlatformSelector';
+export { OptimizationOptions } from './OptimizationOptions';
+export { PreviewGrid } from './PreviewGrid';
diff --git a/frontend/src/components/VideoStudio/modules/SocialVideo/hooks/useSocialVideo.ts b/frontend/src/components/VideoStudio/modules/SocialVideo/hooks/useSocialVideo.ts
new file mode 100644
index 00000000..8bc7836d
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/SocialVideo/hooks/useSocialVideo.ts
@@ -0,0 +1,163 @@
+import { useState, useMemo, useEffect } from 'react';
+import { aiApiClient } from '../../../../../api/client';
+
+export type Platform = 'instagram' | 'tiktok' | 'youtube' | 'linkedin' | 'facebook' | 'twitter';
+export type TrimMode = 'beginning' | 'middle' | 'end';
+
+export interface PlatformResult {
+ platform: string;
+ name: string;
+ aspect_ratio: string;
+ video_url: string;
+ thumbnail_url?: string;
+ duration: number;
+ file_size: number;
+ width: number;
+ height: number;
+}
+
+export const useSocialVideo = () => {
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoPreview, setVideoPreview] = useState(null);
+ const [selectedPlatforms, setSelectedPlatforms] = useState([]);
+ const [autoCrop, setAutoCrop] = useState(true);
+ const [generateThumbnails, setGenerateThumbnails] = useState(true);
+ const [compress, setCompress] = useState(true);
+ const [trimMode, setTrimMode] = useState('beginning');
+ const [optimizing, setOptimizing] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [results, setResults] = useState([]);
+ const [errors, setErrors] = useState>([]);
+ const [platformSpecs, setPlatformSpecs] = useState>({});
+
+ // Update preview when file changes
+ useEffect(() => {
+ if (videoFile) {
+ const url = URL.createObjectURL(videoFile);
+ setVideoPreview(url);
+ return () => URL.revokeObjectURL(url);
+ } else {
+ setVideoPreview(null);
+ }
+ }, [videoFile]);
+
+ // Load platform specifications
+ useEffect(() => {
+ const loadPlatformSpecs = async () => {
+ try {
+ const response = await aiApiClient.get('/api/video-studio/social/platforms');
+ if (response.data.success) {
+ setPlatformSpecs(response.data.platforms);
+ }
+ } catch (error) {
+ console.error('Failed to load platform specs:', error);
+ }
+ };
+ loadPlatformSpecs();
+ }, []);
+
+ const togglePlatform = (platform: Platform) => {
+ setSelectedPlatforms((prev) =>
+ prev.includes(platform)
+ ? prev.filter((p) => p !== platform)
+ : [...prev, platform]
+ );
+ };
+
+ const canOptimize = useMemo(() => {
+ return videoFile !== null && selectedPlatforms.length > 0;
+ }, [videoFile, selectedPlatforms]);
+
+ const costHint = useMemo(() => {
+ if (!videoFile) return 'Upload a video to optimize';
+ if (selectedPlatforms.length === 0) return 'Select at least one platform';
+ return 'Free (FFmpeg processing)';
+ }, [videoFile, selectedPlatforms]);
+
+ const optimize = async (): Promise => {
+ if (!videoFile || selectedPlatforms.length === 0) return;
+
+ setOptimizing(true);
+ setProgress(0);
+ setResults([]);
+ setErrors([]);
+
+ try {
+ const formData = new FormData();
+ formData.append('file', videoFile);
+ formData.append('platforms', selectedPlatforms.join(','));
+ formData.append('auto_crop', autoCrop.toString());
+ formData.append('generate_thumbnails', generateThumbnails.toString());
+ formData.append('compress', compress.toString());
+ formData.append('trim_mode', trimMode);
+
+ setProgress(20);
+
+ const response = await aiApiClient.post('/api/video-studio/social/optimize', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const uploadProgress = Math.round((progressEvent.loaded * 30) / progressEvent.total);
+ setProgress(20 + uploadProgress);
+ }
+ },
+ timeout: 600000, // 10 minutes
+ });
+
+ setProgress(80);
+
+ if (response.data.success) {
+ setResults(response.data.results || []);
+ setErrors(response.data.errors || []);
+ setProgress(100);
+ } else {
+ throw new Error(response.data.error || 'Optimization failed');
+ }
+ } catch (error: any) {
+ setErrors([
+ {
+ platform: 'all',
+ error: error.response?.data?.detail || error.message || 'Optimization failed',
+ },
+ ]);
+ } finally {
+ setOptimizing(false);
+ }
+ };
+
+ const reset = () => {
+ setVideoFile(null);
+ setVideoPreview(null);
+ setSelectedPlatforms([]);
+ setResults([]);
+ setErrors([]);
+ setProgress(0);
+ };
+
+ return {
+ videoFile,
+ videoPreview,
+ selectedPlatforms,
+ autoCrop,
+ generateThumbnails,
+ compress,
+ trimMode,
+ optimizing,
+ progress,
+ results,
+ errors,
+ platformSpecs,
+ setVideoFile,
+ togglePlatform,
+ setAutoCrop,
+ setGenerateThumbnails,
+ setCompress,
+ setTrimMode,
+ canOptimize,
+ costHint,
+ optimize,
+ reset,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/SocialVideo/index.ts b/frontend/src/components/VideoStudio/modules/SocialVideo/index.ts
new file mode 100644
index 00000000..d8dc04ff
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/SocialVideo/index.ts
@@ -0,0 +1,2 @@
+export { SocialVideo } from './SocialVideo';
+export { default } from './SocialVideo';
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/TransformVideo.tsx b/frontend/src/components/VideoStudio/modules/TransformVideo/TransformVideo.tsx
new file mode 100644
index 00000000..8ad59578
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/TransformVideo.tsx
@@ -0,0 +1,449 @@
+import React, { useState, useEffect } from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress, LinearProgress, Alert, Paper } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useTransformVideo } from './hooks/useTransformVideo';
+import {
+ VideoUpload,
+ TransformTabs,
+ FormatConverter,
+ AspectConverter,
+ SpeedAdjuster,
+ ResolutionScaler,
+ Compressor,
+} from './components';
+import { aiApiClient } from '../../../../api/client';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import ErrorIcon from '@mui/icons-material/Error';
+
+const TransformVideo: React.FC = () => {
+ const {
+ videoFile,
+ videoPreview,
+ transformType,
+ outputFormat,
+ codec,
+ quality,
+ audioCodec,
+ targetAspect,
+ cropMode,
+ speedFactor,
+ targetResolution,
+ maintainAspect,
+ targetSizeMb,
+ compressQuality,
+ setVideoFile,
+ setTransformType,
+ setOutputFormat,
+ setCodec,
+ setQuality,
+ setAudioCodec,
+ setTargetAspect,
+ setCropMode,
+ setSpeedFactor,
+ setTargetResolution,
+ setMaintainAspect,
+ setTargetSizeMb,
+ setCompressQuality,
+ canTransform,
+ costHint,
+ } = useTransformVideo();
+
+ const [transforming, setTransforming] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [statusMessage, setStatusMessage] = useState('');
+ const [error, setError] = useState(null);
+ const [result, setResult] = useState<{ video_url: string; cost: number } | null>(null);
+
+ const handleTransform = async () => {
+ if (!videoFile) return;
+
+ setTransforming(true);
+ setError(null);
+ setResult(null);
+ setProgress(0);
+ setStatusMessage('Starting video transformation...');
+
+ try {
+ // Create FormData
+ const formData = new FormData();
+ formData.append('file', videoFile);
+ formData.append('transform_type', transformType);
+
+ // Add transform-specific parameters
+ if (transformType === 'format') {
+ formData.append('output_format', outputFormat);
+ if (codec) formData.append('codec', codec);
+ formData.append('quality', quality);
+ if (audioCodec) formData.append('audio_codec', audioCodec);
+ } else if (transformType === 'aspect') {
+ formData.append('target_aspect', targetAspect);
+ formData.append('crop_mode', cropMode);
+ } else if (transformType === 'speed') {
+ formData.append('speed_factor', speedFactor.toString());
+ } else if (transformType === 'resolution') {
+ formData.append('target_resolution', targetResolution);
+ formData.append('maintain_aspect', maintainAspect.toString());
+ } else if (transformType === 'compress') {
+ formData.append('compress_quality', compressQuality);
+ if (targetSizeMb) {
+ formData.append('target_size_mb', targetSizeMb.toString());
+ }
+ }
+
+ // Submit transformation request
+ setStatusMessage('Uploading video...');
+ const response = await aiApiClient.post('/api/video-studio/transform', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const uploadProgress = Math.round((progressEvent.loaded * 20) / progressEvent.total);
+ setProgress(uploadProgress);
+ setStatusMessage(`Uploading video... ${uploadProgress}%`);
+ }
+ },
+ timeout: 600000, // 10 minutes timeout for long videos
+ });
+
+ setProgress(30);
+ setStatusMessage('Processing video... This may take a few minutes...');
+
+ if (response.data.success) {
+ setTransforming(false);
+ setResult(response.data);
+ setProgress(100);
+ setStatusMessage('Video transformation complete!');
+ } else {
+ throw new Error(response.data.error || 'Transformation failed');
+ }
+ } catch (err: any) {
+ setTransforming(false);
+ setError(err.response?.data?.detail || err.message || 'Failed to transform video');
+ setStatusMessage('Transformation failed');
+ }
+ };
+
+ const handleReset = () => {
+ setTransforming(false);
+ setProgress(0);
+ setStatusMessage('');
+ setError(null);
+ setResult(null);
+ };
+
+ const renderTransformSettings = () => {
+ switch (transformType) {
+ case 'format':
+ return (
+
+ );
+ case 'aspect':
+ return (
+
+ );
+ case 'speed':
+ return (
+
+ );
+ case 'resolution':
+ return (
+
+ );
+ case 'compress':
+ return (
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+ {/* Left Panel - Upload & Settings */}
+
+
+
+
+ {videoFile && (
+ <>
+
+
+
+ {renderTransformSettings()}
+
+ >
+ )}
+
+
+ : }
+ onClick={handleTransform}
+ disabled={!canTransform || transforming}
+ sx={{
+ py: 1.5,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ '&:disabled': {
+ backgroundColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {transforming ? 'Transforming...' : 'Transform Video'}
+
+
+
+ {videoFile && (
+
+
+ Cost: {costHint}
+
+
+ )}
+
+ {transforming && (
+
+
+
+ {statusMessage}
+
+
+
+
+ )}
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+ {result && (
+ }
+ action={
+
+ }
+ >
+ Video transformed successfully! Cost: ${result.cost.toFixed(2)}
+
+ )}
+
+
+
+ {/* Right Panel - Preview & Results */}
+
+
+
+
+ Preview
+
+
+ {videoPreview && !result && (
+
+
+
+
+ Original Video
+
+
+
+ )}
+
+ {result && (
+
+
+
+
+
+ Transformed Video ({transformType})
+
+
+
+
+
+
+
+
+
+ )}
+
+ {!videoPreview && !result && (
+
+
+ Upload a video to see preview
+
+
+ )}
+
+
+ {/* Info Box */}
+
+
+ About Transform Studio
+
+
+ Transform Studio uses FFmpeg for fast, free video processing:
+
+
+
+ Format conversion: MP4, MOV, WebM, GIF
+
+
+ Aspect ratio conversion with smart cropping
+
+
+ Speed adjustment (0.25x to 4x)
+
+
+ Resolution scaling (480p to 4K)
+
+
+ File size compression
+
+
+
+
+
+
+
+ );
+};
+
+export default TransformVideo;
+export { TransformVideo };
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/components/AspectConverter.tsx b/frontend/src/components/VideoStudio/modules/TransformVideo/components/AspectConverter.tsx
new file mode 100644
index 00000000..4a16309a
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/components/AspectConverter.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { Box, Typography, FormControl, InputLabel, Select, MenuItem, Stack, RadioGroup, FormControlLabel, Radio } from '@mui/material';
+import type { AspectRatio } from '../hooks/useTransformVideo';
+
+interface AspectConverterProps {
+ targetAspect: AspectRatio;
+ cropMode: 'center' | 'letterbox';
+ onTargetAspectChange: (aspect: AspectRatio) => void;
+ onCropModeChange: (mode: 'center' | 'letterbox') => void;
+}
+
+export const AspectConverter: React.FC = ({
+ targetAspect,
+ cropMode,
+ onTargetAspectChange,
+ onCropModeChange,
+}) => {
+ return (
+
+
+ Aspect Ratio Conversion Settings
+
+
+
+ Target Aspect Ratio
+
+
+
+
+
+ Crop Mode
+
+ onCropModeChange(e.target.value as 'center' | 'letterbox')}
+ >
+ }
+ label="Center Crop (Crop to fit, may lose edges)"
+ />
+ }
+ label="Letterbox (Add black bars, preserves full video)"
+ />
+
+
+
+
+
+ Center Crop: Crops the video to fit the target aspect ratio. May remove parts of the video.
+
+ Letterbox: Adds black bars to fit the aspect ratio. Preserves the entire video.
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/components/Compressor.tsx b/frontend/src/components/VideoStudio/modules/TransformVideo/components/Compressor.tsx
new file mode 100644
index 00000000..9f2e0afb
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/components/Compressor.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { Box, Typography, FormControl, InputLabel, Select, MenuItem, Stack, TextField } from '@mui/material';
+import type { Quality } from '../hooks/useTransformVideo';
+
+interface CompressorProps {
+ targetSizeMb: number | null;
+ compressQuality: Quality;
+ onTargetSizeMbChange: (size: number | null) => void;
+ onCompressQualityChange: (quality: Quality) => void;
+}
+
+export const Compressor: React.FC = ({
+ targetSizeMb,
+ compressQuality,
+ onTargetSizeMbChange,
+ onCompressQualityChange,
+}) => {
+ return (
+
+
+ Compression Settings
+
+
+
+ Quality Preset
+
+
+
+ {
+ const value = e.target.value;
+ onTargetSizeMbChange(value ? parseFloat(value) : null);
+ }}
+ helperText="Optional: Specify target file size. If not set, quality preset will be used."
+ inputProps={{ min: 1, step: 0.1 }}
+ />
+
+
+
+ Quality Preset: Uses optimized bitrate settings for the selected quality level.
+
+ Target Size: Calculates bitrate to achieve the specified file size. Overrides quality preset if set.
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/components/FormatConverter.tsx b/frontend/src/components/VideoStudio/modules/TransformVideo/components/FormatConverter.tsx
new file mode 100644
index 00000000..569c91e2
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/components/FormatConverter.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+import { Box, Typography, FormControl, InputLabel, Select, MenuItem, Stack } from '@mui/material';
+import type { OutputFormat, Quality } from '../hooks/useTransformVideo';
+
+interface FormatConverterProps {
+ outputFormat: OutputFormat;
+ codec: string;
+ quality: Quality;
+ audioCodec: string;
+ onOutputFormatChange: (format: OutputFormat) => void;
+ onCodecChange: (codec: string) => void;
+ onQualityChange: (quality: Quality) => void;
+ onAudioCodecChange: (codec: string) => void;
+}
+
+export const FormatConverter: React.FC = ({
+ outputFormat,
+ codec,
+ quality,
+ audioCodec,
+ onOutputFormatChange,
+ onCodecChange,
+ onQualityChange,
+ onAudioCodecChange,
+}) => {
+ return (
+
+
+ Format Conversion Settings
+
+
+
+ Output Format
+
+
+
+ {outputFormat !== 'gif' && (
+ <>
+
+ Video Codec
+
+
+
+
+ Audio Codec
+
+
+ >
+ )}
+
+ {outputFormat !== 'gif' && (
+
+ Quality
+
+
+ )}
+
+ {outputFormat === 'gif' && (
+
+
+ GIF format will be optimized for web with reduced frame rate (15fps) and no audio.
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/components/ResolutionScaler.tsx b/frontend/src/components/VideoStudio/modules/TransformVideo/components/ResolutionScaler.tsx
new file mode 100644
index 00000000..90ba0aa8
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/components/ResolutionScaler.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { Box, Typography, FormControl, InputLabel, Select, MenuItem, Stack, FormControlLabel, Checkbox } from '@mui/material';
+import type { Resolution } from '../hooks/useTransformVideo';
+
+interface ResolutionScalerProps {
+ targetResolution: Resolution;
+ maintainAspect: boolean;
+ onTargetResolutionChange: (resolution: Resolution) => void;
+ onMaintainAspectChange: (maintain: boolean) => void;
+}
+
+export const ResolutionScaler: React.FC = ({
+ targetResolution,
+ maintainAspect,
+ onTargetResolutionChange,
+ onMaintainAspectChange,
+}) => {
+ return (
+
+
+ Resolution Scaling Settings
+
+
+
+ Target Resolution
+
+
+
+ onMaintainAspectChange(e.target.checked)}
+ />
+ }
+ label="Maintain Aspect Ratio"
+ />
+
+
+
+ {maintainAspect
+ ? 'The video will be scaled to match the target resolution while preserving the original aspect ratio. This may add letterboxing or pillarboxing.'
+ : 'The video will be stretched or compressed to exactly match the target resolution. This may distort the video if aspect ratios differ.'}
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/components/SpeedAdjuster.tsx b/frontend/src/components/VideoStudio/modules/TransformVideo/components/SpeedAdjuster.tsx
new file mode 100644
index 00000000..d5ee4f17
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/components/SpeedAdjuster.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { Box, Typography, Slider, Stack, Chip } from '@mui/material';
+
+interface SpeedAdjusterProps {
+ speedFactor: number;
+ onSpeedFactorChange: (factor: number) => void;
+}
+
+const speedPresets = [
+ { label: '0.25x', value: 0.25, description: 'Very Slow' },
+ { label: '0.5x', value: 0.5, description: 'Slow Motion' },
+ { label: '1x', value: 1.0, description: 'Normal' },
+ { label: '1.5x', value: 1.5, description: 'Fast' },
+ { label: '2x', value: 2.0, description: '2x Speed' },
+ { label: '4x', value: 4.0, description: 'Time-lapse' },
+];
+
+export const SpeedAdjuster: React.FC = ({
+ speedFactor,
+ onSpeedFactorChange,
+}) => {
+ return (
+
+
+ Speed Adjustment Settings
+
+
+
+
+ Select a preset or use the slider for custom speed:
+
+
+ {speedPresets.map((preset) => (
+ onSpeedFactorChange(preset.value)}
+ color={speedFactor === preset.value ? 'primary' : 'default'}
+ sx={{
+ cursor: 'pointer',
+ fontWeight: speedFactor === preset.value ? 700 : 400,
+ }}
+ />
+ ))}
+
+
+
+ Custom Speed: {speedFactor}x
+
+ onSpeedFactorChange(value as number)}
+ min={0.25}
+ max={4.0}
+ step={0.25}
+ marks={[
+ { value: 0.25, label: '0.25x' },
+ { value: 1.0, label: '1x' },
+ { value: 2.0, label: '2x' },
+ { value: 4.0, label: '4x' },
+ ]}
+ sx={{
+ '& .MuiSlider-markLabel': {
+ fontSize: '0.75rem',
+ },
+ }}
+ />
+
+
+
+
+ Speed adjustment affects both video and audio. Values below 1x create slow motion, values above 1x create fast-forward or time-lapse effects.
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/components/TransformTabs.tsx b/frontend/src/components/VideoStudio/modules/TransformVideo/components/TransformTabs.tsx
new file mode 100644
index 00000000..ee38fa59
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/components/TransformTabs.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { Tabs, Tab, Box } from '@mui/material';
+import type { TransformType } from '../hooks/useTransformVideo';
+
+interface TransformTabsProps {
+ transformType: TransformType;
+ onTransformTypeChange: (type: TransformType) => void;
+}
+
+export const TransformTabs: React.FC = ({
+ transformType,
+ onTransformTypeChange,
+}) => {
+ const handleChange = (_event: React.SyntheticEvent, newValue: TransformType) => {
+ onTransformTypeChange(newValue);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/components/VideoUpload.tsx b/frontend/src/components/VideoStudio/modules/TransformVideo/components/VideoUpload.tsx
new file mode 100644
index 00000000..65c8e9ba
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/components/VideoUpload.tsx
@@ -0,0 +1,126 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import CloudUploadIcon from '@mui/icons-material/CloudUpload';
+import VideocamIcon from '@mui/icons-material/Videocam';
+
+interface VideoUploadProps {
+ videoPreview: string | null;
+ onVideoSelect: (file: File | null) => void;
+}
+
+export const VideoUpload: React.FC = ({
+ videoPreview,
+ onVideoSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+ onVideoSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onVideoSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Upload Video
+
+
+ {videoPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload a video
+
+
+ MP4, WebM up to 500MB (max 10 minutes)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/components/index.ts b/frontend/src/components/VideoStudio/modules/TransformVideo/components/index.ts
new file mode 100644
index 00000000..d3854966
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/components/index.ts
@@ -0,0 +1,7 @@
+export { VideoUpload } from './VideoUpload';
+export { TransformTabs } from './TransformTabs';
+export { FormatConverter } from './FormatConverter';
+export { AspectConverter } from './AspectConverter';
+export { SpeedAdjuster } from './SpeedAdjuster';
+export { ResolutionScaler } from './ResolutionScaler';
+export { Compressor } from './Compressor';
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/hooks/useTransformVideo.ts b/frontend/src/components/VideoStudio/modules/TransformVideo/hooks/useTransformVideo.ts
new file mode 100644
index 00000000..92e2f13d
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/hooks/useTransformVideo.ts
@@ -0,0 +1,135 @@
+import { useState, useMemo, useCallback } from 'react';
+
+export type TransformType = 'format' | 'aspect' | 'speed' | 'resolution' | 'compress';
+export type OutputFormat = 'mp4' | 'mov' | 'webm' | 'gif';
+export type AspectRatio = '16:9' | '9:16' | '1:1' | '4:5' | '21:9';
+export type Quality = 'high' | 'medium' | 'low';
+export type Resolution = '480p' | '720p' | '1080p' | '1440p' | '4k';
+
+export const useTransformVideo = () => {
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoPreview, setVideoPreview] = useState(null);
+ const [transformType, setTransformType] = useState('format');
+
+ // Format conversion state
+ const [outputFormat, setOutputFormat] = useState('mp4');
+ const [codec, setCodec] = useState('libx264');
+ const [quality, setQuality] = useState('medium');
+ const [audioCodec, setAudioCodec] = useState('aac');
+
+ // Aspect ratio state
+ const [targetAspect, setTargetAspect] = useState('16:9');
+ const [cropMode, setCropMode] = useState<'center' | 'letterbox'>('center');
+
+ // Speed state
+ const [speedFactor, setSpeedFactor] = useState(1.0);
+
+ // Resolution state
+ const [targetResolution, setTargetResolution] = useState('720p');
+ const [maintainAspect, setMaintainAspect] = useState(true);
+
+ // Compression state
+ const [targetSizeMb, setTargetSizeMb] = useState(null);
+ const [compressQuality, setCompressQuality] = useState('medium');
+
+ // Cost hint (FFmpeg operations are free)
+ const costHint = useMemo(() => {
+ if (!videoFile) return 'Upload a video to transform';
+ return 'Free (FFmpeg processing)';
+ }, [videoFile]);
+
+ const canTransform = useMemo(() => {
+ if (!videoFile) return false;
+
+ // Validate based on transform type
+ switch (transformType) {
+ case 'format':
+ return !!outputFormat;
+ case 'aspect':
+ return !!targetAspect;
+ case 'speed':
+ return speedFactor > 0 && speedFactor <= 4.0;
+ case 'resolution':
+ return !!targetResolution;
+ case 'compress':
+ return true; // Always valid
+ default:
+ return false;
+ }
+ }, [videoFile, transformType, outputFormat, targetAspect, speedFactor, targetResolution]);
+
+ const handleVideoSelect = useCallback((file: File | null) => {
+ setVideoFile(file);
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+
+ // Create preview URL
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setVideoPreview(e.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ } else {
+ setVideoPreview(null);
+ }
+ }, []);
+
+ // Update codec based on format
+ const handleFormatChange = useCallback((format: OutputFormat) => {
+ setOutputFormat(format);
+ // Auto-select appropriate codec
+ if (format === 'webm') {
+ setCodec('libvpx-vp9');
+ setAudioCodec('libopus');
+ } else if (format === 'gif') {
+ setCodec('');
+ setAudioCodec('');
+ } else {
+ setCodec('libx264');
+ setAudioCodec('aac');
+ }
+ }, []);
+
+ return {
+ // State
+ videoFile,
+ videoPreview,
+ transformType,
+ outputFormat,
+ codec,
+ quality,
+ audioCodec,
+ targetAspect,
+ cropMode,
+ speedFactor,
+ targetResolution,
+ maintainAspect,
+ targetSizeMb,
+ compressQuality,
+ // Setters
+ setVideoFile: handleVideoSelect,
+ setTransformType,
+ setOutputFormat: handleFormatChange,
+ setCodec,
+ setQuality,
+ setAudioCodec,
+ setTargetAspect,
+ setCropMode,
+ setSpeedFactor,
+ setTargetResolution,
+ setMaintainAspect,
+ setTargetSizeMb,
+ setCompressQuality,
+ // Computed
+ canTransform,
+ costHint,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/TransformVideo/index.ts b/frontend/src/components/VideoStudio/modules/TransformVideo/index.ts
new file mode 100644
index 00000000..21c12578
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/TransformVideo/index.ts
@@ -0,0 +1,2 @@
+export { TransformVideo } from './TransformVideo';
+export { default } from './TransformVideo';
diff --git a/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/VideoBackgroundRemover.tsx b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/VideoBackgroundRemover.tsx
new file mode 100644
index 00000000..d5f72492
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/VideoBackgroundRemover.tsx
@@ -0,0 +1,318 @@
+import React from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress, LinearProgress, Alert, Paper, Chip } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useVideoBackgroundRemover } from './hooks/useVideoBackgroundRemover';
+import { VideoUpload, BackgroundImageUpload } from './components';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import ErrorIcon from '@mui/icons-material/Error';
+import WallpaperIcon from '@mui/icons-material/Wallpaper';
+
+const VideoBackgroundRemover: React.FC = () => {
+ const {
+ videoFile,
+ videoPreview,
+ backgroundImageFile,
+ backgroundImagePreview,
+ removing,
+ progress,
+ error,
+ result,
+ setVideoFile,
+ setBackgroundImageFile,
+ canRemove,
+ costHint,
+ removeBackground,
+ reset,
+ } = useVideoBackgroundRemover();
+
+ return (
+
+
+ {/* Left Panel - Upload & Settings */}
+
+
+
+
+
+
+
+
+
+ Estimated Cost:
+
+
+
+
+ Pricing: $0.01/second (min $0.05 for ≤5s, max $6.00 for 600s)
+
+
+ Minimum: $0.05 | Maximum: $6.00 (10 minutes / 600 seconds)
+
+
+
+
+ : }
+ onClick={removeBackground}
+ disabled={!canRemove || removing}
+ sx={{
+ py: 1.5,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ '&:disabled': {
+ backgroundColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {removing ? 'Processing...' : backgroundImageFile ? 'Replace Background' : 'Remove Background'}
+
+
+
+ {removing && (
+
+
+
+ Processing video... This may take a few minutes...
+
+
+
+
+ )}
+
+ {error && (
+ {}} icon={}>
+ {error}
+
+ )}
+
+ {result && (
+ }
+ action={
+
+ }
+ >
+ Background {result.has_background_replacement ? 'replaced' : 'removed'} successfully! Cost: ${result.cost.toFixed(4)}
+
+ )}
+
+
+
+ {/* Right Panel - Preview & Results */}
+
+
+ {result ? (
+ // Result view
+
+
+ Processed Video
+
+
+
+
+
+ {result.has_background_replacement ? 'Background Replaced' : 'Background Removed'}
+
+
+
+
+
+
+
+
+
+ ) : videoPreview ? (
+ // Original video preview
+
+
+ Original Video Preview
+
+
+
+
+
+ Upload a video and optionally add a background image to get started
+
+
+
+
+ ) : (
+
+
+ Upload a video to see preview
+
+
+ Your processed video will appear here
+
+
+ )}
+
+ {/* Info Box */}
+
+
+ About Background Removal
+
+
+ WaveSpeed Video Background Remover provides:
+
+
+
+ Automatic background detection and removal
+
+
+ Custom background replacement with your own images
+
+
+ Transparent background support for further editing
+
+
+ Production-ready quality with high-quality edge detection
+
+
+
+ Tips for Best Results:
+
+
+
+ Use videos with clear subject-background separation
+
+
+ Ensure adequate lighting for better edge detection
+
+
+ Use high-resolution images for replacement backgrounds
+
+
+ Best results with landscape videos (16:9 ratio)
+
+
+
+
+
+
+
+ );
+};
+
+export { VideoBackgroundRemover };
+export default VideoBackgroundRemover;
diff --git a/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/BackgroundImageUpload.tsx b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/BackgroundImageUpload.tsx
new file mode 100644
index 00000000..9a4608c9
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/BackgroundImageUpload.tsx
@@ -0,0 +1,134 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack, Chip } from '@mui/material';
+import ImageIcon from '@mui/icons-material/Image';
+import CloseIcon from '@mui/icons-material/Close';
+
+interface BackgroundImageUploadProps {
+ imagePreview: string | null;
+ onImageSelect: (file: File | null) => void;
+}
+
+export const BackgroundImageUpload: React.FC = ({
+ imagePreview,
+ onImageSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate image file
+ if (!file.type.startsWith('image/')) {
+ alert('Please select an image file');
+ return;
+ }
+ if (file.size > 10 * 1024 * 1024) {
+ alert('Image file must be less than 10MB');
+ return;
+ }
+ onImageSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onImageSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+
+ Background Image (Optional)
+
+
+
+
+ {imagePreview ? (
+
+
+ }
+ sx={{
+ position: 'absolute',
+ top: 8,
+ right: 8,
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
+ }}
+ >
+ Remove
+
+
+ ) : (
+
+
+
+
+ Click to upload background image
+
+
+ JPG, PNG up to 10MB
+
+
+ Leave empty to remove background (transparent)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/VideoUpload.tsx b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/VideoUpload.tsx
new file mode 100644
index 00000000..ab02562c
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/VideoUpload.tsx
@@ -0,0 +1,125 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import VideocamIcon from '@mui/icons-material/Videocam';
+
+interface VideoUploadProps {
+ videoPreview: string | null;
+ onVideoSelect: (file: File | null) => void;
+}
+
+export const VideoUpload: React.FC = ({
+ videoPreview,
+ onVideoSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+ onVideoSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onVideoSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Source Video
+
+
+ {videoPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload video
+
+
+ MP4, WebM up to 500MB (max 10 minutes)
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/index.ts b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/index.ts
new file mode 100644
index 00000000..96ba5de0
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/components/index.ts
@@ -0,0 +1,2 @@
+export { VideoUpload } from './VideoUpload';
+export { BackgroundImageUpload } from './BackgroundImageUpload';
diff --git a/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/hooks/useVideoBackgroundRemover.ts b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/hooks/useVideoBackgroundRemover.ts
new file mode 100644
index 00000000..1d94f13e
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/hooks/useVideoBackgroundRemover.ts
@@ -0,0 +1,196 @@
+import { useState, useMemo, useEffect } from 'react';
+import { aiApiClient } from '../../../../../api/client';
+
+export const useVideoBackgroundRemover = () => {
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoPreview, setVideoPreview] = useState(null);
+ const [backgroundImageFile, setBackgroundImageFile] = useState(null);
+ const [backgroundImagePreview, setBackgroundImagePreview] = useState(null);
+ const [removing, setRemoving] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [error, setError] = useState(null);
+ const [result, setResult] = useState<{ video_url: string; cost: number; has_background_replacement: boolean } | null>(null);
+ const [estimatedDuration, setEstimatedDuration] = useState(10.0);
+ const [costEstimate, setCostEstimate] = useState(null);
+
+ // Update previews when files change
+ useEffect(() => {
+ if (videoFile) {
+ const url = URL.createObjectURL(videoFile);
+ setVideoPreview(url);
+
+ // Rough estimate: 1MB ≈ 1 second at 1080p
+ const estimated = Math.max(5, videoFile.size / (1024 * 1024));
+ setEstimatedDuration(estimated);
+
+ return () => URL.revokeObjectURL(url);
+ } else {
+ setVideoPreview(null);
+ setEstimatedDuration(10.0);
+ }
+ }, [videoFile]);
+
+ useEffect(() => {
+ if (backgroundImageFile) {
+ const url = URL.createObjectURL(backgroundImageFile);
+ setBackgroundImagePreview(url);
+ return () => URL.revokeObjectURL(url);
+ } else {
+ setBackgroundImagePreview(null);
+ }
+ }, [backgroundImageFile]);
+
+ // Fetch cost estimate when duration changes
+ useEffect(() => {
+ const fetchCostEstimate = async () => {
+ if (!videoFile || estimatedDuration < 5) {
+ setCostEstimate(null);
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('estimated_duration', estimatedDuration.toString());
+
+ const response = await aiApiClient.post('/api/video-studio/video-background-remover/estimate-cost', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ if (response.data.estimated_cost) {
+ setCostEstimate(response.data.estimated_cost);
+ }
+ } catch (err) {
+ console.error('Failed to fetch cost estimate:', err);
+ // Fallback to client-side calculation
+ // Pricing: $0.01/second, min $0.05 for ≤5s, max $6.00 for 600s
+ const costPerSecond = 0.01;
+ let estimatedCost = estimatedDuration * costPerSecond;
+ if (estimatedDuration <= 5.0) {
+ estimatedCost = 0.05; // Minimum charge
+ } else if (estimatedDuration >= 600.0) {
+ estimatedCost = 6.00; // Maximum charge
+ }
+ setCostEstimate(estimatedCost);
+ }
+ };
+
+ fetchCostEstimate();
+ }, [videoFile, estimatedDuration]);
+
+ const canRemove = useMemo(() => {
+ return videoFile !== null;
+ }, [videoFile]);
+
+ const costHint = useMemo(() => {
+ if (!videoFile) return 'Upload a video to see cost estimate';
+
+ if (costEstimate !== null) {
+ return `Est. ~$${costEstimate.toFixed(2)} (${estimatedDuration.toFixed(0)}s)`;
+ }
+
+ // Fallback calculation
+ // Pricing: $0.01/second, min $0.05 for ≤5s, max $6.00 for 600s
+ const costPerSecond = 0.01;
+ let estimatedCost = estimatedDuration * costPerSecond;
+ if (estimatedDuration <= 5.0) {
+ estimatedCost = 0.05; // Minimum charge
+ } else if (estimatedDuration >= 600.0) {
+ estimatedCost = 6.00; // Maximum charge
+ }
+ return `Est. ~$${estimatedCost.toFixed(2)} (${estimatedDuration.toFixed(0)}s)`;
+ }, [videoFile, estimatedDuration, costEstimate]);
+
+ const removeBackground = async () => {
+ if (!videoFile) return;
+
+ setRemoving(true);
+ setError(null);
+ setResult(null);
+ setProgress(0);
+
+ try {
+ const formData = new FormData();
+ formData.append('video_file', videoFile);
+ if (backgroundImageFile) {
+ formData.append('background_image_file', backgroundImageFile);
+ }
+
+ // Submit background removal request
+ setProgress(10);
+ const response = await aiApiClient.post('/api/video-studio/video-background-remover', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.total) {
+ const uploadProgress = Math.round((progressEvent.loaded * 30) / progressEvent.total);
+ setProgress(uploadProgress);
+ }
+ },
+ timeout: 600000, // 10 minutes timeout
+ });
+
+ setProgress(40);
+
+ // Simulate progress updates
+ let simulatedProgress = 40;
+ const progressInterval = setInterval(() => {
+ simulatedProgress = Math.min(90, simulatedProgress + 5);
+ setProgress(simulatedProgress);
+ }, 2000);
+
+ try {
+ if (response.data.success) {
+ clearInterval(progressInterval);
+ setRemoving(false);
+ setResult(response.data);
+ setProgress(100);
+ } else {
+ clearInterval(progressInterval);
+ throw new Error(response.data.error || 'Background removal failed');
+ }
+ } catch (err) {
+ clearInterval(progressInterval);
+ throw err;
+ }
+ } catch (err: any) {
+ setRemoving(false);
+ setProgress(0);
+ setError(err.response?.data?.detail || err.message || 'Failed to remove background');
+ }
+ };
+
+ const reset = () => {
+ setRemoving(false);
+ setProgress(0);
+ setError(null);
+ setResult(null);
+ setVideoFile(null);
+ setBackgroundImageFile(null);
+ };
+
+ return {
+ // State
+ videoFile,
+ videoPreview,
+ backgroundImageFile,
+ backgroundImagePreview,
+ removing,
+ progress,
+ error,
+ result,
+ estimatedDuration,
+ costEstimate,
+ // Setters
+ setVideoFile,
+ setBackgroundImageFile,
+ // Computed
+ canRemove,
+ costHint,
+ // Actions
+ removeBackground,
+ reset,
+ };
+};
diff --git a/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/index.ts b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/index.ts
new file mode 100644
index 00000000..bc3c9cb7
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoBackgroundRemover/index.ts
@@ -0,0 +1,2 @@
+export { VideoBackgroundRemover } from './VideoBackgroundRemover';
+export { default } from './VideoBackgroundRemover';
diff --git a/frontend/src/components/VideoStudio/modules/VideoTranslate/VideoTranslate.tsx b/frontend/src/components/VideoStudio/modules/VideoTranslate/VideoTranslate.tsx
new file mode 100644
index 00000000..7bd72a90
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoTranslate/VideoTranslate.tsx
@@ -0,0 +1,246 @@
+import React from 'react';
+import { Grid, Box, Button, Typography, Stack, CircularProgress, LinearProgress, Alert, Paper } from '@mui/material';
+import { VideoStudioLayout } from '../../VideoStudioLayout';
+import { useVideoTranslate } from './hooks/useVideoTranslate';
+import { VideoUpload, LanguageSelector } from './components';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import ErrorIcon from '@mui/icons-material/Error';
+import TranslateIcon from '@mui/icons-material/Translate';
+
+const VideoTranslate: React.FC = () => {
+ const {
+ videoFile,
+ videoPreview,
+ outputLanguage,
+ translating,
+ progress,
+ error,
+ result,
+ supportedLanguages,
+ setVideoFile,
+ setOutputLanguage,
+ canTranslate,
+ costHint,
+ translateVideo,
+ reset,
+ } = useVideoTranslate();
+
+ return (
+
+
+ {/* Left Panel - Upload & Settings */}
+
+
+
+
+ {videoFile && (
+
+ )}
+
+
+ : }
+ onClick={translateVideo}
+ disabled={!canTranslate || translating}
+ sx={{
+ py: 1.5,
+ backgroundColor: '#3b82f6',
+ '&:hover': {
+ backgroundColor: '#2563eb',
+ },
+ '&:disabled': {
+ backgroundColor: '#cbd5e1',
+ color: '#94a3b8',
+ },
+ }}
+ >
+ {translating ? 'Translating Video...' : 'Translate Video'}
+
+
+
+ {videoFile && (
+
+
+ Cost: {costHint}
+
+
+ Pricing: $0.0375/second
+
+
+ )}
+
+ {translating && (
+
+
+ Progress: {progress}%
+
+
+
+ )}
+
+ {error && (
+ {}}>
+ {error}
+
+ )}
+
+
+
+ {/* Right Panel - Preview & Result */}
+
+
+ {result ? (
+
+
+
+
+ Translation Complete!
+
+
+
+
+
+
+
+
+
+ Target Language: {result.output_language}
+
+
+ Cost: ${result.cost.toFixed(4)}
+
+
+
+
+
+
+
+
+ ) : videoPreview ? (
+
+
+ Source Video Preview
+
+
+
+
+
+ ) : (
+
+
+
+ Upload a video to get started
+
+
+ Your translated video will appear here
+
+
+ )}
+
+
+
+
+ );
+};
+
+export { VideoTranslate };
+export default VideoTranslate;
diff --git a/frontend/src/components/VideoStudio/modules/VideoTranslate/components/LanguageSelector.tsx b/frontend/src/components/VideoStudio/modules/VideoTranslate/components/LanguageSelector.tsx
new file mode 100644
index 00000000..eecb63de
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoTranslate/components/LanguageSelector.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { Box, Paper, Stack, Typography, FormControl, InputLabel, Select, MenuItem, Autocomplete, TextField } from '@mui/material';
+import TranslateIcon from '@mui/icons-material/Translate';
+
+interface LanguageSelectorProps {
+ outputLanguage: string;
+ supportedLanguages: string[];
+ onLanguageChange: (language: string) => void;
+}
+
+export const LanguageSelector: React.FC = ({
+ outputLanguage,
+ supportedLanguages,
+ onLanguageChange,
+}) => {
+ return (
+
+
+
+
+ Target Language
+
+
+
+ {
+ if (newValue) {
+ onLanguageChange(newValue);
+ }
+ }}
+ options={supportedLanguages}
+ renderInput={(params) => (
+
+ )}
+ sx={{
+ '& .MuiAutocomplete-input': {
+ py: 1.5,
+ },
+ }}
+ />
+
+
+ Supports 70+ languages and 175+ dialects. The video will be translated with
+ lip-sync preservation.
+
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/VideoTranslate/components/VideoUpload.tsx b/frontend/src/components/VideoStudio/modules/VideoTranslate/components/VideoUpload.tsx
new file mode 100644
index 00000000..b757df62
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoTranslate/components/VideoUpload.tsx
@@ -0,0 +1,125 @@
+import React, { useRef } from 'react';
+import { Box, Button, Typography, Stack } from '@mui/material';
+import VideocamIcon from '@mui/icons-material/Videocam';
+
+interface VideoUploadProps {
+ videoPreview: string | null;
+ onVideoSelect: (file: File | null) => void;
+}
+
+export const VideoUpload: React.FC = ({
+ videoPreview,
+ onVideoSelect,
+}) => {
+ const fileInputRef = useRef(null);
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate video file
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a video file');
+ return;
+ }
+ if (file.size > 500 * 1024 * 1024) {
+ alert('Video file must be less than 500MB');
+ return;
+ }
+ onVideoSelect(file);
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleRemove = () => {
+ onVideoSelect(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+ Source Video
+
+
+ {videoPreview ? (
+
+
+
+
+ ) : (
+
+
+
+
+ Click to upload video
+
+
+ MP4, WebM up to 500MB
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/VideoStudio/modules/VideoTranslate/components/index.ts b/frontend/src/components/VideoStudio/modules/VideoTranslate/components/index.ts
new file mode 100644
index 00000000..0cbd4c5a
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoTranslate/components/index.ts
@@ -0,0 +1,2 @@
+export { VideoUpload } from './VideoUpload';
+export { LanguageSelector } from './LanguageSelector';
diff --git a/frontend/src/components/VideoStudio/modules/VideoTranslate/hooks/useVideoTranslate.ts b/frontend/src/components/VideoStudio/modules/VideoTranslate/hooks/useVideoTranslate.ts
new file mode 100644
index 00000000..c135044f
--- /dev/null
+++ b/frontend/src/components/VideoStudio/modules/VideoTranslate/hooks/useVideoTranslate.ts
@@ -0,0 +1,146 @@
+import { useState, useMemo, useEffect } from 'react';
+import { aiApiClient } from '../../../../../api/client';
+
+export const useVideoTranslate = () => {
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoPreview, setVideoPreview] = useState(null);
+ const [outputLanguage, setOutputLanguage] = useState('English');
+ const [translating, setTranslating] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [error, setError] = useState