hama-js
Version:
G2P, phoneme-ASR, and P2G inference for Node, Bun, and browsers, powered by a self-contained WASM engine (no onnxruntime).
73 lines • 3.02 kB
JavaScript
// Pure-string P2G I/O helpers, ported from hama-training p2g_training.text so
// the TS runtime matches Python/PyTorch exactly. Shared by the Node and browser
// P2G models.
export const P2G_SPECIAL_TOKENS = new Set([
"<pad>", "<unk>", "<bos>", "<eos>", "<src>", "<tgt>", "<no_source>",
]);
// Hangul syllable composition constants.
const S_BASE = 0xac00;
const L_BASE = 0x1100;
const V_BASE = 0x1161;
const T_BASE = 0x11a7;
const L_COUNT = 19;
const V_COUNT = 21;
const T_COUNT = 28;
const N_COUNT = V_COUNT * T_COUNT;
const cp = (ch) => ch.codePointAt(0) ?? 0;
const isLeadingJamo = (ch) => cp(ch) >= L_BASE && cp(ch) < L_BASE + L_COUNT;
const isVowelJamo = (ch) => cp(ch) >= V_BASE && cp(ch) < V_BASE + V_COUNT;
const isTrailingJamo = (ch) => cp(ch) > T_BASE && cp(ch) < T_BASE + T_COUNT;
const composeSyllable = (leading, vowel, trailing) => {
const li = cp(leading) - L_BASE;
const vi = cp(vowel) - V_BASE;
const ti = trailing == null ? 0 : cp(trailing) - T_BASE;
return String.fromCodePoint(S_BASE + li * N_COUNT + vi * T_COUNT + ti);
};
export const normalizePhonemeTokens = (value) => {
const raw = typeof value === "string" ? value.split(/\s+/) : value.map((t) => String(t).trim());
const tokens = [];
for (const token of raw) {
if (!token)
continue;
if (token === "|" && (tokens.length === 0 || tokens[tokens.length - 1] === "|"))
continue;
tokens.push(token);
}
while (tokens.length > 0 && tokens[tokens.length - 1] === "|")
tokens.pop();
return tokens;
};
export const renderText = (tokens) => {
const list = tokens.map((t) => String(t));
const out = [];
let idx = 0;
while (idx < list.length) {
const token = list[idx];
if (isLeadingJamo(token) && idx + 1 < list.length && isVowelJamo(list[idx + 1])) {
const trailing = idx + 2 < list.length && isTrailingJamo(list[idx + 2]) ? list[idx + 2] : null;
out.push(composeSyllable(token, list[idx + 1], trailing));
idx += trailing != null ? 3 : 2;
continue;
}
out.push(token);
idx += 1;
}
return out.join("");
};
export const normalizeP2gText = (text) => text.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
/** Filter special tokens and pair each kept output token with its source-phoneme
* alignment, shared by the Node and browser P2G models. */
export const decodeP2GOutput = (genIds, align, tokens, source) => {
const outTokens = [];
const alignments = [];
for (let i = 0; i < genIds.length; i++) {
const token = tokens[genIds[i]];
if (P2G_SPECIAL_TOKENS.has(token))
continue;
outTokens.push(token);
const ai = align[i] ?? -1;
alignments.push({ token, phonemeIndex: ai, phoneme: ai >= 0 && ai < source.length ? source[ai] : "" });
}
return { text: normalizeP2gText(renderText(outTokens)), tokens: outTokens, alignments };
};
//# sourceMappingURL=p2g-text.js.map