linkedin-cringe-meter
Il Cringiometro. Dato l'URL (o il testo) di un post LinkedIn, ne misura il livello di cringe da 1 a 10 con la scala e il catalogo dei 37 moduli della skill linkedin-cringe, dice quali ganci ha preso, il registro, il sapore-AI e la lead-gen, e produce un report markdown più un'imm
Install
npx skills add https://github.com/matteobaccan/LinkedInCringe/tree/main/.claude/skills/linkedin-cringe-meter
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install matteobaccan-linkedincringe@llmmart
git clone https://github.com/matteobaccan/LinkedInCringe.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole matteobaccan/linkedincringe collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Cringiometro: quanto è cringe questo post
Terza skill della famiglia: linkedin-cringe scrive il post, linkedin-cringe-analytics
misura i commenti, il Cringiometro misura il post di qualcun altro (o il proprio) con
lo stesso metro. L'ispirazione è il blastometro, spostato sul cringe: un voto, i
ganci presi, una frase secca, e un'immagine da lasciare nei commenti.
Si giudica il post, mai la persona. Nessun nome, azienda, headline o foto dell'autore compare nel report o nell'immagine. Questo non è negoziabile.
Flusso
1. Acquisizione del post
Serve solo il testo del post (espanso, con l'"…altro" cliccato), più un'indicazione se c'è un'immagine allegata e cosa mostra. Niente commenti: quelli sono della skill analytics.
A. URL + Claude in Chrome. Se i tool mcp__claude-in-chrome__* sono disponibili,
segui il Setup di ../linkedin-cringe-analytics/references/estrazione.md (caricamento
tool, list_connected_browsers, scelta browser, tabs_context_mcp, navigate). Gli
short link lnkd.in si risolvono da soli con la navigazione. Poi:
- clicca l'"…altro" del post se il testo è troncato;
- prendi il testo del post con
get_page_textoread_pagelimitato al contenitore del post (l'elemento concomponentkeyche inizia perurn:li:activity:o il primo[data-urn]), senza i commenti; - annota se c'è un'immagine, un video, un carosello o un sondaggio, e in una riga cosa si vede (senza persone riconoscibili).
Se la pagina chiede il login o il browser non è collegato, passa a B senza insistere.
B. Testo incollato. L'utente incolla il post in chat, o passa uno screenshot: la skill legge quello. Dallo screenshot ignora nome, foto e headline dell'autore anche se visibili. Il resto del flusso è identico.
I dati grezzi (URL, testo integrale, JSON di lavoro) stanno nella scratchpad di sessione. Nel repository entra solo il report anonimizzato, l'immagine e il JSON con i soli campi che compaiono nell'immagine.
2. Valutazione
Leggi references/valutazione.md e, attraverso i suoi rimandi, tassonomia.md e
moduli.md della skill gemella (../linkedin-cringe/references/). Se la skill
gemella non c'è, fermati e dillo: senza catalogo non c'è metro.
Produci, nell'ordine: livello 1-10 con la parola della scala, registro (credibile / parodico / surreale deadpan / cringe dichiarato), lo scarto in una riga, i moduli rilevati con una citazione breve ciascuno (massimo 8, in ordine di peso), l'eventuale gancio reale, sapore-AI, lead-gen, il verdetto in una frase, e i moduli che mancano per salire di un livello.
Prima di dare il voto, controlla la sezione Cosa non si valuta della rubrica: lutti, malattie e licenziamenti veri fermano la valutazione.
3. Report e immagine
Scrivi analisi/YYYY-MM-DD-<slug>/cringiometro.md seguendo references/report.md
(slug dal tema del post, mai dal nome dell'autore).
Poi genera l'immagine:
- scrivi
cringiometro.jsonnella stessa cartella conscore,register,modules(codice + nome corto, max 8),verdict(la frase, max ~90 caratteri: sull'immagine deve stare in due righe),quote(una citazione del post di max ~90 caratteri, senza nomi) elang; L'immagine parla la lingua del post:langè il codice della lingua del post (it,en,de,fr,es; altre lingue:enpiùlabelsper i testi fissi), e nomi dei moduli, verdetto e citazione vanno scritti in quella lingua. Il report e la chat restano nella lingua dell'utente; - lancia
python scripts/cringiometro.py analisi/YYYY-MM-DD-<slug>/cringiometro.json analisi/YYYY-MM-DD-<slug>/(percorso dello script relativo a questa skill): producecringiometro.svge, se Pillow è installato,cringiometro.png1080×1080; - apri il PNG e guardalo prima di consegnarlo: testo che sborda, pillole tagliate, verdetto su tre righe sono da correggere accorciando i testi nel JSON e rilanciando.
Se Pillow manca, consegna l'SVG e di' come installarlo (pip install pillow).
4. Consegna
In chat: livello e parola, i moduli in una riga, il verdetto. Poi il PNG con
SendUserFile e il percorso del report. La prima volta, una riga: l'immagine non
porta nomi, e lasciarla come risposta sotto al post di un estraneo è una scelta sua.
Chiudi offrendo il giro successivo: rigenerare l'immagine con un verdetto diverso,
o passare il post a linkedin-cringe per la versione "come sarebbe a 10".
Paletti
- Nessun giudizio sulla persona. Il soggetto delle frasi è il post. Vietati aggettivi sull'autore e riferimenti a età, genere, provenienza, aspetto, ruolo.
- Anonimato totale in report, immagine e chat: niente nome, azienda, headline, foto, link al profilo. L'URL del post resta in scratchpad.
- Lutti, malattie, licenziamenti veri: non si valutano. Si dice e ci si ferma.
- Post privati di persone non pubbliche (figli, matrimoni, salute): si valutano al massimo gli elementi formali, dichiarandolo.
- Se l'utente vuole valutare un proprio post generato con
linkedin-cringe, il registro è "cringe dichiarato" e il voto vale lo stesso: è il collaudo del generatore. - Se l'utente chiede di pubblicare l'immagine come risposta, ricordaglielo una volta e poi fai quello che chiede. Non pubblica la skill: consegna il file.
Files (linkedincringe)
-
references
-
report.md 2.9 KB
# Struttura del report File: `analisi/YYYY-MM-DD-<slug>/cringiometro.md`. Lo slug viene dal tema del post (es. `asparagi`, `macchinetta`), mai dal nome dell'autore. Nella stessa cartella vanno `cringiometro.png`, `cringiometro.svg` e `cringiometro.json` (i dati passati allo script, utili per rigenerare l'immagine a mano). Se nella cartella c'è già un `report.md` della skill analytics (stesso post, analizzato dopo la pubblicazione), i due file convivono: il Cringiometro misura il post, analytics misura i commenti. --- ## Regole - **Anonimato totale dell'autore.** Mai nome, cognome, azienda, headline, ruolo, foto, link al profilo. L'URL del post non va nel report (basta nel JSON di scratchpad, che non si committa): chi legge il report legge una valutazione di un testo, non di una persona. Si può dire "un post del 23 agosto 2026". - **Citazioni brevi.** Massimo due righe per citazione, solo quanto serve a provare il modulo. Il post non si riporta mai per intero. - **Il soggetto è il post.** "Il post usa", "la morale arriva", "il P.S. vende". Mai "l'autore è". - **Lingua.** Il report è nella lingua dell'utente; l'immagine nella lingua del post (campo `lang` del JSON, nomi dei moduli e verdetto tradotti). Se il post è in inglese, il verdetto nel report compare in entrambe le lingue: quello italiano nel testo, quello inglese citato sotto, perché è quello che finisce nell'immagine. - **Markdown piatto**, niente HTML. Emoji solo nelle intestazioni di sezione, come sotto. Niente trattini lunghi, come in tutto il repo. --- ## Template ```markdown # Cringiometro: <slug> Post valutato il YYYY-MM-DD · testo di N parole · immagine allegata: sì/no ## 🎯 Livello: X/10 (parola) Due righe di motivazione: lo scarto e il peso dei moduli. **Registro:** credibile / parodico / surreale deadpan / cringe dichiarato (una riga sul perché) **Lo scarto:** [evento] → [morale] ## 🧩 Moduli rilevati | # | Modulo | Prova | |---|---|---| | 1 | C01 Parabola forzata | "…citazione breve…" | | 2 | C13 Numeri finti-precisi | "…" | | … | | | Altri tic minori: C31, C23 (una riga). **Gancio reale:** no / sì, [uso] su [fatto], citato giusto / sbagliato (solo se c'è). ## 🤖 Sapore-AI: bassa / media / alta Gli indizi, in una riga o due. Osservazione sul testo, non accusa. ## 💰 Lead-gen: no / sì Dove si nasconde, in una riga. ## 🗣️ Verdetto > La frase secca (quella dell'immagine). ## 📈 Per salire a X+1 - C.. nome: dove andrebbe, in una riga - C.. nome: idem ## 🖼️ Immagine `cringiometro.png` (1080×1080). Nessun dato dell'autore. Pronta come risposta. ``` --- ## Consegna in chat Nell'ordine: livello e parola, i moduli in una riga (solo i codici con il nome corto), il verdetto, poi il file PNG via `SendUserFile` e il percorso del report. Una riga, la prima volta, per ricordare che rispondere con l'immagine sotto al post di un estraneo è una scelta sua e che l'immagine non porta nomi. -
valutazione.md 7.2 KB
# Rubrica di valutazione La scala e i moduli sono quelli della skill gemella `linkedin-cringe`: `../../linkedin-cringe/references/tassonomia.md` (definizione, legge dello scarto, scala 1-10) e `moduli.md` (il catalogo, C01-C37). Leggili prima di valutare: il Cringiometro misura con lo stesso metro con cui il generatore scrive, altrimenti i due strumenti non si parlano. Se la skill gemella non è installata accanto, fermati e dillo: senza catalogo la valutazione non ha riferimento. --- ## 1. Il livello (1-10) Il cringe non è "scritto male". È imbarazzo vicario: l'autore ci crede, rivendica una profondità che il testo non ha, vende fingendo di non vendere. Il livello misura **quanto imbarazzo prova chi legge**, non quanti difetti ha il testo. La leva principale è **lo scarto**: la distanza fra l'evento raccontato e la morale che ci viene costruita sopra. Poi contano la quantità di moduli e quanto è spinto il registro. | Livello | Come si riconosce | |---|---| | 1-2 | Innocuo: un hashtag, tono normale, nessuna morale. Un post e basta. | | 3-4 | Lieve: un solo tic, scarto minimo, morale proporzionata. Passa inosservato nel feed. | | 5-6 | Fastidioso: due moduli, morale un po' più grande dell'aneddoto. Occhi al cielo. | | 7-8 | Tre o quattro moduli, scarto netto, numeri o dialoghi inventati. Chi legge fa una faccia. Screenshottabile. | | 9 | Cinque o più moduli, lo scarto è cosmico, il post gira nelle chat. Ancora credibile. | | 10 | Tutto insieme e la credibilità si incrina: lead-gen scoperta, foto scollegata, MAIUSCOLO, muro di hashtag. | Regole pratiche: - Parti dallo scarto: evento → morale. Scrivilo in una riga. Se la morale segue logicamente dall'evento, il post difficilmente supera il 5, per quanti tic abbia. - Conta i moduli con prova. Ogni modulo rilevato deve avere una citazione del post che lo dimostra; se non trovi la citazione, non è rilevato. - Poi aggiusta di ±1 per il registro: un post che vende (C27, C30) o che mette l'autore al centro di un fatto pubblico sale; un post sobrio e proporzionato scende. - Ammessi i mezzi punti (7,5) quando sei davvero a cavallo. - **Non confondere cringe e bruttezza.** Un post sgrammaticato e basta è un 2. Un post scritto benissimo in cui il caffè insegna la leadership è un 7. - **Non confondere cringe e disaccordo.** Un'opinione che non condividi, scritta con misura, non è cringe. Lo diventa se si presenta come coraggiosa (C07) o come merito (C18, C33). ## 2. Il registro - **Credibile**: potrebbe essere vero, e probabilmente lo è. L'autore ci crede. - **Parodico**: esagerazione evidente, si capisce che è una presa in giro. - **Surreale deadpan**: i fatti raccontati sono impossibili e la voce non se ne accorge, senza nessuna battuta. Non è parodia: la parodia ammicca, qui no. Raro in natura, frequente nei post generati. Vedi `surreale.md` della skill gemella. - **Cringe dichiarato**: c'è una riga finale che svela lo scherzo, o un'indicazione esplicita di satira. Tipico dei post generati con `linkedin-cringe`. Va detto con naturalezza, senza prendersela: il Cringiometro valuta il post come sta, e la riga di disclaimer non abbassa il livello del corpo che la precede. - **AI-SLOP**: il post non ha un autore. Nessun dettaglio concreto, paragrafi tutti uguali, triadi, "non è X, è Y" in serie, e spesso gli asterischi del markdown rimasti nel testo. Vedi `slop.md` della skill gemella. ## 3. I moduli (i ganci) Scorri il catalogo per sezioni, in quest'ordine, e annota codice + citazione: 1. **Formali** (C21, C22, C23, C10, C24, C30, C31, C34): si vedono a colpo d'occhio. 2. **Narrativi** (C01-C05, C09, C14, C15, C17-C20, C25-C29, C32, C36, C37): qual è la struttura del racconto? Di solito ce n'è uno dominante e uno o due di contorno. 3. **Retorici** (C06-C08, C11-C13, C16, C33, C35): come argomenta? Massimo 8 moduli in output, in ordine di peso: il primo è quello che definisce il post. Se ne trovi di più, tieni gli otto più evidenti e nomina gli altri nel report in una riga. **Il gancio reale.** Se il post parte da una notizia vera (vedi `../../linkedin-cringe/references/attualita.md`), segnalalo: quale dei cinque usi attiva (parabola, dato a sproposito, hot take, io c'ero, sfondo) e se il fatto è citato giusto. Un fatto sbagliato non alza il cringe, lo sposta nel "falso": va detto a parte. ## 4. Sapore-AI Tre livelli, con gli indizi di `linkedin-cringe/SKILL.md` sezione *Non deve sembrare scritto da un'AI*: trattini lunghi, triadi perfette, connettivi levigati, paragrafi tutti uguali, finale riassuntivo, "non è X, è Y" in serie, lessico uniformemente medio-alto. - **Bassa**: ripetizioni, sbavature, ritmo irregolare, un dettaglio inutile. Umano. - **Media**: qualche indizio, ma ci sono anche impronte umane. - **Alta**: tre o più indizi e nessuna sbavatura. Probabile C12 involontario. Non è un'accusa: è un'osservazione sul testo, e nel report si scrive così. **Sapore-AI alta e registro AI-SLOP non sono la stessa cosa.** Il sapore-AI misura il testo; il registro è un giudizio sulla paternità. "Alta" con registro credibile vuol dire che c'è una persona che scrive levigato. AI-SLOP vuol dire che dietro non c'è nessuno: sapore-AI alta **e** zero dettagli concreti **e** nessuna opinione riconoscibile. Nel dubbio si resta su credibile con sapore-AI alta, che è l'ipotesi meno accusatoria. ## 5. Lead-gen C'è un intento commerciale? Dove si nasconde: nel P.S., nell'ultima riga, nella CTA "scrivimi", nel "link nel primo commento", nel test gratuito, nella headline del profilo che contraddice l'umiltà del testo. Se non c'è, dillo: "nessuna vendita, cringe disinteressato" è un'informazione. ## 6. Il verdetto Una frase, secca, che sta in due righe sull'immagine. Giudica il post, mai la persona. Stile: la battuta del recensore, non l'insulto. - "Un 7 solido. Mancano solo i numeri finti per il podio." - "Parabola da manuale, ma la morale arriva con tre righe di ritardo." - "Il P.S. vende quello che il post giurava di non vendere. Nove." - "Cringe dichiarato, e fatto bene: il disclaimer è l'unica riga che non fa male." Vietato: aggettivi sulla persona ("patetico", "narcisista"), riferimenti a età, genere, provenienza, aspetto, e qualunque cosa che suoni come "l'autore è…". Il soggetto delle frasi è il post. ## 7. Per salire Quali moduli mancano per arrivare al livello successivo, con una riga ciascuno che spiega dove andrebbero. È il ponte col generatore: "per arrivare a 9: C13 un paio di numeri finti sulle misurazioni dell'autore, C30 un P.S. che apre le ultime sessioni di un percorso". Due o tre moduli, non il catalogo intero. Se il post è già un 10, il consiglio è uno solo: non toccare niente. --- ## Cosa non si valuta - **Lutti, malattie, licenziamenti veri** di chi scrive o di terzi, anche se il post ne fa un uso cringe (C17 reale). Il Cringiometro si ferma e lo dice: non si dà un voto al dolore di una persona. - **Post di persone non pubbliche su fatti privati** che non hanno nulla di professionale: un post su un figlio, un matrimonio, una malattia. Se l'utente insiste, valuta solo gli elementi formali e dichiara che il resto non si tocca. - **La persona.** Mai la headline, il ruolo, l'azienda, la foto, lo storico del profilo. Solo il testo del post e, se c'è, la descrizione dell'immagine allegata.
-
-
scripts
-
__pycache__
-
cringiometro.cpython-311.pyc 27.1 KB · in bundle
-
-
cringiometro.py 15.7 KB
#!/usr/bin/env python3 """Cringiometro: genera l'immagine di risposta (PNG 1080x1080 + SVG) da un JSON. Uso: python cringiometro.py dati.json cartella_output/ dati.json: { "score": 7, # 1-10, interi o mezzi (7.5) "register": "credibile", # credibile | parodico | surreale | slop | dichiarato "modules": [ # max 8, in ordine di importanza {"code": "C01", "name": "Parabola forzata"}, {"code": "C13", "name": "Numeri finti-precisi"} ], "verdict": "Un 7 solido. Mancano i numeri finti per il podio.", "quote": "Le persone non lasciano le aziende. Lasciano le macchinette.", "footer": "github.com/matteobaccan/LinkedInCringe", # opzionale "lang": "en", # lingua dei testi fissi: it (default), en, de, fr, es "labels": {"title": "..."} # opzionale: override dei testi fissi } I nomi dei moduli, il verdetto e la citazione vanno scritti già nella lingua del post. Niente nomi, foto o headline dell'autore: l'immagine giudica il post, non la persona. Il PNG richiede Pillow; se manca, viene scritto solo l'SVG. """ import json import math import os import sys W = H = 1080 BG = (247, 243, 234) INK = (28, 28, 32) MUTED = (110, 108, 100) CARD = (255, 255, 255) STOPS = [(0.0, (52, 168, 83)), (0.5, (244, 180, 0)), (1.0, (217, 48, 37))] # Testi fissi dell'immagine, per lingua. "lang" nel JSON sceglie la lingua; una # lingua non presente ricade sull'inglese; "labels" nel JSON sovrascrive i singoli # campi (utile per una terza lingua senza toccare lo script). LANGS = { "it": { "title": "CRINGIOMETRO", "register": {"credibile": "registro credibile", "parodico": "registro parodico", "dichiarato": "cringe dichiarato", "surreale": "registro surreale", "slop": "scritto dall'AI"}, "words": ["innocuo", "lieve", "fastidioso", "imbarazzante", "insostenibile", "leggendario"], }, "en": { "title": "CRINGE METER", "register": {"credibile": "believable register", "parodico": "parody register", "dichiarato": "declared cringe", "surreale": "deadpan surreal", "slop": "AI slop"}, "words": ["harmless", "mild", "annoying", "embarrassing", "unbearable", "legendary"], }, "de": { "title": "CRINGEOMETER", "register": {"credibile": "glaubwürdig", "parodico": "Parodie", "dichiarato": "erklärter Cringe", "surreale": "surreal deadpan", "slop": "KI-Schrott"}, "words": ["harmlos", "leicht", "nervig", "peinlich", "unerträglich", "legendär"], }, "fr": { "title": "CRINGEOMÈTRE", "register": {"credibile": "registre crédible", "parodico": "registre parodique", "dichiarato": "cringe assumé", "surreale": "registre surréaliste", "slop": "bouillie IA"}, "words": ["anodin", "léger", "agaçant", "gênant", "insoutenable", "légendaire"], }, "es": { "title": "CRINGÓMETRO", "register": {"credibile": "registro creíble", "parodico": "registro paródico", "dichiarato": "cringe declarado", "surreale": "registro surrealista", "slop": "morralla de IA"}, "words": ["inocuo", "leve", "molesto", "vergonzoso", "insoportable", "legendario"], }, } def lerp_color(t): t = max(0.0, min(1.0, t)) for (t0, c0), (t1, c1) in zip(STOPS, STOPS[1:]): if t <= t1: k = 0 if t1 == t0 else (t - t0) / (t1 - t0) return tuple(int(a + (b - a) * k) for a, b in zip(c0, c1)) return STOPS[-1][1] def verdict_word(score, L): """Parola della scala di tassonomia.md: 1-2 innocuo, 3-4 lieve, 5-6 fastidioso, 7-8 imbarazzante, 9 insostenibile, 10 leggendario.""" words = L["words"] if score <= 2: return words[0] if score <= 4: return words[1] if score <= 6: return words[2] if score <= 8: return words[3] if score < 10: return words[4] return words[5] def layout_rows(pills, measure, maxw, gap): """Spezza le pillole in righe (max 3) dato un misuratore di larghezza.""" rows, row, used = [], [], 0 for p in pills: wpx = measure(p) if used + wpx > maxw and row: rows.append(row) row, used = [], 0 row.append((p, wpx)) used += wpx + gap if row: rows.append(row) return rows[:3] def fit_lines(lines, n): """Tiene al massimo n righe, con puntini sull'ultima se si taglia.""" if len(lines) <= n: return lines kept = lines[:n] kept[-1] = kept[-1].rstrip(".,;: ") + "…" return kept CY, R = 470, 270 # centro e raggio del quadrante FOOTER_Y = H - 34 def load(path): with open(path, encoding="utf-8") as f: d = json.load(f) d["score"] = float(d.get("score", 5)) d["score"] = max(1.0, min(10.0, d["score"])) d.setdefault("register", "credibile") d.setdefault("modules", []) d["modules"] = d["modules"][:8] d.setdefault("verdict", "") d.setdefault("quote", "") d.setdefault("footer", "github.com/matteobaccan/LinkedInCringe") lang = str(d.get("lang", "it")).lower()[:2] base = LANGS.get(lang, LANGS["en"]) L = {"title": base["title"], "register": dict(base["register"]), "words": list(base["words"])} for k, v in (d.get("labels") or {}).items(): if k in L and isinstance(v, type(L[k])): if isinstance(v, dict): L[k].update(v) else: L[k] = v d["_L"] = L return d # ---------------------------------------------------------------------------- # SVG # ---------------------------------------------------------------------------- def esc(s): return (s.replace("&", "&").replace("<", "<").replace(">", ">") .replace('"', """)) def wrap(text, width): words, lines, cur = text.split(), [], "" for w in words: if len(cur) + len(w) + 1 > width and cur: lines.append(cur) cur = w else: cur = (cur + " " + w).strip() if cur: lines.append(cur) return lines def polar(cx, cy, r, deg): rad = math.radians(deg) return cx + r * math.cos(rad), cy + r * math.sin(rad) def svg(d): cx, cy = W / 2, CY L = d["_L"] score = d["score"] frac = (score - 1) / 9.0 parts = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" ' f'viewBox="0 0 {W} {H}" font-family="Segoe UI, Helvetica, Arial, sans-serif">', f'<rect width="{W}" height="{H}" fill="rgb{BG}"/>', f'<text x="{W/2}" y="118" text-anchor="middle" font-size="64" font-weight="800" ' f'letter-spacing="6" fill="rgb{INK}">{esc(L["title"])}</text>', f'<text x="{W/2}" y="162" text-anchor="middle" font-size="26" fill="rgb{MUTED}">' f'{esc(L["register"].get(d["register"], d["register"]))}</text>'] # gauge: 60 segments from 180deg to 360deg n = 60 for i in range(n): a0 = 180 + 180 * i / n a1 = 180 + 180 * (i + 1) / n + 0.6 col = lerp_color(i / (n - 1)) x0, y0 = polar(cx, cy, R, a0) x1, y1 = polar(cx, cy, R, a1) parts.append(f'<path d="M{x0:.1f},{y0:.1f} A{R},{R} 0 0 1 {x1:.1f},{y1:.1f}" ' f'stroke="rgb{col}" stroke-width="58" fill="none"/>') for k in range(1, 11): a = 180 + 180 * (k - 1) / 9 x, y = polar(cx, cy, R - 62, a) parts.append(f'<text x="{x:.1f}" y="{y+10:.1f}" text-anchor="middle" font-size="28" ' f'font-weight="700" fill="rgb{MUTED}">{k}</text>') # needle a = 180 + 180 * frac nx, ny = polar(cx, cy, R - 20, a) parts.append(f'<line x1="{cx}" y1="{cy}" x2="{nx:.1f}" y2="{ny:.1f}" stroke="rgb{INK}" ' f'stroke-width="10" stroke-linecap="round"/>') parts.append(f'<circle cx="{cx}" cy="{cy}" r="22" fill="rgb{INK}"/>') # score col = lerp_color(frac) s = f"{score:g}" parts.append(f'<text x="{W/2}" y="{cy+120}" text-anchor="middle" font-size="150" ' f'font-weight="900" fill="rgb{col}">{s}<tspan font-size="56" ' f'fill="rgb{MUTED}">/10</tspan></text>') parts.append(f'<text x="{W/2}" y="{cy+170}" text-anchor="middle" font-size="34" ' f'font-weight="700" letter-spacing="3" fill="rgb{col}">' f'{verdict_word(score, L).upper()}</text>') # pillole, verdetto e citazione: layout dal basso pills = [f'{m["code"]} {m["name"]}' for m in d["modules"]] rows = layout_rows(pills, lambda p: 22 * len(p) * 0.62 + 40, W - 120, 14) nq_max = 1 if len(rows) >= 3 else 2 qlines = fit_lines(wrap("“" + d["quote"] + "”", 64), nq_max) if d["quote"] else [] vlines = fit_lines(wrap(d["verdict"], 46), 2) block_h = len(vlines) * 42 + (16 + len(qlines) * 30 if qlines else 0) pills_end = cy + 240 + len(rows) * 58 space = (FOOTER_Y - 12) - pills_end verdict_top = pills_end + max(0, (space - block_h) / 2) quote_top = verdict_top + len(vlines) * 42 + 16 y = cy + 240 for row in rows: total = sum(w for _, w in row) + 14 * (len(row) - 1) x = (W - total) / 2 for p, wpx in row: parts.append(f'<rect x="{x:.1f}" y="{y}" rx="24" ry="24" width="{wpx:.1f}" ' f'height="48" fill="rgb{CARD}" stroke="rgb{MUTED}" stroke-width="2"/>') parts.append(f'<text x="{x+wpx/2:.1f}" y="{y+32}" text-anchor="middle" ' f'font-size="22" font-weight="600" fill="rgb{INK}">{esc(p)}</text>') x += wpx + 14 y += 58 y = verdict_top + 34 for line in vlines: parts.append(f'<text x="{W/2}" y="{y}" text-anchor="middle" font-size="34" ' f'font-weight="700" fill="rgb{INK}">{esc(line)}</text>') y += 42 y = quote_top + 24 for line in qlines: parts.append(f'<text x="{W/2}" y="{y}" text-anchor="middle" font-size="24" ' f'font-style="italic" fill="rgb{MUTED}">{esc(line)}</text>') y += 30 parts.append(f'<text x="{W/2}" y="{FOOTER_Y+8}" text-anchor="middle" font-size="20" ' f'fill="rgb{MUTED}">{esc(d["footer"])}</text>') parts.append("</svg>") return "\n".join(parts) # ---------------------------------------------------------------------------- # PNG (Pillow) # ---------------------------------------------------------------------------- FONT_CANDIDATES = { "bold": ["segoeuib.ttf", "arialbd.ttf", "DejaVuSans-Bold.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "/System/Library/Fonts/Supplemental/Arial Bold.ttf"], "regular": ["segoeui.ttf", "arial.ttf", "DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf"], "italic": ["segoeuii.ttf", "ariali.ttf", "DejaVuSans-Oblique.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf", "/System/Library/Fonts/Supplemental/Arial Italic.ttf"], } def font(kind, size): from PIL import ImageFont for name in FONT_CANDIDATES[kind]: try: return ImageFont.truetype(name, size) except OSError: continue return ImageFont.load_default() def text_w(draw, s, f): return draw.textlength(s, font=f) def wrap_px(draw, text, f, maxw): words, lines, cur = text.split(), [], "" for w in words: cand = (cur + " " + w).strip() if text_w(draw, cand, f) > maxw and cur: lines.append(cur) cur = w else: cur = cand if cur: lines.append(cur) return lines def png(d, out_path): from PIL import Image, ImageDraw S = 2 # supersampling per bordi lisci img = Image.new("RGB", (W * S, H * S), BG) dr = ImageDraw.Draw(img) cx, cy, RR = W * S / 2, CY * S, R * S L = d["_L"] score = d["score"] frac = (score - 1) / 9.0 f_title = font("bold", 64 * S) f_sub = font("regular", 26 * S) f_tick = font("bold", 28 * S) f_score = font("bold", 150 * S) f_den = font("bold", 56 * S) f_word = font("bold", 34 * S) f_pill = font("bold", 22 * S) f_verd = font("bold", 34 * S) f_quote = font("italic", 24 * S) f_foot = font("regular", 20 * S) def ctext(y, s, f, fill): w = text_w(dr, s, f) dr.text(((W * S - w) / 2, y), s, font=f, fill=fill) ctext(70 * S, " ".join(L["title"]), f_title, INK) ctext(140 * S, L["register"].get(d["register"], d["register"]), f_sub, MUTED) # gauge n = 90 box = [cx - RR, cy - RR, cx + RR, cy + RR] for i in range(n): a0 = 180 + 180 * i / n a1 = 180 + 180 * (i + 1) / n + 0.8 dr.arc(box, a0, a1, fill=lerp_color(i / (n - 1)), width=58 * S) for k in range(1, 11): a = 180 + 180 * (k - 1) / 9 x, y = polar(cx, cy, RR - 62 * S, a) s = str(k) w = text_w(dr, s, f_tick) dr.text((x - w / 2, y - 16 * S), s, font=f_tick, fill=MUTED) a = 180 + 180 * frac nx, ny = polar(cx, cy, RR - 20 * S, a) dr.line([(cx, cy), (nx, ny)], fill=INK, width=10 * S) dr.ellipse([cx - 22 * S, cy - 22 * S, cx + 22 * S, cy + 22 * S], fill=INK) col = lerp_color(frac) s = f"{score:g}" den = "/10" w1, w2 = text_w(dr, s, f_score), text_w(dr, den, f_den) x0 = (W * S - w1 - w2) / 2 dr.text((x0, cy + 10 * S), s, font=f_score, fill=col) dr.text((x0 + w1, cy + 88 * S), den, font=f_den, fill=MUTED) ctext(cy + 185 * S, " ".join(verdict_word(score, L).upper()), f_word, col) # pillole, verdetto e citazione: layout dal basso pills = [f'{m["code"]} {m["name"]}' for m in d["modules"]] rows = layout_rows(pills, lambda p: text_w(dr, p, f_pill) + 40 * S, (W - 120) * S, 14 * S) nq_max = 1 if len(rows) >= 3 else 2 qlines = (fit_lines(wrap_px(dr, "“" + d["quote"] + "”", f_quote, (W - 160) * S), nq_max) if d["quote"] else []) vlines = fit_lines(wrap_px(dr, d["verdict"], f_verd, (W - 140) * S), 2) block_h = (len(vlines) * 42 + (16 + len(qlines) * 30 if qlines else 0)) * S pills_end = cy + 240 * S + len(rows) * 58 * S space = (FOOTER_Y - 12) * S - pills_end verdict_top = pills_end + max(0, (space - block_h) / 2) quote_top = verdict_top + (len(vlines) * 42 + 16) * S y = cy + 240 * S for row in rows: total = sum(w for _, w in row) + 14 * S * (len(row) - 1) x = (W * S - total) / 2 for p, wpx in row: dr.rounded_rectangle([x, y, x + wpx, y + 48 * S], radius=24 * S, fill=CARD, outline=MUTED, width=2 * S) tw = text_w(dr, p, f_pill) dr.text((x + (wpx - tw) / 2, y + 10 * S), p, font=f_pill, fill=INK) x += wpx + 14 * S y += 58 * S y = verdict_top for line in vlines: ctext(y, line, f_verd, INK) y += 42 * S y = quote_top for line in qlines: ctext(y, line, f_quote, MUTED) y += 30 * S ctext((FOOTER_Y - 8) * S, d["footer"], f_foot, MUTED) img = img.resize((W, H), Image.LANCZOS) img.save(out_path, "PNG", optimize=True) def main(): if len(sys.argv) < 3: print(__doc__) sys.exit(1) d = load(sys.argv[1]) out = sys.argv[2] os.makedirs(out, exist_ok=True) svg_path = os.path.join(out, "cringiometro.svg") with open(svg_path, "w", encoding="utf-8") as f: f.write(svg(d)) print("svg:", svg_path) try: import PIL # noqa: F401 except ImportError: print("png: Pillow non installato (pip install pillow); consegnato solo l'SVG") return png_path = os.path.join(out, "cringiometro.png") png(d, png_path) print("png:", png_path) if __name__ == "__main__": main()
-
-
SKILL.md 5.7 KB
--- name: linkedin-cringe-meter description: Il Cringiometro. Dato l'URL (o il testo) di un post LinkedIn, ne misura il livello di cringe da 1 a 10 con la scala e il catalogo dei 37 moduli della skill linkedin-cringe, dice quali ganci ha preso, il registro, il sapore-AI e la lead-gen, e produce un report markdown più un'immagine 1080x1080 anonima, pronta da usare come risposta sotto al post. Usala quando l'utente chiede "quanto è cringe questo post", "misura / valuta / cringiometro", o vuole un'immagine di risposta stile blastometro. --- # Cringiometro: quanto è cringe questo post Terza skill della famiglia: `linkedin-cringe` scrive il post, `linkedin-cringe-analytics` misura i commenti, il Cringiometro misura il post di qualcun altro (o il proprio) con lo stesso metro. L'ispirazione è il blastometro, spostato sul cringe: un voto, i ganci presi, una frase secca, e un'immagine da lasciare nei commenti. **Si giudica il post, mai la persona.** Nessun nome, azienda, headline o foto dell'autore compare nel report o nell'immagine. Questo non è negoziabile. ## Flusso ### 1. Acquisizione del post Serve solo il testo del post (espanso, con l'"…altro" cliccato), più un'indicazione se c'è un'immagine allegata e cosa mostra. Niente commenti: quelli sono della skill analytics. **A. URL + Claude in Chrome.** Se i tool `mcp__claude-in-chrome__*` sono disponibili, segui il *Setup* di `../linkedin-cringe-analytics/references/estrazione.md` (caricamento tool, `list_connected_browsers`, scelta browser, `tabs_context_mcp`, `navigate`). Gli short link `lnkd.in` si risolvono da soli con la navigazione. Poi: - clicca l'"…altro" del post se il testo è troncato; - prendi il testo del post con `get_page_text` o `read_page` limitato al contenitore del post (l'elemento con `componentkey` che inizia per `urn:li:activity:` o il primo `[data-urn]`), senza i commenti; - annota se c'è un'immagine, un video, un carosello o un sondaggio, e in una riga cosa si vede (senza persone riconoscibili). Se la pagina chiede il login o il browser non è collegato, passa a B senza insistere. **B. Testo incollato.** L'utente incolla il post in chat, o passa uno screenshot: la skill legge quello. Dallo screenshot ignora nome, foto e headline dell'autore anche se visibili. Il resto del flusso è identico. I dati grezzi (URL, testo integrale, JSON di lavoro) stanno nella scratchpad di sessione. Nel repository entra solo il report anonimizzato, l'immagine e il JSON con i soli campi che compaiono nell'immagine. ### 2. Valutazione Leggi `references/valutazione.md` e, attraverso i suoi rimandi, `tassonomia.md` e `moduli.md` della skill gemella (`../linkedin-cringe/references/`). Se la skill gemella non c'è, fermati e dillo: senza catalogo non c'è metro. Produci, nell'ordine: livello 1-10 con la parola della scala, registro (credibile / parodico / surreale deadpan / cringe dichiarato), lo scarto in una riga, i moduli rilevati con una citazione breve ciascuno (massimo 8, in ordine di peso), l'eventuale gancio reale, sapore-AI, lead-gen, il verdetto in una frase, e i moduli che mancano per salire di un livello. Prima di dare il voto, controlla la sezione *Cosa non si valuta* della rubrica: lutti, malattie e licenziamenti veri fermano la valutazione. ### 3. Report e immagine Scrivi `analisi/YYYY-MM-DD-<slug>/cringiometro.md` seguendo `references/report.md` (slug dal tema del post, mai dal nome dell'autore). Poi genera l'immagine: 1. scrivi `cringiometro.json` nella stessa cartella con `score`, `register`, `modules` (codice + nome corto, max 8), `verdict` (la frase, max ~90 caratteri: sull'immagine deve stare in due righe), `quote` (una citazione del post di max ~90 caratteri, senza nomi) e `lang`; **L'immagine parla la lingua del post**: `lang` è il codice della lingua del post (`it`, `en`, `de`, `fr`, `es`; altre lingue: `en` più `labels` per i testi fissi), e nomi dei moduli, verdetto e citazione vanno scritti in quella lingua. Il report e la chat restano nella lingua dell'utente; 2. lancia `python scripts/cringiometro.py analisi/YYYY-MM-DD-<slug>/cringiometro.json analisi/YYYY-MM-DD-<slug>/` (percorso dello script relativo a questa skill): produce `cringiometro.svg` e, se Pillow è installato, `cringiometro.png` 1080×1080; 3. apri il PNG e guardalo prima di consegnarlo: testo che sborda, pillole tagliate, verdetto su tre righe sono da correggere accorciando i testi nel JSON e rilanciando. Se Pillow manca, consegna l'SVG e di' come installarlo (`pip install pillow`). ### 4. Consegna In chat: livello e parola, i moduli in una riga, il verdetto. Poi il PNG con `SendUserFile` e il percorso del report. La prima volta, una riga: l'immagine non porta nomi, e lasciarla come risposta sotto al post di un estraneo è una scelta sua. Chiudi offrendo il giro successivo: rigenerare l'immagine con un verdetto diverso, o passare il post a `linkedin-cringe` per la versione "come sarebbe a 10". ## Paletti - **Nessun giudizio sulla persona.** Il soggetto delle frasi è il post. Vietati aggettivi sull'autore e riferimenti a età, genere, provenienza, aspetto, ruolo. - **Anonimato totale** in report, immagine e chat: niente nome, azienda, headline, foto, link al profilo. L'URL del post resta in scratchpad. - **Lutti, malattie, licenziamenti veri**: non si valutano. Si dice e ci si ferma. - **Post privati di persone non pubbliche** (figli, matrimoni, salute): si valutano al massimo gli elementi formali, dichiarandolo. - Se l'utente vuole valutare un proprio post generato con `linkedin-cringe`, il registro è "cringe dichiarato" e il voto vale lo stesso: è il collaudo del generatore. - Se l'utente chiede di pubblicare l'immagine come risposta, ricordaglielo una volta e poi fai quello che chiede. Non pubblica la skill: consegna il file.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.