Files
remove-ai-watermarks/src/remove_ai_watermarks/video_visible.py
T
Victor KuznetsovandClaude Opus 5 78d9e81d0f Collapse the duplicated detection path and lift the image pipeline into the library
The visible-mark path had grown three copies of one ladder sweep, four
near-identical `detect` arms, and four hand-rolled `footprint_mask` overrides;
mark knowledge sat in five hand-maintained tables across three modules; and the
flagship `all`/`batch` pipeline existed only in cli.py, written twice with
divergent behavior.

Detection is now one measurement. `_ladder_best` replaces the three sweeps,
`_scan`/`_verdict` replace the four arms, and the winning box travels to the
mask on `TextMarkDetection.match_box` instead of being swept a second time.
`detect_both` returns the strict and relaxed verdicts from one scan, which
halves the arbiter's perception cost (260 -> 130 matchTemplate calls on a 2048²
image, verdicts identical field for field). A per-mark demotion goes in the new
`_post_gate` hook, never in a `detect` override -- an override is invisible to
the single-pass path, which is how the RunningHub and Yuanbao anchor gates
briefly stopped applying.

Everything about a mark is now one registry row: product, label regime, the
platform sentence `identify` reports, the metadata signals that confirm it, and
its TC260 producer codes. `identify._VISIBLE_MARK_PLATFORM`, the signal mapping
in `api.visible_provenance`, `_PRODUCT_OF` and the pill veto are derived from
those rows.

`api.remove_all` / `api.remove_batch` are the library form of the `all` and
`batch` commands; the CLI is a wrapper that owns console text and exit codes.
Progress is a `(stage, detail)` pair of stable tokens, so the CLI keys its
wording off structure rather than parsing the library's prose back.

Two intentional behavior changes, both verified against a recorded 811-image
sample of detector verdicts, removal-mask hashes, arbiter decisions and
`identify` reports:

  * A TC260 label now relaxes the vendor its `ContentProducer` names rather than
    ByteDance's pair on every China-AIGC image. 333 of 811 samples move; on 185
    of them the previously relaxed pair was simply the wrong vendor, and the
    mark actually present never reached the relaxed gate its own
    `provenance_ncc_factor` was calibrated for.
  * A confident LibLibAI detection suppresses the Jimeng pill, like every other
    TC260 product's mark. It was registered alongside RunningHub and Baidu, both
    of which were added to the hand-written veto list, and it was not. 1 sample
    moves, and it is exactly the co-firing case.

Nothing else in that record changes: detector verdicts, mask hashes and
`identify` verdicts are byte-identical, and all 200 calibration constants are
untouched.

Also: `aigc_label` and friends plus `extract_c2pa_info` are memoized on
(path, mtime_ns, size) -- size because this package rewrites in place; the
native TC260 container readers route on magic bytes instead of the file
extension, so a mislabeled AVI or FLV is no longer invisible; `identify` shares
one pixel decode between the DWT-DCT and visible stages (TrustMark keeps its own
Pillow decode, which is not substitutable); and the six `stabilize_*` video
wrappers collapse into one policy table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:49:45 -07:00

1461 lines
53 KiB
Python

"""Visible AI-watermark localization and removal for video.
Supported marks use fully synthetic silhouettes made from geometric primitives,
OpenCV's built-in font, and Pillow's bundled font. Sora detection searches the
full frame because the wordmark moves. Veo detection covers both the current
four-point diamond and legacy ``Veo`` text. Seedance detects the boxed ``AI``
label, Dola detects its compact text label, Hailuo detects the composite
MINIMAX/Hailuo label, and Kling detects its version-independent wordmark core.
A single frame is never enough to authorize removal: the temporal arbiter
requires the candidate to recur at the same location across adjacent frames.
This keeps isolated lookalikes in clean videos from becoming removal masks.
Video pixels are decoded with OpenCV and encoded with the system ``ffmpeg``.
Variable frame timestamps cross the pipe in a PyAV-muxed NUT stream; uniform
inputs retain the cheaper raw-BGR pipe. Complete audio is stream-copied from
the source. The video stream must be transcoded because visible-mark removal
changes pixels.
"""
# cv2/numpy boundary: these packages do not expose usable types for many array
# operations. Public signatures remain annotated while unknown third-party types
# are relaxed only in this module.
# pyright: reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownVariableType=false, reportUnknownParameterType=false, reportMissingTypeArgument=false, reportMissingTypeStubs=false, reportMissingImports=false, reportArgumentType=false, reportAssignmentType=false, reportReturnType=false, reportCallIssue=false, reportIndexIssue=false, reportOperatorIssue=false, reportOptionalMemberAccess=false, reportOptionalCall=false, reportOptionalSubscript=false, reportOptionalOperand=false, reportAttributeAccessIssue=false, reportPrivateImportUsage=false, reportPrivateUsage=false, reportInvalidTypeForm=false
from __future__ import annotations
import logging
from contextlib import suppress
from dataclasses import dataclass, replace
from fractions import Fraction
from functools import lru_cache
from itertools import pairwise
from typing import TYPE_CHECKING, Any, Literal
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from remove_ai_watermarks.video import VIDEO_VISIBLE_MARKS
from remove_ai_watermarks.video_encoding import (
abort_raw_video_encoder,
finish_raw_video_encoder,
mux_encoded_video,
probe_video_encode_profile,
probe_video_timestamps,
raw_video_command,
staged_video_output,
start_raw_video_encoder,
)
from remove_ai_watermarks.video_temporal import stabilize_filled_frame
if TYPE_CHECKING:
from pathlib import Path
from numpy.typing import NDArray
from remove_ai_watermarks.watermark_registry import Backend
log = logging.getLogger(__name__)
Region = tuple[int, int, int, int]
_NORMALIZED_SHORT_SIDE = 480
_SORA_TEMPLATE_SIZE = (180, 64)
_SORA_RELATIVE_HEIGHTS = (0.065, 0.075, 0.085, 0.095, 0.105)
_SORA_PROVENANCE_WEAK_CONFIDENCE = 0.58
_SORA_STRICT_WEAK_CONFIDENCE = 0.60
_SORA_STRONG_CONFIDENCE = 0.65
_VEO_PROVENANCE_WEAK_CONFIDENCE = 0.45
_VEO_STRICT_WEAK_CONFIDENCE = 0.50
_VEO_STRONG_CONFIDENCE = 0.55
_SEEDANCE_WEAK_CONFIDENCE = 0.38
_SEEDANCE_STRONG_CONFIDENCE = 0.43
_DOLA_PROVENANCE_WEAK_CONFIDENCE = 0.48
_DOLA_STRICT_WEAK_CONFIDENCE = 0.50
_DOLA_STRONG_CONFIDENCE = 0.52
_HAILUO_WEAK_CONFIDENCE = 0.30
_HAILUO_STRONG_CONFIDENCE = 0.34
_KLING_WEAK_CONFIDENCE = 0.20
_KLING_STRONG_CONFIDENCE = 0.24
_KLING_MIN_WHITE_FRACTION = 0.02
_MIN_STABLE_FRAMES = 5
_MIN_VEO_STABLE_FRAMES = 12
_MIN_FIXED_MARK_STABLE_FRAMES = 12
_MAX_STABLE_GAP = 2
_STABLE_IOU = 0.55
_VEO_REFERENCE_SHORT_SIDE = 720
_VEO_DIAMOND_PROFILES = (
(56, 92, 92),
(48, 72, 72),
(44, 29, 40),
)
_DOLA_RELATIVE_HEIGHTS = tuple(value / 1000 for value in range(22, 41))
_HAILUO_RELATIVE_HEIGHTS = tuple(value / 1000 for value in range(28, 56, 3))
_KLING_RELATIVE_HEIGHTS = tuple(value / 1000 for value in range(24, 49, 3))
_HDR_TRANSFERS = frozenset({"smpte2084", "arib-std-b67"})
@dataclass(frozen=True)
class FrameLocalization:
"""Best untrusted visible-mark candidate found in one decoded frame."""
frame_index: int
confidence: float
region: Region | None
@dataclass(frozen=True)
class VideoScan:
"""Decoded video geometry plus one localization candidate per frame."""
width: int
height: int
fps: float
detections: tuple[FrameLocalization, ...]
timestamps: tuple[float, ...] = ()
@dataclass(frozen=True)
class _PreparedFrame:
"""Frame representations shared by every detector in one scan pass."""
gray: NDArray[Any]
normalized_gray: NDArray[Any]
normalized_scale: float
def _scalable_default_font(size: int) -> ImageFont.ImageFont | ImageFont.FreeTypeFont:
"""Load Pillow's bundled scalable font, with a Pillow 10.0 fallback."""
try:
return ImageFont.load_default(size=size)
except TypeError:
# ``size=`` was added after Pillow 10.0, which is still within the
# package's supported dependency range. Resize that bundled bitmap font
# before compositing it into the synthetic template.
return ImageFont.load_default()
def _crop_nonzero(image: NDArray[Any]) -> NDArray[Any]:
"""Crop a synthetic template to its nonzero footprint."""
ys, xs = np.where(image > 0)
if len(xs) == 0:
return image
return image[ys.min() : ys.max() + 1, xs.min() : xs.max() + 1]
@lru_cache(maxsize=1)
def _sora_templates() -> tuple[NDArray[Any], NDArray[Any]]:
"""Return synthetic full-wordmark and mascot-only silhouettes.
No source frame or provider logo asset contributes pixels to these templates.
The cloud-like mascot is assembled from primitive shapes and the word is
rendered with Pillow's bundled font.
"""
width, height = _SORA_TEMPLATE_SIZE
canvas = Image.new("L", (width, height), 0)
draw = ImageDraw.Draw(canvas)
draw.rounded_rectangle((4, 8, 58, 57), radius=22, fill=255)
draw.ellipse((0, 18, 26, 50), fill=255)
draw.ellipse((38, 16, 64, 51), fill=255)
draw.ellipse((16, 18, 29, 43), fill=0)
draw.ellipse((35, 18, 48, 43), fill=0)
font = _scalable_default_font(50)
if isinstance(font, ImageFont.FreeTypeFont):
draw.text((67, 0), "Sora", font=font, fill=255, stroke_width=1, stroke_fill=255)
else:
text_box = font.getbbox("Sora")
text = Image.new("L", (max(1, text_box[2]), max(1, text_box[3])), 0)
ImageDraw.Draw(text).text((0, 0), "Sora", font=font, fill=255)
text = text.resize((102, 50), Image.Resampling.NEAREST)
canvas.paste(text, (67, 0), text)
full = np.asarray(canvas, dtype=np.uint8)
return full, full[:, :64]
@lru_cache(maxsize=1)
def _veo_templates() -> tuple[NDArray[Any], NDArray[Any]]:
"""Return synthetic current-diamond and legacy-text Veo silhouettes."""
size = 256
diamond_canvas = Image.new("L", (size, size), 0)
diamond_points = (
(0.50, 0.02),
(0.60, 0.39),
(0.98, 0.50),
(0.60, 0.61),
(0.50, 0.98),
(0.40, 0.61),
(0.02, 0.50),
(0.40, 0.39),
)
ImageDraw.Draw(diamond_canvas).polygon(
[(round(x * size), round(y * size)) for x, y in diamond_points],
fill=255,
)
text_canvas = Image.new("L", (140, 60), 0)
text_draw = ImageDraw.Draw(text_canvas)
text_draw.text((2, 0), "Veo", font=_scalable_default_font(48), fill=255)
text = _crop_nonzero(np.asarray(text_canvas, dtype=np.uint8))
return np.asarray(diamond_canvas, dtype=np.uint8), text
@lru_cache(maxsize=1)
def _seedance_template() -> NDArray[Any]:
"""Return a synthetic boxed-AI silhouette for Seedance exports."""
canvas = Image.new("L", (160, 120), 0)
draw = ImageDraw.Draw(canvas)
draw.rounded_rectangle((8, 8, 142, 105), radius=28, outline=255, width=7)
draw.text(
(35, 17),
"AI",
font=_scalable_default_font(70),
fill=255,
stroke_width=1,
stroke_fill=255,
)
draw.rounded_rectangle((142, 94, 157, 109), radius=3, outline=255, width=2)
draw.text((145, 94), "AI", font=_scalable_default_font(8), fill=255)
return np.asarray(canvas, dtype=np.uint8)
@lru_cache(maxsize=1)
def _dola_template() -> NDArray[Any]:
"""Return a synthetic Dola AI text silhouette using OpenCV's font."""
canvas = np.zeros((100, 400), dtype=np.uint8)
cv2.putText(
canvas,
"Dola AI",
(2, 72),
cv2.FONT_HERSHEY_DUPLEX,
2.2,
255,
3,
cv2.LINE_AA,
)
return _crop_nonzero(canvas)
@lru_cache(maxsize=1)
def _hailuo_template() -> NDArray[Any]:
"""Return a synthetic MINIMAX/Hailuo composite-label silhouette."""
canvas = Image.new("L", (680, 112), 0)
draw = ImageDraw.Draw(canvas)
# Five symmetric waveform strokes approximate the provider-independent
# geometry at the left edge without copying pixels from an export.
waveform_heights = (42, 70, 96, 70, 42)
center_y = 56
for index, height in enumerate(waveform_heights):
x = 10 + index * 13
draw.rounded_rectangle(
(x, center_y - height // 2, x + 5, center_y + height // 2),
radius=2,
fill=255,
)
font = _scalable_default_font(58)
draw.text((84, 18), "MINIMAX", font=font, fill=255, stroke_width=1, stroke_fill=255)
draw.rectangle((342, 18, 347, 92), fill=255)
# The Hailuo symbol is a ring with a small offset highlight.
draw.ellipse((370, 20, 446, 96), outline=255, width=12)
draw.ellipse((397, 38, 434, 76), fill=255)
draw.ellipse((397, 29, 420, 52), fill=0)
draw.text((456, 18), "hailuo AI", font=font, fill=255, stroke_width=1, stroke_fill=255)
return np.asarray(canvas, dtype=np.uint8)
@lru_cache(maxsize=1)
def _kling_templates() -> tuple[NDArray[Any], ...]:
"""Return synthetic font variants for the Kling wordmark core."""
canvas = Image.new("L", (430, 104), 0)
draw = ImageDraw.Draw(canvas)
draw.text(
(2, 12),
"KLING AI",
font=_scalable_default_font(64),
fill=255,
stroke_width=1,
stroke_fill=255,
)
templates = [_crop_nonzero(np.asarray(canvas, dtype=np.uint8))]
for font in (cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_DUPLEX):
cv_template = np.zeros((100, 500), dtype=np.uint8)
cv2.putText(
cv_template,
"KLING AI",
(2, 72),
font,
2.2,
255,
3,
cv2.LINE_AA,
)
templates.append(_crop_nonzero(cv_template))
return tuple(templates)
@lru_cache(maxsize=1)
def _kling_logo_template() -> NDArray[Any]:
"""Return a synthetic ring approximation of the Kling swirl."""
template = np.zeros((100, 100), dtype=np.uint8)
cv2.circle(template, (50, 50), 34, 255, 14, cv2.LINE_AA)
return _crop_nonzero(template)
def _top_hat(gray: NDArray[Any]) -> NDArray[Any]:
kernel = np.ones((7, 7), dtype=np.uint8)
return cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel)
@lru_cache(maxsize=1)
def _template_sources() -> dict[str, NDArray[Any]]:
"""Return every immutable synthetic template by detector-local key."""
sora_word, sora_icon = _sora_templates()
veo_diamond, veo_text = _veo_templates()
sources = {
"sora-word": sora_word,
"sora-icon": sora_icon,
"veo-diamond": veo_diamond,
"veo-text": veo_text,
"seedance": _seedance_template(),
"dola": _dola_template(),
"hailuo": _hailuo_template(),
"kling-logo": _kling_logo_template(),
}
sources.update({f"kling-{index}": template for index, template in enumerate(_kling_templates())})
return sources
@lru_cache(maxsize=512)
def _resized_template_feature(
template_key: str,
width: int,
height: int,
kernel_size: int,
) -> tuple[NDArray[Any], NDArray[Any]]:
"""Resize one template and cache its invariant top-hat representation."""
template = cv2.resize(
_template_sources()[template_key],
(width, height),
interpolation=cv2.INTER_AREA,
)
kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
feature = cv2.morphologyEx(template, cv2.MORPH_TOPHAT, kernel)
return template, feature
def _normalized_gray(image_bgr: NDArray[Any]) -> tuple[NDArray[Any], float]:
gray = image_bgr if image_bgr.ndim == 2 else cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
height, width = gray.shape[:2]
short_side = min(height, width)
if short_side <= 0:
return gray, 1.0
scale = min(1.0, _NORMALIZED_SHORT_SIDE / short_side)
if scale < 1.0:
gray = cv2.resize(
gray,
(max(1, round(width * scale)), max(1, round(height * scale))),
interpolation=cv2.INTER_AREA,
)
return gray, scale
def _prepare_frame(image_bgr: NDArray[Any]) -> _PreparedFrame:
"""Compute the shared grayscale representations for one decoded frame."""
gray = image_bgr if image_bgr.ndim == 2 else cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
normalized_gray, normalized_scale = _normalized_gray(gray)
return _PreparedFrame(gray, normalized_gray, normalized_scale)
def _expanded_region(
location: tuple[int, int],
template_width: int,
template_height: int,
*,
icon_only: bool,
scale: float,
frame_width: int,
frame_height: int,
) -> Region:
x = round(location[0] / scale)
y = round(location[1] / scale)
height = max(1, round(template_height / scale))
width = max(1, round(template_width / scale))
if icon_only:
width = round(height * _SORA_TEMPLATE_SIZE[0] / _SORA_TEMPLATE_SIZE[1])
width = min(width, frame_width - x)
height = min(height, frame_height - y)
return x, y, max(1, width), max(1, height)
def detect_sora_frame(
image_bgr: NDArray[Any],
*,
frame_index: int = 0,
prepared: _PreparedFrame | None = None,
) -> FrameLocalization:
"""Locate the strongest synthetic Sora-wordmark match in one frame.
The returned candidate is intentionally untrusted. Call
:func:`stabilize_localizations` across the full sequence before building
any removal mask.
"""
if image_bgr.size == 0:
return FrameLocalization(frame_index, 0.0, None)
frame_height, frame_width = image_bgr.shape[:2]
prepared = prepared or _prepare_frame(image_bgr)
gray, scale = prepared.normalized_gray, prepared.normalized_scale
normalized_height, normalized_width = gray.shape[:2]
feature = _top_hat(gray)
best_confidence = 0.0
best_region: Region | None = None
for template_index, base_template in enumerate(_sora_templates()):
icon_only = template_index == 1
template_key = "sora-icon" if icon_only else "sora-word"
for relative_height in _SORA_RELATIVE_HEIGHTS:
template_height = max(16, round(min(normalized_height, normalized_width) * relative_height))
template_width = max(1, round(base_template.shape[1] * template_height / base_template.shape[0]))
if template_height >= normalized_height or template_width >= normalized_width:
continue
_, template_feature = _resized_template_feature(
template_key,
template_width,
template_height,
7,
)
scores = cv2.matchTemplate(feature, template_feature, cv2.TM_CCOEFF_NORMED)
_, confidence, _, location = cv2.minMaxLoc(scores)
if confidence <= best_confidence:
continue
best_confidence = float(confidence)
best_region = _expanded_region(
location,
template_width,
template_height,
icon_only=icon_only,
scale=scale,
frame_width=frame_width,
frame_height=frame_height,
)
return FrameLocalization(frame_index, best_confidence, best_region)
def _match_template(
gray: NDArray[Any],
template: NDArray[Any],
*,
region: Region,
kernel_size: int,
template_feature: NDArray[Any] | None = None,
) -> tuple[float, Region | None]:
"""Match one synthetic silhouette inside a bounded frame region."""
x, y, width, height = region
roi = gray[y : y + height, x : x + width]
template_height, template_width = template.shape[:2]
if roi.size == 0 or template_height >= roi.shape[0] or template_width >= roi.shape[1]:
return 0.0, None
kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
feature = cv2.morphologyEx(roi, cv2.MORPH_TOPHAT, kernel)
if template_feature is None:
template_feature = cv2.morphologyEx(template, cv2.MORPH_TOPHAT, kernel)
scores = cv2.matchTemplate(feature, template_feature, cv2.TM_CCOEFF_NORMED)
_, confidence, _, location = cv2.minMaxLoc(scores)
return float(confidence), (
x + location[0],
y + location[1],
template_width,
template_height,
)
def _restore_region(
region: Region | None,
*,
scale: float,
frame_width: int,
frame_height: int,
) -> Region | None:
"""Map a localization from normalized pixels back to the source frame."""
if region is None:
return None
x, y, width, height = region
source_x = round(x / scale)
source_y = round(y / scale)
source_width = min(frame_width - source_x, max(1, round(width / scale)))
source_height = min(frame_height - source_y, max(1, round(height / scale)))
return source_x, source_y, source_width, source_height
def _bounded_region(
x: int,
y: int,
width: int,
height: int,
*,
frame_width: int,
frame_height: int,
) -> Region:
"""Clip an expanded region to the frame without changing its anchor."""
bounded_x = max(0, x)
bounded_y = max(0, y)
return (
bounded_x,
bounded_y,
min(frame_width - bounded_x, width + x - bounded_x),
min(frame_height - bounded_y, height + y - bounded_y),
)
def _detect_fixed_mark(
image_bgr: NDArray[Any],
template_key: str,
*,
relative_heights: tuple[float, ...],
search_origin: tuple[float, float],
kernel_fraction: float,
prefer_larger_within: float = 0.0,
normalized: tuple[NDArray[Any], float] | None = None,
frame_index: int,
) -> FrameLocalization:
"""Match one fixed synthetic mark inside a normalized-frame search region."""
if image_bgr.size == 0:
return FrameLocalization(frame_index, 0.0, None)
frame_height, frame_width = image_bgr.shape[:2]
gray, scale = normalized if normalized is not None else _normalized_gray(image_bgr)
normalized_height, normalized_width = gray.shape[:2]
short_side = min(normalized_height, normalized_width)
search_x = round(normalized_width * search_origin[0])
search_y = round(normalized_height * search_origin[1])
search_region = (
search_x,
search_y,
normalized_width - search_x,
normalized_height - search_y,
)
matches: list[tuple[float, Region]] = []
base_template = _template_sources()[template_key]
for relative_height in relative_heights:
template_height = max(6, round(short_side * relative_height))
template_width = max(1, round(base_template.shape[1] * template_height / base_template.shape[0]))
kernel_size = max(3, round(template_height * kernel_fraction) | 1)
resized, template_feature = _resized_template_feature(
template_key,
template_width,
template_height,
kernel_size,
)
confidence, candidate = _match_template(
gray,
resized,
region=search_region,
kernel_size=kernel_size,
template_feature=template_feature,
)
if candidate is not None and confidence > 0:
matches.append((confidence, candidate))
if not matches:
return FrameLocalization(frame_index, 0.0, None)
best_confidence, best_region = max(matches, key=lambda match: match[0])
if prefer_larger_within > 0:
eligible = [
(confidence, candidate)
for confidence, candidate in matches
if confidence >= best_confidence - prefer_larger_within
]
best_confidence, best_region = max(
eligible,
key=lambda match: (match[1][2] * match[1][3], match[0]),
)
return FrameLocalization(
frame_index,
best_confidence,
_restore_region(
best_region,
scale=scale,
frame_width=frame_width,
frame_height=frame_height,
),
)
def detect_seedance_frame(
image_bgr: NDArray[Any],
*,
frame_index: int = 0,
prepared: _PreparedFrame | None = None,
) -> FrameLocalization:
"""Locate the strongest fixed Seedance boxed-AI candidate."""
return _detect_fixed_mark(
image_bgr,
"seedance",
relative_heights=(0.065, 0.075, 0.085, 0.095, 0.105),
search_origin=(0.68, 0.72),
kernel_fraction=0.12,
normalized=None if prepared is None else (prepared.normalized_gray, prepared.normalized_scale),
frame_index=frame_index,
)
def detect_dola_frame(
image_bgr: NDArray[Any],
*,
frame_index: int = 0,
prepared: _PreparedFrame | None = None,
) -> FrameLocalization:
"""Locate the strongest fixed Dola AI text candidate."""
return _detect_fixed_mark(
image_bgr,
"dola",
relative_heights=_DOLA_RELATIVE_HEIGHTS,
search_origin=(0.65, 0.85),
kernel_fraction=0.50,
normalized=None if prepared is None else (prepared.normalized_gray, prepared.normalized_scale),
frame_index=frame_index,
)
def detect_hailuo_frame(
image_bgr: NDArray[Any],
*,
frame_index: int = 0,
prepared: _PreparedFrame | None = None,
) -> FrameLocalization:
"""Locate the strongest fixed MINIMAX/Hailuo composite-label candidate."""
detection = _detect_fixed_mark(
image_bgr,
"hailuo",
relative_heights=_HAILUO_RELATIVE_HEIGHTS,
search_origin=(0.28, 0.76),
kernel_fraction=0.18,
normalized=None if prepared is None else (prepared.normalized_gray, prepared.normalized_scale),
frame_index=frame_index,
)
if detection.region is None:
return detection
frame_width = image_bgr.shape[1]
x, y, width, height = detection.region
horizontal_padding = round(height * 1.25)
region = _bounded_region(
x - horizontal_padding,
y,
width + horizontal_padding * 2,
height,
frame_width=frame_width,
frame_height=image_bgr.shape[0],
)
return FrameLocalization(
frame_index,
detection.confidence,
region,
)
def detect_kling_frame(
image_bgr: NDArray[Any],
*,
frame_index: int = 0,
prepared: _PreparedFrame | None = None,
) -> FrameLocalization:
"""Locate the fixed Kling wordmark core and include its version suffix."""
if image_bgr.size == 0:
return FrameLocalization(frame_index, 0.0, None)
frame_height, frame_width = image_bgr.shape[:2]
prepared = prepared or _prepare_frame(image_bgr)
normalized = prepared.normalized_gray, prepared.normalized_scale
expanded: list[FrameLocalization] = []
for template_index, _template in enumerate(_kling_templates()):
detection = _detect_fixed_mark(
image_bgr,
f"kling-{template_index}",
relative_heights=_KLING_RELATIVE_HEIGHTS,
search_origin=(0.64, 0.84),
kernel_fraction=0.18,
prefer_larger_within=0.06,
normalized=normalized,
frame_index=frame_index,
)
if detection.region is None:
continue
x, y, width, height = detection.region
left_padding = round(height * 1.8)
right_padding = round(height * 4.0)
vertical_padding = round(height * 0.4)
region = _bounded_region(
x - left_padding,
y - vertical_padding,
width + left_padding + right_padding,
height + vertical_padding * 2,
frame_width=frame_width,
frame_height=frame_height,
)
expanded.append(
FrameLocalization(
frame_index,
detection.confidence,
region,
)
)
if not expanded:
return FrameLocalization(frame_index, 0.0, None)
edge_candidates = [
candidate
for candidate in expanded
if candidate.region is not None
and candidate.region[0] + candidate.region[2] >= frame_width * 0.96
and candidate.region[1] + candidate.region[3] >= frame_height * 0.94
]
font_candidate = None if not edge_candidates else max(edge_candidates, key=lambda candidate: candidate.confidence)
logo = _detect_fixed_mark(
image_bgr,
"kling-logo",
relative_heights=tuple(value / 1000 for value in range(20, 61, 3)),
search_origin=(0.62, 0.90),
kernel_fraction=0.18,
prefer_larger_within=0.03,
normalized=normalized,
frame_index=frame_index,
)
logo_candidate: FrameLocalization | None = None
logo_x: int | None = None
if logo.region is not None and logo.confidence >= 0.44:
logo_x, logo_y, _, logo_height = logo.region
logo_candidate = FrameLocalization(
frame_index,
logo.confidence,
_bounded_region(
logo_x - round(logo_height * 0.2),
logo_y - round(logo_height * 0.25),
round(logo_height * 7.8),
round(logo_height * 1.5),
frame_width=frame_width,
frame_height=frame_height,
),
)
best = font_candidate or logo_candidate
if (
font_candidate is not None
and font_candidate.region is not None
and logo_candidate is not None
and logo_x is not None
):
font_x, _, font_width, _ = font_candidate.region
if logo_x <= font_x + round(font_width * 0.50):
best = logo_candidate
if best is None or best.region is None:
return FrameLocalization(frame_index, 0.0, None)
x, y, width, height = best.region
roi = image_bgr[y : y + height, x : x + width]
if roi.ndim == 2:
white_fraction = float(np.mean(roi >= 180))
else:
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
white_fraction = float(np.mean((hsv[:, :, 1] <= 55) & (hsv[:, :, 2] >= 180)))
if white_fraction < _KLING_MIN_WHITE_FRACTION:
return FrameLocalization(frame_index, 0.0, None)
return best
def detect_veo_frame(
image_bgr: NDArray[Any],
*,
frame_index: int = 0,
prepared: _PreparedFrame | None = None,
) -> FrameLocalization:
"""Locate the strongest current-diamond or legacy-text Veo candidate."""
if image_bgr.size == 0:
return FrameLocalization(frame_index, 0.0, None)
frame_height, frame_width = image_bgr.shape[:2]
gray = (prepared or _prepare_frame(image_bgr)).gray
short_scale = min(frame_height, frame_width) / _VEO_REFERENCE_SHORT_SIDE
_, text_base = _veo_templates()
best_confidence = 0.0
best_region: Region | None = None
diamond_sizes: set[int] = set()
for base_size, right_margin, bottom_margin in _VEO_DIAMOND_PROFILES:
diamond_size = max(16, round(base_size * short_scale))
diamond_sizes.add(diamond_size)
kernel_size = max(3, round(7 * short_scale) | 1)
template, template_feature = _resized_template_feature(
"veo-diamond",
diamond_size,
diamond_size,
kernel_size,
)
expected_x = round(frame_width - (right_margin + base_size) * short_scale)
expected_y = round(frame_height - (bottom_margin + base_size) * short_scale)
search_padding = max(6, round(diamond_size * 0.25))
search_x = max(0, expected_x - search_padding)
search_y = max(0, expected_y - search_padding)
search_width = min(frame_width - search_x, diamond_size + search_padding * 2)
search_height = min(frame_height - search_y, diamond_size + search_padding * 2)
confidence, candidate = _match_template(
gray,
template,
region=(search_x, search_y, search_width, search_height),
kernel_size=kernel_size,
template_feature=template_feature,
)
if confidence > best_confidence:
best_confidence = confidence
best_region = candidate
# Provider layouts have moved before. A bounded corner search is a safety
# net for a relocated diamond, but it is admitted only at a much stronger
# per-frame score than the known-profile path. Without this gate, recurring
# bright scene details in clean API exports can become stable false matches.
corner_x = round(frame_width * 0.65)
corner_y = round(frame_height * 0.65)
corner_region = (corner_x, corner_y, frame_width - corner_x, frame_height - corner_y)
for diamond_size in diamond_sizes:
kernel_size = max(3, round(7 * short_scale) | 1)
template, template_feature = _resized_template_feature(
"veo-diamond",
diamond_size,
diamond_size,
kernel_size,
)
confidence, candidate = _match_template(
gray,
template,
region=corner_region,
kernel_size=kernel_size,
template_feature=template_feature,
)
if confidence >= 0.70 and confidence > best_confidence:
best_confidence = confidence
best_region = candidate
text_region_width = min(frame_width, max(32, round(180 * short_scale)))
text_region_height = min(frame_height, max(24, round(120 * short_scale)))
text_region = (
frame_width - text_region_width,
frame_height - text_region_height,
text_region_width,
text_region_height,
)
text_heights = sorted({max(5, round(height * short_scale)) for height in range(8, 22)})
for text_height in text_heights:
text_width = max(1, round(text_base.shape[1] * text_height / text_base.shape[0]))
kernel_size = max(3, round(3 * short_scale) | 1)
template, template_feature = _resized_template_feature(
"veo-text",
text_width,
text_height,
kernel_size,
)
confidence, candidate = _match_template(
gray,
template,
region=text_region,
kernel_size=kernel_size,
template_feature=template_feature,
)
if confidence > best_confidence:
best_confidence = confidence
best_region = candidate
return FrameLocalization(frame_index, best_confidence, best_region)
def _region_iou(left: Region, right: Region) -> float:
lx, ly, lw, lh = left
rx, ry, rw, rh = right
x0 = max(lx, rx)
y0 = max(ly, ry)
x1 = min(lx + lw, rx + rw)
y1 = min(ly + lh, ry + rh)
intersection = max(0, x1 - x0) * max(0, y1 - y0)
union = lw * lh + rw * rh - intersection
return intersection / union if union > 0 else 0.0
@dataclass(frozen=True)
class VisibleMarkPolicy:
"""One provider's temporal-arbiter tuning, plus the fill geometry it needs.
Every value here is MEASURED per provider; the arbiter itself
(:func:`_stabilize_localizations`) is shared and knows nothing about providers.
``accepts_provenance`` is load-bearing rather than cosmetic. Hailuo and Kling have
no metadata that could confirm them, so their rows force ``provenance=False``; that
used to be guaranteed structurally by wrappers that took no ``provenance``
parameter at all, and this flag is what preserves the guarantee now that one entry
point serves every mark.
``padding_fraction`` and ``mask_style`` belong to the removal plan rather than the
arbiter, but they are per-provider constants like the rest, so they live on the same
row instead of in a parallel branch in ``video.py``.
"""
weak_floor: float
strong_floor: float
transition_floor: float
min_stable_frames: int
cover_after_confirmation: bool
padding_fraction: float
mask_style: Literal["box", "veo"]
anchor_iou: float | None = None
accepts_provenance: bool = True
# Weak floor to use when provenance confirms the vendor. None = the mark has no
# relaxed band; metadata never creates a detection, it only lets a stable visual
# run below the strict floor through.
provenance_weak_floor: float | None = None
VISIBLE_MARK_POLICIES: dict[str, VisibleMarkPolicy] = {
"sora": VisibleMarkPolicy(
weak_floor=_SORA_STRICT_WEAK_CONFIDENCE,
provenance_weak_floor=_SORA_PROVENANCE_WEAK_CONFIDENCE,
strong_floor=_SORA_STRONG_CONFIDENCE,
transition_floor=0.45,
min_stable_frames=_MIN_STABLE_FRAMES,
cover_after_confirmation=False,
padding_fraction=0.28,
mask_style="box",
),
"veo": VisibleMarkPolicy(
weak_floor=_VEO_STRICT_WEAK_CONFIDENCE,
provenance_weak_floor=_VEO_PROVENANCE_WEAK_CONFIDENCE,
strong_floor=_VEO_STRONG_CONFIDENCE,
transition_floor=0.35,
min_stable_frames=_MIN_VEO_STABLE_FRAMES,
cover_after_confirmation=True,
padding_fraction=0.18,
mask_style="veo",
),
"seedance": VisibleMarkPolicy(
# One floor at either trust level: provenance still gates the run acceptance
# inside the arbiter, but the confidence bar does not move.
weak_floor=_SEEDANCE_WEAK_CONFIDENCE,
strong_floor=_SEEDANCE_STRONG_CONFIDENCE,
transition_floor=0.30,
min_stable_frames=_MIN_FIXED_MARK_STABLE_FRAMES,
cover_after_confirmation=True,
anchor_iou=0.80,
padding_fraction=0.0,
mask_style="box",
),
"dola": VisibleMarkPolicy(
weak_floor=_DOLA_STRICT_WEAK_CONFIDENCE,
provenance_weak_floor=_DOLA_PROVENANCE_WEAK_CONFIDENCE,
strong_floor=_DOLA_STRONG_CONFIDENCE,
transition_floor=0.40,
min_stable_frames=_MIN_FIXED_MARK_STABLE_FRAMES,
cover_after_confirmation=True,
anchor_iou=0.80,
padding_fraction=0.20,
mask_style="box",
),
"hailuo": VisibleMarkPolicy(
weak_floor=_HAILUO_WEAK_CONFIDENCE,
strong_floor=_HAILUO_STRONG_CONFIDENCE,
transition_floor=0.28,
min_stable_frames=_MIN_FIXED_MARK_STABLE_FRAMES,
cover_after_confirmation=True,
anchor_iou=0.80,
padding_fraction=0.12,
mask_style="box",
accepts_provenance=False,
),
"kling": VisibleMarkPolicy(
weak_floor=_KLING_WEAK_CONFIDENCE,
strong_floor=_KLING_STRONG_CONFIDENCE,
transition_floor=0.30,
min_stable_frames=_MIN_FIXED_MARK_STABLE_FRAMES,
cover_after_confirmation=True,
anchor_iou=0.80,
padding_fraction=0.12,
mask_style="box",
accepts_provenance=False,
),
}
def stabilize_localizations(
mark: str,
detections: tuple[FrameLocalization, ...] | list[FrameLocalization],
*,
provenance: bool = False,
) -> list[Region | None]:
"""Accept only spatially/temporally recurring candidates for ``mark``.
Metadata never creates a detection. It only allows a stable visual run whose scores
remain below the strict confidence floor, which covers a low-contrast mark while
clean metadata-bearing exports stay untouched. A mark whose policy sets
``accepts_provenance=False`` ignores the argument entirely.
"""
policy = VISIBLE_MARK_POLICIES[mark]
trusted = provenance and policy.accepts_provenance
weak_floor = policy.provenance_weak_floor if (trusted and policy.provenance_weak_floor is not None) else None
return _stabilize_localizations(
detections,
provenance=trusted,
weak_floor=weak_floor if weak_floor is not None else policy.weak_floor,
strong_floor=policy.strong_floor,
transition_floor=policy.transition_floor,
min_stable_frames=policy.min_stable_frames,
cover_after_confirmation=policy.cover_after_confirmation,
**({"anchor_iou": policy.anchor_iou} if policy.anchor_iou is not None else {}),
)
def _stabilize_localizations(
detections: tuple[FrameLocalization, ...] | list[FrameLocalization],
*,
provenance: bool,
weak_floor: float,
strong_floor: float,
transition_floor: float,
min_stable_frames: int,
cover_after_confirmation: bool,
anchor_iou: float | None = None,
) -> list[Region | None]:
"""Apply the shared recurrence policy to provider-specific candidates."""
accepted: list[Region | None] = [None] * len(detections)
runs: list[list[int]] = []
current: list[int] = []
for position, detection in enumerate(detections):
if detection.region is None or detection.confidence < weak_floor:
continue
if current:
previous = detections[current[-1]]
anchor = detections[current[0]]
frame_gap = detection.frame_index - previous.frame_index
if (
previous.region is None
or anchor.region is None
or frame_gap > _MAX_STABLE_GAP + 1
or _region_iou(previous.region, detection.region) < _STABLE_IOU
or (anchor_iou is not None and _region_iou(anchor.region, detection.region) < anchor_iou)
):
runs.append(current)
current = []
current.append(position)
if current:
runs.append(current)
for run in runs:
strong = max(detections[position].confidence for position in run) >= strong_floor
if len(run) < min_stable_frames or (not provenance and not strong):
continue
for position in run:
accepted[position] = detections[position].region
for left_position, right_position in pairwise(run):
if right_position - left_position <= 1:
continue
left = detections[left_position]
right = detections[right_position]
if left.region is None or right.region is None or _region_iou(left.region, right.region) < _STABLE_IOU:
continue
for missing_position in range(left_position + 1, right_position):
distance_left = missing_position - left_position
distance_right = right_position - missing_position
accepted[missing_position] = left.region if distance_left <= distance_right else right.region
# Provider provenance plus a confirmed run establishes a continuously
# watermarked app export rather than a clean API export that merely shares
# the generator name. Veo may also cover the sequence without provenance
# after its longer, strong fixed-position run. Cover low-contrast transition
# frames with the nearest confirmed position.
confirmed_positions = [position for position, region in enumerate(accepted) if region is not None]
if (provenance or cover_after_confirmation) and confirmed_positions:
confirmed_regions = [accepted[position] for position in confirmed_positions]
carry_position = confirmed_positions[0]
for position, region in enumerate(accepted):
if region is not None:
carry_position = position
continue
raw = detections[position]
if (
raw.region is not None
and raw.confidence >= transition_floor
and any(
confirmed_region is not None and _region_iou(raw.region, confirmed_region) >= _STABLE_IOU
for confirmed_region in confirmed_regions
)
):
accepted[position] = raw.region
continue
accepted[position] = accepted[carry_position]
return accepted
def _scan_video_detectors(
source: Path,
detectors: dict[str, Any],
*,
collect_timestamps: bool = True,
) -> dict[str, VideoScan]:
"""Decode once and collect one untrusted candidate per detector and frame."""
capture = cv2.VideoCapture(str(source))
if not capture.isOpened():
raise RuntimeError(f"OpenCV could not decode video: {source}")
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = float(capture.get(cv2.CAP_PROP_FPS))
if width <= 0 or height <= 0 or fps <= 0:
capture.release()
raise RuntimeError(f"Video has invalid stream geometry or frame rate: {source}")
detections: dict[str, list[FrameLocalization]] = {mark: [] for mark in detectors}
timestamps: list[float] = []
frame_index = 0
while True:
ok, frame = capture.read()
if not ok:
break
if collect_timestamps:
timestamps.append(float(capture.get(cv2.CAP_PROP_POS_MSEC)) / 1000)
if frame.shape[:2] != (height, width):
capture.release()
raise RuntimeError(f"Video changes frame dimensions at frame {frame_index}: {source}")
prepared = _prepare_frame(frame)
for mark, detector in detectors.items():
detections[mark].append(
detector(
frame,
frame_index=frame_index,
prepared=prepared,
)
)
frame_index += 1
capture.release()
if frame_index == 0:
raise RuntimeError(f"Video contains no decodable frames: {source}")
shared_timestamps: tuple[float, ...] = ()
if collect_timestamps:
probed_timestamps = probe_video_timestamps(source)
if len(probed_timestamps) == frame_index:
shared_timestamps = probed_timestamps
else:
if probed_timestamps:
log.warning(
"ffprobe/OpenCV frame-count mismatch for %s: timestamps=%s decoded=%s; using decoder timestamps",
source,
len(probed_timestamps),
frame_index,
)
shared_timestamps = tuple(timestamps)
return {
mark: VideoScan(width, height, fps, tuple(mark_detections), shared_timestamps)
for mark, mark_detections in detections.items()
}
def scan_video_marks(
source: Path,
marks: tuple[str, ...] = VIDEO_VISIBLE_MARKS,
*,
collect_timestamps: bool = True,
) -> dict[str, VideoScan]:
"""Decode once and collect candidates for every requested provider mark.
Timestamp probing is optional because identification never encodes frames.
Removal keeps it enabled so variable and non-zero-start timing is preserved.
"""
detectors = dict(
zip(
VIDEO_VISIBLE_MARKS,
(
detect_sora_frame,
detect_veo_frame,
detect_seedance_frame,
detect_dola_frame,
detect_hailuo_frame,
detect_kling_frame,
),
strict=True,
)
)
unsupported = sorted(set(marks) - detectors.keys())
if unsupported:
raise ValueError(f"Unsupported visible video mark: {', '.join(unsupported)}")
return _scan_video_detectors(
source,
{mark: detectors[mark] for mark in marks},
collect_timestamps=collect_timestamps,
)
def _mask_for_region(
frame_bgr: NDArray[Any],
region: Region,
*,
padding_fraction: float,
mask_style: Literal["box", "veo"],
) -> NDArray[Any]:
height, width = frame_bgr.shape[:2]
x, y, region_width, region_height = region
if mask_style == "veo" and 0.80 <= region_width / region_height <= 1.25:
mask = np.zeros((height, width), dtype=np.uint8)
diamond_base, _ = _veo_templates()
diamond = cv2.resize(
diamond_base,
(region_width, region_height),
interpolation=cv2.INTER_AREA,
)
diamond = np.where(diamond >= 24, 255, 0).astype(np.uint8)
dilation = max(2, round(region_height * 0.08))
kernel_size = dilation * 2 + 1
diamond = cv2.dilate(
diamond,
cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)),
)
x1 = min(width, x + region_width)
y1 = min(height, y + region_height)
mask[y:y1, x:x1] = diamond[: y1 - y, : x1 - x]
return mask
# A glyph-shaped mask leaves a thin translucent rim outside the approximate
# synthetic silhouette. Classical inpainting then pulls that white rim back
# into the hole, recreating the mascot as a bright blob. The measured clean
# floor on real Sora frames is a full box with roughly 0.28 mark-heights of
# context on every side.
# Padded rectangle + no dilation is exactly region_eraser.boxes_to_mask (the same
# primitive the image fill uses); the padding IS the growth, so `dilate=0`.
from remove_ai_watermarks.region_eraser import boxes_to_mask
padding = max(4, round(region_height * padding_fraction))
return boxes_to_mask(
(height, width),
[(x - padding, y - padding, region_width + 2 * padding, region_height + 2 * padding)],
dilate=0,
)
def _timestamp_time_base(profile_time_base: str | None) -> Fraction:
"""Use the source time base when known, with a microsecond fallback."""
return Fraction(profile_time_base) if profile_time_base is not None else Fraction(1, 1_000_000)
def _timestamps_are_variable(scan: VideoScan, *, time_base: Fraction) -> bool:
"""Whether OpenCV exposed valid timestamps with non-uniform frame intervals."""
if len(scan.timestamps) != len(scan.detections) or len(scan.timestamps) < 3:
return False
ticks = tuple(round(timestamp / float(time_base)) for timestamp in scan.timestamps)
intervals = tuple(current - previous for previous, current in pairwise(ticks))
return all(interval > 0 for interval in intervals) and len(set(intervals)) > 1
class _TimestampedNutWriter:
"""Mux BGR frames with explicit PTS into ffmpeg's standard-input pipe."""
def __init__(
self,
pipe: Any,
*,
width: int,
height: int,
time_base: Fraction,
start_pts: int = 0,
) -> None:
import av
self._av = av
self._time_base = time_base
self._start_pts = start_pts
self._origin: float | None = None
self._container = av.open(pipe, mode="w", format="nut")
self._stream = self._container.add_stream("rawvideo", rate=None)
self._stream.width = width
self._stream.height = height
self._stream.pix_fmt = "bgr24"
self._stream.time_base = time_base
self._stream.codec_context.time_base = time_base
def write(self, frame_bgr: NDArray[Any], timestamp: float) -> None:
"""Mux one contiguous BGR frame at its source timeline timestamp."""
if self._origin is None:
self._origin = timestamp
frame = self._av.VideoFrame.from_ndarray(
np.ascontiguousarray(frame_bgr),
format="bgr24",
)
frame.pts = self._start_pts + round((timestamp - self._origin) / float(self._time_base))
frame.time_base = self._time_base
for packet in self._stream.encode(frame):
self._container.mux(packet)
def close(self) -> None:
"""Flush the rawvideo encoder and NUT container without closing ffmpeg stdin."""
for packet in self._stream.encode():
self._container.mux(packet)
self._container.close()
def encode_clean_video(
source: Path,
output: Path,
scan: VideoScan,
regions: list[Region | None],
*,
backend: Backend,
strip_metadata: bool,
padding_fraction: float = 0.28,
mask_style: Literal["box", "veo"] = "box",
temporal_consistency: bool = True,
) -> int:
"""Decode again, fill accepted regions, and atomically encode with complete audio."""
from remove_ai_watermarks.watermark_registry import fill, resolve_backend
if len(regions) != len(scan.detections):
raise ValueError("Temporal localization count does not match the scanned frame count")
with staged_video_output(output) as (encoded_video, temporary_output):
profile = probe_video_encode_profile(source)
if (profile.component_depth or 0) > 8 or profile.color_transfer in _HDR_TRANSFERS:
source_format = profile.source_pixel_format or "unknown pixel format"
raise RuntimeError(
"Visible video removal currently supports SDR 8-bit input only; "
f"refusing to silently reduce {source_format} / "
f"{profile.color_transfer or 'unknown transfer'} to 8-bit SDR"
)
time_base = _timestamp_time_base(profile.time_base)
preserve_start_offset = profile.start_pts not in (None, 0)
timestamped_input = preserve_start_offset or _timestamps_are_variable(scan, time_base=time_base)
if timestamped_input and profile.time_base is None:
profile = replace(
profile,
time_base=f"{time_base.numerator}/{time_base.denominator}",
)
process = start_raw_video_encoder(
raw_video_command(
encoded_video,
width=scan.width,
height=scan.height,
fps=scan.fps,
crf=14,
profile=profile,
timestamped_input=timestamped_input,
copy_input_timestamps=preserve_start_offset,
)
)
frame_pipe = process.stdin
capture = cv2.VideoCapture(str(source))
if not capture.isOpened():
abort_raw_video_encoder(process)
raise RuntimeError(f"OpenCV could not reopen video for removal: {source}")
removed_frames = 0
timestamped_writer: _TimestampedNutWriter | None = None
previous_source: NDArray[Any] | None = None
previous_cleaned: NDArray[Any] | None = None
previous_mask: NDArray[Any] | None = None
try:
resolved_backend: Literal["cv2", "migan", "lama"] = resolve_backend(backend)
if timestamped_input:
timestamped_writer = _TimestampedNutWriter(
frame_pipe,
width=scan.width,
height=scan.height,
time_base=time_base,
start_pts=profile.start_pts or 0,
)
for frame_index, region in enumerate(regions):
ok, frame = capture.read()
if not ok:
raise RuntimeError(f"Video ended while reading frame {frame_index}: {source}")
source_frame = frame
mask: NDArray[Any] | None = None
if region is not None:
mask = _mask_for_region(
frame,
region,
padding_fraction=padding_fraction,
mask_style=mask_style,
)
frame = fill(frame, mask, backend=resolved_backend)
if (
temporal_consistency
and previous_source is not None
and previous_cleaned is not None
and previous_mask is not None
):
frame = stabilize_filled_frame(
previous_source,
previous_cleaned,
previous_mask,
source_frame,
frame,
mask,
copy=False,
)
removed_frames += 1
if timestamped_writer is None:
frame_pipe.write(frame.tobytes())
else:
timestamped_writer.write(frame, scan.timestamps[frame_index])
previous_source = source_frame
previous_cleaned = frame
previous_mask = mask
if timestamped_writer is not None:
timestamped_writer.close()
timestamped_writer = None
finish_raw_video_encoder(
process,
encoded_video,
operation="visible-watermark encode",
)
mux_encoded_video(
encoded_video,
source,
temporary_output,
strip_metadata=strip_metadata,
copy_input_timestamps=preserve_start_offset,
)
except Exception:
if timestamped_writer is not None:
with suppress(Exception):
timestamped_writer.close()
abort_raw_video_encoder(process)
raise
finally:
capture.release()
return removed_frames
def has_sora_provenance(markers: dict[str, str]) -> bool:
"""Whether container provenance specifically names the Sora generator."""
return "sora" in markers.get("claim_generator", "").lower()
def has_veo_provenance(markers: dict[str, str]) -> bool:
"""Whether container provenance names Google as the AI-video generator."""
identity = " ".join(
(
markers.get("claim_generator", ""),
markers.get("issuer", ""),
)
).lower()
return "google" in identity and "trainedalgorithmicmedia" in markers.get("source_type", "").lower()
def has_bytedance_video_provenance(markers: dict[str, str]) -> bool:
"""Whether container provenance names ByteDance or BytePlus AI video."""
identity = " ".join(
(
markers.get("claim_generator", ""),
markers.get("issuer", ""),
)
).lower()
source_type = markers.get("source_type", "").lower()
return ("bytedance" in identity or "byteplus" in identity) and "trainedalgorithmicmedia" in source_type