# Skill: full/dub-video — "דיבוב סרטון — מקישור יוטיוב או מקובץ" (Complete video)

The same video in another language: bring a YouTube link — your own channel, a Creative Commons video, or one you have permission for; the licence you declare is kept with the job — or upload the file. The speech is transcribed, Gemini watches the speaker and writes the dub the way a native speaker would say it, a stock ElevenLabs voice reads it (the best of three takes a sentence), and every part is cut and fitted to the speaker's phrase, so the new voice starts and stops with his mouth. Captions of the dub, one render, and — if you want — published to your YouTube channel through your connected accounts. A narrator voice, never a clone.

**Includes:** a YouTube link or a file — the licence kept with the job · the whole video or a part, its own frame · the dub written by Gemini watching the speaker · a stock voice of the new language, best of 3 takes · every phrase fitted to the speaker's mouth · captions of the dub · optional: published to YouTube
**Built on:** the dub lines in `voiceover` (one per phrase of the speaker) + captions `classic` ("קלאסי", `/skills/captions-classic.md`) timed by the dub — no hook, the video keeps its own opening
**Price:** 240–290 credits per finished minute · 30 s ≈ 125–150 credits
**Uses:** `/v1/youtube/videos/{id}/file` · `/v1/speech/transcripts` · `/v1/ai/text` · `/v1/ai/voice` · `/v1/studio/convert` · `/v1/studio/edit` · `/v1/social/publish`

Base URL: `https://apihub.co.il` · Auth: `Authorization: Bearer <API key>` · Docs: `/docs`
Send the same `X-Tag: <job name>` header on every call of one video; `GET /v1/usage?tag=<job name>` then
returns exactly what it cost. Times in request bodies are **seconds on the edited (output) timeline**.
Unknown fields are rejected with the name of the right field — read the error instead of guessing.
**Files are kept 24 hours** (renders, uploads, generated clips/images/music) — download the finished video.

**Models.** AssemblyAI transcribes, Gemini 3.1 Pro (`quality: "best"`) watches the speaker and writes the dub,
ElevenLabs v3 (`quality: "best"`) voices it — three takes a sentence; the fitting and the render are ours. Everything is
priced by the video's length: the voice (three takes) and the render are most of it.

---

## 1. The source, and the licence it is used under

**Rights first.** Dub only a video the customer owns or may use, and ask them which of the three it is — never choose it
for them. The voice is a stock voice of the target language, never a clone of the speaker.

**From a YouTube link** — read its facts (free), then take the file:

```jsonc
GET  /v1/youtube/videos/{id or link}      // title, channel, duration_seconds, license: "youtube" (standard) or "creative-commons"
POST /v1/youtube/videos/{id}/file { "kind": "video", "license": "own", "credit": "<only for creative-commons: the credit line>" }
// → output.url (the mp4 on our storage, 24 hours) + output.source { platform, video_id, url, title, channel, license, credit }
// ≈ 8 credits; a long video takes a few minutes — send x-wait-ms: 90000 and poll GET /v1/executions/{id}
```

- `license`: `own` = a channel the customer controls · `creative-commons` = the video is published under CC BY (its facts
  say `"license": "creative-commons"`) — the credit line goes on screen and into the description · `licensed` = written
  permission from the owner that covers a dubbed version. A standard-licence video of someone else is not dubbed.
- Taking a file is switched on per account (`403 youtube_files_not_enabled`): the customer asks through the feedback button
  in the dashboard and names the channel. Until then: download the video from YouTube Studio and upload it (below).

**From a file:**

```jsonc
POST /v1/studio/uploads { "filename": "talk.mp4", "content_type": "video/mp4", "size_bytes": <bytes>,
  "license": "own", "source_url": "<where it was published, if anywhere>" }            // → upload_url (PUT the file there) + url
```

**The job keeps the record.** Send the same `X-Tag: <job>` on every call; `GET /v1/usage?tag=<job>` then lists, next to
the bill, the job's `sources`: where the video came from and the licence declared for it — the answer if anyone asks.



## 2. Transcribe

```jsonc
POST /v1/speech/transcripts { "audio_url": "<source url>", "language_code": "he" }   // ≈ 1 credit a minute of talk
GET  /v1/speech/transcripts/{id}                                                   // text + words [{ text, start, end }] (ms)
```

The dub can only say what the transcript has: a second or more of talking with no words, or words squeezed to zero
length, is a lost phrase — transcribe again with `"speech_models": ["universal-2"]` (≈ 1 credit a minute) and take its
words for that stretch. **A part of the video only** (the example is 32 s of a 110-s talk): keep the words inside it and
render with `trim` (step 7).

## 3. His phrases (no call)

The dub is fitted to the speaker's **phrases** — the stretches he says without stopping — so the new voice starts when
his mouth starts and stops when he stops. From the words:

1. **Sentences** end at `.` `?` `!`; a sentence under 1.5 s joins the one before it.
2. **Phrases**: a sentence splits after every word ending in `,` `.` `?` `!` and wherever two words are 0.3 s or more
   apart; a phrase under 0.6 s joins the next one.
3. **The end of a phrase** = its last word's start + 0.08 s × its letters + 0.2 s, when that is earlier than the word's
   `end`. AssemblyAI stretches the word before a pause over the pause (measured on the example: "ספר," ran 0.78 s where he
   said it in about 0.5 s and paused for the rest) — without this every phrase would seem longer than it is.

The example — 32 s, 5 sentences, 12 phrases (the transcript's words, mishearings included — Gemini hears the right ones):
`[49.53, 52.29, "ואחרי חמש שנים הוא פשוט החליט לעשות משהו סופר נוראז."]`, `[52.50, 53.10, "ונדיר."]`,
`[53.65, 56.67, "מה שאנשים אחרים לא היו מסגרים בחיים לעשות,"]`, …

## 4. The dub, written by Gemini watching him (one call)

```jsonc
POST /v1/ai/text { "quality": "best", "json": true, "temperature": 0.8, "max_tokens": 8000,
  "media": [ { "url": "<source url>", "type": "video", "fps": 1, "start": 49.4, "end": 81.7 } ],   // start/end only for a part
  "system": "You are the best American dubbing writer. You WATCH and HEAR the attached video — an Israeli creator talking to
             camera in Hebrew — and write his English dub, the way an American creator would actually SAY it to camera:
             spoken, direct, energetic, natural — not a translation, not a narrator. The transcript may mishear a word:
             trust what you hear and see. For every sentence you get his phrases (the Hebrew he says in each, and how many
             seconds it lasts). Write ONE English part per phrase, in order, so the English starts and stops with his mouth.
             Each part must fit its phrase: 14–16 characters per second of the phrase (spaces included) — never more than
             17 a second. Keep names as they are. For every sentence also give delivery (one line: energy 1–10, where the
             pitch goes, the word to hit). Return { sentences: [ { i, parts: [\"…\"], delivery } ] } — exactly as many
             parts as the sentence has phrases.",
  "prompt": "[{\"i\": 0, \"phrases\": [{\"seconds\": 2.76, \"he\": \"ואחרי חמש שנים הוא פשוט החליט לעשות משהו סופר נוראז.\"}, {\"seconds\": 0.6, \"he\": \"ונדיר.\"}]}, …]" }
// ≈ 18 credits for 32 s, ~40 s. A sentence whose parts do not match its phrases: ask again.
```

**Into Hebrew** (or any language): the same call with the language swapped — *"the best Israeli dubbing writer … spoken
Israeli Hebrew … 12 Hebrew letters per second of the phrase"*. Only the characters-per-second budget changes with the
language (English 14–16, Hebrew ≈ 12 letters).

## 5. The voice: three takes a sentence, with word timings

One call per sentence — its parts joined by `"... "`, so the voice pauses where his phrases end; three takes, all in parallel:

```jsonc
POST /v1/ai/voice { "text": "After five years he did something super bold,... and rare.", "voice_id": "iP95p4xoKVk53GoZ742B",
  "quality": "best", "language_code": "en", "stability": 0, "timestamps": true }
// → output.url + output.words [{ text, start, end }] (seconds) · ≈ 3–6 credits a take · takes: stability 0, 0, 0.5
```

- **The voice:** a stock voice of the target language from `GET /v1/ai/voices` (the example: "Chris",
  `iP95p4xoKVk53GoZ742B`, a middle-aged American man for a middle-aged speaker) — never a clone of the speaker. Hebrew:
  "API Hub · Israeli speaker A" (`SxE1yZJthRl35h6VxNqR`), `"language_code": "he"`.
- Every voice line runs on the newest model, eleven_v4 (the default): the most alive delivery, and it speaks Hebrew. Never name an older model.
- **Hebrew lines are directed for you** (`prepare`, on by default): niqqud on every Hebrew word (English words stay in Latin
  letters), a delivery tag per sentence and a fitting stability, the words untouched. Send plain Hebrew with the delivery in `mood`; no tags, no niqqud, no `stability` of your own.

## 6. Pick the take, then cut and fit every part to its phrase

- **Split a take** into parts after every word that ends in `...`; a take that does not give exactly as many parts as his
  phrases is out.
- **A part's end** = its last word's start + 0.08 s × its letters + 0.2 s, when that is earlier than the word's `end` —
  the voice's timings stretch the word before `...` over the pause too (measured: "bold,…" ended at 2.72 s in the
  timings, 2.28 s in the audio).
- **The take:** score = the largest |ln(part length ÷ phrase length)| of its parts; the lowest score wins.
- **speed** = part length ÷ min(phrase length, room), where room = until his next phrase starts − 0.08 s. Between 0.92
  and 1.06 → 1; otherwise clamp to 0.9–1.18; only if the part would still run into the next phrase allow up to 1.25.

```jsonc
POST /v1/studio/convert { "video_url": "<take url>", "trim": { "start": <first word start − 0.03>, "end": <part end + 0.1> },
  "speed": 0.913, "output": { "format": "mp3" } }                          // ≈ 1 credit a part, all in parallel, pitch kept
```

Each part plays from **his phrase's start − 0.03 s**. Its words land at that moment + (word start − trim start) ÷ speed —
the captions' timing, free. A caption line ends at `,` `.` `?` `!`; a longer stretch is split into even lines of at
most 5 words.

## 7. One render

```jsonc
POST /v1/studio/edit { "video_url": "<source url>",
  "trim": { "start": 49.4, "end": 81.75 },                                  // only for a part of the video
  "audio": { "mute": true, "normalize": false },                            // his voice is gone; the dub carries the level
  "voiceover": [ { "url": "<part 0.0>", "start": 0.1, "volume": 1.3 }, { "url": "<part 0.1>", "start": 3.07, "volume": 1.3 }, … ],
  "captions": { "style": "classic", "captions": [ { "start": 49500, "end": 50551, "text": "After five years he",
      "words": [ { "text": "After", "start": 49500, "end": 49850 }, … ] }, … ] },
  "output": { "aspect_ratio": "9:16", "resolution": "1080", "fit": "crop", "quality": "high" } }   // 32 s ≈ 42 credits, ~2.3 min
```

- **Two clocks.** `voiceover[].start` is on the **finished video's** clock (phrase start − 0.03 − the trim's start);
  the caption times are on the **source's** clock in ms (the render moves them through `trim`). Without `trim` the two
  are the same.
- **30 lines a render** (dub lines and sound effects together). A longer video — more than 30 phrases, about 80 s of
  talk — is dubbed in chapters of up to 30 phrases, each rendered with its own `trim`, then joined with
  `POST /v1/studio/merge` (`clips`, up to 20).
- **Frame:** a vertical phone video → `"aspect_ratio": "9:16"`; a YouTube video in 16:9 → `"aspect_ratio": "original"`.
  Pick a caption style from `/skills/index.md`; `classic` reads well over any picture.
- **Creative Commons:** the credit line on screen for the first seconds — add
  `"texts": [ { "text": "<title> · <channel> · CC BY", "start": 0, "duration": 4, "effect": "fade", "preset": "small", "style": { "y": 6 } } ]`.
- **Level (measured):** with the original voice muted and `normalize` off, the lines at `"volume": 1.3` came out at
  −16.6 LUFS, true peak −1.6 dB (at 1.1: −18.0 LUFS).

## 8. (Optional) publish it to YouTube

Only when the customer says so, and only to their own channel. Connected accounts first:

```jsonc
GET  /v1/social/profiles                        // a "youtube" profile? if not: GET /v1/social/connect → send the customer connect.links.youtube
POST /v1/social/publish { "text": "<the description — Creative Commons: the credit line>", "media": [ "<render url>" ],
  "profiles": [ "youtube" ],
  "platform_options": { "youtube": { "title": "<up to 100 characters>", "privacy_status": "private",
      "made_for_kids": false, "contains_synthetic_media": true } } }     // per target profile · "private" until the customer checks it, then "public"
```

`contains_synthetic_media: true` marks the video as carrying a generated voice — the honest setting for a dub.

## What the example cost

The owner's own 110-second Hebrew talk → 32 s of it in English (5 sentences, 12 phrases, 15 takes with "Chris"):

| Step | Credits | Time |
|---|---|---|
| Upload | 1 | — |
| Transcribe the whole 110 s + read its words and sentences | 5 | 13 s |
| Gemini watches and writes the dub, fitted to his 12 phrases | 18 | 36 s |
| 15 takes (3 a sentence) with word timings | 60 | 32 s (parallel) |
| Cut + fit the 12 parts | 12 | 2 s (parallel) |
| Render: 1080 × 1920, captions | 42 | 2.3 min |
| **Total** | **138** | **~4 min** |

From YouTube add ≈ 8 credits for the file; publishing is billed per target profile. A minute of talk comes to about
240–290 credits — the voice (three takes) and the render are most of it.

Send `x-wait-ms: 110000` to wait for the render in the same call; if the response is still
`processing`, poll `GET /v1/executions/{id}` until `status` is `completed` and read `output.url`.

## Check the bill

`GET /v1/usage?tag=<job name>` — per call, per operation and the total, in credits. Quote that to the
customer, not the estimate above.

## Rules that keep captions readable on every platform

- **Safe zone.** Keep text inside the middle 80% of the frame: `side_margin` ≥ 6 handles the sides; `margin`
  ≥ 12 keeps the bottom clear of the platform UI (username, buttons, progress bar).
- **A line never leaves the frame** — a line too wide for the side margins is set smaller automatically. Do
  not shrink the size yourself, wrap instead (`max_chars_per_line`).
- **Hebrew has no capitals** — keep `uppercase: false`; it only breaks Latin words mixed into a sentence.
- **Fonts.** `GET /v1/studio/fonts` lists the bundled families and which scripts they draw. A font that
  cannot draw the caption's language is swapped and reported in `warnings`.
- The customer's transcript wins: to fix a word, `GET /v1/speech/transcripts/{id}/captions`, edit the text,
  and send the list back as `captions.captions` — timing is kept when the word count is unchanged.
