Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.2.0 - 2026-10-27

### Added

- Assembly receipt, schema `keycut-assembly-receipt/1`: a record of what an
extraction actually cut. For each merged span it gives the requested ranges
absorbed, the keyframe-snapped source start, copy versus re-encode and the
reason, and where the span landed in the output (parts are probed with
ffprobe; a failed probe falls back to the planned length and says so).
`AssemblyReceipt` and `ReceiptSpan` offer `to_dict()`, `from_dict()` and
`write_json()`.
- `extract_and_concat_ranges_with_receipt` and
`extract_and_concat_segments_with_receipt`, returning `(path, receipt)`, or
`(None, None)` on failure. `extract_and_concat_ranges` and
`extract_and_concat_segments` are unchanged in signature and behaviour.

## 0.1.2 - 2026-10-27

### Fixed
Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ Cutting:
| `extract_and_concat_ranges(master, ranges, output_path, work_dir, **opts)` | The main entry point. Returns the output path, or `None` on failure. |
| `extract_and_concat_segments(master, segments, boundaries, output_path, work_dir, **opts)` | Same, for ranges expressed against the master's source clips. |

| `extract_and_concat_ranges_with_receipt(...)` | Same arguments; returns `(path, receipt)`, or `(None, None)` on failure. See [The assembly receipt](#the-assembly-receipt). |
| `extract_and_concat_segments_with_receipt(...)` | The segment form of the above. |

Options: `exclude_mask`, `max_gap_sec`, `reencode_args`, `allow_reencode`,
`keyframes`, `ffmpeg_bin`, `ffprobe_bin`, `keep_scratch`, `faststart`.

Expand All @@ -360,6 +363,63 @@ Optional companion, not part of the cutting path:
feeds two independent products, works out which output each derived clip came
from, by source footage rather than by dividing one count into another.

## The assembly receipt

`extract_and_concat_ranges` returns only a path. To learn what was actually cut
— where each cut start snapped, which requested ranges were merged, and whether
a span was copied or re-encoded — call the `_with_receipt` form:

```python
from keycut import extract_and_concat_ranges_with_receipt

path, receipt = extract_and_concat_ranges_with_receipt(
"master.mp4", [(3.0, 9.0), (15.0, 21.0)], "out.mp4", "scratch/"
)
if receipt is not None:
receipt.write_json("out.receipt.json")
```

The receipt is `None` exactly when the path is. `receipt.to_dict()` returns the
schema below; `AssemblyReceipt.from_dict()` reads it back.

```json
{
"schema": "keycut-assembly-receipt/1",
"provenance": "keycut",
"keycut_version": "0.2.0",
"master": {"path": "master.mp4", "size_bytes": 123456},
"time_base": "seconds",
"max_gap_sec": 2.0,
"requested": [[3.0, 9.0], [15.0, 21.0]],
"spans": [
{"index": 0, "requested_indices": [0],
"merged_start": 3.0, "merged_end": 9.0,
"source_start": 4.0, "source_end": 9.0,
"method": "copy", "reason": "stream_copy",
"output_start": 0.0, "output_end": 5.0,
"duration_source": "probed"}
],
"output": {"path": "out.mp4", "duration": 11.0}
}
```

All times are seconds, half-open `[start, end)`.

- `requested_indices`: the input ranges that were merged into this span.
- `merged_start` / `merged_end`: the span after merging, before any alignment.
- `source_start`: where the cut really begins in the master. For `copy` it is
the keyframe the start was snapped forward to; for `reencode` it equals
`merged_start`, because a re-encode seeks exactly. `source_end` is
`merged_end`.
- `method` / `reason`: `copy` with `stream_copy`; or `reencode` with
`keyframes_unknown` or `snap_back_over_excluded`.
- `output_start` / `output_end`: where the span lands in the output, contiguous
from 0 in output order. The length is the part's probed duration
(`duration_source: "probed"`); if ffprobe fails it is
`source_end - source_start` and `duration_source` is `"planned"`.
- `output.duration` is the probed duration of the final file, `null` if the
probe failed.

## Development

```bash
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "keycut"
version = "0.1.2"
version = "0.2.0"
description = "Lossless multi-range video extraction with ffmpeg stream copy, without the GOP-replay artefact."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
9 changes: 8 additions & 1 deletion src/keycut/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
concat_demuxer,
default_reencode_args,
extract_and_concat_ranges,
extract_and_concat_ranges_with_receipt,
extract_and_concat_segments,
extract_and_concat_segments_with_receipt,
probe_video_params,
reencode_range,
stream_copy_range,
Expand All @@ -51,6 +53,7 @@
merge_ranges,
span_has_excluded_second,
)
from .receipt import AssemblyReceipt, ReceiptSpan
from .segments import (
Segment,
boundaries_from_durations,
Expand All @@ -59,16 +62,18 @@
segments_to_master_ranges,
)

__version__ = "0.1.2"
__version__ = "0.2.0"

__all__ = [
"AssemblyReceipt",
"DEFAULT_CRF",
"DEFAULT_GOP_DURATION_SEC",
"FAILED_PROBE_RETRY_SEC",
"MIN_IO_BYTES_PER_SEC",
"REENCODE_SEC_PER_SEC",
"MaskLike",
"Range",
"ReceiptSpan",
"Segment",
"__version__",
"as_mask_lookup",
Expand All @@ -80,7 +85,9 @@
"default_reencode_args",
"estimate_gop_duration",
"extract_and_concat_ranges",
"extract_and_concat_ranges_with_receipt",
"extract_and_concat_segments",
"extract_and_concat_segments_with_receipt",
"io_timeout",
"keyframe_at_or_before",
"merge_ranges",
Expand Down
Loading
Loading