Commit Graph
278 Commits
Author SHA1 Message Date
Claude fdace9d8d5 Qwen TTS : serveur GPU distant (Unraid) réglé dans les paramètres
- qwen-tts : GPU NVIDIA détecté automatiquement (bfloat16), image CUDA 12.8
  (RTX 50xx comprises) via TORCH_VARIANT=cu128, docker-compose.gpu.yml
  dédié (clé obligatoire, choix de la carte) et guide d'installation Unraid.
- Paramètres → « Qwen TTS (voix locale) » : adresse + clé du service, test
  de connexion (modèle et carte graphique affichés), stockés en base
  (app_config.qwen_tts_url / qwen_tts_api_key) et transmis à ffmpeg-api à
  chaque voix ; QWEN_TTS_URL reste un défaut.
- Le service qwen-tts CPU du docker-compose principal devient optionnel
  (profil qwen-local).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Upu97wMmsRkBoj6iVM4rH6
2026-09-29 09:57:32 +00:00
Claude 44daa88a49 Voix : plus d'accélération sur les SRT, moteur local Qwen3-TTS
SRT : la voix n'est plus jamais accélérée (atempo sans plafond, jusqu'à 2×
et plus, rendait la voix incompréhensible). Un morceau plus long que son
sous-titre déborde et décale les suivants ; les sous-titres suivent la voix,
la vidéo s'allonge et un avertissement signale le décalage.

Rythme : style par défaut « neutre », consignes Gemini sans « rythme
entraînant », débit Edge des styles dynamique/promo ramené à +0 %/+3 %.

Nouveau moteur « qwen » : service qwen-tts (Qwen3-TTS sur CPU, gratuit,
français, ton piloté par consigne, clonage de voix depuis qwen-tts/voices).
Même vérification Whisper que Gemini, généralisée ; repli qwen → gemini →
edge, et une lecture sous 85 % du texte est écartée (mots sautés/faux).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Upu97wMmsRkBoj6iVM4rH6
2026-09-29 09:39:47 +00:00
Claude d4c544e467 fix(voix): plus de fins de phrases coupées ni de bruits parasites
Fins de phrases coupées :
- Le fondu final ne commence plus avant la fin de la voix (il rognait la
  dernière phrase quand la vidéo finissait peu après) ; il se raccourcit au
  besoin. Même règle côté Python, aperçu et rendu Remotion
- SRT : découpe au milieu de la zone la plus calme de chaque pause, mesurée
  sur l'enveloppe sonore (tranches de 10 ms), avec un seuil relatif au
  niveau de la voix qui respecte les fins douces ; 300 ms gardées après le
  dernier son

Bruits parasites :
- Normalisation de la voix en deux passes, en mode linéaire : le mode
  dynamique relevait souffles et bruits entre les phrases (mesuré : -64 dB
  avant, -75 dB après dans les pauses)
- Réduction de bruit douce (afftdn), compresseur sans gain ajouté, fondus
  de 5 ms aux extrémités ; mix final moins « pompant » (LRA 20)
- Voix Gemini vérifiée : les bruits ajoutés avant le premier ou après le
  dernier mot sont retirés

Vérification de lecture comparée lettre à lettre : les petites erreurs de
Whisper ne déclenchent plus de régénérations inutiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
2026-09-28 09:21:18 +00:00
Claude f3113a0f5a fix(reels): garder le son d'origine de la vidéo quand aucune musique n'est choisie
Sans musique (bouton « Passer »), la bande son de la vidéo était supprimée
dès que la voix était activée. Elle sert désormais de fond sonore : conservée
telle quelle, baissée automatiquement sous la voix, puis normalisée avec le
reste. Une musique choisie la remplace toujours.

Même comportement dans l'aperçu, le rendu Remotion et le rendu FFmpeg de
secours. Le bouton indique « Passer (garder le son de la vidéo) ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
2026-09-28 08:57:38 +00:00
Claude 1d47763b29 feat(reels): interrupteur « Afficher le logo sur la vidéo »
Le petit logo affiché pendant la vidéo devient optionnel (activé par
défaut), sur les 4 pages Reel (vidéo et images, bureau et mobile) ainsi que
dans l'aperçu. Le grand logo de l'effet de fin reste piloté par son propre
interrupteur. Pris en charge par le rendu Remotion et par le rendu FFmpeg de
secours (show_watermark).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
2026-09-28 06:45:38 +00:00
Claude f5e2461da1 feat(reels): import d'un fichier SRT lu par la voix à son minutage
- Nouveau Reel (bureau et mobile) : bouton « Importer un fichier SRT ».
  Tant qu'un SRT est chargé, le texte libre et l'assistant IA sont
  désactivés ; le choix de la voix (moteur, voix, ton) reste disponible.
- Service Python : chaque sous-titre est lu séparément, ses silences
  retirés, puis posé à son instant de début. Une lecture trop longue est
  accélérée (atempo, sans changer la hauteur) pour tenir dans la durée du
  sous-titre, avec un avertissement au-delà de ×1,35. Après un repli sur
  Edge, tous les sous-titres gardent la même voix.
- Sans voix, les mots de chaque sous-titre s'affichent sur son intervalle.
- Aperçu : sous-titres calés sur le minutage du fichier.
- Tests : lecture SRT (TS), fenêtres, accélération et mixage (Python).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WVqEw4Xycgf8BtwmSftE2M
2026-09-27 08:19:44 +00:00
Claude 9b66a6dc5a feat(reels): montage Remotion unifié, aperçu en direct, tests et CI
Montage (étape 3) :
- Nouvelle composition ReelVideo : sous-titres animés mot à mot (3 styles :
  Impact, Surligné, Épuré), logo, effet de fin et fondu, en React
- Le service Python prépare l'image (recadrage, HDR, stabilisation, dernière
  image figée) et la piste son finale (/prepare-reel) ; Remotion compose
- Reels d'images sur les mêmes composants, avec le vrai minutage de la voix
- Police Montserrat embarquée (plus de dépendance à Google Fonts)
- REEL_RENDERER=ffmpeg conserve le rendu FFmpeg, plus rapide, en secours
- Script de pré-bundle réparé (échouait en silence : require en ESM)

Interface (étape 4) :
- Aperçu en direct avec @remotion/player, identique au rendu final ; la voix
  testée cale les sous-titres, sinon minutage estimé
- Choix du style de sous-titres sur les 4 pages Reel
- Vraie progression : étape réelle du rendu, échecs visibles 24 h avec leur
  cause ; fin de la barre simulée et du faux « publié avec succès »

Outillage (étape 5) :
- Tests vitest (minutage identique à Python, validation, sécurité) et
  pytest ; CI GitHub Actions (tsc, tests, build, ruff)
- Captures d'écran, out.mp4 et scripts de test retirés de la racine

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
2026-09-24 14:37:42 +00:00
Claude 3c6a66bf64 feat(reels): voix Gemini complète, sous-titres mot à mot et rendu de qualité
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
2026-09-24 12:31:27 +00:00
Claude d24ab0d4b7 feat(reels): file d'attente persistante des rendus (reel_jobs)
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
2026-09-24 09:31:31 +00:00
Claude 94e961d0f1 perf(api): alléger les requêtes du calendrier, des stats et de la médiathèque
Trois défauts se cumulaient : des réponses bien plus larges que nécessaire,
aucun index en base, et des boucles de requêtes là où une seule suffisait.

Réponses
- /api/scheduled-posts renvoyait la ligne `social_pages` entière, jetons
  d'accès Facebook et TikTok compris, jusque dans le navigateur. Seules les
  colonnes réellement affichées sont désormais sélectionnées.
- Le filtre de dates est poussé en SQL. Il était appliqué en JavaScript après
  avoir chargé tout l'historique — que le client ne bornait jamais. Le
  calendrier demande maintenant les six semaines affichées, le sélecteur de
  date les créneaux à venir, le bloc « publications récentes » ses 90 jours.
- /api/media accepte `limit` et `offset` (sans eux, réponse inchangée : des
  écrans doivent retrouver un média par son identifiant). La médiathèque
  pagine côté serveur au lieu de tout charger pour afficher cinq vignettes, et
  l'écran de création ne demande que les douze médias qu'il propose. Le total
  voyage dans l'en-tête X-Total-Count pour garder le compteur juste.

Index
- server/migrate.ts est le seul script exécuté au démarrage et n'en créait
  aucun ; ceux de migration-docker.sql ne servaient à rien, ce fichier n'étant
  monté nulle part. Postgres n'indexant pas les clés étrangères, le calendrier,
  l'historique et le scheduler parcouraient les tables entières. S'y ajoutent
  deux index partiels pour les boucles de fond : publications en attente et
  suivi asynchrone TikTok.

Requêtes
- /api/stats lisait quatre tables entières pour en compter les lignes : c'est
  désormais quatre COUNT filtrés.
- Les vues d'administration listaient les utilisateurs puis interrogeaient les
  pages de chacun ; une seule requête les remplace.
- getPostWithMedia et l'API externe chargeaient les médias un par un.
- /api/reels/ongoing chargeait tous les posts de l'utilisateur pour n'en garder
  que ceux en cours de génération, toutes les trois secondes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qns6Gcpd95bCHWUxySh4Ew
2026-09-10 06:39:16 +00:00
Claude 25bf0981bd perf(client): rafraîchir les données sans recharger la page
`staleTime: Infinity` combiné à `refetchOnWindowFocus: false` empêchait toute
revalidation : une donnée chargée à la première visite restait affichée pour
toute la session. Or le serveur agit de son côté — le scheduler publie chaque
minute, les jetons se renouvellent en tâche de fond — sans que le navigateur en
soit averti. D'où la nécessité de recharger la page pour voir quoi que ce soit.

Les compteurs du tableau de bord étaient le cas le plus visible : /api/stats et
/api/posts n'étaient invalidés par aucune mutation de l'application, et ne
pouvaient donc plus bouger du tout.

- Péremption courte (30 s) et rafraîchissement au retour sur l'onglet ou sur
  reconnexion, en remplacement du cache éternel.
- /api/stats et /api/posts, qui dérivent de tout le reste, sont invalidés depuis
  le MutationCache après chaque mutation réussie : aucune mutation ne peut
  raisonnablement se souvenir de le faire, et aucune ne le faisait. Les
  mutations d'authentification en sont exclues, la session étant déjà détruite
  quand la déconnexion aboutit.
- Les trois sondages à 3 s (historique desktop et mobile, reels en cours)
  devenaient inutiles hors traitement en cours. Ils ne s'emballent plus que
  lorsqu'il y a effectivement quelque chose à suivre — une publication dont
  l'heure est passée, une génération en route — et retombent à 30 s sinon. La
  page Historique rechargeait tout l'historique 20 fois par minute.
- La liste des modèles OpenRouter, servie par un appel sortant, garde une
  péremption d'une heure pour ne pas être redemandée à chaque retour d'onglet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qns6Gcpd95bCHWUxySh4Ew
2026-09-10 06:22:21 +00:00
Claude d7f002a64f feat(facebook): renouvellement automatique des jetons de page
Un jeton de page collé à la main meurt au bout de 60 jours — ou du jour au
lendemain si le mot de passe change, si le compte perd son rôle d'admin ou si
Meta révoque l'autorisation — et on ne l'apprenait qu'à la première publication
en échec : le contrôle quotidien existant se contentait de marquer la page
« expired ».

On reprend la chaîne recommandée par Meta, comme dans PleinR : autorisation
OAuth, échange du code contre un jeton utilisateur longue durée (~60 j), puis
dérivation des jetons de page — qui n'expirent pas. Le jeton utilisateur est
conservé chiffré sur chaque page : c'est lui qui permet au TokenManager de
régénérer un jeton révoqué sans intervention humaine, et de se prolonger
lui-même avant échéance.

- facebook_config : App ID / App Secret de l'application, saisis dans les
  paramètres (chiffrés en base) ou par variables d'environnement.
- /api/facebook : configuration, connexion OAuth, callback qui crée ou met à
  jour les pages, contrôle d'une page à la demande, contrôle global.
- TokenManager : /debug_token pour connaître l'état réel du jeton, puis
  renouvellement automatique 15 jours avant l'échéance ou dès révocation ;
  repli sur le contrôle historique tant que l'application n'est pas configurée
  ou si Facebook est injoignable, pour ne pas passer l'UI au rouge sur une
  panne réseau. Le cron passe à deux fois par jour.
- Les comptes Instagram professionnels rattachés à une page reçoivent le même
  traitement, puisqu'ils publient avec le jeton de cette page.
- L'UI montre l'état constaté (jeton valide, bientôt expiré, révoqué), l'erreur
  à corriger et un bouton de contrôle immédiat ; un bandeau signale les pages à
  reconnecter.
- /api/pages ne renvoie plus aucun jeton au navigateur, seulement un indicateur
  de renouvellement automatique.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qns6Gcpd95bCHWUxySh4Ew
2026-09-10 06:11:43 +00:00
Claude ac8b494f24 fix: vignettes vidéo persistantes et remise à zéro des erreurs de types
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
2026-08-12 08:00:48 +00:00
Claude c69fe4e462 fix(ia): afficher la cause réelle d'un échec de génération de texte
La route /api/ai/generate connaissait la cause exacte de l'échec
(configuration absente, clé invalide, crédits épuisés, modèle inconnu)
mais la remplaçait par un « Failed to generate text » en 500, et
l'interface affichait de son côté un message figé. Sans accès aux logs
du serveur, l'utilisateur n'avait donc aucun moyen de savoir quoi
corriger.

Le motif renvoyé par OpenRouter est désormais extrait, tronqué et
propagé jusqu'au bandeau d'erreur, avec un code HTTP qui distingue un
problème de configuration (400) d'un refus du service tiers (502).
La clé API n'apparaît à aucun moment dans ces messages.

Même traitement pour /api/reels/generate-text, qui souffrait du même
masquage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
2026-08-12 07:49:23 +00:00
Claude b95cd23ebc feat(tiktok): publier les reels sur les comptes TikTok des magasins
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
2026-08-11 14:29:38 +00:00
Claude 2188fa458d fix: recover gracefully from stale sessions causing repeated 401s
The SPA's session check (/api/auth/session) is cached indefinitely by
react-query (staleTime: Infinity, no refetch on focus/interval), so once a
protected page mounts successfully, the app never re-verifies auth. If the
server-side session later becomes invalid (server restart with in-memory
sessions, cookie/session expiry, etc.), every subsequent API call
(pages, scheduled-posts, audio-tracks, media, media/upload, ...) starts
failing with 401 in a loop with no way for the user to recover short of a
manual page reload.

Add a shared handleUnauthorized() in queryClient.ts that redirects to
/login on any non-auth API 401, wired into both the shared fetch helpers
(apiRequest/getQueryFn) and the raw fetch() calls used for file uploads
and Remotion rendering, which also lacked this recovery path. Also add the
missing credentials: "include" to those raw fetch() calls for consistency
with the rest of the app's API requests.
2026-07-27 11:56:56 +00:00
Claude 09b4e597ae fix(openrouter): respect the requesting user's saved model config
generatePostText ignored the userId it received and always read an
arbitrary row via getAnyOpenrouterConfig() (LIMIT 1, no ORDER BY),
so a model chosen and saved in Settings could be shadowed by another
stale config row. Now it prefers the requesting user's own config,
falling back to the most recently updated shared one. Also swap the
retired anthropic/claude-3.5-sonnet default for a live OpenRouter slug.
2026-07-14 05:49:31 +00:00
Claude 50a011ce23 Simplify Gemini voice options and default to male voice (Charon)
Replace the 8-item Google Cloud TTS voice list with 2 Gemini native
voices: Charon (homme) and Kore (femme). Set Charon as the default
when switching to Gemini engine in all four reel pages.

https://claude.ai/code/session_01QBvwHAMZzVYfWvy1U5paat
2026-05-19 13:19:15 +00:00
Michael 2ffa30133d Fix sync-info to send ttsEngine and ttsVoice (not voice)
- new-reel.tsx was still sending 'voice' parameter instead of 'ttsVoice/ttsEngine'
- This caused sync-info to always fail with 400 'Texte et voix requis' for Gemini users
2026-05-19 14:19:18 +02:00
Michael 72b13b3e42 Add Google Gemini TTS as alternative to Edge TTS
- Store Gemini API key globally in appConfig (single key for all users)
- Add /api/settings/gemini GET/POST/DELETE routes for API key management
- Backend: add tts_engine parameter ("edge" or "gemini") to FFmpeg service
- Backend: add generate_tts_gemini() using Google Cloud TTS REST API
- Frontend: Settings page shows Google Gemini API key input card
- Frontend: new-reel, mobile/new-reel, remotion-video, mobile/remotion-video
  pages now have Edge/Gemini engine toggle and French voice selector
- Fix tts-preview route to extract ttsVoice from req.body instead of
  undefined voice variable
2026-05-19 12:17:42 +02:00
Michael 59bc56aedc refactor: remove Piper TTS, keep edge_tts only
- Remove piper_url from ReelRequest model and all function signatures
- Remove generate_tts_piper() function and Piper branch in generate_tts_with_subs()
- Remove /api/piper/config routes from server/routes.ts
- Remove piperConfig table, schemas and storage methods
- Remove piper_config migration
- Remove Piper TTS settings UI card
- Update "Piper TTS" labels to "TTS — voix activée"

edge_tts is now the only TTS engine, using precise word-boundary timing
2026-05-19 10:55:01 +02:00
MichaelandClaude Sonnet 4.6 797e12dde5 refactor(tts): replace Minimax+Freesound with Piper TTS
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>
2026-05-19 09:15:26 +02:00
Michael 0209c3eacf feat(minimax): add Group ID support to fix rate limit quota
Pass GroupId query param to Minimax T2A v2 API — required for paid
plan quota allocation. Without it, Minimax defaults to (0/0 used).

- shared/schema.ts: groupId field on minimaxConfig table
- server/migrate.ts: ADD COLUMN IF NOT EXISTS group_id
- server/routes/reels.ts: fetch and pass groupId alongside apiKey
- server/services/ffmpeg.ts: minimax_group_id in request interface/body
- ffmpeg-service/main.py: GroupId in URL, threaded through all call sites
- client/src/pages/settings.tsx: Group ID input in Minimax settings card
2026-05-18 13:50:41 +02:00
Michael b7ef4940f7 feat: add Minimax Speech as alternative TTS provider for Reels
- Add minimax_config table (migration + schema + storage CRUD)
- Add GET/POST /api/minimax/config routes
- Pass tts_provider + minimax_api_key through ffmpeg service
- Add generate_tts_minimax() in Python using Minimax T2A v2 API
- Fall back to ffsubsync for subtitle sync (no WordBoundary events)
- Add Minimax config card in Settings page
- Add provider toggle (Edge TTS / Minimax) + French voices in new-reel
2026-05-18 11:55:07 +02:00
MichaelandClaude Sonnet 4.6 391acfdb83 feat: add external API for publishing posts with image URL and scheduling
- New POST /api/v1/publish endpoint: download image from URL, create post and schedule it per page
- New GET /api/v1/pages endpoint: list available pages for external callers
- API key auth via X-API-Key header, configurable from admin settings (stored in DB)
- New app_config table (migration included) to store the external API key
- Admin-only settings section (desktop + mobile) to set/revoke the key
- Fallback to EXTERNAL_API_KEY env var if no DB key is configured

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:12:27 +02:00
MichaelandClaude Opus 4.7 9ce735dc23 feat: add adaptive TTS text-to-speech synchronization for Reels with real-time sync info widget
Calculate optimal word_duration based on actual TTS audio duration and text punctuation.

- server/services/ttsSync.ts: new service to measure TTS audio and compute sync timing

- server/routes/reels.ts: integrate sync into preview and background processing

- client: auto-calculate sync info widget in desktop and mobile Reel creation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:35:04 +02:00
Michael 9913c307ed feat: implement Remotion video generation, text-to-speech, and automated social media publishing workflow 2026-04-29 11:09:28 +02:00
Michael 0eca57efab feat: Introduce Remotion video creation and publishing page with a media thumbnail component and backend routes. 2026-03-27 09:45:38 +01:00
MichaelandClaude Sonnet 4.6 2a93c2aea7 fix: detect publish_video permission error + show required permissions in Pages UI
- Detect Facebook error subcode 1363042 (missing publish_video permission)
  and throw a clear actionable message explaining how to fix it
- Add permission notice banner in Pages admin showing the 3 required
  Facebook permissions: pages_manage_posts, pages_read_engagement, publish_video

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 20:51:47 +01:00
MichaelandClaude Sonnet 4.6 b8dc93eb2d fix: invalidate scheduled-posts cache after video publish/schedule
Calendar now updates instantly without page reload after creating
a video publication or scheduling from the Remotion generator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 17:11:58 +01:00
MichaelandClaude Sonnet 4.6 5d7ab93bf8 feat: add Ken Burns effect, fix caption visibility and Facebook binary upload
- 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>
2026-03-25 17:01:52 +01:00
Michael 487a8ee352 feat: Add Remotion video rendering API and composition for dynamic video generation from images, text, and audio. 2026-03-25 16:52:58 +01:00
Michael 56210687a2 feat: implement Facebook posting service, add new sidebar component, and introduce Remotion server route. 2026-03-25 16:36:57 +01:00
Michael e1a6d1e06c feat: add mobile Remotion video generator with image, text, audio, and Facebook publishing features. 2026-03-25 16:16:30 +01:00
Michael 0c525665d8 feat: implement Remotion image composition with dynamic text, audio, and branding, and a server-side rendering API. 2026-03-25 12:53:52 +01:00
Michael 1957445225 feat: Implement Remotion video generation and publishing page with image selection, text-to-speech, music, and social media integration. 2026-03-25 11:58:34 +01:00
Michael 59caa19fbe feat: add mobile video generator page with Remotion integration for rendering and publishing. 2026-03-25 11:46:15 +01:00
MichaelandClaude Sonnet 4.6 281597e944 feat: add camera capture button to mobile video generator
Users can now take photos directly with the phone camera in the
Générateur Vidéo page, matching the behavior of Nouvelle Publication.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:56:33 +01:00
MichaelandClaude Sonnet 4.6 695d219724 fix: restore 12-image limit and fix blank thumbnails on mobile
Thumbnails were blank (empty squares) on iOS/Android because h-full
does not resolve correctly inside a button with aspect-ratio sizing.
Switched to absolute inset-0 positioning which works in all mobile
browsers.

Also restored the slice(0, 12) limit on the media grid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 09:02:30 +01:00
MichaelandClaude Sonnet 4.6 be46d43dd6 fix: show camera-uploaded images immediately in mobile new post
- Remove 12-item limit so all library images are visible (scrollable grid)
- Track newly uploaded items in recentUploads state so camera captures
  appear instantly without waiting for the API refetch to complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 08:40:16 +01:00
Michael 8f7f580b2f feat: Implement Facebook Reels API, including Jamendo music management, AI text generation, and MinIO storage integration. 2026-03-25 08:14:17 +01:00
MichaelandClaude Sonnet 4.6 2744b538e3 fix: Replace broken media thumbnails with MediaThumbnail component
Old Cloudinary URLs no longer load (account full). The new MediaThumbnail
component handles broken URLs gracefully with a fallback icon, and uses
<video preload="metadata"> for local video files to show the first frame.

- New component: client/src/components/media-thumbnail.tsx
- Updated: new-reel.tsx, new-post.tsx, media-upload.tsx,
           edit-scheduled-post-dialog.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 08:05:04 +01:00
MichaelandClaude Sonnet 4.6 7733acbc22 feat: Replace external storage with local file storage + auto cleanup
Media is now stored directly on the server disk (no external service).
Files are served via Express static middleware at /uploads/*.

Cleanup rules:
- Videos deleted immediately after successful publish to Facebook/Instagram
- Videos older than 7 days purged daily by cron job
- Images older than 30 days purged daily by cron job

Changes:
- server/services/minio.ts: replaced S3 client with local fs read/write
- server/index.ts: added static serving for /uploads/media, /logos, /stories
- server/services/media-purge.ts: new rules (images 30d, videos 7d)
- server/services/scheduler.ts: delete local videos after successful publish
- docker-compose.yml: add media_uploads, logos_uploads, stories_uploads volumes
- settings UI: removed credentials card, shows local storage info
- package.json: removed googleapis (unneeded)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 07:35:27 +01:00
MichaelandClaude Sonnet 4.6 ba94e7cc7d feat: Add MinIO endpoint/public URL configuration via settings UI
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>
2026-03-24 21:19:49 +01:00
MichaelandClaude Sonnet 4.6 841ffb306c fix: add sidebar and topbar to desktop remotion video page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:09:03 +01:00
MichaelandClaude Sonnet 4.6 e39fa1026d feat: replace Cloudinary with MinIO for all media storage
- 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>
2026-03-24 21:05:37 +01:00
MichaelandClaude Sonnet 4.6 b0e11cb8ab feat: clean up desktop remotion page — remove preview player and unused code
Remove Player, ImageComposition, useMemo imports, estimateWordTimings,
stripForTTS, cloudinaryConfig query, and all preview-related computed values
from the desktop page. Same cleanup already applied to mobile page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:29:04 +01:00
MichaelandClaude Sonnet 4.6 ff5e02e30d feat: clean up mobile remotion page — remove preview player and download button
Remove unused Player, ImageComposition, useMemo imports and all associated
computed values (combinedUrls, previewWordTimings, ENDING_FRAMES, totalFrames,
previewLogoUrl, previewStoreName, cloudinaryConfig query). Remove Aperçu card
with broken Player preview, download button, and conditional generate button wrapper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:27:55 +01:00
MichaelandClaude Sonnet 4.6 36fe05c04b feat: show 3 words at a time in text overlay (TikTok style)
Groups word timings by chunks of 3. The active word is highlighted in
yellow/scaled; the 2 surrounding words show in white. Gives enough
reading time while staying in sync with the voice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 19:41:29 +01:00
MichaelandClaude Sonnet 4.6 4072f8954f feat: add publication scheduling to Remotion video generator
- 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>
2026-03-24 19:36:53 +01:00