Créer un site de génération d’images par IA : modèle et projet GitHub

Un modèle fonctionnel de site de génération d’images par IA : la stack Next.js, l’organisation du projet GitHub, la route serveur qui appelle l’API d’un modèle d’image, la page de prompt, les contrôles de sécurité, les limites de requêtes et le déploiement. Chaque fichier est présenté, pour que vous puissiez le copier et le mettre en ligne.

Créer un site de génération d’images par IA : modèle et projet GitHub
Cristian Da Conceicao
Fondateur de Picasso IA

La plupart des tutoriels sur la manière de créer un site de génération d’images par IA se terminent par une capture d’écran et une promesse vague. Celui-ci se termine par un projet fonctionnel : une zone de saisie de prompt, une route serveur qui appelle un modèle d’image, une boucle d’interrogation, une vue des résultats et une checklist de déploiement. Chaque fichier tient sur cette page, vous pouvez donc le coller dans un dépôt vierge, le pousser sur GitHub et disposer d’un générateur en ligne le même après-midi.

Le modèle est volontairement petit. Il comporte quatre fichiers de logique réelle, pas de base de données, pas de fournisseur de connexion et pas de bibliothèque de gestion d’état. Il se lit d’une traite et s’étend facilement plus tard avec des comptes, un historique ou de la vidéo. Si vous savez exécuter npm install, vous savez le mettre en ligne.

💡 Réponse rapide : un site de génération d’images est un formulaire, une route serveur qui masque votre token d’API, une boucle d’interrogation et une balise <img>. Tout le reste relève de la finition.

Ce que fait le modèle

Le site final reçoit un prompt textuel, l’envoie à un modèle d’image, attend le résultat et affiche l’image avec un lien de téléchargement. Le navigateur ne voit jamais votre token d’API, car chaque appel passe par votre propre route serveur.

Voici le déroulement complet d’une requête :

  1. Le visiteur saisit un prompt et appuie sur le bouton.
  2. La page envoie le prompt à /api/generate sur votre serveur.
  3. Votre serveur valide le prompt et crée une tâche sur l’API d’images.
  4. La page demande à /api/generate/{id} le statut toutes les deux secondes.
  5. Lorsque le statut passe à succeeded, la page affiche l’URL de l’image.

Croquis sur tableau blanc de l’architecture navigateur, serveur et modèle d’image

Fonctionnalités en un coup d’œil

FonctionnalitéIncluseOù elle se trouve
Formulaire de prompt avec limite de caractèresOuicomponents/Generator.tsx
Route serveur qui crée la tâcheOuiapp/api/generate/route.ts
Route de statut pour l’interrogationOuiapp/api/generate/[id]/route.ts
États de chargement, d’erreur et de délai dépasséOuicomponents/Generator.tsx
Lien de téléchargement et historique localOuiAjouté à l’étape de la galerie
Comptes, facturation, galeries utilisateurNonÀ ajouter après le premier lancement

À qui il convient : les développeurs indépendants obtiennent une pièce de portfolio qui génère réellement des images. Les agences disposent d’une base qu’elles peuvent personnaliser aux couleurs d’un client en une journée. Les équipes produit obtiennent un prototype pour tester la demande avant d’investir dans une plateforme complète.

Stack et organisation du projet

Ordinateur portable dans un café affichant une page simple de générateur d’images avec un champ de prompt

Choisir la stack

Le modèle utilise Next.js avec l’App Router, TypeScript et Tailwind CSS. Un seul framework fournit la page et les routes serveur dans le même dépôt, ce qui le rend adapté à un premier projet. Toute stack disposant d’un serveur fonctionne de la même manière : React avec Express, SvelteKit, Nuxt ou Node pur.

Créez le projet et mettez-le sur GitHub en quatre commandes :

npx create-next-app@latest ai-image-generator --ts --app --tailwind --eslint
cd ai-image-generator
git init && git add . && git commit -m "Initial template"
gh repo create ai-image-generator --public --source=. --push

Le backend est une API de texte vers image. L’API PicassoIA suit la convention de Replicate : POST /v1/models/{owner}/{name}/predictions crée une tâche, et GET /v1/predictions/{id} renvoie son statut. Au moment de la rédaction, l’API propose quatre modèles : picassoia/picassoia-image, picassoia/picassoia-image-editor-pro, picassoia/picassoia-video et picassoia/seedance-2.5-lite. Créez votre token sur la page de l’API PicassoIA.

Arborescence et variables

ai-image-generator/
├── app/
│   ├── api/
│   │   └── generate/
│   │       ├── route.ts          # creates the prediction
│   │       └── [id]/route.ts     # returns status and output
│   ├── layout.tsx
│   └── page.tsx                  # renders <Generator />
├── components/
│   └── Generator.tsx             # form, polling, result
├── .env.example
├── .gitignore
└── README.md

Vue de dessus d’un bureau avec un wireframe, un ordinateur portable et un téléphone affichant une galerie de photos

Deux variables d’environnement pilotent l’ensemble :

# .env.example
PICASSOIA_API_TOKEN=pia_sk_replace_me
PICASSOIA_MODEL=picassoia/picassoia-image

Copiez le fichier vers .env.local et collez-y votre vrai token. create-next-app ignore tous les fichiers .env*, donc ajoutez la ligne !.env.example dans .gitignore si vous voulez conserver le fichier d’exemple dans le dépôt. Ne mettez jamais le token dans une variable qui commence par NEXT_PUBLIC_, car Next.js envoie celles-ci au navigateur.

Écrire la route serveur

Mains d’un développeur tapant du code sur un ordinateur portable devant un écran

Créer la prédiction

// app/api/generate/route.ts
import { NextResponse } from "next/server";

const API = "https://api.picassoia.com/v1";
const MODEL = process.env.PICASSOIA_MODEL ?? "picassoia/picassoia-image";

export async function POST(req: Request) {
  const { prompt } = await req.json();
  if (typeof prompt !== "string" || prompt.length < 3 || prompt.length > 4000) {
    return NextResponse.json({ error: "Invalid prompt" }, { status: 400 });
  }

  const res = await fetch(`${API}/models/${MODEL}/predictions`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ input: { prompt } }),
  });

  if (!res.ok) {
    return NextResponse.json({ error: "Upstream error" }, { status: 502 });
  }
  const prediction = await res.json();
  return NextResponse.json({ id: prediction.id });
}

Le corps suit la convention de Replicate, un objet input qui contient le prompt. Chaque page de modèle liste des champs supplémentaires, comme le format, que vous pouvez ajouter à côté de prompt. La vérification de 4 000 caractères correspond à la limite de prompt documentée, si bien que les utilisateurs reçoivent une erreur claire avant que la requête ne quitte votre serveur.

Interroger jusqu’à la fin du traitement

La génération est asynchrone. Le premier appel renvoie un identifiant, et vous demandez le statut jusqu’à ce qu’il indique succeeded ou failed.

// app/api/generate/[id]/route.ts
import { NextResponse } from "next/server";

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  if (!/^[A-Za-z0-9_-]+$/.test(id)) {
    return NextResponse.json({ error: "Bad id" }, { status: 400 });
  }

  const res = await fetch(`https://api.picassoia.com/v1/predictions/${id}`, {
    headers: { Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}` },
    cache: "no-store",
  });
  const p = await res.json();
  const output = Array.isArray(p.output) ? p.output[0] : p.output;
  return NextResponse.json({
    status: p.status,
    output: output ?? null,
    error: p.error ?? null,
  });
}

La vérification id est importante. Sans elle, un visiteur pourrait transmettre ../ segments et faire appeler par votre serveur un autre point de terminaison avec votre token attaché.

Gérer les erreurs et les limites

Trois types d’échec apparaissent dans le trafic réel :

  1. Entrée invalide : renvoyez une erreur 400 avant de consommer une requête.
  2. Erreurs amont ou files saturées : renvoyez 502 ou 429 et laissez la page afficher un message invitant à réessayer.
  3. Tâches lentes : arrêtez l’interrogation après environ deux minutes et prévenez l’utilisateur.

L’API autorise 5 prédictions simultanées par compte, partagées entre tous les tokens. Si votre site reçoit dix visiteurs en même temps, cinq attendent. Ajoutez une petite file d’attente ou désactivez le bouton pendant qu’une tâche tourne.

SymptômeCause probableCorrectif
401 venant de l’APIToken incorrect ou absentVérifiez .env.local et redémarrez npm run dev
429 ou attentes longuesLes 5 emplacements sont occupésMettez les requêtes en file ou affichez un message d’attente
Le statut ne change jamaisInterrogation du mauvais identifiantJournalisez l’identifiant renvoyé par le premier appel
Fonctionne en local, échoue en ligneVariables non définies sur l’hébergeurAjoutez les deux variables dans les paramètres de l’hébergeur

Construire la page de prompt

Designer esquissant la mise en page d’une carte pour une galerie d’images sur une tablette

Formulaire et état

// components/Generator.tsx
"use client";
import { useState } from "react";

export default function Generator() {
  const [prompt, setPrompt] = useState("");
  const [image, setImage] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function generate() {
    setBusy(true);
    setError(null);
    setImage(null);
    try {
      const start = await fetch("/api/generate", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt }),
      });
      if (!start.ok) throw new Error("Could not start the job");
      const { id } = await start.json();

      for (let i = 0; i < 60; i++) {
        await new Promise((r) => setTimeout(r, 2000));
        const poll = await (await fetch(`/api/generate/${id}`)).json();
        if (poll.status === "succeeded") return setImage(poll.output);
        if (poll.status === "failed" || poll.status === "canceled") {
          throw new Error(poll.error ?? "Generation failed");
        }
      }
      throw new Error("Timed out, try again");
    } catch (e) {
      setError((e as Error).message);
    } finally {
      setBusy(false);
    }
  }

  return (
    <main className="mx-auto max-w-3xl p-6">
      <textarea
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        maxLength={4000}
        placeholder="A lighthouse at dawn, 35mm film, soft fog"
        className="w-full rounded border p-3"
      />
      <button
        onClick={generate}
        disabled={busy || prompt.length < 3}
        className="mt-3 rounded bg-black px-4 py-2 text-white disabled:opacity-50"
      >
        {busy ? "Generating..." : "Generate"}
      </button>
      {error && <p className="mt-3 text-red-600">{error}</p>}
      {image && <img src={image} alt={prompt} className="mt-6 w-full rounded" />}
    </main>
  );
}

Importez-le dans app/page.tsx et affichez <Generator />. Lancez npm run dev, ouvrez localhost:3000, saisissez un prompt et vous disposez d’un générateur fonctionnel.

Galerie et téléchargements

Ajoutez deux touches une fois les bases en place. Enveloppez l’image dans un lien avec l’attribut download pour que les utilisateurs puissent enregistrer le fichier. Stockez ensuite chaque résultat dans localStorage, un tableau d’objets { prompt, url }, et affichez-le sous le formulaire sous forme de grille. Vous obtenez ainsi un historique sans base de données.

💡 Gardez le prompt comme texte alternatif. Il aide les lecteurs d’écran et donne à votre galerie un texte utile pour les moteurs de recherche.

Comment utiliser Imagen 4 sur PicassoIA

Avant de figer un style dans votre site, testez-le là où itérer coûte peu. Le site PicassoIA propose une aire de test pour chaque modèle, et les commandes suivent le même schéma : un champ de prompt, quelques réglages et un bouton de génération.

Photographies imprimées disposées en grille à côté d’un ordinateur portable affichant une page de galerie

Lancer un prompt de test

  1. Ouvrez la page Imagen 4.
  2. Collez un prompt structuré : sujet, décor, lumière, objectif et texture.
  3. Choisissez un format 16:9 si le modèle le propose.
  4. Appuyez sur générer et examinez le résultat en taille réelle.
  5. Modifiez un seul détail à la fois et générez de nouveau.

Un prompt qui donne de bons résultats photographiques :

Une tasse de café noir en céramique sur une table en chêne usé, lumière de fenêtre venant de la gauche, objectif 50 mm à f/2, faible profondeur de champ, grain du bois et vapeur visibles, couleurs de Kodak Portra 400.

Chaque partie de ce prompt a un rôle :

ÉlémentExemplePourquoi c’est utile
SujetUne tasse de café noir en céramiqueNomme la seule chose dont parle l’image
DécorUne table en chêne uséDonne à l’arrière-plan une matière et une ambiance
LumièreLumière de fenêtre venant de la gaucheDéfinit les ombres et la direction
Objectif50 mm à f/2Contrôle la profondeur de champ et la perspective
TextureGrain du bois, vapeurRapproche le résultat d’une vraie photographie

Comparer trois modèles

Faites passer le même prompt dans plusieurs modèles avant de décider de ce qui alimentera le site. L’API ne propose que les quatre modèles picassoia/* au moment de la rédaction ; les autres de ce tableau servent donc à choisir un style dans l’aire de test.

ModèlePoint fortPage
Imagen 4Détail photographique naturelOuvrir
FLUX 2 ProBon respect du promptOuvrir
Seedream 4.5Couleurs riches et portraitsOuvrir
GPT Image 2Texte à l’intérieur des imagesOuvrir
FLUX SchnellBrouillons rapidesOuvrir
PicassoIA ImageLe modèle par défaut du projetOuvrir

Conservez le prompt gagnant comme texte de départ dans l’attribut placeholder, et transformez vos trois meilleurs prompts en suggestions cliquables sous la zone de texte. Les visiteurs qui voient un bon exemple rédigent de meilleurs prompts, et de meilleurs prompts signifient moins de générations gâchées.

Ajouter plus tard les LLM et la vidéo

Rédiger le code avec un LLM

Vous pouvez construire tout le modèle ci-dessus avec un assistant de code. Claude Sonnet 5 et GPT 5.6 Sol sont tous deux recommandés pour les tâches de code, et Gemini 3.5 Flash convient aux retouches rapides. Collez l’arborescence de cet article et demandez un fichier à la fois, puis relisez chaque ligne avant de la valider.

Deux développeurs en binôme sur une application web avec une grille d’images générées

Réécrire les prompts courts

La plupart des visiteurs tapent cinq mots. Un modèle de langage peut les développer avant l’appel à l’image :

  1. Ajoutez une étape rewrite dans route.ts qui envoie le texte de l’utilisateur à un modèle comme Kimi K2.6.
  2. Demandez-lui de renvoyer un seul prompt avec sujet, lumière, objectif et texture.
  3. Envoyez ce prompt au modèle d’image et affichez les deux versions à l’utilisateur.

Animer les résultats

Les deux mêmes routes gèrent la vidéo. Remplacez PICASSOIA_MODEL par un modèle vidéo, comme picassoia/seedance-2.5-lite, et le résultat devient un lien MP4. Affichez-le avec une balise <video controls> et augmentez l’intervalle d’interrogation, car la vidéo prend plus de temps que l’image. Pour comparer d’abord les options, les pages Seedance 2.5 Lite et Wan 3 montrent ce que produit chaque modèle.

Mettre en ligne sans mauvaise surprise

Baies de serveurs dans une allée calme d’un centre de données

Modérer les prompts

Un générateur public est détourné en quelques jours. Faites passer chaque prompt par un modèle de modération avant de créer la prédiction. Llama Guard 4 12B est conçu pour cette tâche : il classe un prompt comme sûr ou dangereux, et vous bloquez la requête lorsqu’il le juge dangereux. Ajoutez aussi un avis visible sur les conditions d’utilisation sous le formulaire.

Limites de requêtes et files d’attente

RisqueCorrectif
Un visiteur inonde la routeLimitez les requêtes par IP, par exemple 10 par heure
Plus de visiteurs que d’emplacements simultanésDésactivez le bouton pendant qu’une tâche tourne et affichez un message d’attente
Token exposé dans le navigateurGardez les appels côté serveur et n’utilisez jamais NEXT_PUBLIC_ pour des secrets
Coût qui s’emballePlafonnez les générations quotidiennes et déclenchez une alerte à 80 %

Un compteur en mémoire fonctionne sur un seul serveur. Sur un hébergement serverless, chaque instance de fonction a sa propre mémoire ; utilisez donc un stockage partagé pour le limiteur.

Déployer et surveiller

Le déploiement se fait en cinq étapes :

  1. Poussez le dépôt sur GitHub.
  2. Importez-le dans la plateforme d’hébergement de votre choix.
  3. Ajoutez PICASSOIA_API_TOKEN et PICASSOIA_MODEL comme variables d’environnement.
  4. Définissez une durée maximale de fonction suffisamment longue pour votre route d’interrogation.
  5. Ouvrez l’URL en ligne et générez trois images de test.

Après la mise en ligne, journalisez le statut de chaque tâche (jamais le token) et vérifiez le taux d’échec chaque semaine. Une hausse soudaine signale en général un changement de modèle ou une limite atteinte. Rédigez aussi un court README.md, avec les deux variables, la commande de lancement et une capture d’écran, car le README est la première chose que voient les gens sur un projet GitHub.

Construisez le vôtre sur Picasso IA

Vous avez maintenant les éléments : une stack, une arborescence, deux routes serveur, une page de prompt et une checklist de sécurité. Le moyen le plus rapide de donner au site une allure finie est de choisir son style avant d’écrire davantage de code.

Petite équipe célébrant le lancement de son site de génération d’images

Ouvrez Picasso IA, lancez votre premier prompt avec Imagen 4 ou FLUX 2 Pro, et enregistrez les trois résultats que vous préférez. Ces prompts deviendront vos suggestions, votre page d’accueil et vos premiers cas de test. Créez ensuite votre token d’API sur la page de l’API PicassoIA, collez le code de cet article dans un nouveau dépôt et lancez la génération. La première image sur votre propre domaine marque le moment où le projet devient réel, alors choisissez un bon prompt pour cette occasion.

Partager cet article

Choisissez votre langue