Files
Content-Hunter_BE/src/modules/visual-generation/services/veo-video.service.ts
Harun CAN dee6e29cfd
Some checks failed
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled
main
2026-03-14 14:01:01 +03:00

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'];
}
}