Comment monter une vidéo avec Claude, Codex ou n’importe quel agent IA

Fournissez à l’agent une transcription avec le début et la fin exacts de chaque mot. Il choisit les phrases à garder, puis un petit script coupe la vidéo à ces endroits avec ffmpeg. L’API KolWrite génère cette transcription pour l’audio et la vidéo dans 100 langues, et ces mêmes mots servent ensuite à sous-titrer le montage final.

Obtenir une clé API Démarrage rapide Langues prises en charge

Dernière mise à jour: · Markdown

Ce que l’agent sait faire, et ce qui lui échappe

La transcription ne contient que la parole. L’agent monte donc à partir de ce qui est dit : il supprime les faux départs, les tics de langage, les prises répétées, les digressions et les longs silences, ou isole les phrases qui forment un court extrait. En revanche, il ne voit pas l’image. Le choix des plans à montrer vous revient, à moins de lui envoyer des images à examiner.

Un agent qui a accès au terminal (Claude Code, Codex CLI, un agent Cursor) peut appeler l’API et lancer ffmpeg tout seul. Une simple interface de chat ne peut pas exécuter le code, mais elle peut vous écrire les scripts.

Le prompt à donner à votre agent

Ce prompt demande à l’agent d’indiquer des numéros de passage au lieu de saisir des horodatages, pour éviter qu’il n’invente des repères temporels. Il pointe aussi vers les instructions de l’API lisibles par machine.

Edit talk.mp4 down to the parts worth keeping, using the KolWrite API for the transcript.
Read https://console.kolwrite.com/agent-instructions.md first. The key is in KOLWRITE_API_KEY.
1. Extract the audio with ffmpeg and transcribe it with model "oracle", output_format "json", word_timestamps true.
2. Print the segments as numbered lines: S<number> [start-end] text.
3. Choose the segments to keep (remove false starts, filler, repeated takes and tangents) and write
   their numbers to keep.json as {"keep": [0, 2, 4]}. Do not invent timestamps; use segment numbers only.
4. Cut the video from those segments' word times with ffmpeg and write edited.mp4.
5. Make edited.srt for the edited video and attach it as a subtitle track.
Show me the list of removed segments before you cut anything.

Pour que l’agent connaisse l’API à chaque session, ajoutez la règle du guide de l’outil dans votre fichier AGENTS.md ou CLAUDE.md.

Étape 1 : extraire l’audio et le transcrire

Un envoi signé est limité à 100 MiB. Sur une vidéo longue, commencez donc par extraire l’audio : une heure en mono à 48 kbit/s pèse environ 21 Mo.

ffmpeg -i talk.mp4 -vn -ac 1 -ar 16000 -b:a 48k talk.mp3

Cette fonction envoie un fichier local, lance la transcription et attend qu’elle se termine :

kolwrite.py

# kolwrite.py — three small functions around the KolWrite REST API
import os, time, requests

API = "https://app.kolwrite.com"
HEADERS = {"x-api-key": os.environ["KOLWRITE_API_KEY"]}


def upload(path):
    """Upload a local file (up to 100 MiB) and return a URL the API can read."""
    name = os.path.basename(path)
    form = requests.post(f"{API}/upload", headers=HEADERS, json={"filename": name}, timeout=30)
    form.raise_for_status()
    form = form.json()
    with open(path, "rb") as media:
        # No x-api-key on this request: it goes to the storage address.
        sent = requests.post(form["uploadUrl"], data=form["uploadFields"],
                             files={"file": (name, media)}, timeout=600)
    sent.raise_for_status()
    return form["fileUrl"]


def transcribe(audio_url, timeout_s=3600, **options):
    """Start a job, wait for it, return the result: a dict for json, a string for srt and txt."""
    body = {"audio_url": audio_url, "model": "oracle", **options}
    job = requests.post(f"{API}/transcribe", headers=HEADERS, json=body, timeout=30)
    job.raise_for_status()
    job_id = job.json()["jobId"]
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        time.sleep(5)
        reply = requests.get(f"{API}/transcribe/{job_id}", headers=HEADERS, timeout=30)
        reply.raise_for_status()
        reply = reply.json()
        if reply["status"] == "COMPLETED":
            return reply["result"]
        if reply["status"] == "FAILED":
            raise RuntimeError(reply.get("error", "transcription failed"))
    raise TimeoutError(f"job {job_id} did not finish in {timeout_s} seconds")

Le script edit.py ci-dessous demande du JSON avec l’horodatage par mot (ce sont déjà les options par défaut, on les précise juste pour la clarté). En retour, vous obtenez segments[], où chaque élément contient start, end, text, speaker (en option) et words[] :

Voici un extrait de la réponse :

{
  "jobId": "34c63d45-4cb5-473b-bc60-25c175e69c57",
  "status": "COMPLETED",
  "result": {
    "language": "en",
    "segments": [
      {
        "start": 1.0,
        "end": 3.75,
        "text": "Hello everyone and welcome to the show.",
        "speaker": "Speaker 1",
        "words": [
          { "start": 1.0, "end": 1.35, "word": "Hello" },
          { "start": 1.4, "end": 1.75, "word": "everyone" }
        ]
      }
    ]
  }
}

Étape 2 : laisser l’agent choisir les passages à garder

Affichez la transcription sous forme de lignes numérotées. C’est ce texte que lira l’agent, et cela consomme beaucoup moins de jetons que le JSON brut avec le détail de chaque mot.

view.py

# view.py — print the transcript the way an agent can read it
import json, sys

result = json.load(open(sys.argv[1]))
for number, segment in enumerate(result["segments"]):
    who = f'{segment["speaker"]}: ' if segment.get("speaker") else ""
    print(f'S{number} [{segment["start"]:.1f}-{segment["end"]:.1f}] {who}{segment["text"]}')

L’agent répond simplement par la liste des numéros à conserver :

keep.json

{"keep": [0, 2, 4]}

Demandez-lui de lister ce qu’il écarte et pourquoi avant de lancer la coupe. Refaire une coupe ne coûte pas grand-chose, mais on relit une liste bien plus vite qu’on ne revisionne toute une vidéo.

Étape 3 : couper avec ffmpeg à partir des temps des mots

Pour chaque numéro de passage, le script récupère le début du premier mot et la fin du dernier. Il ajoute quelques centièmes de seconde de marge, regroupe les passages qui se suivent et réencode le tout en une passe pour couper à l’image près. Il recalcule au passage la position des mots sur la nouvelle vidéo pour générer les sous-titres.

cut.py

# cut.py — ranges from segment numbers, one ffmpeg pass, word times for the new timeline
import subprocess

PAD = 0.08  # seconds of room kept around every range


def ranges_from_ids(segments, keep_ids, pad=PAD):
    """Turn the segment numbers the agent chose into [start, end] ranges in seconds."""
    ranges = []
    for i in sorted(set(keep_ids)):
        segment = segments[i]
        words = segment.get("words") or []
        start = words[0]["start"] if words else segment["start"]
        end = words[-1]["end"] if words else segment["end"]
        start, end = max(0.0, start - pad), end + pad
        if ranges and start <= ranges[-1][1]:
            ranges[-1][1] = max(ranges[-1][1], end)
        else:
            ranges.append([start, end])
    return ranges


def cut(video, ranges, out):
    """Re-encode the kept ranges into one file (frame accurate)."""
    parts = []
    for i, (start, end) in enumerate(ranges):
        parts.append(f"[0:v]trim=start={start:.3f}:end={end:.3f},setpts=PTS-STARTPTS[v{i}]")
        parts.append(f"[0:a]atrim=start={start:.3f}:end={end:.3f},asetpts=PTS-STARTPTS[a{i}]")
    joined = "".join(f"[v{i}][a{i}]" for i in range(len(ranges)))
    parts.append(f"{joined}concat=n={len(ranges)}:v=1:a=1[v][a]")
    subprocess.run(["ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-i", video,
                    "-filter_complex", ";".join(parts), "-map", "[v]", "-map", "[a]", out], check=True)


def remap(words, ranges):
    """Move word times onto the edited timeline, so subtitles match the cut video."""
    moved, offset = [], 0.0
    for start, end in ranges:
        for word in words:
            if word["start"] >= start and word["end"] <= end:
                moved.append({**word, "start": word["start"] - start + offset,
                              "end": word["end"] - start + offset})
        offset += end - start
    return moved

edit.py

# edit.py — the whole pipeline: transcript in, edited video and subtitles out
import json
from kolwrite import upload, transcribe
from cut import ranges_from_ids, cut, remap
from srt_tools import cues_from_words, write_srt

result = transcribe(upload("talk.mp3"), output_format="json", word_timestamps=True)
with open("talk.json", "w", encoding="utf-8") as out:
    json.dump(result, out, ensure_ascii=False)
# ... the agent reads talk.json (python view.py talk.json) and writes keep.json ...

keep = json.load(open("keep.json"))["keep"]
ranges = ranges_from_ids(result["segments"], keep)
cut("talk.mp4", ranges, "edited.mp4")

words = [w for segment in result["segments"] for w in segment.get("words", [])]
write_srt(cues_from_words(remap(words, ranges)), "edited.srt")

Copier les flux sans réencoder (-c copy) va plus vite, mais ffmpeg ne peut alors couper que sur les images clés, parfois à quelques secondes de l’endroit voulu. Si la coupe doit être nette, il faut réencoder.

Étape 4 : ajouter les sous-titres à la vidéo montée

Le script edit.py a déjà généré le fichier edited.srt avec les nouveaux horaires des mots. Le découpage des lignes est détaillé dans le guide SRT.

srt_tools.py

# srt_tools.py — neat subtitle cues from word timings
def srt_time(seconds):
    ms = round(seconds * 1000)
    h, ms = divmod(ms, 3_600_000)
    m, ms = divmod(ms, 60_000)
    s, ms = divmod(ms, 1000)
    return f"{h:02}:{m:02}:{s:02},{ms:03}"


def cues_from_words(words, max_chars=42, max_seconds=6.0, max_gap=0.8):
    """Group timed words into subtitle cues: (start, end, text)."""
    cues, line = [], []
    for word in words:
        text = word["word"].strip()
        if not text:
            continue
        if line:
            length = sum(len(w["word"]) + 1 for w in line) + len(text)
            if (length > max_chars
                    or word["end"] - line[0]["start"] > max_seconds
                    or word["start"] - line[-1]["end"] > max_gap):
                cues.append(line)
                line = []
        line.append({"start": word["start"], "end": word["end"], "word": text})
    if line:
        cues.append(line)
    return [(c[0]["start"], c[-1]["end"], " ".join(w["word"] for w in c)) for c in cues]


def cues_from_result(result):
    """Cues for a whole JSON result; a segment without word times becomes one cue."""
    cues = []
    for segment in result["segments"]:
        if segment.get("words"):
            cues += cues_from_words(segment["words"])
        elif segment["text"].strip():
            cues.append((segment["start"], segment["end"], segment["text"].strip()))
    return cues


def write_srt(cues, path):
    with open(path, "w", encoding="utf-8") as out:
        for number, (start, end, text) in enumerate(cues, 1):
            out.write(f"{number}\n{srt_time(start)} --> {srt_time(end)}\n{text}\n\n")

Vous pouvez ensuite graver les sous-titres dans l’image, ou les intégrer comme une piste activable dans le lecteur :

ffmpeg -i edited.mp4 -vf "subtitles=edited.srt" -c:a copy burned.mp4
ffmpeg -i edited.mp4 -i edited.srt -c copy -c:s mov_text -metadata:s:s:0 language=eng soft.mp4

Autres idées de montage avec l’horodatage par mot

  • Raccourcir les blancs : gardez les moments où ça parle et coupez les silences qui dépassent une certaine durée (voir le guide de l’horodatage par mot).
  • Isoler un extrait : cherchez une phrase précise dans la liste des mots et coupez juste ce passage.
  • Nettoyer les hésitations : laissez l’agent repérer les passages qui ne servent à rien, ou filtrez-les directement dans la liste des mots.
  • Chapitrage et moments forts : l’agent relève les numéros des passages clés et le script en déduit les codes temporels (guide podcasts et YouTube).
  • Garder une seule voix : activez include_diarization et ne conservez que les interventions de la personne qui vous intéresse.

Combien ça coûte

TâcheModèleCoût
Extrait de 10 minutes, transcrit une seule foisoracle0,24 US$
Une heure de vidéo, son proprebase0,48 US$
Une heure de vidéo, qualité productionoracle1,44 US$
Une heure, son difficileoracle_max3,60 US$

La facturation se fait à la seconde, sans minimum. Si un passage pose problème, vous pouvez découper juste cet extrait audio et le relancer avec un modèle plus poussé : vous ne paierez que ces quelques secondes. Grille complète : /llms/pricing.md.

Limites à connaître

  • L’horodatage est assez précis pour couper entre deux mots, mais il peut décaler légèrement sur l’attaque ou la fin du mot lui-même. La petite marge ajoutée par cut.py sert à amortir ce décalage. Réécoutez toujours les raccords avant de publier.
  • Toute transcription automatique comporte des erreurs. Relisez les noms propres et les chiffres avant publication. KolWrite n’affiche pas de taux de précision par langue pour cette API.
  • Gardez bien votre fichier vidéo d’origine : c’est à partir de lui que le script réencode les extraits, et vous en aurez besoin si vous voulez retoucher le montage plus tard.
  • Si vous gardez des centaines de petits passages, la commande de filtre ffmpeg va devenir énorme. Au-delà de quelques centaines de coupes, procédez par lots.

Questions et réponses

Claude peut-il monter une vidéo ?

Oui, à condition qu’il puisse lancer des commandes sur votre machine. Il appelle alors l’API KolWrite pour récupérer la transcription horodatée au mot près, choisit les phrases à garder et lance ffmpeg pour découper la vidéo. C’est possible avec Claude Code, Codex CLI ou Cursor. Dans une simple fenêtre de chat, il pourra seulement vous écrire le code.

Comment couper une vidéo à partir de sa transcription ?

Lancez une transcription avec l’horodatage par mot et sélectionnez les passages à garder. Pour chacun, prenez le début du premier mot et la fin du dernier, puis découpez et assemblez ces plages avec les filtres trim et concat de ffmpeg, comme le montre cut.py.

Faut-il installer un plugin ou un SDK ?

Non. L’API est en HTTPS et JSON ; l’agent l’appelle avec curl ou Python. Il n’existe à ce jour ni paquet SDK ni serveur MCP publié.

Est-ce que ça marche en hébreu, en arabe ou dans d’autres langues ?

Pour la transcription, oui : l’API gère 100 langues, en détection automatique ou en précisant le paramètre language. L’horodatage par mot dépend des modèles présentés dans le guide de l’horodatage par mot. Faites d’abord un essai sur un extrait de vos fichiers et jetez un œil à la page des langues.

Comment éviter que l’agent n’invente des horodatages ?

Ne lui demandez jamais de manipuler des heures ou des secondes. Envoyez-lui une liste de passages numérotés, demandez-lui uniquement des numéros en retour, et laissez votre script retrouver les vrais horaires à partir des mots.