// Advanced Simulation Controller const BACKEND_URL = window.location.origin; // Dynamically set backend URL for HuggingFace Spaces compatibility class SystemSimulator { constructor() { this.logs = document.getElementById('system-logs'); this.outputCanvas = document.getElementById('output-canvas'); this.outputCtx = this.outputCanvas?.getContext('2d'); this.isGenerating = false; this.sourceImage = null; this.config = { prompt: '', influence: 5, // Default 5% depth: 16, // Default 16 layers method: 'adaptive' }; // Director Mode State this.directorMode = true; this.movieFrames = []; // Stores ImageBitmaps or DataURLs this.accumulatedFrames = 0; this.init(); } async callBackendApi(endpoint, data) { try { const response = await fetch(`${BACKEND_URL}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); const jsonResponse = await response.json(); if (!response.ok) { throw new Error(jsonResponse.error || `Backend error: ${response.statusText}`); } return jsonResponse; } catch (error) { this.log(`Backend API Error (${endpoint}): ${error.message}`, 'error'); console.error(`Backend API Error (${endpoint}):`, error); throw error; // Re-throw to be caught by the calling function } } init() { this.setupListeners(); this.resizeCanvas(); window.addEventListener('resize', () => this.resizeCanvas()); // Initial visual state this.drawStaticNoise(); } setupListeners() { // Image Upload Handling const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('image-input'); dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); if(e.dataTransfer.files.length) { this.handleImage(e.dataTransfer.files[0]); } }); fileInput.addEventListener('change', (e) => { if(e.target.files.length) { this.handleImage(e.target.files[0]); } }); // Director Mode Listeners const directorToggle = document.getElementById('director-mode-toggle'); if (directorToggle) { directorToggle.addEventListener('change', (e) => { this.directorMode = e.target.checked; this.log(`Director Mode: ${this.directorMode ? 'ENABLED' : 'DISABLED'}`, 'info'); }); } document.getElementById('download-btn').addEventListener('click', () => this.downloadMovie()); document.getElementById('reset-movie-btn').addEventListener('click', () => this.resetMovie()); // Inputs document.getElementById('quantum-influence').addEventListener('input', (e) => { document.getElementById('influence-val').textContent = `${e.target.value}%`; this.config.influence = parseInt(e.target.value); }); document.getElementById('entanglement-depth').addEventListener('input', (e) => { document.getElementById('depth-val').textContent = e.target.value; this.config.depth = parseInt(e.target.value); }); // Tabs document.querySelectorAll('.viz-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.viz-tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.viz-view').forEach(v => v.classList.remove('active')); tab.classList.add('active'); document.getElementById(`view-${tab.dataset.view}`).classList.add('active'); }); }); // Start Button document.getElementById('start-btn').addEventListener('click', () => this.startGeneration()); } handleImage(file) { const reader = new FileReader(); reader.onload = async (e) => { // Made onload async this.sourceImage = new Image(); this.sourceImage.onload = async () => { // Made onload async // Show preview const preview = document.getElementById('preview-img'); preview.src = this.sourceImage.src; preview.classList.remove('hidden'); document.querySelector('.drop-content').style.opacity = '0'; this.log(`Image loaded: ${file.name} (${this.sourceImage.width}x${this.sourceImage.height})`, 'success'); // Call backend for CLIP analysis try { await this.analyzeImageContext(e.target.result); // Pass dataURL directly } catch (error) { this.log(`Failed CLIP analysis for ${file.name}: ${error.message}`, 'error'); } }; this.sourceImage.src = e.target.result; }; reader.readAsDataURL(file); } async analyzeImageContext(imageDataURL) { this.log('CLIP-Encoder: Sending image for feature extraction...', 'info'); try { const response = await this.callBackendApi('/embed_image', { image: imageDataURL }); const embeddings = response.embeddings; this.log(`CLIP-Encoder: Extracted feature vector [${embeddings[0].toFixed(4)}, ${embeddings[1].toFixed(4)}, ${embeddings[2].toFixed(4)}, ...]`, 'success'); } catch (error) { this.log(`CLIP-Encoder: Failed to get embeddings. Is backend running? ${error.message}`, 'error'); throw error; } } updateDirectorUI() { document.getElementById('total-frames').textContent = `${this.movieFrames.length} FRAMES`; document.getElementById('download-btn').disabled = this.movieFrames.length === 0; document.getElementById('reset-movie-btn').disabled = this.movieFrames.length === 0; } resetMovie() { this.movieFrames = []; this.updateDirectorUI(); this.log('Director Mode: Timeline cleared.', 'warn'); } resizeCanvas() { if (!this.outputCanvas) return; const rect = this.outputCanvas.parentElement.getBoundingClientRect(); this.outputCanvas.width = rect.width; this.outputCanvas.height = rect.height; if (!this.isGenerating) this.drawStaticNoise(); } log(message, type = 'info') { const div = document.createElement('div'); div.className = `log-line ${type}`; const time = new Date().toLocaleTimeString('en-US', { hour12: false }); div.innerHTML = `[${time}] ${message}`; this.logs.appendChild(div); this.logs.scrollTop = this.logs.scrollHeight; } async startGeneration() { if (this.isGenerating) return; // Disable UI elements during generation this.isGenerating = true; document.getElementById('start-btn').disabled = true; document.getElementById('prompt-input').disabled = true; document.getElementById('image-input').disabled = true; document.getElementById('quantum-influence').disabled = true; document.getElementById('entanglement-depth').disabled = true; document.getElementById('sampling-method').disabled = true; document.getElementById('generation-stats').style.display = 'block'; try { if (!this.sourceImage) { this.log('Error: Source image required for I2V generation.', 'error'); alert("Please upload a source image first."); return; // Exit if no source image } const prompt = document.getElementById('prompt-input').value.trim() || "Quantum interpolation"; // --- Backend Health Check --- this.log('Checking backend availability...', 'info'); try { const health = await this.callBackendApi('/'); this.log(`Backend Status: ${health.status} (LLM: ${health.llm_status}, CLIP: ${health.clip_status})`, 'success'); if (health.llm_status.includes("not loaded") || health.clip_status.includes("not loaded")) { throw new Error("One or more AI models not loaded on backend. Check backend console."); } } catch (error) { this.log(`Backend not available or unhealthy: ${error.message}. Please ensure your Python Flask backend is running.`, 'error'); alert(`Backend Error: ${error.message}. Please start the backend.`); return; // Exit if backend is not healthy } // --- End Backend Health Check --- this.log(`Initializing I2V pipeline for: "${prompt.substring(0, 30)}..."`, 'info'); // Phase 1: Initialization await this.phaseInitialization(); // Phase 2: Quantum Circuit await this.phaseQuantumCircuit(); // Phase 3: WebGPU Compute await this.phaseWebGPU(); // Phase 4: Bridge & Diffusion (Real Emulation) // This will now also handle recording if in Director Mode await this.phaseRealDiffusion(prompt); this.log('Generation Sequence Complete.', 'success'); document.getElementById('generation-stats').innerHTML = 'GENERATION COMPLETE'; // DIRECTOR MODE: PREP NEXT FRAME if (this.directorMode && this.movieFrames.length > 0) { this.prepareNextContext(); } } catch (error) { this.log(`System Error during generation: ${error.message}`, 'error'); document.getElementById('generation-stats').innerHTML = `ERROR: ${error.message}`; console.error(error); } finally { // Re-enable UI elements this.isGenerating = false; document.getElementById('start-btn').disabled = false; document.getElementById('prompt-input').disabled = false; document.getElementById('image-input').disabled = false; document.getElementById('quantum-influence').disabled = false; document.getElementById('entanglement-depth').disabled = false; document.getElementById('sampling-method').disabled = false; } } prepareNextContext() { // Get the last frame from the movie array const lastFrameBitmap = this.movieFrames[this.movieFrames.length - 1]; // Create a temp canvas to extract the image const canvas = document.createElement('canvas'); canvas.width = this.outputCanvas.width; canvas.height = this.outputCanvas.height; const ctx = canvas.getContext('2d'); ctx.drawImage(lastFrameBitmap, 0, 0); // Convert to Image object for sourceImage const newUrl = canvas.toDataURL(); const nextImg = new Image(); nextImg.onload = () => { this.sourceImage = nextImg; // Update Preview UI const preview = document.getElementById('preview-img'); preview.src = newUrl; this.log('Director Mode: Context refreshed. Last frame set as input for next sequence.', 'secondary'); }; nextImg.src = newUrl; } async sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async phaseInitialization() { this.log('Allocating WebGPU buffers for I2V tensor...', 'info'); await this.sleep(600); this.log('Quantizing source image to 512-dim latent space...', 'info'); await this.sleep(800); } async phaseQuantumCircuit() { this.log(`Constructing ${this.config.depth}-layer quantum circuit...`, 'info'); // Trigger Viz animation if available globally if (window.circuitViz) window.circuitViz.updateVizParameters(this.config.influence, this.config.depth); await this.sleep(1000); this.log('Applying Hadamard gates to initialization layer...', 'info'); await this.sleep(400); this.log(`Entangling qubits 0-511 with depth ${this.config.depth}...`, 'info'); await this.sleep(800); } async phaseWebGPU() { this.log('Compiling circuit to WGSL shaders...', 'info'); await this.sleep(600); this.log('Injecting quantum noise into CLIP embeddings...', 'info'); // Simulate intense calculation, trigger stateViz with parameters if (window.stateViz) window.stateViz.updateVizParameters(this.config.influence, this.config.depth); // Keep sleep for visual pacing for (let i = 0; i < 5; i++) { await this.sleep(200); } const entropy = (Math.random() * 3 + 0.5).toFixed(4); document.getElementById('entropy-value').textContent = entropy; this.log(`Latent perturbation complete. Entropy: ${entropy}`, 'success'); } async phaseRealDiffusion(prompt) { this.log('Starting Frame-by-Frame Quantum Diffusion...', 'warn'); // Switch tab to output to show the magic document.querySelector('[data-view="output"]').click(); // Get initial image data from the source image let currentImage = this.sourceImage; const totalFrames = 48; // Total frames for the movie let currentFrameDataURL = currentImage.src; // Data URL of the current frame for (let frame = 0; frame < totalFrames; frame++) { this.log(`Requesting guidance for Frame ${frame + 1}/${totalFrames}...`, 'info'); document.getElementById('generation-stats').innerHTML = `GETTING GUIDANCE FOR FRAME ${frame + 1}/${totalFrames}
Quantum-Diffusing...`; // Call backend for LLM guidance on how to transform the current frame const guidanceResponse = await this.callBackendApi('/generate_frame_guidance', { image: currentFrameDataURL, prompt: prompt, influence: this.config.influence, depth: this.config.depth, frame_number: frame }); const llmGuidance = guidanceResponse.guidance; this.log(`LLM Guidance (Frame ${frame + 1}): ${llmGuidance.substring(0, 80)}...`, 'secondary'); document.getElementById('generation-stats').innerHTML = `RENDERING FRAME ${frame + 1}/${totalFrames}
Applying Quantum Effects...`; // Render the next frame based on LLM guidance and current image const newFrameDataURL = await this.renderFrameTransition(currentImage, this.config.influence, llmGuidance, frame); // Update currentImage for the next iteration currentImage = await this this.loadImageFromDataURL(newFrameDataURL); currentFrameDataURL = newFrameDataURL; // Update dataURL as well // Director Mode: Record Frame if (this.directorMode) { const bitmap = await createImageBitmap(this.outputCanvas); this.movieFrames.push(bitmap); this.updateDirectorUI(); } await this.sleep(50); // Render speed } } async loadImageFromDataURL(dataURL) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = dataURL; }); } async renderFrameTransition(currentImage, influence, llmGuidance, frameNumber) { const w = this.outputCanvas.width; const h = this.outputCanvas.height; this.outputCtx.clearRect(0, 0, w, h); // Clear canvas for new frame // Create a temporary canvas to draw the currentImage and apply effects const tempCanvas = document.createElement('canvas'); tempCanvas.width = w; tempCanvas.height = h; const tempCtx = tempCanvas.getContext('2d'); // Draw the current image, scaled to fit const aspectRatio = currentImage.width / currentImage.height; let drawWidth = w; let drawHeight = h; if (w / h > aspectRatio) { // Canvas is wider than image drawWidth = h * aspectRatio; } else { // Canvas is taller than image drawHeight = w / aspectRatio; } const offsetX = (w - drawWidth) / 2; const offsetY = (h - drawHeight) / 2; tempCtx.drawImage(currentImage, offsetX, offsetY, drawWidth, drawHeight); // Get ImageData for pixel manipulation let imageData = tempCtx.getImageData(0, 0, w, h); let data = imageData.data; // --- Parse LLM Guidance and apply effects --- const instructions = llmGuidance.toLowerCase().split(',').map(s => s.trim()); let pixelShiftX = 0; let pixelShiftY = 0; let colorShiftR = 0; let colorShiftG = 0; let colorShiftB = 0; let blurRadius = 0; let zoomFactor = 1; let staticOverlayOpacity = 0; for (const instruction of instructions) { if (instruction.includes("shift red by")) { colorShiftR += parseInt(instruction.match(/by (-?\d+)/)?.[1] || "0"); } else if (instruction.includes("shift green by")) { colorShiftG += parseInt(instruction.match(/by (-?\d+)/)?.[1] || "0"); } else if (instruction.includes("shift blue by")) { colorShiftB += parseInt(instruction.match(/by (-?\d+)/)?.[1] || "0"); } else if (instruction.includes("pixel displacement x-axis")) { pixelShiftX += parseInt(instruction.match(/random (-?\d+)px/)?.[1] || "0"); } else if (instruction.includes("pixel displacement y-axis")) { pixelShiftY += parseInt(instruction.match(/random (-?\d+)px/)?.[1] || "0"); } else if (instruction.includes("apply gaussian blur radius")) { blurRadius = Math.max(blurRadius, parseInt(instruction.match(/radius (\d+)/)?.[1] || "0")); } else if (instruction.includes("zoom in")) { zoomFactor *= (1 + parseFloat(instruction.match(/zoom in (\d+(\.\d+)?)/)?.[1] || "0")); } else if (instruction.includes("zoom out")) { zoomFactor /= (1 + parseFloat(instruction.match(/zoom out (\d+(\.\d+)?)/)?.[1] || "0")); } else if (instruction.includes("static pattern opacity")) { staticOverlayOpacity = Math.max(staticOverlayOpacity, parseFloat(instruction.match(/opacity (\d+(\.\d+)?)/)?.[1] || "0")); } // Add more parsing for other instructions... } // Apply pixel shifts and color changes const tempImageData = tempCtx.createImageData(w, h); const tempData = tempImageData.data; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const originalIndex = (y * w + x) * 4; const shiftedX = (x - pixelShiftX + w) % w; const shiftedY = (y - pixelShiftY + h) % h; const shiftedIndex = (shiftedY * w + shiftedX) * 4; if (shiftedIndex >= 0 && shiftedIndex < data.length) { tempData[originalIndex] = Math.min(255, Math.max(0, data[shiftedIndex] + colorShiftR)); // Red tempData[originalIndex + 1] = Math.min(255, Math.max(0, data[shiftedIndex + 1] + colorShiftG)); // Green tempData[originalIndex + 2] = Math.min(255, Math.max(0, data[shiftedIndex + 2] + colorShiftB)); // Blue tempData[originalIndex + 3] = data[shiftedIndex + 3]; // Alpha } else { // Handle out-of-bounds pixels (e.g., fill with black or transparent) tempData[originalIndex] = 0; tempData[originalIndex + 1] = 0; tempData[originalIndex + 2] = 0; tempData[originalIndex + 3] = 255; } } } imageData = tempImageData; // Update imageData with shifted pixels // Apply blur (very basic box blur for performance, Gaussian is complex with pixel data) if (blurRadius > 0) { const blurredImageData = tempCtx.createImageData(w, h); const blurredData = blurredImageData.data; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { let rSum = 0, gSum = 0, bSum = 0, aSum = 0; let count = 0; for (let ky = -blurRadius; ky <= blurRadius; ky++) { for (let kx = -blurRadius; kx <= blurRadius; kx++) { const nx = x + kx; const ny = y + ky; if (nx >= 0 && nx < w && ny >= 0 && ny < h) { const index = (ny * w + nx) * 4; rSum += data[index]; gSum += data[index + 1]; bSum += data[index + 2]; aSum += data[index + 3]; count++; } } } const outputIndex = (y * w + x) * 4; blurredData[outputIndex] = rSum / count; blurredData[outputIndex + 1] = gSum / count; blurredData[outputIndex + 2] = bSum / count; blurredData[outputIndex + 3] = aSum / count; } } imageData = blurredImageData; } // Apply static overlay if (staticOverlayOpacity > 0) { for (let i = 0; i < imageData.data.length; i += 4) { const staticValue = Math.random() * 255; imageData.data[i] = (imageData.data[i] * (1 - staticOverlayOpacity)) + (staticValue * staticOverlayOpacity); imageData.data[i+1] = (imageData.data[i+1] * (1 - staticOverlayOpacity)) + (staticValue * staticOverlayOpacity); imageData.data[i+2] = (imageData.data[i+2] * (1 - staticOverlayOpacity)) + (staticValue * staticOverlayOpacity); } } // Draw the processed ImageData back to the temporary canvas tempCtx.putImageData(imageData, 0, 0); // Apply zoom (done by redrawing tempCanvas onto outputCanvas) const zoomedWidth = w * zoomFactor; const zoomedHeight = h * zoomFactor; const zoomOffsetX = (w - zoomedWidth) / 2; const zoomOffsetY = (h - zoomedHeight) / 2; this.outputCtx.drawImage(tempCanvas, zoomOffsetX, zoomOffsetY, zoomedWidth, zoomedHeight); // Periodically draw circuit overlay if influence is high if (influence > 50 && frameNumber % 5 === 0) { this.drawCircuitOverlay(); } // Convert the final rendered canvas state to a DataURL for the next iteration return this.outputCanvas.toDataURL(); } drawCircuitOverlay() { const ctx = this.outputCtx; const w = this.outputCanvas.width; const h = this.outputCanvas.height; ctx.strokeStyle = 'rgba(0, 240, 255, 0.3)'; ctx.lineWidth = 1; ctx.beginPath(); const y = Math.random() * h; ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); ctx.fillStyle = 'rgba(0, 240, 255, 0.5)'; // Attempt to get a more dynamic font size const fontSize = Math.max(10, Math.min(w, h) / 30); ctx.font = `${fontSize}px Arial`; ctx.fillText(`Q-GATE-${Math.floor(Math.random()*100)}`, 10, y - 5); } drawStaticNoise() { const w = this.outputCanvas.width; const h = this.outputCanvas.height; const id = this.outputCtx.createImageData(w, h); const d = id.data; for (let i = 0; i < d.length; i += 4) { const v = Math.random() * 20; // Dark noise d[i] = v; d[i+1] = v; d[i+2] = v + 10; d[i+3] = 255; } this.outputCtx.putImageData(id, 0, 0); } // renderDiffusionStep is no longer needed as phaseRealDiffusion and renderFrameTransition handle it. // Keeping it as a placeholder/commented out for now if previous functionality needs reference. // async renderDiffusionStep(step, totalSteps) { } async downloadMovie() { if (this.movieFrames.length === 0) return; const btn = document.getElementById('download-btn'); const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = 'RENDER...'; this.log('Starting Movie Rendering...', 'info'); try { // Create a hidden canvas for playback const canvas = document.createElement('canvas'); canvas.width = this.outputCanvas.width; canvas.height = this.outputCanvas.height; const ctx = canvas.getContext('2d'); // Setup MediaRecorder const stream = canvas.captureStream(30); // 30 FPS const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9') ? 'video/webm;codecs=vp9' : 'video/webm'; const recorder = new MediaRecorder(stream, { mimeType: mimeType, videoBitsPerSecond: 5000000 // 5Mbps }); const chunks = []; recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); }; recorder.onstop = () => { const blob = new Blob(chunks, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `wan-quantum-director-cut-${Date.now()}.webm`; a.click(); URL.revokeObjectURL(url); this.log('Movie Downloaded Successfully.', 'success'); btn.innerHTML = originalText; btn.disabled = false; }; recorder.start(); // Play frames into recorder // We need to pace this to match the stream FPS roughly const frameDuration = 1000 / 30; for (const bitmap of this.movieFrames) { ctx.drawImage(bitmap, 0, 0); // Request dummy frame to keep stream active if needed, // but loop should be enough if async enough. // Actually, for captureStream to pick it up, we should wait a tick. await new Promise(r => setTimeout(r, frameDuration)); } recorder.stop(); } catch (e) { this.log(`Export failed: ${e.message}`, 'error'); btn.innerHTML = originalText; btn.disabled = false; } }