|
|
#!/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", /(?<![ghc])g[eièé]/i, "'{w}': G antes de E/I soa 'dj' macio (como 'giorno' = 'djórno')."],
|
|
|
|
|
|
["cg_k", /cch/i, "'{w}': CCH soa 'kk' seco e longo (como 'amico')."],
|
|
|
["s_inter_vocalica", /[aeiou]s[aeiou]/i, "'{w}': S entre vogais soa 'z' (como 'casa' = 'káza')."],
|
|
|
["s_sorda", /s[tcpkf]/i, "'{w}': S antes de consoante surda soa 'z' (como 'stazione')."],
|
|
|
];
|
|
|
|
|
|
function cleanWord(w) {
|
|
|
return w.replace(/[^\p{L}\p{M}'’-]/gu, "");
|
|
|
}
|
|
|
|
|
|
function normalizeWord(w) {
|
|
|
return cleanWord(w)
|
|
|
.normalize("NFD")
|
|
|
.replace(/[\u0300-\u036f]/g, "")
|
|
|
.toLowerCase();
|
|
|
}
|
|
|
|
|
|
/** Aplica regras a uma palavra; retorna dicas unicas com a palavra citada. */
|
|
|
function wordHints(word) {
|
|
|
const norm = normalizeWord(word);
|
|
|
if (!norm || SKIP_WORDS.has(norm)) return [];
|
|
|
const out = [];
|
|
|
for (const [id, re, hint] of WORD_RULES) {
|
|
|
const m = word.match(re);
|
|
|
if (!m) continue;
|
|
|
// SC+I/E que na verdade e SCH nao dispara a regra do 'ch' italiano
|
|
|
if (id === "sc_x" && /^sch/i.test(m[0])) continue;
|
|
|
out.push({ id, hint, word: cleanWord(word) });
|
|
|
}
|
|
|
return out;
|
|
|
}
|
|
|
|
|
|
export function detectPronuncia(text) {
|
|
|
const words = text.split(/\s+/);
|
|
|
const merged = [];
|
|
|
const seen = new Set();
|
|
|
for (const w of words) {
|
|
|
for (const h of wordHints(w)) {
|
|
|
if (!seen.has(h.id)) {
|
|
|
seen.add(h.id);
|
|
|
merged.push({ id: h.id, hint: h.hint.replace("{w}", `“${h.word}”`) });
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// Sons que mais mudam para o brasileiro vao primeiro
|
|
|
const order = ["gl_lh", "gn_nh", "sc_x", "sch_sk", "ch_k", "gh_g", "zz_ts", "z_ts", "cc_tch", "gg_dj", "g_soft", "cg_k", "s_sorda", "s_inter_vocalica"];
|
|
|
merged.sort((a, b) => 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();
|