Claude Skill

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

LLM Mart · 0 points · 12 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download matteobaccan-linkedincringe-.claude_skills_linkedin-cringe-meter-88bb824.zip · 26 KB
Part of matteobaccan/linkedincringe — 3 skills

Install

skills CLI npx skills add https://github.com/matteobaccan/LinkedInCringe/tree/main/.claude/skills/linkedin-cringe-meter
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install matteobaccan-linkedincringe@llmmart
Git 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_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.
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
                   .replace('"', "&quot;"))
      
      
      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.

No comments yet.

Reviews (0)

No reviews yet.

Related