
Generación de IA para juegos: optimizar costes con caché
Construye un pipeline con caché prioritario, claves semánticas, single-flight, sondeo asincrónico, límites presupuestarios y análisis coste-aceptación.
El caché puede reducir el coste medio de la API de imágenes por acción del jugador a $0.002, pero no puede hacer que una imagen genuinamente nueva salga más barata. Con la tarifa actual de Z-Image en reAPI de $0.005 por imagen completada, la regla de control es tasa de fallos pagados × intentos completados por activo aceptado ≤ 0.4. Una tasa de fallos del 40 % llega al límite solo cuando cada nuevo activo pasa en su primer intento completado; mantenerse por debajo de $0.002 requiere un resultado por debajo de ese límite.[1]
Cachea el significado de un objeto, no solo su prompt. Fusiona fallos concurrentes, pregeneра solo objetos probables y mide el coste por activo aceptado de juego en lugar de por respuesta de API.
TL;DR
- Con $0.005 por generación completada de Z-Image, un juego que busque un coste efectivo
de $0.002 o inferior por creación necesita
tasa de fallos pagados × intentos completados por activo aceptado ≤ 0.4. Con exactamente un intento, el 40 % alcanza el límite; estrictamente por debajo significa menos del 40 %.[1] - Mantén una clave de receta para lo que el jugador combinó y una clave de activo semántico para el objeto resultante y versión de arte.
- Aplica una restricción de unicidad de base de datos para un trabajo de activo activo compartido. Un mapa de Promise en memoria puede fusionar llamadas del mismo proceso, pero no es el bloqueo que protege múltiples workers.
- La generación de imágenes es asincrónica. Envía una vez, mantén el ID de tarea, sondea el endpoint de tarea y copia la salida completada a almacenamiento que controlas. [2][3]
- Una prueba de humo de tres tareas completó tres candidatos originales en el primer envío en 15 créditos o $0.015 total. No fueron revisados visualmente, así que son candidatos, no activos aceptados.[4]
- Si 3.000 imágenes diarias ya son activos semánticos genuinamente nuevos después del caché, un caché convencional no rescatará la economía. El juego debe reutilizar más arte, estrechar la superficie generativa, o aceptar un coste mayor.
El problema real es coste por creación, no coste por imagen
Supón que un juego de combinaciones infinitas une objetos como hierro y fuego, y genera tanto un resultado nuevo como su imagen. ¿Cómo puede mantener el coste multimedia por debajo de $0.002 por creación si los jugadores activan 3.000 creaciones al día, muchas de ellas combinaciones nunca vistas? La pregunta deja al descubierto la restricción principal: el juego puede producir solicitudes nuevas más rápido de lo que sus ingresos permiten pagarlas.
Empieza con esta ecuación:
coste diario de generación =
eventos de creación
x tasa de fallos pagados
x intentos completados por activo aceptado
x precio por generación completadaCon $0.005 por tarea de Z-Image completada, el límite es:
$0.005 x tasa de fallos pagados x intentos completados por activo aceptado <= $0.002
tasa de fallos pagados x intentos completados por activo aceptado <= 0.4Para 3.000 eventos de creación al día durante 30 días, los escenarios de un intento se ven así:
| Tasa de fallos pagados | Nuevas generaciones/día | Coste API 30 días |
|---|---|---|
| 100% | 3.000 | $450 |
| 40% | 1.200 | $180 |
| 20% | 600 | $90 |
| 10% | 300 | $45 |
Estos son escenarios, no tasas de acierto predichas, y cada fila supone un intento completado por activo aceptado. Con 1.5 intentos, por ejemplo, la tasa de fallos debe ser como máximo 26.7 % para mantener el mismo límite de $0.002. Si los 3.000 eventos permanecen únicos después de deduplicación semántica, se aplica la primera fila.
Las claves de receta y las claves de activo semántico resuelven problemas diferentes
Una clave de receta representa la entrada del jugador. Si el orden no importa en tus reglas, ordena los IDs de objeto canónicos exactos antes de hacerles hash. La identidad sin hash es fácil de inspeccionar:
identidad de receta: v3 | fuego | hierroEso hace que hierro + fuego y fuego + hierro se resuelvan a través de la misma
fila de receta propiedad del servidor. Incluye una versión de reglas porque un
parche de balance posterior puede cambiar el resultado. No slugifiques IDs para la clave:
la normalización con pérdida puede fusionar dos objetos diferentes.
Una clave de activo semántico representa lo que el juego finalmente muestra. Su identidad sin hash puede seguir siendo igualmente clara:
identidad de activo: ember-shield | inventory-icon-v1 | z-image | 1:1 | filtradoVarias recetas pueden resolverse a ember-shield; las traducciones también pueden
nombrarlo de manera diferente. Esas rutas aún pueden reutilizar un visual aprobado.
Los prompts crudos hacen claves pobres: los cambios de redacción fragmentan el caché, e idénticos prompts se vuelven anticuados después de un cambio de dirección artística. Mapea cada receta a un ID de resultado estable, luego cachea el arte por ID de resultado, modelo, relación de aspecto, modo de seguridad y versión de estilo. Aumenta la versión cuando quieras arte nuevo.
Un flujo de solicitud cache-first
Trata la generación como producción de activo en segundo plano, incluso cuando el jugador la inicia. Un flujo práctico es:
- Resuelve la receta entrante a un ID de resultado semántico.
- Busca la clave de activo semántico en almacenamiento duradero.
- En un acierto, devuelve la URL almacenada inmediatamente.
- En un fallo, inserta atómicamente una fila de trabajo compartido clave por el activo semántico.
- En la misma transacción de base de datos, reserva un slot en un presupuesto compartido de envío diario. Otros workers devuelven el trabajo existente.
- Compromete la fila como
enviandoantes de hacer un POST, luego persiste el ID de tarea inmediatamente después de la respuesta. - Si el envío es ambiguo, marca
envío_inciertoy detente. Sondea un ID de tarea conocido con GET hasta que se vuelvacompletadoofallido. - Registra el estado terminal y los créditos liquidados. Copia una imagen completada a tu
propio almacenamiento y muévela a
pendiente_revisión. - Solo una verificación de aceptación puede mover
pendiente_revisiónalisto.
Después del paso seis, devuelve un placeholder y estado pendiente. Deja que un worker sondee
mientras el cliente verifica tu endpoint de juego. Nunca expongas la clave de reAPI
al cliente.
El sondeo es gratuito, y el endpoint de tarea en vuelo se cachea durante cinco segundos, así que
sondea aproximadamente cada cinco segundos.[3] Respeta
Retry-After en un 429. No reintentas ciegamente el POST: reAPI no desduplica
solicitudes de generación por Idempotency-Key, así que una respuesta perdida puede
ocultar una tarea aceptada y un cargo doble.[5]
Un módulo Node 20 ejecutable con un bloqueo compartido
El ejemplo más pequeño que protege múltiples workers necesita estado compartido. El
módulo debajo es un .mjs plano, usa PostgreSQL a través del paquete postgres,
y devuelve tras el envío en lugar de mantener la solicitud del jugador abierta.
npm install postgresCrea las tablas una vez. Rellena game_recipes desde tus propios datos de regla de juego;
el navegador envía dos IDs de objeto, nunca un ID de resultado o nombre de visualización.
CREATE TABLE game_recipes (
recipe_key text PRIMARY KEY,
result_item_id text NOT NULL,
result_display_name text NOT NULL
);
CREATE TABLE game_asset_jobs (
asset_key text PRIMARY KEY,
origin_recipe_key text NOT NULL REFERENCES game_recipes(recipe_key),
result_item_id text NOT NULL,
state text NOT NULL CHECK (state IN (
'submitting', 'submit_uncertain', 'processing', 'completed',
'pending_review', 'ready', 'failed', 'rejected'
)),
submit_owner uuid NOT NULL,
prompt text NOT NULL,
task_id text UNIQUE,
source_url text,
durable_url text,
usage_credits integer,
error_message text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE game_generation_budgets (
budget_day date PRIMARY KEY,
submissions integer NOT NULL CHECK (submissions >= 0),
submission_limit integer NOT NULL CHECK (submission_limit > 0)
);Guarda esto como cache-first.mjs. Las claves SHA-256 completas hacen hash de IDs canónicos exactos;
no hay paso de slugificación que pueda colapsar dos IDs diferentes.
import { createHash, randomUUID } from 'node:crypto';
import postgres from 'postgres';
const API_BASE = 'https://reapi.ai/api/v1';
const apiKey = process.env.REAPI_API_KEY;
const databaseUrl = process.env.DATABASE_URL;
const dailyLimit = Number(process.env.DAILY_NEW_ASSET_LIMIT ?? '100');
if (!apiKey) throw new Error('Set REAPI_API_KEY on the server');
if (!databaseUrl) throw new Error('Set DATABASE_URL on the server');
if (!Number.isInteger(dailyLimit) || dailyLimit < 1) {
throw new Error('DAILY_NEW_ASSET_LIMIT must be a positive integer');
}
const sql = postgres(databaseUrl);
const localFlights = new Map(); // L1 only; PostgreSQL owns correctness.
function hashIdentity(value) {
return createHash('sha256')
.update(JSON.stringify(value), 'utf8')
.digest('hex');
}
function canonicalId(value) {
if (typeof value !== 'string' || value.length === 0 || value.length > 200) {
throw new Error('Item IDs must be non-empty canonical server IDs');
}
return value; // Preserve the exact string; do not trim, slugify, or case-fold.
}
export function recipeKey(leftItemId, rightItemId) {
const pair = [canonicalId(leftItemId), canonicalId(rightItemId)].sort(
(a, b) => (a < b ? -1 : a > b ? 1 : 0),
);
return `recipe:v3:${hashIdentity({ rulesVersion: 'v3', itemIds: pair })}`;
}
function assetKey(resultItemId) {
return `asset:${hashIdentity({
resultItemId,
styleVersion: 'inventory-icon-v1',
model: 'z-image',
aspectRatio: '1:1',
contentFilter: true,
})}`;
}
async function resolveRecipe(leftItemId, rightItemId) {
const key = recipeKey(leftItemId, rightItemId);
const [recipe] = await sql`
SELECT result_item_id, result_display_name
FROM game_recipes
WHERE recipe_key = ${key}
`;
if (!recipe) throw new Error('Unknown recipe');
if (recipe.result_display_name.length > 200) {
throw new Error('Canonical display name is too long');
}
return { ...recipe, recipeKey: key };
}
function publicJob(job) {
return {
assetKey: job.asset_key,
state: job.state,
taskId: job.task_id ?? null,
url: job.state === 'ready' ? job.durable_url : null,
usageCredits: job.usage_credits ?? null,
};
}
async function claimSharedJob(recipe, key, prompt) {
const owner = randomUUID();
return sql.begin(async (tx) => {
const created = await tx`
INSERT INTO game_asset_jobs (
asset_key, origin_recipe_key, result_item_id,
state, submit_owner, prompt
) VALUES (
${key}, ${recipe.recipeKey}, ${recipe.result_item_id},
'submitting', ${owner}, ${prompt}
)
ON CONFLICT (asset_key) DO NOTHING
RETURNING *
`;
if (created.length === 0) {
const [existing] = await tx`
SELECT * FROM game_asset_jobs WHERE asset_key = ${key}
`;
if (!existing) throw new Error('Concurrent job was not readable');
return { ownsSubmit: false, job: existing };
}
const budget = await tx`
INSERT INTO game_generation_budgets (
budget_day, submissions, submission_limit
) VALUES (
(now() AT TIME ZONE 'UTC')::date, 1, ${dailyLimit}
)
ON CONFLICT (budget_day) DO UPDATE SET
submissions = game_generation_budgets.submissions + 1,
submission_limit = EXCLUDED.submission_limit
WHERE game_generation_budgets.submissions < EXCLUDED.submission_limit
RETURNING submissions
`;
if (budget.length === 0) throw new Error('Shared daily budget reached');
return { ownsSubmit: true, owner, job: created[0] };
});
}
async function markSubmitUncertain(key, owner, message) {
const [job] = await sql`
UPDATE game_asset_jobs
SET state = 'submit_uncertain', error_message = ${message},
updated_at = now()
WHERE asset_key = ${key} AND submit_owner = ${owner}
AND state = 'submitting'
RETURNING *
`;
return job;
}
async function submitClaimedJob(claim, key, prompt) {
let response;
let body = '';
try {
response = await fetch(`${API_BASE}/images/generations`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'z-image',
prompt,
aspect_ratio: '1:1',
content_filter: true,
}),
});
body = await response.text();
} catch (error) {
const job = await markSubmitUncertain(
key,
claim.owner,
error instanceof Error ? error.message : String(error),
);
return publicJob(job ?? claim.job);
}
let task;
try {
task = JSON.parse(body);
} catch {
task = null;
}
if (!response.ok || !task?.id) {
const message = `Unconfirmed submit response: HTTP ${response.status}`;
const job = await markSubmitUncertain(key, claim.owner, message);
return publicJob(job ?? claim.job);
}
const [saved] = await sql`
UPDATE game_asset_jobs
SET state = 'processing', task_id = ${task.id}, updated_at = now()
WHERE asset_key = ${key} AND submit_owner = ${claim.owner}
AND state = 'submitting'
RETURNING *
`;
if (!saved) {
throw new Error(`Task ${task.id} was accepted but its ID was not persisted; do not resubmit`);
}
return publicJob(saved);
}
async function startOrRead(recipe, key) {
const prompt = [
`A clean 3D inventory icon of ${recipe.result_display_name}.`,
'One centered object, readable silhouette, plain warm background,',
'soft studio light, no text, no logo, no existing game character.',
].join(' ');
const claim = await claimSharedJob(recipe, key, prompt);
if (!claim.ownsSubmit) return publicJob(claim.job);
return submitClaimedJob(claim, key, prompt);
}
export async function getOrCreateCraftAsset({ leftItemId, rightItemId }) {
const recipe = await resolveRecipe(leftItemId, rightItemId);
const key = assetKey(recipe.result_item_id);
const existing = localFlights.get(key);
if (existing) return existing;
const work = startOrRead(recipe, key).finally(() => {
if (localFlights.get(key) === work) localFlights.delete(key);
});
localFlights.set(key, work);
return work;
}
export async function pollAsset(key) {
const [job] = await sql`
SELECT * FROM game_asset_jobs WHERE asset_key = ${key}
`;
if (!job) throw new Error('Unknown asset job');
if (job.state !== 'processing' || !job.task_id) return publicJob(job);
const response = await fetch(`${API_BASE}/tasks/${job.task_id}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429) {
const parsed = Number.parseInt(response.headers.get('retry-after') ?? '', 10);
return {
...publicJob(job),
retryAfterSeconds: Number.isFinite(parsed) ? Math.max(parsed, 1) : 5,
};
}
if (!response.ok) {
throw new Error(`GET task failed with HTTP ${response.status}; retry this GET, not the POST`);
}
const task = await response.json();
if (task.status === 'processing') return publicJob(job);
const credits = Number.isInteger(task.usage?.credits)
? task.usage.credits
: null;
if (task.status === 'failed') {
const [failed] = await sql`
UPDATE game_asset_jobs
SET state = 'failed', usage_credits = ${credits},
error_message = ${task.error?.message ?? 'Generation failed'},
updated_at = now()
WHERE asset_key = ${key} AND task_id = ${job.task_id}
RETURNING *
`;
return publicJob(failed);
}
if (task.status !== 'completed') {
throw new Error(`Unexpected task status: ${task.status}`);
}
const sourceUrl = task.output?.image_urls?.[0] ?? null;
const [completed] = await sql`
UPDATE game_asset_jobs
SET state = 'completed', source_url = ${sourceUrl},
usage_credits = ${credits},
error_message = ${sourceUrl ? null : 'Completed task returned no image URL'},
updated_at = now()
WHERE asset_key = ${key} AND task_id = ${job.task_id}
RETURNING *
`;
return publicJob(completed);
}
export async function stageForReview(key, durableUrl) {
const [job] = await sql`
UPDATE game_asset_jobs
SET state = 'pending_review', durable_url = ${durableUrl},
updated_at = now()
WHERE asset_key = ${key} AND state = 'completed'
AND source_url IS NOT NULL
RETURNING *
`;
if (!job) throw new Error('Only an archived completed asset can enter review');
return publicJob(job);
}
export async function reviewAsset(key, accepted, reason = null) {
const nextState = accepted ? 'ready' : 'rejected';
const [job] = await sql`
UPDATE game_asset_jobs
SET state = ${nextState}, error_message = ${reason}, updated_at = now()
WHERE asset_key = ${key} AND state = 'pending_review'
RETURNING *
`;
if (!job) throw new Error('Asset is not pending review');
return publicJob(job);
}La fila de base de datos, no localFlights, evita que un segundo worker envíe
el mismo activo. Un proceso que muere durante POST deja enviando; trata una fila
anticuada como envío_incierto e investígala en lugar de reciclarla.
Después de que existe un ID de tarea conocido, los fallos de sondeo transitorio reintentarán solo GET.
El contador compartido es un límite de envío conservador. Un POST no confirmado o rechazado
aún consume su slot, lo que es más seguro que exceder silenciosamente el límite.
Si una tarea terminal falla, su estado y usage.credits permanecen en la fila. Este
ejemplo no reintenta automáticamente activos fallidos o rechazados; agrega un ledger de intento
separado y una acción de operador explícita y presupuestada antes de aceptar reintentos.
Tu worker de almacenamiento debe descargar source_url, subirlo a tu object
store, y pasar esa URL duradera a stageForReview. Solo reviewAsset(..., true) crea un activo cacheable listo.
Lo que la prueba de humo de tres tareas demuestra
El 3 de septiembre de 2026, enviamos tres prompts originales 1:1 de objeto de inventario
con content_filter: true: una linterna de musgo, una brújula de cristal y un escudo de ascuas.
Cada tarea se completó en su primer envío e informó 5 créditos.
[4]
| Candidato | ID de tarea | Tiempo de completación observado | Créditos |
|---|---|---|---|
| Linterna de musgo | task_01a065490250739d893a1efcd83f4d1d | 25.1 s | 5 |
| Brújula de cristal | task_01a0654995df7265b44b132dce34a914 | 25.0 s | 5 |
| Escudo de ascuas | task_01a0654995fd73e28e7c682170664070 | 18.2 s | 5 |
| Total | 3 tareas | — | 15 / $0.015 |
El registro público de prueba de humo contiene la configuración de solicitud, IDs de tarea, tiempos transcurridos y créditos liquidados. Deliberadamente omite las URLs de salida temporales.[4]
Esta es una prueba de humo, no un benchmark de latencia; no dice nada sobre p95,
ráfagas o confiabilidad a largo plazo. El protocolo registró resultados de tareas y
liquidación pero no incluyó una revisión de arte. completado significa que tres candidatos
llegaron, no que un revisor de arte los aceptó.
Por eso la métrica creativa útil es:
coste por activo aceptado =
total de créditos de generación liquidados x $0.001 / activos aceptadosSi tres intentos producen un icono usable, su coste efectivo es $0.015. Registra el estado técnico y la aceptación creativa por separado.
Pregeneración para latencia, no ahorros imaginarios
Pregeneración solo para solicitudes predecibles, como los 20 primeros receptos alcanzables desde el inventario de un jugador. Usa el mismo caché semántico y ruta single-flight, con un límite diario separado.
No generes el espacio de combinaciones teórico. Los activos no utilizados cuestan dinero, y la combinación abierta crece más allá de una cola de fuerza bruta. Clasifica por demanda observada y detente en el límite.
El híbrido usualmente se siente mejor:
- los activos semánticos aprobados devuelven inmediatamente;
- los siguientes elementos de alta probabilidad se pregeneren;
- los fallos de cola larga muestran un placeholder mientras se ejecuta una tarea en segundo plano;
- las solicitudes impopulares o abusivas alcanzan una cuota en lugar de un bucle de reintento ilimitado.
Seguridad, almacenamiento y límites operacionales
Mantén content_filter: true para generación cara al jugador. Z-Image deja esa
configuración apagada por defecto, así que confiar en el defecto no es suficiente.
[2] Modera o restringe el texto del jugador antes de que
se convierta en un prompt, bloquea intentos de imitar personajes protegidos o personas reales,
e implementa rate-limiting por cuenta y dispositivo. Un filtro ascendente es una capa,
no tu política de juego completa.
Mantén la clave del lado del servidor. Registra claves de receta y activo, ID de tarea, estado, créditos y aceptación sin retener datos innecesarios del jugador.
Finalmente, copia archivos completados a almacenamiento duradero antes de publicar sus URLs a clientes de juego. Las URLs de salida de tarea no son un contrato de activo permanente. [3] Almacena una suma de verificación y tipo de contenido junto al registro de activo para que un reintento no pueda reemplazar silenciosamente arte aprobado.
Conclusión: cuando el caché no es suficiente
El caché solo funciona cuando el juego permite reutilización. Si tasa de fallos pagados × intentos completados por activo aceptado excede 0.4, el límite de $0.002 no puede mantenerse a un
precio unitario de $0.005. Si las 3.000 solicitudes diarias en el ejemplo original ya
son activos semánticos únicos después del caché, cambiar el software de caché no resolverá
el problema.
Entonces cambia una restricción de producto: comparte arte entre objetos similares, usa recoloración procedural u overlays, genera solo artículos populares, limita descubrimientos, cobra por creación de cola larga, o acepta un presupuesto de medios más grande.
Verifica la página del modelo Z-Image en vivo antes de establecer un presupuesto, luego usa la guía de API de Z-Image y la referencia de API de tareas para implementar envío y sondeo. Los precios y el comportamiento del modelo pueden cambiar; tus propios créditos liquidados siguen siendo la fuente de verdad para una factura de producción.
FAQ
¿El caché reduce el precio de una nueva imagen de IA?
No. Reduce la frecuencia con la que la compras. El cargo actual de Z-Image permanece en cinco créditos por generación completada; un acierto de caché no cuesta nuevos créditos de generación.
¿Debería el prompt mismo ser la clave del caché?
Generalmente no. Resuelve recetas a un ID de resultado estable, luego versiona su arte por separado.
¿Y si el orden de los objetos importa?
No ordenes las entradas de la receta. agua + fuego y fuego + agua deberían tener
claves de receta diferentes siempre que tus reglas les den resultados diferentes.
¿Debería un juego pregeneración activos o generarlos en tiempo real?
Usa ambos: pregeneración un conjunto probable presupuestado, sirve aciertos inmediatamente, y genera objetos impredecibles de cola larga en el segundo plano.
¿Con qué frecuencia debo sondear una tarea de imagen de reAPI?
Aproximadamente cada cinco segundos es un punto de partida práctico. El sondeo más rápido no hace que la generación termine antes y puede desperdiciar presupuesto de límite de velocidad.
¿Se cobran las generaciones fallidas?
La documentación de Z-Image dice que las solicitudes fallidas y rechazadas no se cobran.
Aún así inspecciona status junto con usage.credits: un resultado completado pero visualmente
inutilizable es diferente de una tarea fallida y cuenta hacia coste creativo.[2][3]
¿Puedo prometer un promedio por debajo de $0.002 por creación?
Solo después de medir tu tasa de fallos pagados e intentos completados por activo aceptado. Con $0.005 por imagen, mantenerse estrictamente por debajo de $0.002 requiere que su producto permanezca por debajo de 0.4. Una tasa de fallos del 40 % apenas alcanza el límite cuando cada activo pasa en su primer intento completado.
¿Puede el cliente de juego llamar directamente al endpoint de imagen?
No debería. Una credencial del lado del cliente puede ser extraída y abusada. Enruta la generación a través de tu backend, aplica cuotas allí, y deja que los clientes consulten una tarea de trabajo propiedad del juego.
Referencias
- reAPI. Z-Image model page and live pricing. Retrieved September 3, 2026 from reapi.ai/models/z-image
- reAPI. Z-Image API documentation: inputs, asynchronous response, pricing, filters, and output retention. Retrieved September 3, 2026 from reapi.ai/docs/z-image
- reAPI. Tasks API: status, settled usage, polling, rate-limit behavior, and output storage. Retrieved September 3, 2026 from reapi.ai/docs/api/tasks
- reAPI. Three-task Z-Image smoke-test record. Run September 3, 2026. Temporary output URLs are omitted; no visual acceptance review was performed.
- reAPI. API overview: idempotency behavior. Retrieved September 3, 2026.
Autor

Categorías
Más publicaciones

Estado de Seedance 2.5: acceso API, precios y límites
Seedance 2.5 ya está disponible: ID del modelo, precios a 480p y 720p, vídeos de 30 segundos, más referencias y sin ruta 4K.


¿Usó alguien ChatGPT? Señales, no pruebas forenses
Detecta si alguien utilizó ChatGPT. Aprende las señales recurrentes, por qué ninguna frase lo prueba y cómo revisar textos sospechosos con criterio.


Hailuo AI vs Seedance, Kling, Veo: comparación de precios
¿Cómo compara Hailuo AI con Seedance 2.5, Kling 3.0 y Veo 3.1? Precio por segundo, duración, audio, referencias y capacidades del MiniMax H3 frente a otras IA.
