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
|
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
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>
|
</head>
|
||||||
<body>
|
<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>
|
<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
|
<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>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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> —
|
||||||
|
|||||||
@@ -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
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