From f0b8f28fa9df56b5c12b889bf160265a1f331b3e Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sat, 12 Sep 2026 17:40:18 +0000 Subject: [PATCH] Detectare duplicate: Q&A 18.08.2026 este acelasi video cu 11.08.2026 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) Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1 --- PROGRESS.md | 24 + audio_fingerprint.py | 84 + check_duplicates.py | 62 + summaries/09_QA/03_2026-08-11.html | 6 + summaries/09_QA/03_2026-08-11.md | 5 + summaries/09_QA/INDEX_TEMATIC.html | 2 + summaries/09_QA/INDEX_TEMATIC.md | 3 + transcripts/09_Q_and_A/10_02122025.txt | 2829 ++++++++++++++++++++++++ transcripts/09_Q_and_A/11_25112025.txt | 0 9 files changed, 3015 insertions(+) create mode 100644 audio_fingerprint.py create mode 100644 check_duplicates.py create mode 100644 transcripts/09_Q_and_A/11_25112025.txt diff --git a/PROGRESS.md b/PROGRESS.md index 28a1e75..e5fdd7e 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -70,6 +70,30 @@ Zilnică (147). Decizii luate cu el: generic câștigă clar și nu e nici mai lent (moltbot small-ro: 2.26x realtime; local medium: 1.78x). +### Duplicate în curs — verifică ÎNTOTDEAUNA înainte de sumarizare +Cursul publică aceeași înregistrare sub două date. Găsit: **Q&A 18.08.2026 +este identic cu 11.08.2026** (lecțiile 2 și 3). Sumarizat o singură dată, +sub 11.08.2026, cu notă în fișier și în `INDEX_TEMATIC.md`. + +Două unelte, ambele în rădăcina repo-ului: +- `audio_fingerprint.py` — rulează pe mp3-uri **înainte** de transcriere. + Amprentă = RMS pe ferestre de o secundă, corelat peste decalaje de ±15s. + `md5` NU prinde duplicatele: cele două urcări sunt encodate separat, deci + bytes diferiți și durate care diferă cu ~0.08s. Validat pe perechea + cunoscută: **1.000** pentru duplicat, **0.038** pentru sesiuni diferite. + Prag: >0.90 = duplicat, 0.70-0.90 = verifică manual. + `\.venv/bin/python audio_fingerprint.py "audio/09_Q_and_A/*.mp3"` +- `check_duplicates.py` — pe transcrieri, pentru ce e deja transcris + (`transcribe.py` șterge audio-ul după succes). Are `--selftest`. + +Rulate pe toate cele 28 de sesiuni Q&A: **un singur duplicat**, cel de mai +sus. Atenție la durate apropiate care NU sunt duplicate — 13_28102025 și +16_07102025 diferă cu 0.288s dar sunt sesiuni complet diferite; verificat +prin transcrierea unor felii de 200s din mijloc. + +**Rulează amândouă pe Analiza Zilnică (147 lecții) înainte de transcriere** — +acolo un duplicat costă ore, nu minute. + ### Ce urmează după Q&A 1. Cele 5-6 lecții 1:1 (module 11, 17, 18, 20, 21, 22 — câte 1 lecție). Formatul se decide **după** transcriere — nu știm dacă e mentorat diff --git a/audio_fingerprint.py b/audio_fingerprint.py new file mode 100644 index 0000000..60d77d8 --- /dev/null +++ b/audio_fingerprint.py @@ -0,0 +1,84 @@ +""" +Detectează sesiuni duplicate (același videoclip urcat sub două date) direct +din audio, fără transcriere. + +De ce nu md5: cele două urcări sunt encodate separat, deci bytes diferiți și +durate care diferă cu zecimi de secundă. De ce nu whisper: ar însemna să +transcrii tot ca să afli că n-avea rost. + +Amprenta = energia (RMS) pe ferestre de o secundă, normalizată. Două urcări +ale aceleiași înregistrări au aceeași curbă de energie, chiar dacă sunt +decalate cu o fracțiune de secundă sau encodate diferit. +""" + +import subprocess +import sys +from itertools import combinations +from pathlib import Path + +import numpy as np + +SR = 1000 # decodăm la 1 kHz: energia vorbirii se vede, e de 16x mai ieftin +WIN = SR # o fereastră = o secundă +MAX_SHIFT = 15 # secunde de decalaj tolerate între cele două urcări + + +def envelope(path: Path) -> np.ndarray: + raw = subprocess.run( + ["ffmpeg", "-v", "error", "-i", str(path), "-f", "s16le", "-ar", str(SR), "-ac", "1", "-"], + capture_output=True, check=True, + ).stdout + x = np.frombuffer(raw, dtype=np.int16).astype(np.float32) + n = len(x) // WIN + rms = np.sqrt((x[: n * WIN].reshape(n, WIN) ** 2).mean(axis=1)) + return rms / (rms.std() or 1.0) + + +def similarity(a: np.ndarray, b: np.ndarray) -> float: + """Cea mai bună corelație normalizată peste toate decalajele plauzibile.""" + if min(len(a), len(b)) < 60: + return 0.0 + best = 0.0 + for shift in range(-MAX_SHIFT, MAX_SHIFT + 1): + x, y = (a[shift:], b) if shift >= 0 else (a, b[-shift:]) + n = min(len(x), len(y)) + if n < 60: + continue + x, y = x[:n] - x[:n].mean(), y[:n] - y[:n].mean() + denom = np.linalg.norm(x) * np.linalg.norm(y) + if denom: + best = max(best, float(x @ y / denom)) + return best + + +def main(): + args = [a for a in sys.argv[1:] if not a.startswith("--")] + expanded = set() + for a in args: + pa = Path(a) + expanded |= {pa} if pa.is_file() else set(Path(pa.anchor or ".").glob(str(pa.relative_to(pa.anchor or ".")))) + files = sorted(expanded or Path("audio").glob("*/*.mp3")) + if len(files) < 2: + print("Nimic de comparat.") + return 0 + + print(f"Amprentez {len(files)} fișiere...") + fps = {f: envelope(f) for f in files} + found = False + for x, y in combinations(files, 2): + # durate foarte diferite => sigur nu e aceeași înregistrare + if abs(len(fps[x]) - len(fps[y])) > MAX_SHIFT: + continue + s = similarity(fps[x], fps[y]) + if s > 0.90: + found = True + print(f"DUPLICAT ({s:.3f}): {x.stem} == {y.stem}") + elif s > 0.70: + print(f"suspect ({s:.3f}): {x.stem} vs {y.stem} — verifică manual") + if not found: + print("Niciun duplicat.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/check_duplicates.py b/check_duplicates.py new file mode 100644 index 0000000..2a696bd --- /dev/null +++ b/check_duplicates.py @@ -0,0 +1,62 @@ +""" +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()) diff --git a/summaries/09_QA/03_2026-08-11.html b/summaries/09_QA/03_2026-08-11.html index 73b8151..86df6d5 100644 --- a/summaries/09_QA/03_2026-08-11.html +++ b/summaries/09_QA/03_2026-08-11.html @@ -18,6 +18,12 @@

Q&A 11.08.2026 — stop-loss-ul de 4 candele, matematica opțiunilor, Buy Doji weekly

+
+

Aceeași înregistrare e publicată în curs și sub data 18.08.2026 +(lecțiile 2 și 3 din modulul Q&A). Verificat: durata identică, transcrieri +86.5% identice cuvânt cu cuvânt, amprentă audio 1.000. Nu e o sesiune +pierdută — e un duplicat al platformei.

+

Sesiune scurtă (~46 min), cu puține întrebări din sală — Bogdan a umplut timpul cu analize live pe ONON, APTV și Tesla. Conținutul cel mai util nu vine din întrebări, ci din regulile pe care le-a formulat în treacăt.

diff --git a/summaries/09_QA/03_2026-08-11.md b/summaries/09_QA/03_2026-08-11.md index f335956..05c0a38 100644 --- a/summaries/09_QA/03_2026-08-11.md +++ b/summaries/09_QA/03_2026-08-11.md @@ -1,5 +1,10 @@ # Q&A 11.08.2026 — stop-loss-ul de 4 candele, matematica opțiunilor, Buy Doji weekly +> **Aceeași înregistrare e publicată în curs și sub data 18.08.2026** +> (lecțiile 2 și 3 din modulul Q&A). Verificat: durata identică, transcrieri +> 86.5% identice cuvânt cu cuvânt, amprentă audio 1.000. Nu e o sesiune +> pierdută — e un duplicat al platformei. + Sesiune scurtă (~46 min), cu puține întrebări din sală — Bogdan a umplut timpul cu analize live pe ONON, APTV și Tesla. Conținutul cel mai util nu vine din întrebări, ci din regulile pe care le-a formulat în treacăt. diff --git a/summaries/09_QA/INDEX_TEMATIC.html b/summaries/09_QA/INDEX_TEMATIC.html index c06b396..84241fe 100644 --- a/summaries/09_QA/INDEX_TEMATIC.html +++ b/summaries/09_QA/INDEX_TEMATIC.html @@ -22,6 +22,8 @@ caută după ce vrei să afli, nu după când s-a discutat.

Acoperire: 3 din 28 sesiuni (25.08, 11.08, 04.08.2026). Se completează pe măsură ce sesiunile sunt procesate.

+

Duplicate găsite în curs: 18.08.2026 = 11.08.2026 (aceeași înregistrare, +publicată sub două date). Sumarizată o singură dată, sub 11.08.2026.

Setup-uri și triggere