- PHP 66.9%
- JavaScript 24.2%
- Twig 7%
- CSS 1.7%
- Dockerfile 0.1%
|
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
Reviewed-on: #132 |
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| .idea | ||
| app | ||
| docker | ||
| docs | ||
| renderer | ||
| story-telling | ||
| .dockerignore | ||
| .gitignore | ||
| CLAUDE.md | ||
| compose.override.yaml | ||
| compose.prod.yaml | ||
| compose.yaml | ||
| README.md | ||
| release.sh | ||
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 composedepuis la racine du dépôt (où vitcompose.yaml), jamais depuisapp/(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 : FeedBufferMessage → GenerateArtworkMessage (async) →
RenderArtwork → auto-validated ; en parallèle PublishNextArtworkMessage
pioche la plus ancienne validated → published. 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.localpour un flux plus serré.
Choix de l'algorithme : le feeder tire un algorithme au hasard parmi tous ceux marqués
activeen base (tirage uniforme à chaque génération). Le buffer est donc un mélange des algos actifs. Pour piloter le mix, basculer la colonneactive(UPDATE algorithm SET active = ...) ou ajusterACTIVE_ALGORITHMSdansAlgorithmFixtures. Les fixtures activent tous les algos marquésactive(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 RedisRelaymanque 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
/museeest vide alors que l'accueil affiche une œuvre (typiquement après undoctrine:fixtures:load) → la projection publique est mise en cache (poolcache.gallery_catalog, décorateurCachedPublishedArtworkCatalog). Elle n'est purgée que sur l'événementArtworkPublished; or les fixtures appellentArtwork::publish()directement (sans passer parPublishNextArtwork), donc le listenerInvalidateGalleryCacheOnPublicationne 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(oucache:clear). À refaire à chaque rechargement de fixtures. Could not resolve host: renderer(ex. audoctrine:fixtures:load) → la commande a été lancée depuis l'hôte. Le hostnamerenderern'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
- 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). - 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-slide→sharprasterise en PNG 1080×1350 (4:5) → dépôt dans le bucket S3 public. Le fondartworkréutilise l'asset d'une œuvre déjà rendue (cover-crop), jamais un re-rendu. - 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. - Dédup : la table Postgres
editorial_carousel_post_log(clé =slug) empêche toute republication. Rien de tout cela n'entre dans lecanonical_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éfaut0 12 * * 3= mercredi 12h, ancré Europe/Paris ; transportasync, jetable). Vide ⇒ pas de cron du tout. - À chaque tick, la file pioche le carrousel de plus petit
orderqui n'a pas encore été publié (c.-à-d. dont leslugest absent de la table de dédupeditorial_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 (warningsi la file est basse,errorsi 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 danseditorial_carousel_post_log— ne jamais renommer unslugpublié (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'enumSlideLayout(Open/Closed, aucunswitchà toucher).background.kind∈{brand, gradient, artwork}.artworkréférence une œuvre publiée par songeneration_number(asset introuvable → échec loud au rendu).- Le
bodyest 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.