Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """Cicero Cola - Design Studio Screen Analysis Platform""" | |
| import os | |
| import asyncio | |
| import tempfile | |
| import base64 | |
| from pathlib import Path | |
| from typing import Optional | |
| from PIL import Image | |
| from loguru import logger | |
| import uvicorn | |
| from fastapi import FastAPI, File, UploadFile, Form, HTTPException | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic_ai import Agent | |
| from pydantic_ai.models.gemini import GeminiModel | |
| from pydantic_ai.messages import BinaryContent | |
| from dotenv import load_dotenv | |
| # Load environment variables | |
| load_dotenv() | |
| def setup_environment(): | |
| """Setup environment variables and configurations.""" | |
| hf_token = os.environ.get("HF_TOKEN") | |
| gemini_key = os.environ.get("GEMINI_API_KEY") | |
| if hf_token: | |
| print(f"β HF_TOKEN loaded...") | |
| else: | |
| print("β οΈ HF_TOKEN not found in environment") | |
| if gemini_key: | |
| print(f"β GEMINI_API_KEY loaded...") | |
| else: | |
| print("β οΈ GEMINI_API_KEY not found in environment") | |
| return hf_token, gemini_key | |
| async def analyze_image_with_ai(image_data: bytes, question: str | None, api_key: str) -> str: | |
| """Analyze image using pydantic_ai with BinaryContent.""" | |
| try: | |
| # Set the API key in environment for Gemini | |
| #os.environ['GEMINI_API_KEY'] = api_key | |
| if question is None or question is "": | |
| logger.info(f"Question is {question}. Enforcing the default.") | |
| question = """ | |
| <vision_task> | |
| <objective> | |
| Analyze the provided image containing a multiple-choice question. | |
| Generate a highly structured, accurate, and succinct response in Markdown. | |
| Minimize token usage. | |
| </objective> | |
| <output_constraints> | |
| <format>Markdown.</format> | |
| <precision>Include a confidence score (0.00-1.00).</precision> | |
| <explanation_order> | |
| 1. Explanation for each incorrect alternative (A, B, C, etc.). | |
| 2. Explanation for the correct alternative. | |
| 3. The final correct alternative. | |
| </explanation_order> | |
| <word_limit>Maximum of 10 words per explanation.</word_limit> | |
| </output_constraints> | |
| <prompt> | |
| Based on the image, identify the question and the correct alternative. | |
| Provide the answer using the following strict structure: | |
| 1. **Confidence:** (e.g., `Confidence: 0.98`) | |
| 2. **Incorrect Rationale:** For each option that is not the answer, state why it is wrong. | |
| 3. **Correct Rationale:** State why the correct option is the answer. | |
| 4. **Answer:** The letter corresponding to the correct alternative. | |
| </prompt> | |
| </vision_task> | |
| """ | |
| # Create agent | |
| agent = Agent('gemini-2.5-flash') | |
| logger.info(f"Agent created with Gemini model") | |
| # Create binary content for the image | |
| image_content = BinaryContent( | |
| data=image_data, | |
| media_type='image/png' | |
| ) | |
| # Run the agent with image and question | |
| result = await agent.run([question, image_content]) | |
| logger.info(f"Analysis completed successfully") | |
| return result.output | |
| except Exception as e: | |
| logger.error(f"Analysis failed: {str(e)}") | |
| return f"Analysis failed: {str(e)}" | |
| # Initialize FastAPI app | |
| app = FastAPI(title="Cicero Passa a Cola", description="Screen Analysis Platform") | |
| # Configure CORS | |
| origins = [ | |
| "http://localhost", | |
| "https://localhost", | |
| "http://localhost:7864", | |
| "https://localhost:7864", | |
| "http://127.0.0.1:7864", | |
| "https://127.0.0.1:7864", | |
| "http://localhost:8864", | |
| "https://localhost:8864", | |
| "http://127.0.0.1:8864", | |
| "https://127.0.0.1:8864", | |
| "https://*.cicero.im", | |
| "http://*.cicero.im", | |
| "https://huggingface.co", | |
| "http://huggingface.co", | |
| "https://*.huggingface.co", | |
| "http://*.huggingface.co", | |
| ] | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=origins, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Setup environment | |
| hf_token, gemini_key = setup_environment() | |
| async def get_main_page(): | |
| """Serve the main HTML page with Cicero Cola design studio interface.""" | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Cicero Passa a Cola</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| :root {{ | |
| --black: #000000; | |
| --white: #ffffff; | |
| --scarlet: #dc143c; | |
| --gray-50: #fafafa; | |
| --gray-100: #f5f5f5; | |
| --gray-200: #e5e5e5; | |
| --gray-300: #d4d4d4; | |
| --gray-600: #525252; | |
| --gray-800: #262626; | |
| }} | |
| * {{ | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| }} | |
| body {{ | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| background: var(--white); | |
| color: var(--black); | |
| line-height: 1.6; | |
| font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; | |
| }} | |
| .header {{ | |
| background: var(--black); | |
| color: var(--white); | |
| padding: 1rem 0; | |
| position: relative; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| }} | |
| .header::before {{ | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -50%; | |
| width: 200%; | |
| height: 100%; | |
| background: linear-gradient(45deg, transparent, var(--scarlet), transparent); | |
| opacity: 0.1; | |
| animation: sweep 3s ease-in-out infinite; | |
| }} | |
| @keyframes sweep {{ | |
| 0% {{ transform: translateX(-100%); }} | |
| 100% {{ transform: translateX(100%); }} | |
| }} | |
| .header-content {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| width: 100%; | |
| }} | |
| .brand {{ | |
| font-size: 2.8rem; | |
| font-weight: 700; | |
| letter-spacing: 0.1em; | |
| position: relative; | |
| z-index: 2; | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .logo {{ | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| position: relative; | |
| z-index: 2; | |
| flex: 1; | |
| text-align: center; | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .container {{ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 0 2rem; | |
| }} | |
| .hero {{ | |
| padding: 2rem 0 1rem 0; | |
| text-align: center; | |
| background: var(--gray-50); | |
| }} | |
| .hero h1 {{ | |
| font-size: 2.5rem; | |
| margin-bottom: 0; | |
| font-weight: 600; | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .main-controls {{ | |
| display: grid; | |
| grid-template-columns: 300px 1fr; | |
| gap: 2rem; | |
| margin: 2rem auto; | |
| max-width: 1000px; | |
| min-height: 300px; | |
| }} | |
| .controls-panel {{ | |
| background: var(--white); | |
| border: 1px solid var(--gray-200); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: flex-start; | |
| gap: 1rem; | |
| }} | |
| .status-panel {{ | |
| background: var(--white); | |
| border: 1px solid var(--gray-200); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| }} | |
| .recording-controls {{ | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| }} | |
| .studio-grid {{ | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 2rem; | |
| padding: 2rem 0; | |
| }} | |
| .studio-card {{ | |
| background: var(--white); | |
| border: 1px solid var(--gray-200); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| }} | |
| .studio-card:hover {{ | |
| transform: translateY(-8px); | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.1); | |
| border-color: var(--scarlet); | |
| }} | |
| .studio-card::before {{ | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 4px; | |
| background: var(--scarlet); | |
| transform: scaleX(0); | |
| transition: transform 0.3s ease; | |
| }} | |
| .studio-card:hover::before {{ | |
| transform: scaleX(1); | |
| }} | |
| .card-title {{ | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| margin-bottom: 1rem; | |
| color: var(--black); | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .card-description {{ | |
| color: var(--gray-600); | |
| margin-bottom: 2rem; | |
| }} | |
| .studio-recording-controls {{ | |
| display: flex; | |
| gap: 1rem; | |
| margin-bottom: 2rem; | |
| flex-wrap: wrap; | |
| }} | |
| .btn {{ | |
| padding: 1rem 1.5rem; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 1rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| text-decoration: none; | |
| width: 100%; | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .btn-primary {{ | |
| background: var(--scarlet); | |
| color: var(--white); | |
| }} | |
| .btn-primary:hover {{ | |
| background: #b91c3c; | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(220, 20, 60, 0.2); | |
| }} | |
| .btn-secondary {{ | |
| background: var(--black); | |
| color: var(--white); | |
| }} | |
| .btn-secondary:hover {{ | |
| background: var(--gray-800); | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); | |
| }} | |
| .btn:disabled {{ | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none; | |
| }} | |
| .input-group {{ | |
| margin-bottom: 1.5rem; | |
| }} | |
| .input-label {{ | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| color: var(--black); | |
| }} | |
| .input-field {{ | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 1px solid var(--gray-300); | |
| border-radius: 8px; | |
| font-size: 0.95rem; | |
| transition: border-color 0.3s ease; | |
| background: var(--white); | |
| }} | |
| .input-field:focus {{ | |
| outline: none; | |
| border-color: var(--scarlet); | |
| box-shadow: 0 0 0 3px rgba(220, 20, 60, 0.1); | |
| }} | |
| .textarea {{ | |
| resize: vertical; | |
| min-height: 100px; | |
| }} | |
| .status-display {{ | |
| padding: 1.5rem; | |
| border-radius: 8px; | |
| text-align: center; | |
| font-weight: 500; | |
| border: 1px solid var(--gray-200); | |
| background: var(--gray-50); | |
| font-family: 'Inter', sans-serif; | |
| font-size: 1.1rem; | |
| }} | |
| .video-preview {{ | |
| width: 100%; | |
| max-width: 100%; | |
| border-radius: 12px; | |
| background: var(--black); | |
| margin-bottom: 2rem; | |
| display: none; | |
| }} | |
| .upload-area {{ | |
| border: 2px dashed var(--gray-300); | |
| border-radius: 12px; | |
| padding: 2rem; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| margin-bottom: 2rem; | |
| }} | |
| .upload-area:hover {{ | |
| border-color: var(--scarlet); | |
| background: var(--gray-50); | |
| }} | |
| .upload-area.dragover {{ | |
| border-color: var(--scarlet); | |
| background: rgba(220, 20, 60, 0.05); | |
| }} | |
| .results-area {{ | |
| background: var(--gray-50); | |
| border-radius: 12px; | |
| padding: 2rem; | |
| margin-top: 2rem; | |
| border: 1px solid var(--gray-200); | |
| min-height: 200px; | |
| }} | |
| .results-title {{ | |
| font-weight: 600; | |
| margin-bottom: 1rem; | |
| color: var(--black); | |
| }} | |
| .results-content {{ | |
| line-height: 1.7; | |
| color: var(--gray-800); | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .results-content h1, .results-content h2, .results-content h3, | |
| .results-content h4, .results-content h5, .results-content h6 {{ | |
| font-family: 'Inter', sans-serif; | |
| font-weight: 600; | |
| margin: 1rem 0 0.5rem 0; | |
| color: var(--black); | |
| }} | |
| .results-content p {{ | |
| margin-bottom: 1rem; | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .results-content ul, .results-content ol {{ | |
| margin-bottom: 1rem; | |
| padding-left: 1.5rem; | |
| font-family: 'Inter', sans-serif; | |
| }} | |
| .results-content li {{ | |
| margin-bottom: 0.5rem; | |
| }} | |
| .results-content strong {{ | |
| font-weight: 600; | |
| color: var(--black); | |
| }} | |
| .results-content code {{ | |
| background: var(--gray-100); | |
| padding: 0.2rem 0.4rem; | |
| border-radius: 4px; | |
| font-family: 'SF Mono', 'Monaco', 'Cascadia Code', monospace; | |
| font-size: 0.9em; | |
| }} | |
| .results-content blockquote {{ | |
| border-left: 4px solid var(--scarlet); | |
| padding-left: 1rem; | |
| margin: 1rem 0; | |
| font-style: italic; | |
| color: var(--gray-600); | |
| }} | |
| .footer {{ | |
| background: var(--black); | |
| color: var(--white); | |
| text-align: center; | |
| padding: 3rem 0; | |
| margin-top: 4rem; | |
| }} | |
| .footer-content {{ | |
| opacity: 0.8; | |
| }} | |
| .accent {{ | |
| color: var(--scarlet); | |
| }} | |
| @media (max-width: 768px) {{ | |
| .logo {{ | |
| font-size: 2rem; | |
| }} | |
| .hero h1 {{ | |
| font-size: 2rem; | |
| }} | |
| .recording-controls {{ | |
| flex-direction: column; | |
| }} | |
| .btn {{ | |
| justify-content: center; | |
| }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <header class="header"> | |
| <div class="container"> | |
| <div class="header-content"> | |
| <div class="brand">CICERO</div> | |
| <div style="flex: 1;"></div> <!-- Spacer for balance --> | |
| <div style="width: 80px;"></div> <!-- Spacer for balance --> | |
| </div> | |
| </div> | |
| </header> | |
| <section class="hero"> | |
| <div class="container"> | |
| <h1>Capture. Analyze. <span class="accent">Cole.</span></h1> | |
| </div> | |
| </section> | |
| <main class="container"> | |
| <!-- Main Recording Controls --> | |
| <div class="main-controls"> | |
| <div class="controls-panel"> | |
| <div class="recording-controls"> | |
| <button id="startBtn" class="btn btn-primary"> | |
| Start Recording | |
| </button> | |
| <button id="stopBtn" class="btn btn-secondary" disabled> | |
| Stop Recording | |
| </button> | |
| <button id="snapshotBtn" class="btn btn-primary" disabled> | |
| Capture & Analyze | |
| </button> | |
| </div> | |
| </div> | |
| <div class="status-panel"> | |
| <div id="status" class="status-display">Ready to start recording</div> | |
| <!-- Results Area --> | |
| <div class="results-area" id="resultsArea" style="display: none; margin-top: 2rem;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> | |
| <div class="results-title">Analysis Results</div> | |
| <button id="okBtn" class="btn btn-primary" style="padding: 0.5rem 1rem; width: auto;">OK</button> | |
| </div> | |
| <div class="results-content" id="resultsContent"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="studio-grid"> | |
| <!-- Live Recording Studio --> | |
| <div class="studio-card"> | |
| <h2 class="card-title">Live Recording Studio</h2> | |
| <p class="card-description">Capture your screen in real-time with professional-grade recording capabilities</p> | |
| <video id="videoPreview" class="video-preview" autoplay muted playsinline></video> | |
| </div> | |
| <!-- AI Analysis Lab --> | |
| <div class="studio-card"> | |
| <h2 class="card-title">AI Analysis Lab</h2> | |
| <p class="card-description">Upload images for intelligent design and content analysis</p> | |
| <div class="input-group"> | |
| <label class="input-label" for="apiKey">Gemini API Key</label> | |
| <div style="display: flex; gap: 0.5rem;"> | |
| <input type="password" id="apiKey" class="input-field" placeholder="Enter your Gemini API key" value="{gemini_key or ''}" style="flex: 2; min-width: 300px;"> | |
| <button id="loadEnvBtn" class="btn btn-secondary" style="white-space: nowrap; width: 140px;">Load from Env</button> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label class="input-label" for="question">Analysis Question</label> | |
| <textarea id="question" class="input-field textarea" placeholder="What would you like to analyze about this image?">Analyze this design from a professional perspective. What are the key visual elements, design principles, and potential improvements?</textarea> | |
| </div> | |
| <div class="upload-area" onclick="document.getElementById('imageUpload').click()"> | |
| <div> | |
| <strong>Click to upload</strong> or drag and drop<br> | |
| <small>PNG, JPG, GIF up to 10MB</small> | |
| </div> | |
| </div> | |
| <input type="file" id="imageUpload" accept="image/*" style="display: none;"> | |
| <button id="analyzeBtn" class="btn btn-primary" style="width: 100%;"> | |
| Analyze Image | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| <footer class="footer"> | |
| <div class="container"> | |
| <div class="footer-content"> | |
| <p>Cicero Passa a Cola β’ Powered by AI β’ Built with β€οΈ</p> | |
| </div> | |
| </div> | |
| </footer> | |
| <script> | |
| let mediaRecorder; | |
| let stream; | |
| let recordedChunks = []; | |
| // DOM elements | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const snapshotBtn = document.getElementById('snapshotBtn'); | |
| const statusDiv = document.getElementById('status'); | |
| const videoPreview = document.getElementById('videoPreview'); | |
| const apiKeyInput = document.getElementById('apiKey'); | |
| const questionInput = document.getElementById('question'); | |
| const imageUpload = document.getElementById('imageUpload'); | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| const resultsArea = document.getElementById('resultsArea'); | |
| const resultsContent = document.getElementById('resultsContent'); | |
| const uploadArea = document.querySelector('.upload-area'); | |
| const loadEnvBtn = document.getElementById('loadEnvBtn'); | |
| const okBtn = document.getElementById('okBtn'); | |
| // Event listeners | |
| startBtn.addEventListener('click', startRecording); | |
| stopBtn.addEventListener('click', stopRecording); | |
| snapshotBtn.addEventListener('click', captureSnapshot); | |
| analyzeBtn.addEventListener('click', analyzeUploadedImage); | |
| imageUpload.addEventListener('change', handleImageUpload); | |
| loadEnvBtn.addEventListener('click', loadFromEnvironment); | |
| okBtn.addEventListener('click', () => {{ resultsArea.style.display = 'none'; }}); | |
| // Drag and drop functionality | |
| uploadArea.addEventListener('dragover', (e) => {{ | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }}); | |
| uploadArea.addEventListener('dragleave', () => {{ | |
| uploadArea.classList.remove('dragover'); | |
| }}); | |
| uploadArea.addEventListener('drop', (e) => {{ | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) {{ | |
| imageUpload.files = files; | |
| handleImageUpload(); | |
| }} | |
| }}); | |
| function updateStatus(message, color = '#000') {{ | |
| statusDiv.textContent = message; | |
| statusDiv.style.color = color; | |
| }} | |
| async function startRecording() {{ | |
| try {{ | |
| stream = await navigator.mediaDevices.getDisplayMedia({{ | |
| video: true, | |
| audio: true | |
| }}); | |
| videoPreview.srcObject = stream; | |
| videoPreview.style.display = 'block'; | |
| mediaRecorder = new MediaRecorder(stream); | |
| recordedChunks = []; | |
| mediaRecorder.ondataavailable = (event) => {{ | |
| if (event.data.size > 0) {{ | |
| recordedChunks.push(event.data); | |
| }} | |
| }}; | |
| mediaRecorder.onstop = () => {{ | |
| const blob = new Blob(recordedChunks, {{ type: 'video/webm' }}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `cicero-recording-${{Date.now()}}.webm`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }}; | |
| mediaRecorder.start(); | |
| updateStatus('Recording in progress...', '#000000'); | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| snapshotBtn.disabled = false; | |
| stream.getVideoTracks()[0].onended = () => {{ | |
| stopRecording(); | |
| }}; | |
| }} catch (err) {{ | |
| console.error('Error starting recording:', err); | |
| updateStatus('Error: ' + err.message, '#000000'); | |
| }} | |
| }} | |
| function stopRecording() {{ | |
| if (mediaRecorder && mediaRecorder.state === 'recording') {{ | |
| mediaRecorder.stop(); | |
| }} | |
| if (stream) {{ | |
| stream.getTracks().forEach(track => track.stop()); | |
| stream = null; | |
| }} | |
| videoPreview.srcObject = null; | |
| videoPreview.style.display = 'none'; | |
| updateStatus('Recording stopped', '#000000'); | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| snapshotBtn.disabled = true; | |
| }} | |
| async function captureSnapshot() {{ | |
| if (!videoPreview.srcObject) {{ | |
| updateStatus('No active recording to capture', '#000000'); | |
| return; | |
| }} | |
| const apiKey = apiKeyInput.value.trim(); | |
| if (!apiKey) {{ | |
| updateStatus('Please enter Gemini API key', '#000000'); | |
| return; | |
| }} | |
| updateStatus('Capturing and analyzing...', '#000000'); | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = videoPreview.videoWidth; | |
| canvas.height = videoPreview.videoHeight; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(videoPreview, 0, 0); | |
| canvas.toBlob(async (blob) => {{ | |
| try {{ | |
| // Save snapshot | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `cicero-snapshot-${{Date.now()}}.png`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| // Analyze with AI | |
| const formData = new FormData(); | |
| formData.append('image', blob, 'snapshot.png'); | |
| formData.append('question', questionInput.value); | |
| formData.append('api_key', apiKey); | |
| const response = await fetch('/analyze-image', {{ | |
| method: 'POST', | |
| body: formData | |
| }}); | |
| const result = await response.json(); | |
| if (response.ok) {{ | |
| showResults('Live Snapshot Analysis', result.analysis); | |
| updateStatus('Snapshot captured and analyzed!', '#000000'); | |
| }} else {{ | |
| showResults('Analysis Failed', result.error || 'Unknown error'); | |
| updateStatus('Analysis failed', '#000000'); | |
| }} | |
| }} catch (error) {{ | |
| console.error('Error:', error); | |
| updateStatus('Error during analysis', '#000000'); | |
| }} | |
| }}, 'image/png'); | |
| }} | |
| function handleImageUpload() {{ | |
| const file = imageUpload.files[0]; | |
| if (file) {{ | |
| uploadArea.innerHTML = ` | |
| <div> | |
| <strong>${{file.name}}</strong><br> | |
| <small>Ready for analysis</small> | |
| </div> | |
| `; | |
| }} | |
| }} | |
| async function analyzeUploadedImage() {{ | |
| const file = imageUpload.files[0]; | |
| const apiKey = apiKeyInput.value.trim(); | |
| const question = questionInput.value.trim(); | |
| if (!file) {{ | |
| alert('Please upload an image first'); | |
| return; | |
| }} | |
| if (!apiKey) {{ | |
| alert('Please enter your Gemini API key'); | |
| return; | |
| }} | |
| analyzeBtn.disabled = true; | |
| analyzeBtn.innerHTML = 'Analyzing...'; | |
| try {{ | |
| const formData = new FormData(); | |
| formData.append('image', file); | |
| formData.append('question', question); | |
| formData.append('api_key', apiKey); | |
| const response = await fetch('/analyze-image', {{ | |
| method: 'POST', | |
| body: formData | |
| }}); | |
| const result = await response.json(); | |
| if (response.ok) {{ | |
| showResults('Design Analysis', result.analysis); | |
| }} else {{ | |
| showResults('Analysis Failed', result.error || 'Unknown error'); | |
| }} | |
| }} catch (error) {{ | |
| console.error('Error:', error); | |
| showResults('Error', 'Failed to analyze image'); | |
| }} finally {{ | |
| analyzeBtn.disabled = false; | |
| analyzeBtn.innerHTML = 'Analyze Image'; | |
| }} | |
| }} | |
| async function loadFromEnvironment() {{ | |
| try {{ | |
| const response = await fetch('/get-env-key'); | |
| const result = await response.json(); | |
| if (response.ok && result.api_key) {{ | |
| apiKeyInput.value = result.api_key; | |
| updateStatus('API key loaded from environment', '#000000'); | |
| }} else {{ | |
| updateStatus('No API key found in environment', '#000000'); | |
| }} | |
| }} catch (error) {{ | |
| console.error('Error loading from environment:', error); | |
| updateStatus('Error loading from environment', '#000000'); | |
| }} | |
| }} | |
| function showResults(title, content) {{ | |
| // Parse markdown content | |
| const titleHtml = `<h3 style="font-family: 'Inter', sans-serif; font-weight: 600; color: var(--black); margin-bottom: 1rem;">${{title}}</h3>`; | |
| const contentHtml = marked.parse(content); | |
| resultsContent.innerHTML = titleHtml + contentHtml; | |
| resultsArea.style.display = 'block'; | |
| resultsArea.scrollIntoView({{ behavior: 'smooth' }}); | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return html_content | |
| async def get_env_key(): | |
| """Get API key from environment variables.""" | |
| try: | |
| api_key = os.environ.get("GEMINI_API_KEY") | |
| if api_key: | |
| return JSONResponse(content={"api_key": "uga"}) | |
| else: | |
| return JSONResponse(content={"api_key": None}, status_code=404) | |
| except Exception as e: | |
| logger.error(f"Error getting environment key: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def analyze_image_endpoint( | |
| image: UploadFile = File(...), | |
| question: str = Form(...), | |
| api_key: str = Form(...) | |
| ): | |
| """Analyze uploaded image with AI.""" | |
| try: | |
| # Read image data | |
| image_data = await image.read() | |
| # Validate image type | |
| if image.content_type and not image.content_type.startswith('image/'): | |
| raise HTTPException(status_code=400, detail=f"Invalid file type: {image.content_type}") | |
| # Analyze with AI | |
| result = await analyze_image_with_ai(image_data, question, api_key) | |
| return JSONResponse(content={"analysis": result}) | |
| except Exception as e: | |
| logger.error(f"Error analyzing image: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| if __name__ == "__main__": | |
| hf_token, gemini_key = setup_environment() | |
| print("π Starting Cicero Passa a Cola...") | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |