generated from fahricansecer/boilerplate-be
361 lines
11 KiB
TypeScript
361 lines
11 KiB
TypeScript
// 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<string, {
|
|
duration: VideoDuration;
|
|
aspectRatio: VideoAspectRatio;
|
|
style: VideoStyle;
|
|
}> = {
|
|
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<VideoStyle, string> = {
|
|
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<GeneratedVideo> {
|
|
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<GeneratedVideo> {
|
|
return this.generateVideo({
|
|
prompt: `Animate image with ${motion} motion`,
|
|
duration,
|
|
motion,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate multi-scene video
|
|
*/
|
|
async generateMultiSceneVideo(
|
|
scenes: VideoScene[],
|
|
audio?: AudioOptions,
|
|
): Promise<GeneratedVideo> {
|
|
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<MotionType, string> = {
|
|
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<VideoAspectRatio, VideoResolution> = {
|
|
'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<string, string[]> = {
|
|
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'];
|
|
}
|
|
}
|