Skip to main content

dubfit

Fit dubbed narration to a video scene by scene — capping the speed change and carrying the residual drift forward, so timing never slides and no scene is ever squeezed into sounding rushed.

pip install dubfit        # ffmpeg required on PATH
dubfit plan  es.json en.json      # show the fit, change nothing
dubfit fit   es.json en.json narration_es.wav out.wav --aac out.m4a

The problem

A translation is never the same length as the original. The usual approach is to stretch the whole track by one constant ratio:

60 seconds of narration into a 45 second scene  ->  atempo=1.333

That has two failures. Every scene gets the same stretch even when only one needs it, and the error accumulates — a constant ratio applied across a 45-minute film can end seconds out of sync, and the stretch is audible throughout.

What dubfit does instead

It works scene by scene, and per scene:

available = target_scene_end - output_so_far
speed     = clamp(source_scene_len / available, 0.90, 1.12)
out_len   = source_scene_len / speed

The key part is output_so_far. The residual from each scene is carried into the next scene's available, so a scene that had to overrun is corrected by its neighbours instead of pushing everything after it late. No single scene is ever stretched past the window, and the final scene absorbs whatever remains so the file lands exactly on the target length.

Measured on real work

Three history documentaries, Spanish narration fitted to English picture. The scratch script this was extracted from produced these, and dubfit reproduces the same drift and the same worst scene:

Film Scenes Length Max drift Worst scene
Al-Andalus 53 43:52 -1.21s s19_treaty
Sefarad 44 32:44 -0.82s s_crescas
Crusades 59 45:50 +1.46s s_after

Under a second and a half of drift across 45 minutes, with every scene inside the speed window. That is the whole point.

When a scene cannot be fitted

If one scene is far too long — a paragraph in the translation with no counterpart in the original — it cannot fit inside the window. dubfit's contract in that case is:

  1. Clamp, do not rush. The scene gets max_speed, not the 2.0x it "needed"
  2. Absorb in the remainder, do not compound. The overrun is carried forward
  3. Say so. max_drift and worst_scene name the scene and the size of the problem
p = plan_fit(source, target, max_speed=1.12)
if abs(p.max_drift) > 3.0:
    print(f"cannot fit {p.worst_scene} ({p.max_drift:+.2f}s) -- consider re-recording it")

Silent drift is the failure mode this avoids. A tool that hides a 7-second overrun is worse than one that refuses.

Timings

Two files, one per cut. Ids must match exactly, in order — if they don't, the files describe different cuts and plan_fit raises rather than producing plausible nonsense.

[{"id": "s01", "start": 0.0, "end": 12.5}, {"id": "s02", "start": 12.5, "end": 30.0}]

Also accepted: {"scenes": [...]}, {"s01": [0, 12.5], ...}, a dur key instead of end, and SRT (one span per cue, so you can work from subtitles without machine-readable scene data).

API

from dubfit import load_timings, plan_fit
from dubfit.render import render, to_aac

source = load_timings("narration_es.json")   # the dub
target = load_timings("picture_en.json")     # the video being fitted to

plan = plan_fit(source, target, min_speed=0.90, max_speed=1.12)
print(plan.summary())

render("narration_es.wav", plan, "fitted.wav")
to_aac("fitted.wav", "upload.m4a")

plan_fit is pure logic with no ffmpeg — it returns Slices with exact output sample counts, so you can inspect, test and assert on the fit without rendering anything. The tests run without ffmpeg for that reason, apart from three rendering tests.

Two traps baked into the code

-ss/-to must go BEFORE -i. Placed after, they are output options and trim the wrong timeline — which produced a 9-second file from a 27-second plan before it was caught by a test. For audio there are no keyframes, so input seeking is sample-accurate.

AAC overshoots peaks. A source peaking at -4 dBFS can leave the encoder at -0.3 dBFS and clip on some decoders. to_aac() applies -1.5 dB of headroom by default.

Status

Alpha. The planner is exercised by 18 tests and validated against three real films. The renderer is exercised by 3 tests with synthetic tones — it has not been run on a large real project since extraction, though the scratch version it came from produced the figures above.

Roadmap

  • Spread the overrun instead of dumping it on the final scene. Today an unfittable scene is absorbed at the end; distributing it across the remaining scenes would sound better. The clamp-and-report contract should not change.
  • Subtitle-aware splitting, so a scene can be broken at a sentence boundary rather than only at scene boundaries.
  • --dry-run audio previews — render one badly-fitted scene so you can hear the problem rather than reading a number.

Contributions welcome. Issues labelled good first issue are scoped to be self-contained.

Licence

MIT.

Metadata

Release files for dubfit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dubfit 0.1.0
File Size Uploaded Provenance
dubfit-0.1.0.tar.gz 14.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for dubfit 0.1.0
File Interpreter ABI Platform
dubfit-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 26.6 kB

Release files / dubfit-0.1.0.tar.gz

Download URL dubfit-0.1.0.tar.gz
Size 14.9 kB
Tags Source
SHA-256 checksum
How to use checksums
6b1e96b4d42b1115ed46a30db021f9566c23c5a7e290216ad300e9159a16cb4f
BLAKE2b-256 checksum
How to use checksums
26c9f5da9b30a30c9c4bf2eca8904412b649c4730bc91bf1c08ef34ba159e4ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / dubfit-0.1.0-py3-none-any.whl

Download URL dubfit-0.1.0-py3-none-any.whl
Size 11.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
43069fc5491fed334b72b2ce182765a1d12b12df4d227ffcaae25dcb3b7fd27d
BLAKE2b-256 checksum
How to use checksums
f15c9a4de0a71d78efe3db4f40cd99d039b1e09c056415c971d294f78400d745
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Security for dubfit 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for dubfit-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page