Skip to main content
Glama
video-assembler.js10.3 kB
// Video Assembly System for Puppet Productions // Combines scene images with audio to create final video production import fs from 'fs/promises'; import path from 'path'; export class VideoAssembler { constructor(outputPath = './production_output/videos') { this.outputPath = outputPath; this.ensureOutputDirectory(); } async ensureOutputDirectory() { try { await fs.mkdir(this.outputPath, { recursive: true }); } catch (error) { console.log('Video output directory exists'); } } // Create video production manifest for external video tools async createVideoManifest(characterName, sceneImages, audioFiles, options = {}) { console.log(`🎬 Creating video manifest for ${characterName}...`); try { const scenes = []; const defaultDuration = 3; // seconds per scene if no audio for (let i = 0; i < sceneImages.length; i++) { const image = sceneImages[i]; const audio = audioFiles.find(a => a.scene_number === image.shot_number); scenes.push({ scene_number: i + 1, image_path: image.image_url, audio_path: audio?.audio_path || null, duration: audio?.duration_estimate || defaultDuration, emotion: image.emotion, camera_angle: image.camera_angle, dialogue: audio?.dialogue || null, transition: options.transition || 'fade', timing: { start_time: scenes.reduce((sum, s) => sum + s.duration, 0), end_time: scenes.reduce((sum, s) => sum + s.duration, 0) + (audio?.duration_estimate || defaultDuration) } }); } const manifest = { character_name: characterName, production_info: { total_scenes: scenes.length, total_duration: scenes.reduce((sum, scene) => sum + scene.duration, 0), created_at: new Date().toISOString(), format: options.format || 'mp4', resolution: options.resolution || '1920x1080', fps: options.fps || 30 }, scenes: scenes, rendering_instructions: { image_duration_per_scene: defaultDuration, audio_sync: true, transition_type: options.transition || 'fade', background_music: options.backgroundMusic || null, end_credits: options.endCredits || false }, export_settings: { output_filename: `${characterName}_production_${Date.now()}.mp4`, output_path: this.outputPath, quality: options.quality || 'high' } }; // Save manifest const manifestPath = path.join(this.outputPath, `${characterName}_video_manifest.json`); await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); // Generate FFmpeg command const ffmpegCmd = this.generateFFmpegCommand(manifest); manifest.ffmpeg_command = ffmpegCmd.command; manifest.output_file = ffmpegCmd.output_file; // Save updated manifest with FFmpeg command await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); console.log(`✅ Video manifest created: ${manifestPath}`); console.log(`📊 Total duration: ${manifest.production_info.total_duration} seconds`); console.log(`🎬 Total scenes: ${manifest.production_info.total_scenes}`); console.log(`🎯 FFmpeg command generated`); return { success: true, manifest_path: manifestPath, manifest: manifest, total_duration: manifest.production_info.total_duration, total_scenes: manifest.production_info.total_scenes, ffmpeg_command: ffmpegCmd.command, output_file: ffmpegCmd.output_file, ready_for_video_rendering: true }; } catch (error) { console.error('❌ Video manifest creation failed:', error.message); return { success: false, error: error.message }; } } // Generate FFmpeg command for video assembly (for external use) generateFFmpegCommand(manifest) { const scenes = manifest.scenes; const outputFile = manifest.export_settings.output_filename; // Create input files list let inputFiles = []; let filterComplex = []; let inputIndex = 0; scenes.forEach((scene, index) => { // Add image input inputFiles.push(`-loop 1 -t ${scene.duration} -i "${scene.image_path}"`); // Add audio input if available if (scene.audio_path) { inputFiles.push(`-i "${scene.audio_path}"`); // Create video with synced audio filterComplex.push( `[${inputIndex}:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30[v${index}];` + `[${inputIndex + 1}:a]volume=1.0[a${index}]` ); inputIndex += 2; } else { // Video only filterComplex.push( `[${inputIndex}:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30[v${index}]` ); inputIndex += 1; } }); // Concatenate all scenes const videoInputs = scenes.map((_, i) => `[v${i}]`).join(''); const audioInputs = scenes.filter(s => s.audio_path).map((_, i) => `[a${i}]`).join(''); if (audioInputs) { filterComplex.push(`${videoInputs}concat=n=${scenes.length}:v=1:a=0[outv]`); filterComplex.push(`${audioInputs}concat=n=${scenes.filter(s => s.audio_path).length}:v=0:a=1[outa]`); } else { filterComplex.push(`${videoInputs}concat=n=${scenes.length}:v=1:a=0[outv]`); } const command = [ 'ffmpeg', ...inputFiles, '-filter_complex', `"${filterComplex.join('')}"`, audioInputs ? '-map "[outv]" -map "[outa]"' : '-map "[outv]"', '-c:v libx264 -preset medium -crf 23', audioInputs ? '-c:a aac -b:a 192k' : '', `-r ${manifest.production_info.fps}`, `"${path.join(manifest.export_settings.output_path, outputFile)}"` ].filter(Boolean).join(' '); return { command: command, output_file: path.join(manifest.export_settings.output_path, outputFile), estimated_duration: manifest.production_info.total_duration }; } // Create simple slideshow video (basic implementation) async createSlideshowVideo(characterName, sceneImages, options = {}) { console.log(`🎞️ Creating slideshow video for ${characterName}...`); try { const duration = options.imageDuration || 3; // seconds per image const transition = options.transition || 'fade'; const videoInstructions = { type: 'slideshow', character_name: characterName, images: sceneImages.map(img => ({ path: img.image_url, duration: duration, emotion: img.emotion, scene_number: img.shot_number })), settings: { total_duration: sceneImages.length * duration, transition_type: transition, resolution: options.resolution || '1920x1080', fps: options.fps || 30 }, output_filename: `${characterName}_slideshow_${Date.now()}.mp4` }; const instructionsPath = path.join(this.outputPath, `${characterName}_slideshow_instructions.json`); await fs.writeFile(instructionsPath, JSON.stringify(videoInstructions, null, 2)); console.log(`✅ Slideshow instructions created: ${instructionsPath}`); return { success: true, instructions_path: instructionsPath, estimated_duration: videoInstructions.settings.total_duration, total_images: sceneImages.length, instructions: videoInstructions, note: 'Use external video editing software or FFmpeg to render the final video' }; } catch (error) { console.error('❌ Slideshow creation failed:', error.message); return { success: false, error: error.message }; } } // Get video production summary async getProductionSummary(characterName) { try { const files = await fs.readdir(this.outputPath); const characterFiles = files.filter(f => f.includes(characterName)); const manifests = characterFiles.filter(f => f.includes('manifest')); const instructions = characterFiles.filter(f => f.includes('instructions')); const videos = characterFiles.filter(f => f.endsWith('.mp4')); return { character_name: characterName, manifests: manifests.length, instruction_files: instructions.length, rendered_videos: videos.length, files: characterFiles, output_directory: this.outputPath }; } catch (error) { return { error: error.message, output_directory: this.outputPath }; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bermingham85/mcp-puppet-pipeline'

If you have feedback or need assistance with the MCP directory API, please join our Discord server