File size: 2,230 Bytes
32800ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
"""Image / audio decoding helpers."""

from __future__ import annotations

import base64
import io
from typing import Optional

import numpy as np
from PIL import Image


def decode_base64_image(b64: str) -> bytes:
    """Decode a base64 string to raw image bytes.

    Strips data URL prefix if present.
    Raises ValueError on bad input.
    """
    if not b64:
        raise ValueError("empty base64 image")
    if "," in b64 and b64.startswith("data:"):
        b64 = b64.split(",", 1)[1]
    try:
        return base64.b64decode(b64, validate=True)
    except Exception as exc:
        raise ValueError(f"invalid base64: {exc}") from exc


def decode_base64_audio(b64: str) -> bytes:
    """Decode a base64 string to raw audio bytes."""
    if not b64:
        raise ValueError("empty base64 audio")
    if "," in b64 and b64.startswith("data:"):
        b64 = b64.split(",", 1)[1]
    try:
        return base64.b64decode(b64, validate=True)
    except Exception as exc:
        raise ValueError(f"invalid base64: {exc}") from exc


def image_to_pil(data: bytes) -> Image.Image:
    """Decode image bytes to a PIL Image. Raises ValueError on failure."""
    try:
        img = Image.open(io.BytesIO(data))
        img.load()
        if img.mode not in ("RGB", "RGBA", "L"):
            img = img.convert("RGB")
        return img
    except Exception as exc:
        raise ValueError(f"could not decode image: {exc}") from exc


def perceptual_hash(data: bytes, size: int = 16) -> str:
    """Compute a perceptual hash of an image for cache lookup.

    Returns a 64-char hex string. Robust to small JPEG re-encodes
    and minor color shifts.
    """
    img = image_to_pil(data).convert("L").resize((size + 1, size))
    pixels = np.asarray(img, dtype=np.float32)
    diff = pixels[:, 1:] > pixels[:, :-1]
    bits = diff.flatten()
    h = 0
    for bit in bits:
        h = (h << 1) | int(bit)
    return format(h, f"0{size*size // 4}x")


def audio_size_hash(data: bytes) -> str:
    """Cheap audio hash: size + first/last 1KB + sha-like prefix."""
    import hashlib
    h = hashlib.sha256()
    h.update(len(data).to_bytes(8, "big"))
    h.update(data[:1024])
    h.update(data[-1024:])
    return h.hexdigest()[:32]