Mower1776's picture
Update index.html
d12e560 verified
<!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>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Lucide Icons -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.460.0/umd/lucide.min.js"></script>
<style>
/* Forces Android Webviews to allow long-press context menus */
.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 -->
<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>
<!-- Controls Section -->
<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>
<!-- Customization Fields -->
<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>
<!-- Checklist Sidebar -->
<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>
<!-- Generated Workspace Blocks -->
<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">
<!-- Dynamically populated -->
</div>
</div>
</div>
<!-- Export Hub Overlay -->
<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>
<!-- Application Logic -->
<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() {
// Bind inputs
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; });
// BYPASS BROWSER PROTECTION: Global Event Delegation (No inline onclick handlers!)
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.");
}
},