feat: enrich FFmpeg with evidence-driven media editing

Adds evidence-bounded video and podcast editing references, reusable templates, deterministic media workflow helpers, and tests. Closes #438.
This commit is contained in:
Magnus Hedemark
2026-09-01 21:20:51 -04:00
committed by GitHub
parent 02927adb63
commit 3b416dda7e
25 changed files with 1657 additions and 61 deletions
+71 -25
View File
@@ -1,34 +1,71 @@
# FFmpeg Expert Skill
A practical FFmpeg command-line skill for agents that need to understand media files, choose safe transformations, and verify the result instead of blindly copying recipes.
A practical FFmpeg skill for inspecting local media, planning reviewable video or podcast edits, rendering safely, and accepting outputs from evidence rather than command success alone.
## Why Install This Skill
FFmpeg is powerful but its failures often happen at boundaries: a command selects the wrong stream, an option applies to the wrong input, a filter is missing from the installed build, or timestamps make a seemingly correct cut unusable. This skill gives an agent a repeatable way to inspect the media first and explain what the command will actually do.
FFmpeg failures often happen at boundaries: the wrong stream is selected, a cut lands on an unexpected keyframe, a filter is absent from the installed build, timestamps drift, or a technically valid output fails in its real destination. Editorial work adds another risk: sparse frames, silence intervals, and imperfect transcripts can look more conclusive than they are.
After installation, an agent can reason about remuxing versus transcoding, construct explicit filtergraphs, diagnose timing and concatenation problems, write safer batch operations, and validate outputs against the intended player, editor, receiver, or archive. The guidance is grounded in official FFmpeg manuals, with version and build caveats called out clearly.
This skill provides a repeatable intake-to-acceptance workflow. It separates technical measurements from editorial judgment, preserves originals, makes cuts reviewable in an edit decision list, and records what was actually checked.
## What You Get
### Core guidance
| Path | Purpose |
|---|---|
| `SKILL.md` | Trigger boundaries and the core inspect-decide-run-verify workflow |
| `SKILL.md` | Trigger boundaries, capability routing, evidence classes, and the core workflow |
| `references/core-model-and-command-anatomy.md` | Containers, streams, codecs, mapping, option scope, and timestamps |
| `references/filters-and-transformations.md` | Simple and complex filtergraphs, audio/video filters, and graph debugging |
| `references/intermediate-workflows.md` | Trimming, concat, metadata, subtitles, scripting, pipes, and streaming |
| `references/advanced-operations-and-safety.md` | Hardware, synchronization, reproducibility, and operational safety |
| `references/command-cookbook.md` | Short, assumption-labeled commands |
| `references/source-inventory.md` | Primary and secondary sources with evidence boundaries |
| `references/local-verification.md` | Recorded local-build experiments and their limits |
| `references/learning-summary.md` | Learning progression and consolidated mental model |
| `scripts/ffmpeg-preflight` | Capability preflight: tool availability, inventory counts, and named filter/encoder/hwaccel checks |
| `scripts/test_ffmpeg_preflight.py` | Deterministic pytest suite for the preflight (fake tools, no media or network) |
| `scripts/fixtures/ffmpeg-8.1.2-inventories.json` | Small version-labeled real-output parser fixture |
| `evals/evals.json` | Portable output-quality cases for the skill |
| `references/source-inventory.md` | Original primary/secondary source inventory and evidence boundaries |
| `references/local-verification.md` | Version- and host-specific FFmpeg 8.1.2 experiments |
### Media editing and evidence guidance
| Path | Purpose |
|---|---|
| `references/media-intake-and-manifest.md` | Authorization, source identity, probe capture, output contracts, privacy, and preservation |
| `references/video-inspection-and-visual-evidence.md` | Bounded frame/clip sampling and defensible visual claims |
| `references/editorial-video-editing.md` | Transcript-assisted decisions, sequencing, treatments, and review gates |
| `references/audio-and-podcast-editing.md` | Mechanical edits, signal cleanup, editorial audio decisions, and listening checks |
| `references/ffmpeg-edit-decision-lists.md` | EDL semantics, validation, keyframe status, mapping, and command planning |
| `references/media-verification-and-acceptance.md` | Layered probe, decode, content, editorial, and downstream acceptance evidence |
| `references/media-failure-modes.md` | Diagnosis matrix, safe recovery, and stop rules |
| `references/media-research-source-index.md` | Claim-to-source map for official docs, standards, experiments, and heuristics |
### Copyable templates
| Path | Purpose |
|---|---|
| `templates/media-intake.json` | Parseable source, stream, timing, contract, privacy, and assumption manifest |
| `templates/edit-decision-list.json` | Parseable source ranges, evidence, confidence, treatments, mapping, and verification |
| `templates/video-inspection-report.md` | Fixed-section technical and sampled-evidence report |
| `templates/visual-review-packet.md` | Timestamped review samples with attribution and coverage limits |
| `templates/podcast-edit-plan.md` | Mechanical, signal-processing, and editorial audio plan |
| `templates/media-acceptance-report.md` | Criterion-by-criterion evidence and release verdict |
| `templates/research-experiment-record.md` | Versioned, reproducible command experiment record |
### Existing automation and evals
| Path | Purpose |
|---|---|
| `scripts/ffmpeg-preflight` | Tool status, inventory counts, and named filter/encoder/hwaccel checks |
| `scripts/test_ffmpeg_preflight.py` | Deterministic tests for the capability preflight |
| `scripts/fixtures/ffmpeg-8.1.2-inventories.json` | Small version-labeled parser fixture |
| `scripts/media-intake` | Read-only input inventory with bounded `ffprobe` metadata |
| `scripts/extract-review-frames` | Bounded timestamp frame extraction for human or vision review |
| `scripts/render-edl` | Validate an EDL and emit a non-executing FFmpeg command plan (review before multi-source rendering) |
| `scripts/audio-inspect` | Read-only audio metadata inspection with bounded probing |
| `scripts/media-verify` | Compare input/output probe documents against basic criteria |
| `evals/evals.json` | Output-quality cases for core FFmpeg, media evidence, video, podcast, EDL, safety, and acceptance behavior |
## Quick Start
Install FFmpeg with your platform's package manager, then verify both tools:
Install FFmpeg with your platform package manager and inspect the source before choosing an edit:
```sh
ffmpeg -version
@@ -36,31 +73,40 @@ ffprobe -version
ffprobe -v error -show_format -show_streams -of json input.mp4
```
Ask your agent to inspect the input before selecting a command. During exploration, write to a new output path and use `-n` to refuse accidental overwrites.
For a media editing task:
Before automating a version-sensitive recipe, check that the installed build actually has the required capabilities:
1. Copy `templates/media-intake.json` into a private task workspace and record the source and output contract.
2. Collect only the bounded frame, clip, transcript, or signal evidence needed for the decision.
3. Copy `templates/edit-decision-list.json` or `templates/podcast-edit-plan.md` and review consequential cuts.
4. Render to a new path with overwrite refusal while exploring.
5. Copy `templates/media-acceptance-report.md`, probe and review the result, then test the intended player, editor, service, or archive boundary.
Before using a version-sensitive recipe, inspect the local capability:
```sh
scripts/ffmpeg-preflight --filter scale --filter subtitles --encoder libx264 --hwaccel videotoolbox
scripts/ffmpeg-preflight --filter scale --filter loudnorm --encoder libx264 --hwaccel videotoolbox
```
Each name check is repeatable and reported `present` or `absent`. Exit codes: `0` all requested capabilities present, `1` a required tool or probe failed (including timeout or unparseable inventory), `2` a requested capability is absent after a usable inventory was parsed. Add `--json` for machine-readable output. The default probe timeout is 10 seconds; override it with `--timeout SECONDS`. Named FFmpeg-only checks do not require `ffprobe`, but media inspection and the no-query environment check do.
Named checks report each capability as present or absent. Exit code `1` means a required tool/probe failed; `2` means a requested capability is absent from a usable inventory. Add `--json` for machine-readable output.
## Triggers
Load this skill when the task involves:
Load this skill for:
- Inspecting or explaining a media file's streams, codecs, container, metadata, or timestamps
- Converting, remuxing, transcoding, filtering, trimming, joining, extracting, or subtitle handling
- Building FFmpeg batch scripts, pipe workflows, or network streaming commands
- Checking filter, encoder, protocol, or hardware-acceleration availability
- Diagnosing synchronization, concat, mapping, muxing, or playback failures
- Media intake, FFprobe manifests, stream/container/timestamp inspection, or output contracts
- Remuxing, transcoding, filtering, trimming, joining, extraction, subtitles, or synchronization
- Bounded review-frame or audio-evidence preparation from supplied/authorized local media
- Transcript-assisted video edits, reviewable EDLs, or deterministic render plans
- Podcast cutting, silence/noise analysis, loudness measurement, and audio cleanup
- Build capability checks, overwrite-safe batch work, output verification, or failure diagnosis
Do not load it as the primary skill for libav API development, DRM circumvention, professional color management, or a named platform's account/API operations.
Use another capability first for online media/transcript acquisition, semantic image interpretation, HTML-authored HyperFrames composition, platform publishing/API work, DRM, or rights clearance.
## Requirements
- `ffmpeg` and `ffprobe` on `PATH` for execution
- A shell for the examples, with careful quoting for filenames and filter expressions
- Network access only when consulting linked online documentation or exercising a network protocol
- Hardware acceleration requires the relevant device, drivers, compiled FFmpeg support, and a tested end-to-end path
- A shell with careful filename and filter-expression quoting
- A vision-capable or human reviewer for semantic claims about extracted images
- Listening playback for editorial audio acceptance
- Network access only for linked documentation or an explicitly requested network protocol
- Hardware acceleration only with the relevant device, drivers, compiled support, and a verified end-to-end path
+83 -34
View File
@@ -1,56 +1,105 @@
---
name: ffmpeg
description: >-
Use this skill when an agent needs to inspect, convert, remux, transcode, filter,
combine, stream, or troubleshoot audio and video with the FFmpeg command-line
tools, especially ffmpeg and ffprobe. It teaches explicit stream selection,
filtergraph construction, timestamp diagnosis, build-aware commands, safe
scripting, and post-run verification. Do not use it for libav API programming,
professional color-management certification, DRM circumvention, or untested
platform-specific capture hardware; route those to specialized guidance.
Use this skill for local FFmpeg/FFprobe media inspection, remuxing, transcoding,
filtering, evidence-bounded video review, transcript-assisted editorial plans,
edit decision lists, podcast/audio cleanup, rendering, and output acceptance.
It emphasizes explicit stream selection, source preservation, build-aware commands,
bounded evidence, and verified new outputs. Do not use it for libav API programming,
opaque whole-video understanding, automatic publishing, rights clearance, DRM
circumvention, professional broadcast/color certification, or HyperFrames-authored
compositions; route those tasks to their owning capabilities.
license: MIT
compatibility: Requires ffmpeg and ffprobe for execution; exact filters, codecs, protocols, and hardware backends vary by build and version.
---
# FFmpeg Expert
Treat FFmpeg commands as typed media pipelines, not incantations. Start from what the input actually contains, choose the smallest operation that satisfies the output contract, and verify the resulting artifact at the boundary that matters.
Treat FFmpeg as a typed media pipeline and media editing as an evidence-driven workflow. Inspect the actual source, separate measurements from interpretations, make decisions reviewable, render to a new path, and verify at the intended boundary.
## When Not to Use
- Do not use this skill for libav*/FFmpeg C API application development.
- Do not use it as a complete codec encyclopedia or a professional color-management certification guide.
- Do not use it to circumvent DRM or to document capture hardware that has not been tested on the target platform.
- For a named hosting or media platform's API, use that platform skill and use this skill only for the local media transformation.
- Do not use this skill for libav API programming, opaque whole-video semantic understanding, automatic publishing, rights clearance, DRM circumvention, professional broadcast/color certification, or HyperFrames-authored compositions.
- Route online media or transcript acquisition to the owning source skill, semantic frame interpretation to a vision-capable reviewer, and platform upload/API work to the platform skill.
## Operating Loop
## Boundaries and Routing
1. **Inspect first.** Run `ffprobe -v error -show_format -show_streams -of json INPUT` and identify streams, codecs, dimensions, rates, durations, time bases, metadata, and start timestamps.
2. **Classify the operation.** Choose remux/stream copy, transcode, filter, combine, extract, or protocol/pipe output. Remuxing changes packaging; transcoding decodes and re-encodes.
3. **Check capabilities.** Use `ffmpeg -formats`, `-codecs`, `-encoders`, `-filters`, and `-hwaccels`. Never assume a tutorial's filter, encoder, or hardware backend exists locally.
4. **Make selection explicit.** Use `-map` for multiple inputs, tracks, or complex graphs. Remember that options generally apply to the next input or output, so order matters.
5. **Protect the source.** Use `-n` while exploring, write to a new path, avoid untrusted shell concatenation, and keep credentials out of command lines and logs.
6. **Probe and exercise the result.** Check the output with `ffprobe`, then test the actual player, editor, receiver, archive rule, or API consumer. Exit code and container validity are necessary but not sufficient.
- Use a YouTube/transcript capability to acquire online video or transcripts; return here only for local supplied media and transcript artifacts.
- Use HyperFrames for HTML-authored motion graphics or composition; use this skill to inspect and preprocess its media inputs or verify rendered outputs.
- Use the named platform skill for upload, publishing, account, or API operations.
- FFmpeg can extract bounded frames and audio segments but does not interpret their semantic content. Route visual interpretation to a vision-capable reviewer and preserve its observations as attributed evidence.
- Do not infer rights, consent, identity, intent, or whole-program meaning from technical metadata, sparse frames, silence intervals, or an unaligned transcript.
## Choose the Right Reference
## Evidence Classes
Label consequential claims so unlike evidence is not blended:
- **Technical contract** — behavior documented by an official FFmpeg or standards source.
- **Observed artifact** — probe output, measured signal result, extracted frame, listened segment, or downstream test from this source/output.
- **Reproducible experiment** — exact version, input identity/generator, command, result, and limits.
- **Editorial heuristic** — a reversible judgment that requires human review, not a fact established by FFmpeg.
- **User requirement** — the requested output contract, preservation policy, and acceptance threshold.
## Media Editing Loop
1. **Intake.** Confirm authorization and privacy boundaries; identify every source; use a private workspace copy of `templates/media-intake.json` to record probe evidence, timing, the output contract, preservation policy, and unresolved assumptions.
2. **Inspect.** Probe streams and format. Check required local capabilities with inventories or `scripts/ffmpeg-preflight`; never assume a filter, encoder, or hardware backend exists.
3. **Collect bounded evidence.** Extract only the frames, clips, waveform/signal measurements, or transcript spans needed for the decision. Record sample timestamps, count, byte/size limits, and the statement that samples cover sampled times only.
4. **Plan before rendering.** For editorial changes, write a reviewable EDL or podcast edit plan. Every consequential cut needs a source range, reason, evidence, confidence, treatment, mapping, and verification state. Leave ambiguous decisions unresolved rather than improvising.
5. **Render safely.** Make stream mapping explicit; prefer `-n` and a new output path; avoid untrusted shell concatenation. Distinguish keyframe-limited stream copy from decoded/re-encoded precise cuts.
6. **Verify in layers.** Check exit status, decodeability, output probe, stream/timing contract, bounded frame/audio evidence, editorial review, and the actual downstream consumer as applicable.
7. **Accept or stop.** Use a private workspace copy of `templates/media-acceptance-report.md` to record pass/fail/blocked per criterion. A valid container or successful command alone is not acceptance.
Start technical inspection with:
```sh
ffprobe -v error -show_format -show_streams -of json INPUT
```
## Route to the Focused Reference
### Evidence-driven media work
- Read `references/media-intake-and-manifest.md` before handling supplied/generated media, sensitive material, multiple sources, or a defined delivery contract.
- Read `references/video-inspection-and-visual-evidence.md` when extracting or reviewing frames/clips, choosing samples, or making visual claims.
- Read `references/editorial-video-editing.md` for transcript-assisted selection, sequencing, pacing, transitions, overlays, and reviewable editorial decisions.
- Read `references/audio-and-podcast-editing.md` for podcast cuts, signal cleanup, silence/noise analysis, loudness measurement, and listening gates.
- Read `references/ffmpeg-edit-decision-lists.md` before creating, validating, or turning an EDL into a command plan.
- Read `references/media-verification-and-acceptance.md` before declaring an output complete or compatible.
- Read `references/media-failure-modes.md` when evidence is contradictory, a cut drifts, a filter is missing, review samples are sparse, or a workflow repeatedly fails.
- Read `references/media-research-source-index.md` when supporting claims, refreshing version-sensitive guidance, or recording a technical experiment.
### Core FFmpeg work
- Read `references/core-model-and-command-anatomy.md` for containers, streams, codecs, option scope, mapping, copy/transcode, and timestamps.
- Read `references/filters-and-transformations.md` for simple and complex filtergraphs, labels, audio/video processing, and incremental graph debugging.
- Read `references/intermediate-workflows.md` for seeking, trimming, concatenation, metadata, subtitles, batch scripts, pipes, and streaming.
- Read `references/advanced-operations-and-safety.md` for hardware acceleration, synchronization diagnosis, reproducibility, network and overwrite safety, and failure boundaries.
- Read `references/command-cookbook.md` for short examples with stated assumptions. Adapt them only after inspection and capability checks.
- Read `references/learning-summary.md` for the newcomer-first progression and consolidated mental model.
- Read `references/source-inventory.md` when assessing evidence, choosing authoritative documentation, or refreshing version-sensitive guidance.
- Read `references/local-verification.md` when interpreting the recorded local FFmpeg 8.1.2 evidence. It is a host-specific observation, not a universal capability claim.
- Run `scripts/ffmpeg-preflight` before automating a version-sensitive workflow. It reports tool status, parsed filter/encoder/hwaccel counts, and repeatable named checks: `--filter NAME`, `--encoder NAME`, `--hwaccel NAME`. Use `--json` for machine-readable output and `--timeout SECONDS` to bound each probe (10 seconds by default). Named FFmpeg-only checks do not require `ffprobe`; media inspection and the no-query environment check do. Exit codes: 0 every requested capability is present, 1 a required tool/probe failed or the requested inventory was empty/unparseable, 2 a usable inventory was parsed and a requested capability is absent. Empty inventories without named queries are reported as warnings.
- Read `references/filters-and-transformations.md` for filtergraphs, labels, audio/video processing, and incremental graph debugging.
- Read `references/intermediate-workflows.md` for seeking, trimming, concat, metadata, subtitles, batching, pipes, and streaming.
- Read `references/advanced-operations-and-safety.md` for hardware acceleration, synchronization, reproducibility, network safety, and failure boundaries.
- Read `references/command-cookbook.md` only after inspection and capability checks; every recipe is conditional.
- Read `references/learning-summary.md` for the newcomer-first mental model.
- Read `references/source-inventory.md` for the original FFmpeg source survey and `references/local-verification.md` only for its explicitly host-specific FFmpeg 8.1.2 observations.
## Debugging Rules
## Templates
- For missing filters, encoders, or protocols, reproduce with `ffmpeg -filters`, `-encoders`, or the relevant inventory before changing the command.
- For drift, bad cuts, concat jumps, or unexpected duration, compare timestamps and stream properties before adding flags. `-copyts`, `-start_at_zero`, synchronization controls, `setpts`, `asetpts`, `aresample`, and `avoid_negative_ts` solve different problems.
- Build filtergraphs incrementally: baseline transcode, one filter, then labels/branches. Distinguish parser, availability, format negotiation, timestamp, encoder, and muxer failures.
- Treat examples as conditional on input, target, build, version, and downstream consumer. State those conditions in explanations and scripts.
- `templates/media-intake.json` — source identities, probes, contract, privacy, preservation, assumptions
- `templates/edit-decision-list.json` — reviewable source ranges and treatments
- `templates/video-inspection-report.md` — technical inspection and bounded evidence ledger
- `templates/visual-review-packet.md` — attributed frame/clip observations and coverage limits
- `templates/podcast-edit-plan.md` — mechanical, signal, and editorial audio decisions
- `templates/media-acceptance-report.md` — layered verification and criterion verdicts
- `templates/research-experiment-record.md` — reproducible version/command/result record
Copy a template into the task workspace and replace its placeholder/example values. Do not put private paths, media, transcripts, or review evidence in the public skill repository.
## Non-Negotiable Checks
- Make stream selection explicit whenever multiple inputs/tracks or a complex graph are involved.
- Treat option order as significant: options generally apply to the next input or output.
- Do not call silence useless; `silencedetect` reports threshold crossings, not editorial value.
- Claim loudness, clipping, timing, or keyframe status only from an available measurement method and retain its output.
- A transcript is evidence only for its text and supplied timing quality; spot-check alignment against media before frame-accurate edits.
- Stop for review when evidence is sparse, ambiguity could remove meaningful content, an optional tool/filter is absent, privacy/authorization is unclear, or two materially different approaches fail.
## Completion
Stop when the requested artifact exists, the relevant output probe and downstream-boundary check pass, and any untested capability or compatibility gap is stated explicitly. If execution is blocked, report the exact layer and evidence rather than substituting a plausible result.
Finish only when the requested artifact exists at a new path, required probes and bounded reviews are recorded, the output has been exercised at the relevant downstream boundary, and every acceptance criterion is passed or explicitly blocked. Report untested claims and remaining assumptions instead of filling gaps with plausible output.
@@ -0,0 +1,66 @@
# Audio and Podcast Editing
Separate mechanical assembly, signal processing, and editorial judgment. FFmpeg can measure and transform audio; it cannot decide whether a pause, breath, correction, tone, identity, or statement should be removed.
## Intake and plan
Probe each candidate stream and record codec, sample format/rate, channel count/layout, time base, start/duration, disposition, and language metadata. Define the output contract, target loudness policy if one exists, required channel layout, allowed repairs, prohibited edits, and review owner.
Divide proposed work into:
- **Mechanical:** explicit range cuts, reorder, fades, slate/tone removal, channel mapping, resampling, and encoding.
- **Signal repair:** gain, equalization, hum/noise reduction, de-essing, dynamics, clipping prevention, or dropout repair.
- **Editorial:** removing speech, changing sequence, shortening pauses, selecting takes, or altering context.
Put source ranges and evidence in a podcast edit plan or EDL. A transcript is a navigation aid; verify words, timing, speaker changes, and context by listening to the source.
## Measurement and candidate detection
Relevant filters include:
- `astats` for time-domain audio statistics;
- `volumedetect` for volume statistics;
- `ebur128` for EBU R128 analysis and metadata/log output;
- `loudnorm` for EBU R128 normalization and measured values;
- `silencedetect` for threshold- and duration-based silence candidates;
- `aphasemeter` for channel phase measurements where applicable.
Record the exact interval, filter options, channel mode, FFmpeg build, and unfiltered source. Noise floors, breaths, room tone, music, cross-talk, and codec artifacts can invalidate a generic threshold.
For loudness normalization, a measured first pass followed by a parameterized second pass is more reviewable than assuming one-pass behavior meets a delivery policy. Verify the rendered output again; a filters reported target is not acceptance evidence by itself.
## Editing and processing
- Use `atrim` for ranges and `asetpts=PTS-STARTPTS` when a segment needs a zero-based timeline.
- Use the concat filter after making sample rate, sample format, and channel layout deliberate.
- Use `afade`/`acrossfade` only when their duration and overlap are editorially approved.
- Treat `silenceremove` as an editorial transform, not harmless cleanup.
- Apply `highpass`, `lowpass`, `afftdn`, `arnndn`, compression, limiting, or normalization only after a bounded comparison. Filter availability and behavior depend on the local build and options.
- Avoid repeated lossy encoding. Keep a suitable intermediate when multiple review passes are required.
- Map audio and attached video/subtitle streams explicitly; define metadata and chapter retention.
## Listening acceptance
Listen to the opening and closing, every cut/fade/transition, all repaired regions, representative loud and quiet passages, channel fold-down if relevant, and any section flagged by measurements. Check speech intelligibility, clicks, truncation, pumping, tonal shifts, room-tone jumps, phase issues, context, and sync with video. Test the intended destination.
Keep review records privacy-safe: use opaque speaker labels, quote only the minimum required text, and do not publish raw transcripts, private paths, or embedded tags.
## Evidence and heuristic boundary
| Classification | Boundary |
|---|---|
| Direct evidence | Probe fields, decoded samples, filter measurements, commands, logs, and attributed listening observations for declared intervals. |
| Threshold evidence | Silence, loudness, clipping, or phase candidates under explicitly recorded filter settings. |
| Heuristic | Speaker labels, transcript timing, “noise-only” regions, acceptable pause length, or a preset suitable for another recording. |
| Editorial judgment | Whether an edit preserves meaning, consent, tone, and continuity. This requires accountable listening review. |
| Not established | Whole-program quality from a few measurements, speaker identity, legal clearance, accessibility, or destination acceptance from an FFmpeg exit status. |
## Official FFmpeg sources
- [ffprobe Documentation](https://ffmpeg.org/ffprobe.html) — audio stream and packet/frame inspection.
- [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html) — mapping, filtering, codecs, timestamps, and transcoding.
- [FFmpeg Filters Documentation](https://ffmpeg.org/ffmpeg-filters.html) — `atrim`, `asetpts`, concat, fades, silence, statistics, loudness, equalization, denoising, dynamics, and resampling filters.
- [FFmpeg Resampler Documentation](https://ffmpeg.org/ffmpeg-resampler.html) — resampling and rematrixing options.
- [FFmpeg Codecs Documentation](https://ffmpeg.org/ffmpeg-codecs.html) — codec-specific capabilities and options.
Official documentation establishes mechanics, not local component availability, editorial correctness, or compliance with an external delivery specification.
@@ -0,0 +1,58 @@
# Editorial Video Editing
Treat an editorial edit as a sequence of reviewable decisions, not as one opaque FFmpeg command. Preserve source media, use an edit decision list (EDL), render to a new path, and separate technical conformance from human approval.
## Plan before rendering
1. Write the editorial goal, audience, required duration or range, prohibited changes, output contract, and reviewer.
2. Probe all selected streams and establish their timelines, time bases, start times, frame cadence, audio layout, subtitles, and metadata.
3. Use transcripts, scene scores, silence events, and frame samples only to navigate. Verify consequential words, cuts, identities, and context against decoded media.
4. Record each keep/remove/reorder/treatment decision with source range, rationale, evidence locator, confidence, and required review.
5. Validate the EDL before generating a filtergraph: ranges must be ordered, bounded, and compatible with transitions and linked audio.
## Cut and assembly choices
- **Filter-based cuts:** `trim` and `atrim` select ranges but do not reset timestamps. Follow them with `setpts=PTS-STARTPTS` and `asetpts=PTS-STARTPTS` when segments must begin at zero before concatenation.
- **Concat filter:** use for decoded segments in one graph. Corresponding streams must have compatible parameters; normalize geometry, pixel format, sample format/rate, and channel layout deliberately.
- **Concat demuxer:** use an `ffconcat` list for separate files whose streams are suitable for packet-level concatenation. Its `duration`, `inpoint`, and `outpoint` directives have documented timestamp and packet-boundary caveats.
- **Stream-copy cuts:** fast and lossless at the packet level, but start/end precision is constrained by seek points, inter-frame dependencies, timestamps, and muxer behavior. Do not promise frame-accurate editorial cuts without checking the decoded result.
- **Transitions:** overlaps such as `xfade` or `acrossfade` consume timeline duration and require adequate handles. Put the overlap and expected output-duration calculation in the EDL.
Map streams explicitly. Define whether chapters, attachments, data streams, subtitles, language tags, dispositions, and metadata are retained, rewritten, or removed. An omitted `-map` leaves stream selection to automatic rules that may not match editorial intent.
## Treatments
Apply only treatments required by the brief:
- geometry: `crop`, `scale`, `pad`, rotation/orientation handling;
- timing: `fps`, `setpts`, `atempo`, or resampling only with an explicit cadence/sync decision;
- compositing: overlays, masks, titles, or subtitles with rights and readability review;
- picture: deinterlacing, range/color conversion, or grading with source and target color assumptions recorded;
- sound: fades, gain, loudness, noise reduction, and channel mapping under the audio plan.
Keep an intermediate render when it improves reviewability, but avoid unnecessary generations. Record codecs and settings at every lossy boundary.
## Review gates
1. **EDL review:** all consequential cuts and treatments approved.
2. **Technical render review:** expected streams, timestamps, duration, geometry, cadence, color tags, audio format, and decode behavior.
3. **Content review:** opening/closing, every join and transition, titles/subtitles, linked audio, and any high-risk treatment.
4. **Editorial review:** meaning, pacing, continuity, context, accessibility, and approved claims.
5. **Destination review:** playback or import in the intended player, editor, service, or archive workflow.
## Evidence and heuristic boundary
- **Direct evidence:** probe/decode results, recorded EDL ranges, commands, logs, and reviewed samples for this build and artifact.
- **Derived evidence:** expected duration or transition math computed from declared ranges and rounding rules.
- **Heuristic:** transcript alignment, scene/silence candidates, automated crop choices, inferred continuity, or “visually lossless” judgments. Mark and review them.
- **Human decision:** editorial suitability and preservation of meaning require an accountable reviewer.
- **Not established:** a successful render does not prove frame-accurate cuts, correct context, accessibility, rights, sync everywhere, or destination acceptance.
## Official FFmpeg sources
- [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html) — seeking, stream selection, mapping, filtering, codecs, metadata, and overwrite controls.
- [FFmpeg Filters Documentation](https://ffmpeg.org/ffmpeg-filters.html) — `trim`, `atrim`, timestamp filters, concat, transitions, geometry, subtitles, and audio/video treatments.
- [FFmpeg Formats Documentation](https://ffmpeg.org/ffmpeg-formats.html) — concat demuxer and muxer/container behavior.
- [FFmpeg Utilities Documentation](https://ffmpeg.org/ffmpeg-utils.html) — timeline and duration expressions.
These sources define mechanics. They do not supply an editorial rationale, validate transcript meaning, or guarantee compatibility with a particular destination.
@@ -0,0 +1,65 @@
# FFmpeg Edit Decision Lists
An edit decision list (EDL) is the reviewable source of truth between editorial intent and an FFmpeg render plan. Keep it data-oriented, versioned, and independent of private filesystem paths.
## Canonical semantics
Use opaque asset IDs and identify streams explicitly. For each source, retain probe evidence for stream index, time base, start time, duration, cadence, audio layout, and source digest.
Define ranges as half-open intervals, `[in, out)`, on the selected source stream timeline. State the time unit and precision. Decimal seconds are convenient for review; preserve exact timestamps or integer ticks when frame/sample boundaries matter. Never silently treat timecode, container time, wall-clock time, and frame number as interchangeable.
Each event should record:
- stable event ID and action (`keep`, `remove`, `insert`, or `treatment`);
- source asset and stream references;
- source `in` and `out`, plus any transition handles;
- destination order or lane;
- linked audio/video policy;
- rationale, evidence locators, confidence, and reviewer status;
- transformations, transition type/duration, and expected duration effect;
- whether the cut requires decoded precision or permits packet-level copy;
- verification points around the resulting boundary.
Store commands as generated render records, not as the EDLs only meaning. Raw paths and shell fragments are unsafe substitutes for structured fields.
## Pre-render validation
Reject or flag an EDL when:
- a source, stream, time unit, or range endpoint is missing;
- `in >= out`, a range falls outside declared source bounds, or rounding behavior is undefined;
- events overlap unintentionally or leave an unexplained gap;
- linked streams use incompatible timelines or omit a sync policy;
- transitions lack sufficient handles or their overlap is absent from duration math;
- concat inputs have unresolved format differences;
- frame/sample-accurate intent is paired with an unverified stream-copy strategy;
- required editorial decisions have no evidence or review status.
Calculate expected output duration from kept ranges, inserts, speed changes, and transition overlaps. Mark the result as derived and declare a tolerance for timestamp/time-base rounding.
## Mapping to FFmpeg
For decoded segment assembly, use `trim`/`atrim`, reset segment timestamps with `setpts`/`asetpts` where required, normalize compatible media parameters deliberately, and join with the concat filter. Map output streams explicitly.
For separate compatible files, the concat demuxer consumes an `ffconcat` list. Its `inpoint` and `outpoint` can include packets outside the requested interval because of inter-frame dependencies and packet boundaries; timestamps can also be adjusted globally. Review the decoded joins.
Fast seek and stream copy may choose seek points or packets that do not correspond to an exact visual/audio edit boundary. Label keyframe/packet status as one of `verified`, `not_verified`, or `not_applicable`; never infer it from a round timestamp.
Record FFmpeg/ffprobe versions, complete generated command, mapping, codec settings, environment-sensitive capabilities, output digest, and acceptance report alongside the rendered artifact.
## Evidence and heuristic boundary
- **Direct evidence:** source probe data, exact EDL fields, reviewed source samples, packet/frame observations, generated command, and output verification records.
- **Derived evidence:** output order and duration computed from declared EDL semantics and a stated rounding rule.
- **Heuristic:** transcript-aligned endpoints, scene/silence candidates, guessed keyframes, or assumed concat compatibility. These must be labeled and tested.
- **Human decision:** rationale, continuity, context, and approval are editorial evidence only when attributed.
- **Not established:** an internally valid EDL does not prove render precision, sync, semantic correctness, rights, or downstream acceptance.
## Official FFmpeg sources
- [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html) — seeking, timestamps, stream selection/mapping, filtering, and codec-copy behavior.
- [FFmpeg Filters Documentation](https://ffmpeg.org/ffmpeg-filters.html) — `trim`, `atrim`, `setpts`, `asetpts`, concat, `xfade`, and `acrossfade` semantics.
- [FFmpeg Formats Documentation](https://ffmpeg.org/ffmpeg-formats.html) — concat demuxer syntax, `duration`, `inpoint`, `outpoint`, and format behavior.
- [FFmpeg Utilities Documentation](https://ffmpeg.org/ffmpeg-utils.html) — duration and time expression syntax.
The sources define FFmpegs timeline and assembly mechanisms. The EDL conventions above are workflow rules; they are not an FFmpeg-native interchange standard.
+48
View File
@@ -0,0 +1,48 @@
# Media Failure Modes
Diagnose from the first discriminating evidence, preserve the source, and change one assumption at a time. Do not “repair” the only copy or hide warnings merely to obtain a zero exit status.
## Diagnostic matrix
| Symptom | Evidence to collect | Common hypotheses | Safe next action |
|---|---|---|---|
| Wrong or missing output stream | Full input/output `ffprobe`; complete command; mapping log | Automatic stream selection, disposition, program, optional map, or unsupported type | Declare `-map` and retention policy explicitly; verify the new output. |
| Duration/start time is surprising | Format and stream start/duration/time base; chapters; packet/frame sample near boundaries | Container estimate, edit list, timestamp offset, VFR, sparse index, truncation | Keep raw timestamps; compare decoded boundaries; do not overwrite metadata blindly. |
| Cut begins early/late | Seek placement, keyframe/packet data, decoded frames/audio around the boundary | Input seek point, inter-frame dependency, packet-level copy, time-base rounding | Use a decoded trim for precision or approve the observed packet boundary. |
| Concat fails or drifts | Probe every segment; concat method; per-stream parameters and timestamps | Codec/parameter mismatch, inconsistent time bases, missing streams, incorrect duration directives | Normalize intentionally or use decoded concat; review every join. |
| Non-monotonic DTS or timestamp warnings | Full warning context; packet timestamps around event; muxer and sync options | Broken source timestamps, reordered frames, concat offsets, inappropriate passthrough | Isolate the first bad interval; avoid speculative timestamp generation; test a new artifact. |
| Filter, encoder, or hardware path is unavailable | Local version/build configuration and component inventories | Local build lacks dependency/component; unsupported device/pixel format | Choose a verified available path; software fallback must be explicit and reaccepted. |
| A/V sync changes over time | Independent stream starts/durations/time bases; declared sync points across the timeline | Clock/cadence mismatch, dropped/duplicated frames, resampling, incorrect trim/concat | Measure drift before choosing timestamp, frame-rate, or resampling correction. |
| Color, range, or geometry is wrong | Source/output color and aspect metadata; decoded reference frames; filtergraph | Unstated conversion, ignored display metadata, range/matrix mismatch, SAR/DAR error | Declare the conversion and target; compare in the intended display path. |
| Audio clips, pumps, or changes tone | Source/output measurements plus listening around affected regions | Excess gain, dynamics/denoise settings, resampling, channel rematrixing | Bypass filters, compare one stage at a time, then re-measure and listen. |
| Probe succeeds but decode fails | Decode-check errors and first failing timestamp/stream | Truncated/corrupt packets, unsupported feature, damaged index, decoder defect | Preserve evidence; isolate stream/interval; seek an alternate authorized source when repair is uncertain. |
| Local playback succeeds but destination rejects | Exact accepted artifact plus destination error/version/settings | Unsupported container/codec/profile/level, metadata, file limit, ingest policy | Test against documented destination requirements; make a new derivative and reaccept it. |
## Recovery rules
1. Save the exact command, build, exit status, and unfiltered first relevant warnings.
2. Probe before changing anything; compare source and failed output.
3. Reduce to one stream and the smallest failing interval only in a new diagnostic artifact.
4. Verify local availability before adding codec, filter, format, protocol, or hardware options.
5. Prefer explicit mapping, formats, time bases, and channel/pixel choices over guessed defaults.
6. After a change, repeat the checks affected by that change. A workaround is not a diagnosis until evidence distinguishes it.
Stop when authorization is unclear, the only source would be modified, damage/repair would alter meaning, encryption or access controls are encountered, private material would leave its approved boundary, or acceptance requires an unavailable human/destination review.
## Evidence and heuristic boundary
- **Direct evidence:** exact probe fields, packet/frame observations, logs, commands, decoded samples, and destination errors for the named artifact/build.
- **Discriminating experiment:** a controlled one-variable comparison; its conclusion is limited to the recorded fixture and environment.
- **Hypothesis:** every “common cause” in the matrix until evidence rules it in. Similar symptoms can have different causes.
- **Heuristic:** increasing probe limits, regenerating timestamps, forcing a codec/tag, changing sync mode, or re-encoding “because it usually works.” Never present these as proven repairs.
- **Not established:** absence of warnings is not proof of integrity, editorial correctness, sync everywhere, or downstream compatibility.
## Official FFmpeg sources
- [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html) — option scope, seeking, mapping, timestamps, filtering, codecs, logging, and overwrite controls.
- [ffprobe Documentation](https://ffmpeg.org/ffprobe.html) — format/stream/packet/frame inspection and bounded intervals.
- [FFmpeg Formats Documentation](https://ffmpeg.org/ffmpeg-formats.html) — probing, concat, muxer/demuxer, and timestamp-related format options.
- [FFmpeg Filters Documentation](https://ffmpeg.org/ffmpeg-filters.html) — filter requirements, timeline behavior, analysis filters, and transformations.
- [FFmpeg Codecs Documentation](https://ffmpeg.org/ffmpeg-codecs.html) — codec options and implementation-specific constraints.
The documentation describes interfaces and known semantics; it does not identify the cause of a particular failure without artifact-specific evidence.
@@ -0,0 +1,54 @@
# Media Intake and Manifest
Use an intake manifest before inspecting or editing supplied or otherwise authorized media. The manifest is a record of what was received, what the local tools reported, and what the output must satisfy; it is not a rights determination or an editorial brief.
## Minimum intake
1. Assign a non-identifying `asset_id`. Keep real names, account identifiers, and private storage paths out of shareable records.
2. Record authorization scope, permitted operations, retention limits, and who may review the media. If authority is unclear, stop.
3. Preserve the source. Work from a copy or render to a new destination; do not normalize, rename, or overwrite the only original.
4. Record source identity separately from content claims: byte size, whole-file digest if available, acquisition date, and a private locator. A filename extension is only a hint.
5. Capture the exact `ffprobe` and FFmpeg versions used. Build options and enabled libraries can change available codecs, filters, and behavior.
6. Probe structure without decoding the entire asset:
```sh
ffprobe -v error -show_format -show_streams -show_chapters -of json INPUT
```
Preserve absent fields as absent or `null`; do not invent duration, frame rate, language, or channel layout. For ambiguous or damaged inputs, record probe warnings and any non-default `-probesize` or `-analyzeduration` used.
## Manifest fields that matter
- **Source:** opaque asset ID, private locator, size, digest, preservation status.
- **Tool context:** `ffprobe` version, FFmpeg version, build identity, command, exit status, warning-log locator.
- **Container:** reported format names, start time, duration, bit rate, chapters, and tags needed for the task.
- **Streams:** index, codec type/name, time base, start time, duration, disposition, language, and stream-specific fields such as dimensions, pixel format, field order, color metadata, sample rate, channel layout, and subtitle type.
- **Timing:** whether durations come from the container, stream metadata, counted packets/frames, or another declared method.
- **Output contract:** required container, stream set and order, codecs, geometry, frame cadence, audio layout/rate, duration tolerance, subtitle/metadata policy, file-size constraint, and target player/editor/service.
- **Assumptions and unknowns:** each assumption, why it is being used, risk, and how it will be tested.
Probe output may contain identifying tags, device data, creation times, titles, comments, and private paths. Store raw output in the restricted task workspace; publish only a minimized/redacted derivative.
## Output-contract discipline
Express acceptance criteria before rendering. Prefer measurable criteria such as “one H.264 video stream and one AAC stereo audio stream,” “1920×1080,” or “duration within the declared tolerance.” Keep subjective criteria such as pacing or intelligibility as separate human-review items. “Plays for me” and “command exited zero” are not output contracts.
## Evidence and heuristic boundary
| Classification | What may be claimed |
|---|---|
| Direct evidence | The exact probe output, warnings, command, build, source digest, and authorization record captured for this asset. |
| Derived evidence | Values calculated from recorded fields using a stated formula and rounding rule. Label them as derived. |
| Heuristic | Meaning inferred from an extension, filename, tags, nominal frame rate, sampled content, or prior behavior of a destination. Mark it as an assumption. |
| Not established | Rights ownership, complete decodability, semantic content, editorial quality, sync, or destination compatibility. Probe metadata alone establishes none of these. |
A detector or probe result is evidence about this input under the recorded build and options. It is not universal evidence about every copy, FFmpeg build, decoder, or playback environment.
## Official FFmpeg sources
- [ffprobe Documentation](https://ffmpeg.org/ffprobe.html) — stream/container inspection, output writers, counting, intervals, and probe options.
- [FFmpeg Formats Documentation](https://ffmpeg.org/ffmpeg-formats.html) — demuxer/muxer behavior, probing controls, and format-specific options.
- [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html) — option scope, stream selection, mapping, transcoding, and overwrite behavior.
- [FFmpeg Utilities Documentation](https://ffmpeg.org/ffmpeg-utils.html) — duration, size, rate, and related value syntax.
These official pages define FFmpeg interfaces and documented behavior. They do not prove that a local build includes a component, that metadata is truthful, or that an output meets an editorial or downstream requirement.
@@ -0,0 +1,45 @@
# Media Research Source Index
Use this index to connect a media claim to the strongest available source. Prefer official documentation for FFmpeg semantics, local evidence for installed capability, controlled experiments for uncertain behavior, and attributed review for editorial judgments.
## Official FFmpeg sources
| Claim area | Official source | Supports | Does not establish |
|---|---|---|---|
| CLI option scope, seeking, stream selection, mapping, filtering, metadata, overwrite behavior | [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html) | Documented command-line semantics and processing model | Local component availability, editorial correctness, or destination acceptance |
| Format, stream, packet, frame, interval, and output-writer inspection | [ffprobe Documentation](https://ffmpeg.org/ffprobe.html) | Probe fields, selection, counting, intervals, and machine-readable output | Truth of embedded metadata, complete decode, or semantic content |
| Demuxers, muxers, concat, probing, and format-specific behavior | [FFmpeg Formats Documentation](https://ffmpeg.org/ffmpeg-formats.html) | Documented container and format options | Universal player/service support or validity of a specific damaged file |
| Audio/video filters, timelines, analysis, transforms, and metrics | [FFmpeg Filters Documentation](https://ffmpeg.org/ffmpeg-filters.html) | Filter parameters, outputs, and documented constraints | Suitability of thresholds, perceptual quality, or editorial intent |
| Encoder and decoder options | [FFmpeg Codecs Documentation](https://ffmpeg.org/ffmpeg-codecs.html) | Codec-specific controls exposed by FFmpeg | Presence in a local build, conformance of every output, or destination policy |
| Resampling and rematrixing | [FFmpeg Resampler Documentation](https://ffmpeg.org/ffmpeg-resampler.html) | `libswresample` options and documented behavior | Listening quality or correctness of a chosen channel policy |
| Scaling, pixel conversion, and dithering | [FFmpeg Scaler Documentation](https://ffmpeg.org/ffmpeg-scaler.html) | `libswscale` options and conversion controls | End-to-end color accuracy on an untested display pipeline |
| Time, duration, rate, size, color, and expression syntax | [FFmpeg Utilities Documentation](https://ffmpeg.org/ffmpeg-utils.html) | Shared value and expression syntax | The correct editorial timebase or rounding policy for a project |
| Device and protocol interfaces | [FFmpeg Devices Documentation](https://ffmpeg.org/ffmpeg-devices.html) and [FFmpeg Protocols Documentation](https://ffmpeg.org/ffmpeg-protocols.html) | Documented input/output devices and protocol options | Authorization, network safety, availability, or reliability in a given environment |
The website reflects a documented FFmpeg version that may differ from an installed binary. Capture `ffmpeg -version`, `ffprobe -version`, build configuration, and relevant local inventories before making availability claims.
## Evidence hierarchy
1. **Artifact-specific direct evidence:** exact probe/decode output, packets/frames, measurements, logs, samples, destination results, and attributed review tied to an asset digest.
2. **Official FFmpeg documentation:** primary source for documented interfaces and semantics; cite the page and, when consequential, the option/filter section and access date.
3. **Controlled local experiment:** fixture, digest, command, build, environment, raw result, expected result, and limitations. Reproduction supports only the tested conditions.
4. **External specification or destination documentation:** normative format/delivery requirement. It does not prove a particular encoder output or ingest result; test both.
5. **Secondary explanation:** useful for discovery, never stronger than the primary source it interprets.
6. **Heuristic or editorial convention:** label it, explain why it is reasonable, and assign a reviewer/test.
## Claim-record pattern
For consequential claims, record:
- claim and classification (`direct`, `derived`, `documented`, `experimental`, `heuristic`, or `human_review`);
- source URL or evidence locator;
- FFmpeg version/build and command where applicable;
- asset/fixture ID and bounded interval;
- observed result, confidence, assumptions, and counter-evidence;
- what the evidence does **not** establish.
Do not paste private URLs, credentials, absolute paths, personal names, raw transcripts, or identifying metadata into public research records. Use opaque IDs and restricted evidence locators.
## Evidence and heuristic boundary
Official documentation is evidence for documented behavior, not proof that the local build implements a feature or that a specific command produced the intended artifact. Local inventories establish availability only for that build. Experiments establish observations only for their fixture and conditions. Automated scores and detector events are measurements under declared parameters, not semantic truth. Editorial quality, meaning, identity, consent, and rights require appropriate human or authoritative evidence outside FFmpeg.
@@ -0,0 +1,76 @@
# Media Verification and Acceptance
Accept a media artifact from recorded evidence against a declared contract. A zero exit status proves only that one command completed without reporting a fatal error; it does not prove correct streams, complete decode, editorial quality, or destination compatibility.
## Layered acceptance
### 1. Artifact and provenance
Record the output asset ID, digest, size, producing command, source/EDL versions, FFmpeg build, completion status, warning log, and whether the path was new or pre-existing. Confirm the accepted file is the file that was reviewed.
### 2. Structural conformance
Probe the output and compare every required field with the contract:
```sh
ffprobe -v error -show_format -show_streams -show_chapters -of json OUTPUT
```
Check container, stream count/order, codecs, dispositions, language, dimensions, aspect ratios, pixel format, color metadata, cadence, start times, durations, audio sample rate/layout, subtitles, chapters, and metadata policy. Treat absent or ambiguous fields explicitly.
### 3. Processing-path check
Exercise all expected audio/video streams through FFmpeg and preserve errors:
```sh
ffmpeg -v error -i OUTPUT -map '0:v?' -map '0:a?' -f null -
```
This can expose decode or timeline faults in FFmpegs processing path. It does not exercise every player, subtitle/data stream, hardware decoder, display pipeline, or network/service ingest path.
### 4. Signal and timing checks
Use only metrics tied to criteria: frame/packet counts, start/end timestamps, cadence, A/V offset at declared points, black/freeze candidates, audio statistics, loudness, peaks, or silence events. Record filters, thresholds, intervals, and tolerances. Re-measure the rendered artifact rather than assuming encoder/filter targets were met.
### 5. Content review
Review the opening and closing, every cut/join/transition/treatment, titles/subtitles, high-risk regions, representative motion and detail, loud/quiet passages, and declared sync points. Use full-resolution frames or short clips where contact sheets are insufficient. Attribute reviewer and time.
### 6. Editorial and downstream acceptance
An accountable reviewer decides whether meaning, pacing, continuity, intelligibility, accessibility, and the brief are satisfied. Then import, play, upload, or validate the exact artifact in the intended destination. Record destination identity/version, settings, result, warnings, and any transformed derivative.
## Acceptance record
For every criterion, capture:
- criterion and tolerance;
- evidence method and exact artifact/interval;
- observed value or attributed observation;
- status: `pass`, `fail`, `blocked`, or `not_applicable`;
- reviewer and date;
- exception owner and rationale, if any.
The final verdict is `accepted`, `rejected`, or `blocked`. Do not convert an untested criterion into a pass. Any post-review change invalidates affected evidence and requires re-verification.
Minimize reports before sharing: remove private paths, personal names, account identifiers, unnecessary transcript excerpts, and embedded metadata.
## Evidence and heuristic boundary
| Classification | Boundary |
|---|---|
| Direct evidence | Probe/decode output, measurements, samples, destination result, and attributed review for the exact accepted artifact. |
| Derived evidence | Contract comparisons and timing calculations with a stated method and tolerance. |
| Heuristic | Sparse sampling, automated quality scores, detector events, or compatibility inferred from a similar file. Label as supporting evidence only. |
| Human judgment | Editorial quality, intelligibility, context, and visual acceptability require attributed review. |
| Not established | Universal playback, rights, long-term preservation, accessibility, or unsampled-content correctness unless separately tested. |
## Official FFmpeg sources
- [ffprobe Documentation](https://ffmpeg.org/ffprobe.html) — machine-readable structural, packet, and frame inspection.
- [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html) — stream mapping, decoding/transcoding, progress, logging, and exit behavior context.
- [FFmpeg Filters Documentation](https://ffmpeg.org/ffmpeg-filters.html) — measurable video/audio analysis filters and their parameters.
- [FFmpeg Formats Documentation](https://ffmpeg.org/ffmpeg-formats.html) — muxer/demuxer behavior and container-specific options.
- [FFmpeg Codecs Documentation](https://ffmpeg.org/ffmpeg-codecs.html) — decoder/encoder options and capabilities.
Official documentation supports the mechanics of checks. It does not define the projects acceptance criteria or guarantee behavior outside the recorded build and destination.
@@ -0,0 +1,66 @@
# Video Inspection and Visual Evidence
Inspect video in bounded layers: structural metadata, decoded technical signals, sampled frames or clips, then semantic review. Keep the source ID, stream index, timestamps, commands, build, and coverage limits attached to every artifact.
## Inspection sequence
1. **Probe the stream.** Record dimensions, sample/display aspect ratio, pixel format, field order, nominal and average frame rates, time base, start/duration, disposition, rotation/display metadata, and color fields.
2. **Define the question.** Examples: “Is there a black interval near the head?” or “Does the approved title appear in the sampled opening?” Avoid an unbounded request to “check the video.”
3. **Choose declared coverage.** Use specific timestamps, a fixed cadence, bounded intervals, or event-driven candidates. Record the interval, cadence, number of samples, and omissions.
4. **Decode evidence to a new review location.** A typical single-frame extraction is:
```sh
ffmpeg -ss START -i INPUT -map 0:v:0 -frames:v 1 -an REVIEW_FRAME.png
```
For cadence sampling, use the documented `fps` filter; for representative candidates, `thumbnail` can select a frame from each batch and `tile` can assemble a contact sheet. Preserve each samples source timestamp rather than relying only on sequential filenames.
5. **Review at the right resolution.** Contact sheets establish overview, not fine detail. Use full-resolution frames or short clips for text, motion, transitions, sync, compression artifacts, and color judgments.
Input `-ss` seeks to a nearby seek point; with transcoding, accurate seek processing normally decodes and discards material before the requested position. Container indexing, timestamps, variable frame rate, and output rounding can still affect the exact frame. Record the observed frame timestamp when precision matters.
## Technical aids
Useful video filters include:
- `showinfo` for per-frame timestamps, format, type, and checksums.
- `signalstats` for frame signal statistics.
- `blackdetect` or `blackframe` for threshold-based black candidates.
- `freezedetect` for threshold- and duration-based freeze candidates.
- `scdet` for scene-change scores and candidate events.
- `cropdetect` for suggested crop values.
Treat every detector as a candidate generator. Record thresholds, durations, filter order, and any color/range conversion performed before the detector. A detectors log line is not a semantic judgment.
## Visual review packet
For each sample include:
- opaque asset ID and selected stream;
- source and observed timestamps;
- extraction command and FFmpeg build;
- whether the image was scaled, cropped, deinterlaced, tone-mapped, or color-converted;
- the question being tested and reviewer observation;
- coverage statement and known blind spots.
Avoid embedding private source paths, personal names, faces, transcript text, or location metadata unless required and authorized. Share the smallest packet that answers the review question.
## Evidence and heuristic boundary
| Classification | Defensible statement |
|---|---|
| Direct evidence | Probe fields for the recorded stream; decoded pixels and filter measurements at the listed timestamps under the recorded command/build. |
| Threshold evidence | A configured detector emitted a candidate under stated thresholds. This is reproducible, but threshold-dependent. |
| Human observation | A reviewer observed a visible feature in the supplied sample. Attribute it and preserve the sample. |
| Heuristic | A sparse sample represents an interval; a scene score denotes an editorial cut; a crop suggestion is compositionally correct. Label and review these assumptions. |
| Not established | Absence throughout unsampled material, speaker/person identity, intent, rights, complete accessibility, exact color on another display, or downstream playback quality. |
Frame samples cannot prove what happens between samples. Extracted stills can also differ from target playback because of scaling, color management, HDR handling, deinterlacing, and display behavior.
## Official FFmpeg sources
- [ffprobe Documentation](https://ffmpeg.org/ffprobe.html) — stream/frame inspection and bounded read intervals.
- [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html) — seeking, stream mapping, frame limits, and transcoding behavior.
- [FFmpeg Filters Documentation](https://ffmpeg.org/ffmpeg-filters.html) — `fps`, `thumbnail`, `tile`, `showinfo`, `signalstats`, `blackdetect`, `freezedetect`, `scdet`, and `cropdetect`.
- [FFmpeg Utilities Documentation](https://ffmpeg.org/ffmpeg-utils.html) — time-duration and rate syntax.
The documentation supports option and filter semantics. It does not validate a chosen sampling plan, detector threshold, semantic interpretation, or display pipeline.
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Inspect audio metadata with bounded, read-only ffprobe."""
import argparse
import json
import shutil
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input")
parser.add_argument("--ffprobe", default="ffprobe")
parser.add_argument("--timeout", type=float, default=15.0)
parser.add_argument("--silence", action="store_true")
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
tool = shutil.which(args.ffprobe)
if not tool:
print(json.dumps({"ok": False, "status": "missing_tool", "error": "ffprobe not found"}))
return 3
try:
result = subprocess.run([tool, "-v", "error", "-show_format", "-show_streams", "-of", "json", args.input], capture_output=True, text=True, timeout=args.timeout)
except subprocess.TimeoutExpired:
print(json.dumps({"ok": False, "status": "timeout", "error": "ffprobe timed out"}))
return 4
if result.returncode:
print(json.dumps({"ok": False, "status": "probe_failed", "error": result.stderr.strip() or "ffprobe failed"}))
return 1
try:
document = json.loads(result.stdout)
except json.JSONDecodeError:
print(json.dumps({"ok": False, "status": "invalid_json", "error": "invalid ffprobe JSON"}))
return 1
payload = {"ok": True, "status": "ok", "input": args.input, "probe": document}
if args.silence:
payload["silence"] = {"status": "candidate_only", "note": "silence intervals require review; no editorial cut was made"}
print(json.dumps(payload, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
sys.exit(main())
+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""Extract a bounded set of explicitly requested review frames."""
from __future__ import annotations
import argparse
from decimal import Decimal, InvalidOperation
import json
import math
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
from typing import Any
MAX_FRAMES_HARD = 100
MAX_TIMESTAMP_HARD = Decimal("604800")
MAX_TIMEOUT_HARD = 300.0
MAX_DIAGNOSTIC_BYTES = 1024 * 1024
class CLIError(Exception):
def __init__(self, code: str, message: str, exit_code: int = 2, **details: Any) -> None:
super().__init__(message)
self.code = code
self.message = message
self.exit_code = exit_code
self.details = details
class JSONArgumentParser(argparse.ArgumentParser):
def error(self, message: str) -> None:
if "--json" in sys.argv[1:]:
print(json.dumps({"ok": False, "error": {"code": "invalid_arguments", "message": message}}, sort_keys=True))
raise SystemExit(2)
super().error(message)
def emit(payload: dict[str, Any], as_json: bool, *, error: bool = False) -> None:
if as_json:
print(json.dumps(payload, sort_keys=True, separators=(",", ":")))
elif error:
err = payload.get("error", {})
print(f"{err.get('code', 'error')}: {err.get('message', 'unknown error')}", file=sys.stderr)
else:
print(json.dumps(payload, sort_keys=True, indent=2))
def resolve_tool(command: str) -> str | None:
expanded = os.path.expanduser(command)
if os.path.sep in expanded or (os.path.altsep and os.path.altsep in expanded):
path = Path(expanded)
if path.is_file() and os.access(path, os.X_OK):
return str(path.resolve())
return None
return shutil.which(command)
def canonical_decimal(value: Decimal) -> str:
rendered = format(value, "f")
if "." in rendered:
rendered = rendered.rstrip("0").rstrip(".")
return "0" if rendered in {"", "-0"} else rendered
def parse_timestamp(raw: str) -> Decimal:
if len(raw) > 64:
raise CLIError("invalid_timestamp", f"timestamp is too long: {raw[:32]}…")
parts = raw.split(":")
try:
if len(parts) == 1:
value = Decimal(parts[0])
elif len(parts) == 2:
minutes = Decimal(parts[0])
seconds = Decimal(parts[1])
if minutes != minutes.to_integral_value() or minutes < 0 or not 0 <= seconds < 60:
raise InvalidOperation
value = minutes * 60 + seconds
elif len(parts) == 3:
hours = Decimal(parts[0])
minutes = Decimal(parts[1])
seconds = Decimal(parts[2])
if (
hours != hours.to_integral_value()
or minutes != minutes.to_integral_value()
or hours < 0
or not 0 <= minutes < 60
or not 0 <= seconds < 60
):
raise InvalidOperation
value = hours * 3600 + minutes * 60 + seconds
else:
raise InvalidOperation
except (InvalidOperation, ValueError):
raise CLIError("invalid_timestamp", f"invalid timestamp: {raw}") from None
if not value.is_finite() or value < 0:
raise CLIError("invalid_timestamp", f"timestamp must be finite and nonnegative: {raw}")
return value
def run_bounded(argv: list[str], timeout: float) -> tuple[int, str]:
with tempfile.TemporaryFile() as stderr_file:
try:
process = subprocess.Popen(
argv,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=stderr_file,
)
except OSError as exc:
raise CLIError("tool_start_failed", f"could not start {argv[0]}: {exc}", 3) from exc
try:
process.wait(timeout=timeout)
except subprocess.TimeoutExpired as exc:
process.kill()
process.wait()
raise CLIError("extract_timeout", f"ffmpeg exceeded {timeout:g} seconds", 4) from exc
size = stderr_file.tell()
if size > MAX_DIAGNOSTIC_BYTES:
raise CLIError(
"diagnostic_output_too_large",
f"ffmpeg stderr exceeded {MAX_DIAGNOSTIC_BYTES} bytes",
4,
stderr_bytes=size,
)
stderr_file.seek(0)
return process.returncode, stderr_file.read().decode("utf-8", errors="replace").strip()
def build_parser() -> argparse.ArgumentParser:
parser = JSONArgumentParser(description=__doc__)
parser.add_argument("input", help="source media file")
parser.add_argument("-o", "--output-dir", required=True, help="directory for extracted JPEG frames")
parser.add_argument(
"-t",
"--timestamp",
action="append",
default=[],
help="timestamp in seconds or [HH:]MM:SS; repeat for each frame",
)
parser.add_argument(
"--timestamps",
nargs="+",
default=[],
metavar="TIME",
help="additional explicit timestamps",
)
parser.add_argument("--ffmpeg", default="ffmpeg", help="ffmpeg executable (default: ffmpeg from PATH)")
parser.add_argument("--max-frames", type=int, default=24, help="maximum requested frames (1-100)")
parser.add_argument(
"--max-timestamp",
default="86400",
help="maximum allowed timestamp in seconds (default: 86400; hard maximum: 604800)",
)
parser.add_argument("--timeout", type=float, default=30.0, help="per-frame ffmpeg timeout in seconds")
parser.add_argument("--overwrite", action="store_true", help="allow replacing existing frame files")
parser.add_argument("--json", action="store_true", help="emit compact JSON")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
if not 1 <= args.max_frames <= MAX_FRAMES_HARD:
raise CLIError("invalid_limit", f"--max-frames must be between 1 and {MAX_FRAMES_HARD}")
if not math.isfinite(args.timeout) or not 0 < args.timeout <= MAX_TIMEOUT_HARD:
raise CLIError("invalid_limit", f"--timeout must be greater than 0 and at most {MAX_TIMEOUT_HARD:g}")
max_timestamp = parse_timestamp(args.max_timestamp)
if max_timestamp <= 0 or max_timestamp > MAX_TIMESTAMP_HARD:
raise CLIError(
"invalid_limit",
f"--max-timestamp must be greater than 0 and at most {canonical_decimal(MAX_TIMESTAMP_HARD)}",
)
raw_timestamps = [*args.timestamp, *args.timestamps]
if not raw_timestamps:
raise CLIError("timestamps_required", "at least one explicit --timestamp is required")
if len(raw_timestamps) > args.max_frames:
raise CLIError(
"frame_limit_exceeded",
f"requested {len(raw_timestamps)} frames; limit is {args.max_frames}",
requested=len(raw_timestamps),
limit=args.max_frames,
)
timestamps = [parse_timestamp(raw) for raw in raw_timestamps]
for raw, timestamp in zip(raw_timestamps, timestamps):
if timestamp > max_timestamp:
raise CLIError(
"timestamp_limit_exceeded",
f"timestamp {raw} exceeds limit {canonical_decimal(max_timestamp)} seconds",
timestamp=raw,
limit_seconds=canonical_decimal(max_timestamp),
)
source = Path(args.input).expanduser()
if not source.exists():
raise CLIError("input_not_found", f"input does not exist: {source}")
if not source.is_file():
raise CLIError("invalid_input", f"input is not a regular file: {source}")
source = source.resolve()
output_dir = Path(args.output_dir).expanduser().resolve()
if output_dir.exists() and not output_dir.is_dir():
raise CLIError("invalid_output_directory", f"output path is not a directory: {output_dir}")
outputs = [output_dir / f"frame-{index:04d}.jpg" for index in range(1, len(timestamps) + 1)]
existing = [str(path) for path in outputs if path.exists()]
if existing and not args.overwrite:
raise CLIError(
"output_exists",
"one or more output frames already exist; use --overwrite to replace them",
4,
paths=existing,
)
tool = resolve_tool(args.ffmpeg)
if tool is None:
raise CLIError("tool_not_found", f"ffmpeg executable not found: {args.ffmpeg}", 3, tool=args.ffmpeg)
output_dir.mkdir(parents=True, exist_ok=True)
results: list[dict[str, Any]] = []
for timestamp, output in zip(timestamps, outputs):
timestamp_text = canonical_decimal(timestamp)
command = [
tool,
"-hide_banner",
"-loglevel",
"error",
"-nostdin",
"-ss",
timestamp_text,
"-i",
str(source),
"-frames:v",
"1",
"-q:v",
"2",
"-y" if args.overwrite else "-n",
str(output),
]
returncode, diagnostic = run_bounded(command, args.timeout)
if returncode != 0:
raise CLIError(
"extract_failed",
diagnostic or f"ffmpeg exited {returncode}",
1,
returncode=returncode,
timestamp=timestamp_text,
output=str(output),
)
if not output.is_file():
raise CLIError(
"output_missing",
"ffmpeg reported success but did not create the requested frame",
1,
timestamp=timestamp_text,
output=str(output),
)
results.append(
{
"timestamp_seconds": timestamp_text,
"output": str(output),
"size_bytes": output.stat().st_size,
}
)
emit(
{
"ok": True,
"input": str(source),
"output_directory": str(output_dir),
"frame_count": len(results),
"overwrite": args.overwrite,
"frames": results,
},
args.json,
)
return 0
except CLIError as exc:
emit(
{"ok": False, "error": {"code": exc.code, "message": exc.message, **exc.details}},
args.json,
error=True,
)
return exc.exit_code
except OSError as exc:
emit(
{"ok": False, "error": {"code": "io_error", "message": str(exc)}},
args.json,
error=True,
)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""Inventory files and attach bounded ffprobe JSON metadata."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
from typing import Any, Iterator
MAX_FILES_HARD = 1000
MAX_TIMEOUT_HARD = 300.0
MAX_OUTPUT_BYTES_HARD = 16 * 1024 * 1024
class CLIError(Exception):
def __init__(self, code: str, message: str, exit_code: int = 2, **details: Any) -> None:
super().__init__(message)
self.code = code
self.message = message
self.exit_code = exit_code
self.details = details
class JSONArgumentParser(argparse.ArgumentParser):
def error(self, message: str) -> None:
if "--json" in sys.argv[1:]:
print(json.dumps({"ok": False, "error": {"code": "invalid_arguments", "message": message}}, sort_keys=True))
raise SystemExit(2)
super().error(message)
def emit(payload: dict[str, Any], as_json: bool, *, error: bool = False) -> None:
if as_json:
print(json.dumps(payload, sort_keys=True, separators=(",", ":")))
elif error:
err = payload.get("error", {})
print(f"{err.get('code', 'error')}: {err.get('message', 'unknown error')}", file=sys.stderr)
else:
print(json.dumps(payload, sort_keys=True, indent=2))
def resolve_tool(command: str) -> str | None:
expanded = os.path.expanduser(command)
if os.path.sep in expanded or (os.path.altsep and os.path.altsep in expanded):
path = Path(expanded)
if path.is_file() and os.access(path, os.X_OK):
return str(path.resolve())
return None
return shutil.which(command)
def run_bounded(argv: list[str], timeout: float, max_output_bytes: int) -> tuple[int, bytes, bytes]:
with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file:
try:
process = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=stdout_file, stderr=stderr_file)
except OSError as exc:
raise CLIError("tool_start_failed", f"could not start {argv[0]}: {exc}", 3) from exc
try:
process.wait(timeout=timeout)
except subprocess.TimeoutExpired as exc:
process.kill()
process.wait()
raise CLIError("probe_timeout", f"ffprobe exceeded {timeout:g} seconds", 4) from exc
stdout_size = stdout_file.tell()
stderr_size = stderr_file.tell()
if stdout_size > max_output_bytes or stderr_size > max_output_bytes:
raise CLIError(
"probe_output_too_large",
f"ffprobe output exceeded {max_output_bytes} bytes",
4,
stdout_bytes=stdout_size,
stderr_bytes=stderr_size,
)
stdout_file.seek(0)
stderr_file.seek(0)
return process.returncode, stdout_file.read(), stderr_file.read()
def iter_regular_files(root: Path) -> Iterator[Path]:
if root.is_file():
yield root
return
for current, dirnames, filenames in os.walk(root, followlinks=False):
dirnames[:] = sorted(name for name in dirnames if not (Path(current) / name).is_symlink())
for filename in sorted(filenames):
candidate = Path(current) / filename
if candidate.is_file():
yield candidate
def probe_file(tool: str, path: Path, timeout: float, max_output_bytes: int) -> dict[str, Any]:
argv = [
tool,
"-v",
"error",
"-print_format",
"json",
"-show_format",
"-show_streams",
str(path),
]
returncode, stdout, stderr = run_bounded(argv, timeout, max_output_bytes)
if returncode != 0:
message = stderr.decode("utf-8", errors="replace").strip()
raise CLIError("probe_failed", message or f"ffprobe exited {returncode}", 1, returncode=returncode)
try:
document = json.loads(stdout.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise CLIError("invalid_probe_json", f"ffprobe returned invalid JSON: {exc}", 1) from exc
if not isinstance(document, dict):
raise CLIError("invalid_probe_json", "ffprobe JSON root must be an object", 1)
return document
def build_parser() -> argparse.ArgumentParser:
parser = JSONArgumentParser(description=__doc__)
parser.add_argument("path", help="file or directory to inventory")
parser.add_argument("--ffprobe", default="ffprobe", help="ffprobe executable (default: ffprobe from PATH)")
parser.add_argument("--max-files", type=int, default=100, help="maximum files to inventory (1-1000)")
parser.add_argument("--timeout", type=float, default=15.0, help="per-file ffprobe timeout in seconds")
parser.add_argument(
"--max-output-bytes",
type=int,
default=1024 * 1024,
help="maximum stdout or stderr bytes accepted from each ffprobe call",
)
parser.add_argument("--json", action="store_true", help="emit compact JSON")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
if not 1 <= args.max_files <= MAX_FILES_HARD:
raise CLIError("invalid_limit", f"--max-files must be between 1 and {MAX_FILES_HARD}")
if not 0 < args.timeout <= MAX_TIMEOUT_HARD:
raise CLIError("invalid_limit", f"--timeout must be greater than 0 and at most {MAX_TIMEOUT_HARD:g}")
if not 1024 <= args.max_output_bytes <= MAX_OUTPUT_BYTES_HARD:
raise CLIError(
"invalid_limit",
f"--max-output-bytes must be between 1024 and {MAX_OUTPUT_BYTES_HARD}",
)
source = Path(args.path).expanduser()
if not source.exists():
raise CLIError("input_not_found", f"input does not exist: {source}")
if not source.is_file() and not source.is_dir():
raise CLIError("invalid_input", f"input is not a regular file or directory: {source}")
source = source.resolve()
tool = resolve_tool(args.ffprobe)
if tool is None:
raise CLIError("tool_not_found", f"ffprobe executable not found: {args.ffprobe}", 3, tool=args.ffprobe)
selected: list[Path] = []
truncated = False
for candidate in iter_regular_files(source):
if len(selected) == args.max_files:
truncated = True
break
selected.append(candidate.resolve())
entries: list[dict[str, Any]] = []
failures = 0
for candidate in selected:
stat = candidate.stat()
entry: dict[str, Any] = {
"path": str(candidate),
"size_bytes": stat.st_size,
"mtime_ns": stat.st_mtime_ns,
}
try:
entry["ffprobe"] = probe_file(tool, candidate, args.timeout, args.max_output_bytes)
entry["probe_ok"] = True
except CLIError as exc:
failures += 1
entry["probe_ok"] = False
entry["probe_error"] = {"code": exc.code, "message": exc.message, **exc.details}
entries.append(entry)
payload = {
"ok": failures == 0,
"schema_version": 1,
"root": str(source),
"limits": {
"max_files": args.max_files,
"timeout_seconds": args.timeout,
"max_output_bytes": args.max_output_bytes,
},
"file_count": len(entries),
"probe_failures": failures,
"truncated": truncated,
"files": entries,
}
emit(payload, args.json)
return 0 if failures == 0 else 1
except CLIError as exc:
error = {"code": exc.code, "message": exc.message, **exc.details}
emit({"ok": False, "error": error}, args.json, error=True)
return exc.exit_code
except OSError as exc:
emit(
{"ok": False, "error": {"code": "io_error", "message": str(exc)}},
args.json,
error=True,
)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Compare two ffprobe JSON documents against basic media criteria."""
import argparse
import json
import sys
from pathlib import Path
def load(path):
try:
return json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(str(exc)) from exc
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_probe")
parser.add_argument("output_probe")
parser.add_argument("--duration-tolerance", type=float, default=0.1)
parser.add_argument("--require-subtitles", action="store_true")
args = parser.parse_args()
try:
before = load(args.input_probe)
after = load(args.output_probe)
except ValueError as exc:
print(json.dumps({"ok": False, "status": "invalid_probe", "error": str(exc)}))
return 2
before_streams = before.get("streams", [])
after_streams = after.get("streams", [])
checks = [{"criterion": "output_has_streams", "passed": bool(after_streams)}]
for stream_type in ("video", "audio"):
before_stream = next((s for s in before_streams if s.get("codec_type") == stream_type), None)
after_stream = next((s for s in after_streams if s.get("codec_type") == stream_type), None)
if before_stream:
checks.append({"criterion": f"{stream_type}_stream", "passed": after_stream is not None})
if after_stream and before_stream.get("codec_name") and after_stream.get("codec_name"):
checks.append({"criterion": f"{stream_type}_codec", "passed": before_stream["codec_name"] == after_stream["codec_name"], "input": before_stream["codec_name"], "output": after_stream["codec_name"]})
if args.require_subtitles:
checks.append({"criterion": "subtitle_stream", "passed": any(s.get("codec_type") == "subtitle" for s in after_streams)})
try:
input_duration = float(before.get("format", {}).get("duration"))
output_duration = float(after.get("format", {}).get("duration"))
except (TypeError, ValueError):
input_duration = output_duration = None
if input_duration is not None and output_duration is not None:
checks.append({"criterion": "duration", "passed": abs(input_duration - output_duration) <= args.duration_tolerance, "input": input_duration, "output": output_duration, "tolerance": args.duration_tolerance})
ok = all(check["passed"] for check in checks)
print(json.dumps({"ok": ok, "status": "pass" if ok else "fail", "checks": checks}, indent=2, sort_keys=True))
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
import argparse,json,math,sys
from pathlib import Path
def main():
p=argparse.ArgumentParser(description=__doc__); p.add_argument("edl"); p.add_argument("--output",default="output.mp4"); a=p.parse_args()
try: d=json.loads(Path(a.edl).read_text())
except (OSError,json.JSONDecodeError) as e: print(json.dumps({"ok":False,"error":str(e)})); return 2
if not isinstance(d,dict) or d.get("schema_version")!=1: print(json.dumps({"ok":False,"error":"schema_version must be 1"})); return 2
events=d.get("events",[])
if not isinstance(events,list) or not events: print(json.dumps({"ok":False,"error":"events must be a non-empty array"})); return 2
sources=d.get("sources")
if not isinstance(sources,list) or len(sources)!=1 or len(events)!=1: print(json.dumps({"ok":False,"error":"render-edl supports exactly one source and one event"})); return 2
src={x.get("asset_id"):x for x in sources if isinstance(x,dict)}; out=[]; last=-1
for i,e in enumerate(events):
if not isinstance(e,dict) or e.get("asset_id") not in src: print(json.dumps({"ok":False,"error":f"invalid source at event {i}"})); return 2
x,y=e.get("in"),e.get("out"); dur=src[e["asset_id"]].get("duration")
if not all(isinstance(v,(int,float)) and not isinstance(v,bool) and math.isfinite(v) for v in (x,y)) or x<0 or y<=x or x<last or isinstance(dur,(int,float)) and y>dur: print(json.dumps({"ok":False,"error":f"invalid interval at event {i}"})); return 2
last=y; out.append({"asset_id":e["asset_id"],"in":x,"out":y,"action":e.get("action","keep")})
argv=["ffmpeg","-n"]
for e in out: argv += ["-ss",str(e["in"]),"-to",str(e["out"]),"-i",str(src[e["asset_id"]].get("source",e["asset_id"]))]
argv += ["-map","0:v:0?","-map","0:a:0?","-c","copy",a.output]
print(json.dumps({"ok":True,"executed":False,"events":out,"argv":argv,"output":a.output},indent=2)); return 0
if __name__=="__main__": sys.exit(main())
+209
View File
@@ -0,0 +1,209 @@
"""Deterministic subprocess tests for the FFmpeg media workflow scripts."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
SCRIPTS = Path(__file__).resolve().parent
def run_script(name: str, *arguments: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(SCRIPTS / name), *arguments],
capture_output=True,
text=True,
check=False,
)
def write_json(path: Path, value: object) -> Path:
path.write_text(json.dumps(value))
return path
def probe(*stream_types: str, duration: str = "5.0") -> dict[str, object]:
return {
"streams": [
{"index": index, "codec_type": stream_type}
for index, stream_type in enumerate(stream_types)
],
"format": {"duration": duration},
}
def test_render_edl_valid_plan_does_not_execute(tmp_path: Path) -> None:
source = tmp_path / "source.mp4"
source.write_bytes(b"")
output = tmp_path / "rendered.mp4"
edl = write_json(
tmp_path / "valid-edl.json",
{
"schema_version": 1,
"sources": [
{
"asset_id": "camera-a",
"source": str(source),
"duration": 10.0,
}
],
"events": [
{"asset_id": "camera-a", "in": 1.25, "out": 3.5, "action": "keep"}
],
},
)
result = run_script("render-edl", str(edl), "--output", str(output))
assert result.returncode == 0, result.stderr
report = json.loads(result.stdout)
assert report == {
"ok": True,
"executed": False,
"events": [
{"asset_id": "camera-a", "in": 1.25, "out": 3.5, "action": "keep"}
],
"argv": [
"ffmpeg",
"-n",
"-ss",
"1.25",
"-to",
"3.5",
"-i",
str(source),
"-map",
"0:v:0?",
"-map",
"0:a:0?",
"-c",
"copy",
str(output),
],
"output": str(output),
}
assert not output.exists()
def test_render_edl_rejects_multi_source_plan(tmp_path: Path) -> None:
edl = write_json(
tmp_path / "multi-source-edl.json",
{
"schema_version": 1,
"sources": [
{"asset_id": "camera-a", "source": "camera-a.mp4", "duration": 2.0},
{"asset_id": "camera-b", "source": "camera-b.mp4", "duration": 2.0},
],
"events": [{"asset_id": "camera-a", "in": 0.0, "out": 1.0}],
},
)
result = run_script("render-edl", str(edl))
assert result.returncode == 2
assert json.loads(result.stdout) == {
"ok": False,
"error": "render-edl supports exactly one source and one event",
}
def test_render_edl_rejects_multi_event_plan(tmp_path: Path) -> None:
edl = write_json(
tmp_path / "multi-event-edl.json",
{
"schema_version": 1,
"sources": [
{"asset_id": "camera-a", "source": "camera-a.mp4", "duration": 3.0}
],
"events": [
{"asset_id": "camera-a", "in": 0.0, "out": 1.0},
{"asset_id": "camera-a", "in": 1.0, "out": 2.0},
],
},
)
result = run_script("render-edl", str(edl))
assert result.returncode == 2
assert json.loads(result.stdout) == {
"ok": False,
"error": "render-edl supports exactly one source and one event",
}
def test_render_edl_rejects_invalid_interval(tmp_path: Path) -> None:
edl = write_json(
tmp_path / "invalid-edl.json",
{
"schema_version": 1,
"sources": [{"asset_id": "camera-a", "duration": 2.0}],
"events": [{"asset_id": "camera-a", "in": 1.0, "out": 3.0}],
},
)
result = run_script("render-edl", str(edl))
assert result.returncode == 2
assert json.loads(result.stdout) == {
"ok": False,
"error": "invalid interval at event 0",
}
def test_audio_inspect_reports_missing_ffprobe(tmp_path: Path) -> None:
media = tmp_path / "audio.wav"
media.write_bytes(b"")
missing_ffprobe = tmp_path / "missing-ffprobe"
result = run_script(
"audio-inspect",
str(media),
"--ffprobe",
str(missing_ffprobe),
)
assert result.returncode == 3
assert json.loads(result.stdout) == {
"ok": False,
"status": "missing_tool",
"error": "ffprobe not found",
}
def test_media_verify_passes_matching_probe_files(tmp_path: Path) -> None:
input_probe = write_json(tmp_path / "input-probe.json", probe("video", "audio"))
output_probe = write_json(tmp_path / "output-probe.json", probe("video", "audio"))
result = run_script("media-verify", str(input_probe), str(output_probe))
assert result.returncode == 0, result.stderr
report = json.loads(result.stdout)
assert report["ok"] is True
assert {check["criterion"]: check["passed"] for check in report["checks"]} == {
"output_has_streams": True,
"video_stream": True,
"audio_stream": True,
"duration": True,
}
def test_media_verify_fails_probe_contract_mismatches(tmp_path: Path) -> None:
input_probe = write_json(tmp_path / "input-probe.json", probe("video", "audio"))
output_probe = write_json(
tmp_path / "output-probe.json",
probe("video", duration="5.5"),
)
result = run_script("media-verify", str(input_probe), str(output_probe))
assert result.returncode == 1
report = json.loads(result.stdout)
assert report["ok"] is False
assert {check["criterion"]: check["passed"] for check in report["checks"]} == {
"output_has_streams": True,
"video_stream": True,
"audio_stream": False,
"duration": False,
}
+22
View File
@@ -0,0 +1,22 @@
{
"schema_version": 1,
"timebase": "seconds",
"sources": [{"asset_id": "asset-001", "duration": 120.0}],
"events": [
{
"id": "event-001",
"action": "keep",
"asset_id": "asset-001",
"stream_refs": ["0:v:0", "0:a:0"],
"in": 10.0,
"out": 25.0,
"reason": "<editorial reason>",
"evidence": [{"type": "transcript", "locator": "00:10-00:25", "confidence": 0.8}],
"boundary_precision": "not_verified",
"treatment": {"transition": null, "audio_fade": true},
"review_status": "needs_review",
"verification": []
}
],
"output": {"mapping": ["video", "audio"], "expected_duration": 15.0, "tolerance_seconds": 0.1}
}
@@ -0,0 +1,26 @@
# Media Acceptance Report
- Input identity and digest:
- Output path and digest:
- Exact render command:
- FFmpeg/ffprobe version:
## Criteria
| Criterion | Evidence command/artifact | Boundary | Verdict |
|---|---|---|---|
| Output decodes | | component | PASS/FAIL/BLOCKED |
| Streams/codecs/mapping | | component | PASS/FAIL/BLOCKED |
| Timing/duration/sync | | integration | PASS/FAIL/BLOCKED |
| Visual boundaries/captions | | editorial | PASS/FAIL/BLOCKED |
| Audio levels/listening | | editorial | PASS/FAIL/BLOCKED |
| Downstream consumer | | end-to-end | PASS/FAIL/BLOCKED |
## Unverified Boundaries
-
## Decision
- Overall verdict:
- Follow-up trigger:
+26
View File
@@ -0,0 +1,26 @@
{
"schema_version": 1,
"assets": [
{
"id": "asset-001",
"source": "input.mp4",
"source_digest": "sha256:<record-after-intake>",
"probe": {
"command": "ffprobe -v error -show_format -show_streams -of json input.mp4",
"captured_at": "<UTC timestamp>",
"result_path": "<task-local path>"
}
}
],
"output_contract": {
"purpose": "<editorial purpose>",
"container": "mp4",
"video_codec": "<required or null>",
"audio_codec": "<required or null>",
"downstream_consumer": "<player, editor, host, archive, or other>",
"acceptance_thresholds": ["<measurable condition>"]
},
"preservation": {"originals_untouched": true, "overwrite_policy": "refuse"},
"privacy_boundary": "<where task-local media and evidence must remain>",
"assumptions": ["<explicit unknown>"]
}
+29
View File
@@ -0,0 +1,29 @@
# Podcast Edit Plan
## Contract
- Episode/source ID:
- Intended listener and destination:
- Output format/loudness requirements:
- Original preservation path:
## Evidence
- Transcript and timing quality:
- Audio probe:
- Waveform/silence candidate method:
- Listening review segments:
## Decisions
| Range | Action | Reason | Evidence | Confidence | Review |
|---|---|---|---|---|---|
| | keep/remove/shorten/treat | | | | |
## Safety
- Meaningful pauses preserved:
- Speech/breath/room-tone policy:
- Handles and fades:
- Clipping/noise policy:
- Missing measurement or playback evidence:
@@ -0,0 +1,14 @@
# FFmpeg Research Experiment Record
- Question/claim:
- Evidence class: technical contract / reproduced behavior / heuristic / local observation
- Official source URLs:
- FFmpeg/ffprobe version and build:
- Input identity or synthetic generator:
- Exact command:
- Expected result:
- Observed result:
- Output probe:
- Downstream check:
- Limitations and untested variants:
- Verdict: verified / partial / contradicted / unverified
@@ -0,0 +1,38 @@
# Video Inspection Report
## Identity and Contract
- Asset ID:
- Source digest:
- Editorial objective:
- Downstream consumer:
- Privacy/authorization boundary:
## Technical Probe
- Command:
- FFmpeg/ffprobe version/build:
- Container:
- Streams, codecs, dimensions, frame rate, sample rate, channels:
- Start times, durations, time bases, VFR/CFR:
## Bounded Visual Evidence
- Sampling rule and rationale:
- Sample count/size limit:
- Sample timestamps:
- Frame/contact-sheet artifact:
- Vision or human reviewer:
- Coverage limitation: samples describe sampled times only.
## Candidates and Unknowns
| Time | Candidate observation | Evidence class | Confidence | Needs review |
|---|---|---|---|---|
| | | | | |
## Decision
- Proposed next step:
- Unresolved assumptions:
- Do not render until:
+22
View File
@@ -0,0 +1,22 @@
# Visual Review Packet
## Coverage
- Source asset ID:
- Sampling timestamps:
- Sampling method and bounds:
- Review target:
- Coverage statement: this packet represents only the listed timestamps and neighboring windows.
## Observations
| Timestamp | Artifact | Observation | Reviewer | Evidence class | Confidence |
|---|---|---|---|---|---|
| | | | | | |
## Editorial Consequence
- Candidate cut/join:
- Continuity concerns:
- Missing evidence:
- Human decision required: