Service ffmpeg-api réécrit (ffmpeg-service/app/) :
- Appels FFmpeg asynchrones : le service ne se fige plus pendant un rendu
- Vidéo récupérée par téléchargement (GET /files/…) au lieu de base64 en JSON
- Vraies erreurs HTTP ; échec explicite si la voix demandée est impossible
- 30 voix Gemini + ton de lecture (dynamique, chaleureux, promo, calme),
clé en en-tête, modèle configurable avec repli
- Edge TTS 7 (minutage des mots restauré), secours en voix françaises
- Voix traitée : filtre, compression, niveau constant ; musique bouclée et
baissée automatiquement sous la voix ; mix final à -14 LUFS
- Sous-titres calés mot à mot (Whisper pour Gemini et la voix d'origine,
à la place de ffsubsync), style Montserrat, placés hors des boutons Reels
- Vidéo : plus de retouche luminosité forcée, scaling lanczos, HDR iPhone
converti, BT.709, AAC 48 kHz 192k ; la vidéo s'allonge si la voix dépasse
- Grand logo de fin affiché après la voix ; FFmpeg 7.0.2 épinglé, polices et
modèle Whisper intégrés à l'image ; tests pytest et ruff
Application :
- Sélecteur de voix partagé (4 pages) : moteur, 30 voix, ton, écoute
- Reel images : minutage réel des mots, interrupteur voix respecté
- sync-info ne génère plus de voix à chaque frappe (estimation locale)
- Stabilisation désactivée par défaut, route /reels/preview inutilisée retirée
- Log « [ReelQueue] Worker démarré » pour vérifier la version déployée
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
L'ancienne file comptait les posts « processing » : un redémarrage pendant
un rendu la bloquait définitivement. Les rendus passent désormais par une
table reel_jobs, réservée avec FOR UPDATE SKIP LOCKED, avec heartbeat et
reprise des jobs interrompus au démarrage (2 tentatives maximum).
- Pipeline vidéo et pipeline images (Remotion) extraits des routes vers
server/services/reels/, les routes ne font que valider (zod) et mettre en file
- Rendus d'images suivis en base au lieu d'une Map en mémoire
- storeName conservé pour les jobs mis en attente
- Publication Facebook en binaire : l'URL relative /uploads/... transmise
auparavant n'était pas téléchargeable par Facebook
- Job en échec si toutes les pages échouent ou si la voix demandée manque
(le service Python renseigne enfin tts_error)
- La voix choisie sur la page images est réellement utilisée
- sync-info mesure la voix Gemini avec sa clé
- Timeouts sur les appels au service FFmpeg, vignettes via execFile,
/debug-ffmpeg protégé par la clé API, plus de voix anglaise en secours
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
Deux corrections demandées ensemble.
1. Vignettes vidéo
Le planificateur supprime la vidéo locale une fois publiée, alors que la
ligne `media` survit pour l'historique. L'interface tentait donc de lire
un fichier disparu pour en extraire une image : d'où les avertissements
« Video thumbnail generation failed » et les tuiles vides.
Une vignette JPEG est désormais extraite à la création du reel, dans les
deux flux de génération, et stockée à part : elle reste disponible quand
la vidéo ne l'est plus. La purge quotidienne la nettoie en même temps que
la ligne qu'elle illustre. L'extraction est tolérante à l'échec — une
vignette manquante dégrade l'affichage mais ne fait jamais échouer une
publication — et le composant retombe sur son icône de remplacement pour
les médias créés avant ce changement. La fonction d'extraction, jusque-là
privée à la route Remotion, devient un service partagé.
2. Erreurs de compilation préexistantes (27 → 0)
- remotion-video.tsx (bureau et mobile) : les composants Select étaient
utilisés sans être importés, ce qui faisait planter le sélecteur de voix
à l'exécution.
- external.ts : fileSize et aiGenerated étaient transmis dans un type que
le schéma ne déclare pas, et ne fonctionnaient que par conversion
implicite de PostgreSQL.
- reels.ts : l'interface MusicTrack, référencée mais jamais déclarée dans
ce module.
- remotion.ts : chromiumOptions.args retiré. Remotion ne lit jamais cette
clé, les drapeaux Chrome qu'elle contenait n'atteignaient donc pas le
navigateur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
Ajoute la gestion multi-comptes TikTok et permet d'envoyer la même vidéo
sur plusieurs pages Facebook ET plusieurs comptes TikTok en une seule
publication.
Un compte TikTok vit dans social_pages comme une page Facebook : il hérite
donc des permissions par utilisateur, de la planification et du calendrier.
Trois particularités de l'API TikTok structurent l'implémentation :
- pas de jeton saisi à la main : chaque compte passe par OAuth, et
l'access token (24h) est renouvelé via le refresh token (1 an) avant
chaque publication ;
- la publication est asynchrone : l'upload rend un publish_id, et un
poller récupère l'identifiant définitif du post ;
- creator_info doit être interrogé avant chaque envoi pour ne demander
qu'un niveau de confidentialité réellement autorisé sur le compte.
Serveur :
- service TikTok (OAuth, creator_info, upload FILE_UPLOAD par chunks,
suivi de statut) et routes de connexion/configuration
- schéma : platform 'tiktok', refresh token et scopes sur social_pages,
publish_id/publish_status sur scheduled_posts, table tiktok_config
- routage par plateforme dans les deux flux de reels et le planificateur
- cron de suivi des publications, token manager et analytics adaptés
Client :
- connexion et reconnexion des comptes TikTok depuis « Pages gérées »
- sélection combinée pages Facebook / comptes TikTok à la publication
- configuration de l'application TikTok dans les paramètres (admin)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
Remove Minimax Speech API and Freesound integrations entirely.
Add Piper TTS as the sole TTS provider via configurable HTTP URL.
- Add piper_config DB table (url field, per-user)
- Add GET/POST /api/piper/config routes
- Python: replace generate_tts_minimax with generate_tts_piper (GET ?text=, WAV→MP3)
- Simplify generate_tts_with_subs: piper_url param replaces tts_provider+minimax fields
- Drop ttsVoice/ttsProvider from all UI, API, and background job params
- Settings page: replace Minimax+Freesound cards with single Piper URL input
- Remove freeSoundService init from server startup and CSP headers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add slow zoom/pan (Ken Burns) effect to each image slide, cycling through
8 deterministic presets per image index for natural variety
- CapCut-style captions: semi-transparent pill background + yellow glow on
active word; background/text now only visible while voice is speaking
- Fix Facebook error 6000: pass Node.js Buffer directly instead of unsafe
ArrayBuffer pool-slice conversion which could corrupt the upload payload
- Fix TypeScript error: replace u-flag emoji regex with BMP surrogate pairs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remotion's headless Chromium cannot reliably fetch http://localhost
inside Docker due to network sandboxing. Instead of HTTP URLs, convert
all local /uploads/... files to data:image/...;base64,... before passing
to inputProps. This is reliable in all environments (Docker, dev, CI).
Audio files remain as HTTP URLs since they are loaded via Web Audio API
which has different network handling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logo stored as relative /uploads/logos/... was resolving to Remotion's
bundle server (port 3000) instead of the app (port 5555).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remotion's headless Chromium runs inside the same container as the app,
so it must use http://localhost:PORT to fetch local media files.
The Docker service hostname (socialflow-app) is only reachable from
other containers on the Docker network, not from within the container itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remotion's headless Chromium cannot load relative /uploads/... paths.
Library images were passed as-is, causing "Error loading image" at render time.
Now resolves them using INTERNAL_APP_URL (Docker) or the request host (dev).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Media files were stored with absolute http://localhost:5555/... URLs,
which got blocked by the Content Security Policy when the app runs at
https://socialflow.fnancy.fr. Now stores relative /uploads/... paths
and resolves to absolute URLs only when needed by external APIs
(Facebook, FFmpeg internal service).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store endpointUrl and publicUrl in cloudinary_config DB table so
MinIO connection can be fully configured from the app settings
without needing environment variables.
- DB migration: adds endpoint_url and public_url columns
- Schema: adds endpointUrl/publicUrl to cloudinaryConfig table
- MinIO service: reads endpoint from DB (falls back to MINIO_ENDPOINT env)
- buildMinioUrl: accepts optional publicUrl override from DB config
- All routes updated to pass config.publicUrl to buildMinioUrl
- Settings UI (desktop + mobile): adds Endpoint URL and Public URL fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create server/services/minio.ts: S3-compatible service (same interface as
cloudinaryService) using @aws-sdk/client-s3 and MINIO_ENDPOINT env var
- Replace all cloudinaryService imports with minioService across routes.ts,
routes/reels.ts, routes/remotion.ts, services/media-purge.ts
- Replace Cloudinary logo URL pattern (res.cloudinary.com/...) with
buildMinioUrl() helper throughout all server routes
- GET /api/cloudinary/config now returns a logoUrl field (full MinIO URL)
so clients never need to build the URL themselves
- Update settings pages (desktop + mobile): labels changed to MinIO
(Bucket Name, Access Key, Secret Key)
- Update image-editor.tsx: remove Cloudinary URL transformation, use logoUrl
- Update media-utils.ts: simplify getVideoThumbnailUrl (no URL transforms)
- Add MINIO_ENDPOINT and MINIO_PUBLIC_URL to docker-compose.yml + .env.example
- DB table reuse: cloudinary_config.cloud_name = bucket, api_key = access key,
api_secret = secret key — no migration needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New POST /api/remotion/publish endpoint: uploads MP4 to Cloudinary,
creates post/media records, publishes or schedules per-page as a Reel
- Desktop and mobile pages show page selector + DateTimePicker + description
after video generation, matching the Reels publication workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parses the generated TTS audio file with music-metadata to get the real
duration before computing word timings, eliminating desync between text
overlay and voice playback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Video duration forced to 25-30s (content 22-27s + 3s ending)
- Logo watermark (bottom-right) visible throughout content section
- Ending slide: logo centered + store name with spring animation (3s)
- Text overlay now shows only the active spoken word (true TikTok sync)
- Server fetches logo from Cloudinary config and store name from user's pages
- Player preview also shows logo/storeName via /api/cloudinary/config + /api/pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert relative music track URLs to absolute before passing to Remotion renderer
- Reverts Dockerfile from Debian-slim back to Alpine + gcompat/libc6-compat for lighter image
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>