
Images IA pour jeux : réduire les coûts par mise en cache
Pipeline cache-first pour images avec clés sémantiques, requêtes monoflight, polling asynchrone, plafonds budgétaires, stockage et mathématique.
La mise en cache peut ramener le coût moyen de l'API de génération d'images IA par action de joueur à 0,002 $, mais elle ne peut pas rendre une image véritablement nouvelle moins chère. Au taux reAPI actuel de Z-Image de 0,005 $ par image générée, la règle décisive est taux de défaut rémunéré × tentatives complétées par actif accepté ≤ 0,4. Un taux de défaut de 40 % atteint le plafond uniquement lorsque chaque nouvel actif réussit au premier essai complété ; rester en dessous de 0,002 $ nécessite un résultat inférieur à cette limite.[1]
Mettez en cache le sens d'un élément, pas seulement son prompt. Fusionnez les défauts concurrents, préégénérez seulement les éléments probables et mesurez le coût par actif de jeu accepté plutôt que par réponse d'API.
TL;DR
- Au 0,005 $ par génération Z-Image complétée, un jeu ciblant un coût
effectif de 0,002 $ ou moins par craft a besoin de
taux de défaut rémunéré × tentatives complétées par actif accepté ≤ 0,4. Avec exactement une tentative, 40 % atteint le plafond ; en deçà, cela signifie moins de 40 %.[1] - Conservez une clé recette pour ce que le joueur a combiné et une clé d'actif sémantique pour l'objet résultant et la version artistique.
- Appliquez un job d'actif actif unique avec une contrainte d'unicité de base de données partagée. Une map de Promise en mémoire peut fusionner les appelants du même processus, mais ce n'est pas le verrou qui protège plusieurs workers.
- La génération d'images est asynchrone. Soumettez une fois, conservez l'ID de tâche, interrogez le point de terminaison de la tâche et copiez la sortie complétée vers le stockage que vous contrôlez. [2][3]
- Un test de fumée de trois tâches a complété trois candidats originaux à la première soumission au 15 crédits ou 0,015 $ au total. Ils n'ont pas été examinés visuellement, il s'agit donc de candidats, pas d'actifs acceptés.[4]
- Si 3 000 images quotidiennes sont déjà des actifs sémantiques véritablement nouveaux après mise en cache, un cache conventionnel ne sauvera pas l'économie. Le jeu doit réutiliser plus d'art, réduire la surface générative ou supporter un coût plus élevé.
Le vrai problème est le coût par craft, pas le coût par image
Imaginons un jeu de craft infini qui combine des objets comme le fer et le feu, puis génère à la fois un nouveau résultat et son image. Comment maintenir le coût média sous 0,002 $ par craft si les joueurs déclenchent 3 000 crafts par jour, dont beaucoup de combinaisons encore inédites ? Cette question met au jour la contrainte essentielle : le jeu peut produire de nouvelles demandes plus vite que ses revenus ne permettent de les financer.
Commencez par cette équation :
coût de génération quotidien =
événements de craft
× taux de défaut rémunéré
× tentatives complétées par actif accepté
× prix par génération complétéeÀ 0,005 $ par tâche Z-Image complétée, le plafond est :
0,005 $ × taux de défaut rémunéré × tentatives complétées par actif accepté <= 0,002 $
taux de défaut rémunéré × tentatives complétées par actif accepté <= 0,4Pour 3 000 événements de craft par jour sur 30 jours, les scénarios monoflight ressemblent à ceci :
| Taux de défaut rémunéré | Nouvelles générations/jour | Coût API 30 jours |
|---|---|---|
| 100% | 3 000 | 450 $ |
| 40% | 1 200 | 180 $ |
| 20% | 600 | 90 $ |
| 10% | 300 | 45 $ |
Il s'agit de scénarios, pas de taux de hit prédits, et chaque ligne suppose une tentative complétée par actif accepté. À 1,5 tentatives, par exemple, le taux de défaut ne doit pas dépasser 26,7 % pour maintenir le même plafond de 0,002 $. Si les 3 000 événements restent tous uniques après déduplica sémantique, la première ligne s'applique.
Les clés recette et les clés d'actif sémantique résolvent des problèmes
différents
Une clé recette représente l'entrée du joueur. Si l'ordre n'a pas d'importance dans vos règles, triez les ID d'éléments canoniques exacts avant de les hacher. L'identité non hachée est facile à inspecter :
identité recette : v3 | fire | ironCela fait en sorte que iron + fire et fire + iron se résolvent par la même
ligne de recette appartenant au serveur. Incluez une version de règles car un
patch d'équilibre ultérieur peut modifier le résultat. Ne slugifiez pas les ID
pour la clé : la normalisation avec perte peut fusionner deux éléments
différents.
Une clé d'actif sémantique représente ce que le jeu affiche finalement. Son identité non hachée peut rester tout aussi claire :
identité d'actif : ember-shield | inventory-icon-v1 | z-image | 1:1 | filteredPlusieurs recettes peuvent se résoudre en ember-shield ; les traductions peuvent
aussi la nommer différemment. Ces chemins peuvent toujours réutiliser un visuel
approuvé.
Les prompts bruts font de mauvaises clés : les changements de formulation fragmentent le cache, et les prompts identiques deviennent obsolètes après un changement de direction artistique. Mappez chaque recette à un ID de résultat stable, puis clé l'art par ID de résultat, modèle, rapport d'aspect, mode de sécurité et version de style. Augmentez la version lorsque vous voulez un nouvel art.
Un flux de requête orienté cache
Traitez la génération comme une production d'actif d'arrière-plan, même lorsque le joueur la commence. Un flux pratique est :
- Résolvez la recette entrante en un ID de résultat sémantique.
- Recherchez la clé d'actif sémantique dans le stockage durable.
- En cas de hit, renvoyez l'URL stockée immédiatement.
- En cas de défaut, insérez atomiquement une ligne de job partagée clée par l'actif sémantique.
- Dans la même transaction de base de données, réservez un slot dans un budget de soumission quotidien partagé. Les autres workers renvoient le job existant.
- Engagez la ligne comme
submittingavant de faire un POST, puis persistez l'ID de tâche immédiatement après la réponse. - Si la soumission est ambiguë, marquez
submit_uncertainet arrêtez. Interrogez un ID de tâche connu avec GET jusqu'à ce qu'il deviennecompletedoufailed. - Enregistrez l'état terminal et les crédits réglés. Copiez une image complétée
vers votre propre stockage et déplacez-la vers
pending_review. - Seul un contrôle d'acceptation peut déplacer
pending_reviewversready.
Après l'étape six, renvoyez un placeholder et l'état pending. Laissez un
worker interroger tandis que le client vérifie votre point de terminaison de jeu.
Ne jamais exposer la clé reAPI au client.
L'interrogation est gratuit, et le point de terminaison de tâche en vol est mis
en cache pendant cinq secondes, alors interrogez environ toutes les cinq
secondes.[3] Respectez Retry-After sur un
429. Ne réessayez pas aveuglément le POST : reAPI ne déduplique pas les
requêtes de génération par Idempotency-Key, donc une réponse perdue peut
masquer une tâche acceptée et une deuxième charge.[5]
Un module Node 20 exécutable avec un verrou partagé
Le plus petit exemple qui protège plusieurs workers a besoin d'état partagé. Le
module ci-dessous est un .mjs simple, utilise PostgreSQL via le package
postgres et retourne après soumission au lieu de tenir la requête du joueur
ouverte.
npm install postgresCréez les tables une fois. Peuplez game_recipes à partir des données de règles
du jeu lui-même ; le navigateur envoie deux ID d'élément, jamais un ID de
résultat ou un nom d'affichage.
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)
);Enregistrez ceci comme cache-first.mjs. Les clés complètes SHA-256 hachent des
ID canoniques exacts ; il n'existe pas d'étape de slugification qui puisse
fusionner deux ID différents.
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 ligne de base de données, pas localFlights, empêche un second worker de
soumettre le même actif. Un processus qui meurt pendant POST laisse submitting ;
traitez une ligne périmée comme submit_uncertain et enquêtez-y plutôt que de la
recycler. Après qu'un ID de tâche connu existe, les défaillances d'interrogation
transitoires ne réessayent que GET.
Le compteur partagé est un plafond de soumission conservateur. Un POST non
confirmé ou rejeté consomme toujours son slot, ce qui est plus sûr que de
dépasser silencieusement le plafond. Si une tâche terminale échoue, son statut et
usage.credits restent dans la ligne. Cet exemple ne réessaie pas
automatiquement les actifs échoués ou rejetés ; ajoutez un grand livre de
tentatives séparé et une action d'opérateur budgétée explicite avant de supporter
les tentatives.
Votre worker de stockage doit télécharger source_url, l'envoyer vers votre
magasin d'objets et passer cette URL durable à stageForReview. Seul
reviewAsset(..., true) crée un actif ready cacheable.
Ce que le test de fumée à trois tâches prouve
Le 3 septembre 2026, nous avons soumis trois prompts d'éléments d'inventaire
originaux 1:1 avec content_filter: true : une lanterne en mousse, une boussole
en verre et un bouclier incandescent. Chaque tâche s'est complétée à sa première
soumission et a signalé 5 crédits.
[4]
| Candidat | ID de tâche | Temps de fin observé | Crédits |
|---|---|---|---|
| Lanterne mousse | task_01a065490250739d893a1efcd83f4d1d | 25,1 s | 5 |
| Boussole verre | task_01a0654995df7265b44b132dce34a914 | 25,0 s | 5 |
| Bouclier incandescent | task_01a0654995fd73e28e7c682170664070 | 18,2 s | 5 |
| Total | 3 tâches | — | 15 / 0,015 $ |
L'enregistrement de test de fumée public contient les paramètres de requête, les ID de tâches, les temps écoulés et les crédits réglés. Il omet intentionnellement les URL de sortie temporaires.[4]
Il s'agit d'un test de fumée, pas d'un repère de latence ; il ne dit rien sur
p95, les rafales ou la fiabilité à long terme. Le protocole a enregistré les
résultats des tâches et le règlement mais n'a pas inclus un examen artistique.
completed signifie que trois candidats sont arrivés, pas qu'un réviseur
artistique les a acceptés.
C'est pourquoi la métrique créative utile est :
coût par actif accepté =
total des crédits de génération réglés × $0,001 / actifs acceptésSi trois tentatives rapportent un icône utilisable, son coût effectif est 0,015 $. Enregistrez l'état technique et l'acceptation créative séparément.
Préégénérer pour la latence, pas les économies imaginaires
Préégénérez uniquement les requêtes prévisibles, comme les 20 meilleures recettes accessibles depuis l'inventaire d'un joueur. Utilisez le même cache sémantique et le chemin monoflight, avec un plafond quotidien séparé.
Ne générez pas l'espace de combinaison théorique. Les actifs inutilisés coûtent de l'argent, et l'artisanat à ciel ouvert dépasse une file d'attente par force brute. Classez par demande observée et arrêtez au plafond.
L'hybride ressent généralement le meilleur :
- les actifs sémantiques approuvés reviennent immédiatement ;
- les éléments suivants à haute probabilité sont préégénérés ;
- les défauts de queue longue montrent un placeholder tandis qu'une tâche d'arrière-plan s'exécute ;
- les requêtes impopulaires ou abusives atteignent un quota au lieu d'une boucle de retry illimitée.
Sécurité, stockage et limites opérationnelles
Conservez content_filter: true pour la génération dirigée par les joueurs.
Z-Image laisse ce paramètre désactivé par défaut, donc ne pas s'appuyer sur le
défaut n'est pas suffisant.
[2] Modérez ou contraignez le texte du joueur
avant qu'il ne devienne un prompt, bloquez les tentatives d'imitation de
personnages protégés ou de personnes réelles, et limitez le taux par compte et
appareil. Un filtre en amont est une couche, pas votre politique de jeu complète.
Conservez la clé côté serveur. Enregistrez les clés recette et actif, l'ID de tâche, le statut, les crédits et l'acceptation sans conserver les données du joueur inutiles.
Enfin, copiez les fichiers complétés vers le stockage durable avant de publier leurs URL aux clients de jeu. Les URL de sortie de tâche ne sont pas un contrat d'actif permanent. [3] Stockez une somme de contrôle et un type de contenu à côté de l'enregistrement d'actif afin qu'une tentative ne puisse pas remplacer silencieusement l'art approuvé.
Conclusion : quand la mise en cache n'est pas suffisante
La mise en cache ne fonctionne que lorsque le jeu permet la réutilisation. Si
taux de défaut rémunéré × tentatives complétées par actif accepté dépasse 0,4,
le plafond de 0,002 $ ne peut pas tenir à un prix unitaire de 0,005 $. Si les
3 000 requêtes quotidiennes dans l'exemple original sont déjà des actifs
sémantiques uniques après mise en cache, changer le logiciel de cache ne
résoudra pas le problème.
Modifiez plutôt une contrainte de produit : partagez l'art entre des éléments similaires, utilisez la recoloration procédurale ou les superpositions, générez uniquement les éléments populaires, limitez les découvertes, facturez la création de queue longue ou acceptez un budget média plus grand.
Consultez la page de modèle Z-Image en direct avant de définir un budget, puis utilisez le guide API Z-Image et la référence API Tasks pour implémenter la soumission et l'interrogation. Les prix et le comportement du modèle peuvent changer ; vos propres crédits réglés restent la source de vérité pour une facture de production.
FAQ
La mise en cache réduit-elle le prix d'une nouvelle image IA ?
Non. Cela réduit la fréquence à laquelle vous en achetez une. La charge Z-Image actuelle reste à cinq crédits par génération complétée ; un hit de cache coûte zéro crédit de génération nouveau.
Le prompt lui-même doit-il être la clé de cache ?
Habituellement non. Résolvez les recettes en un ID de résultat stable, puis versionnez son art séparément.
Et si l'ordre des éléments importe ?
Ne triez pas les entrées de recette. water + fire et fire + water doivent
avoir des clés recette différentes chaque fois que vos règles leur donnent des
résultats différents.
Un jeu doit-il préégénérer les actifs ou les générer en temps réel ?
Utilisez les deux : préégénérez un ensemble probable budgété, servez les hits immédiatement et générez les éléments de queue longue imprévisibles en arrière-plan.
À quelle fréquence dois-je interroger une tâche d'image reAPI ?
Environ toutes les cinq secondes est un point de départ pratique. L'interrogation plus rapide ne fait pas terminer la génération plus tôt et peut gaspiller le budget de limite de taux.
Les générations échouées sont-elles facturées ?
La documentation Z-Image dit que les requêtes échouées et rejetées ne sont pas
facturées. Vérifiez toujours status avec usage.credits : un résultat
complété mais visuellement inutilisable est différent d'une tâche échouée et
compte dans le coût créatif.[2][3]
Puis-je promettre une moyenne en dessous de 0,002 $ par craft ?
Seulement après avoir mesuré votre taux de défaut rémunéré et les tentatives complétées par actif accepté. À 0,005 $ par image, rester strictement en dessous de 0,002 $ nécessite que leur produit reste en dessous de 0,4. Un taux de défaut de 40 % n'atteint le plafond que lorsque chaque actif réussit à sa première tentative complétée.
Le client du jeu peut-il appeler directement le point de terminaison d'image ?
Il ne devrait pas. Une information d'identification côté client peut être extraite et abusée. Acheminez la génération via votre backend, appliquez les quotas là, et laissez les clients interroger un job en attente détenu par le jeu.
Références
- 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.
Auteur

Catégories
Plus d'articles

Prompts Wan 3.0 pour vidéos de 30 secondes cohérentes
Structurez un prompt Wan 3.0 pour vidéo de 30 secondes : beats chronométrés, directions caméra, rôles de référence, audio, et comment corriger les problèmes.


MiniMax H3 vs Seedance 2.5 : quel modèle vidéo gagne ?
Comparez MiniMax H3 et Seedance 2.5 sur la durée, la sortie 2K, l'audio natif, les références, l'édition, le tarif, l'API et les meilleurs cas d'usage.


Seedance 2.0 ou Happyhorse 1.0 : quel modèle choisir en 2026 ?
Comparatif Seedance 2.0 et Happyhorse 1.0 en 2026 : multi-plans, montage, audio natif, prix et raisons du classement #1.
