From 0ba05b6483a80ac3c99068cc2e9fb9e2b5097ede Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 26 Nov 2025 19:00:04 +0000 Subject: [PATCH] Initial commit --- .gitignore | 26 ++ docker-compose.yml | 12 + frontend/.env.local.example | 6 + frontend/.gitignore | 37 ++ frontend/Dockerfile | 15 + frontend/README.md | 138 ++++++++ frontend/START.md | 88 +++++ frontend/app/api/generate-image/route.ts | 154 ++++++++ frontend/app/api/generate-video/route.ts | 163 +++++++++ frontend/app/globals.css | 55 +++ frontend/app/layout.tsx | 22 ++ frontend/app/page.tsx | 426 +++++++++++++++++++++++ frontend/components/image-card.tsx | 98 ++++++ frontend/components/image-config.tsx | 115 ++++++ frontend/components/model-selector.tsx | 50 +++ frontend/components/reference-images.tsx | 169 +++++++++ frontend/components/video-card.tsx | 79 +++++ frontend/components/video-config.tsx | 118 +++++++ frontend/lib/google-ai-server.ts | 29 ++ frontend/lib/google-ai.ts | 180 ++++++++++ frontend/lib/utils.ts | 26 ++ frontend/next.config.js | 6 + frontend/postcss.config.mjs | 9 + frontend/tailwind.config.ts | 43 +++ pyproject.toml | 14 + test/banana.py | 166 +++++++++ test/list_models.py | 273 +++++++++++++++ 27 files changed, 2517 insertions(+) create mode 100644 .gitignore create mode 100644 docker-compose.yml create mode 100644 frontend/.env.local.example create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/START.md create mode 100644 frontend/app/api/generate-image/route.ts create mode 100644 frontend/app/api/generate-video/route.ts create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/components/image-card.tsx create mode 100644 frontend/components/image-config.tsx create mode 100644 frontend/components/model-selector.tsx create mode 100644 frontend/components/reference-images.tsx create mode 100644 frontend/components/video-card.tsx create mode 100644 frontend/components/video-config.tsx create mode 100644 frontend/lib/google-ai-server.ts create mode 100644 frontend/lib/google-ai.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/next.config.js create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/tailwind.config.ts create mode 100644 pyproject.toml create mode 100644 test/banana.py create mode 100644 test/list_models.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a15fcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Entorno virtual +.venv/ +venv/ +env/ + +# Archivos de Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Credenciales y configuración sensible +*.json +!pyproject.toml + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Distribución +dist/ +build/ +*.egg-info/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..da9ae13 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + frontend: + build: + context: ./frontend + ports: + - "3000:3000" + env_file: + - ./frontend/.env + environment: + - NODE_ENV=production diff --git a/frontend/.env.local.example b/frontend/.env.local.example new file mode 100644 index 0000000..fd9fd8b --- /dev/null +++ b/frontend/.env.local.example @@ -0,0 +1,6 @@ +# Google Generative AI API Key (OPCIONAL) +# El proyecto usa automáticamente las credenciales del archivo bnt-ia-innovacion-new.json +# Solo necesitas configurar esto si quieres usar una API key diferente +# Obtén tu API key en: https://makersuite.google.com/app/apikey + +# GOOGLE_API_KEY=tu_api_key_aqui diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..00bba9b --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..1ae1dc4 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,15 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +RUN npm run build + +EXPOSE 3000 + +CMD ["npm", "start"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..5ebb515 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,138 @@ +# 🎨 Image Playground - Google Generative AI + +Playground interactivo para generar imágenes usando los modelos de Google Generative AI (Gemini). + +## ✨ Características + +- 🤖 **Múltiples modelos de Gemini** - Elige entre diferentes versiones de Gemini +- 🎭 **Configuración completa** - Aspect ratios, negative prompts, safety levels +- 🖼️ **Visualización en tiempo real** - Ve tus imágenes generadas al instante +- 💾 **Descarga directa** - Guarda las imágenes con un clic +- 📱 **Responsive** - Funciona en desktop y mobile +- ⚡ **Vercel AI SDK** - Integración completa con el SDK oficial + +## 🚀 Instalación + +1. **Instalar dependencias:** +```bash +cd frontend +npm install +``` + +2. **Configuración automática:** + +El proyecto usa automáticamente las credenciales del archivo `bnt-ia-innovacion-new.json` ubicado en la raíz del proyecto. **No necesitas configurar nada más.** + +Si quieres usar una API key diferente (opcional), crea un archivo `.env.local`: + +```bash +cp .env.local.example .env.local +# Edita y descomenta GOOGLE_API_KEY +``` + +3. **Ejecutar el proyecto:** +```bash +npm run dev +``` + +Abre [http://localhost:3000](http://localhost:3000) en tu navegador. + +## 📦 Estructura del Proyecto + +``` +frontend/ +├── app/ +│ ├── api/ +│ │ └── generate-image/ +│ │ └── route.ts # API endpoint para generación +│ ├── layout.tsx # Layout principal +│ ├── page.tsx # Página del playground +│ └── globals.css # Estilos globales +├── components/ +│ ├── model-selector.tsx # Selector de modelos +│ ├── image-config.tsx # Configuración de imágenes +│ └── image-card.tsx # Tarjeta de imagen generada +├── lib/ +│ ├── google-ai.ts # Cliente de Google AI +│ └── utils.ts # Utilidades +└── package.json +``` + +## 🎯 Uso + +1. **Selecciona un modelo** en el panel lateral +2. **Configura los parámetros:** + - Aspect Ratio (1:1, 16:9, etc.) + - Número de imágenes + - Negative prompt (opcional) + - Nivel de seguridad +3. **Escribe tu prompt** describiendo la imagen que quieres +4. **Haz clic en "Generar Imagen"** o presiona Enter +5. **Descarga las imágenes** haciendo hover y clic en el botón de descarga + +## 🛠️ Tecnologías + +- **Next.js 14** - Framework React con App Router +- **TypeScript** - Type safety +- **Vercel AI SDK** - Integración con IA +- **Google Generative AI SDK** - API de Google Gemini +- **Tailwind CSS** - Estilos +- **Lucide Icons** - Iconos + +## 🔧 Configuración Avanzada + +### Modelos Disponibles + +El playground incluye estos modelos de Gemini: + +- `gemini-2.0-flash-exp` - Experimental, rápido +- `gemini-exp-1206` - Experimental con mejoras +- `gemini-1.5-pro` - Más capaz para tareas complejas +- `gemini-1.5-flash` - Rápido y eficiente + +### Aspect Ratios Soportados + +- `1:1` - Cuadrado (1024x1024) +- `3:4` - Vertical (768x1024) +- `4:3` - Horizontal (1024x768) +- `9:16` - Móvil vertical (576x1024) +- `16:9` - Widescreen (1024x576) + +## 🤝 Integración con el Backend Python + +El proyecto también incluye un backend en Python en `/backend` que puedes usar alternativamente: + +```bash +cd ../backend +python list_models.py +``` + +## 📝 Notas + +- Las imágenes se generan usando la API de Google Generative AI +- Asegúrate de tener cuota suficiente en tu cuenta de Google Cloud +- Algunos prompts pueden ser bloqueados por políticas de seguridad +- Las imágenes se guardan temporalmente en el navegador + +## 🐛 Troubleshooting + +**Error: "API key inválida"** +- Verifica que tu API key esté correctamente configurada en `.env.local` +- Asegúrate de que la API key tenga permisos para Generative AI + +**Error: "No se generaron imágenes"** +- Prueba con un prompt más descriptivo +- Cambia el modelo seleccionado +- Verifica los logs del servidor en la consola + +**Las imágenes no se muestran** +- Verifica la consola del navegador para errores +- Asegúrate de que el modelo soporte generación de imágenes + +## 📄 Licencia + +MIT + +## 🙋 Soporte + +Si encuentras algún problema o tienes preguntas, crea un issue en el repositorio. diff --git a/frontend/START.md b/frontend/START.md new file mode 100644 index 0000000..f23a745 --- /dev/null +++ b/frontend/START.md @@ -0,0 +1,88 @@ +# 🚀 Inicio Rápido + +## Pasos para ejecutar el proyecto: + +### 1. Instalar dependencias +```bash +npm install +``` + +### 2. Configuración (Automática) + +✅ **El proyecto usa automáticamente las credenciales del archivo `bnt-ia-innovacion-new.json`** + +No necesitas configurar nada adicional. El archivo JSON ya contiene todas las credenciales necesarias. + +Si quieres usar una API key diferente (opcional): +```bash +cp .env.local.example .env.local +# Edita y descomenta GOOGLE_API_KEY +``` + +### 3. Ejecutar el proyecto + +```bash +npm run dev +``` + +### 4. Abrir en el navegador + +Abre http://localhost:3000 + +--- + +## 📝 Notas Importantes + +### Sobre la Autenticación + +El proyecto usa **Google Generative AI SDK** (`@google/generative-ai`) con las credenciales del archivo `bnt-ia-innovacion-new.json`. + +La autenticación es automática mediante `google-auth-library` que lee el Service Account del JSON. + +### Verificar que todo funciona + +1. Abre http://localhost:3000 +2. Selecciona un modelo (por defecto: gemini-2.0-flash-exp) +3. Escribe un prompt simple: "un gato naranja" +4. Haz clic en "Generar Imagen" + +Si ves errores: +- Verifica que la API key sea válida +- Revisa los logs en la terminal +- Revisa la consola del navegador (F12) + +--- + +## 🔧 Comandos útiles + +```bash +# Desarrollo +npm run dev + +# Build para producción +npm run build + +# Ejecutar producción +npm start + +# Linting +npm run lint +``` + +--- + +## 🐛 Problemas Comunes + +### Error: "API key not valid" +- Verifica que copiaste correctamente la API key +- Asegúrate de que el archivo `.env.local` existe +- Reinicia el servidor de desarrollo (Ctrl+C y `npm run dev` de nuevo) + +### Error: "Module not found" +- Ejecuta `npm install` de nuevo +- Borra `node_modules` y `.next`, luego `npm install` + +### Las imágenes no se generan +- Algunos modelos de Gemini aún no soportan generación de imágenes +- Prueba con diferentes prompts +- Revisa los logs del servidor para más detalles diff --git a/frontend/app/api/generate-image/route.ts b/frontend/app/api/generate-image/route.ts new file mode 100644 index 0000000..b7d14eb --- /dev/null +++ b/frontend/app/api/generate-image/route.ts @@ -0,0 +1,154 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { type ImageGenerationConfig, getSafetySettings } from '@/lib/google-ai'; +import { vertexAI } from '@/lib/google-ai-server'; + +export const runtime = 'nodejs'; +export const maxDuration = 60; + +export async function POST(req: NextRequest) { + try { + const body: ImageGenerationConfig = await req.json(); + + const { + model, + prompt, + aspectRatio = '1:1', + numberOfImages = 1, + negativePrompt, + temperature = 1, + safetyFilterLevel = 'block_some', + referenceImages, + } = body; + + if (!prompt || !model) { + return NextResponse.json( + { error: 'Prompt y modelo son requeridos' }, + { status: 400 } + ); + } + + + // Obtener el modelo generativo de Vertex AI + const generativeModel = vertexAI.getGenerativeModel({ + model: model, + safetySettings: getSafetySettings(safetyFilterLevel) as any, + generationConfig: { + temperature: temperature, + maxOutputTokens: 8192, + }, + }); + + // Construir el prompt completo + let fullPrompt = prompt; + if (negativePrompt) { + fullPrompt += `\n\nNo incluir: ${negativePrompt}`; + } + + // Agregar instrucciones de aspect ratio si no es 1:1 + if (aspectRatio !== '1:1') { + fullPrompt += `\n\nGenera la imagen en formato ${aspectRatio}.`; + } + + // Construir parts con imágenes de referencia si existen + const parts: any[] = []; + + // Agregar imágenes de referencia primero + if (referenceImages && referenceImages.length > 0) { + for (const refImg of referenceImages) { + parts.push({ + inlineData: { + data: refImg.data, + mimeType: refImg.mimeType, + }, + }); + } + } + + // Agregar el prompt de texto + parts.push({ text: fullPrompt }); + + // Generar contenido + const result = await generativeModel.generateContent({ + contents: [ + { + role: 'user', + parts: parts, + }, + ], + }); + + const response = result.response; + + // Extraer imágenes de la respuesta + const images: string[] = []; + + if (response.candidates && response.candidates.length > 0) { + for (const candidate of response.candidates) { + if (candidate.content?.parts) { + for (const part of candidate.content.parts) { + // Verificar si la parte contiene datos inline (imagen) + if ('inlineData' in part && part.inlineData) { + const imageData = part.inlineData.data; + if (imageData) { + images.push(imageData); + } + } + } + } + } + } + + // Si no hay imágenes, intentar generar más información de debug + if (images.length === 0) { + console.error('No se encontraron imágenes en la respuesta'); + + return NextResponse.json( + { + error: 'El modelo no generó imágenes', + details: 'El modelo respondió pero no incluyó datos de imagen', + response: response, + suggestion: 'Prueba con un prompt más descriptivo o cambia el modelo', + }, + { status: 500 } + ); + } + + return NextResponse.json({ + images: images, + model: model, + prompt: prompt, + aspectRatio: aspectRatio, + count: images.length, + }); + + } catch (error: any) { + console.error('Error generando imagen:', error); + + // Manejar errores específicos de la API + let errorMessage = 'Error generando imagen'; + let errorDetails = error.message; + + if (error.message?.includes('credentials') || error.message?.includes('authentication')) { + errorMessage = 'Error de autenticación'; + errorDetails = 'Verifica las credenciales del Service Account'; + } else if (error.message?.includes('quota')) { + errorMessage = 'Límite de cuota excedido'; + errorDetails = 'Has alcanzado el límite de uso de la API'; + } else if (error.message?.includes('safety')) { + errorMessage = 'Contenido bloqueado por seguridad'; + errorDetails = 'El prompt fue bloqueado por políticas de seguridad'; + } else if (error.message?.includes('permission')) { + errorMessage = 'Error de permisos'; + errorDetails = 'El Service Account no tiene permisos para Vertex AI'; + } + + return NextResponse.json( + { + error: errorMessage, + details: errorDetails, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, + }, + { status: 500 } + ); + } +} diff --git a/frontend/app/api/generate-video/route.ts b/frontend/app/api/generate-video/route.ts new file mode 100644 index 0000000..d621a68 --- /dev/null +++ b/frontend/app/api/generate-video/route.ts @@ -0,0 +1,163 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { type VideoGenerationConfig } from '@/lib/google-ai'; +import { projectId, location, credentials } from '@/lib/google-ai-server'; +import { GoogleAuth } from 'google-auth-library'; + +export const runtime = 'nodejs'; +export const maxDuration = 300; // Los videos pueden tardar más + +export async function POST(req: NextRequest) { + try { + const body: VideoGenerationConfig = await req.json(); + + const { + model, + prompt, + aspectRatio = '16:9', + duration = 5, + negativePrompt, + } = body; + + if (!prompt || !model) { + return NextResponse.json( + { error: 'Prompt y modelo son requeridos' }, + { status: 400 } + ); + } + + + // Configurar autenticación + const auth = new GoogleAuth({ + credentials: credentials, + scopes: ['https://www.googleapis.com/auth/cloud-platform'], + }); + + const client = await auth.getClient(); + const accessToken = await client.getAccessToken(); + + if (!accessToken.token) { + throw new Error('No se pudo obtener el token de acceso'); + } + + // Construir el endpoint de la API + const endpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:predict`; + + // Construir el prompt completo + let fullPrompt = prompt; + if (negativePrompt) { + fullPrompt += `\n\nNegative prompt: ${negativePrompt}`; + } + + // Construir el payload + const payload = { + instances: [ + { + prompt: fullPrompt, + }, + ], + parameters: { + aspectRatio: aspectRatio, + videoDuration: `${duration}s`, + }, + }; + + console.log(`Endpoint: ${endpoint}`); + + // Hacer la solicitud a la API de Vertex AI + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + const data = await response.json(); + + if (!response.ok) { + console.error('Error de la API:', data); + throw new Error( + data.error?.message || + `Error ${response.status}: ${response.statusText}` + ); + } + + + // Extraer video de la respuesta + let videoData: string | null = null; + + if (data.predictions && data.predictions.length > 0) { + const prediction = data.predictions[0]; + + // Buscar el video en diferentes formatos posibles + if (prediction.bytesBase64Encoded) { + videoData = prediction.bytesBase64Encoded; + } else if (prediction.videoBase64) { + videoData = prediction.videoBase64; + } else if (prediction.video) { + videoData = prediction.video; + } + } + + // Si no hay video, retornar error con más información + if (!videoData) { + console.error('No se encontró video en la respuesta'); + + return NextResponse.json( + { + error: 'El modelo no generó un video', + details: 'El modelo respondió pero no incluyó datos de video en el formato esperado', + response: data, + suggestion: 'Los modelos Veo pueden necesitar acceso especial o estar en preview. Verifica que tu proyecto tenga acceso a estos modelos.', + }, + { status: 500 } + ); + } + + return NextResponse.json({ + videoData: videoData, + model: model, + prompt: prompt, + aspectRatio: aspectRatio, + duration: duration, + }); + + } catch (error: any) { + console.error('Error generando video:', error); + + // Manejar errores específicos de la API + let errorMessage = 'Error generando video'; + let errorDetails = error.message; + + if (error.message?.includes('credentials') || error.message?.includes('authentication')) { + errorMessage = 'Error de autenticación'; + errorDetails = 'Verifica las credenciales del Service Account'; + } else if (error.message?.includes('Quota exceeded') || error.message?.includes('quota')) { + errorMessage = 'Cuota de uso excedida'; + errorDetails = 'Necesitas solicitar un aumento de cuota para los modelos Veo en Google Cloud Console. Ve a: https://console.cloud.google.com/iam-admin/quotas y busca "Online prediction requests per base model"'; + } else if (error.message?.includes('safety')) { + errorMessage = 'Contenido bloqueado por seguridad'; + errorDetails = 'El prompt fue bloqueado por políticas de seguridad'; + } else if (error.message?.includes('permission') || error.message?.includes('403')) { + errorMessage = 'Error de permisos'; + errorDetails = 'El Service Account no tiene permisos para Vertex AI o los modelos Veo no están habilitados en tu proyecto'; + } else if (error.message?.includes('not found') || error.message?.includes('404')) { + errorMessage = 'Modelo no encontrado'; + errorDetails = 'El modelo Veo puede no estar disponible en tu región (us-central1). Prueba solicitar acceso a los modelos Veo en Google Cloud Console.'; + } else if (error.message?.includes('Internal')) { + errorMessage = 'Error interno del servidor'; + errorDetails = 'Los modelos Veo están en preview y pueden tener disponibilidad limitada. Intenta con otro modelo o más tarde.'; + } + + return NextResponse.json( + { + error: errorMessage, + details: errorDetails, + fullError: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, + }, + { status: 500 } + ); + } +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..bc1f724 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,55 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..c69f2d6 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'Image Playground - Google Generative AI', + description: 'Genera imágenes con modelos de Google Generative AI', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..910c285 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,426 @@ +'use client'; + +import { useState } from 'react'; +import { Sparkles, Loader2, ImageIcon, Video } from 'lucide-react'; +import { ModelSelector } from '@/components/model-selector'; +import { ImageConfig } from '@/components/image-config'; +import { VideoConfig } from '@/components/video-config'; +import { ImageCard } from '@/components/image-card'; +import { VideoCard } from '@/components/video-card'; +import { ReferenceImages } from '@/components/reference-images'; + +type ContentType = 'image' | 'video'; + +interface GeneratedImage { + id: string; + imageData: string; + prompt: string; + model: string; + aspectRatio: string; + timestamp: number; +} + +interface GeneratedVideo { + id: string; + videoData: string; + prompt: string; + model: string; + aspectRatio: string; + duration: number; + timestamp: number; +} + +interface ReferenceImage { + id: string; + data: string; + mimeType: string; + preview: string; + name: string; +} + +export default function Home() { + // Estado del tipo de contenido + const [contentType, setContentType] = useState('image'); + + // Estado del modelo y configuración + const [selectedModel, setSelectedModel] = useState('gemini-2.5-flash-image'); + const [aspectRatio, setAspectRatio] = useState('1:1'); + const [numberOfImages, setNumberOfImages] = useState(1); + const [videoDuration, setVideoDuration] = useState(5); + const [negativePrompt, setNegativePrompt] = useState(''); + const [safetyLevel, setSafetyLevel] = useState<'block_none' | 'block_some' | 'block_most'>('block_some'); + const [referenceImages, setReferenceImages] = useState([]); + + // Estado del chat + const [prompt, setPrompt] = useState(''); + const [isGenerating, setIsGenerating] = useState(false); + const [generatedImages, setGeneratedImages] = useState([]); + const [generatedVideos, setGeneratedVideos] = useState([]); + const [error, setError] = useState(null); + + // Cambiar tipo de contenido y actualizar modelo por defecto + const handleContentTypeChange = (type: ContentType) => { + setContentType(type); + if (type === 'image') { + setSelectedModel('gemini-2.5-flash-image'); + setAspectRatio('1:1'); + } else { + setSelectedModel('veo-3.0-generate-001'); + setAspectRatio('16:9'); + } + }; + + const handleGenerateImage = async () => { + if (!prompt.trim()) return; + + setIsGenerating(true); + setError(null); + + try { + const response = await fetch('/api/generate-image', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: selectedModel, + prompt: prompt.trim(), + aspectRatio, + numberOfImages, + negativePrompt: negativePrompt.trim() || undefined, + safetyFilterLevel: safetyLevel, + temperature: 1, + referenceImages: referenceImages.length > 0 + ? referenceImages.map(img => ({ + data: img.data, + mimeType: img.mimeType, + })) + : undefined, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.details || data.error || 'Error generando imagen'); + } + + // Agregar imágenes generadas al historial + const newImages: GeneratedImage[] = data.images.map((imageData: string, index: number) => ({ + id: `${Date.now()}-${index}`, + imageData, + prompt: prompt.trim(), + model: selectedModel, + aspectRatio, + timestamp: Date.now(), + })); + + setGeneratedImages([...newImages, ...generatedImages]); + setPrompt(''); // Limpiar prompt después de generar + + } catch (err: any) { + setError(err.message); + } finally { + setIsGenerating(false); + } + }; + + const handleGenerateVideo = async () => { + if (!prompt.trim()) return; + + setIsGenerating(true); + setError(null); + + try { + const response = await fetch('/api/generate-video', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: selectedModel, + prompt: prompt.trim(), + aspectRatio, + duration: videoDuration, + negativePrompt: negativePrompt.trim() || undefined, + safetyFilterLevel: safetyLevel, + temperature: 1, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.details || data.error || 'Error generando video'); + } + + // Agregar video generado al historial + const newVideo: GeneratedVideo = { + id: `${Date.now()}`, + videoData: data.videoData, + prompt: prompt.trim(), + model: selectedModel, + aspectRatio, + duration: videoDuration, + timestamp: Date.now(), + }; + + setGeneratedVideos([newVideo, ...generatedVideos]); + setPrompt(''); // Limpiar prompt después de generar + + } catch (err: any) { + setError(err.message); + } finally { + setIsGenerating(false); + } + }; + + const handleGenerate = () => { + if (contentType === 'image') { + handleGenerateImage(); + } else { + handleGenerateVideo(); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleGenerate(); + } + }; + + return ( +
+
+ {/* Header */} +
+
+ +

+ AI Playground +

+
+

+ Genera imágenes y videos con Google Generative AI +

+
+ + {/* Selector de tipo de contenido */} +
+
+ + +
+
+ +
+ {/* Panel de configuración */} +
+
+ + +
+ {contentType === 'image' ? ( + + ) : ( + + )} +
+ + {contentType === 'image' && ( +
+ +
+ )} +
+ + {/* Stats */} +
+
+ {contentType === 'image' ? ( + + ) : ( +
+
+ {contentType === 'image' ? ( +

Imágenes generadas: {generatedImages.length}

+ ) : ( +

Videos generados: {generatedVideos.length}

+ )} +

Modelo actual: {selectedModel}

+
+
+
+ + {/* Área principal */} +
+ {/* Input de prompt */} +
+ +
+