איך עורכים וידאו עם Claude,‏ Codex או כל סוכן AI

מעבירים לסוכן תמליל עם זמן ההתחלה והסיום המדויק של כל מילה. הסוכן בוחר אילו משפטים להשאיר, וסקריפט קצר חותך את הווידאו ב־ffmpeg לפי הזמנים האלה. ה־API של KolWrite מחזיר תמליל כזה לקובצי אודיו ווידאו ב־100 שפות, ומאותן מילים בונים גם את הכתוביות לסרטון הערוך.

קבלת מפתח API התחלה מהירה שפות נתמכות

עודכן לאחרונה: · Markdown

מה הסוכן יכול לעשות ומה לא

התמליל כולל רק את מה שנאמר, ולכן הסוכן עורך לפי הטקסט: הוא מוריד התחלות מפוספסות, מילות מילוי, טייקים כפולים, חפירות שלא קשורות לנושא ושתיקות ארוכות, או שולף משפטים לקליפ קצר. את הווידאו עצמו הוא לא רואה. בחירות ויזואליות, כמו איזה שוט להראות, נשארות אצלכם, אלא אם תעבירו לסוכן גם פריימים מתוך הסרטון.

סוכן שיכול להריץ פקודות טרמינל (כמו Claude Code,‏ Codex CLI או הסוכן של Cursor) פונה ל־API ומריץ את ffmpeg לבד. צ׳אט רגיל שלא מריץ קוד לא יעשה את זה בעצמו, אבל הוא יכול לכתוב בשבילכם את הסקריפטים.

הפרומפט להדבקה בסוכן

הפרומפט מכוון את הסוכן לעבוד עם מספרי מקטעים במקום להקליד חותמות זמן בעצמו, וככה מונעים ממנו להמציא זמנים. יש בו גם הפניה לתיעוד ה־API שמותאם לקריאת מכונה.

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.

כדי שה־API יהיה זמין לסוכן בכל שיחה, הוסיפו את ההגדרה ממדריך הכלי לקובץ AGENTS.md או ל־CLAUDE.md.

שלב 1: מחלצים את האודיו ומתמללים

אפשר להעלות קבצים עד 100 MiB בהעלאה חתומה, אז בסרטונים ארוכים כדאי לחלץ קודם רק את האודיו. שעה של אודיו מונו ב־48 קילובית לשנייה שוקלת בערך 21 מגה־בייט.

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

הסקריפט הקצר הזה מעלה קובץ מקומי, פותח משימת תמלול ומחכה לתוצאה:

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")

הסקריפט edit.py שלמטה מבקש פלט JSON עם חותמות זמן למילים (שניהם מוגדרים כברירת מחדל, וכתובים כאן במפורש כדי שיהיה ברור). התשובה חוזרת במערך segments[], כשבכל מקטע יש start,‏ end,‏ text, לפעמים speaker, ומערך words[]:

התשובה שחוזרת (בקיצור):

{
  "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" }
        ]
      }
    ]
  }
}

שלב 2: הסוכן בוחר מה לשמור

מדפיסים את התמליל בתור שורות ממוספרות. זה הטקסט שהסוכן קורא, והוא צורך הרבה פחות טוקנים מקובץ ה־JSON המלא עם כל המילים.

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"]}')

הסוכן מחזיר רק את מספרי המקטעים:

keep.json

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

לפני שחותכים, בקשו ממנו לפרט מה הוא מוריד ולמה. קל להריץ חיתוך מחדש אם יש טעות, אבל הרבה יותר מהיר לעבור על רשימה מאשר לצפות בכל הסרטון הערוך.

שלב 3: חותכים ב־ffmpeg לפי זמני המילים

הסקריפט מתרגם את מספרי המקטעים לזמני התחלה וסיום לפי המילה הראשונה והאחרונה בכל מקטע, מוסיף מרווח של כמה מאיות שנייה, מאחד מקטעים צמודים ומקודד מחדש במעבר אחד לקבלת חיתוך מדויק ברמת הפריים. על הדרך הוא גם מחשב מחדש את זמני המילים לפי ציר הזמן של הסרטון הערוך, כדי שיתאימו לכתוביות.

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")

העתקת ערוצים בלי קידוד מחדש (-c copy) רצה מהר יותר, אבל חותכת רק בנקודות keyframe, ככה שהחיתוך עלול לסטות בכמה שניות. כשחשוב לחתוך בנקודה המדויקת, קודדו מחדש.

שלב 4: כתוביות לסרטון הערוך

הסקריפט edit.py כבר יצר את הקובץ edited.srt מהמילים אחרי חישוב הזמנים מחדש. במדריך ה־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")

אפשר לצרוב את הכתוביות לתוך הווידאו, או לצרף אותן כערוץ נפרד שאפשר להדליק ולכבות בנגן:

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

עריכות נוספות שאפשר לעשות עם זמני המילים

  • קיצור שתיקות: משאירים רצפים של דיבור וחותכים החוצה הפסקות שעוברות אורך מסוים (ראו במדריך חותמות הזמן למילים).
  • שליפת קליפ: מחפשים ביטוי מסוים בתוך המילים וחותכים את טווח הזמנים שלו.
  • הסרת מילות מילוי: נותנים לסוכן לסמן מקטעים שמכילים רק מילות מילוי, או מחפשים אותן ישירות ברשימת המילים.
  • פרקים וקטעים בולטים: הסוכן בוחר את מספרי המקטעים והסקריפט מתרגם אותם לזמנים (מדריך פודקאסטים ויוטיוב).
  • בידוד דובר מסוים: מוסיפים include_diarization לבקשה ושומרים רק את המקטעים של אותו דובר.

כמה זה עולה

משימהמודלעלות
קליפ של 10 דקות, תמלול אחדoracle0.24 דולר
שעת וידאו, אודיו נקיbase0.48 דולר
שעת וידאו, איכות לפרודקשןoracle1.44 דולר
שעה, אודיו מאתגרoracle_max3.60 דולר

החיוב הוא לפי שנייה ובלי מינימום. אם יש קטע בעייתי אחד שרוצים להריץ שוב במודל חזק יותר, אפשר לחתוך רק את האודיו שלו ולשלם רק על הזמן של אותו קטע. פירוט התעריפים נמצא ב־/llms/pricing.md.

מגבלות שכדאי להכיר

  • זמני המילים מדויקים מספיק כדי לחתוך בין מילים, אבל בקצוות של המילה עצמה יכולה להיות סטייה קלה. המרווח שמוגדר ב־cut.py מפצה על זה, ובכל מקרה כדאי להאזין לנקודות החיתוך לפני שמפרסמים.
  • בתמלול אוטומטי יש טעויות. עברו על שמות ומספרים בכל טקסט שיוצא לפרסום. KolWrite לא מפרסמת אחוזי דיוק לכל שפה עבור ה־API הזה.
  • שמרו את קובץ המקור. החיתוך מבצע קידוד מחדש מתוך המקור, ואולי תרצו לשנות את החיתוך בהמשך.
  • רשימה ארוכה מאוד של טווחי חיתוך מייצרת פילטר ענק ב־ffmpeg. אם יש לכם יותר מכמה מאות טווחים, עדיף לחתוך בכמה נגלות.

שאלות ותשובות

האם Claude יכול לערוך סרטון?

כן, כש־Claude רץ בסביבה שמאפשרת להריץ פקודות: הוא שולח בקשה ל־KolWrite API כדי לקבל תמליל עם חותמות זמן למילים, בוחר אילו משפטים להשאיר ומריץ ffmpeg כדי לחתוך את הווידאו. כלים כמו Claude Code,‏ Codex CLI והסוכן של Cursor יודעים לעשות את זה. חלון צ׳אט רגיל יכול רק לכתוב לכם את הסקריפטים.

איך חותכים סרטון לפי התמליל שלו?

מתמללים את הסרטון עם חותמות זמן למילים, בוחרים אילו מקטעים להשאיר, לוקחים מכל מקטע את זמן ההתחלה של המילה הראשונה ואת זמן הסיום של המילה האחרונה, וחותכים את הטווחים האלה עם הפילטרים trim ו־concat של ffmpeg, כמו שמוצג ב־cut.py למעלה.

צריך תוסף או SDK?

לא. ה־API הוא HTTPS ו־JSON; הסוכן קורא לו עם curl או Python. נכון לזמן הכתיבה אין חבילת SDK או שרת MCP שפורסמו.

זה עובד בעברית, בערבית ובשפות אחרות?

ברמת התמליל כן: ה־API תומך ב־100 שפות, גם בזיהוי אוטומטי וגם בהגדרה מראש עם הפרמטר language. זמני המילים מופקים על ידי המודלים שמפורטים במדריך חותמות הזמן למילים. מומלץ לבדוק קודם דגימה מתוך ההקלטות שלכם, ולהציץ בדף השפות.

איך מונעים מהסוכן להמציא חותמות זמן?

פשוט לא מבקשים ממנו זמנים. מעבירים לו שורות ממוספרות, מקבלים ממנו בחזרה רק את מספרי השורות, ואת הזמנים עצמם שולפים בקוד מתוך זמני המילים.