No description
  • PHP 66.9%
  • JavaScript 24.2%
  • Twig 7%
  • CSS 1.7%
  • Dockerfile 0.1%
Find a file
Nora e2d53a0735
All checks were successful
CI / Renderer - tests + lint JS (push) Successful in 3m16s
Release / PHP - tests + static + lint (push) Successful in 5m41s
Release / Renderer - tests + lint JS (push) Successful in 3m7s
CI / PHP - tests + static + lint (push) Successful in 6m26s
Release / Merge develop → main (push) Successful in 13s
Merge pull request 'update manifesto' (#132) from fix/style-manifest into develop
Reviewed-on: #132
2026-08-03 13:40:50 +00:00
.claude init docs 2026-08-02 16:20:30 +02:00
.forgejo/workflows new node ci 2026-07-30 20:32:01 +02:00
.idea init docs 2026-08-02 16:20:30 +02:00
app update manifesto 2026-08-03 15:31:37 +02:00
docker T11: postgresql16-client dans l'image app (lockstep serveur PG 16, smoke Dockerfile) 2026-07-31 21:12:09 +02:00
docs T11: mark done - all 11 tasks of the cadrage/mouvement backlog complete 2026-08-02 17:43:28 +02:00
renderer remove 2026-08-03 12:09:29 +02:00
story-telling on goibng 2026-07-30 10:29:43 +02:00
.dockerignore remove - 2026-06-27 14:49:47 +02:00
.gitignore delete 2026-07-14 16:44:43 +02:00
CLAUDE.md cleaning the doc 2026-07-14 18:08:52 +02:00
compose.override.yaml done 2026-07-24 16:15:08 +02:00
compose.prod.yaml done 2026-07-24 16:15:08 +02:00
compose.yaml fix motion 2026-08-03 12:04:45 +02:00
README.md on going style 2026-07-30 16:10:30 +02:00
release.sh some fix on ci 2026-06-26 14:15:32 +02:00

ArtAutomatic

« L'Art du Jour » (FR) / « Daily Design Art » (EN, dailydesignart.com), studio Code & Canvas - une œuvre d'art algorithmique générée et publiée chaque jour, vendue en tirages limités numérotés. Voir CLAUDE.md et docs/ pour la spec et l'architecture. Déploiement & runbook prod : docs/06-deploiement-production.md.

Démarrer la stack

docker compose up          # app, postgres, redis, minio, renderer, mailpit
                           # app exposé sur http://localhost:8080

⚠️ Lancer docker compose depuis la racine du dépôt (où vit compose.yaml), jamais depuis app/ (cela démarre un Postgres en double).


Accélérer la boucle de génération en dev

En production, le pipeline tourne lentement : le buffer s'alimente en continu et une seule œuvre est publiée à minuit, après curation humaine. Pour vérifier le fonctionnement de bout en bout sans attendre, on peut faire battre les deux rouages à la minute et auto-valider les rendus.

1. Régler les cadences dans app/.env.local

.env.local n'est pas commité : ces réglages restent locaux, la prod garde ses défauts (app/.env : publication à minuit, alimentation toutes les 10 min, auto-validation désactivée).

# Boucle rapide : alimente ET publie chaque minute.
STUDIO_PUBLISH_CRON='* * * * *'
STUDIO_FEED_CRON='* * * * *'

# Auto-valide chaque œuvre rendue → buffer publiable sans clic back-office.
STUDIO_AUTO_VALIDATE=true

Vérifier que le conteneur voit bien ces valeurs :

docker compose exec app php bin/console debug:scheduler
# → les deux triggers à '* * * * *'
docker compose exec app php bin/console debug:dotenv STUDIO_AUTO_VALIDATE
# → Value: true (depuis .env.local)

2. Lancer la boucle autonome

Un seul worker consomme les deux transports (scheduler_default = les triggers cron, async = génération + rendu). Le laisser tourner :

docker compose exec app \
  php bin/console messenger:consume scheduler_default async -vv

Chaque minute : FeedBufferMessageGenerateArtworkMessage (async) → RenderArtworkauto-validated ; en parallèle PublishNextArtworkMessage pioche la plus ancienne validatedpublished. Aucun clic back-office requis.

Le buffer est plafonné par STUDIO_BUFFER_TARGET (défaut 7) : 1 œuvre publiée par minute ⇒ 1 régénérée par minute. Baisser ce plafond dans .env.local pour un flux plus serré.

Choix de l'algorithme : le feeder tire un algorithme au hasard parmi tous ceux marqués active en base (tirage uniforme à chaque génération). Le buffer est donc un mélange des algos actifs. Pour piloter le mix, basculer la colonne active (UPDATE algorithm SET active = ...) ou ajuster ACTIVE_ALGORITHMS dans AlgorithmFixtures. Les fixtures activent tous les algos marqués active (le catalogue en compte plusieurs dizaines : familles fractales, automates cellulaires, Voronoï, réaction-diffusion…).

3. Vérifier que ça marche

Observer la base bouger (autre terminal) - validated se vide d'une unité par minute, published grimpe :

watch -n5 "docker compose exec -T app php bin/console dbal:run-sql \
  \"SELECT status, count(*) FROM artwork GROUP BY status ORDER BY 1\""

Visuel : ouvrir http://localhost:8080/ et http://localhost:8080/musee - la dernière œuvre publiée doit apparaître (preuve que le raster et l'URL d'asset MinIO sont bons). Le generation_number s'incrémente sans trou.

Variante : test de fumée manuel (sans attendre les tops minute)

Pour dérouler la chaîne une fois à la main :

# 1. Alimenter le buffer (dispatche les générations manquantes)
docker compose exec app php bin/console studio:feed-buffer

# 2. Générer + rendre + auto-valider (génération dispatche un rendu : prévoir
#    ~2 messages par œuvre ; ajuster --limit)
docker compose exec app php bin/console messenger:consume async --limit=14 --time-limit=240 -v

# 3. Publier une œuvre (attend le prochain top minute, ≤ 60 s)
docker compose exec app php bin/console messenger:consume scheduler_default --limit=1 -v

# 4. Contrôler
docker compose exec -T app php bin/console dbal:run-sql \
  "SELECT status, count(*) FROM artwork GROUP BY status ORDER BY 1"

Dépannage

  • Une œuvre ne se rend pas → suspecter le renderer (image figée : un algo inconnu fait crasher le process). Logs : docker compose logs -f renderer.
  • Voir les messages en échec : docker compose exec app php bin/console messenger:failed:show.
  • messenger:stats / messenger:failed:* : toujours dans le conteneur (sur l'hôte, l'extension Redis Relay manque et la commande casse).
  • Aucune génération → vérifier qu'au moins un algorithme est actif : dbal:run-sql "SELECT code, active FROM algorithm".
  • Le /musee est vide alors que l'accueil affiche une œuvre (typiquement après un doctrine:fixtures:load) → la projection publique est mise en cache (pool cache.gallery_catalog, décorateur CachedPublishedArtworkCatalog). Elle n'est purgée que sur l'événement ArtworkPublished ; or les fixtures appellent Artwork::publish() directement (sans passer par PublishNextArtwork), donc le listener InvalidateGalleryCacheOnPublication ne se déclenche pas et le musée garde le résultat vide mémorisé avant le chargement. Vider le pool : docker compose exec app php bin/console cache:pool:clear cache.gallery_catalog (ou cache:clear). À refaire à chaque rechargement de fixtures.
  • Could not resolve host: renderer (ex. au doctrine:fixtures:load) → la commande a été lancée depuis l'hôte. Le hostname renderer n'existe que dans le réseau Docker : toute commande qui appelle renderer/minio/postgres se lance dans le conteneur (docker compose exec app php bin/console …).

Carrousels éditoriaux (Instagram)

Second registre de contenu, indépendant de l'œuvre du jour : des carrousels de storytelling qui racontent la marque (« Qui je suis », « Comment ça marche », le certificat, les coulisses…). Plan de référence : docs/plan-carrousels-editoriaux.md.

Comment ça marche

  1. Source de vérité : le fichier app/config/catalog/editorial-carousels.json (versionné). Il est lu directement au runtime — aucune synchro DB, aucune commande de sync. Un JSON malformé casse le build (un test CI charge le fichier réel à travers son Shape de validation).
  2. Rendu des slides : chaque slide est dessinée par un template SVG-Twig (app/templates/social/slides/{cover,text,artwork-quote}.svg.twig) → endpoint renderer /render-slidesharp rasterise en PNG 1080×1350 (4:5) → dépôt dans le bucket S3 public. Le fond artwork réutilise l'asset d'une œuvre déjà rendue (cover-crop), jamais un re-rendu.
  3. Publication : les slides rendues sont assemblées en carrousel Instagram via le mécanisme Graph existant (InstagramGraphPublisher::publishCarousel). Une caption FR (voix Wilfried) + 1-2 lignes EN + hashtags est ajoutée.
  4. Dédup : la table Postgres editorial_carousel_post_log (clé = slug) empêche toute republication. Rien de tout cela n'entre dans le canonical_hash/la signature d'œuvre : couche de présentation pure.

Quand sont-ils générés / publiés

  • Cron hebdomadaire SOCIAL_EDITORIAL_CAROUSEL_CRON (défaut 0 12 * * 3 = mercredi 12h, ancré Europe/Paris ; transport async, jetable). Vide ⇒ pas de cron du tout.
  • À chaque tick, la file pioche le carrousel de plus petit order qui n'a pas encore été publié (c.-à-d. dont le slug est absent de la table de dédup editorial_carousel_post_log), rend ses slides à la publication (pas d'avance), publie, puis journalise sa publication dans cette table. File vide → skip. Quand il reste ≤ 2 carrousels non publiés, un log d'alerte est émis (warning si la file est basse, error si elle est vide) — pas de mail : c'est une ligne Monolog à surveiller, le temps d'en rédiger d'autres. On ne boucle jamais, on ne republie jamais.
  • La banque compte aujourd'hui 14 carrousels rédigés (≈ 3-4 mois à 1/semaine).

Prévisualiser, publier à la demande, régénérer

Page back-office dédiée : /admin/carrousels-editoriaux (liste la file + l'état publié/non publié).

  • Prévisualiser : rend les slides d'un carrousel et affiche leurs URLs S3 publiques — sans publier (mêmes chemins que la publication).
  • Publier maintenant : bouton POST (CSRF) qui déclenche la publication immédiate.
  • Régénérer : les assets sont nommés social/editorial/<slug>/slide-<index>-<hash-de-contenu>.png (hash du SVG + fond). Modifier le texte JSON ou un template produit donc automatiquement un nouvel asset au prochain rendu (preview ou publication). Pour republier un carrousel déjà publié (bloqué par la dédup), supprimer sa ligne dans editorial_carousel_post_log — ne jamais renommer un slug publié (il est immuable, c'est la clé de dédup).

Ajouter un carrousel

Ajouter une entrée à app/config/catalog/editorial-carousels.json :

{
  "slug": "mon-carrousel",        // unique, [a-z0-9-]+, IMMUABLE une fois publié
  "order": 150,                    // ordre de passage (croissant)
  "caption": "…",                  // FR, voix Wilfried (via /ghost-writer)
  "caption_en": "…",               // 1-2 lignes EN ajoutées en fin de caption
  "hashtags": ["generativeart", "creativecoding", "fractalart"],  // 3-8
  "slides": [                      // 2 à 10 slides
    { "layout": "cover", "title": "…", "body": null,
      "background": { "kind": "brand" } },
    { "layout": "text", "title": "…",
      "body": ["Ligne 1", "Ligne 2"],   // liste de lignes EXPLICITES (pas de wrap auto)
      "background": { "kind": "gradient", "from": "#0b1020", "to": "#1a2a6c" } },
    { "layout": "artwork-quote", "title": null, "body": ["…"],
      "background": { "kind": "artwork", "generation_number": 42 } }
  ]
}
  • layout{cover, text, artwork-quote}. Un nouveau layout = 1 template SVG-Twig + 1 case d'enum SlideLayout (Open/Closed, aucun switch à toucher).
  • background.kind{brand, gradient, artwork}. artwork référence une œuvre publiée par son generation_number (asset introuvable → échec loud au rendu).
  • Le body est une liste de lignes : le découpage est éditorial, le SVG ne wrappe pas. Textes en FR. Rédaction via la compétence /ghost-writer.
  • Vérifier que le fichier passe toujours son Shape : vendor/bin/phpunit --filter Editorial.

Mise en production

Rebuild de l'image renderer (endpoint /render-slide) + positionner SOCIAL_EDITORIAL_CAROUSEL_CRON. La connexion INSTAGRAM_* existante est réutilisée.