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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1
This commit is contained in:
Claude Agent
2026-09-12 17:40:18 +00:00
parent 4ae8579c10
commit f0b8f28fa9
9 changed files with 3015 additions and 0 deletions

View File

@@ -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 generic câștigă clar și nu e nici mai lent (moltbot small-ro: 2.26x
realtime; local medium: 1.78x). 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 ### 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). 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 Formatul se decide **după** transcriere — nu știm dacă e mentorat

84
audio_fingerprint.py Normal file
View File

@@ -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())

62
check_duplicates.py Normal file
View File

@@ -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())

View File

@@ -18,6 +18,12 @@
</head> </head>
<body> <body>
<h1 id="qa-11082026-stop-loss-ul-de-4-candele-matematica-optiunilor-buy-doji-weekly">Q&amp;A 11.08.2026 — stop-loss-ul de 4 candele, matematica opțiunilor, Buy Doji weekly</h1> <h1 id="qa-11082026-stop-loss-ul-de-4-candele-matematica-optiunilor-buy-doji-weekly">Q&amp;A 11.08.2026 — stop-loss-ul de 4 candele, matematica opțiunilor, Buy Doji weekly</h1>
<blockquote>
<p><strong>Aceeași înregistrare e publicată în curs și sub data 18.08.2026</strong>
(lecțiile 2 și 3 din modulul Q&amp;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.</p>
</blockquote>
<p>Sesiune scurtă (~46 min), cu puține întrebări din sală — Bogdan a umplut <p>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 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.</p> vine din întrebări, ci din regulile pe care le-a formulat în treacăt.</p>

View File

@@ -1,5 +1,10 @@
# Q&A 11.08.2026 — stop-loss-ul de 4 candele, matematica opțiunilor, Buy Doji weekly # 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 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 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. vine din întrebări, ci din regulile pe care le-a formulat în treacăt.

View File

@@ -22,6 +22,8 @@
caută după ce vrei să afli, nu după când s-a discutat.</p> caută după ce vrei să afli, nu după când s-a discutat.</p>
<p><em>Acoperire: 3 din 28 sesiuni (25.08, 11.08, 04.08.2026). Se completează pe <p><em>Acoperire: 3 din 28 sesiuni (25.08, 11.08, 04.08.2026). Se completează pe
măsură ce sesiunile sunt procesate.</em></p> măsură ce sesiunile sunt procesate.</em></p>
<p><em>Duplicate găsite în curs: </em><em>18.08.2026 = 11.08.2026</em><em> (aceeași înregistrare,
publicată sub două date). Sumarizată o singură dată, sub 11.08.2026.</em></p>
<h2 id="setup-uri-si-triggere">Setup-uri și triggere</h2> <h2 id="setup-uri-si-triggere">Setup-uri și triggere</h2>
<ul> <ul>
<li><strong>MP² — care pivot trece sub care, la long și la short</strong> — <li><strong>MP² — care pivot trece sub care, la long și la short</strong> —

View File

@@ -6,6 +6,9 @@ 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 *Acoperire: 3 din 28 sesiuni (25.08, 11.08, 04.08.2026). Se completează pe
măsură ce sesiunile sunt procesate.* 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 ## Setup-uri și triggere
- **MP² — care pivot trece sub care, la long și la short** — - **MP² — care pivot trece sub care, la long și la short** —

File diff suppressed because it is too large Load Diff

View File