From d82e1d5ce583c26750fce92304ca938345669506 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Fri, 2 Oct 2026 06:07:34 +0000 Subject: [PATCH] eticheta_libera: filtru orb pentru cifrele care dispar sub candele cifre_ascunse() ruleaza pe tot repo-ul, nu pe etichete numite, si raporteaza doar cazul care chiar costa: eticheta cu cifra peste un plin, fie de aceeasi culoare (distanta RGB sub 60, cu transparenta amestecata peste alb), fie desenat dupa ea si deci acoperind-o indiferent de culoare. Al doilea caz a fost adaugat pentru ca m2d_directie avea text gri sub corp verde - aceeasi paguba, dar filtrul pe culoare singur il rata. Calibrare care conta: CW=6.2 e potrivit pentru bold si prea larg pentru rest, iar coada goala prindea candela de alaturi. CW=5.4 pentru nebold a eliminat primele doua rezultate, verificate pe ecran ca false pozitive. Ancora inversa: pe versiunile dinainte de reparatiile de azi filtrul prinde exact cele doua defecte confirmate vizual. Scanarea: 17 etichete in 13 diagrame, din care 9 cu gaura pe cifra. Noua din cele 17 sunt in summaries/06_MODUL_5, unde nu s-a facut niciodata o trecere de etichete; in gen/strategii, dupa trei treceri, au ramas cinci. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ --- gen/eticheta_libera.py | 183 +++++++++++++++++++++++++++++++++++++++- triaj/ETICHETE_VECHI.md | 81 ++++++++++++++++++ 2 files changed, 263 insertions(+), 1 deletion(-) diff --git a/gen/eticheta_libera.py b/gen/eticheta_libera.py index d11a315..03285fd 100644 --- a/gen/eticheta_libera.py +++ b/gen/eticheta_libera.py @@ -168,6 +168,146 @@ def eticheta_libera(svg, name, etichete, min_px=5.0, rects=False, segmente=True) f"({rx0:.0f},{ry0:.0f})-({rx1:.0f},{ry1:.0f})") +# =========================================================================== +# Filtrul orb: o cifră scrisă peste un corp plin de aceeași culoare +# +# Trierea vizuală din 02.10.2026 (02, 04, A3, 13 diagrame citite pe ecran) a +# dat o singură cauză fizică pentru suprapunerile care chiar schimbă ce +# citești: textul stă peste un `` **de culoarea lui**, iar caracterele +# nu se încalecă — dispar („intrare 577.00" se citea „intrare 577. 0"). +# Celelalte ~91 de atingeri — text peste fitil, peste linia unui nivel, peste +# chenar, peste coada unei săgeți — rămân lizibile: fundalul e o linie +# subțire, nu o suprafață plină. +# +# De aceea filtrul ăsta NU e opt-in pe etichete numite ca `eticheta_libera`: +# fiind îngust, poate rula orb pe tot repo-ul. +# +# Ordinea de desenare nu contează: verde pe verde e invizibil și dacă textul +# e pictat deasupra. De aceea singurul criteriu e culoarea, nu ordinea. + +DIST = 60.0 # distanță RGB sub care două culori nu se mai disting +ACOPERIRE = 4.0 # px de suprapunere pe orizontală sub care e doar atingere +CW_N = 5.4 # un caracter de 12px NEbold e mai îngust decât CW +# `_bbox` folosește CW=6.2 peste tot, bun pentru etichetele bold și prudent +# pentru restul. Aici prudența costă: la o etichetă lungă și nebold, cei ~0.8 +# px pe caracter se adună într-o coadă goală de 30 px, care prinde candela de +# alături și raportează o înghițire care nu există (`buy_doji_labu`). + + +def _rgb(c, alpha=1.0): + """#rgb/#rrggbb -> (r, g, b), amestecat peste alb dacă e transparent.""" + c = c.lstrip("#") + if len(c) == 3: + c = "".join(ch * 2 for ch in c) + v = tuple(int(c[i:i + 2], 16) for i in (0, 2, 4)) + return tuple(255 - (255 - x) * alpha for x in v) + + +def _departe(a, b): + return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5 + + +def _panouri(svg): + """Fiecare `` imbricat e un sistem de coordonate separat. + + Întoarce bucățile care se pot compara între ele: panoul exterior fără + imbricate, apoi fiecare imbricat, recursiv. + """ + out, adanc, start = [], 0, None + rest = [] + i = 0 + for m in re.finditer(r"", svg): + if m.group(0) == "": + adanc -= 1 + if adanc == 1 and start is not None: + out.extend(_panouri(svg[start:m.end()])) + i = m.end() + start = None + else: + adanc += 1 + if adanc == 2: + rest.append(svg[i:m.start()]) + start = m.start() + rest.append(svg[i:]) + return ["".join(rest)] + out + + +def cifre_ascunse(svg, nume=""): + """Etichetele cu cifre care stau peste un dreptunghi de culoarea lor. + + Întoarce o listă de dicte (fără să ridice nimic): `eticheta`, `culoare`, + `peste` (culoarea efectivă a dreptunghiului) și `px` — cât din lățimea + etichetei e înghițit. + """ + gasite = [] + for panou in _panouri(svg): + panou = _aplatizeaza(panou) + rects = [] + for m in re.finditer(r']*?)/?>', panou): + a = dict(_ATTR.findall(m.group(1))) + fill = re.search(r'fill="(#[0-9a-fA-F]{3,6})"', m.group(1)) + if not fill or "width" not in a or "height" not in a: + continue + op = float(a.get("opacity", 1.0)) * float(a.get("fill-opacity", 1.0)) + x, y = float(a["x"]), float(a["y"]) + rects.append((x, y, x + float(a["width"]), y + float(a["height"]), + _rgb(fill.group(1), op), op, m.start())) + for m in re.finditer(r']*)>([^<]*)', + panou): + s = m.group(4) + if not re.search(r"\d", s): + continue + culoare = re.search(r'fill="(#[0-9a-fA-F]{3,6})"', m.group(3)) + if not culoare: + continue + c = _rgb(culoare.group(1)) + _, (x0, x1, top, bot) = _bbox(m, 0.0) + if "bold" not in m.group(3): # caseta reală, nu cea prudentă + w = (x1 - x0) * CW_N / CW + x0, x1 = ((x1 - w, x1) if 'anchor="end"' in m.group(3) else + ((x0 + x1 - w) / 2, (x0 + x1 + w) / 2) + if 'anchor="middle"' in m.group(3) else (x0, x0 + w)) + for rx0, ry0, rx1, ry1, rc, op, poz in rects: + lat = min(x1, rx1) - max(x0, rx0) + inalt = min(bot, ry1) - max(top, ry0) + if lat < ACOPERIRE or inalt < 4.0: + continue + if _departe(c, rc) <= DIST: + de_ce = "aceeași culoare" + elif poz > m.start() and op >= 0.5: + # desenat PESTE text: îl acoperă oricare i-ar fi culoarea + # (`m2d_directie`: corp verde peste „…mișcarea 99.80" gri) + de_ce = "desenat peste" + else: + continue + # ce anume dispare: lățimea e aproximată uniform, deci + # bucata e orientativă — spune însă dacă gaura cade pe cifră + lat_car = (x1 - x0) / max(len(s), 1) + i0 = int((max(x0, rx0) - x0) / lat_car) + i1 = min(len(s), int((min(x1, rx1) - x0) / lat_car) + 1) + gasite.append({"diagrama": nume, "eticheta": s, "de_ce": de_ce, + "culoare": culoare.group(1), "px": round(lat), + "inghitit": s[i0:i1], + "peste": tuple(round(v) for v in rc)}) + break + return gasite + + +def blocuri_svg(text): + """Blocurile `…` de nivel întâi dintr-un .md sau .html.""" + adanc, start = 0, None + for m in re.finditer(r"", text): + if m.group(0) == "": + adanc -= 1 + if adanc == 0 and start is not None: + yield text[start:m.end()] + start = None + else: + if adanc == 0: + start = m.start() + adanc += 1 + + def demo(): svg = ('' '' @@ -215,6 +355,47 @@ def demo(): raise AssertionError("'pe candela scalata' trebuia prinsa") print("eticheta_libera: ok") + # --- filtrul orb: cifră peste un corp de aceeași culoare --------------- + # cazul real (mp2_exemplu, 02.10.2026): verde pe verde, caracterul dispare + verde = ('' + '' + '' + '' + '' + 'intrare 577.00' + 'stop 554.40' + 'volum 1.2 mil.' + 'liber 100.00' + 'fara cifra aici') + prinse = {g["eticheta"] for g in cifre_ascunse(verde)} + assert prinse == {"intrare 577.00"}, prinse + # dovada pe rând: linia și curba de aceeași culoare NU se aprind (textul + # rămâne lizibil peste un fir de 1 px), corpul roșu de sub text nici el + # (altă culoare = contrast), bara de volum la 0.55 nici ea (e pală), iar + # eticheta fără cifră nu intră în filtru deloc. + print("cifre_ascunse: ok") + + +def scaneaza(cai): + """Raport pe fișiere: .svg întregi, sau blocurile dintr-un .md/.html.""" + from pathlib import Path + tot = [] + for cale in cai: + text = Path(cale).read_text(encoding="utf-8") + bucati = [text] if str(cale).endswith(".svg") else list(blocuri_svg(text)) + for k, b in enumerate(bucati): + nume = Path(cale).name + (f" #{k + 1}" if len(bucati) > 1 else "") + tot += cifre_ascunse(b, nume) + for g in tot: + print(f"{g['diagrama']}: '{g['eticheta']}' — dispare ~'{g['inghitit']}' " + f"({g['px']} px, {g['de_ce']})") + print(f"{len(tot)} etichete cu cifre înghițite de un corp plin") + return tot + if __name__ == "__main__": - demo() + import sys + if len(sys.argv) > 1: + scaneaza(sys.argv[1:]) + else: + demo() diff --git a/triaj/ETICHETE_VECHI.md b/triaj/ETICHETE_VECHI.md index 5feec8e..b458552 100644 --- a/triaj/ETICHETE_VECHI.md +++ b/triaj/ETICHETE_VECHI.md @@ -147,3 +147,84 @@ Doua defecte vechi, vazute dar neatinse (nu erau pe lista lui): rasturnarea exemplului long)" e traversat de coada sagetii „piciorul 1". - `mdsrt_aplus`, panoul NU — `cds_n[18].l` e 94.50, dar nivelul se cheama „primul low 94.60". E close-ul, nu low-ul. + +--- + +## Filtrul orb: cifră peste un corp plin (scanare 02.10.2026) + +Cerut după trierea vizuală a capitolelor 02 / 04 / A3, care a dat o singură +cauză fizică pentru suprapunerile care chiar schimbă ce citești: textul stă +peste un `` **plin**, iar caracterele nu se încalecă — dispar. Peste o +linie subțire (nivel, fitil, coadă de săgeată, chenar) textul rămâne lizibil, +oricât de urât arată. De aceea tiparele (a)-(d) de mai sus nu merită un lot, +dar clasa asta merită o listă. + +`gen/eticheta_libera.py` are acum `cifre_ascunse(svg)`, care nu e opt-in pe +etichete numite — fiind îngustă, rulează oarbă: + + .venv/bin/python gen/eticheta_libera.py gen/strategii/*.svg \ + summaries/06_MODUL_5/*.md + +Ce raportează: o etichetă **care conține o cifră**, peste un dreptunghi plin, +în unul din două cazuri — (1) **aceeași culoare** (verde pe verde: caracterul +dispare oricare ar fi ordinea de desenare), sau (2) **desenat peste** text +(corpul e pictat după etichetă, deci o acoperă indiferent de culoare — cazul +`m2d_directie` din lista „mincinoase"). Pragurile: distanță RGB sub 60, +suprapunere de cel puțin 4 px pe orizontală și 4 px pe verticală, opacitatea +dreptunghiului cel puțin 0.5 (zonele palide nu acoperă nimic). Transparența se +amestecă peste alb înainte de comparație, deci barele de volum la 0.55 nu se +aprind. Bucata „dispare ~" e aproximativă (lățime uniformă pe caracter), dar +spune dacă gaura cade pe cifră sau pe o literă. + +Calibrare: lățimea din `_bbox` (CW = 6.2) e potrivită pentru bold și prea +largă pentru restul; la o etichetă lungă nebold coada goală de ~30 px prindea +candela de alături (`buy_doji_labu`, `mp2_aplus` — ambele verificate pe ecran, +false pozitive). Filtrul folosește CW = 5.4 pentru textul nebold. Pe versiunea +de dinaintea reparațiilor de azi, `mp2_exemplu` („intrare 577.00", 0 păstrat de +corpul verde) și `m2d_directie` (ambele panouri) sunt prinse — cele două +defecte reale confirmate vizual, deci filtrul nu e doar strâmt, e și calibrat. + +### Rezultat: 17 etichete, în 13 diagrame + +**Gaura cade pe cifră (9):** + +``` +m2d_iesire.svg: 'intrare 99.60' — dispare ~'60' (12 px, desenat peste) +mdsrt_exemplu.svg: 'TP1 624.70 = 1:1 (construit)' — dispare ~'1 (' (9 px, desenat peste) +mp2_short.svg: '128.97: crossing bear' — dispare ~'8.' (9 px, desenat peste) +02_patternuri_complexe_partea_a_treia.md #3: 'SL 29.40' — dispare ~'9.4' (9 px, aceeași culoare) +02_patternuri_complexe_partea_a_treia.md #4: 'target 62.00 = 65.00 − 3.00' — dispare ~'62.' (9 px, desenat peste) +02_patternuri_complexe_partea_a_treia.md #5: 'target 60.50 = 65.00 − 4.50' — dispare ~'0.5' (9 px, desenat peste) +02_patternuri_complexe_partea_a_treia.md #8: 'intrare short 89.00' — dispare ~' 8' (9 px, desenat peste) +02_patternuri_complexe_partea_a_treia.md #8: 'target 60.00 = 90.00 − 30.00' — dispare ~' 90' (9 px, desenat peste) +04_strategia_7_micis.md #6: 'intri cu 1/3 și rămâi cu 1/3' — dispare ~'1/3' (10 px, aceeași culoare) +``` + +**Gaura cade pe o literă (8) — eticheta are cifre, dar ce dispare e text:** + +``` +hammer_mbt10_bh_stop.svg: 'intrare 63.35 (close-ul confirmării)' — dispare ~'irm' (13 px, desenat peste) +hammer_mbt10_mbt_close.svg: 'la 55.30, în timpul zilei, semnalul' — dispare ~'sem' (13 px, aceeași culoare) +mdsrt_exemplu.svg: 'hammer închide 622.60, înapoi peste pivot' — dispare ~'oi' (9 px, aceeași culoare) +mdsrt_exemplu.svg: 'intri aici: close peste high-ul hammer-ului (622.70)' — dispare ~'r-' (9 px, aceeași culoare) +02_patternuri_complexe_partea_a_treia.md #5: 'suport (neckline) 65.00' — dispare ~'in' (9 px, desenat peste) +02_patternuri_complexe_partea_a_treia.md #7: 'limita de 50% 100.00' — dispare ~'ta ' (9 px, desenat peste) +03_patternuri_complexe_partea_a_patra.md #8: 'TP2 42.40 = batul steagului = 6.2R' — dispare ~' = ' (9 px, desenat peste) +05_strategia_8_dhrt.md #10: 'vârful consolidării 53.70 = locul crimei' — dispare ~'ons' (12 px, desenat peste) +``` + +Verificate pe ecran, cu zoom 4x: 6 din 17, luate din ambele categorii +(`m2d_iesire` „99.6|", `mdsrt_exemplu` „= 1:_ (construit)", `mp2_short` cu +„128" sub corpul roșu, `hammer_mbt10_bh_stop` „confirm_rii", +`02_patternuri…` „neckl_ne", `05_strategia_8_dhrt` „c_nsolidării"). Toate +șase, reale. Nu am reparat nimic și n-am atins capitolele 06, 09, 10, care +sunt în lucru la alt agent. + +Două observații pentru cine ia lista: + +- Jumătate din cazuri înghit o literă, nu o cifră: filtrul cere ca eticheta să + **conțină** o cifră, nu ca gaura să cadă pe ea. Dacă lotul trebuie scurtat, + primele 9 sunt cele care costă. +- Nouă din cele 17 sunt în `summaries/06_MODUL_5/` — adică în + diagramele de modul, nu în cele de strategie. Acolo nu s-a făcut niciodată + o trecere de etichete.