Default tts_engine changed from "edge" to "gemini".
Both /process-reel and /preview-tts now try Gemini first,
fall back to Edge TTS on failure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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
- 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')
- Add response structure logging to see actual keys returned by Gemini API
- Log timepoints count and first item to diagnose word boundary parsing
- Add 'unexpected timepoint format' warning for easier debugging
- Convert emoji to ASCII-safe versions for container logs
- Edge TTS fallback list now includes fr-FR-RemyMultilingualNeural
- Removed 'Neural not in voice' check that rejected Gemini voices (fr-FR-Standard-A etc) before falling back - these are valid voices and Edge-tts handles them properly
- Improved is_male check to match 'Remy' not 'Remi'
- 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
- 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
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>
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
- 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
- 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)
- 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
- 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>
Some voices/texts do not produce WordBoundary events from edge_tts.
When word_boundaries is empty, generate_ass_from_word_boundaries returns
without writing the ASS file, causing FFmpeg to fail with ENOENT.
Fallback to the old ffsubsync path when no boundaries are captured.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 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>
- 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>
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>
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>
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>
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>
- 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>
req.file.buffer is undefined with diskStorage — must read the temp file
from req.file.path using fs.readFileSync instead.
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>
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>
Images uploaded between the local storage switch and the relative URL
fix have http://hostname/uploads/... stored in DB, which is blocked by
CSP. POST /api/media/fix-urls (admin only) updates all affected rows
to use relative /uploads/... paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
Alpine + gcompat lacks posix_fallocate64 symbol required by Remotion's Chrome
headless shell. Debian-slim has real glibc so Chrome runs correctly.
NODE_OPTIONS stays before npm install to prevent OOM during build.
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>