FireEdit-Deployment / index.html
toiram's picture
Upload 2 files
32bf000 verified
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Studio PRO - Multi-Model Editor</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {
--bg: #0a0a0a;
--sidebar: #121212;
--card: #1e1e1e;
--primary: #ff4d4d;
--accent: #ff8080;
--text: #e0e0e0;
--muted: #888888;
--border: #333333;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
overflow: hidden;
height: 100vh;
}
.app-layout { display: flex; height: 100vh; }
.sidebar {
width: 380px;
background: var(--sidebar);
border-right: 1px solid var(--border);
padding: 25px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.logo { font-size: 1.5rem; font-weight: 900; color: var(--primary); display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.main-content { flex: 1; padding: 40px; overflow-y: auto; position: relative; }
.form-group { display: flex; flex-direction: column; gap: 8px; }
label { font-size: 0.75rem; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
textarea, select, input {
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
padding: 12px; color: white; font-family: inherit; outline: none; transition: border 0.2s;
font-size: 0.9rem;
}
textarea:focus, select:focus, input:focus { border-color: var(--primary); }
.slider-group { display: flex; align-items: center; gap: 15px; }
input[type="range"] { flex: 1; accent-color: var(--primary); }
.btn-generate {
background: var(--primary); color: white; border: none; padding: 15px;
border-radius: 12px; font-weight: 800; cursor: pointer; font-size: 1rem;
display: flex; align-items: center; justify-content: center; gap: 10px;
transition: all 0.2s;
margin-top: 10px;
}
.btn-generate:hover { transform: translateY(-2px); filter: brightness(1.1); box-shadow: 0 10px 20px rgba(255, 77, 77, 0.2); }
.btn-generate:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.dropzone {
border: 2px dashed var(--border); border-radius: 20px; padding: 40px;
text-align: center; cursor: pointer; transition: all 0.2s;
background: rgba(255,255,255,0.02);
display: block;
}
.dropzone:hover, .dropzone.drag-over { border-color: var(--primary); background: rgba(255,77,77,0.05); }
.dropzone i { font-size: 3rem; color: var(--muted); margin-bottom: 15px; }
.dropzone.drag-over i { color: var(--primary); transform: scale(1.1); transition: transform 0.2s; }
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
margin-top: 10px;
}
.gallery-item {
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border);
position: relative;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.result-container { margin-top: 20px; display: flex; flex-direction: column; align-items: center; gap: 15px; }
.result-img { max-width: 100%; border-radius: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.5); border: 1px solid var(--border); }
.loader {
width: 30px; height: 30px; border: 3px solid rgba(255,255,255,0.1); border-top-color: var(--primary);
border-radius: 50%; animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.footer-info { font-size: 0.75rem; color: var(--muted); margin-top: auto; padding-top: 20px; border-top: 1px solid var(--border); }
.model-card {
padding: 10px; border: 1px solid var(--border); border-radius: 10px; cursor: pointer;
transition: all 0.2s; display: flex; align-items: center; gap: 12px; background: var(--card);
}
.model-card.active { border-color: var(--primary); background: rgba(255, 77, 77, 0.1); }
.model-card i { font-size: 1.2rem; color: var(--muted); }
.model-card.active i { color: var(--primary); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 5px; }
.status-online { background: #22c55e; box-shadow: 0 0 8px #22c55e; }
.status-offline { background: #ef4444; }
.status-text { font-size: 0.6rem; text-transform: uppercase; font-weight: bold; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
const QWEN_LORA_OPTIONS = [
'Multiple-Angles', 'Photo-to-Anime', 'Anime-V2', 'Light-Migration', 'Upscaler',
'Style-Transfer', 'Manga-Tone', 'Anything2Real', 'Fal-Multiple-Angles', 'Polaroid-Photo',
'Unblur-Anything', 'Midnight-Noir-Eyes-Spotlight', 'Hyper-Realistic-Portrait',
'Ultra-Realistic-Portrait', 'Pixar-Inspired-3D', 'Noir-Comic-Book', 'Any-light',
'Studio-DeLight', 'Cinematic-FlatLog'
];
const FLUX_STYLE_OPTIONS = [
'None', 'Cinematic', 'Anime', 'Cyberpunk', 'Fantasy', '3D Render', 'Oil Painting', 'Sketch', 'Comic Book', 'Photography'
];
function App() {
const [images, setImages] = useState([]);
const [previews, setPreviews] = useState([]);
const [prompt, setPrompt] = useState('');
const [model, setModel] = useState('firered');
const [lora, setLora] = useState('Photo-to-Anime');
const [fluxStyle, setFluxStyle] = useState('None');
const [steps, setSteps] = useState(4);
const [guidance, setGuidance] = useState(1.0);
const [seed, setSeed] = useState(0);
const [width, setWidth] = useState(1024);
const [height, setHeight] = useState(1024);
const [azimuth, setAzimuth] = useState(0);
const [elevation, setElevation] = useState(0);
const [distance, setDistance] = useState(1.0);
const [rewritePrompt, setRewritePrompt] = useState(false);
const [modelStatus, setModelStatus] = useState({});
const [isGenerating, setIsGenerating] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [result, setResult] = useState(null);
const API_URL = window.location.origin;
React.useEffect(() => {
const checkStatus = async () => {
try {
const res = await fetch(`${API_URL}/status`);
const data = await res.json();
setModelStatus(data);
} catch (e) {
console.error("Error checking status:", e);
}
};
checkStatus();
const interval = setInterval(checkStatus, 30000); // Check every 30s
return () => clearInterval(interval);
}, []);
const processFiles = (files) => {
if (files.length > 0) {
setImages(files);
const newPreviews = files.map(file => URL.createObjectURL(file));
setPreviews(newPreviews);
}
};
const handleImageChange = (e) => {
processFiles(Array.from(e.target.files));
};
const handleDragOver = (e) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'));
processFiles(files);
};
const useOutputAsInput = async () => {
if (!result) return;
try {
const response = await fetch(`${API_URL}${result}`);
const blob = await response.blob();
const file = new File([blob], `edit_iteration_${Date.now()}.webp`, { type: 'image/webp' });
setImages([file]);
setPreviews([URL.createObjectURL(file)]);
setResult(null);
} catch (e) {
alert("Error al transferir la imagen");
}
};
const generateImage = async () => {
if (model !== 'turbo' && model !== 'banana' && images.length === 0) return alert("Sube al menos una imagen para editar");
if (!prompt) return alert("Escribe un prompt descriptivo");
setIsGenerating(true);
setResult(null);
const formData = new FormData();
if (images.length > 0) {
images.forEach(img => formData.append('images', img));
}
formData.append('prompt', prompt);
formData.append('model', model);
formData.append('lora_adapter', lora);
formData.append('style_name', fluxStyle);
formData.append('steps', steps);
formData.append('guidance_scale', guidance);
formData.append('seed', seed);
formData.append('width', width);
formData.append('height', height);
formData.append('azimuth', azimuth);
formData.append('elevation', elevation);
formData.append('distance', distance);
formData.append('rewrite_prompt', rewritePrompt);
formData.append('randomize_seed', true);
try {
const res = await fetch(`${API_URL}/edit-image`, { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
setResult(data.images[0]);
} else {
alert("Error: " + (data.detail || "Error desconocido"));
}
} catch (e) {
alert("Error de conexión");
} finally {
setIsGenerating(false);
}
};
return (
<div className="app-layout">
<div className="sidebar">
<div className="logo">
<i className="fa-solid fa-wand-sparkles"></i>
<span>AI Studio <span>PRO</span></span>
</div>
<div className="form-group">
<label>Seleccionar Modelo</label>
<div style={{display:'flex', flexDirection:'column', gap:'8px'}}>
<div className={`model-card ${model === 'firered' ? 'active' : ''}`} onClick={() => setModel('firered')}>
<i className="fa-solid fa-fire"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>FireRed Fast</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Velocidad extrema (Edición)</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus.firered ? 'status-online' : 'status-offline'}`}></span>
{modelStatus.firered ? 'Online' : 'Offline'}
</div>
</div>
<div className={`model-card ${model === 'qwen' ? 'active' : ''}`} onClick={() => setModel('qwen')}>
<i className="fa-solid fa-robot"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen Lora</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Precisión con estilos LoRA</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus.qwen ? 'status-online' : 'status-offline'}`}></span>
{modelStatus.qwen ? 'Online' : 'Offline'}
</div>
</div>
<div className={`model-card ${model === 'flux' ? 'active' : ''}`} onClick={() => setModel('flux')}>
<i className="fa-solid fa-bolt"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Flux Klein</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Calidad FLUX.2 LoRA Studio</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus.flux ? 'status-online' : 'status-offline'}`}></span>
{modelStatus.flux ? 'Online' : 'Offline'}
</div>
</div>
<div className={`model-card ${model === 'turbo' ? 'active' : ''}`} onClick={() => {setModel('turbo'); if(steps < 9) setSteps(9);}}>
<i className="fa-solid fa-gauge-high"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Turbo (Generation)</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Texto a Imagen Ultrarrápido</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus.turbo ? 'status-online' : 'status-offline'}`}></span>
{modelStatus.turbo ? 'Online' : 'Offline'}
</div>
</div>
<div className={`model-card ${model === 'banana' ? 'active' : ''}`} onClick={() => {setModel('banana'); if(steps < 20) setSteps(20);}}>
<i className="fa-solid fa-lemon"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Nano Banana</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Alta Calidad Flux (Generation)</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus.banana ? 'status-online' : 'status-offline'}`}></span>
{modelStatus.banana ? 'Online' : 'Offline'}
</div>
</div>
<div className={`model-card ${model === 'qwen_rapid' ? 'active' : ''}`} onClick={() => setModel('qwen_rapid')}>
<i className="fa-solid fa-bolt-lightning"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen Rapid AIO</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Edición SFW V2.3 (Fast)</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus.qwen_rapid ? 'status-online' : 'status-offline'}`}></span>
{modelStatus.qwen_rapid ? 'Online' : 'Offline'}
</div>
</div>
<div className={`model-card ${model === 'anypose' ? 'active' : ''}`} onClick={() => setModel('anypose')}>
<i className="fa-solid fa-person-walking"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen AnyPose</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Transferencia de Pose 2511</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus.anypose ? 'status-online' : 'status-offline'}`}></span>
{modelStatus.anypose ? 'Online' : 'Offline'}
</div>
</div>
<div className={`model-card ${model === 'qwen3_vl' ? 'active' : ''}`} onClick={() => setModel('qwen3_vl')}>
<i className="fa-solid fa-brain"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen3 VL MAX</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Vision-Language (Unredacted)</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus.qwen3_vl ? 'status-online' : 'status-offline'}`}></span>
{modelStatus.qwen3_vl ? 'Online' : 'Offline'}
</div>
</div>
<div className={`model-card ${model === '3d_camera' ? 'active' : ''}`} onClick={() => setModel('3d_camera')}>
<i className="fa-solid fa-camera-rotate"></i>
<div style={{flex:1}}>
<div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen 3D Camera</div>
<div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Control de Ángulo 3D</div>
</div>
<div className="status-text">
<span className={`status-dot ${modelStatus["3d_camera"] ? 'status-online' : 'status-offline'}`}></span>
{modelStatus["3d_camera"] ? 'Online' : 'Offline'}
</div>
</div>
</div>
</div>
{model === 'qwen' && (
<div className="form-group">
<label>Adaptador LoRA</label>
<select value={lora} onChange={(e) => setLora(e.target.value)}>
{QWEN_LORA_OPTIONS.map(opt => <option key={opt} value={opt}>{opt.replace(/-/g, ' ')}</option>)}
</select>
</div>
)}
{model === 'flux' && (
<div className="form-group">
<label>Estilo de Arte</label>
<select value={fluxStyle} onChange={(e) => setFluxStyle(e.target.value)}>
{FLUX_STYLE_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
</select>
</div>
)}
{(model === 'turbo' || model === 'banana') && (
<div className="form-group">
<label>Dimensiones (Ancho x Alto)</label>
<div style={{display:'flex', gap:'10px'}}>
<select value={width} onChange={(e) => setWidth(parseInt(e.target.value))} style={{flex:1}}>
<option value="512">512</option>
<option value="768">768</option>
<option value="1024">1024</option>
</select>
<select value={height} onChange={(e) => setHeight(parseInt(e.target.value))} style={{flex:1}}>
<option value="512">512</option>
<option value="768">768</option>
<option value="1024">1024</option>
</select>
</div>
</div>
)}
{model === '3d_camera' && (
<div style={{display:'flex', flexDirection:'column', gap:'15px', background:'rgba(255,255,255,0.03)', padding:'15px', borderRadius:'12px', border:'1px solid var(--border)'}}>
<div className="form-group">
<label>Azimuth (Horizontal: {azimuth}°)</label>
<input type="range" min="-180" max="180" value={azimuth} onChange={(e) => setAzimuth(parseFloat(e.target.value))} />
</div>
<div className="form-group">
<label>Elevation (Vertical: {elevation}°)</label>
<input type="range" min="-90" max="90" value={elevation} onChange={(e) => setElevation(parseFloat(e.target.value))} />
</div>
<div className="form-group">
<label>Distance (Zoom: {distance})</label>
<input type="range" min="0.1" max="2.0" step="0.1" value={distance} onChange={(e) => setDistance(parseFloat(e.target.value))} />
</div>
</div>
)}
{(model === 'qwen_rapid' || model === 'anypose') && (
<div className="form-group" style={{flexDirection:'row', alignItems:'center', gap:'10px', background:'rgba(255,255,255,0.03)', padding:'12px', borderRadius:'10px', border:'1px solid var(--border)', cursor:'pointer'}} onClick={() => setRewritePrompt(!rewritePrompt)}>
<input type="checkbox" checked={rewritePrompt} onChange={() => {}} style={{width:'18px', height:'18px', cursor:'pointer'}} />
<label style={{cursor:'pointer', margin:0, textTransform:'none'}}>Optimizar con Qwen-2.5-VL</label>
</div>
)}
{model === 'anypose' && (
<div style={{fontSize:'0.7rem', color:'var(--primary)', padding:'10px', background:'rgba(255,77,77,0.05)', borderRadius:'8px', border:'1px solid rgba(255,77,77,0.2)'}}>
<i className="fa-solid fa-circle-info"></i> AnyPose requiere 2 imágenes: la 1ª es la Referencia y la 2ª es la Pose.
</div>
)}
<div className="form-group">
<label>{(model === 'turbo' || model === 'banana') ? 'Prompt de Creación' : 'Prompt de Edición'}</label>
<textarea
placeholder={(model === 'turbo' || model === 'banana') ? "Describe la imagen que quieres crear..." : "Escribe los cambios deseados..."}
rows="3"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</div>
<div className="form-group">
<label>Pasos ({steps}) {(model !== 'turbo' && model !== 'banana') && `/ Guía (${guidance})`}</label>
<div style={{display:'flex', gap:'10px'}}>
<input type="range" min="1" max="50" value={steps} onChange={(e) => setSteps(parseInt(e.target.value))} style={{flex:1}} />
{(model !== 'turbo' && model !== 'banana') && (
<input type="range" min="0.1" max="10" step="0.1" value={guidance} onChange={(e) => setGuidance(parseFloat(e.target.value))} style={{flex:1}} />
)}
</div>
</div>
<button className="btn-generate" onClick={generateImage} disabled={isGenerating}>
{isGenerating ? <div className="loader"></div> : <><i className="fa-solid fa-magic-wand-sparkles"></i> {(model === 'turbo' || model === 'banana') ? 'Generar Imagen' : 'Ejecutar IA'}</>}
</button>
<div className="footer-info">
{model === 'firered' && 'Model: FireRed-Fast'}
{model === 'qwen' && 'Model: Qwen-2511-LoRA'}
{model === 'flux' && 'Model: FLUX.2-Klein'}
{model === 'turbo' && 'Model: Z-Image-Turbo'}
{model === 'banana' && 'Model: Nano-Banana'}
{model === 'qwen_rapid' && 'Model: Qwen-Rapid-AIO'}
{model === 'anypose' && 'Model: Qwen-AnyPose'}
{model === 'qwen3_vl' && 'Model: Qwen3-VL-MAX'}
{model === '3d_camera' && 'Model: Qwen-3D-Camera'}
</div>
</div>
<div className="main-content">
{previews.length === 0 && model !== 'turbo' ? (
<label
className={`dropzone ${isDragging ? 'drag-over' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<input type="file" multiple hidden onChange={handleImageChange} accept="image/*" />
<i className="fa-solid fa-images"></i>
<h2>Galería de Entrada</h2>
<p>Arrastra y suelta imágenes aquí o haz clic para subir</p>
</label>
) : (
<div style={{display: 'grid', gridTemplateColumns: (model === 'turbo' && previews.length === 0) ? '1fr' : '1fr 1fr', gap: '30px', height: '100%'}}>
{(previews.length > 0 || model !== 'turbo') && (
<div>
<label>Input Gallery ({previews.length})</label>
<div className="gallery-grid">
{previews.map((src, i) => (
<div key={i} className="gallery-item">
<img src={src} />
</div>
))}
</div>
{previews.length > 0 && (
<button onClick={() => {setImages([]); setPreviews([]); setResult(null);}} style={{marginTop:'15px', background:'transparent', color:'var(--muted)', border:'none', cursor:'pointer', fontSize:'0.8rem'}}>
<i className="fa-solid fa-trash"></i> Limpiar todo
</button>
)}
{model === 'turbo' && previews.length === 0 && (
<div style={{padding:'40px', color:'var(--muted)', background:'var(--sidebar)', borderRadius:'15px', marginTop:'20px'}}>
<p><i className="fa-solid fa-info-circle"></i> En modo Turbo, no necesitas subir imágenes. Describe tu idea a la izquierda.</p>
</div>
)}
</div>
)}
<div style={{maxWidth: (model === 'turbo' && previews.length === 0) ? '800px' : 'none', margin: '0 auto', width: '100%'}}>
<label>AI Output</label>
<div className="result-container">
{isGenerating && (
<div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:'15px', padding:'40px'}}>
<div className="loader" style={{width:'50px', height:'50px'}}></div>
<div style={{color:'var(--muted)', fontSize:'0.9rem'}}>Invocando a la IA...</div>
</div>
)}
{result && (
<>
<img src={`${API_URL}${result}`} className="result-img" />
<div style={{display:'flex', gap:'10px', width:'100%'}}>
<a href={`${API_URL}${result}`} download className="btn-generate" style={{background:'#22c55e', textDecoration:'none', flex:1}}>
<i className="fa-solid fa-download"></i> Descargar
</a>
<button onClick={useOutputAsInput} className="btn-generate" style={{background:'#3b82f6', flex:1}}>
<i className="fa-solid fa-arrow-left"></i> Usar como Entrada
</button>
</div>
</>
)}
{!result && !isGenerating && (
<div style={{padding:'60px 20px', color:'var(--muted)', border:'1px dashed var(--border)', borderRadius:'15px', textAlign:'center', width:'100%'}}>
<i className="fa-solid fa-wand-magic-sparkles" style={{fontSize:'2rem', marginBottom:'15px', opacity:0.3}}></i>
<br/>El resultado aparecerá aquí
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>