La traduzione viene troncata in silenzio quando l'analisi del flusso JSON si interrompe (nessun errore segnalato)

Riepilogo
DiscourseAi::Translation::PostLocalizer (localizzazione dei contenuti) produce occasionalmente
traduzioni tronche senza registrare alcun errore (log di Rails,
log di Sidekiq, code di retry/dead di Sidekiq). La troncatura avviene a metà frase,
in modo silenzioso, e il risultato parziale viene salvato come se fosse una traduzione
completa e riuscita.

Osservato su: Discourse core + discourse-ai 46bea6613 (2026-07-09), LLM:
Gemini 2.5 Flash tramite provider Google.

Prove di riproduzione
Due casi reali sulla nostra istanza:

  • Un post tedesco di ~2090 caratteri tradotto in spagnolo ha prodotto solo 50 caratteri,
    interrompendosi immediatamente prima di una parola tra virgolette (“aufwendiger”) nel testo
    sorgente.
  • Un post tedesco di ~1830 caratteri tradotto in inglese (83 caratteri) e slovacco
    (97 caratteri) — entrambi si sono interrotti nello stesso punto esatto della frase,
    immediatamente prima di uno shortcode emoji (:collision:) nel testo sorgente.
    Tutte le altre lingue (fr, it, pt, ru, he) per lo stesso post sono state
    tradotte completamente (1600–1950 caratteri).

Causa principale (per quanto riesco a rintracciare)
In lib/completions/structured_output.rb, StructuredOutput#read_buffered_property
ricade su DiscourseAi::Utils::BestEffortJsonParser.extract_key ogni volta
che @partial_json_tracker.broken? è true. Questo fallback restituisce qualsiasi cosa sia
stata parsata prima che il flusso JSON si interrompesse — senza eccezioni, senza avvisi, senza
alcuna indicazione che l’output sia incompleto. In
lib/translation/base_translator.rb#get_translation, questo valore viene utilizzato così com’è
come traduzione finale e persistito tramite PostLocalization#save!.

I punti di troncatura nei nostri dati cadono immediatamente prima di un carattere “speciale”
nel testo sorgente (una virgoleta dritta, uno shortcode emoji),
suggerendo che il tracker del flusso JSON si confonda per determinati
caratteri/sequenze nell’output streaming del modello e marchi il flusso
come interrotto (broken?) prematuramente.

Impatto
I contenuti vengono pubblicati con traduzioni visibilmente incomplete, a volte prive di senso,
e non c’è alcun segnale operativo (log, job morto, notifica admin)
per rilevarlo. L’abbiamo scoperto solo perché un membro della comunità
ha notato del testo incomprensibile a metà frase nell’interfaccia utente.

Correzioni suggerite

  • Segnala un avviso/errore (Rails.logger + Discourse-error-log) ogni volta
    che StructuredOutput#broken? è true e viene utilizzato il fallback best-effort, in modo
    che gli amministratori possano rilevare il problema invece di pubblicare in silenzio contenuti parziali.
  • Considera la validazione della lunghezza della traduzione rispetto a una soglia di sanità mentale relativa
    alla lunghezza del testo sorgente prima di salvare una PostLocalization.

Disponibile a fornire coppie complete di testo greco/tradotto in privato se utile per
debuggare il tracker JSON.

2 Mi Piace

Aggiungo un dato a questo — stiamo osservando ciò che sembra essere lo stesso meccanismo sottostante, ma che si manifesta in modo diverso.

Osservato su: Discourse core + discourse-ai, LLM: GPT-5.1 via OpenAI (connessione API personalizzata, non un modello predefinito).

Evidenze di riproduzione

Stesso post sorgente (russo, ~2000 caratteri, intestazioni/grassetto/elenco puntati/link),
tradotto in più lingue tramite traduzione AI nativa:

  • Polacco (pl): la markdown dell’immagine incorporata ha perso la sua struttura — è passata da
    ![testo alternativo|690x460](upload://...) a un link malformato privo del prefisso !
    e del separatore |, con il testo alternativo e le dimensioni uniti come testo normale. Visualizzato come un link cliccabile invece che come un’immagine incorporata.
  • Ucraino (uk): le sequenze letterali \n\n appaiono come testo normale in tutto il post, al posto delle interruzioni di paragrafo. La markdown dell’immagine in questa stessa traduzione era intatta — quindi la corruzione non è legata a un singolo sintomo fisso, ma varia a seconda dell’esecuzione.

A differenza della troncamento descritto sopra, il nostro caso non mostra contenuto mancante —
il testo completo è presente, ma con sequenze di escape / sintassi markdown corrotte invece di un output abbreviato. Provider diverso (OpenAI vs Google qui), sintomo diverso (corruzione vs troncamento), stessa causa radice sospettata:
StructuredOutput#read_buffered_property che ricade su
BestEffortJsonParser#extract_key, che non decodifica le sequenze di stringhe JSON (\n rimane letterale) e sembra anche gestire male i caratteri speciali adiacenti alla sintassi markdown quando il fallback si attiva a metà parsing.

Nessun errore nei log di Sidekiq o Rails dalla nostra parte — stesso comportamento di fallimento silenzioso.