Build · Django

Générer des images de try-on depuis un worker Django

En une phrase

Une intégration Django stocke une ligne par job, soumet depuis une tâche Celery, effectue un polling toutes les 3 secondes jusqu’à ce que le statut soit completed, puis télécharge le fichier dans votre propre stockage.

  • Crédits: 5 crédits en 2K · 6 crédits en 4K
  • Délai typique: 1.5–4 min
  • Traitement: asynchrone, polling

Chaque chiffre sur cette page provient de la documentation de l'API Photta · vérifié

Un modèle qui reproduit le job, une tâche Celery qui soumet et effectue le polling, et un stockage qui conserve le fichier une fois l’URL de livraison expirée.

RESPONSE=$(curl -s -X POST https://api.photta.app/api/v1/tryon/apparel \
  -H "Authorization: Bearer $PHOTTA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "product_type": "top",
  "product_images": [
    "https://cdn.yourstore.com/products/shirt-front.jpg"
  ],
  "product_back_image": "https://cdn.yourstore.com/products/shirt-back.jpg",
  "product_category": "t_shirt",
  "mannequin_id": "mnk_f3a9b1",
  "pose_id": "pose_c2d4e5",
  "background": "https://cdn.yourstore.com/backgrounds/studio-loft.jpg",
  "lock_background": true,
  "resolution": "2K",
  "aspect_ratio": "3:4",
  "body_size": "M",
  "expression": "neutral",
  "user_instruction": "Keep the collar pattern crisp and centered."
}')
ID=$(echo "$RESPONSE" | jq -r '.data.id')

Comment ça fonctionne

Une ligne, une tâche, un fichier

Modélisez le job, soumettez depuis une tâche, effectuez le polling avec backoff, persistez le fichier, exposez le statut.

  1. Étape 1

    Modéliser le job

    Une ligne par génération : le produit, l’id de génération, le statut et l’URL de sortie. Vos vues lisent la ligne ; le worker lit l’API.

  2. Étape 2

    Obtenir une clé d’API Photta

    Lisez-la depuis l’environnement dans settings, jamais depuis la base de données, et utilisez une clé distincte par environnement pour qu’une fuite en staging ne consomme pas vos credits de production.

  3. Étape 3

    Soumettre dans une tâche

    Les vues doivent répondre immédiatement. La tâche Celery envoie le job en POST et écrit l’id retourné sur la ligne.

  4. Étape 4

    Effectuer le polling avec backoff

    Effectuez un polling toutes les 3 secondes jusqu’à ce que le statut soit completed ou failed. En cas de 429, attendez les secondes indiquées par Retry-After au lieu de réessayer immédiatement.

  5. Étape 5

    Persister le fichier

    Téléchargez l’image finale dans votre propre stockage. L’URL fournit un job ; votre bucket assure la diffusion sur la page produit pendant des années.

  6. Étape 6

    Exposer le statut

    Votre front-end interroge votre ligne, pas l’API Photta. Cela permet de garder la clé sur le serveur et d’afficher la progression comme vous le souhaitez.

Modèle, tâche, erreurs, stockage

Le mécanisme de soumission-polling et la gestion des erreurs sont issus de la documentation de l’API.

# store/models.py
from django.db import models

class TryOn(models.Model):
    product = models.ForeignKey("Product", on_delete=models.CASCADE)
    generation_id = models.CharField(max_length=64, db_index=True)
    status = models.CharField(max_length=16, default="processing")
    output_url = models.URLField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
# store/tasks.py
from celery import shared_task

@shared_task(bind=True, max_retries=3)
def run_try_on(self, try_on_id):
    import os, requests
    
    res = requests.post(
        "https://api.photta.app/api/v1/tryon/apparel",
        headers={"Authorization": f"Bearer {os.environ['PHOTTA_API_KEY']}"},
        json={
          "product_type": "top",
          "product_images": [
            "https://cdn.yourstore.com/products/shirt-front.jpg"
          ],
          "product_back_image": "https://cdn.yourstore.com/products/shirt-back.jpg",
          "product_category": "t_shirt",
          "mannequin_id": "mnk_f3a9b1",
          "pose_id": "pose_c2d4e5",
          "background": "https://cdn.yourstore.com/backgrounds/studio-loft.jpg",
          "lock_background": True,
          "resolution": "2K",
          "aspect_ratio": "3:4",
          "body_size": "M",
          "expression": "neutral",
          "user_instruction": "Keep the collar pattern crisp and centered."
        },
    )
    data = res.json()["data"]  # data["id"], data["status"]
    
    import time
    
    def wait_for(job_id):
        for _ in range(120):
            res = requests.get(f"https://api.photta.app/api/v1/tryon/apparel/{job_id}", headers={"Authorization": f"Bearer {os.environ['PHOTTA_API_KEY']}"})
            data = res.json()["data"]
            if data["status"] == "completed":
                return data["output_url"]
            if data["status"] == "failed":
                raise RuntimeError(data.get("error_message", "generation failed"))
            time.sleep(3)
        raise TimeoutError("timed out after 6 minutes")
try:
    photta("POST", "/tryon/apparel", json={
      "product_type": "top",
      "product_images": [
        "https://cdn.yourstore.com/products/shirt-front.jpg"
      ],
      "product_back_image": "https://cdn.yourstore.com/products/shirt-back.jpg",
      "product_category": "t_shirt",
      "mannequin_id": "mnk_f3a9b1",
      "pose_id": "pose_c2d4e5",
      "background": "https://cdn.yourstore.com/backgrounds/studio-loft.jpg",
      "lock_background": True,
      "resolution": "2K",
      "aspect_ratio": "3:4",
      "body_size": "M",
      "expression": "neutral",
      "user_instruction": "Keep the collar pattern crisp and centered."
    })
except PhottaError as e:
    if e.status == 402:
        print("insufficient_credits_error: top up before retrying")
    elif e.status == 429:
        time.sleep(int(e.retry_after or 30))
    else:
        raise
# store/tasks.py — the output URL delivers one job; your storage keeps it
import requests
from django.core.files.base import ContentFile

def persist(try_on, output_url):
    body = requests.get(output_url, timeout=60).content
    try_on.product.gallery.save(f"{try_on.generation_id}.jpg", ContentFile(body))
    try_on.status, try_on.output_url = "completed", output_url
    try_on.save(update_fields=["status", "output_url"])

Questions fréquentes des développeurs

Questions fréquentes des développeurs Django

Pourquoi un worker plutôt qu’une vue ?

Une génération prend plus de temps qu’une requête ne le devrait. Un worker survit à la fermeture d’un onglet, à un déploiement et à un timeout ; une vue ne survit à aucun des trois.

Combien de temps la tâche doit-elle effectuer le polling ?

Suffisamment longtemps pour couvrir la latence documentée de l’endpoint, avec un plafond. Les exemples présentés ici s’arrêtent après un nombre fixe de tentatives plutôt que d’effectuer un polling indéfini.

Puis-je utiliser Django asynchrone à la place de Celery ?

Oui — les appels sont du simple HTTP. L’essentiel est que le traitement survive à la requête, quelle que soit la solution retenue.

Comment tester sans dépenser de credits ?

Enregistrez une réponse réelle et rejouez-la dans vos tests. Les endpoints de ressources et l’analyse détaillée des photos sont gratuits, les vérifications de clé et de connectivité ne coûtent donc rien.

Une même clé peut-elle servir pour tous les environnements ?

C’est possible, mais chaque clé consomme le même solde : une boucle en staging viderait donc les credits de production. Des clés séparées rendent l’incident visible et facile à stopper.

Un worker, une ligne et un fichier

Le mécanisme de soumission-polling présenté sur cette page est issu de la documentation de l’API.