// Veo Video Service - AI-powered video generation // Path: src/modules/visual-generation/services/veo-video.service.ts import { Injectable, Logger } from '@nestjs/common'; export interface VideoGenerationRequest { prompt: string; duration?: VideoDuration; aspectRatio?: VideoAspectRatio; style?: VideoStyle; motion?: MotionType; audio?: AudioOptions; platform?: string; } export type VideoDuration = '5s' | '10s' | '15s' | '30s' | '60s'; export type VideoAspectRatio = '1:1' | '9:16' | '16:9' | '4:5'; export type VideoStyle = | 'cinematic' | 'documentary' | 'animated' | 'motion_graphics' | 'time_lapse' | 'slow_motion' | 'hyperlapse' | 'stopmotion' | 'vlog' | 'corporate'; export type MotionType = | 'static' | 'pan_left' | 'pan_right' | 'zoom_in' | 'zoom_out' | 'dolly_in' | 'dolly_out' | 'orbit' | 'tilt_up' | 'tilt_down'; export interface AudioOptions { music?: MusicStyle; voiceover?: boolean; soundEffects?: boolean; } export type MusicStyle = | 'upbeat' | 'calm' | 'dramatic' | 'corporate' | 'inspirational' | 'electronic' | 'acoustic' | 'none'; export interface GeneratedVideo { id: string; prompt: string; url: string; thumbnailUrl: string; duration: VideoDuration; aspectRatio: VideoAspectRatio; style: VideoStyle; resolution: VideoResolution; fps: number; fileSize: number; // in bytes metadata: VideoMetadata; status: VideoStatus; createdAt: Date; } export interface VideoResolution { width: number; height: number; quality: '720p' | '1080p' | '4k'; } export interface VideoMetadata { model: string; generationTime: number; frames: number; hasAudio: boolean; audioTrack?: string; tags: string[]; } export type VideoStatus = 'queued' | 'processing' | 'completed' | 'failed'; export interface VideoScene { order: number; duration: string; prompt: string; motion?: MotionType; transition?: TransitionType; } export type TransitionType = | 'cut' | 'fade' | 'dissolve' | 'wipe' | 'zoom' | 'slide'; @Injectable() export class VeoVideoService { private readonly logger = new Logger(VeoVideoService.name); // Platform-specific video defaults private readonly platformDefaults: Record = { tiktok: { duration: '15s', aspectRatio: '9:16', style: 'vlog' }, instagram_reel: { duration: '15s', aspectRatio: '9:16', style: 'cinematic' }, instagram_story: { duration: '10s', aspectRatio: '9:16', style: 'motion_graphics' }, youtube_short: { duration: '30s', aspectRatio: '9:16', style: 'vlog' }, youtube: { duration: '60s', aspectRatio: '16:9', style: 'cinematic' }, linkedin: { duration: '30s', aspectRatio: '16:9', style: 'corporate' }, twitter: { duration: '15s', aspectRatio: '16:9', style: 'motion_graphics' }, }; // Style prompt modifiers private readonly styleModifiers: Record = { cinematic: 'cinematic quality, film-like, dramatic lighting, professional', documentary: 'documentary style, authentic, natural lighting, storytelling', animated: 'animated, cartoon style, vibrant colors, fun', motion_graphics: 'motion graphics, smooth transitions, modern design, clean', time_lapse: 'time-lapse, accelerated motion, passage of time', slow_motion: 'slow motion, detailed, dramatic, fluid movement', hyperlapse: 'hyperlapse, dynamic movement, urban, travel', stopmotion: 'stop motion animation, creative, artistic', vlog: 'vlog style, personal, casual, authentic', corporate: 'corporate style, professional, clean, business-oriented', }; /** * Generate a video */ async generateVideo(request: VideoGenerationRequest): Promise { const { prompt, duration = '15s', aspectRatio = '9:16', style = 'cinematic', motion = 'static', audio, platform, } = request; // Apply platform defaults let finalDuration = duration; let finalRatio = aspectRatio; let finalStyle = style; if (platform && this.platformDefaults[platform]) { const defaults = this.platformDefaults[platform]; finalDuration = defaults.duration; finalRatio = defaults.aspectRatio; finalStyle = defaults.style; } // Get resolution based on aspect ratio const resolution = this.getResolution(finalRatio); const frames = this.calculateFrames(finalDuration); // Mock generation (in production, this would call Veo API) const video: GeneratedVideo = { id: `vid-${Date.now()}`, prompt, url: `https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4`, thumbnailUrl: `https://storage.example.com/videos/${Date.now()}_thumb.jpg`, duration: finalDuration, aspectRatio: finalRatio, style: finalStyle, resolution, fps: 30, fileSize: this.estimateFileSize(finalDuration, resolution), metadata: { model: 'veo-2.0', generationTime: 15000 + Math.random() * 10000, frames, hasAudio: !!audio?.music || !!audio?.voiceover, audioTrack: audio?.music, tags: this.extractTags(prompt), }, status: 'completed', createdAt: new Date(), }; this.logger.log(`Generated video: ${video.id}`); return video; } /** * Generate video from image (image-to-video) */ async imageToVideo( imageUrl: string, motion: MotionType = 'zoom_in', duration: VideoDuration = '5s', ): Promise { return this.generateVideo({ prompt: `Animate image with ${motion} motion`, duration, motion, }); } /** * Generate multi-scene video */ async generateMultiSceneVideo( scenes: VideoScene[], audio?: AudioOptions, ): Promise { const totalDuration = scenes.reduce((sum, scene) => { const seconds = parseInt(scene.duration.replace('s', '')); return sum + seconds; }, 0); const combinedPrompt = scenes.map((s) => s.prompt).join(' | '); return this.generateVideo({ prompt: combinedPrompt, duration: `${totalDuration}s` as VideoDuration, audio, }); } /** * Get video generation status */ async getVideoStatus(videoId: string): Promise<{ status: VideoStatus; progress: number; estimatedRemaining?: number; }> { // Mock status check return { status: 'completed', progress: 100, }; } /** * Get platform recommendations */ getPlatformRecommendations(platform: string): { duration: VideoDuration; aspectRatio: VideoAspectRatio; style: VideoStyle; tips: string[]; } | null { const defaults = this.platformDefaults[platform]; if (!defaults) return null; return { ...defaults, tips: this.getPlatformTips(platform), }; } /** * Get available styles */ getStyles(): { style: VideoStyle; description: string }[] { return Object.entries(this.styleModifiers).map(([style, description]) => ({ style: style as VideoStyle, description: description.split(',')[0], })); } /** * Get available motions */ getMotions(): { motion: MotionType; description: string }[] { const descriptions: Record = { static: 'No camera movement', pan_left: 'Camera pans from right to left', pan_right: 'Camera pans from left to right', zoom_in: 'Camera zooms in towards subject', zoom_out: 'Camera zooms out from subject', dolly_in: 'Camera moves forward', dolly_out: 'Camera moves backward', orbit: 'Camera orbits around subject', tilt_up: 'Camera tilts upward', tilt_down: 'Camera tilts downward', }; return Object.entries(descriptions).map(([motion, description]) => ({ motion: motion as MotionType, description, })); } // Private helper methods private getResolution(aspectRatio: VideoAspectRatio): VideoResolution { const resolutions: Record = { '1:1': { width: 1080, height: 1080, quality: '1080p' }, '9:16': { width: 1080, height: 1920, quality: '1080p' }, '16:9': { width: 1920, height: 1080, quality: '1080p' }, '4:5': { width: 1080, height: 1350, quality: '1080p' }, }; return resolutions[aspectRatio]; } private calculateFrames(duration: VideoDuration): number { const seconds = parseInt(duration.replace('s', '')); return seconds * 30; // 30 fps } private estimateFileSize(duration: VideoDuration, resolution: VideoResolution): number { const seconds = parseInt(duration.replace('s', '')); const baseSize = resolution.quality === '4k' ? 50 : resolution.quality === '1080p' ? 20 : 10; return seconds * baseSize * 1024 * 1024; // MB to bytes } private extractTags(prompt: string): string[] { const words = prompt.toLowerCase().split(/\s+/); return words.filter((w) => w.length > 3).slice(0, 10); } private getPlatformTips(platform: string): string[] { const tips: Record = { tiktok: [ 'Hook viewers in first 3 seconds', 'Use trending sounds', 'Include text overlays', 'End with a loop-friendly transition', ], instagram_reel: [ 'Start with an attention-grabbing moment', 'Use vertical format fully', 'Add captions for sound-off viewing', 'Keep it under 30 seconds for best engagement', ], youtube_short: [ 'Vertical format required', 'First 2 seconds are critical', 'Add a clear CTA', 'Hashtags help discovery', ], linkedin: [ 'Keep professional tone', 'Add captions (most watch on mute)', 'Lead with value', 'Optimal length: 30-90 seconds', ], }; return tips[platform] || ['Optimize for the platform guidelines']; } }