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) :
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.