From 5077ac21610a4f1037cd11839f88ce053b32f7be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:15:18 +0000 Subject: [PATCH 1/9] Support variable-length frame index Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/635ce0d2-bc41-442a-bce7-05690a2115c1 Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- README.md | 23 +++++++++---------- decode.py | 54 +++++++++++++++++++++++++++++++-------------- encode.py | 66 ++++++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 98 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 6c8f5e9..8e17023 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Each 1920×1080 frame contains a 30×16 grid of 64×64 pixel blocks: +--------+--------+--------+--------+--------+ ... +--------+ | SYNC 0 | SYNC 1 | SYNC 2 | SYNC 3 | SYNC 4 | | SYNC 7 | <- row 0, cols 0-7: sync pattern +--------+--------+--------+--------+--------+ +--------+ -| IDX 0 | IDX 1 | IDX 2 | ... | IDX 15 | <- row 0, cols 8-23: frame index +| IDX 0 | IDX 1 | IDX 2 | ... | IDX 15 | <- row 0, cols 8-23: frame index (16-bit default) +--------+--------+--------+ +--------+ | DATA | DATA | DATA | DATA | DATA | ... | DATA | <- remaining 456 blocks: data +--------+--------+--------+--------+--------+ +--------+ @@ -72,7 +72,8 @@ Each 1920×1080 frame contains a 30×16 grid of 64×64 pixel blocks: ``` - Sync pattern: `10101100` (8 bits, fixed). Used to validate frames and reject corrupted ones. -- Frame index: 16-bit big-endian integer. Supports up to 65,535 frames (~36 minutes at 30fps). +- Frame index: big-endian integer, 16-bit by default. Encoder switches to 32-bit when needed (hard limit). + When 32-bit is used, the index continues into the next blocks and data capacity per frame shrinks. - Data: 456 bits = 57 bytes of Reed-Solomon encoded payload per frame. The first 1 second (`METADATA_DURATION_SEC`) in the video is reserved for QR metadata @@ -221,15 +222,15 @@ This tests the full encode/decode cycle locally. If this fails, the issue is in | Frame dimensions | 1920 × 1080 | | Block size | 64 × 64 px | | Grid | 30 × 16 = 480 blocks/frame | -| Header bits/frame | 24 (8 sync + 16 index) | -| Data bits/frame | 456 | -| Data bytes/frame | 57 | -| Video data rate | 1,710 bytes/sec at 30fps | +| Header bits/frame | 24 (8 sync + 16 index) / 40 (8 sync + 32 index) | +| Data bits/frame | 456 (16-bit) / 440 (32-bit) | +| Data bytes/frame | 57 (16-bit) / 55 (32-bit) | +| Video data rate | 1,710 bytes/sec (16-bit) / 1,650 bytes/sec (32-bit) | | Audio data rate | 25 bytes/sec (4-FSK, 100 baud) | | ECC overhead | ~14% (RS-32 over GF(2^8)) | -| Net video throughput | ~1,500 bytes/sec after ECC | -| Max video duration | ~36 min (16-bit frame index) | -| Max file size (video) | ~3.2 GB before ECC | +| Net video throughput | ~1,500 bytes/sec (16-bit) / ~1,450 bytes/sec (32-bit) after ECC | +| Max video duration | ~36 min (16-bit) / ~4.5 years (32-bit) | +| Max file size (video) | Scales with index width (16-bit default, 32-bit hard limit) | Audio capacity is 1.5% of video capacity at these settings. For small files (under ~1.5 KB after ECC), audio covers 100% of the payload and provides full redundancy. For larger files it covers a prefix. @@ -239,7 +240,7 @@ To increase audio coverage: raise `BAUD_RATE` in both scripts. 200 baud doubles ## Known limitations -**Frame index ceiling.** The 16-bit frame index supports 65,535 frames, which is ~36 minutes at 30fps and ~3.2 GB of raw file data after ECC. For larger files, increase `FRAME_INDEX_BITS` to 24 or 32 in both scripts and adjust `HEADER_BITS` and `DATA_BITS_PER_FRAME` accordingly. +**Frame index ceiling.** The encoder uses 16-bit frame indices by default and automatically switches to 32-bit if the payload needs more than 16-bit indices. 32-bit is the hard limit. **YouTube re-encoding is untested.** The local round-trip works. YouTube's actual VP9/H.264 output has not yet been tested against this codec. The 64×64 block size was chosen conservatively for this reason. If YouTube's encoder corrupts blocks, the first thing to try is increasing `BLOCK_SIZE` to 128. @@ -273,7 +274,7 @@ Payload layout (before ECC): +-------------------+---------------------------+-------------------+ Metadata JSON fields: - v encoding version (integer, currently 2) + v encoding version (integer, currently 3) filename original filename (string) size original file size in bytes (integer) sha256 SHA256 hex digest of the original file bytes (string) diff --git a/decode.py b/decode.py index 47a2408..ab8fdeb 100644 --- a/decode.py +++ b/decode.py @@ -25,10 +25,9 @@ TOTAL_BLOCKS = COLS * ROWS # 480 SYNC_PATTERN = np.array([1, 0, 1, 0, 1, 1, 0, 0], dtype=np.uint8) -FRAME_INDEX_BITS = 16 -HEADER_BITS = len(SYNC_PATTERN) + FRAME_INDEX_BITS # 24 -DATA_BITS_PER_FRAME = TOTAL_BLOCKS - HEADER_BITS # 456 -DATA_BYTES_PER_FRAME = DATA_BITS_PER_FRAME // 8 # 57 +SYNC_BITS = len(SYNC_PATTERN) +DEFAULT_FRAME_INDEX_BITS = 16 +EXTENDED_FRAME_INDEX_BITS = 32 SAMPLE_RATE = 44100 BAUD_RATE = 100 @@ -87,18 +86,26 @@ def read_blocks(frame: np.ndarray) -> np.ndarray: return bits -def decode_frame(bits: np.ndarray): +def frame_layout(index_bits: int) -> tuple[int, int]: + header_bits = SYNC_BITS + index_bits + data_bits_per_frame = TOTAL_BLOCKS - header_bits + if data_bits_per_frame <= 0: + raise ValueError("Frame index bits exceed available block capacity.") + if data_bits_per_frame % 8 != 0: + raise ValueError("Frame data bits must align to full bytes.") + return header_bits, data_bits_per_frame + + +def decode_frame(bits: np.ndarray, index_bits: int, header_bits: int): # Returns (frame_idx, data_bits) or (None, None) if sync check fails. - sync = bits[:len(SYNC_PATTERN)] + sync = bits[:SYNC_BITS] if not np.array_equal(sync, SYNC_PATTERN): return None, None - idx_bits = bits[len(SYNC_PATTERN):HEADER_BITS] - frame_idx = int(np.packbits(np.pad(idx_bits, (16 - FRAME_INDEX_BITS, 0)))[1]) - # Cleaner index decode: + idx_bits = bits[SYNC_BITS:header_bits] frame_idx = 0 for b in idx_bits: frame_idx = (frame_idx << 1) | int(b) - data_bits = bits[HEADER_BITS:] + data_bits = bits[header_bits:] return frame_idx, data_bits @@ -193,6 +200,7 @@ def to_int(value): "sha256": raw.get("h") if "h" in raw else raw.get("sha256"), "ecc_bytes": to_int(raw.get("e") if "e" in raw else raw.get("ecc_bytes")), "frames": to_int(raw.get("n") if "n" in raw else raw.get("frames")), + "index_bits": to_int(raw.get("i") if "i" in raw else raw.get("index_bits")), "metadata_frames": to_int(raw.get("m") if "m" in raw else raw.get("metadata_frames")), } @@ -223,6 +231,9 @@ def decode(video_path: str, output_dir: str = "."): # Step 1: video channel print("\n[1/5] Decoding video channel...") + index_bits = DEFAULT_FRAME_INDEX_BITS + header_bits, data_bits_per_frame = frame_layout(index_bits) + data_bytes_per_frame = data_bits_per_frame // 8 frames = {} total = 0 sync_fail = 0 @@ -236,8 +247,19 @@ def decode(video_path: str, output_dir: str = "."): qr_meta = decode_qr_metadata(frame_np, qr_detector) if qr_meta: print(f" QR metadata decoded from frame {total}.") + qr_index_bits = qr_meta.get("index_bits") + if qr_index_bits in (DEFAULT_FRAME_INDEX_BITS, EXTENDED_FRAME_INDEX_BITS): + if qr_index_bits != index_bits: + if frames or early_frames: + frames = {} + early_frames = {} + index_bits = qr_index_bits + header_bits, data_bits_per_frame = frame_layout(index_bits) + data_bytes_per_frame = data_bits_per_frame // 8 + elif qr_index_bits is not None: + print(f" Warning: unsupported index width {qr_index_bits}; using {index_bits}-bit.") bits = read_blocks(frame_np) - idx, data_bits = decode_frame(bits) + idx, data_bits = decode_frame(bits, index_bits, header_bits) if idx is None: sync_fail += 1 else: @@ -270,19 +292,19 @@ def decode(video_path: str, output_dir: str = "."): else: frame_count = max(frames.keys()) + 1 - total_bits = frame_count * DATA_BITS_PER_FRAME + total_bits = frame_count * data_bits_per_frame all_bits = np.zeros(total_bits, dtype=np.uint8) missing = [] for i in range(frame_count): if i in frames: - all_bits[i * DATA_BITS_PER_FRAME:(i + 1) * DATA_BITS_PER_FRAME] = frames[i] + all_bits[i * data_bits_per_frame:(i + 1) * data_bits_per_frame] = frames[i] else: missing.append(i) if missing: print(f" Missing {len(missing)} frames: {missing[:10]}{'...' if len(missing)>10 else ''}") - print(f" Reed-Solomon will attempt recovery ({len(missing)*DATA_BYTES_PER_FRAME} bytes zeroed).") + print(f" Reed-Solomon will attempt recovery ({len(missing)*data_bytes_per_frame} bytes zeroed).") else: print(f" All {frame_count} frames present.") @@ -314,9 +336,9 @@ def decode(video_path: str, output_dir: str = "."): if expected_ecc_bytes is None and len(ecc_video) >= rs_block_size: ecc_video = ecc_video[: (len(ecc_video) // rs_block_size) * rs_block_size] missing_byte_positions = [ - i * DATA_BYTES_PER_FRAME + j + i * data_bytes_per_frame + j for i in missing - for j in range(DATA_BYTES_PER_FRAME) + for j in range(data_bytes_per_frame) ] erasures_video = [pos for pos in missing_byte_positions if pos < len(ecc_video)] diff --git a/encode.py b/encode.py index 8e0f77e..59cee86 100644 --- a/encode.py +++ b/encode.py @@ -4,6 +4,7 @@ import os import json import hashlib +import math import struct import subprocess import tempfile @@ -23,11 +24,14 @@ ROWS = FRAME_HEIGHT // BLOCK_SIZE # 16 TOTAL_BLOCKS = COLS * ROWS # 480 -# Frame layout: [8 sync bits][16 index bits][456 data bits] +# Frame layout: [8 sync bits][frame index bits][data bits] SYNC_PATTERN = np.array([1, 0, 1, 0, 1, 1, 0, 0], dtype=np.uint8) -FRAME_INDEX_BITS = 16 # max 65535 frames (~36 min at 30fps) -HEADER_BITS = len(SYNC_PATTERN) + FRAME_INDEX_BITS # 24 -DATA_BITS_PER_FRAME = TOTAL_BLOCKS - HEADER_BITS # 456 bits = 57 bytes/frame +SYNC_BITS = len(SYNC_PATTERN) +DEFAULT_FRAME_INDEX_BITS = 16 +EXTENDED_FRAME_INDEX_BITS = 32 +MAX_FRAME_INDEX = (1 << EXTENDED_FRAME_INDEX_BITS) - 1 +MAX_FRAME_COUNT_DEFAULT = 1 << DEFAULT_FRAME_INDEX_BITS +MAX_FRAME_COUNT_EXTENDED = MAX_FRAME_INDEX + 1 # Audio (4-FSK) SAMPLE_RATE = 44100 @@ -47,7 +51,7 @@ QR_ERROR_CORRECTION = qrcode.constants.ERROR_CORRECT_Q QR_MIN_MODULE_PX = 6 -ENCODING_VERSION = 2 +ENCODING_VERSION = 3 def build_payload(filepath: str): @@ -66,7 +70,7 @@ def build_payload(filepath: str): return payload, meta -def build_qr_metadata(meta: dict, ecc_len: int, num_frames: int) -> bytes: +def build_qr_metadata(meta: dict, ecc_len: int, num_frames: int, index_bits: int) -> bytes: qr_meta = { "v": ENCODING_VERSION, "f": meta["filename"], @@ -74,6 +78,7 @@ def build_qr_metadata(meta: dict, ecc_len: int, num_frames: int) -> bytes: "h": meta["sha256"], "e": ecc_len, "n": num_frames, + "i": index_bits, "m": METADATA_FRAMES, } return json.dumps(qr_meta, separators=(",", ":")).encode() @@ -89,15 +94,25 @@ def ecc_encode(data: bytes) -> bytes: return encoded -def index_to_bits(idx: int) -> np.ndarray: +def frame_layout(index_bits: int) -> tuple[int, int]: + header_bits = SYNC_BITS + index_bits + data_bits_per_frame = TOTAL_BLOCKS - header_bits + if data_bits_per_frame <= 0: + raise ValueError("Frame index bits exceed available block capacity.") + if data_bits_per_frame % 8 != 0: + raise ValueError("Frame data bits must align to full bytes.") + return header_bits, data_bits_per_frame + + +def index_to_bits(idx: int, index_bits: int) -> np.ndarray: return np.array( - [(idx >> (FRAME_INDEX_BITS - 1 - i)) & 1 for i in range(FRAME_INDEX_BITS)], + [(idx >> (index_bits - 1 - i)) & 1 for i in range(index_bits)], dtype=np.uint8 ) -def make_frame(frame_idx: int, data_bits: np.ndarray) -> bytes: - header = np.concatenate([SYNC_PATTERN, index_to_bits(frame_idx)]) +def make_frame(frame_idx: int, data_bits: np.ndarray, index_bits: int) -> bytes: + header = np.concatenate([SYNC_PATTERN, index_to_bits(frame_idx, index_bits)]) block_bits = np.concatenate([header, data_bits]) # (480,) grid = (block_bits.reshape(ROWS, COLS) * 255).astype(np.uint8) # (16, 30) frame_2d = np.repeat(np.repeat(grid, BLOCK_SIZE, axis=0), BLOCK_SIZE, axis=1) # (1080, 1920) @@ -187,18 +202,32 @@ def encode(input_path: str, output_path: str): print("\n[2/5] Reed-Solomon ECC...") ecc_data = ecc_encode(payload) + total_bits = len(ecc_data) * 8 + _, data_bits_16 = frame_layout(DEFAULT_FRAME_INDEX_BITS) + frames_16 = math.ceil(total_bits / data_bits_16) + if frames_16 <= MAX_FRAME_COUNT_DEFAULT: + index_bits = DEFAULT_FRAME_INDEX_BITS + data_bits_per_frame = data_bits_16 + num_frames = frames_16 + else: + _, data_bits_per_frame = frame_layout(EXTENDED_FRAME_INDEX_BITS) + num_frames = math.ceil(total_bits / data_bits_per_frame) + if num_frames > MAX_FRAME_COUNT_EXTENDED: + raise ValueError("Payload exceeds maximum 32-bit frame index capacity.") + index_bits = EXTENDED_FRAME_INDEX_BITS + all_bits = np.unpackbits(np.frombuffer(ecc_data, dtype=np.uint8)) - rem = len(all_bits) % DATA_BITS_PER_FRAME + rem = len(all_bits) % data_bits_per_frame if rem: - all_bits = np.concatenate([all_bits, np.zeros(DATA_BITS_PER_FRAME - rem, dtype=np.uint8)]) + all_bits = np.concatenate([all_bits, np.zeros(data_bits_per_frame - rem, dtype=np.uint8)]) - num_frames = len(all_bits) // DATA_BITS_PER_FRAME - qr_payload = build_qr_metadata(meta, len(ecc_data), num_frames) + num_frames = len(all_bits) // data_bits_per_frame + qr_payload = build_qr_metadata(meta, len(ecc_data), num_frames, index_bits) qr_frame = make_qr_frame(qr_payload) total_frames = num_frames + METADATA_FRAMES duration_sec = total_frames / FRAME_RATE - video_bps = DATA_BITS_PER_FRAME * FRAME_RATE // 8 + video_bps = data_bits_per_frame * FRAME_RATE // 8 audio_byte_count = min(len(ecc_data), int(duration_sec * BYTES_PER_SEC_AUDIO)) audio_coverage_pct = audio_byte_count / len(ecc_data) * 100 @@ -206,7 +235,8 @@ def encode(input_path: str, output_path: str): print(f" Metadata frames:{METADATA_FRAMES:>10} ({METADATA_DURATION_SEC:.1f}s)") print(f" Data frames: {num_frames:>10}") print(f" Total frames: {total_frames:>10} @ {FRAME_RATE}fps ({duration_sec:.1f}s)") - print(f" Video channel: {video_bps:,} bytes/sec ({DATA_BITS_PER_FRAME} bits/frame)") + print(f" Frame index: {index_bits} bits") + print(f" Video channel: {video_bps:,} bytes/sec ({data_bits_per_frame} bits/frame)") print(f" Audio channel: {BYTES_PER_SEC_AUDIO} bytes/sec (4-FSK @ {BAUD_RATE} baud)") print(f" Audio covers: {audio_byte_count:,}/{len(ecc_data):,} bytes ({audio_coverage_pct:.1f}%)") if audio_coverage_pct < 100: @@ -251,8 +281,8 @@ def encode(input_path: str, output_path: str): written = METADATA_FRAMES for i in range(num_frames): - chunk = all_bits[i * DATA_BITS_PER_FRAME:(i + 1) * DATA_BITS_PER_FRAME] - proc.stdin.write(make_frame(i, chunk)) + chunk = all_bits[i * data_bits_per_frame:(i + 1) * data_bits_per_frame] + proc.stdin.write(make_frame(i, chunk, index_bits)) written += 1 if written % 30 == 0 or written == total_frames: pct = written / total_frames * 100 From f7b2e3c978919d1514a040848dd8f0bc063460fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:16:06 +0000 Subject: [PATCH 2/9] Clarify frame index errors Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/635ce0d2-bc41-442a-bce7-05690a2115c1 Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- decode.py | 9 +++++++-- encode.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/decode.py b/decode.py index ab8fdeb..83b3e46 100644 --- a/decode.py +++ b/decode.py @@ -90,9 +90,13 @@ def frame_layout(index_bits: int) -> tuple[int, int]: header_bits = SYNC_BITS + index_bits data_bits_per_frame = TOTAL_BLOCKS - header_bits if data_bits_per_frame <= 0: - raise ValueError("Frame index bits exceed available block capacity.") + raise ValueError( + f"Frame index bits ({index_bits}) exceed available block capacity ({TOTAL_BLOCKS} blocks)." + ) if data_bits_per_frame % 8 != 0: - raise ValueError("Frame data bits must align to full bytes.") + raise ValueError( + f"Frame data bits ({data_bits_per_frame}) must align to full bytes (multiples of 8)." + ) return header_bits, data_bits_per_frame @@ -251,6 +255,7 @@ def decode(video_path: str, output_dir: str = "."): if qr_index_bits in (DEFAULT_FRAME_INDEX_BITS, EXTENDED_FRAME_INDEX_BITS): if qr_index_bits != index_bits: if frames or early_frames: + print(" Warning: index width updated; discarding previously decoded frames.") frames = {} early_frames = {} index_bits = qr_index_bits diff --git a/encode.py b/encode.py index 59cee86..446cfb8 100644 --- a/encode.py +++ b/encode.py @@ -98,9 +98,13 @@ def frame_layout(index_bits: int) -> tuple[int, int]: header_bits = SYNC_BITS + index_bits data_bits_per_frame = TOTAL_BLOCKS - header_bits if data_bits_per_frame <= 0: - raise ValueError("Frame index bits exceed available block capacity.") + raise ValueError( + f"Frame index bits ({index_bits}) exceed available block capacity ({TOTAL_BLOCKS} blocks)." + ) if data_bits_per_frame % 8 != 0: - raise ValueError("Frame data bits must align to full bytes.") + raise ValueError( + f"Frame data bits ({data_bits_per_frame}) must align to full bytes (multiples of 8)." + ) return header_bits, data_bits_per_frame @@ -213,7 +217,9 @@ def encode(input_path: str, output_path: str): _, data_bits_per_frame = frame_layout(EXTENDED_FRAME_INDEX_BITS) num_frames = math.ceil(total_bits / data_bits_per_frame) if num_frames > MAX_FRAME_COUNT_EXTENDED: - raise ValueError("Payload exceeds maximum 32-bit frame index capacity.") + raise ValueError( + f"Payload needs {num_frames} frames which exceeds 32-bit limit ({MAX_FRAME_COUNT_EXTENDED})." + ) index_bits = EXTENDED_FRAME_INDEX_BITS all_bits = np.unpackbits(np.frombuffer(ecc_data, dtype=np.uint8)) From 5456f07128c2ccac99cd974d076d3efa78426aac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:16:44 +0000 Subject: [PATCH 3/9] Refine index width messaging Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/635ce0d2-bc41-442a-bce7-05690a2115c1 Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- decode.py | 6 +++++- encode.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/decode.py b/decode.py index 83b3e46..bc8edc1 100644 --- a/decode.py +++ b/decode.py @@ -254,8 +254,12 @@ def decode(video_path: str, output_dir: str = "."): qr_index_bits = qr_meta.get("index_bits") if qr_index_bits in (DEFAULT_FRAME_INDEX_BITS, EXTENDED_FRAME_INDEX_BITS): if qr_index_bits != index_bits: + old_index_bits = index_bits if frames or early_frames: - print(" Warning: index width updated; discarding previously decoded frames.") + print( + " Warning: index width changed from " + f"{old_index_bits} to {qr_index_bits} bits; discarding previously decoded frames." + ) frames = {} early_frames = {} index_bits = qr_index_bits diff --git a/encode.py b/encode.py index 446cfb8..ff5f980 100644 --- a/encode.py +++ b/encode.py @@ -218,7 +218,8 @@ def encode(input_path: str, output_path: str): num_frames = math.ceil(total_bits / data_bits_per_frame) if num_frames > MAX_FRAME_COUNT_EXTENDED: raise ValueError( - f"Payload needs {num_frames} frames which exceeds 32-bit limit ({MAX_FRAME_COUNT_EXTENDED})." + f"Payload needs {num_frames} frames which exceeds the 32-bit max frame count " + f"({MAX_FRAME_COUNT_EXTENDED})." ) index_bits = EXTENDED_FRAME_INDEX_BITS From 500d69098bae692f9e2ab7fbdf5c0322d3acc67b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:17:26 +0000 Subject: [PATCH 4/9] Clarify index width docs Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/635ce0d2-bc41-442a-bce7-05690a2115c1 Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- README.md | 4 ++-- decode.py | 5 +++-- encode.py | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8e17023..c6d39b4 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Each 1920×1080 frame contains a 30×16 grid of 64×64 pixel blocks: +--------+--------+--------+--------+--------+ ... +--------+ | SYNC 0 | SYNC 1 | SYNC 2 | SYNC 3 | SYNC 4 | | SYNC 7 | <- row 0, cols 0-7: sync pattern +--------+--------+--------+--------+--------+ +--------+ -| IDX 0 | IDX 1 | IDX 2 | ... | IDX 15 | <- row 0, cols 8-23: frame index (16-bit default) +| IDX 0 | IDX 1 | IDX 2 | ... | IDX 15 | <- row 0, cols 8-23: frame index (16-bit default; 32-bit continues) +--------+--------+--------+ +--------+ | DATA | DATA | DATA | DATA | DATA | ... | DATA | <- remaining 456 blocks: data +--------+--------+--------+--------+--------+ +--------+ @@ -74,7 +74,7 @@ Each 1920×1080 frame contains a 30×16 grid of 64×64 pixel blocks: - Sync pattern: `10101100` (8 bits, fixed). Used to validate frames and reject corrupted ones. - Frame index: big-endian integer, 16-bit by default. Encoder switches to 32-bit when needed (hard limit). When 32-bit is used, the index continues into the next blocks and data capacity per frame shrinks. -- Data: 456 bits = 57 bytes of Reed-Solomon encoded payload per frame. +- Data: 456 bits (16-bit index) or 440 bits (32-bit index) of Reed-Solomon encoded payload per frame. The first 1 second (`METADATA_DURATION_SEC`) in the video is reserved for QR metadata and does **not** contain data blocks. The decoder uses these frames to learn the diff --git a/decode.py b/decode.py index bc8edc1..b2791b4 100644 --- a/decode.py +++ b/decode.py @@ -257,8 +257,9 @@ def decode(video_path: str, output_dir: str = "."): old_index_bits = index_bits if frames or early_frames: print( - " Warning: index width changed from " - f"{old_index_bits} to {qr_index_bits} bits; discarding previously decoded frames." + " Warning: unexpected index width change from " + f"{old_index_bits} to {qr_index_bits} bits; this may indicate corrupted " + "or mixed sources. Discarding previously decoded frames." ) frames = {} early_frames = {} diff --git a/encode.py b/encode.py index ff5f980..2931fe8 100644 --- a/encode.py +++ b/encode.py @@ -207,11 +207,11 @@ def encode(input_path: str, output_path: str): ecc_data = ecc_encode(payload) total_bits = len(ecc_data) * 8 - _, data_bits_16 = frame_layout(DEFAULT_FRAME_INDEX_BITS) - frames_16 = math.ceil(total_bits / data_bits_16) + _, data_bits_default = frame_layout(DEFAULT_FRAME_INDEX_BITS) + frames_16 = math.ceil(total_bits / data_bits_default) if frames_16 <= MAX_FRAME_COUNT_DEFAULT: index_bits = DEFAULT_FRAME_INDEX_BITS - data_bits_per_frame = data_bits_16 + data_bits_per_frame = data_bits_default num_frames = frames_16 else: _, data_bits_per_frame = frame_layout(EXTENDED_FRAME_INDEX_BITS) From 7e8592dd99614facc5566509e89ff6ad66e58ec1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:18:23 +0000 Subject: [PATCH 5/9] Document QR index width Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/635ce0d2-bc41-442a-bce7-05690a2115c1 Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- README.md | 2 ++ decode.py | 6 +++--- encode.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c6d39b4..4eef3d5 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ Each 1920×1080 frame contains a 30×16 grid of 64×64 pixel blocks: The first 1 second (`METADATA_DURATION_SEC`) in the video is reserved for QR metadata and does **not** contain data blocks. The decoder uses these frames to learn the expected ECC length, frame count, and SHA256 before assembling payload data. +QR metadata also carries the frame index width (`i`) along with filename, size, +ECC length, and metadata frame count. Each block is sampled at its center 32×32 region (the inner half, margin = 16px). Block edges are where DCT compression artifacts accumulate; the center is clean. diff --git a/decode.py b/decode.py index b2791b4..79d4205 100644 --- a/decode.py +++ b/decode.py @@ -257,9 +257,9 @@ def decode(video_path: str, output_dir: str = "."): old_index_bits = index_bits if frames or early_frames: print( - " Warning: unexpected index width change from " - f"{old_index_bits} to {qr_index_bits} bits; this may indicate corrupted " - "or mixed sources. Discarding previously decoded frames." + f" Warning: unexpected index width change from {old_index_bits} to " + f"{qr_index_bits} bits; this may indicate corrupted or mixed sources. " + "Discarding previously decoded frames." ) frames = {} early_frames = {} diff --git a/encode.py b/encode.py index 2931fe8..c2bdf99 100644 --- a/encode.py +++ b/encode.py @@ -218,8 +218,8 @@ def encode(input_path: str, output_path: str): num_frames = math.ceil(total_bits / data_bits_per_frame) if num_frames > MAX_FRAME_COUNT_EXTENDED: raise ValueError( - f"Payload needs {num_frames} frames which exceeds the 32-bit max frame count " - f"({MAX_FRAME_COUNT_EXTENDED})." + f"Payload needs {num_frames} frames which exceeds 32-bit frame index capacity " + f"(max frame count {MAX_FRAME_COUNT_EXTENDED})." ) index_bits = EXTENDED_FRAME_INDEX_BITS From ef194efc849519800ae3579e199d2277e653766b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:19:10 +0000 Subject: [PATCH 6/9] Tidy index width handling Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/635ce0d2-bc41-442a-bce7-05690a2115c1 Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- README.md | 2 +- decode.py | 33 +++++++++++++++++---------------- encode.py | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4eef3d5..dd4cc83 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Each 1920×1080 frame contains a 30×16 grid of 64×64 pixel blocks: +--------+--------+--------+--------+--------+ ... +--------+ | SYNC 0 | SYNC 1 | SYNC 2 | SYNC 3 | SYNC 4 | | SYNC 7 | <- row 0, cols 0-7: sync pattern +--------+--------+--------+--------+--------+ +--------+ -| IDX 0 | IDX 1 | IDX 2 | ... | IDX 15 | <- row 0, cols 8-23: frame index (16-bit default; 32-bit continues) +| IDX 0 | IDX 1 | IDX 2 | ... | IDX 15 | <- row 0, cols 8-23: frame index (16-bit default; 32-bit continues past col 23) +--------+--------+--------+ +--------+ | DATA | DATA | DATA | DATA | DATA | ... | DATA | <- remaining 456 blocks: data +--------+--------+--------+--------+--------+ +--------+ diff --git a/decode.py b/decode.py index 79d4205..a0ae93d 100644 --- a/decode.py +++ b/decode.py @@ -248,26 +248,27 @@ def decode(video_path: str, output_dir: str = "."): for frame_np in stream_frames(video_path): total += 1 if total <= METADATA_FRAMES and qr_meta is None: - qr_meta = decode_qr_metadata(frame_np, qr_detector) + qr_meta = decode_qr_metadata(frame_np, qr_detector) if qr_meta: print(f" QR metadata decoded from frame {total}.") qr_index_bits = qr_meta.get("index_bits") - if qr_index_bits in (DEFAULT_FRAME_INDEX_BITS, EXTENDED_FRAME_INDEX_BITS): - if qr_index_bits != index_bits: - old_index_bits = index_bits - if frames or early_frames: - print( - f" Warning: unexpected index width change from {old_index_bits} to " - f"{qr_index_bits} bits; this may indicate corrupted or mixed sources. " - "Discarding previously decoded frames." - ) - frames = {} - early_frames = {} - index_bits = qr_index_bits - header_bits, data_bits_per_frame = frame_layout(index_bits) - data_bytes_per_frame = data_bits_per_frame // 8 - elif qr_index_bits is not None: + if qr_index_bits is None: + pass + elif qr_index_bits not in (DEFAULT_FRAME_INDEX_BITS, EXTENDED_FRAME_INDEX_BITS): print(f" Warning: unsupported index width {qr_index_bits}; using {index_bits}-bit.") + elif qr_index_bits != index_bits: + old_index_bits = index_bits + if frames or early_frames: + print( + f" Warning: unexpected index width change from {old_index_bits} to " + f"{qr_index_bits} bits; this may indicate corrupted or mixed sources. " + "Discarding previously decoded frames." + ) + frames = {} + early_frames = {} + index_bits = qr_index_bits + header_bits, data_bits_per_frame = frame_layout(index_bits) + data_bytes_per_frame = data_bits_per_frame // 8 bits = read_blocks(frame_np) idx, data_bits = decode_frame(bits, index_bits, header_bits) if idx is None: diff --git a/encode.py b/encode.py index c2bdf99..b1c6e70 100644 --- a/encode.py +++ b/encode.py @@ -234,7 +234,7 @@ def encode(input_path: str, output_path: str): total_frames = num_frames + METADATA_FRAMES duration_sec = total_frames / FRAME_RATE - video_bps = data_bits_per_frame * FRAME_RATE // 8 + video_bps = (data_bits_per_frame * FRAME_RATE) // 8 audio_byte_count = min(len(ecc_data), int(duration_sec * BYTES_PER_SEC_AUDIO)) audio_coverage_pct = audio_byte_count / len(ecc_data) * 100 From 6faff1d1cd82eb69e698da9cc6a7ab8be92df036 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:19:51 +0000 Subject: [PATCH 7/9] Polish index width docs Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/635ce0d2-bc41-442a-bce7-05690a2115c1 Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- README.md | 2 +- decode.py | 33 ++++++++++++++++----------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index dd4cc83..0a09557 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Each 1920×1080 frame contains a 30×16 grid of 64×64 pixel blocks: +--------+--------+--------+--------+--------+ ... +--------+ | SYNC 0 | SYNC 1 | SYNC 2 | SYNC 3 | SYNC 4 | | SYNC 7 | <- row 0, cols 0-7: sync pattern +--------+--------+--------+--------+--------+ +--------+ -| IDX 0 | IDX 1 | IDX 2 | ... | IDX 15 | <- row 0, cols 8-23: frame index (16-bit default; 32-bit continues past col 23) +| IDX 0 | IDX 1 | IDX 2 | ... | IDX 15 | <- row 0, cols 8-23: frame index (16-bit default; 32-bit spans cols 8-39) +--------+--------+--------+ +--------+ | DATA | DATA | DATA | DATA | DATA | ... | DATA | <- remaining 456 blocks: data +--------+--------+--------+--------+--------+ +--------+ diff --git a/decode.py b/decode.py index a0ae93d..5c93f55 100644 --- a/decode.py +++ b/decode.py @@ -252,23 +252,22 @@ def decode(video_path: str, output_dir: str = "."): if qr_meta: print(f" QR metadata decoded from frame {total}.") qr_index_bits = qr_meta.get("index_bits") - if qr_index_bits is None: - pass - elif qr_index_bits not in (DEFAULT_FRAME_INDEX_BITS, EXTENDED_FRAME_INDEX_BITS): - print(f" Warning: unsupported index width {qr_index_bits}; using {index_bits}-bit.") - elif qr_index_bits != index_bits: - old_index_bits = index_bits - if frames or early_frames: - print( - f" Warning: unexpected index width change from {old_index_bits} to " - f"{qr_index_bits} bits; this may indicate corrupted or mixed sources. " - "Discarding previously decoded frames." - ) - frames = {} - early_frames = {} - index_bits = qr_index_bits - header_bits, data_bits_per_frame = frame_layout(index_bits) - data_bytes_per_frame = data_bits_per_frame // 8 + if qr_index_bits is not None: + if qr_index_bits not in (DEFAULT_FRAME_INDEX_BITS, EXTENDED_FRAME_INDEX_BITS): + print(f" Warning: unsupported index width {qr_index_bits}; using {index_bits}-bit.") + elif qr_index_bits != index_bits: + old_index_bits = index_bits + if frames or early_frames: + print( + f" Warning: unexpected index width change from {old_index_bits} to " + f"{qr_index_bits} bits; this may indicate corrupted or mixed sources. " + "Discarding previously decoded frames." + ) + frames = {} + early_frames = {} + index_bits = qr_index_bits + header_bits, data_bits_per_frame = frame_layout(index_bits) + data_bytes_per_frame = data_bits_per_frame // 8 bits = read_blocks(frame_np) idx, data_bits = decode_frame(bits, index_bits, header_bits) if idx is None: From dd5c8954ed35d36ab61f73fc6e859f6c2d5e7c93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:20:29 +0000 Subject: [PATCH 8/9] Refactor frame count calc Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/635ce0d2-bc41-442a-bce7-05690a2115c1 Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- decode.py | 5 ++++- encode.py | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/decode.py b/decode.py index 5c93f55..5dca964 100644 --- a/decode.py +++ b/decode.py @@ -254,7 +254,10 @@ def decode(video_path: str, output_dir: str = "."): qr_index_bits = qr_meta.get("index_bits") if qr_index_bits is not None: if qr_index_bits not in (DEFAULT_FRAME_INDEX_BITS, EXTENDED_FRAME_INDEX_BITS): - print(f" Warning: unsupported index width {qr_index_bits}; using {index_bits}-bit.") + print( + f" Warning: unsupported index width {qr_index_bits}; falling back to " + f"{index_bits}-bit decoding (may fail)." + ) elif qr_index_bits != index_bits: old_index_bits = index_bits if frames or early_frames: diff --git a/encode.py b/encode.py index b1c6e70..5d79b1a 100644 --- a/encode.py +++ b/encode.py @@ -108,6 +108,10 @@ def frame_layout(index_bits: int) -> tuple[int, int]: return header_bits, data_bits_per_frame +def frames_needed(total_bits: int, data_bits_per_frame: int) -> int: + return math.ceil(total_bits / data_bits_per_frame) + + def index_to_bits(idx: int, index_bits: int) -> np.ndarray: return np.array( [(idx >> (index_bits - 1 - i)) & 1 for i in range(index_bits)], @@ -208,14 +212,14 @@ def encode(input_path: str, output_path: str): total_bits = len(ecc_data) * 8 _, data_bits_default = frame_layout(DEFAULT_FRAME_INDEX_BITS) - frames_16 = math.ceil(total_bits / data_bits_default) + frames_16 = frames_needed(total_bits, data_bits_default) if frames_16 <= MAX_FRAME_COUNT_DEFAULT: index_bits = DEFAULT_FRAME_INDEX_BITS data_bits_per_frame = data_bits_default num_frames = frames_16 else: _, data_bits_per_frame = frame_layout(EXTENDED_FRAME_INDEX_BITS) - num_frames = math.ceil(total_bits / data_bits_per_frame) + num_frames = frames_needed(total_bits, data_bits_per_frame) if num_frames > MAX_FRAME_COUNT_EXTENDED: raise ValueError( f"Payload needs {num_frames} frames which exceeds 32-bit frame index capacity " From 238960fbab6aa7d48dae3a6908461915d6ddfc12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 20:37:59 +0000 Subject: [PATCH 9/9] Fix QR metadata decode indentation Agent-Logs-Url: https://github.com/Advik-B/FrameVault/sessions/e3129a11-2efc-471c-8fc6-07ebd0bebf2e Co-authored-by: Advik-B <86160411+Advik-B@users.noreply.github.com> --- decode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decode.py b/decode.py index 5dca964..26ea4d0 100644 --- a/decode.py +++ b/decode.py @@ -248,7 +248,7 @@ def decode(video_path: str, output_dir: str = "."): for frame_np in stream_frames(video_path): total += 1 if total <= METADATA_FRAMES and qr_meta is None: - qr_meta = decode_qr_metadata(frame_np, qr_detector) + qr_meta = decode_qr_metadata(frame_np, qr_detector) if qr_meta: print(f" QR metadata decoded from frame {total}.") qr_index_bits = qr_meta.get("index_bits")