- Galerias Gaditanas

Breaking

BANNER 728X90

lunes, 24 de noviembre de 2025

import React, { useState, useCallback, useEffect } from 'react'; import { Loader2, Zap, Image, FileUp, X, Sparkles, Download, MessageSquare, Brush, Undo2, Maximize2 } from 'lucide-react'; // Constantes para la configuración de la API const API_KEY = ""; // Modelos para Imagen a Imagen (I2I) const IMAGE_MODEL_NAME = "gemini-2.5-flash-image-preview"; const IMAGE_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${IMAGE_MODEL_NAME}:generateContent?key=${API_KEY}`; // Modelo para Texto a Imagen (T2I) const T2I_MODEL_NAME = "imagen-4.0-generate-001"; const T2I_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${T2I_MODEL_NAME}:predict?key=${API_KEY}`; const TEXT_MODEL_NAME = "gemini-2.5-flash-preview-09-2025"; const TEXT_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/${TEXT_MODEL_NAME}:generateContent?key=${API_KEY}`; const MAX_RETRIES = 3; // DIMENSIONES SOLICITADAS POR EL USUARIO (1920x1080) const TARGET_WIDTH = 1920; const TARGET_HEIGHT = 1080; // Lista de autores de tebeos y el nuevo estilo const COMIC_AUTHORS = [ { id: 'vazquez', name: 'Manuel Vázquez Gallego', description: 'Línea suelta, humor absurdo (El botones Sacarino).' }, { id: 'ibanez', name: 'Francisco Ibáñez', description: 'Línea clara, expresividad exagerada (Mortadelo y Filemón).' }, { id: 'escobar', name: 'José Escobar', description: 'Diseño de personajes sencillo, gestos marcados (Zipi y Zape).' }, { id: 'sanchis', name: 'José Sanchis', description: 'Dibujo detallado y estético (El Capitán Trueno).' }, { id: 'ceras', name: 'Dibujo a Ceras (Crayón)', description: 'Estilo de ilustración con textura de cera, contornos difusos.' }, ]; /** * Función de utilidad para convertir un archivo de imagen a Base64. * @param {File} file - El objeto File a convertir. * @returns {Promise} La cadena Base64 (sin el prefijo 'data:image/...'). */ const fileToBase64 = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { // Extrae solo la parte Base64 (después de la coma) const base64String = reader.result.split(',')[1]; resolve(base64String); }; reader.onerror = (error) => reject(error); }); }; // Componente principal de la aplicación const App = () => { // Estado para la pestaña/modo actual: 'comic' o 'repair' const [mode, setMode] = useState('comic'); // Estados compartidos const [inputImageFile, setInputImageFile] = useState(null); const [inputImageBase64, setInputImageBase64] = useState(null); const [generatedImageUrl, setGeneratedImageUrl] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Estados específicos del modo 'comic' const [selectedStyle, setSelectedStyle] = useState(COMIC_AUTHORS[1].id); const [promptText, setPromptText] = useState(''); const [dialogueContext, setDialogueContext] = useState(''); const [generatedDialogue, setGeneratedDialogue] = useState(''); const [isDialogueLoading, setIsDialogueLoading] = useState(false); const currentAuthor = COMIC_AUTHORS.find(a => a.id === selectedStyle); // Efecto para limpiar la imagen generada y errores al cambiar de modo useEffect(() => { setGeneratedImageUrl(null); setGeneratedDialogue(''); setPromptText(''); setDialogueContext(''); setError(null); setIsLoading(false); setIsDialogueLoading(false); }, [mode]); const handleFileChange = async (event) => { setError(null); const file = event.target.files[0]; if (file) { if (!file.type.startsWith('image/')) { setError('Por favor, sube un archivo de imagen (JPEG, PNG).'); return; } try { const base64 = await fileToBase64(file); setInputImageBase64(base64); setInputImageFile(file); setGeneratedImageUrl(null); // Borrar resultado anterior al subir nueva imagen } catch (e) { setError('Error al procesar la imagen.'); console.error("Error al procesar el archivo:", e); } } else { setInputImageBase64(null); setInputImageFile(null); setGeneratedImageUrl(null); } }; const removeImage = () => { setInputImageFile(null); setInputImageBase64(null); setGeneratedImageUrl(null); setError(null); document.getElementById('file-upload-input').value = ''; }; /** * Función unificada para manejar las llamadas a la API de Imagen. * @param {string} finalPrompt - El prompt final para el modelo. * @param {boolean} isImageProvided - Indica si se ha proporcionado una imagen de entrada (I2I) o no (T2I). */ const callImageApi = async (finalPrompt, isImageProvided) => { // Definir la configuración de resolución para ambos modelos const imageConfig = { width: TARGET_WIDTH, height: TARGET_HEIGHT }; for (let i = 0; i < MAX_RETRIES; i++) { try { let response, result, base64Data; if (isImageProvided) { // --- Image-to-Image (I2I) usando gemini-2.5-flash-image-preview --- const payload = { contents: [ { role: "user", parts: [ { text: finalPrompt }, { inlineData: { mimeType: inputImageFile.type, data: inputImageBase64 } } ] } ], generationConfig: { responseModalities: ['TEXT', 'IMAGE'] }, config: imageConfig, }; response = await fetch(IMAGE_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`Error HTTP! Estado: ${response.status}`); } result = await response.json(); base64Data = result?.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData?.data; if (base64Data) { setGeneratedImageUrl(`data:image/png;base64,${base64Data}`); return; // Éxito I2I } else { throw new Error("Respuesta del modelo incompleta o sin imagen (I2I)."); } } else { // --- Text-to-Image (T2I) usando imagen-4.0-generate-001 --- const payload = { instances: [{ prompt: finalPrompt }], parameters: { sampleCount: 1, outputMimeType: "image/png", config: imageConfig } }; response = await fetch(T2I_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`Error HTTP! Estado: ${response.status}`); } result = await response.json(); base64Data = result.predictions && result.predictions.length > 0 && result.predictions[0].bytesBase64Encoded; if (base64Data) { // CORRECCIÓN: Se cambió 'base664' a 'base64' para que el navegador pueda decodificar la imagen correctamente. setGeneratedImageUrl(`data:image/png;base64,${base64Data}`); return; // Éxito T2I } else { throw new Error("Respuesta del modelo incompleta o sin imagen (T2I)."); } } } catch (e) { console.error(`Error en el intento ${i + 1}:`, e); if (i < MAX_RETRIES - 1) { await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000)); // Backoff exponencial } else { throw new Error(`Error al generar la imagen. Inténtalo de nuevo. (${e.message})`); } } } }; /** * Generación de Imagen para el modo CÓMIC. */ const handleGenerateComicImage = useCallback(async () => { const isImageProvided = !!inputImageBase64; if (!isImageProvided && !promptText) { setError('Para generar una imagen sin subir una foto, debes proporcionar una descripción en el paso 3.'); return; } if (!currentAuthor) { setError('Por favor, selecciona un estilo de autor.'); return; } setIsLoading(true); setError(null); setGeneratedImageUrl(null); let styleInstruction = ''; // Instrucciones que difieren si es I2I o T2I const i2iInstruction = `Transforma radicalmente la imagen proporcionada en una ILUSTRACIÓN, un DIBUJO HECHO A MANO y PURO. Prohíbido cualquier rastro de fotorealismo, texturas fotográficas, iluminación natural, o cualquier elemento que sugiera una foto procesada o un filtro. Reinterpreta todos los elementos (personas, objetos, fondos) como si hubieran sido dibujados desde cero.`; const t2iInstruction = `Genera un DIBUJO HECHO A MANO y PURO, basado en la siguiente descripción. Prohíbido cualquier rastro de fotorealismo, texturas fotográficas, iluminación natural, o cualquier elemento que sugiera una foto procesada.`; const generalInstruction = isImageProvided ? i2iInstruction : t2iInstruction; const textProhibition = `La imagen NUNCA debe incluir NINGÚN TIPO de texto, rótulo, letrero, palabra escrita, firma, marca de agua o globo de diálogo (speech bubble). El dibujo debe estar completamente limpio de elementos textuales.`; if (selectedStyle === 'ceras') { styleInstruction = `${generalInstruction} **ESTILO DE DIBUJO CON CERAS (CRAYÓN):** 1. Textura granulada y áspera, simulando cera. 2. Contornos gruesos. Colores brillantes, aplicados con trazos visibles. 3. Apariencia de boceto vigoroso y no pulido.`; } else { styleInstruction = `${generalInstruction} al estilo artístico inconfundible de ${currentAuthor.name} (${currentAuthor.description}). **ESTILO DE TEBEO CLÁSICO (TINTA):** 1. Líneas de contorno gruesas (Line Art). 2. Colores planos, saturados y limitados, sin degradados. 3. Exageración en gestos y expresiones (humorístico).`; } const userPromptText = promptText ? `Además, incluye estas instrucciones: "${promptText}".` : (isImageProvided ? 'Asegúrate de que la transformación al estilo sea fiel al ambiente y composición de la foto original.' : ''); // Se añade el requisito de dimensión en el prompt como refuerzo const dimensionConstraint = `El resultado debe ser una imagen de alta calidad, específicamente con una relación de aspecto de ${TARGET_WIDTH}:${TARGET_HEIGHT}.`; const finalPrompt = `${styleInstruction} ${textProhibition} ${userPromptText} ${dimensionConstraint}`; try { await callImageApi(finalPrompt, isImageProvided); } catch (e) { setError(e.message); } finally { setIsLoading(false); } }, [inputImageBase64, inputImageFile, selectedStyle, promptText, currentAuthor]); /** * Generación de Imagen para el modo REPARAR y COLORIZAR. */ const handleRepairImage = useCallback(async () => { // El modo reparación SIEMPRE requiere una imagen if (!inputImageBase64) { setError('Por favor, primero sube una foto antigua para repararla.'); return; } setIsLoading(true); setError(null); setGeneratedImageUrl(null); // Se añade el requisito de dimensión en el prompt como refuerzo const dimensionConstraint = `El resultado debe ser una imagen de alta calidad, específicamente con una relación de aspecto de ${TARGET_WIDTH}:${TARGET_HEIGHT}.`; const repairPrompt = `RESTORE AND COLORIZE: Act as a professional photo restorer. Analyze the historical image provided, remove all signs of damage, degradation, scratches, mold, or dust. Improve clarity and sharpness while maintaining historical authenticity. After restoration, colorize the image using natural, realistic colors, avoiding any stylized, artistic, or comic effects. The final output must be a fully restored, realistic, and colorized photo. ${dimensionConstraint}`; try { // Usamos true porque el modo Reparar siempre es I2I await callImageApi(repairPrompt, true); } catch (e) { setError(e.message); } finally { setIsLoading(false); } }, [inputImageBase64, inputImageFile]); /** * Genera un diálogo basado en el contexto y el autor seleccionado. */ const handleGenerateDialogue = useCallback(async () => { if (!dialogueContext || !currentAuthor) { console.warn('Faltan datos para generar el diálogo.'); return; } setIsDialogueLoading(true); setGeneratedDialogue(''); const systemInstruction = { parts: [{ text: `Actúa como un guionista experto de cómics españoles de la Escuela Bruguera, imitando el tono cómico y absurdo de ${currentAuthor.name}. Genera una línea de diálogo o una frase de pie de viñeta basada en la situación que se te proporciona. La respuesta debe ser fiel al estilo del autor, breve y punzante. Responde SÓLO con el texto del diálogo generado, sin explicaciones, títulos, comillas externas o encabezados.` }], }; const userQuery = `Situación: "${dialogueContext}". Genera un diálogo al estilo de ${currentAuthor.name}.`; const payload = { contents: [{ parts: [{ text: userQuery }] }], systemInstruction: systemInstruction, }; // Lógica de llamada a API de texto (gemini-2.5-flash) for (let i = 0; i < MAX_RETRIES; i++) { try { const response = await fetch(TEXT_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`Error HTTP! Estado: ${response.status}`); } const result = await response.json(); const text = result.candidates?.[0]?.content?.parts?.[0]?.text; if (text) { setGeneratedDialogue(text.trim()); return; // Éxito } else { throw new Error("Respuesta del modelo incompleta o sin texto."); } } catch (e) { console.error(`Error en el intento ${i + 1} (Texto):`, e); if (i < MAX_RETRIES - 1) { await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000)); } else { setError(`Error al generar el diálogo. Inténtalo de nuevo. (${e.message})`); } } } setIsDialogueLoading(false); }, [dialogueContext, currentAuthor]); const copyDialogue = () => { if (generatedDialogue) { const el = document.createElement('textarea'); el.value = generatedDialogue; document.body.appendChild(el); el.select(); document.execCommand('copy'); console.log('Diálogo copiado al portapapeles.'); } }; /** * Componente para el panel de configuración del Modo CÓMIC. */ const ComicConfigPanel = () => ( <> {/* 2. Elige Estilo de Autor */}

2. Elige un Estilo de Dibujo

{COMIC_AUTHORS.map((author) => ( ))}
{/* 3. Añade Personajes/Instrucciones (para la Imagen) */}

3. Descripción del Dibujo (Necesaria si no hay foto)