Metadata-Version: 2.4
Name: media-gate
Version: 0.1.0
Summary: Fail-closed media QC: detects black video, silent audio, and duplicate images before you publish.
Author: Luan Ribeiro de Brito
License: MIT
Project-URL: Homepage, https://github.com/luandv92/media-gate
Project-URL: Issues, https://github.com/luandv92/media-gate/issues
Keywords: ffmpeg,video,qc,quality-assurance,content-pipeline,phash,duplicate-detection
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Pillow>=9.0
Requires-Dist: imagehash>=4.3
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# media-gate

Fail-closed media QC for content pipelines: catches **black video**, **silent
audio**, and **duplicate/near-duplicate images** before they go out the door.

```
$ media-gate check output/final_render.mp4
FAIL: final_render.mp4
  FAIL: SILENT_AUDIO — mean_volume -61.2 dB (at/below -50.0 dB threshold)
  PASS: no black intervals detected
```

## Why this exists

If you run any kind of automated content pipeline — video generation,
faceless-channel automation, bulk image export, TTS narration — you will,
eventually, publish something broken. Not because the pipeline is bad, but
because silent failures are the default failure mode of media processing:

- A TTS/rendering step silently drops the audio track and the video *looks*
  fine in a file browser (correct duration, correct thumbnail) right up until
  someone presses play.
- A render step crashes partway through and produces a video that is
  technically valid (openable, correct container) but is just black frames.
- A "new" asset is actually the same image re-exported, resized, or
  re-encoded from something already published — byte-different, but visually
  identical. A checksum won't catch it; a platform's spam/dedup detection
  will.

Each of these is invisible to a naive `if file exists and file size > 0`
check, and each one is exactly the kind of thing that erodes trust with an
audience or gets an account flagged. `media-gate` gives you a small,
dependency-light, scriptable tool to run as a pre-publish gate so these
never make it out silently.

**Fail-closed by design**: if a check can't produce a confident answer (e.g.
ffmpeg is missing, or a video's duration can't be read), the result leans
toward failure/warning rather than a silent pass. The entire point of a QC
gate is defeated if "I couldn't check" is treated the same as "it's fine."

## Install

```bash
pip install media-gate
```

This installs the Python dependencies (`Pillow`, `imagehash`). The video
checks additionally require **ffmpeg** and **ffprobe** on your `PATH`:

- Windows: https://ffmpeg.org/download.html or `winget install Gyan.FFmpeg`
- macOS: `brew install ffmpeg`
- Debian/Ubuntu: `apt install ffmpeg`

## CLI usage

```bash
# Video: checks for black frames + silent/missing audio
media-gate check my_video.mp4

# Image: computes a perceptual hash, and (optionally) compares it against
# already-published images to catch near-duplicates
media-gate check new_thumbnail.png --compare-against published/*.png

# Machine-readable output for use in scripts/CI
media-gate check my_video.mp4 --json
echo $?   # 0 = PASS, 1 = FAIL, 2 = error (file not found, bad args, ...)
```

Useful flags (see `media-gate check --help` for the full list):

| Flag | Default | Meaning |
|---|---|---|
| `--silence-threshold-db` | `-50.0` | mean_volume at/below this dB counts as silent |
| `--max-black-fraction` | `0.20` | fail if more than this fraction of the video is black |
| `--hash-algorithm` | `phash` | perceptual hash algorithm (`phash`, `dhash`, `ahash`, `whash`) |
| `--max-hamming-distance` | `6` | bit distance at/below which two images count as near-duplicates |

Exit codes are designed for shell/CI gating: `0` = PASS, non-zero = do not
publish.

## Library usage

```python
from media_gate import dedup, video_qc

# --- video ---
audio = video_qc.check_silent_audio("clip.mp4")
black = video_qc.check_black_frames("clip.mp4")
if not audio.passed or not black.passed:
    raise RuntimeError(f"QC failed: {audio.reason} / {black.reason}")

# --- images ---
known = dedup.build_registry(["published/a.png", "published/b.png"])
matches = dedup.find_duplicates("new/candidate.png", known)
if matches:
    fp, distance = matches[0]
    print(f"near-duplicate of {fp.path} (hamming distance {distance})")
```

## How the checks work

**Black video** — runs ffmpeg's `blackdetect` video filter
(`-vf blackdetect=d=...:pic_th=...:pix_th=...`) and parses the
`black_start`/`black_end`/`black_duration` lines it writes to stderr. Fails
if the total black duration exceeds `--max-black-fraction` of the video's
total length.

**Silent audio** — first checks (via `ffprobe`) that the file has an audio
stream at all (a missing stream is `NO_AUDIO_TRACK`, a distinct and common
failure mode from a present-but-silent track). Then runs ffmpeg's
`volumedetect` audio filter and parses `mean_volume`/`max_volume` from
stderr; a mean volume at or below the threshold is `SILENT_AUDIO`.

**Duplicate images** — computes a perceptual hash (default: `phash`, via the
`imagehash` library) for each image and compares hashes by Hamming distance.
Unlike a checksum, this catches the same image re-encoded, resized, or
lightly edited — the actual failure mode automated pipelines produce, since
two independent generations are almost never byte-identical but are often
visually identical.

## Development

```bash
git clone https://github.com/luandv92/media-gate
cd media-gate
pip install -e ".[dev]"
pytest
```

## License

MIT — see [LICENSE](LICENSE).
