Aller au contenu

Pipeline TikTok automatisé

LXC 118 — 192.168.1.77 | Port API : 8201 | Service : tiktok

Pipeline de création automatique de vidéos TikTok "faceless" en français (faits insolites).


Stack technique

Piper TTS (fr_FR-tom-medium, ONNX local)
    → WAV → MP3 voix
faster-whisper small (CPU)
    → word timestamps
SequenceMatcher
    → alignement forcé script ↔ Whisper
Pexels API
    → 15 clips B-roll
FFmpeg
    → MP4 1080×1920 + sous-titres karaoké + musique
FastAPI port 8201
    → validation ntfy + téléchargement

Flow de production

python3 /opt/tiktok/pipeline.py "Sujet du fait insolite"
         ↓
  Génération script (Claude CLI)
         ↓
  Notification ntfy topic `tiktok` → boutons Valider / Rejeter
         ↓  (après validation)
  Voix TTS  →  Sous-titres Whisper  →  Clips Pexels  →  Rendu FFmpeg
         ↓
  Notification ntfy "vidéo prête" avec lien téléchargement
         ↓
  Télécharger : http://192.168.1.77:8201/download/{job_id}
         ↓
  Upload manuel TikTok Studio

Musique de fond

  • 9 pistes instrumentales dans /opt/tiktok/music/ (SoundCloud, royalty-free)
  • Volume mixé à 5% sous la voix via FFmpeg amix
  • Les fichiers préfixés _REMOVED_ sont ignorés automatiquement
  • Roulement : _pick_music() pioche parmi les pistes non encore utilisées ; quand toutes ont été jouées, le cycle repart de zéro. État dans /opt/tiktok/music_rotation.json
  • La piste choisie est loggée dans le job JSON (music_file)

Ajouter une piste :

/usr/local/bin/yt-dlp -x --audio-format mp3 \
  -o '%(title)s.%(ext)s' \
  --no-playlist \
  'scsearch1:epic cinematic instrumental no vocals' && \
mv "Titre.mp3" /opt/tiktok/music/

Désactiver une piste (voix détectée) :

mv /opt/tiktok/music/NomPiste.mp3 /opt/tiktok/music/_REMOVED_NomPiste.mp3


Prononciation TTS

generate_voice.py applique des substitutions avant d'envoyer le texte à Piper TTS. Les sous-titres affichent toujours le texte original du script.

Texte original Texte envoyé au TTS
essaim / essaims éssain / éssains
av. J.-C. avant Jésus-Christ
apr. J.-C. après Jésus-Christ
Hitler Hitlere

Pour ajouter un mot mal prononcé, éditer _TTS_SUBSTITUTIONS dans /opt/tiktok/generate_voice.py.


Sous-titres

  • Police : Anton, taille 110, position 500 px du bas
  • Effet karaoké : mot actif en jaune {\c&H00FFFF&}, autres mots en gris {\c&H909090&}
  • Un événement ASS par mot — alignement Whisper + SequenceMatcher sur le script original

Clips vidéo (Pexels)

  • Algorithme round-robin : 1 clip par keyword, tour à tour — tous les keywords contribuent
  • 5 premiers keywords : spécifiques au sujet | 5 derniers : contextuels/génériques
  • Préférence clips portrait natif (9:16) pour limiter le crop
  • Clé API Pexels : dans /opt/tiktok/config.py

Jobs et fichiers

Chemin Rôle
/opt/tiktok/jobs/{job_id}.json État complet du job (script, chemins, music_file…)
/opt/tiktok/counter.json Numéro auto-incrémenté de la prochaine vidéo
/opt/tiktok/music_rotation.json Pistes déjà utilisées dans le cycle en cours
/opt/tiktok/audio/ Fichiers MP3 voix
/opt/tiktok/subtitles/ Fichiers ASS sous-titres
/opt/tiktok/footage/{job_id}/ Clips Pexels bruts
/opt/tiktok/output/ Vidéos MP4 finales

Régénérer une vidéo existante

import json, sys
sys.path.insert(0, '/opt/tiktok')
import generate_voice, generate_subtitles, render_video

job = json.load(open('/opt/tiktok/jobs/{job_id}.json'))

# Regénérer uniquement le rendu (audio et clips déjà présents)
video_path, music_file = render_video.render(
    job['audio_path'], job['subtitle_path'], job['clips'], job['job_id']
)
job['video_path'] = video_path
job['music_file'] = music_file
json.dump(job, open(f"/opt/tiktok/jobs/{job['job_id']}.json", 'w'), ensure_ascii=False)

Pour regénérer audio + sous-titres + rendu (ex. correction prononciation) :

audio_path = generate_voice.generate(job['script'], job['job_id'])
subtitle_path = generate_subtitles.generate(audio_path, job['job_id'], job['script'])
video_path, music_file = render_video.render(audio_path, subtitle_path, job['clips'], job['job_id'])


Bugs résolus

2026-10-05 — _fix_plus dans generate_script.py fusionnait le commentaire méta de Claude avec le script. Corrigé via _strip_preamble().

2026-10-05 — Lien de téléchargement absent de la notif "vidéo prête". Corrigé dans notify.py : URL en première ligne.

2026-10-11 — edge-tts abandonnée : Microsoft a modifié son protocole TTS côté européen (Sec-MS-GEC-Version 2-155.x, nouvel endpoint) → erreur 403 permanente. Remplacé par Piper TTS local (fr_FR-tom-medium.onnx, 61 Mo ONNX, /opt/tiktok/voices/). Voix masculine FR, aucune dépendance externe.

2026-10-11 — FORBIDDEN: animals retiré du prompt keywords Pexels : la règle empêchait les clips d'animaux même quand l'animal est le sujet principal (ex. vidéo 16 sur Acoustic Kitty).

2026-10-09 — Piste "Mountains Epic Cinematic (Short version)" contenait un chœur vocal répété (identifié par cross-corrélation Python/NumPy sur l'audio de la vidéo 14). Désactivée (_REMOVED_).

2026-10-18 — notify.py — httpx.post() n'appelait pas raise_for_status() ; une erreur HTTP ntfy était avalée silencieusement → statut done mais notif jamais reçue. Corrigé : response.raise_for_status() ajouté dans _post().

2026-10-19 — Retry automatique des notifs manquées : flag notified: true écrit dans le job JSON après envoi ntfy réussi. Script check_notifs.py lancé toutes les 5 min par cron renvoie la notif sur tout job done sans ce flag.