39 lines
1.2 KiB
Python
39 lines
1.2 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
MAX_SECONDS = 120
|
|
|
|
|
|
def chromaprint(path: Path) -> str | None:
|
|
try:
|
|
p = subprocess.run(
|
|
["fpcalc", "-length", str(MAX_SECONDS), "-json", str(path)],
|
|
capture_output=True, text=True, timeout=180, check=True,
|
|
)
|
|
return json.loads(p.stdout).get("fingerprint")
|
|
except (OSError, subprocess.SubprocessError, ValueError, json.JSONDecodeError):
|
|
return None
|
|
|
|
|
|
def similarity(a: str, b: str) -> float:
|
|
"""Conservative normalized positional similarity for equal-length prefixes."""
|
|
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:
|
|
return 0.0
|
|
n = min(len(aa), len(bb))
|
|
if not n:
|
|
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
|