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:
24
PROGRESS.md
24
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
|
||||
|
||||
84
audio_fingerprint.py
Normal file
84
audio_fingerprint.py
Normal 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
62
check_duplicates.py
Normal 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())
|
||||
@@ -18,6 +18,12 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="qa-11082026-stop-loss-ul-de-4-candele-matematica-optiunilor-buy-doji-weekly">Q&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&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
|
||||
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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
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
|
||||
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>
|
||||
<ul>
|
||||
<li><strong>MP² — care pivot trece sub care, la long și la short</strong> —
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
- **MP² — care pivot trece sub care, la long și la short** —
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
0
transcripts/09_Q_and_A/11_25112025.txt
Normal file
0
transcripts/09_Q_and_A/11_25112025.txt
Normal file
Reference in New Issue
Block a user