#!/usr/bin/env node /** * Anotacoes de pronuncia para o Italiano Vivo. * * O italiano e fonetico, mas varios grupos de letras soam DIFERENTE do que um * falante de PT-BR espera ler. Casos onde o som "muda" em relacao a leitura * portuguesista: * Voglio/griglia -> GLI = som de 'lh' ('vó-lho'), nunca 'vóglio' * bagno/giorno -> GN = 'nh'; GG antes de E/I = 'dj' macio * scusi/cena -> SC/C antes de I/E = 'ch' italiano; SCH mantem 'sk' * che/chi -> CH = K seco, nunca 'tch' * grazie/piazza -> Z/ZZ = 'ts' surdo ('grá-tsie', 'pít-tsa') * casa/stazione -> S entre vogais = 'z'; S + surda = 'z' mudo * piatto/palla -> consoante dupla alonga o som (muda significado!) * * Detecta esses padroes nas frases (chunks e dialogos) das missoes e grava o * campo `pronuncia` (dica curta em PT-BR citando a palavra da frase). * * Uso: * node tools/pronuncia.mjs # dry-run: relatorio por missao * node tools/pronuncia.mjs --write # grava "pronuncia" em content/missions/*.json */ import { readFileSync, writeFileSync, readdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const MISSIONS_DIR = join(__dirname, "..", "content", "missions"); // Palavras funcionais que nao devem disparar regras (evita falsos positivos). const SKIP_WORDS = new Set([ "e", "un", "una", "uno", "il", "lo", "la", "i", "le", "di", "a", "ad", "al", "alla", "allo", "da", "dal", "dalla", "in", "con", "su", "per", "tra", "fra", "non", "si", "mi", "ti", "ci", "vi", "ne", "ho", "hai", "ha", "hanno", "sono", "sei", "siamo", "siete", "questo", "questa", "quello", "quella", "qui", "qua", "gli", ]); // Regras por palavra: [id, regexNaPalavra, dicaPT com placeholder {w}] export const WORD_RULES = [ ["gl_lh", /gl[iyeò]/i, "'{w}': GLI tem som de 'lh' — fala-se 'vó-lho', nunca 'vóglio'."], ["gn_nh", /g[nh]n?|ghn/i, "'{w}': GN tem som de 'nh' (como em 'sonho')."], ["sc_x", /sc[ièé]/i, "'{w}': SC antes de I/E tem som de 'ch' italiano (nunca 'sk')."], ["sch_sk", /sch[ièé]/i, "'{w}': SCH mantém o K — som de 'sk'."], ["ch_k", /ch[eioà]/i, "'{w}': CH é sempre K seco (nunca 'tch' do português)."], ["gh_g", /gh[eio]/i, "'{w}': GH mantém G duro mesmo antes de E/I."], ["zz_ts", /zz/i, "'{w}': ZZ duplo soa 'tss' longo e surdo (como 'pizza' = 'pít-tsa')."], ["z_ts", /(?:^|(?<=[aeiou]))z/i, "'{w}': Z costuma soar 'ts' surdo (como 'grazie' = 'grá-tsie')."], ["cc_tch", /cc[h]?[ei]/i, "'{w}': CC antes de E/I soa 'tch' duplo."], ["gg_dj", /gg[h]?[ei]/i, "'{w}': GG antes de E/I soa 'dj' macio e longo (como 'giorno')."], ["g_soft", /(? order.indexOf(a.id) - order.indexOf(b.id)); return merged.slice(0, 2); // maximo 2 dicas por frase } export function buildPronuncia(text) { return detectPronuncia(text).map((h) => h.hint).join(" "); } function main() { const write = process.argv.includes("--write"); const files = readdirSync(MISSIONS_DIR).filter((f) => f.endsWith(".json")).sort(); let totalTagged = 0; let totalItems = 0; for (const file of files) { const path = join(MISSIONS_DIR, file); const mission = JSON.parse(readFileSync(path, "utf-8")); let tagged = 0; let items = 0; const annotate = (obj) => { items++; const p = buildPronuncia(obj.it); if (p) { obj.pronuncia = p; tagged++; } else if ("pronuncia" in obj) { delete obj.pronuncia; } }; for (const chunk of mission.chunks ?? []) annotate(chunk); for (const dialogue of mission.dialogues ?? []) { for (const line of dialogue) annotate(line); } totalTagged += tagged; totalItems += items; console.log(`${mission.id}: ${tagged}/${items} frases com alternancia de som`); if (write) { writeFileSync(path, JSON.stringify(mission, null, 2) + "\n", "utf-8"); } } console.log(`\n${write ? "GRAVADO:" : "Dry-run:"} ${totalTagged}/${totalItems} frases anotadas em ${files.length} missoes.`); } main();