mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-09 23:50:40 +02:00
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
"""Bounded FLV metadata reader for native TC260 AIGC labels.
|
|
|
|
TC260-PG-20257A stores the label in the ``onMetaData`` script tag as an AMF0
|
|
property named ``AIGC`` whose string value is the normative JSON object. Media
|
|
tag payloads are skipped without being loaded.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from remove_ai_watermarks.metadata import MAX_TC260_VALUE_BYTES, parse_tc260_aigc_json
|
|
|
|
_SCRIPT_TAG = 18
|
|
_MAX_SCRIPT_BYTES = 4 * 1024 * 1024
|
|
|
|
|
|
def _u24(value: bytes) -> int:
|
|
return int.from_bytes(value, "big")
|
|
|
|
|
|
def _amf0_string(data: bytes, position: int, *, long: bool = False) -> tuple[bytes, int] | None:
|
|
length_size = 4 if long else 2
|
|
if position + length_size > len(data):
|
|
return None
|
|
length = int.from_bytes(data[position : position + length_size], "big")
|
|
start = position + length_size
|
|
end = start + length
|
|
if end > len(data):
|
|
return None
|
|
return data[start:end], end
|
|
|
|
|
|
def _skip_amf0(data: bytes, position: int, depth: int = 0) -> int | None:
|
|
if position >= len(data) or depth > 8:
|
|
return None
|
|
value_type = data[position]
|
|
position += 1
|
|
if value_type == 0:
|
|
return position + 8 if position + 8 <= len(data) else None
|
|
if value_type == 1:
|
|
return position + 1 if position + 1 <= len(data) else None
|
|
if value_type == 2:
|
|
parsed = _amf0_string(data, position)
|
|
return parsed[1] if parsed is not None else None
|
|
if value_type in {5, 6}:
|
|
return position
|
|
if value_type == 7:
|
|
return position + 2 if position + 2 <= len(data) else None
|
|
if value_type == 11:
|
|
return position + 10 if position + 10 <= len(data) else None
|
|
if value_type == 12:
|
|
parsed = _amf0_string(data, position, long=True)
|
|
return parsed[1] if parsed is not None else None
|
|
if value_type == 10:
|
|
if position + 4 > len(data):
|
|
return None
|
|
count = int.from_bytes(data[position : position + 4], "big")
|
|
position += 4
|
|
for _ in range(count):
|
|
next_position = _skip_amf0(data, position, depth + 1)
|
|
if next_position is None:
|
|
return None
|
|
position = next_position
|
|
return position
|
|
if value_type in {3, 8}:
|
|
if value_type == 8:
|
|
if position + 4 > len(data):
|
|
return None
|
|
position += 4
|
|
while position + 3 <= len(data):
|
|
name_length = int.from_bytes(data[position : position + 2], "big")
|
|
position += 2
|
|
if name_length == 0 and data[position] == 9:
|
|
return position + 1
|
|
position += name_length
|
|
if position > len(data):
|
|
return None
|
|
next_position = _skip_amf0(data, position, depth + 1)
|
|
if next_position is None:
|
|
return None
|
|
position = next_position
|
|
return None
|
|
return None
|
|
|
|
|
|
def _script_payloads(data: bytes) -> tuple[bytes, ...]:
|
|
first = _amf0_string(data, 1) if data[:1] == b"\x02" else None
|
|
if first is None or first[0] != b"onMetaData":
|
|
return ()
|
|
position = first[1]
|
|
if position >= len(data) or data[position] not in {3, 8}:
|
|
return ()
|
|
if data[position] == 8:
|
|
position += 5
|
|
else:
|
|
position += 1
|
|
found: list[bytes] = []
|
|
while position + 3 <= len(data):
|
|
name_length = int.from_bytes(data[position : position + 2], "big")
|
|
position += 2
|
|
if name_length == 0 and data[position] == 9:
|
|
break
|
|
name_end = position + name_length
|
|
if name_end > len(data):
|
|
break
|
|
name = data[position:name_end]
|
|
position = name_end
|
|
if name == b"AIGC" and position < len(data) and data[position] in {2, 12}:
|
|
long = data[position] == 12
|
|
parsed = _amf0_string(data, position + 1, long=long)
|
|
if parsed is None:
|
|
break
|
|
value, position = parsed
|
|
if len(value) <= MAX_TC260_VALUE_BYTES and parse_tc260_aigc_json(value) is not None:
|
|
found.append(value)
|
|
continue
|
|
next_position = _skip_amf0(data, position)
|
|
if next_position is None:
|
|
break
|
|
position = next_position
|
|
return tuple(found)
|
|
|
|
|
|
def tc260_aigc_payloads(path: str | Path) -> tuple[bytes, ...]:
|
|
"""Read validated TC260 values from FLV ``script.onMetaData.AIGC``."""
|
|
found: list[bytes] = []
|
|
try:
|
|
with open(path, "rb") as stream:
|
|
header = stream.read(9)
|
|
if len(header) != 9 or header[:3] != b"FLV":
|
|
return ()
|
|
data_offset = int.from_bytes(header[5:9], "big")
|
|
stream.seek(0, 2)
|
|
file_size = stream.tell()
|
|
position = data_offset + 4
|
|
while position + 11 <= file_size:
|
|
stream.seek(position)
|
|
tag_header = stream.read(11)
|
|
if len(tag_header) != 11:
|
|
break
|
|
tag_type = tag_header[0] & 0x1F
|
|
data_size = _u24(tag_header[1:4])
|
|
payload_start = position + 11
|
|
payload_end = payload_start + data_size
|
|
if payload_end + 4 > file_size:
|
|
break
|
|
if tag_type == _SCRIPT_TAG and data_size <= _MAX_SCRIPT_BYTES:
|
|
payload = stream.read(data_size)
|
|
if len(payload) == data_size:
|
|
found.extend(_script_payloads(payload))
|
|
if found:
|
|
return tuple(found)
|
|
position = payload_end + 4
|
|
except OSError:
|
|
return ()
|
|
return tuple(found)
|