CoolFace
Apppublic

snowy-0wl/supertonic-3

sourceHugging Faceopenrailupdated 19d agoView on Hugging Face
0likes
script.js3100 linesDownload Raw Back to root
1import * as ort from 'onnxruntime-web';2const presetTexts = window.presetTexts || {};3 4const PLAY_ICON_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 5v14l11-7-11-7z"></path></svg>`;5const PAUSE_ICON_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 6h3v12H8V6zm5 0h3v12h-3V6z"></path></svg>`;6const STOP_ICON_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M7 7h10v10H7V7z"></path></svg>`;7 8// Lightning background parallax9(function initLightningParallax() {10    if (typeof document === 'undefined') {11        return;12    }13 14    const runBlink = (className, onComplete) => {15        let remaining = 1 + Math.round(Math.random());16        const blink = () => {17            if (remaining-- <= 0) {18                if (typeof onComplete === 'function') {19                    onComplete();20                }21                return;22            }23            const wait = 20 + Math.random() * 80;24            document.body.classList.add(className);25            setTimeout(() => {26                document.body.classList.remove(className);27                setTimeout(blink, wait);28            }, wait);29        };30        blink();31    };32 33    const schedule = () => {34        setTimeout(() => runBlink('lightning-flicker', schedule), Math.random() * 10000);35    };36    schedule();37})();38 39function escapeHtml(value) {40    return value.replace(/[&<>"']/g, (match) => {41        switch (match) {42            case '&': return '&amp;';43            case '<': return '&lt;';44            case '>': return '&gt;';45            case '"': return '&quot;';46            case "'": return '&#39;';47            default: return match;48        }49    });50}51 52function getErrorMessage(error) {53    if (error instanceof Error && error.message) {54        return error.message;55    }56    if (typeof error === 'string') {57        return error;58    }59    if (error === undefined || error === null) {60        return 'Unknown error';61    }62    try {63        return JSON.stringify(error);64    } catch (_) {65        return String(error);66    }67}68 69function formatStatValueWithSuffix(value, suffix, options = {}) {70    const { firstLabel = false } = options;71    if (value === undefined || value === null) {72        return '';73    }74    if (!suffix) {75        const raw = `${value}`;76        return escapeHtml(raw);77    }78    const raw = `${value}`.trim();79    if (!raw || raw === '--' || raw === '-' || raw.toLowerCase() === 'error') {80        return escapeHtml(raw);81    }82    const appendSuffix = (segment, includePrefix = false) => {83        const trimmed = segment.trim();84        if (!trimmed) {85            return '';86        }87        const escapedValue = `<span class="stat-value-number">${escapeHtml(trimmed)}</span>`;88        const suffixSpan = `<span class="stat-label stat-suffix">${escapeHtml(suffix)}</span>`;89        const prefixSpan = includePrefix && firstLabel90            ? `<span class="stat-label stat-suffix stat-prefix">First</span>`91            : '';92        const segmentClass = includePrefix && firstLabel93            ? 'stat-value-segment has-prefix'94            : 'stat-value-segment';95        return `<span class="${segmentClass}">${prefixSpan}${escapedValue}${suffixSpan}</span>`;96    };97    if (raw.includes('/')) {98        const parts = raw.split('/');99        const segments = parts.map((part, index) => appendSuffix(part, index === 0));100        return segments.join(' / ');101    }102    return appendSuffix(raw);103}104 105/**106 * Unicode text processor107 */108export class UnicodeProcessor {109    constructor(indexer) {110        this.indexer = indexer;111    }112 113    call(textList, lang = null) {114        const processedTexts = textList.map(t => preprocessText(t, lang));115        const textIdsLengths = processedTexts.map(t => t.length);116        const maxLen = Math.max(...textIdsLengths);117        118        const textIds = [];119        const unsupportedChars = new Set();120        121        for (let i = 0; i < processedTexts.length; i++) {122            const row = new Array(maxLen).fill(0);123            const unicodeVals = textToUnicodeValues(processedTexts[i]);124            for (let j = 0; j < unicodeVals.length; j++) {125                const indexValue = this.indexer[unicodeVals[j]];126                // Check if character is supported (not -1, undefined, or null)127                if (indexValue === undefined || indexValue === null || indexValue === -1) {128                    unsupportedChars.add(processedTexts[i][j]);129                    row[j] = 0; // Use 0 as fallback130                } else {131                    row[j] = indexValue;132                }133            }134            textIds.push(row);135        }136        137        const textMask = getTextMask(textIdsLengths);138        return { textIds, textMask, unsupportedChars: Array.from(unsupportedChars) };139    }140}141 142const AVAILABLE_LANGS = ["en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi"];143 144/**145 * Language detection \u2014 Unicode-script first, then Latin scoring.146 *147 * Strategy:148 *   1. Detect non-Latin scripts via Unicode block matches (high confidence).149 *      Hangul \u2192 ko, Hiragana/Katakana \u2192 ja, Arabic \u2192 ar, Devanagari \u2192 hi,150 *      Greek \u2192 el, Cyrillic \u2192 bg/ru/uk via stopword tiebreak.151 *   2. For Latin script, score against a curated set of distinctive152 *      characters and stopwords for the most common languages we cover.153 *      Languages without a high-signal scoring rule (e.g. et, fi, hu, sk,154 *      sl, ro, lt, lv, hr, id) fall through; the caller then uses the155 *      dropdown selection.156 *157 * The model itself is language-agnostic, so misdetection is graceful:158 * the wrong tag still produces understandable speech.159 */160export function detectLanguage(text) {161    if (!text || text.trim().length < 3) {162        return null;163    }164 165    const sampleText = text.length > 200 ? text.substring(text.length - 200) : text;166    const normalizedText = sampleText.normalize('NFC').toLowerCase();167 168    // 1) Non-Latin scripts via Unicode blocks (definitive)169    if (/[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]/.test(normalizedText)) return 'ko';170    if (/[\u3040-\u30FF]/.test(normalizedText)) return 'ja';                  // Hiragana/Katakana171    if (/[\u0600-\u06FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(normalizedText)) return 'ar';172    if (/[\u0900-\u097F]/.test(normalizedText)) return 'hi';                  // Devanagari173    if (/[\u0370-\u03FF]/.test(normalizedText)) return 'el';                  // Greek174 175    // Cyrillic \u2014 distinguish bg/ru/uk via stopwords + diacritics176    if (/[\u0400-\u04FF]/.test(normalizedText)) {177        const cyrText = ' ' + normalizedText + ' ';178        let ru = 0, uk = 0, bg = 0;179        for (const w of ['\u0438', '\u043D\u0435', '\u044D\u0442\u043E', '\u0447\u0442\u043E', '\u043A\u0430\u043A', '\u0434\u043B\u044F', '\u043D\u043E', '\u0432\u0441\u0435']) if (cyrText.includes(' ' + w + ' ')) ru += 2;180        for (const w of ['\u0442\u0430', '\u043D\u0435', '\u0449\u043E', '\u044F\u043A', '\u0434\u043B\u044F', '\u0430\u043B\u0435', '\u0432\u0441\u0435', '\u0430\u0431\u043E']) if (cyrText.includes(' ' + w + ' ')) uk += 2;181        for (const w of ['\u0438', '\u043D\u0430', '\u043D\u0435', '\u0447\u0435', '\u043A\u0430\u0442\u043E', '\u0441\u044A\u0441', '\u0441\u044A\u0449\u043E']) if (cyrText.includes(' ' + w + ' ')) bg += 2;182        if (/[\u0456\u0457\u0454\u0491]/.test(normalizedText)) uk += 6;183        if (/\u044A/.test(normalizedText)) bg += 4;184        if (/[\u044B\u044D\u0451]/.test(normalizedText)) ru += 4;185        if (uk > ru && uk > bg) return 'uk';186        if (bg > ru && bg > uk) return 'bg';187        return 'ru';188    }189 190    // 2) Latin script scoring191    const scores = { en: 0, es: 0, fr: 0, pt: 0, de: 0, it: 0, nl: 0, pl: 0, sv: 0, da: 0, tr: 0, vi: 0 };192 193    // Highly distinctive characters194    if (/\u00F1/.test(normalizedText)) scores.es += 15;195    if (/[\u00BF\u00A1]/.test(normalizedText)) scores.es += 12;196    if (/\u00E3/.test(normalizedText)) scores.pt += 15;197    if (/\u00F5/.test(normalizedText)) scores.pt += 15;198    if (/\u0153/.test(normalizedText)) scores.fr += 15;199    if (/[\u00F9\u00FB]/.test(normalizedText)) scores.fr += 10;200    if (/\u00E7/.test(normalizedText)) { scores.fr += 4; scores.pt += 4; scores.tr += 4; }201    if (/[\u00E8\u00EA\u00EB]/.test(normalizedText)) scores.fr += 5;202    if (/[\u00E0\u00E2]/.test(normalizedText)) scores.fr += 3;203    if (/[\u00EE\u00EF]/.test(normalizedText)) scores.fr += 4;204    if (/\u00F4/.test(normalizedText)) scores.fr += 3;205    if (/\u00DF/.test(normalizedText)) scores.de += 15;206    if (/[\u00E4\u00F6\u00FC]/.test(normalizedText)) { scores.de += 4; scores.sv += 2; }207    if (/\u00E5/.test(normalizedText)) { scores.sv += 8; scores.da += 8; }208    if (/[\u00E6\u00F8]/.test(normalizedText)) scores.da += 12;209    if (/[\u0105\u0119\u0107\u0142\u0144\u015B\u017A\u017C]/.test(normalizedText)) scores.pl += 12;210    if (/[\u011F\u015F\u0131\u0130]/.test(normalizedText)) scores.tr += 12;211    if (/[\u01A1\u01B0\u0103\u0111]/.test(normalizedText)) scores.vi += 12;212    if (/[\u00E0\u1EA3\u00E3\u00E1\u1EA1\u1EB1\u1EAF\u1EB3\u1EB5\u1EB7\u00E2\u1EA7\u1EA5\u1EA9\u1EAB\u1EAD\u00E8\u1EBB\u1EBD\u00E9\u1EB9\u00EA\u1EC1\u1EBF\u1EC3\u1EC5\u1EC7\u00EC\u1EC9\u0129\u00ED\u1ECB\u00F2\u1ECF\u00F5\u00F3\u1ECD\u00F4\u1ED3\u1ED1\u1ED5\u1ED7\u1ED9\u01A1\u1EDD\u1EDB\u1EDF\u1EE1\u1EE3\u00F9\u1EE7\u0169\u00FA\u1EE5\u01B0\u1EEB\u1EE9\u1EED\u1EEF\u1EF1\u1EF3\u1EF7\u1EF9\u00FD\u1EF5]/.test(normalizedText)) scores.vi += 6;213 214    // Exclusive stopwords (highest signal per language)215    const exclusiveWords = {216        en: ['the', 'is', 'are', 'was', 'were', 'have', 'has', 'been', 'will', 'would', 'this', 'that', 'with', 'from', 'they', 'what', 'which', 'there', 'their', 'about', 'these', 'other', 'into', 'just', 'your', 'some', 'than', 'them', 'then', 'only', 'being', 'through', 'after', 'before'],217        es: ['el', 'los', 'las', 'est\u00E1', 'est\u00E1n', 'porque', 'pero', 'muy', 'tambi\u00E9n', 'm\u00E1s', 'este', 'esta', 'estos', 'estas', 'ese', 'esa', 'nosotros', 'ellos', 'ellas', 'hola', 'gracias', 'ahora', 'siempre', 'nunca'],218        fr: ['le', 'les', 'est', 'sont', 'dans', 'ce', 'cette', 'ces', 'elle', 'ils', 'elles', 'nous', 'vous', 'avec', 'sur', 'pas', 'plus', 'tout', 'bien', 'fait', '\u00EAtre', 'avoir', 'donc', 'car', 'jamais', 'toujours', 'aussi', 'tr\u00E8s'],219        pt: ['os', 'as', 's\u00E3o', 'est\u00E3o', 'n\u00E3o', 'na', 'no', 'da', 'do', 'das', 'dos', 'ao', 'aos', 'ele', 'ela', 'eles', 'elas', 'n\u00F3s', 'voc\u00EA', 'voc\u00EAs', 'seu', 'sua', 'muito', 'tamb\u00E9m', 'foi', 'mesmo', 'at\u00E9', 'isso', 'ol\u00E1', 'obrigado', 'obrigada'],220        de: ['der', 'die', 'das', 'und', 'ist', 'sind', 'nicht', 'ich', 'wir', 'sie', 'er', 'mit', 'f\u00FCr', 'auf', 'eine', 'einen', 'einem', 'auch', 'aber', 'doch', 'noch', 'nur', 'sehr', 'so', 'oder', 'wenn', 'weil', 'als'],221        it: ['il', 'la', 'gli', 'le', '\u00E8', 'sono', 'non', 'che', 'di', 'per', 'con', 'una', 'uno', 'noi', 'voi', 'loro', 'questo', 'questa', 'anche', 'ma', 'pi\u00F9', 'molto', 'sempre', 'mai'],222        nl: ['de', 'het', 'een', 'en', 'is', 'zijn', 'niet', 'van', 'voor', 'met', 'op', 'aan', 'om', 'maar', 'ook', 'wel', 'nog', 'als', 'dan', 'wat', 'wie', 'hoe', 'omdat', 'altijd', 'nooit'],223        pl: ['jest', 's\u0105', 'nie', 'si\u0119', 'tak', 'czy', 'ale', 'oraz', 'jak', 'tym', 'tego', 'tej', 'jeszcze', 'tylko', 'bardzo', 'zawsze', 'nigdy'],224        sv: ['\u00E4r', 'och', 'inte', 'det', 'att', 'f\u00F6r', 'p\u00E5', 'med', 'som', 'jag', 'vi', 'ni', 'de', 'eller', 'men', 'ocks\u00E5', 'alltid', 'aldrig', 'bara'],225        da: ['er', 'og', 'ikke', 'det', 'at', 'for', 'p\u00E5', 'med', 'som', 'jeg', 'vi', 'de', 'eller', 'men', 'ogs\u00E5', 'altid', 'aldrig', 'bare'],226        tr: ['ve', 'ile', 'i\u00E7in', 'bir', 'bu', '\u015Fu', 'de\u011Fil', 'gibi', '\u00E7ok', 'ama', 'her', 'hi\u00E7', 'yine', 'daha'],227        vi: ['v\u00E0', 'l\u00E0', 'c\u1EE7a', 'kh\u00F4ng', 'm\u1ED9t', 'nh\u1EEFng', 'n\u00E0y', '\u0111\u00F3', 'c\u0169ng', 'v\u1EDBi', 'nh\u01B0', '\u0111\u1EC3', 'nh\u01B0ng', 'r\u1EA5t', 'lu\u00F4n', 'bao']228    };229 230    const words = normalizedText.match(/[\p{Letter}']+/gu) || [];231    for (const word of words) {232        for (const [lang, wordList] of Object.entries(exclusiveWords)) {233            if (wordList.includes(word)) {234                scores[lang] += 3;235            }236        }237    }238 239    // 3. Common n-grams (character patterns)240    const ngramPatterns = {241        en: [/th/g, /ing/g, /tion/g, /ight/g, /ould/g],242        es: [/ción/g, /mente/g, /ado/g, /ido/g],243        fr: [/tion/g, /ment/g, /eau/g, /aux/g, /eux/g, /oir/g, /ais/g, /ait/g, /ont/g],244        pt: [/ção/g, /ões/g, /mente/g, /ado/g, /ido/g, /nh/g, /lh/g],245        de: [/sch/g, /chen/g, /lich/g, /ung/g, /ein/g],246        it: [/zione/g, /mente/g, /ono/g, /are/g, /ere/g],247        nl: [/sch/g, /eer/g, /ijk/g, /aar/g],248        pl: [/cz/g, /sz/g, /rz/g, /dzie/g],249        sv: [/skt/g, /tion/g],250        da: [/skt/g, /tion/g, /tt/g],251        tr: [/lar/g, /ler/g, /siz/g, /lik/g],252        vi: [/ng/g, /nh/g, /th/g]253    };254 255    for (const [lang, patterns] of Object.entries(ngramPatterns)) {256        for (const pattern of patterns) {257            const matches = normalizedText.match(pattern) || [];258            scores[lang] += matches.length * 2;259        }260    }261 262    // 4. French apostrophe contractions263    const frenchContractions = /[cdjlmnst]'[aeiouéèêàâîïôûù]/g;264    const frenchContractionMatches = normalizedText.match(frenchContractions) || [];265    scores.fr += frenchContractionMatches.length * 5;266 267    // 5. Definite-article anchors268    if (/\bthe\b/.test(normalizedText)) scores.en += 5;269    if (/\b(el|los)\b/.test(normalizedText)) scores.es += 4;270    if (/\b(le|les)\b/.test(normalizedText)) scores.fr += 4;271    if (/\b(o|os)\b/.test(normalizedText)) scores.pt += 3;272    if (/\b(der|die|das)\b/.test(normalizedText)) scores.de += 5;273 274    // Pick winner with confidence threshold275    let maxScore = 0;276    let detectedLang = null;277 278    for (const [lang, score] of Object.entries(scores)) {279        if (score > maxScore) {280            maxScore = score;281            detectedLang = lang;282        }283    }284 285    if (maxScore >= 4) {286        return detectedLang;287    }288 289    return null;290}291 292// Language display names for toast notification (31 languages)293const LANGUAGE_NAMES = {294    'en': 'English',295    'ko': 'Korean',296    'ja': 'Japanese',297    'ar': 'Arabic',298    'bg': 'Bulgarian',299    'cs': 'Czech',300    'da': 'Danish',301    'de': 'German',302    'el': 'Greek',303    'es': 'Spanish',304    'et': 'Estonian',305    'fi': 'Finnish',306    'fr': 'French',307    'hi': 'Hindi',308    'hr': 'Croatian',309    'hu': 'Hungarian',310    'id': 'Indonesian',311    'it': 'Italian',312    'lt': 'Lithuanian',313    'lv': 'Latvian',314    'nl': 'Dutch',315    'pl': 'Polish',316    'pt': 'Portuguese',317    'ro': 'Romanian',318    'ru': 'Russian',319    'sk': 'Slovak',320    'sl': 'Slovenian',321    'sv': 'Swedish',322    'tr': 'Turkish',323    'uk': 'Ukrainian',324    'vi': 'Vietnamese'325};326 327export function preprocessText(text, lang = null) {328    // Normalize unicode characters329    text = text.normalize('NFKD');330    331    // Remove emojis332    text = text.replace(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F1E6}-\u{1F1FF}]+/gu, '');333    334    // Replace various dashes and symbols335    const replacements = {336        "–": "-",337        "‑": "-",338        "—": "-",339        "_": " ",340        "\u201C": '"',  // "341        "\u201D": '"',  // "342        "\u2018": "'",  // '343        "\u2019": "'",  // '344        "´": "'",345        "`": "'",346        "[": " ",347        "]": " ",348        "|": " ",349        "/": " ",  // FIXME: `/` should be pronounced.350        "#": " ",  // FIXME: `#` should be pronounced.351        "→": " ",352        "←": " ",353    };354    355    for (const [k, v] of Object.entries(replacements)) {356        text = text.replaceAll(k, v);357    }358 359    // Remove special symbols360    text = text.replace(/[♥☆♡©\\]/g, "");361 362    // Replace known expressions363    const exprReplacements = {364        "@": " at ",365        "e.g.,": "for example,",366        "i.e.,": "that is,",367    };368    369    for (const [k, v] of Object.entries(exprReplacements)) {370        text = text.replaceAll(k, v);371    }372    373    // Fix spacing around punctuation374    text = text.replace(/ ,/g, ",");375    text = text.replace(/ \./g, ".");376    text = text.replace(/ !/g, "!");377    text = text.replace(/ \?/g, "?");378    text = text.replace(/ ;/g, ";");379    text = text.replace(/ :/g, ":");380    text = text.replace(/ '/g, "'");381    382    // Remove duplicate quotes383    while (text.includes('""')) {384        text = text.replace(/""/g, '"');385    }386    while (text.includes("''")) {387        text = text.replace(/''/g, "'");388    }389    while (text.includes("``")) {390        text = text.replace(/``/g, "`");391    }392    393    // Remove extra spaces394    text = text.replace(/\s+/g, " ").trim();395 396    // If text doesn't end with punctuation, quotes, or closing brackets, add a period397    if (!/[.!?;:,'"')\]}…。」』】〉》›»]$/.test(text)) {398        text += ".";399    }400    401    // Add language tags402    if (lang !== null) {403        if (!AVAILABLE_LANGS.includes(lang)) {404            throw new Error(`Invalid language: ${lang}`);405        }406        text = `<${lang}>` + text + `</${lang}>`;407    } else {408        text = `<na>` + text + `</na>`;409    }410    411    return text;412}413 414export function textToUnicodeValues(text) {415    return Array.from(text).map(char => char.charCodeAt(0));416}417 418export function lengthToMask(lengths, maxLen = null) {419    maxLen = maxLen || Math.max(...lengths);420    const mask = [];421    for (let i = 0; i < lengths.length; i++) {422        const row = [];423        for (let j = 0; j < maxLen; j++) {424            row.push(j < lengths[i] ? 1.0 : 0.0);425        }426        mask.push([row]);427    }428    return mask;429}430 431export function getTextMask(textIdsLengths) {432    return lengthToMask(textIdsLengths);433}434 435export function getLatentMask(wavLengths, cfgs) {436    const baseChunkSize = cfgs.ae.base_chunk_size;437    const chunkCompressFactor = cfgs.ttl.chunk_compress_factor;438    const latentSize = baseChunkSize * chunkCompressFactor;439    const latentLengths = wavLengths.map(len => 440        Math.floor((len + latentSize - 1) / latentSize)441    );442    return lengthToMask(latentLengths);443}444 445export function sampleNoisyLatent(duration, cfgs) {446    const sampleRate = cfgs.ae.sample_rate;447    const baseChunkSize = cfgs.ae.base_chunk_size;448    const chunkCompressFactor = cfgs.ttl.chunk_compress_factor;449    const ldim = cfgs.ttl.latent_dim;450 451    const wavLenMax = Math.max(...duration.map(d => d[0][0])) * sampleRate;452    const wavLengths = duration.map(d => Math.floor(d[0][0] * sampleRate));453    const chunkSize = baseChunkSize * chunkCompressFactor;454    const latentLen = Math.floor((wavLenMax + chunkSize - 1) / chunkSize);455    const latentDim = ldim * chunkCompressFactor;456 457    const noisyLatent = [];458    for (let b = 0; b < duration.length; b++) {459        const batch = [];460        for (let d = 0; d < latentDim; d++) {461            const row = [];462            for (let t = 0; t < latentLen; t++) {463                const u1 = Math.random();464                const u2 = Math.random();465                const randNormal = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);466                row.push(randNormal);467            }468            batch.push(row);469        }470        noisyLatent.push(batch);471    }472 473    const latentMask = getLatentMask(wavLengths, cfgs);474    475    for (let b = 0; b < noisyLatent.length; b++) {476        for (let d = 0; d < noisyLatent[b].length; d++) {477            for (let t = 0; t < noisyLatent[b][d].length; t++) {478                noisyLatent[b][d][t] *= latentMask[b][0][t];479            }480        }481    }482 483    return { noisyLatent, latentMask };484}485 486export async function loadOnnx(onnxPath, opts) {487    return await ort.InferenceSession.create(onnxPath, opts);488}489 490export async function loadOnnxAll(basePath, opts, onProgress) {491    const models = [492        { name: 'Duration Predictor', path: `${basePath}/duration_predictor.onnx`, key: 'dpOrt' },493        { name: 'Text Encoder', path: `${basePath}/text_encoder.onnx`, key: 'textEncOrt' },494        { name: 'Vector Estimator', path: `${basePath}/vector_estimator.onnx`, key: 'vectorEstOrt' },495        { name: 'Vocoder', path: `${basePath}/vocoder.onnx`, key: 'vocoderOrt' }496    ];497 498    const result = {};499    let loadedCount = 0;500    501    // Load all models in parallel502    const loadPromises = models.map(async (model) => {503        const session = await loadOnnx(model.path, opts);504        loadedCount++;505        if (onProgress) {506            onProgress(model.name, loadedCount, models.length);507        }508        return { key: model.key, session };509    });510    511    // Wait for all models to load512    const loadedModels = await Promise.all(loadPromises);513    514    // Organize results515    loadedModels.forEach(({ key, session }) => {516        result[key] = session;517    });518 519    try {520        // Download counting. Skip localhost to avoid noisy requests during local testing.521        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';522        const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(hostname);523        if (!isLocalhost) {524            await fetch('https://huggingface.co/snowy-0wl/supertonic-3/resolve/main/config.json', {525                mode: 'no-cors',526                cache: 'no-store'527            });528        }529    } catch (error) {530        console.warn('Failed to update download count:', error);531    }532    return result;533}534 535export async function loadCfgs(basePath) {536    const response = await fetch(`${basePath}/tts.json`);537    return await response.json();538}539 540export async function loadProcessors(basePath) {541    const response = await fetch(`${basePath}/unicode_indexer.json`);542    const unicodeIndexerData = await response.json();543    const textProcessor = new UnicodeProcessor(unicodeIndexerData);544    545    return { textProcessor };546}547 548function parseWavFile(buffer) {549    const view = new DataView(buffer);550    551    // Check RIFF header552    const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));553    if (riff !== 'RIFF') {554        throw new Error('Not a valid WAV file');555    }556    557    const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11));558    if (wave !== 'WAVE') {559        throw new Error('Not a valid WAV file');560    }561    562    let offset = 12;563    let fmtChunk = null;564    let dataChunk = null;565    566    while (offset < buffer.byteLength) {567        const chunkId = String.fromCharCode(568            view.getUint8(offset), 569            view.getUint8(offset + 1), 570            view.getUint8(offset + 2), 571            view.getUint8(offset + 3)572        );573        const chunkSize = view.getUint32(offset + 4, true);574        575        if (chunkId === 'fmt ') {576            fmtChunk = {577                audioFormat: view.getUint16(offset + 8, true),578                numChannels: view.getUint16(offset + 10, true),579                sampleRate: view.getUint32(offset + 12, true),580                bitsPerSample: view.getUint16(offset + 22, true)581            };582        } else if (chunkId === 'data') {583            dataChunk = {584                offset: offset + 8,585                size: chunkSize586            };587            break;588        }589        590        offset += 8 + chunkSize;591    }592    593    if (!fmtChunk || !dataChunk) {594        throw new Error('Invalid WAV file format');595    }596    597    const bytesPerSample = fmtChunk.bitsPerSample / 8;598    const numSamples = Math.floor(dataChunk.size / (bytesPerSample * fmtChunk.numChannels));599    const audioData = new Float32Array(numSamples);600    601    if (fmtChunk.bitsPerSample === 16) {602        for (let i = 0; i < numSamples; i++) {603            let sample = 0;604            for (let ch = 0; ch < fmtChunk.numChannels; ch++) {605                const sampleOffset = dataChunk.offset + (i * fmtChunk.numChannels + ch) * 2;606                sample += view.getInt16(sampleOffset, true);607            }608            audioData[i] = (sample / fmtChunk.numChannels) / 32768.0;609        }610    } else if (fmtChunk.bitsPerSample === 24) {611        // Support 24-bit PCM612        for (let i = 0; i < numSamples; i++) {613            let sample = 0;614            for (let ch = 0; ch < fmtChunk.numChannels; ch++) {615                const sampleOffset = dataChunk.offset + (i * fmtChunk.numChannels + ch) * 3;616                // Read 3 bytes and convert to signed 24-bit integer617                const byte1 = view.getUint8(sampleOffset);618                const byte2 = view.getUint8(sampleOffset + 1);619                const byte3 = view.getUint8(sampleOffset + 2);620                let value = (byte3 << 16) | (byte2 << 8) | byte1;621                // Convert to signed (two's complement)622                if (value & 0x800000) {623                    value = value - 0x1000000;624                }625                sample += value;626            }627            audioData[i] = (sample / fmtChunk.numChannels) / 8388608.0; // 2^23628        }629    } else if (fmtChunk.bitsPerSample === 32) {630        for (let i = 0; i < numSamples; i++) {631            let sample = 0;632            for (let ch = 0; ch < fmtChunk.numChannels; ch++) {633                const sampleOffset = dataChunk.offset + (i * fmtChunk.numChannels + ch) * 4;634                sample += view.getFloat32(sampleOffset, true);635            }636            audioData[i] = sample / fmtChunk.numChannels;637        }638    } else {639        throw new Error(`Unsupported bit depth: ${fmtChunk.bitsPerSample}. Supported formats: 16-bit, 24-bit, 32-bit`);640    }641    642    return {643        sampleRate: fmtChunk.sampleRate,644        audioData: audioData645    };646}647 648export function arrayToTensor(array, dims) {649    const flat = array.flat(Infinity);650    return new ort.Tensor('float32', Float32Array.from(flat), dims);651}652 653export function intArrayToTensor(array, dims) {654    const flat = array.flat(Infinity);655    return new ort.Tensor('int64', BigInt64Array.from(flat.map(x => BigInt(x))), dims);656}657 658export function writeWavFile(audioData, sampleRate) {659    const numChannels = 1;660    const bitsPerSample = 16;661    const byteRate = sampleRate * numChannels * bitsPerSample / 8;662    const blockAlign = numChannels * bitsPerSample / 8;663    const dataSize = audioData.length * bitsPerSample / 8;664 665    const buffer = new ArrayBuffer(44 + dataSize);666    const view = new DataView(buffer);667    668    // RIFF header669    view.setUint8(0, 'R'.charCodeAt(0));670    view.setUint8(1, 'I'.charCodeAt(0));671    view.setUint8(2, 'F'.charCodeAt(0));672    view.setUint8(3, 'F'.charCodeAt(0));673    view.setUint32(4, 36 + dataSize, true);674    view.setUint8(8, 'W'.charCodeAt(0));675    view.setUint8(9, 'A'.charCodeAt(0));676    view.setUint8(10, 'V'.charCodeAt(0));677    view.setUint8(11, 'E'.charCodeAt(0));678    679    // fmt chunk680    view.setUint8(12, 'f'.charCodeAt(0));681    view.setUint8(13, 'm'.charCodeAt(0));682    view.setUint8(14, 't'.charCodeAt(0));683    view.setUint8(15, ' '.charCodeAt(0));684    view.setUint32(16, 16, true);685    view.setUint16(20, 1, true); // PCM686    view.setUint16(22, numChannels, true);687    view.setUint32(24, sampleRate, true);688    view.setUint32(28, byteRate, true);689    view.setUint16(32, blockAlign, true);690    view.setUint16(34, bitsPerSample, true);691    692    // data chunk693    view.setUint8(36, 'd'.charCodeAt(0));694    view.setUint8(37, 'a'.charCodeAt(0));695    view.setUint8(38, 't'.charCodeAt(0));696    view.setUint8(39, 'a'.charCodeAt(0));697    view.setUint32(40, dataSize, true);698    699    // Write audio data700    for (let i = 0; i < audioData.length; i++) {701        const sample = Math.max(-1, Math.min(1, audioData[i]));702        const intSample = Math.floor(sample * 32767);703        view.setInt16(44 + i * 2, intSample, true);704    }705    706    return buffer;707}708 709 710 711// Smooth scroll functionality712document.addEventListener('DOMContentLoaded', () => {713    // Smooth scroll for anchor links714    document.querySelectorAll('a[href^="#"]').forEach(anchor => {715        anchor.addEventListener('click', function (e) {716            e.preventDefault();717            const href = this.getAttribute('href');718            const target = document.querySelector(href);719            if (target) {720                // Update URL with anchor721                if (history.pushState) {722                    history.pushState(null, null, href);723                }724                target.scrollIntoView({725                    behavior: 'smooth',726                    block: 'start'727                });728            }729        });730    });731    732    // Add scroll animation for sections733    const observerOptions = {734        threshold: 0.1,735        rootMargin: '0px 0px -100px 0px'736    };737    738    const observer = new IntersectionObserver((entries) => {739        entries.forEach(entry => {740            if (entry.isIntersecting) {741                entry.target.style.opacity = '1';742                entry.target.style.transform = 'translateY(0)';743            }744        });745    }, observerOptions);746    747});748 749// TTS Demo functionality750(async function() {751    // Check if we're on a page with the TTS demo752    const demoTextInput = document.getElementById('demoTextInput');753    if (!demoTextInput) return;754    755    // Configure ONNX Runtime WASM assets756    ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.0/dist/';757    ort.env.wasm.numThreads = 1;758    759 760    // Configuration761    const REF_EMBEDDING_PATHS = {762        'F1': 'assets/voice_styles/F1.json',763        'F2': 'assets/voice_styles/F2.json',764        'F3': 'assets/voice_styles/F3.json',765        'F4': 'assets/voice_styles/F4.json',766        'F5': 'assets/voice_styles/F5.json',767        'M1': 'assets/voice_styles/M1.json',768        'M2': 'assets/voice_styles/M2.json',769        'M3': 'assets/voice_styles/M3.json',770        'M4': 'assets/voice_styles/M4.json',771        'M5': 'assets/voice_styles/M5.json'772    };773 774    // Voice descriptions775    const VOICE_DESCRIPTIONS = {776        'F1': 'Sarah - A calm female voice with a slightly low tone; steady and composed.',777        'F2': 'Lily - A bright, cheerful female voice; lively, playful, and youthful with spirited energy.',778        'F3': 'Jessica - A clear, professional announcer-style female voice; articulate and broadcast-ready.',779        'F4': 'Olivia - A crisp, confident female voice; distinct and expressive with strong delivery.',780        'F5': 'Emily - A kind, gentle female voice; soft-spoken, calm, and naturally soothing.',781        'M1': 'Alex - A lively, upbeat male voice with confident energy and a standard, clear tone.',782        'M2': 'James - A deep, robust male voice; calm, composed, and serious with a grounded presence.',783        'M3': 'Robert - A polished, authoritative male voice; confident and trustworthy with strong presentation quality.',784        'M4': 'Sam - A soft, neutral-toned male voice; gentle and approachable with a youthful, friendly quality.',785        'M5': 'Daniel - A warm, soft-spoken male voice; calm and soothing with a natural storytelling quality.'786    };787 788    // Global state789    let models = null;790    let cfgs = null;791    let processors = null;792    let currentVoice = 'M3'; // Default to Robert voice793    794    // Detect browser language and set initial language795    function detectBrowserLanguage() {796        // Get browser language (works in Chrome, Safari, Edge, Firefox, Opera, Samsung Internet)797        const browserLang = navigator.language || navigator.userLanguage || 'en';798 799        // Extract language code (e.g., 'en-US' -> 'en', 'ko-KR' -> 'ko')800        const langCode = browserLang.split('-')[0].toLowerCase();801 802        // Reuse the 31-language list defined at module top-level803        return AVAILABLE_LANGS.includes(langCode) ? langCode : 'en';804    }805    806    let currentLanguage = detectBrowserLanguage(); // Auto-detect from browser807    let refEmbeddingCache = {}; // Cache for embeddings808    let currentStyleTtlTensor = null;809    let currentStyleDpTensor = null;810    let modelsLoading = false; // Track if models are currently loading811    let modelsLoaded = false; // Track if models are fully loaded812    let modelsLoadPromise = null; // Promise for model loading813 814    // UI Elements815    const demoStatusBox = document.getElementById('demoStatusBox');816    const demoStatusText = document.getElementById('demoStatusText');817    const demoGenerateBtn = document.getElementById('demoGenerateBtn');818    const demoTotalSteps = document.getElementById('demoTotalSteps');819    const demoSpeed = document.getElementById('demoSpeed');820    const demoTotalStepsValue = document.getElementById('demoTotalStepsValue');821    const demoSpeedValue = document.getElementById('demoSpeedValue');822    const demoResults = document.getElementById('demoResults');823    const demoError = document.getElementById('demoError');824    const demoCharCount = document.getElementById('demoCharCount');825    const demoCharCounter = document.getElementById('demoCharCounter');826    const demoCharWarning = document.getElementById('demoCharWarning');827    const fixedFontPresets = new Set(['paragraph', 'script']);828    let currentPreset = 'quote'; // Initialize with quote829 830    // Text validation constants831    const MIN_CHARS = 10;832    const MAX_CHUNK_LENGTH_DEFAULT = 300; // Maximum length for each chunk (default)833    const MAX_CHUNK_LENGTH_CJK = 120; // Maximum length for Korean/Japanese834    function getMaxChunkLength() {835        return (currentLanguage === 'ko' || currentLanguage === 'ja') ? MAX_CHUNK_LENGTH_CJK : MAX_CHUNK_LENGTH_DEFAULT;836    }837    838    // Custom audio player state (shared across generations)839    let audioContext = null;840    let scheduledSources = [];841    let audioChunks = [];842    let totalDuration = 0;843    let startTime = 0;844    let pauseTime = 0;845    let isPaused = false;846    let isPlaying = false;847    let animationFrameId = null;848    let playPauseBtn = null;849    let progressBar = null;850    let currentTimeDisplay = null;851    let durationDisplay = null;852    let progressFill = null;853    let firstChunkGenerationTime = 0; // Processing time for first chunk854    let totalChunks = 0;855    let nextScheduledTime = 0; // Next time to schedule audio chunk856    let currentGenerationTextLength = 0;857    let supertonicPlayerRecord = null; // Supertonic player record for cross-player pause management858    let isGenerating = false; // Track if speech generation is in progress859    860    // Track all custom audio players861    let customAudioPlayers = [];862 863    const isMobileViewport = () => window.matchMedia('(max-width: 768px)').matches;864    // Check if device actually supports touch (not just viewport size)865    const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0;866    const trimDecimalsForMobile = (formatted) => {867        if (!formatted) return formatted;868        return isMobileViewport() ? formatted.replace(/\.\d{2}$/, '') : formatted;869    };870 871    function pauseAllPlayersExcept(currentPlayer) {872        customAudioPlayers.forEach(player => {873            if (player !== currentPlayer && player && typeof player.pausePlayback === 'function') {874                player.pausePlayback();875            }876        });877    }878 879 880    /**881     * Chunk text into smaller pieces based on sentence boundaries882     * @param {string} text - The text to chunk883     * @param {number} maxLen - Maximum length for each chunk884     * @returns {Array<string>} - Array of text chunks885     */886    function chunkText(text, maxLen = getMaxChunkLength()) {887        // Split by paragraph (two or more newlines)888        const paragraphs = text.trim().split(/\n\s*\n+/).filter(p => p.trim());889        890        const chunks = [];891        892        for (let paragraph of paragraphs) {893            paragraph = paragraph.trim();894            if (!paragraph) continue;895            896            // Split by sentence boundaries (period, question mark, exclamation mark followed by space)897            // But exclude common abbreviations like Mr., Mrs., Dr., etc. and single capital letters like F.898            const sentences = paragraph.split(/(?<!Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.|Sr\.|Jr\.|Ph\.D\.|etc\.|e\.g\.|i\.e\.|vs\.|Inc\.|Ltd\.|Co\.|Corp\.|St\.|Ave\.|Blvd\.)(?<!\b[A-Z]\.)(?<=[.!?])\s+/);899            900            let currentChunk = "";901            902            for (let sentence of sentences) {903                if (currentChunk.length + sentence.length + 1 <= maxLen) {904                    currentChunk += (currentChunk ? " " : "") + sentence;905                } else {906                    if (currentChunk) {907                        chunks.push(currentChunk.trim());908                    }909                    currentChunk = sentence;910                }911            }912            913            if (currentChunk) {914                chunks.push(currentChunk.trim());915            }916        }917        918        return chunks;919    }920 921    function showDemoStatus(message, type = 'info', progress = null) {922        demoStatusText.innerHTML = message;923        demoStatusBox.className = 'demo-status-box';924        demoStatusBox.style.removeProperty('--status-progress');925        demoStatusBox.style.display = ''; // Show the status box926        927        if (type === 'success') {928            demoStatusBox.classList.add('success');929        } else if (type === 'error') {930            demoStatusBox.classList.add('error');931        }932        933        // Update progress bar934        if (progress !== null && progress >= 0 && progress <= 100) {935            const clampedProgress = Math.max(0, Math.min(progress, 100));936            demoStatusBox.style.setProperty('--status-progress', `${clampedProgress}%`);937            demoStatusBox.classList.toggle('complete', clampedProgress >= 100);938        } else if (type === 'success' || type === 'error') {939            demoStatusBox.style.removeProperty('--status-progress');940            demoStatusBox.classList.remove('complete');941        } else {942            demoStatusBox.style.removeProperty('--status-progress');943            demoStatusBox.classList.remove('complete');944        }945    }946 947    function hideDemoStatus() {948        demoStatusBox.style.display = 'none';949    }950 951    function showDemoError(message) {952        demoError.textContent = message;953        demoError.classList.add('active');954    }955 956    function hideDemoError() {957        demoError.classList.remove('active');958    }959    960    // Language toast notification961    const languageToast = document.getElementById('languageToast');962    const languageToastMessage = document.getElementById('languageToastMessage');963    let languageToastTimeout = null;964    965    function showLanguageToast(fromLang, toLang) {966        if (!languageToast || !languageToastMessage) return;967        968        const fromName = LANGUAGE_NAMES[fromLang] || fromLang;969        const toName = LANGUAGE_NAMES[toLang] || toLang;970        971        languageToastMessage.innerHTML = `Language auto-detected: <strong>${toName}</strong>`;972        973        // Clear any existing timeout974        if (languageToastTimeout) {975            clearTimeout(languageToastTimeout);976        }977        978        // Show toast979        languageToast.classList.add('show');980        981        // Hide after 3 seconds982        languageToastTimeout = setTimeout(() => {983            languageToast.classList.remove('show');984        }, 3000);985    }986 987    // Validate characters in text988    function validateCharacters(text) {989        if (!processors || !processors.textProcessor) {990            return { valid: true, unsupportedChars: [] };991        }992        993        try {994            // Extract unique characters to minimize preprocessText calls995            const uniqueChars = [...new Set(text)];996            997            // Build mapping for unique chars only (much faster for long texts)998            // For example, Korean '간' -> 'ㄱㅏㄴ', so we map 'ㄱ','ㅏ','ㄴ' -> '간'999            const processedToOriginal = new Map();1000            const charToProcessed = new Map();1001            1002            for (const char of uniqueChars) {1003                const processedChar = preprocessText(char);1004                charToProcessed.set(char, processedChar);1005                1006                // Map each processed character back to its original1007                for (const pc of processedChar) {1008                    if (!processedToOriginal.has(pc)) {1009                        processedToOriginal.set(pc, new Set());1010                    }1011                    processedToOriginal.get(pc).add(char);1012                }1013            }1014            1015            // Build full processed text using cached mappings1016            const fullProcessedText = Array.from(text).map(c => charToProcessed.get(c)).join('');1017            1018            // Check the entire processed text once (efficient)1019            const { unsupportedChars } = processors.textProcessor.call([fullProcessedText]);1020            1021            // Map unsupported processed chars back to original chars1022            const unsupportedOriginalChars = new Set();1023            if (unsupportedChars && unsupportedChars.length > 0) {1024                for (const unsupportedChar of unsupportedChars) {1025                    const originalChars = processedToOriginal.get(unsupportedChar);1026                    if (originalChars) {1027                        originalChars.forEach(c => unsupportedOriginalChars.add(c));1028                    }1029                }1030            }1031            1032            const unsupportedCharsArray = Array.from(unsupportedOriginalChars);1033            return { 1034                valid: unsupportedCharsArray.length === 0, 1035                unsupportedChars: unsupportedCharsArray1036            };1037        } catch (error) {1038            return { valid: true, unsupportedChars: [] };1039        }1040    }1041 1042    // Update character counter and validate text length1043    function updateCharCounter() {1044        const rawText = demoTextInput.textContent || demoTextInput.innerText || '';1045        const text = rawText.replace(/\n$/g, ''); // Remove trailing newline that browsers may add1046        const length = text.length;1047        1048        demoCharCount.textContent = length;1049        1050        if (fixedFontPresets.has(currentPreset)) {1051            demoTextInput.style.fontSize = '1.5rem';1052        } else {1053            // Get the actual width of the textarea1054            const textareaWidth = demoTextInput.offsetWidth;1055            // Check if mobile (572px or less) for 2x font size scaling1056            const isMobile = window.innerWidth <= 572;1057            const mobileMultiplier = isMobile ? 2 : 1;1058 1059            let fontSizeRatio;1060            if (length <= 100) {1061                fontSizeRatio = 0.055 * mobileMultiplier; // 5.5% of width1062            } else if (length <= 200) {1063                fontSizeRatio = 0.04 * mobileMultiplier; // 4% of width1064            } else if (length < 240) {1065                fontSizeRatio = 0.053125 * mobileMultiplier; // ~5.3125% of width1066            } else if (length < 400) {1067                fontSizeRatio = 0.0425 * mobileMultiplier; // ~4.25% of width1068            } else if (length < 700) {1069                fontSizeRatio = 0.031875 * mobileMultiplier; // ~3.1875% of width1070            } else {1071                fontSizeRatio = 0.025 * mobileMultiplier; // 2.5% of width1072            }1073 1074            // Calculate font size based on actual width1075            const fontSize = textareaWidth * fontSizeRatio;1076            demoTextInput.style.fontSize = `${fontSize}px`;1077        }1078        1079        // Remove all status classes1080        demoCharCounter.classList.remove('error', 'warning', 'valid');1081        1082        // Check for unsupported characters first (only if models are loaded)1083        let hasUnsupportedChars = false;1084        if (models && processors && length > 0) {1085            const validation = validateCharacters(text);1086            if (!validation.valid && validation.unsupportedChars.length > 0) {1087                hasUnsupportedChars = true;1088                const charList = validation.unsupportedChars.slice(0, 5).map(c => `"${c}"`).join(', ');1089                const moreChars = validation.unsupportedChars.length > 5 ? ` and ${validation.unsupportedChars.length - 5} more` : '';1090                showDemoError(`Unsupported characters detected: ${charList}${moreChars}. Please remove them before generating speech.`);1091            } else {1092                hideDemoError();1093            }1094        }1095        1096        // Update status based on length and character validation1097        if (length < MIN_CHARS) {1098            demoCharCounter.classList.add('error');1099            demoCharWarning.textContent = '(At least 10 characters)';1100            demoGenerateBtn.disabled = true;1101        } else if (hasUnsupportedChars) {1102            demoCharCounter.classList.add('error');1103            demoCharWarning.textContent = '(Unsupported characters)';1104            demoGenerateBtn.disabled = true;1105        } else {1106            demoCharCounter.classList.add('valid');1107            demoCharWarning.textContent = '';1108            // Enable only if models are loaded AND not currently generating1109            demoGenerateBtn.disabled = !models || isGenerating;1110        }1111    }1112 1113    // Validate text input1114    function validateTextInput(text) {1115        if (!text || text.trim().length === 0) {1116            return { valid: false, message: 'Please enter some text.' };1117        }1118        if (text.length < MIN_CHARS) {1119            return { valid: false, message: `Text must be at least ${MIN_CHARS} characters long. (Currently ${text.length})` };1120        }1121        return { valid: true };1122    }1123 1124    // Load pre-extracted style embeddings from JSON1125    async function loadStyleEmbeddings(voice) {1126        try {1127            // Check if already cached1128            if (refEmbeddingCache[voice]) {1129                return refEmbeddingCache[voice];1130            }1131            1132            const embeddingPath = REF_EMBEDDING_PATHS[voice];1133            if (!embeddingPath) {1134                throw new Error(`No embedding path configured for voice: ${voice}`);1135            }1136            1137            const response = await fetch(embeddingPath);1138            if (!response.ok) {1139                throw new Error(`Failed to fetch embedding: ${response.statusText}`);1140            }1141            1142            const embeddingData = await response.json();1143            1144            // Convert JSON data to ONNX tensors1145            // Flatten nested arrays before creating Float32Array1146            const styleTtlData = embeddingData.style_ttl.data.flat(Infinity);1147            const styleTtlTensor = new ort.Tensor(1148                embeddingData.style_ttl.type || 'float32',1149                Float32Array.from(styleTtlData),1150                embeddingData.style_ttl.dims1151            );1152            1153            const styleDpData = embeddingData.style_dp.data.flat(Infinity);1154            const styleDpTensor = new ort.Tensor(1155                embeddingData.style_dp.type || 'float32',1156                Float32Array.from(styleDpData),1157                embeddingData.style_dp.dims1158            );1159            1160            const embeddings = {1161                styleTtl: styleTtlTensor,1162                styleDp: styleDpTensor1163            };1164            1165            // Cache the embeddings1166            refEmbeddingCache[voice] = embeddings;1167            1168            return embeddings;1169        } catch (error) {1170            throw error;1171        }1172    }1173    1174    // Switch to a different voice1175    async function switchVoice(voice) {1176        try {1177            const embeddings = await loadStyleEmbeddings(voice);1178            1179            currentStyleTtlTensor = embeddings.styleTtl;1180            currentStyleDpTensor = embeddings.styleDp;1181            currentVoice = voice;1182            1183            // Update active speaker in UI1184            if (typeof window.updateActiveSpeaker === 'function') {1185                window.updateActiveSpeaker(voice);1186            }1187            1188            // Re-validate text after switching voice1189            updateCharCounter();1190        } catch (error) {1191            showDemoError(`Failed to load voice ${voice}: ${getErrorMessage(error)}`);1192            throw error;1193        }1194    }1195 1196    // Warmup models with dummy inference (no audio playback, no UI updates)1197    async function warmupModels() {1198        try {1199            const dummyText = 'Hello, this is a quick warmup.';1200            const totalStep = 1;

Showing the first 1,200 of 3100 lines. Download the file for the rest.