Commit Graph
244 Commits
Author SHA1 Message Date
Claude 086ef956e2 Reels : option « Publier aussi en story » sur Facebook
Une case sur les écrans de création (bureau, mobile) et de programmation
d'un Reel double la publication d'une story vidéo sur les pages Facebook
sélectionnées. TikTok n'ayant pas de stories, l'option ne le concerne pas.

- Publication immédiate : la story part juste après le Reel, à partir de la
  vidéo rendue envoyée en binaire (nouvelle publishVideoStoryFromBuffer).
  Un échec de la story est noté dans l'historique sans faire échouer le Reel.
- Publication programmée : une entrée « story » est planifiée à côté du
  Reel ; le planificateur la publie comme les autres stories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPMYHUDaZyrG3xRCM5iGVr
2026-10-05 06:50:33 +00:00
Claude da9a74dbe6 Retire le générateur vidéo, les badges « New » et permet d'effacer les Reels en échec
- Suppression de la page « Générateur Vidéo » (/remotion) : routes
  /api/remotion, pipeline de rendu d'images, composition ImageVideo et
  aperçu associé. Le générateur de Reels est conservé.
- Plus de badge « New » à côté des menus.
- Tableau de bord : bouton pour supprimer un Reel en échec et sa
  notification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tFkVXQDATYfw5aFtjQahi
2026-09-29 13:32:17 +00:00
Claude 188706f4dc Reels : programmer la publication d'une vidéo déjà montée
Nouvelle page « Programmer un Reel » (/reel/schedule) : envoi ou choix
d'une vidéo, description, pages Facebook/TikTok, date et heure. Le Reel
est publié tel quel par le planificateur, sans passer par le générateur.

- POST /api/posts valide les Reels (une vidéo, pages Facebook/TikTok)
- vignette extraite à l'envoi des vidéos pour l'historique

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tFkVXQDATYfw5aFtjQahi
2026-09-29 13:16:06 +00:00
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 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 3af7f9418b fix(oauth): déduire l'URI de redirection du domaine public réel
Sans APP_URL, Facebook et TikTok recevaient
http://localhost:5555/api/{facebook,tiktok}/callback : une URI que les
fournisseurs refusent, donc aucune page connectable derrière un reverse
proxy. Le repli localhost n'est plus utilisé que hors requête HTTP ; en
service, la base publique est reconstruite depuis X-Forwarded-Proto /
X-Forwarded-Host (trust proxy est déjà activé).

- `resolvePublicBaseUrl(req)` centralise la résolution (APP_URL prioritaire)
- l'URI de redirection est déduite de la requête au démarrage du flux, à
  l'échange du code et dans l'affichage des paramètres : les trois restent
  identiques, comme l'exige OAuth
- docker-compose ne force plus APP_URL à localhost, qui rendait l'auto
  détection inopérante
- cookie de session en `secure: 'auto'` à défaut d'APP_URL, pour ne pas
  émettre un cookie non sécurisé derrière un proxy HTTPS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Db2EazEcvnqgTTeg8oGTif
2026-09-11 18:01:28 +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 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 0570088722 feat(tiktok): servir le fichier de vérification du domaine
TikTok exige de prouver la propriété du domaine ("URL properties") avant
d'autoriser la Content Posting API, en servant un fichier à la racine du
site.

Le dossier public-root/ est exposé publiquement à la racine du domaine et
monté avant le catch-all du frontend. Y déposer un fichier suffit : pas de
modification de code ni de reconstruction du client, ce qui couvre aussi
les prochaines vérifications (autre domaine, Google, Meta).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
2026-08-11 16:27:26 +00:00
Claude 3e00a3ca67 feat(legal): préremplir les mentions légales de la société exploitante
Renseigne l'identité de FROUARD DISTRIBUTION (enseigne LA FOIR'FOUILLE)
comme valeur par défaut des pages /terms et /privacy : forme juridique,
capital, siège et numéro RCS. Les relecteurs TikTok ouvrent ces pages
pendant l'audit et refusent les mentions incomplètes.

Chaque champ reste surchargeable par variable d'environnement si l'outil
venait à être exploité par une autre société du groupe.

L'adresse de contact reste à fournir via LEGAL_CONTACT_EMAIL : elle ne
figure pas au registre, et le RGPD impose un point de contact pour
l'exercice des droits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
2026-08-11 16:19:16 +00:00
Claude d0f63c68e4 feat(legal): servir les pages /terms et /privacy exigées par TikTok
L'audit de l'application développeur TikTok impose une URL publique pour
les conditions d'utilisation et pour la politique de confidentialité, et
les relecteurs les ouvrent réellement.

Les pages sont rendues en HTML côté serveur, sans authentification, pour
rester lisibles par un robot qui n'exécute pas le JavaScript du client.
La politique décrit précisément ce que l'intégration fait : données reçues
de TikTok (open_id, nom d'affichage, avatar, jetons), finalité de
publication, chiffrement des jetons au repos, suppression à la
déconnexion et marche à suivre pour retirer l'autorisation.

Les mentions de l'exploitant se configurent via LEGAL_COMPANY_NAME,
LEGAL_COMPANY_ADDRESS et LEGAL_CONTACT_EMAIL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
2026-08-11 16:14:13 +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 8155b30c63 fix(openrouter): stop logging API key material, use a hash fingerprint
Address Codex review: the debug log previously included the key's
first 10 characters. Replace with a non-reversible SHA-256 fingerprint
so logs remain useful for correlating support reports without ever
exposing key material.
2026-07-14 06:31:18 +00:00
Claude de7fc7d03c fix(openrouter): trim API key and guard against empty key
An OpenRouter key with trailing whitespace/newline (common from
copy-paste) produces "Missing Authentication header" from their API,
which looked identical to a missing key. Trim apiKey on save (schema
level) and on use (defense in depth), fail fast with a clear message
if the stored key is empty, and log a masked key preview + model on
each generation call to make future auth failures diagnosable from
container logs.
2026-07-14 06:25:47 +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 07fd43a5ed Add GEMINI_API_KEY env var fallback for TTS
Add GEMINI_API_KEY to docker-compose so it can be set in Portainer.
Fall back to this env var when the DB app_config has no geminiApiKey,
so Gemini TTS works without requiring the user to save the key through
the Settings UI.

https://claude.ai/code/session_01QBvwHAMZzVYfWvy1U5paat
2026-05-19 13:39:37 +00:00
Claude e2002cf163 Pass Gemini API key in /reels/tts-preview route
The TTS preview route hardcoded the Gemini API key to undefined when
calling the ffmpeg service, causing the service to silently fall back to
Edge TTS even when the user selected Gemini. Mirror the lookup already
used in the reel processing route so the selected engine is honored.

https://claude.ai/code/session_01QBvwHAMZzVYfWvy1U5paat
2026-05-19 12:49:16 +00:00
Michael a9cda4ffd4 Fix sync-info route to use ttsEngine and ttsVoice
- ttsSyncService.calculateSyncTiming now accepts ttsEngine parameter
- sync-info route extracts ttsVoice/ttsEngine from req.body instead of voice
- Previously it always fell back to Edge TTS even when Gemini was selected
  (because server stored 'fr-FR-Standard-B' but route only used 'voice')
2026-05-19 14:12:08 +02:00
Michael a2c2753b0b Add gemini_api_key column migration to migrate.ts 2026-05-19 12:25:14 +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 cf20b0a264 fix(tts): surface TTS errors from Python to Node.js logs and DB
- Python: capture tts_error_msg on exception, return in response
- Python: log tts_provider, minimax_api_key presence before call
- ffmpeg.ts: read tts_error from response, log and return it
- reels.ts: store TTS error in post.generationError for visibility
2026-05-18 13:26:13 +02:00
Michael 211b7dc272 fix(minimax): correct API payload and add TTS debug logging
- Fix model name: speech-2.8-hd -> speech-02-hd
- Fix bitrate: 128 -> 128000 bps
- Add channel, speed, pitch, vol to voice/audio settings
- Handle binary audio response (Content-Type: audio/*)
- Fallback audio field lookup: data.data.audio || data.audio
- Add background job log: ttsProvider + hasMinimaxKey
- Log full FFmpeg request body (text truncated, key masked)
2026-05-18 12:50:59 +02:00
Michael 35cf21209f fix(settings): prevent empty string API keys from being saved or preserved
- Add .min(1) validation to insertOpenrouterConfigSchema
- Strip empty/whitespace-only apiKey before fallback to existing key
- Return 400 if resulting apiKey is empty (forces user to enter valid key)
2026-05-18 12:32:11 +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 Opus 4.7 9932561050 feat: add list, edit, delete endpoints to external API
GET /api/v1/posts - list upcoming scheduled posts with filters
PATCH /api/v1/posts/:id - edit content, schedule, or image
DELETE /api/v1/posts/:id - remove scheduled post and cascading relations

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 22:05:20 +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 d763be6322 fix: prevent ENOENT on scheduled reel publishing when local file deleted early
- facebook.ts: use resolvePublicUrl + getMediaBuffer for reels so missing local files fall back to HTTP fetch.
- scheduler.ts: only delete local video files after the last pending scheduled post for that postId is published.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 14:08:15 +02:00
MichaelandClaude Opus 4.7 37aba33425 fix: precise TTS word-boundary timing and remove wasted sync calculations
- ffmpeg-service/main.py: replace ffsubsync path with exact word-boundary timing from edge_tts for TTS subtitles. Karaoke styling preserved.
- server/services/ttsSync.ts: fix word count to match TTS-cleaned text, remove artificial punctuationPause subtraction, strip punctuation tokens from count.
- server/routes/reels.ts: remove redundant ttsSyncService calls in preview/background; word_duration is ignored by Python, these only wasted TTS generations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 13:13:55 +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 73e3a9d30f feat: add remotion router for video rendering with automated TTS synchronization and asset embedding 2026-04-29 10:33:05 +02:00
Michael f56a89e61c feat: implement FacebookService to handle post publishing for text, photos, videos, and multi-photo carousels 2026-04-01 20:19:48 +02:00
Michael 3122e162cc feat: implement user authentication and management routes with multer file upload support 2026-03-30 11:30:56 +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
Michael e82d73c4e1 feat: Add FFmpeg service for video processing, TTS generation, and dynamic subtitles. 2026-03-27 09:34:16 +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 8f13d3021e fix: use multipart/form-data for all resumable upload phases + verbose logging
Facebook resumable upload START/FINISH phases require multipart/form-data
(not application/x-www-form-urlencoded). Add full raw response logging for
each phase to diagnose any remaining issues (subcode, etc).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 20:46:35 +01:00
MichaelandClaude Sonnet 4.6 47ed1711dd fix: use Facebook Resumable Upload API for video publishing
Replace simple multipart upload (which fails with error 6000 on large
files) with the 3-phase Resumable Upload API (start → transfer → finish).
This is the recommended Facebook approach for files > ~50 MB and is much
more reliable regardless of file size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 20:15:11 +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