TuringsSolutions's picture
Create app.py
2ff4d2d verified
raw
history blame
27.3 kB
# ============================================================
# FLC v1.3 — Standalone Fibonacci Lattice Compression
# + Real .flc file format (v2 header)
# + Hologram preview PNG
# + "Holographic Unzip" progressive GIF (REAL partial reconstructions)
# + max_bands horizon control
# + Fibonacci spiral unfold layout (right panel)
# + NEW: Event-horizon ring overlay (left panel)
# + NEW: Fibonacci tile outlines (right panel)
#
# Notebook usage:
# enc = flc_encode_file("in.bin","out.flc","preview.png", unzip_gif="unzip.gif",
# block_len=1024, n_bands=10, base_step=0.004)
# dec = flc_decode_file("out.flc","recovered.bin", unzip_gif="decode_unzip.gif", max_bands=None)
#
# Partial/horizon:
# decp = flc_decode_file("out.flc","partial.bin", unzip_gif="partial_unzip.gif", max_bands=4)
#
# Depends: numpy, pillow
# ============================================================
import os, struct, math, argparse
import numpy as np
from PIL import Image, ImageDraw
PHI = (1.0 + 5.0**0.5) / 2.0
MAGIC = b"FLC1\x00\x00\x00\x00" # 8 bytes
VER_V2 = 2 # float64 params
# --------------------------
# Fibonacci utilities
# --------------------------
def fibonacci_sequence(n):
fibs = [1, 2]
while len(fibs) < n:
fibs.append(fibs[-1] + fibs[-2])
return np.array(fibs[:n], dtype=np.int64)
def fibonacci_sequence_std(n):
fibs = [1, 1]
while len(fibs) < n:
fibs.append(fibs[-1] + fibs[-2])
return np.array(fibs[:n], dtype=np.int64)
def fibonacci_frequency_boundaries(n_coeffs: int, n_bands: int):
"""
Strict: returns exactly n_bands+1 boundaries => exactly n_bands intervals.
Deterministic repair enforces strict monotone boundaries.
"""
if n_bands < 2:
return [0, n_coeffs]
fibs = fibonacci_sequence(n_bands).astype(np.float64)
w = fibs / (fibs.sum() + 1e-12)
cum = np.cumsum(w)
b = [0]
for i in range(n_bands - 1):
bi = int(round(n_coeffs * cum[i]))
b.append(bi)
b.append(n_coeffs)
b = [max(0, min(n_coeffs, int(x))) for x in b]
b[0] = 0
b[-1] = n_coeffs
# strictify forward
for i in range(1, len(b) - 1):
if b[i] <= b[i-1]:
b[i] = b[i-1] + 1
# strictify backward
for i in range(len(b) - 2, 0, -1):
if b[i] >= b[i+1]:
b[i] = b[i+1] - 1
# remove invalid interior points
b = [0] + [x for x in b[1:-1] if 0 < x < n_coeffs] + [n_coeffs]
# repack if we lost points (n_coeffs small or rounding)
while len(b) < n_bands + 1:
gaps = [(b[i+1] - b[i], i) for i in range(len(b) - 1)]
gaps.sort(reverse=True)
_, gi = gaps[0]
mid = (b[gi] + b[gi+1]) // 2
if mid == b[gi] or mid == b[gi+1]:
break
b.insert(gi+1, mid)
# trim if we have too many
while len(b) > n_bands + 1:
gaps = [(b[i+1] - b[i], i) for i in range(len(b) - 1)]
gaps.sort() # smallest first
_, gi = gaps[0]
if 0 < gi+1 < len(b)-1:
b.pop(gi+1)
else:
break
return b
# --------------------------
# Orthonormal DCT-II / IDCT (no scipy)
# --------------------------
def dct_ortho_1d(x: np.ndarray) -> np.ndarray:
x = np.asarray(x, dtype=np.float64)
N = x.shape[0]
v = np.concatenate([x, x[::-1]])
V = np.fft.fft(v)
k = np.arange(N)
X = np.real(V[:N] * np.exp(-1j * np.pi * k / (2 * N)))
X *= 2.0
X[0] *= (1.0 / math.sqrt(4 * N))
X[1:] *= (1.0 / math.sqrt(2 * N))
return X
def idct_ortho_1d(X: np.ndarray) -> np.ndarray:
X = np.asarray(X, dtype=np.float64)
N = X.shape[0]
x = X.copy()
x0 = x[0] * math.sqrt(4 * N)
xr = x[1:] * math.sqrt(2 * N)
c = np.empty(N, dtype=np.complex128)
c[0] = x0 / 2.0
c[1:] = xr / 2.0
k = np.arange(N)
c = c * np.exp(1j * np.pi * k / (2 * N))
V = np.zeros(2 * N, dtype=np.complex128)
V[:N] = c
V[N+1:] = np.conj(c[1:][::-1])
v = np.fft.ifft(V).real
return v[:N]
def dct_blocks_ortho(x_blocks: np.ndarray) -> np.ndarray:
B, L = x_blocks.shape
out = np.empty((B, L), dtype=np.float64)
for i in range(B):
out[i] = dct_ortho_1d(x_blocks[i])
return out
def idct_blocks_ortho(X_blocks: np.ndarray) -> np.ndarray:
B, L = X_blocks.shape
out = np.empty((B, L), dtype=np.float64)
for i in range(B):
out[i] = idct_ortho_1d(X_blocks[i])
return out
# --------------------------
# Bit IO
# --------------------------
class BitWriter:
def __init__(self):
self.buf = bytearray()
self.acc = 0
self.nbits = 0
def write_bit(self, b: int):
self.acc = (self.acc << 1) | (b & 1)
self.nbits += 1
if self.nbits == 8:
self.buf.append(self.acc & 0xFF)
self.acc = 0
self.nbits = 0
def finish(self) -> bytes:
if self.nbits:
self.acc <<= (8 - self.nbits)
self.buf.append(self.acc & 0xFF)
self.acc = 0
self.nbits = 0
return bytes(self.buf)
class BitReader:
def __init__(self, data: bytes):
self.data = data
self.i = 0
self.acc = 0
self.nbits = 0
def read_bit(self) -> int:
if self.nbits == 0:
if self.i >= len(self.data):
raise EOFError("BitReader EOF")
self.acc = self.data[self.i]
self.i += 1
self.nbits = 8
b = (self.acc >> (self.nbits - 1)) & 1
self.nbits -= 1
return b
# --------------------------
# Fibonacci coding
# --------------------------
def _fib_numbers_upto(n: int):
fibs = [1, 2]
while fibs[-1] <= n:
fibs.append(fibs[-1] + fibs[-2])
return fibs
def fib_encode_nonneg(bw: BitWriter, n: int):
m = int(n) + 1
fibs = _fib_numbers_upto(m)
bits = [0] * (len(fibs) - 1)
rem = m
for i in reversed(range(len(bits))):
f = fibs[i]
if f <= rem:
bits[i] = 1
rem -= f
hi = max((i for i, b in enumerate(bits) if b), default=0)
for i in range(hi + 1):
bw.write_bit(bits[i])
bw.write_bit(1)
def fib_decode_nonneg(br: BitReader) -> int:
fibs = [1, 2]
bits = []
prev = 0
while True:
b = br.read_bit()
bits.append(b)
if prev == 1 and b == 1:
break
prev = b
if len(bits) > len(fibs):
fibs.append(fibs[-1] + fibs[-2])
payload = bits[:-1]
while len(fibs) < len(payload):
fibs.append(fibs[-1] + fibs[-2])
m = 0
for i, bi in enumerate(payload):
if bi:
m += fibs[i]
return m - 1
def zigzag_encode_i64(x: int) -> int:
x = int(x)
return (x << 1) ^ (x >> 63)
def zigzag_decode_i64(u: int) -> int:
u = int(u)
return (u >> 1) ^ (-(u & 1))
def rle_fib_encode_ints(ints: np.ndarray) -> bytes:
bw = BitWriter()
ints = ints.astype(np.int64, copy=False)
zrun = 0
for v in ints:
if v == 0:
zrun += 1
continue
if zrun:
bw.write_bit(0)
fib_encode_nonneg(bw, zrun)
zrun = 0
bw.write_bit(1)
fib_encode_nonneg(bw, zigzag_encode_i64(int(v)))
if zrun:
bw.write_bit(0)
fib_encode_nonneg(bw, zrun)
return bw.finish()
def rle_fib_decode_ints(payload: bytes, n_out: int) -> np.ndarray:
br = BitReader(payload)
out = np.zeros(n_out, dtype=np.int64)
i = 0
while i < n_out:
tag = br.read_bit()
if tag == 0:
k = fib_decode_nonneg(br)
i = min(n_out, i + k)
else:
u = fib_decode_nonneg(br)
out[i] = zigzag_decode_i64(u)
i += 1
return out
def delta1d(x: np.ndarray) -> np.ndarray:
x = x.astype(np.int64, copy=False)
out = np.empty_like(x, dtype=np.int64)
prev = 0
for i, v in enumerate(x):
out[i] = int(v) - prev
prev = int(v)
return out
def inv_delta1d(d: np.ndarray) -> np.ndarray:
d = d.astype(np.int64, copy=False)
out = np.empty_like(d, dtype=np.int64)
acc = 0
for i, v in enumerate(d):
acc += int(v)
out[i] = acc
return out
# --------------------------
# φ-scaled band quantization
# --------------------------
def band_quantize_dct(coeffs: np.ndarray, boundaries, base_step: float, phi: float = PHI):
B, L = coeffs.shape
q = np.zeros((B, L), dtype=np.int32)
for bi in range(len(boundaries) - 1):
a, b = boundaries[bi], boundaries[bi + 1]
if a >= b:
continue
step = base_step * (phi ** bi)
q[:, a:b] = np.round(coeffs[:, a:b] / step).astype(np.int32)
return q
def band_dequantize_dct(q: np.ndarray, boundaries, base_step: float, phi: float = PHI):
B, L = q.shape
coeffs = np.zeros((B, L), dtype=np.float64)
for bi in range(len(boundaries) - 1):
a, b = boundaries[bi], boundaries[bi + 1]
if a >= b:
continue
step = base_step * (phi ** bi)
coeffs[:, a:b] = q[:, a:b].astype(np.float64) * step
return coeffs
# --------------------------
# Hologram (left panel) + horizon ring overlay
# --------------------------
def choose_fib_grid(n_symbols: int) -> int:
N = int(math.ceil(math.sqrt(n_symbols)))
fibs = [1, 2]
while fibs[-1] < N:
fibs.append(fibs[-1] + fibs[-2])
return fibs[-1]
def ints_to_constellation(zints: np.ndarray):
zints = zints.astype(np.int64)
v = np.tanh(zints / 32.0).astype(np.float64)
k = np.arange(v.size, dtype=np.float64)
golden = 2 * math.pi / (PHI**2)
theta = golden * k + 2.0 * math.pi * (v * 0.25)
r = 1.0 + 0.35 * np.abs(v)
return (r * np.cos(theta) + 1j * r * np.sin(theta)).astype(np.complex64)
def hologram_spectrum_image(zints: np.ndarray, max_symbols: int = 262144):
if zints.size > max_symbols:
zints = zints[:max_symbols]
syms = ints_to_constellation(zints)
N = choose_fib_grid(syms.size)
total = N * N
if syms.size < total:
syms = np.pad(syms, (0, total - syms.size), constant_values=(1+0j)).astype(np.complex64)
U = syms.reshape(N, N)
F = np.fft.fftshift(np.fft.fft2(U))
mag = np.log1p(np.abs(F)).astype(np.float32)
mag -= mag.min()
mag /= (mag.max() + 1e-12)
return (mag * 255).astype(np.uint8)
def make_hologram_preview(zints: np.ndarray, out_png: str, max_symbols: int = 262144):
img = hologram_spectrum_image(zints, max_symbols=max_symbols)
Image.fromarray(img).save(out_png)
def overlay_event_horizon_ring(imL: Image.Image, t: int, T: int):
"""
Draw a glowing event-horizon ring (as 3 concentric ellipses) on the hologram.
Radius expands with progress t/T.
"""
W, H = imL.size
cx, cy = W // 2, H // 2
# start small, expand
r0 = int(0.08 * min(W, H))
r1 = int((0.45 * min(W, H)) * (t / max(1, T)) + r0)
rgb = imL.convert("RGB")
d = ImageDraw.Draw(rgb)
# glow: 3 rings
for k, alpha in [(0, 255), (3, 170), (6, 90)]:
rr = r1 + k
bbox = (cx - rr, cy - rr, cx + rr, cy + rr)
# gold-ish ring
col = (255, 215, 0) if alpha == 255 else (220, 180, 0)
d.ellipse(bbox, outline=col, width=2)
# center dot (singularity hint)
d.ellipse((cx-2, cy-2, cx+2, cy+2), fill=(255, 215, 0))
return rgb
# --------------------------
# Fibonacci spiral unfold (right panel) + tile outlines
# --------------------------
def fibonacci_spiral_tiles_for_area(n_pixels: int, max_tiles: int = 30):
fibs = fibonacci_sequence_std(max_tiles) # 1,1,2,3,5...
sizes = []
area = 0
for s in fibs:
sizes.append(int(s))
area += int(s) * int(s)
if area >= n_pixels:
break
return sizes
def fibonacci_spiral_tile_positions(sizes):
tiles = []
s0 = sizes[0]
minx, miny, maxx, maxy = 0, 0, s0, s0
tiles.append((0, 0, s0))
for i in range(1, len(sizes)):
s = sizes[i]
d = (i - 1) % 4 # right, up, left, down
if d == 0: # right
x, y = maxx, miny
elif d == 1: # up
x, y = maxx - s, maxy
elif d == 2: # left
x, y = minx - s, maxy - s
else: # down
x, y = minx, miny - s
tiles.append((x, y, s))
minx = min(minx, x)
miny = min(miny, y)
maxx = max(maxx, x + s)
maxy = max(maxy, y + s)
bbox = (minx, miny, maxx, maxy)
return tiles, bbox
def bytes_to_fib_spiral_image(data: bytes, max_pixels: int = 262144, want_tiles=False):
arr = np.frombuffer(data, dtype=np.uint8)
if arr.size > max_pixels:
arr = arr[:max_pixels]
n = int(arr.size)
if n == 0:
if want_tiles:
return np.zeros((1, 1), dtype=np.uint8), [(0,0,1)], (0,0,1,1)
return np.zeros((1, 1), dtype=np.uint8)
sizes = fibonacci_spiral_tiles_for_area(n_pixels=n, max_tiles=32)
tiles, (minx, miny, maxx, maxy) = fibonacci_spiral_tile_positions(sizes)
W = int(maxx - minx)
H = int(maxy - miny)
img = np.zeros((H, W), dtype=np.uint8)
idx = 0
for (x, y, s) in tiles:
if idx >= n:
break
x0 = int(x - minx)
y0_up = int(y - miny)
y0 = int((H - (y0_up + s)))
take = min(s * s, n - idx)
block = arr[idx:idx + take]
if take < s * s:
block = np.pad(block, (0, s * s - take), constant_values=0)
block2d = block.reshape(s, s)
img[y0:y0+s, x0:x0+s] = block2d
idx += take
if want_tiles:
return img, tiles, (minx, miny, maxx, maxy)
return img
def overlay_tile_outlines(imL: Image.Image, tiles, bbox_info, outline=(120,120,120)):
"""
Draw square outlines for the Fibonacci tiling on the right panel.
`tiles` are in y-up coords with original bbox min offsets.
"""
rgb = imL.convert("RGB")
d = ImageDraw.Draw(rgb)
# rebuild H,W from image
W, H = rgb.size
# bbox is y-up coords: minx,miny,maxx,maxy
minx, miny, maxx, maxy = bbox_info
# For each tile, map y-up to y-down in image coords
for (x, y, s) in tiles:
x0 = int(x - minx)
y0_up = int(y - miny)
y0 = int((H - (y0_up + s)))
d.rectangle((x0, y0, x0 + s - 1, y0 + s - 1), outline=outline, width=1)
return rgb
# --------------------------
# GIF assembly
# --------------------------
def save_gif(frames, out_gif: str, duration=90):
if not frames:
return
frames[0].save(out_gif, save_all=True, append_images=frames[1:], duration=duration, loop=0, optimize=True)
def make_unzip_gif_from_Q(Q_full: np.ndarray,
boundaries,
base_step: float,
mu: float,
n_bytes: int,
out_gif: str,
max_bands: int = None,
gif_scale: int = 2,
max_symbols: int = 262144,
max_pixels: int = 262144,
draw_outlines: bool = True,
draw_horizon: bool = True):
n_total_bands = len(boundaries) - 1
T = n_total_bands if max_bands is None else max(0, min(int(max_bands), n_total_bands))
if T == 0:
T = 1
frames = []
band_slices = [(boundaries[i], boundaries[i+1]) for i in range(n_total_bands)]
for t in range(1, T + 1):
Q_part = np.zeros_like(Q_full)
for bi in range(t):
a, b = band_slices[bi]
if a < b:
Q_part[:, a:b] = Q_full[:, a:b]
# partial recon bytes
C_part = band_dequantize_dct(Q_part, boundaries, base_step=base_step, phi=PHI)
X_part = idct_blocks_ortho(C_part)
x = X_part.reshape(-1)[:n_bytes]
x = (x * 127.5) + float(mu)
x = np.clip(np.round(x), 0, 255).astype(np.uint8).tobytes()
# left hologram
qflat = Q_part.reshape(-1).astype(np.int64)
holo_u8 = hologram_spectrum_image(qflat, max_symbols=max_symbols)
A = Image.fromarray(holo_u8).convert("L")
if gif_scale != 1:
A = A.resize((A.size[0]*gif_scale, A.size[1]*gif_scale), Image.NEAREST)
# horizon ring overlay
if draw_horizon:
A_rgb = overlay_event_horizon_ring(A, t=t, T=T)
else:
A_rgb = A.convert("RGB")
# right spiral image
field_u8, tiles, bbox_info = bytes_to_fib_spiral_image(x, max_pixels=max_pixels, want_tiles=True)
Bimg = Image.fromarray(field_u8).convert("L")
if gif_scale != 1:
Bimg = Bimg.resize((Bimg.size[0]*gif_scale, Bimg.size[1]*gif_scale), Image.NEAREST)
if draw_outlines:
# scale tile coordinates too (by rendering outlines after scaling)
# easiest: draw outlines on scaled image using scaled bbox & tiles
# so adjust bbox and tiles by gif_scale
minx, miny, maxx, maxy = bbox_info
tiles_s = [(x*gif_scale, y*gif_scale, s*gif_scale) for (x,y,s) in tiles]
bbox_s = (minx*gif_scale, miny*gif_scale, maxx*gif_scale, maxy*gif_scale)
B_rgb = overlay_tile_outlines(Bimg, tiles_s, bbox_s, outline=(120,120,120))
else:
B_rgb = Bimg.convert("RGB")
# match heights by padding
H = max(A_rgb.size[1], B_rgb.size[1])
def pad_to_h_rgb(im, H):
if im.size[1] == H:
return im
canvas = Image.new("RGB", (im.size[0], H), (0,0,0))
canvas.paste(im, (0, (H - im.size[1])//2))
return canvas
A_rgb = pad_to_h_rgb(A_rgb, H)
B_rgb = pad_to_h_rgb(B_rgb, H)
W = A_rgb.size[0]
canvas = Image.new("RGB", (W*2, H + 44), (0, 0, 0))
canvas.paste(A_rgb, (0, 44))
canvas.paste(B_rgb, (W, 44))
draw = ImageDraw.Draw(canvas)
draw.text((12, 10), f"FLC HOLOGRAPHIC UNZIP | bands {t}/{T} (horizon={T})", fill=(255, 215, 0))
draw.text((12, 26), "LEFT: hologram + event horizon | RIGHT: Fibonacci spiral + tile outlines", fill=(180, 180, 180))
frames.append(canvas)
save_gif(frames, out_gif, duration=90)
# ============================================================
# Encoder / Decoder
# ============================================================
def flc_encode_file(in_path: str,
out_flc: str,
preview_png: str = None,
unzip_gif: str = None,
block_len: int = 1024,
n_bands: int = 10,
base_step: float = 0.004,
use_mean_center: bool = True,
gif_scale: int = 2,
gif_horizon: int = None,
draw_horizon: bool = True,
draw_outlines: bool = True):
raw = open(in_path, "rb").read()
n_bytes = len(raw)
x = np.frombuffer(raw, dtype=np.uint8).astype(np.float64)
mu = float(np.mean(x)) if use_mean_center else 127.5
x = (x - mu) / 127.5
pad = (-x.size) % block_len
if pad:
x = np.pad(x, (0, pad), constant_values=0.0)
n_blocks = x.size // block_len
X = x.reshape(n_blocks, block_len)
C = dct_blocks_ortho(X)
boundaries = fibonacci_frequency_boundaries(block_len, n_bands)
Q = band_quantize_dct(C, boundaries, base_step=base_step, phi=PHI)
q_flat = Q.reshape(-1).astype(np.int64)
d = delta1d(q_flat)
payload = rle_fib_encode_ints(d)
header = struct.pack(
"<8sH Q I I H d d H",
MAGIC, VER_V2,
int(n_bytes),
int(block_len),
int(n_blocks),
int(n_bands),
float(base_step),
float(mu),
int(len(boundaries)),
)
bnds = struct.pack("<" + "I"*len(boundaries), *[int(b) for b in boundaries])
paylen = struct.pack("<I", int(len(payload)))
with open(out_flc, "wb") as f:
f.write(header)
f.write(bnds)
f.write(paylen)
f.write(payload)
if preview_png is not None:
make_hologram_preview(q_flat, preview_png)
if unzip_gif is not None:
make_unzip_gif_from_Q(
Q_full=Q,
boundaries=boundaries,
base_step=float(base_step),
mu=float(mu),
n_bytes=int(n_bytes),
out_gif=unzip_gif,
max_bands=gif_horizon,
gif_scale=gif_scale,
draw_horizon=draw_horizon,
draw_outlines=draw_outlines,
)
return {
"n_bytes": int(n_bytes),
"block_len": int(block_len),
"n_blocks": int(n_blocks),
"n_bands": int(n_bands),
"base_step": float(base_step),
"mu": float(mu),
"payload_len": int(len(payload)),
"out_flc": out_flc,
"preview_png": preview_png,
"unzip_gif": unzip_gif,
"ratio": (os.path.getsize(out_flc) / max(1, n_bytes)),
}
def flc_decode_file(in_flc: str,
out_path: str,
unzip_gif: str = None,
max_bands: int = None,
gif_scale: int = 2,
draw_horizon: bool = True,
draw_outlines: bool = True):
blob = open(in_flc, "rb").read()
off = 0
(magic, ver, n_bytes, block_len, n_blocks, n_bands, base_step, mu, bnd_len) = struct.unpack_from(
"<8sH Q I I H d d H", blob, off
)
off += struct.calcsize("<8sH Q I I H d d H")
if magic != MAGIC:
raise ValueError("Bad magic (not FLC1)")
if ver != VER_V2:
raise ValueError(f"Unsupported version {ver}")
boundaries = list(struct.unpack_from("<" + "I"*bnd_len, blob, off))
off += 4 * bnd_len
(payload_len,) = struct.unpack_from("<I", blob, off)
off += 4
payload = blob[off:off+payload_len]
off += payload_len
n_coeffs = int(n_blocks) * int(block_len)
d = rle_fib_decode_ints(payload, n_coeffs)
q_flat = inv_delta1d(d).astype(np.int64)
Q_full = q_flat.reshape(int(n_blocks), int(block_len)).astype(np.int32)
# horizon
n_total_bands = len(boundaries) - 1
bands_to_apply = n_total_bands if max_bands is None else max(0, min(int(max_bands), n_total_bands))
if bands_to_apply < n_total_bands:
Q_use = np.zeros_like(Q_full)
for bi in range(bands_to_apply):
a, b = boundaries[bi], boundaries[bi+1]
if a < b:
Q_use[:, a:b] = Q_full[:, a:b]
else:
Q_use = Q_full
C = band_dequantize_dct(Q_use, boundaries, base_step=float(base_step), phi=PHI)
X = idct_blocks_ortho(C)
x = X.reshape(-1)[:int(n_bytes)]
x = (x * 127.5) + float(mu)
x = np.clip(np.round(x), 0, 255).astype(np.uint8)
with open(out_path, "wb") as f:
f.write(x.tobytes())
if unzip_gif is not None:
make_unzip_gif_from_Q(
Q_full=Q_full,
boundaries=boundaries,
base_step=float(base_step),
mu=float(mu),
n_bytes=int(n_bytes),
out_gif=unzip_gif,
max_bands=(bands_to_apply if bands_to_apply > 0 else 1),
gif_scale=gif_scale,
draw_horizon=draw_horizon,
draw_outlines=draw_outlines,
)
return {
"n_bytes": int(n_bytes),
"block_len": int(block_len),
"n_blocks": int(n_blocks),
"n_bands": int(n_bands),
"base_step": float(base_step),
"mu": float(mu),
"payload_len": int(payload_len),
"out_path": out_path,
"unzip_gif": unzip_gif,
"bands_applied": int(bands_to_apply),
}
# --------------------------
# Quality metric
# --------------------------
def cosine_similarity_bytes(a: bytes, b: bytes) -> float:
x = np.frombuffer(a, dtype=np.uint8).astype(np.float64)
y = np.frombuffer(b, dtype=np.uint8).astype(np.float64)
n = min(x.size, y.size)
x = x[:n]; y = y[:n]
x -= x.mean(); y -= y.mean()
nx = np.linalg.norm(x) + 1e-12
ny = np.linalg.norm(y) + 1e-12
return float(np.dot(x, y) / (nx * ny))
# --------------------------
# CLI wrapper (call explicitly)
# --------------------------
def flc_cli(argv=None):
ap = argparse.ArgumentParser(description="FLC v1.3: Fibonacci-banded DCT + φ quant + Fibonacci coding + horizon ring + tile outlines")
ap.add_argument("inp", type=str)
ap.add_argument("out_flc", type=str)
ap.add_argument("preview_png", type=str)
ap.add_argument("unzip_gif", type=str)
ap.add_argument("out_recovered", type=str)
ap.add_argument("--block", type=int, default=1024)
ap.add_argument("--bands", type=int, default=10)
ap.add_argument("--step", type=float, default=0.004)
ap.add_argument("--no_center", action="store_true")
ap.add_argument("--horizon", type=int, default=None)
ap.add_argument("--gif_scale", type=int, default=2)
ap.add_argument("--no_ring", action="store_true")
ap.add_argument("--no_outlines", action="store_true")
args = ap.parse_args(argv)
enc = flc_encode_file(
args.inp, args.out_flc, args.preview_png, unzip_gif=args.unzip_gif,
block_len=args.block, n_bands=args.bands, base_step=args.step,
use_mean_center=(not args.no_center),
gif_scale=args.gif_scale,
gif_horizon=None,
draw_horizon=(not args.no_ring),
draw_outlines=(not args.no_outlines),
)
print("ENC:", enc)
dec = flc_decode_file(
args.out_flc, args.out_recovered,
unzip_gif="decode_" + os.path.basename(args.unzip_gif),
max_bands=args.horizon,
gif_scale=args.gif_scale,
draw_horizon=(not args.no_ring),
draw_outlines=(not args.no_outlines),
)
print("DEC:", dec)
a = open(args.inp, "rb").read()
b = open(args.out_recovered, "rb").read()
cs = cosine_similarity_bytes(a, b)
print("Cosine similarity (bytes):", cs, " => ", cs*100, "%")
print("ORIG:", os.path.getsize(args.inp), "FLC:", os.path.getsize(args.out_flc),
"RATIO:", os.path.getsize(args.out_flc)/max(1, os.path.getsize(args.inp)))
# ============================================================
# Notebook demo (run manually)
# ============================================================
with open("test_input.bin","wb") as f:
f.write(b"Black hole horizon unzip meets Fibonacci spiral. " * 220)
enc = flc_encode_file("test_input.bin","test_output.flc","test_preview.png",
unzip_gif="test_unzip.gif",
block_len=1024, n_bands=10, base_step=0.004,
gif_scale=2, draw_horizon=True, draw_outlines=True)
print("ENC:", enc)
dec = flc_decode_file("test_output.flc","test_recovered.bin",
unzip_gif="test_decode_unzip.gif",
max_bands=None, gif_scale=2,
draw_horizon=True, draw_outlines=True)
print("DEC:", dec)
a = open("test_input.bin","rb").read()
b = open("test_recovered.bin","rb").read()
print("Cosine similarity:", cosine_similarity_bytes(a,b))
decp = flc_decode_file("test_output.flc","test_partial.bin",
unzip_gif="test_partial_unzip.gif",
max_bands=4, gif_scale=2,
draw_horizon