| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>Pro Video Prompt Workspace</title> |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.460.0/umd/lucide.min.js"></script> |
| <style> |
| |
| .allow-long-press { |
| -webkit-touch-callout: default !important; |
| user-select: auto !important; |
| } |
| </style> |
| </head> |
| <body class="min-h-screen bg-neutral-950 text-neutral-100 p-4 md:p-8 font-sans pb-24"> |
| <div class="max-w-5xl mx-auto space-y-6"> |
| |
| |
| <header class="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-neutral-800 pb-4"> |
| <div class="flex items-center gap-3"> |
| <i data-lucide="clapperboard" class="w-8 h-8 text-blue-400"></i> |
| <div> |
| <h1 class="text-2xl font-bold tracking-tight text-white">Google Flow & Veo 3.1 Workspace</h1> |
| <p class="text-neutral-400 text-sm">Pro-Level Prompt Engineering & Asset Generator</p> |
| </div> |
| </div> |
| <div class="flex flex-col gap-1 w-full md:w-72"> |
| <label class="text-xs font-medium text-neutral-400">Gemini API Key (Required for hosting)</label> |
| <input |
| type="password" |
| id="customApiKey" |
| placeholder="Paste key here..." |
| class="w-full bg-black border border-neutral-700 rounded p-2 text-sm text-neutral-200 outline-none focus:border-blue-500" |
| /> |
| </div> |
| </header> |
|
|
| |
| <div class="grid md:grid-cols-3 gap-6"> |
| <div class="md:col-span-2 space-y-4 bg-neutral-900 p-5 rounded-xl border border-neutral-800 shadow-xl"> |
| <div> |
| <label class="block text-sm font-medium text-neutral-300 mb-2">Scene Concept</label> |
| <textarea |
| id="conceptInput" |
| placeholder="e.g., A macro shot of a dewdrop falling from a lush green fern leaf in a dense rainforest..." |
| class="w-full h-28 bg-black border border-neutral-700 rounded-lg p-3 text-neutral-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none resize-none" |
| ></textarea> |
| </div> |
| |
| <div class="flex flex-wrap items-center gap-6"> |
| <div> |
| <label class="block text-sm font-medium text-neutral-300 mb-2">Total Segments (8s each)</label> |
| <div class="flex items-center gap-3"> |
| <input |
| type="range" |
| id="segmentsInput" |
| min="1" max="5" |
| value="1" |
| class="w-32 accent-blue-500" |
| /> |
| <span id="segmentsDisplay" class="font-mono bg-black border border-neutral-800 px-2 py-1 rounded text-sm text-blue-400"> |
| 1 (8s total) |
| </span> |
| </div> |
| </div> |
|
|
| <label class="flex items-center gap-2 cursor-pointer mt-4 md:mt-0"> |
| <input |
| type="checkbox" |
| id="realisticToggle" |
| checked |
| class="w-4 h-4 rounded text-blue-500 bg-black border-neutral-700 focus:ring-blue-500" |
| /> |
| <span class="text-sm font-medium text-neutral-300">Apply True-to-Life Tags</span> |
| </label> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4 pt-4 border-t border-neutral-800"> |
| <div> |
| <label class="block text-xs font-medium text-neutral-400 mb-1">Bookend Start Motion</label> |
| <input type="text" id="startMotion" value="Slow tracking shot moving forward" class="w-full bg-black border border-neutral-700 rounded p-2 text-sm text-neutral-200 outline-none focus:border-blue-500" /> |
| </div> |
| <div> |
| <label class="block text-xs font-medium text-neutral-400 mb-1">Bookend End Motion</label> |
| <input type="text" id="endMotion" value="Camera settles into a static, locked-off composition" class="w-full bg-black border border-neutral-700 rounded p-2 text-sm text-neutral-200 outline-none focus:border-blue-500" /> |
| </div> |
| </div> |
| |
| <div id="tagsContainer" class="space-y-3 mt-4 pt-4 border-t border-neutral-800"> |
| <div> |
| <label class="block text-xs font-medium text-neutral-400 mb-1">True-to-Life Meta Tags</label> |
| <textarea id="metaTags" class="w-full h-16 bg-black border border-neutral-700 rounded p-2 text-xs text-neutral-300 outline-none focus:border-blue-500 resize-none">8k resolution, photorealistic, cinematic lighting, national geographic style, highly detailed, sharp focus, true-to-life, nature photography, real life, masterpiece, pro-level cinematography</textarea> |
| </div> |
| <div> |
| <label class="block text-xs font-medium text-neutral-400 mb-1">Standard Negative Prompts</label> |
| <textarea id="negativeTags" class="w-full h-16 bg-black border border-neutral-700 rounded p-2 text-xs text-neutral-300 outline-none focus:border-blue-500 resize-none">cartoon, 3d render, animation, low quality, blurry, distorted, deformed, artificial lighting, text, watermark, CGI, painting, illustration</textarea> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="space-y-4 bg-neutral-900 p-5 rounded-xl border border-neutral-800 shadow-xl"> |
| <h3 class="font-semibold text-lg flex items-center gap-2 border-b border-neutral-800 pb-2 text-white"> |
| <i data-lucide="settings" class="w-5 h-5 text-purple-400"></i> Pro Workflow |
| </h3> |
| <ul class="space-y-3 text-sm text-neutral-300"> |
| <li class="flex items-start gap-2"><i data-lucide="film" class="w-4 h-4 text-emerald-400 mt-0.5 shrink-0"></i> Apply Bookend Strategy</li> |
| <li class="flex items-start gap-2"><i data-lucide="video" class="w-4 h-4 text-emerald-400 mt-0.5 shrink-0"></i> Format into 8s outputs</li> |
| <li class="flex items-start gap-2"><i data-lucide="image" class="w-4 h-4 text-blue-400 mt-0.5 shrink-0"></i> Generate First Frame per segment</li> |
| <li class="flex items-start gap-2"><i data-lucide="image" class="w-4 h-4 text-blue-400 mt-0.5 shrink-0"></i> Generate Last Frame per segment</li> |
| <li class="flex items-start gap-2"><i data-lucide="image" class="w-4 h-4 text-blue-400 mt-0.5 shrink-0"></i> Generate Ingredients</li> |
| </ul> |
| </div> |
| </div> |
|
|
| |
| <div class="space-y-6 pt-4"> |
| <h2 class="text-xl font-bold flex items-center gap-2 text-white"> |
| <i data-lucide="video" class="w-6 h-6 text-blue-400"></i> Generated Workspace Blocks |
| </h2> |
| <div id="segmentsContainer" class="space-y-6"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="exportModal" class="hidden fixed inset-0 z-50 flex-col items-center justify-center bg-black/95 p-4 backdrop-blur-md transition-all"> |
| <div class="bg-neutral-900 border border-neutral-800 p-5 rounded-2xl max-w-sm w-full shadow-2xl"> |
| <div class="flex justify-between items-center mb-4 border-b border-neutral-800 pb-3"> |
| <h3 class="font-bold text-white flex items-center gap-2"> |
| <i data-lucide="download-cloud" class="w-5 h-5 text-blue-400"></i> Export Hub |
| </h3> |
| <button data-action="close-export" class="text-neutral-400 hover:text-white p-1"> |
| <i data-lucide="x" class="w-5 h-5"></i> |
| </button> |
| </div> |
|
|
| <div class="bg-black rounded-lg p-2 mb-4"> |
| <img id="exportModalImage" src="" alt="Asset to export" class="w-full h-auto rounded pointer-events-auto select-auto allow-long-press" /> |
| <p class="text-center text-[10px] text-neutral-500 mt-2"> |
| 💡 Try long-pressing the image above to save |
| </p> |
| </div> |
|
|
| <div class="space-y-3"> |
| <button data-action="export-copy" class="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-500 text-white px-4 py-3 rounded-xl font-medium transition-colors shadow-lg"> |
| <i data-lucide="clipboard-copy" class="w-4 h-4"></i> Copy Image to Clipboard |
| </button> |
| <button data-action="export-share" class="w-full flex items-center justify-center gap-2 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 text-white px-4 py-3 rounded-xl font-medium transition-colors"> |
| <i data-lucide="share-2" class="w-4 h-4"></i> Force Native Share |
| </button> |
| <button data-action="export-download" class="w-full flex items-center justify-center gap-2 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 text-white px-4 py-3 rounded-xl font-medium transition-colors"> |
| <i data-lucide="download-cloud" class="w-4 h-4"></i> Fallback Download |
| </button> |
| </div> |
|
|
| <div id="exportStatus" class="hidden mt-4 text-center text-sm font-medium text-emerald-400 bg-emerald-400/10 py-2 rounded-lg"></div> |
| </div> |
| </div> |
|
|
| |
| <script> |
| window.App = { |
| state: { |
| concept: '', |
| segments: 1, |
| isRealistic: true, |
| customApiKey: '', |
| startMotion: 'Slow tracking shot moving forward', |
| endMotion: 'Camera settles into a static, locked-off composition', |
| tags: "8k resolution, photorealistic, cinematic lighting, national geographic style, highly detailed, sharp focus, true-to-life, nature photography, real life, masterpiece, pro-level cinematography", |
| negativeTags: "cartoon, 3d render, animation, low quality, blurry, distorted, deformed, artificial lighting, text, watermark, CGI, painting, illustration", |
| images: {}, |
| exportData: null |
| }, |
| |
| refreshIcons() { |
| if (typeof lucide !== 'undefined') { |
| try { lucide.createIcons(); } catch (e) { console.warn("Icon render skipped"); } |
| } |
| }, |
| |
| init() { |
| |
| document.getElementById('conceptInput').addEventListener('input', (e) => { this.state.concept = e.target.value; this.renderSegments(); }); |
| document.getElementById('segmentsInput').addEventListener('input', (e) => { this.state.segments = parseInt(e.target.value); document.getElementById('segmentsDisplay').innerText = `${this.state.segments} (${this.state.segments * 8}s total)`; this.renderSegments(); }); |
| document.getElementById('realisticToggle').addEventListener('change', (e) => { this.state.isRealistic = e.target.checked; document.getElementById('tagsContainer').style.display = this.state.isRealistic ? 'block' : 'none'; this.renderSegments(); }); |
| document.getElementById('startMotion').addEventListener('input', (e) => { this.state.startMotion = e.target.value; this.renderSegments(); }); |
| document.getElementById('endMotion').addEventListener('input', (e) => { this.state.endMotion = e.target.value; this.renderSegments(); }); |
| document.getElementById('metaTags').addEventListener('input', (e) => { this.state.tags = e.target.value; this.renderSegments(); }); |
| document.getElementById('negativeTags').addEventListener('input', (e) => { this.state.negativeTags = e.target.value; this.renderSegments(); }); |
| document.getElementById('customApiKey').addEventListener('input', (e) => { this.state.customApiKey = e.target.value; }); |
| |
| |
| document.addEventListener('click', (e) => { |
| const btn = e.target.closest('[data-action]'); |
| if (!btn) return; |
| |
| const action = btn.dataset.action; |
| const segmentNum = btn.dataset.segment ? parseInt(btn.dataset.segment) : null; |
| const type = btn.dataset.type; |
| |
| if (action === 'copy-prompt') { |
| const text = this.generatePromptText(segmentNum); |
| this.copyToClipboard(text, btn.id); |
| } else if (action === 'generate-image') { |
| this.triggerGenerate(segmentNum, type); |
| } else if (action === 'open-export') { |
| const stateKey = `${segmentNum}-${type}`; |
| const imgState = this.state.images[stateKey]; |
| if (imgState && imgState.data) { |
| this.openExportModal(imgState.data, segmentNum, type); |
| } |
| } else if (action === 'close-export') { |
| this.closeExportModal(); |
| } else if (action === 'export-copy') { |
| this.executeCopyImage(); |
| } else if (action === 'export-share') { |
| this.executeShare(); |
| } else if (action === 'export-download') { |
| this.executeDownload(); |
| } |
| }); |
| |
| this.renderSegments(); |
| }, |
| |
| generatePromptText(segmentNum) { |
| const baseConcept = this.state.concept || "[Enter your scene concept here]"; |
| let promptText = `Segment ${segmentNum} (8 Seconds):\nScene: ${baseConcept}\n`; |
| if (this.state.isRealistic) { |
| promptText += `Meta Tags: ${this.state.tags}\nNegative Prompts: ${this.state.negativeTags}\n`; |
| } |
| if (segmentNum === 1) promptText += `\nCamera/Motion (Start): ${this.state.startMotion}`; |
| else if (segmentNum === this.state.segments && this.state.segments > 1) promptText += `\nCamera/Motion (End): ${this.state.endMotion}`; |
| else promptText += `\nCamera/Motion: Continuous flowing motion maintaining subject focus`; |
| promptText += `\n\n[Workflow Note: Generate 3 Images for this segment (First Frame, Last Frame, Ingredient)]`; |
| return promptText; |
| }, |
| |
| copyToClipboard(text, btnId) { |
| const textArea = document.createElement("textarea"); |
| textArea.value = text; |
| textArea.style.position = "fixed"; |
| textArea.style.left = "-999999px"; |
| document.body.appendChild(textArea); |
| textArea.focus(); |
| textArea.select(); |
| try { |
| document.execCommand('copy'); |
| const btn = document.getElementById(btnId); |
| const originalHTML = btn.innerHTML; |
| btn.innerHTML = `<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-400"></i> Copied Block`; |
| this.refreshIcons(); |
| setTimeout(() => { btn.innerHTML = originalHTML; this.refreshIcons(); }, 2000); |
| } catch (err) { |
| console.error('Failed to copy', err); |
| } |
| document.body.removeChild(textArea); |
| }, |
| |
| async fetchWithRetry(url, options, retries = 5) { |
| const delays = [1000, 2000, 4000, 8000, 16000]; |
| for (let i = 0; i < retries; i++) { |
| try { |
| const res = await fetch(url, options); |
| if (res.ok) return res; |
| if (res.status === 400 || res.status === 403 || res.status === 401) { |
| const errData = await res.json().catch(() => ({})); |
| throw new Error(errData?.error?.message || `API Error: ${res.status}`); |
| } |
| if (i === retries - 1) throw new Error(`Status: ${res.status}`); |
| } catch (err) { |
| if (err.message.includes("API Error") || i === retries - 1) throw err; |
| } |
| await new Promise(resolve => setTimeout(resolve, delays[i])); |
| } |
| }, |
| |
| async triggerGenerate(segmentNum, type) { |
| if (!this.state.concept.trim()) { |
| this.setImageState(segmentNum, type, 'error', null, "Please type your idea in the box at the top first!"); |
| const input = document.getElementById('conceptInput'); |
| input.focus(); |
| input.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| return; |
| } |
| |
| this.setImageState(segmentNum, type, 'loading'); |
| |
| try { |
| const apiKey = this.state.customApiKey.trim(); |
| if (!apiKey) throw new Error("Please paste your Google AI Studio API Key in the top right box to generate images."); |
| |
| const url = `https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict?key=${apiKey}`; |
| |
| let imgPrompt = this.state.concept; |
| if (type === 'First Frame') imgPrompt += ` - establishing shot.`; |
| if (type === 'Last Frame') imgPrompt += ` - final ending frame composition.`; |
| if (type === 'Ingredient') imgPrompt += ` - isolated core subject ingredient shot.`; |
| if (this.state.isRealistic) imgPrompt += `, ${this.state.tags}`; |
| |
| const payload = { instances: { prompt: imgPrompt }, parameters: { sampleCount: 1 } }; |
| |
| const response = await this.fetchWithRetry(url, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(payload) |
| }); |
| |
| const data = await response.json(); |
| const base64Data = data.predictions?.[0]?.bytesBase64Encoded; |
| if (!base64Data) throw new Error("No image generated. Please try again."); |
| |
| this.setImageState(segmentNum, type, 'success', `data:image/png;base64,${base64Data}`); |
| } catch (err) { |
| console.error(err); |
| this.setImageState(segmentNum, type, 'error', null, err.message || "Failed to generate image."); |
| } |
| }, |
| |
| |