audio_fingerprint.py - amprenta pe anvelopa de energie, fara transcriere. md5 nu prinde duplicatele (urcari encodate separat, durate la 0.08s distanta). Validat: 1.000 pe perechea duplicat, 0.038 pe sesiuni diferite. check_duplicates.py - acelasi lucru pe transcrieri, cu --selftest. Scanat tot modulul Q&A: un singur duplicat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1
63 lines
2.5 KiB
Python
63 lines
2.5 KiB
Python
"""
|
|
Găsește sesiuni Q&A duplicate (același videoclip urcat sub două date).
|
|
Compară transcrierile la nivel de cuvinte normalizate (fără diacritice, fără
|
|
punctuație), pentru că whisper produce text ușor diferit la două rulări pe
|
|
aceeași sursă — un md5 pe audio nu le prinde.
|
|
|
|
Prag: >60% cuvinte potrivite ȘI un bloc comun de >=40 de cuvinte consecutive.
|
|
Blocul lung e cel care contează: două sesiuni diferite pe aceleași subiecte
|
|
împart multe cuvinte, dar niciodată 40 la rând.
|
|
"""
|
|
import difflib
|
|
import re
|
|
import sys
|
|
import unicodedata
|
|
from itertools import combinations
|
|
from pathlib import Path
|
|
|
|
|
|
def words(path: Path) -> list[str]:
|
|
t = unicodedata.normalize("NFKD", path.read_text(encoding="utf-8").lower())
|
|
return re.findall(r"[a-z]+", t.encode("ascii", "ignore").decode())
|
|
|
|
|
|
def is_duplicate(a: list[str], b: list[str]) -> tuple[bool, float, int]:
|
|
if not a or not b or min(len(a), len(b)) / max(len(a), len(b)) < 0.7:
|
|
return False, 0.0, 0 # lungimi prea diferite ca să fie același video
|
|
blocks = difflib.SequenceMatcher(None, a, b, autojunk=False).get_matching_blocks()
|
|
pct = 200 * sum(bl.size for bl in blocks) / (len(a) + len(b))
|
|
longest = max(bl.size for bl in blocks)
|
|
return pct > 60 and longest >= 40, pct, longest
|
|
|
|
|
|
def selftest():
|
|
base = "cand pretul ajunge in suport major si apare un buy doji intri cu toata pozitia".split() * 12
|
|
same = base[:-3] + "intri cu jumatate".split() # mici diferente de transcriere
|
|
other = "volumul arata cine domina piata in fiecare moment al zilei".split() * 12
|
|
assert is_duplicate(base, same)[0], "acelasi video nu a fost detectat"
|
|
assert not is_duplicate(base, other)[0], "sesiuni diferite marcate ca duplicat"
|
|
assert not is_duplicate(base, [])[0], "transcriere goala (inca se scrie) trebuie ignorata"
|
|
assert not is_duplicate(base, base[:20])[0], "lungimi foarte diferite trebuie ignorate"
|
|
print("selftest ok")
|
|
|
|
|
|
def main():
|
|
if "--selftest" in sys.argv:
|
|
selftest()
|
|
return 0
|
|
d = Path(sys.argv[1] if len(sys.argv) > 1 else "transcripts/09_Q_and_A")
|
|
docs = {p.stem: words(p) for p in sorted(d.glob("*.txt"))}
|
|
found = []
|
|
for x, y in combinations(docs, 2):
|
|
dup, pct, longest = is_duplicate(docs[x], docs[y])
|
|
if dup:
|
|
found.append((x, y))
|
|
print(f"DUPLICAT: {x} == {y} ({pct:.1f}% potrivite, bloc comun {longest} cuvinte)")
|
|
if not found:
|
|
print(f"Niciun duplicat între cele {len(docs)} transcrieri.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|