Spaces:
Sleeping
Sleeping
#!/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, 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 | |
# Create agent | |
agent = Agent('gemini-2.0-flash-exp') | |
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.data | |
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": api_key}) | |
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__": | |
print("π Starting Cicero Passa a Cola...") | |
uvicorn.run(app, host="0.0.0.0", port=7860) | |