feat: harden dedupe runtime and NAS deployment
This commit is contained in:
+25
-13
@@ -5,6 +5,8 @@ import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
MAX_SECONDS = 120
|
||||
MAX_BIT_ERROR = 2
|
||||
MAX_ALIGN_OFFSET = 120
|
||||
|
||||
|
||||
def chromaprint(path: Path) -> str | None:
|
||||
@@ -13,26 +15,36 @@ def chromaprint(path: Path) -> str | None:
|
||||
["fpcalc", "-length", str(MAX_SECONDS), "-json", str(path)],
|
||||
capture_output=True, text=True, timeout=180, check=True,
|
||||
)
|
||||
return json.loads(p.stdout).get("fingerprint")
|
||||
value = json.loads(p.stdout).get("fingerprint")
|
||||
return value if isinstance(value, str) and value else None
|
||||
except (OSError, subprocess.SubprocessError, ValueError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
def _popcount(value: int) -> int:
|
||||
return value.bit_count()
|
||||
|
||||
|
||||
def similarity(a: str, b: str) -> float:
|
||||
"""Conservative normalized positional similarity for equal-length prefixes."""
|
||||
"""Compare Chromaprint fingerprints using the AcoustID alignment method.
|
||||
|
||||
This is deliberately bounded to the local fingerprint comparison and does
|
||||
not contact the AcoustID service.
|
||||
"""
|
||||
if not a or not b:
|
||||
return 0.0
|
||||
n = min(len(a), len(b))
|
||||
if not n:
|
||||
return 0.0
|
||||
# Chromaprint JSON fingerprints are comma-separated integer features.
|
||||
try:
|
||||
aa, bb = [int(x) for x in a.split(",")], [int(x) for x in b.split(",")]
|
||||
except ValueError:
|
||||
import chromaprint as cp
|
||||
aa = [int(x) for x in cp.decode_fingerprint(a.encode())[0]]
|
||||
bb = [int(x) for x in cp.decode_fingerprint(b.encode())[0]]
|
||||
except (ImportError, AttributeError, TypeError, ValueError):
|
||||
return 0.0
|
||||
n = min(len(aa), len(bb))
|
||||
if not n:
|
||||
if not aa or not bb:
|
||||
return 0.0
|
||||
# Feature-wise equality is intentionally conservative. A future backend
|
||||
# can replace this with a library-specific fingerprint distance.
|
||||
return sum(x == y for x, y in zip(aa[:n], bb[:n])) / n
|
||||
|
||||
counts = [0] * (len(aa) + len(bb) + 1)
|
||||
for i, av in enumerate(aa):
|
||||
for j in range(max(0, i - MAX_ALIGN_OFFSET), min(len(bb), i + MAX_ALIGN_OFFSET)):
|
||||
if _popcount(av ^ bb[j]) <= MAX_BIT_ERROR:
|
||||
counts[i - j + len(bb)] += 1
|
||||
return max(counts) / min(len(aa), len(bb))
|
||||
|
||||
Reference in New Issue
Block a user