CoolFace
Apppublic

lovegold/supertonic-2

sourceHugging Faceopenrailupdated 8mo agoView on Hugging Face
0likes
script.js3050 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 formatStatValueWithSuffix(value, suffix, options = {}) {53    const { firstLabel = false } = options;54    if (value === undefined || value === null) {55        return '';56    }57    if (!suffix) {58        const raw = `${value}`;59        return escapeHtml(raw);60    }61    const raw = `${value}`.trim();62    if (!raw || raw === '--' || raw === '-' || raw.toLowerCase() === 'error') {63        return escapeHtml(raw);64    }65    const appendSuffix = (segment, includePrefix = false) => {66        const trimmed = segment.trim();67        if (!trimmed) {68            return '';69        }70        const escapedValue = `<span class="stat-value-number">${escapeHtml(trimmed)}</span>`;71        const suffixSpan = `<span class="stat-label stat-suffix">${escapeHtml(suffix)}</span>`;72        const prefixSpan = includePrefix && firstLabel73            ? `<span class="stat-label stat-suffix stat-prefix">First</span>`74            : '';75        const segmentClass = includePrefix && firstLabel76            ? 'stat-value-segment has-prefix'77            : 'stat-value-segment';78        return `<span class="${segmentClass}">${prefixSpan}${escapedValue}${suffixSpan}</span>`;79    };80    if (raw.includes('/')) {81        const parts = raw.split('/');82        const segments = parts.map((part, index) => appendSuffix(part, index === 0));83        return segments.join(' / ');84    }85    return appendSuffix(raw);86}87 88/**89 * Unicode text processor90 */91export class UnicodeProcessor {92    constructor(indexer) {93        this.indexer = indexer;94    }95 96    call(textList, lang = null) {97        const processedTexts = textList.map(t => preprocessText(t, lang));98        const textIdsLengths = processedTexts.map(t => t.length);99        const maxLen = Math.max(...textIdsLengths);100        101        const textIds = [];102        const unsupportedChars = new Set();103        104        for (let i = 0; i < processedTexts.length; i++) {105            const row = new Array(maxLen).fill(0);106            const unicodeVals = textToUnicodeValues(processedTexts[i]);107            for (let j = 0; j < unicodeVals.length; j++) {108                const indexValue = this.indexer[unicodeVals[j]];109                // Check if character is supported (not -1, undefined, or null)110                if (indexValue === undefined || indexValue === null || indexValue === -1) {111                    unsupportedChars.add(processedTexts[i][j]);112                    row[j] = 0; // Use 0 as fallback113                } else {114                    row[j] = indexValue;115                }116            }117            textIds.push(row);118        }119        120        const textMask = getTextMask(textIdsLengths);121        return { textIds, textMask, unsupportedChars: Array.from(unsupportedChars) };122    }123}124 125const AVAILABLE_LANGS = ["en", "ko", "es", "pt", "fr"];126 127/**128 * Language detection based on character patterns and language-specific markers129 * Returns the detected language code or null if uncertain130 */131export function detectLanguage(text) {132    if (!text || text.trim().length < 3) {133        return null;134    }135    136    // Only consider last 100 characters for efficiency137    const sampleText = text.length > 100 ? text.substring(text.length - 100) : text;138    139    // Normalize text for analysis140    const normalizedText = sampleText.normalize('NFC').toLowerCase();141    142    // Korean detection: Hangul characters (most reliable)143    const koreanRegex = /[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uD7B0-\uD7FF]/g;144    const koreanMatches = normalizedText.match(koreanRegex) || [];145    if (koreanMatches.length >= 2) {146        return 'ko';147    }148    149    // Scoring system for Latin-based languages150    const scores = { en: 0, es: 0, fr: 0, pt: 0 };151    152    // 1. Highly distinctive characters (definitive markers)153    if (/ñ/.test(normalizedText)) scores.es += 15;154    if (/[¿¡]/.test(normalizedText)) scores.es += 12;155    if (/ã/.test(normalizedText)) scores.pt += 15;156    if (/õ/.test(normalizedText)) scores.pt += 15;157    if (/œ/.test(normalizedText)) scores.fr += 15;158    if (/[ùû]/.test(normalizedText)) scores.fr += 10;159    160    // ç is shared between French and Portuguese161    if (/ç/.test(normalizedText)) {162        scores.fr += 4;163        scores.pt += 4;164    }165    166    // French-specific accent patterns167    if (/[èêë]/.test(normalizedText)) scores.fr += 5;168    if (/[àâ]/.test(normalizedText)) scores.fr += 3;169    if (/[îï]/.test(normalizedText)) scores.fr += 4;170    if (/ô/.test(normalizedText)) scores.fr += 3;171    172    // 2. Exclusive stopwords (words unique to one language)173    const exclusiveWords = {174        en: ['the', 'is', 'are', 'was', 'were', 'have', 'has', 'been', 'will', 'would', 'could', 'should', 'this', 'that', 'with', 'from', 'they', 'what', 'which', 'there', 'their', 'about', 'these', 'other', 'into', 'just', 'your', 'some', 'than', 'them', 'then', 'only', 'being', 'through', 'after', 'before'],175        es: ['el', 'los', 'las', 'es', 'está', 'están', 'porque', 'pero', 'muy', 'también', 'más', 'este', 'esta', 'estos', 'estas', 'ese', 'esa', 'yo', 'tú', 'nosotros', 'ellos', 'ellas', 'hola', 'gracias', 'buenos', 'buenas', 'ahora', 'siempre', 'nunca', 'todo', 'nada', 'algo', 'alguien'],176        fr: ['le', 'les', 'est', 'sont', 'dans', 'ce', 'cette', 'ces', 'il', 'elle', 'ils', 'elles', 'je', 'tu', 'nous', 'vous', 'avec', 'sur', 'ne', 'pas', 'plus', 'tout', 'bien', 'fait', 'être', 'avoir', 'donc', 'car', 'ni', 'jamais', 'toujours', 'rien', 'quelque', 'encore', 'aussi', 'très', 'peu', 'ici'],177        pt: ['os', 'as', 'é', 'são', 'está', 'estão', 'não', 'na', 'no', 'da', 'do', 'das', 'dos', 'ao', 'aos', 'ele', 'ela', 'eles', 'elas', 'eu', 'nós', 'você', 'vocês', 'seu', 'sua', 'seus', 'suas', 'muito', 'também', 'já', 'foi', 'só', 'mesmo', 'ter', 'até', 'isso', 'olá', 'obrigado', 'obrigada', 'bom', 'boa', 'agora', 'sempre', 'nunca', 'tudo', 'nada', 'algo', 'alguém']178    };179    180    // Extract words from text181    const words = normalizedText.match(/[a-záàâãäåçéèêëíìîïñóòôõöúùûüýÿœæ]+/g) || [];182    183    for (const word of words) {184        for (const [lang, wordList] of Object.entries(exclusiveWords)) {185            if (wordList.includes(word)) {186                scores[lang] += 3;187            }188        }189    }190    191    // 3. Common n-grams (character patterns)192    const ngramPatterns = {193        en: [/th/g, /ing/g, /tion/g, /ight/g, /ould/g],194        es: [/ción/g, /mente/g, /ado/g, /ido/g],195        fr: [/tion/g, /ment/g, /eau/g, /aux/g, /eux/g, /oir/g, /ais/g, /ait/g, /ont/g],196        pt: [/ção/g, /ões/g, /mente/g, /ado/g, /ido/g, /nh/g, /lh/g]197    };198    199    for (const [lang, patterns] of Object.entries(ngramPatterns)) {200        for (const pattern of patterns) {201            const matches = normalizedText.match(pattern) || [];202            scores[lang] += matches.length * 2;203        }204    }205    206    // 4. French contractions and apostrophes207    const frenchContractions = /[cdjlmnst]'[aeiouéèêàâîïôûù]/g;208    const frenchContractionMatches = normalizedText.match(frenchContractions) || [];209    scores.fr += frenchContractionMatches.length * 5;210    211    // 5. Article patterns that help distinguish212    // "the" is very English, "el/la" Spanish, "le/la" French, "o/a" Portuguese213    if (/\bthe\b/.test(normalizedText)) scores.en += 5;214    if (/\b(el|los)\b/.test(normalizedText)) scores.es += 4;215    if (/\b(le|les)\b/.test(normalizedText)) scores.fr += 4;216    if (/\b(o|os)\b/.test(normalizedText)) scores.pt += 3;217    218    // Find the language with the highest score219    let maxScore = 0;220    let detectedLang = null;221    222    for (const [lang, score] of Object.entries(scores)) {223        if (score > maxScore) {224            maxScore = score;225            detectedLang = lang;226        }227    }228    229    // Only return if we have enough confidence (minimum threshold)230    if (maxScore >= 4) {231        return detectedLang;232    }233    234    return null;235}236 237// Language display names for toast notification238const LANGUAGE_NAMES = {239    'en': 'English',240    'ko': 'Korean',241    'es': 'Spanish',242    'pt': 'Portuguese',243    'fr': 'French'244};245 246export function preprocessText(text, lang = null) {247    // Normalize unicode characters248    text = text.normalize('NFKD');249    250    // Remove emojis251    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, '');252    253    // Replace various dashes and symbols254    const replacements = {255        "–": "-",256        "‑": "-",257        "—": "-",258        "_": " ",259        "\u201C": '"',  // "260        "\u201D": '"',  // "261        "\u2018": "'",  // '262        "\u2019": "'",  // '263        "´": "'",264        "`": "'",265        "[": " ",266        "]": " ",267        "|": " ",268        "/": " ",  // FIXME: `/` should be pronounced.269        "#": " ",  // FIXME: `#` should be pronounced.270        "→": " ",271        "←": " ",272    };273    274    for (const [k, v] of Object.entries(replacements)) {275        text = text.replaceAll(k, v);276    }277 278    // Remove special symbols279    text = text.replace(/[♥☆♡©\\]/g, "");280 281    // Replace known expressions282    const exprReplacements = {283        "@": " at ",284        "e.g.,": "for example,",285        "i.e.,": "that is,",286    };287    288    for (const [k, v] of Object.entries(exprReplacements)) {289        text = text.replaceAll(k, v);290    }291    292    // Fix spacing around punctuation293    text = text.replace(/ ,/g, ",");294    text = text.replace(/ \./g, ".");295    text = text.replace(/ !/g, "!");296    text = text.replace(/ \?/g, "?");297    text = text.replace(/ ;/g, ";");298    text = text.replace(/ :/g, ":");299    text = text.replace(/ '/g, "'");300    301    // Remove duplicate quotes302    while (text.includes('""')) {303        text = text.replace(/""/g, '"');304    }305    while (text.includes("''")) {306        text = text.replace(/''/g, "'");307    }308    while (text.includes("``")) {309        text = text.replace(/``/g, "`");310    }311    312    // Remove extra spaces313    text = text.replace(/\s+/g, " ").trim();314 315    // If text doesn't end with punctuation, quotes, or closing brackets, add a period316    if (!/[.!?;:,'"')\]}…。」』】〉》›»]$/.test(text)) {317        text += ".";318    }319    320    // Add language tags321    if (lang !== null) {322        if (!AVAILABLE_LANGS.includes(lang)) {323            throw new Error(`Invalid language: ${lang}`);324        }325        text = `<${lang}>` + text + `</${lang}>`;326    } else {327        text = `<na>` + text + `</na>`;328    }329    330    return text;331}332 333export function textToUnicodeValues(text) {334    return Array.from(text).map(char => char.charCodeAt(0));335}336 337export function lengthToMask(lengths, maxLen = null) {338    maxLen = maxLen || Math.max(...lengths);339    const mask = [];340    for (let i = 0; i < lengths.length; i++) {341        const row = [];342        for (let j = 0; j < maxLen; j++) {343            row.push(j < lengths[i] ? 1.0 : 0.0);344        }345        mask.push([row]);346    }347    return mask;348}349 350export function getTextMask(textIdsLengths) {351    return lengthToMask(textIdsLengths);352}353 354export function getLatentMask(wavLengths, cfgs) {355    const baseChunkSize = cfgs.ae.base_chunk_size;356    const chunkCompressFactor = cfgs.ttl.chunk_compress_factor;357    const latentSize = baseChunkSize * chunkCompressFactor;358    const latentLengths = wavLengths.map(len => 359        Math.floor((len + latentSize - 1) / latentSize)360    );361    return lengthToMask(latentLengths);362}363 364export function sampleNoisyLatent(duration, cfgs) {365    const sampleRate = cfgs.ae.sample_rate;366    const baseChunkSize = cfgs.ae.base_chunk_size;367    const chunkCompressFactor = cfgs.ttl.chunk_compress_factor;368    const ldim = cfgs.ttl.latent_dim;369 370    const wavLenMax = Math.max(...duration.map(d => d[0][0])) * sampleRate;371    const wavLengths = duration.map(d => Math.floor(d[0][0] * sampleRate));372    const chunkSize = baseChunkSize * chunkCompressFactor;373    const latentLen = Math.floor((wavLenMax + chunkSize - 1) / chunkSize);374    const latentDim = ldim * chunkCompressFactor;375 376    const noisyLatent = [];377    for (let b = 0; b < duration.length; b++) {378        const batch = [];379        for (let d = 0; d < latentDim; d++) {380            const row = [];381            for (let t = 0; t < latentLen; t++) {382                const u1 = Math.random();383                const u2 = Math.random();384                const randNormal = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);385                row.push(randNormal);386            }387            batch.push(row);388        }389        noisyLatent.push(batch);390    }391 392    const latentMask = getLatentMask(wavLengths, cfgs);393    394    for (let b = 0; b < noisyLatent.length; b++) {395        for (let d = 0; d < noisyLatent[b].length; d++) {396            for (let t = 0; t < noisyLatent[b][d].length; t++) {397                noisyLatent[b][d][t] *= latentMask[b][0][t];398            }399        }400    }401 402    return { noisyLatent, latentMask };403}404 405export async function loadOnnx(onnxPath, opts) {406    return await ort.InferenceSession.create(onnxPath, opts);407}408 409export async function loadOnnxAll(basePath, opts, onProgress) {410    const models = [411        { name: 'Duration Predictor', path: `${basePath}/duration_predictor.onnx`, key: 'dpOrt' },412        { name: 'Text Encoder', path: `${basePath}/text_encoder.onnx`, key: 'textEncOrt' },413        { name: 'Vector Estimator', path: `${basePath}/vector_estimator.onnx`, key: 'vectorEstOrt' },414        { name: 'Vocoder', path: `${basePath}/vocoder.onnx`, key: 'vocoderOrt' }415    ];416 417    const result = {};418    let loadedCount = 0;419    420    // Load all models in parallel421    const loadPromises = models.map(async (model) => {422        const session = await loadOnnx(model.path, opts);423        loadedCount++;424        if (onProgress) {425            onProgress(model.name, loadedCount, models.length);426        }427        return { key: model.key, session };428    });429    430    // Wait for all models to load431    const loadedModels = await Promise.all(loadPromises);432    433    // Organize results434    loadedModels.forEach(({ key, session }) => {435        result[key] = session;436    });437 438    try {439        // Download counting440        await fetch('https://huggingface.co/Supertone/supertonic-2/resolve/main/config.json');441    } catch (error) {442        console.warn('Failed to update download count:', error);443    }444    return result;445}446 447export async function loadCfgs(basePath) {448    const response = await fetch(`${basePath}/tts.json`);449    return await response.json();450}451 452export async function loadProcessors(basePath) {453    const response = await fetch(`${basePath}/unicode_indexer.json`);454    const unicodeIndexerData = await response.json();455    const textProcessor = new UnicodeProcessor(unicodeIndexerData);456    457    return { textProcessor };458}459 460function parseWavFile(buffer) {461    const view = new DataView(buffer);462    463    // Check RIFF header464    const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));465    if (riff !== 'RIFF') {466        throw new Error('Not a valid WAV file');467    }468    469    const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11));470    if (wave !== 'WAVE') {471        throw new Error('Not a valid WAV file');472    }473    474    let offset = 12;475    let fmtChunk = null;476    let dataChunk = null;477    478    while (offset < buffer.byteLength) {479        const chunkId = String.fromCharCode(480            view.getUint8(offset), 481            view.getUint8(offset + 1), 482            view.getUint8(offset + 2), 483            view.getUint8(offset + 3)484        );485        const chunkSize = view.getUint32(offset + 4, true);486        487        if (chunkId === 'fmt ') {488            fmtChunk = {489                audioFormat: view.getUint16(offset + 8, true),490                numChannels: view.getUint16(offset + 10, true),491                sampleRate: view.getUint32(offset + 12, true),492                bitsPerSample: view.getUint16(offset + 22, true)493            };494        } else if (chunkId === 'data') {495            dataChunk = {496                offset: offset + 8,497                size: chunkSize498            };499            break;500        }501        502        offset += 8 + chunkSize;503    }504    505    if (!fmtChunk || !dataChunk) {506        throw new Error('Invalid WAV file format');507    }508    509    const bytesPerSample = fmtChunk.bitsPerSample / 8;510    const numSamples = Math.floor(dataChunk.size / (bytesPerSample * fmtChunk.numChannels));511    const audioData = new Float32Array(numSamples);512    513    if (fmtChunk.bitsPerSample === 16) {514        for (let i = 0; i < numSamples; i++) {515            let sample = 0;516            for (let ch = 0; ch < fmtChunk.numChannels; ch++) {517                const sampleOffset = dataChunk.offset + (i * fmtChunk.numChannels + ch) * 2;518                sample += view.getInt16(sampleOffset, true);519            }520            audioData[i] = (sample / fmtChunk.numChannels) / 32768.0;521        }522    } else if (fmtChunk.bitsPerSample === 24) {523        // Support 24-bit PCM524        for (let i = 0; i < numSamples; i++) {525            let sample = 0;526            for (let ch = 0; ch < fmtChunk.numChannels; ch++) {527                const sampleOffset = dataChunk.offset + (i * fmtChunk.numChannels + ch) * 3;528                // Read 3 bytes and convert to signed 24-bit integer529                const byte1 = view.getUint8(sampleOffset);530                const byte2 = view.getUint8(sampleOffset + 1);531                const byte3 = view.getUint8(sampleOffset + 2);532                let value = (byte3 << 16) | (byte2 << 8) | byte1;533                // Convert to signed (two's complement)534                if (value & 0x800000) {535                    value = value - 0x1000000;536                }537                sample += value;538            }539            audioData[i] = (sample / fmtChunk.numChannels) / 8388608.0; // 2^23540        }541    } else if (fmtChunk.bitsPerSample === 32) {542        for (let i = 0; i < numSamples; i++) {543            let sample = 0;544            for (let ch = 0; ch < fmtChunk.numChannels; ch++) {545                const sampleOffset = dataChunk.offset + (i * fmtChunk.numChannels + ch) * 4;546                sample += view.getFloat32(sampleOffset, true);547            }548            audioData[i] = sample / fmtChunk.numChannels;549        }550    } else {551        throw new Error(`Unsupported bit depth: ${fmtChunk.bitsPerSample}. Supported formats: 16-bit, 24-bit, 32-bit`);552    }553    554    return {555        sampleRate: fmtChunk.sampleRate,556        audioData: audioData557    };558}559 560export function arrayToTensor(array, dims) {561    const flat = array.flat(Infinity);562    return new ort.Tensor('float32', Float32Array.from(flat), dims);563}564 565export function intArrayToTensor(array, dims) {566    const flat = array.flat(Infinity);567    return new ort.Tensor('int64', BigInt64Array.from(flat.map(x => BigInt(x))), dims);568}569 570export function writeWavFile(audioData, sampleRate) {571    const numChannels = 1;572    const bitsPerSample = 16;573    const byteRate = sampleRate * numChannels * bitsPerSample / 8;574    const blockAlign = numChannels * bitsPerSample / 8;575    const dataSize = audioData.length * bitsPerSample / 8;576 577    const buffer = new ArrayBuffer(44 + dataSize);578    const view = new DataView(buffer);579    580    // RIFF header581    view.setUint8(0, 'R'.charCodeAt(0));582    view.setUint8(1, 'I'.charCodeAt(0));583    view.setUint8(2, 'F'.charCodeAt(0));584    view.setUint8(3, 'F'.charCodeAt(0));585    view.setUint32(4, 36 + dataSize, true);586    view.setUint8(8, 'W'.charCodeAt(0));587    view.setUint8(9, 'A'.charCodeAt(0));588    view.setUint8(10, 'V'.charCodeAt(0));589    view.setUint8(11, 'E'.charCodeAt(0));590    591    // fmt chunk592    view.setUint8(12, 'f'.charCodeAt(0));593    view.setUint8(13, 'm'.charCodeAt(0));594    view.setUint8(14, 't'.charCodeAt(0));595    view.setUint8(15, ' '.charCodeAt(0));596    view.setUint32(16, 16, true);597    view.setUint16(20, 1, true); // PCM598    view.setUint16(22, numChannels, true);599    view.setUint32(24, sampleRate, true);600    view.setUint32(28, byteRate, true);601    view.setUint16(32, blockAlign, true);602    view.setUint16(34, bitsPerSample, true);603    604    // data chunk605    view.setUint8(36, 'd'.charCodeAt(0));606    view.setUint8(37, 'a'.charCodeAt(0));607    view.setUint8(38, 't'.charCodeAt(0));608    view.setUint8(39, 'a'.charCodeAt(0));609    view.setUint32(40, dataSize, true);610    611    // Write audio data612    for (let i = 0; i < audioData.length; i++) {613        const sample = Math.max(-1, Math.min(1, audioData[i]));614        const intSample = Math.floor(sample * 32767);615        view.setInt16(44 + i * 2, intSample, true);616    }617    618    return buffer;619}620 621 622 623// Smooth scroll functionality624document.addEventListener('DOMContentLoaded', () => {625    // Smooth scroll for anchor links626    document.querySelectorAll('a[href^="#"]').forEach(anchor => {627        anchor.addEventListener('click', function (e) {628            e.preventDefault();629            const href = this.getAttribute('href');630            const target = document.querySelector(href);631            if (target) {632                // Update URL with anchor633                if (history.pushState) {634                    history.pushState(null, null, href);635                }636                target.scrollIntoView({637                    behavior: 'smooth',638                    block: 'start'639                });640            }641        });642    });643    644    // Add scroll animation for sections645    const observerOptions = {646        threshold: 0.1,647        rootMargin: '0px 0px -100px 0px'648    };649    650    const observer = new IntersectionObserver((entries) => {651        entries.forEach(entry => {652            if (entry.isIntersecting) {653                entry.target.style.opacity = '1';654                entry.target.style.transform = 'translateY(0)';655            }656        });657    }, observerOptions);658    659});660 661// TTS Demo functionality662(async function() {663    // Check if we're on a page with the TTS demo664    const demoTextInput = document.getElementById('demoTextInput');665    if (!demoTextInput) return;666    667    // Configure ONNX Runtime for WebGPU support668    ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.0/dist/';669    ort.env.wasm.numThreads = 1;670    671 672    // Configuration673    const REF_EMBEDDING_PATHS = {674        'F1': 'assets/voice_styles/F1.json',675        'F2': 'assets/voice_styles/F2.json',676        'F3': 'assets/voice_styles/F3.json',677        'F4': 'assets/voice_styles/F4.json',678        'F5': 'assets/voice_styles/F5.json',679        'M1': 'assets/voice_styles/M1.json',680        'M2': 'assets/voice_styles/M2.json',681        'M3': 'assets/voice_styles/M3.json',682        'M4': 'assets/voice_styles/M4.json',683        'M5': 'assets/voice_styles/M5.json'684    };685 686    // Voice descriptions687    const VOICE_DESCRIPTIONS = {688        'F1': 'Sarah - A calm female voice with a slightly low tone; steady and composed.',689        'F2': 'Lily - A bright, cheerful female voice; lively, playful, and youthful with spirited energy.',690        'F3': 'Jessica - A clear, professional announcer-style female voice; articulate and broadcast-ready.',691        'F4': 'Olivia - A crisp, confident female voice; distinct and expressive with strong delivery.',692        'F5': 'Emily - A kind, gentle female voice; soft-spoken, calm, and naturally soothing.',693        'M1': 'Alex - A lively, upbeat male voice with confident energy and a standard, clear tone.',694        'M2': 'James - A deep, robust male voice; calm, composed, and serious with a grounded presence.',695        'M3': 'Robert - A polished, authoritative male voice; confident and trustworthy with strong presentation quality.',696        'M4': 'Sam - A soft, neutral-toned male voice; gentle and approachable with a youthful, friendly quality.',697        'M5': 'Daniel - A warm, soft-spoken male voice; calm and soothing with a natural storytelling quality.'698    };699 700    // Global state701    let models = null;702    let cfgs = null;703    let processors = null;704    let currentVoice = 'M3'; // Default to Robert voice705    706    // Detect browser language and set initial language707    function detectBrowserLanguage() {708        // Get browser language (works in Chrome, Safari, Edge, Firefox, Opera, Samsung Internet)709        const browserLang = navigator.language || navigator.userLanguage || 'en';710        711        // Extract language code (e.g., 'en-US' -> 'en', 'ko-KR' -> 'ko')712        const langCode = browserLang.split('-')[0].toLowerCase();713        714        // Supported languages715        const supportedLangs = ['en', 'es', 'pt', 'fr', 'ko'];716        717        // Return detected language if supported, otherwise default to English718        return supportedLangs.includes(langCode) ? langCode : 'en';719    }720    721    let currentLanguage = detectBrowserLanguage(); // Auto-detect from browser722    let refEmbeddingCache = {}; // Cache for embeddings723    let currentStyleTtlTensor = null;724    let currentStyleDpTensor = null;725    let modelsLoading = false; // Track if models are currently loading726    let modelsLoaded = false; // Track if models are fully loaded727    let modelsLoadPromise = null; // Promise for model loading728 729    // UI Elements730    const demoStatusBox = document.getElementById('demoStatusBox');731    const demoStatusText = document.getElementById('demoStatusText');732    const wasmWarningBanner = document.getElementById('wasmWarningBanner');733    const demoGenerateBtn = document.getElementById('demoGenerateBtn');734    const demoTotalSteps = document.getElementById('demoTotalSteps');735    const demoSpeed = document.getElementById('demoSpeed');736    const demoTotalStepsValue = document.getElementById('demoTotalStepsValue');737    const demoSpeedValue = document.getElementById('demoSpeedValue');738    const demoResults = document.getElementById('demoResults');739    const demoError = document.getElementById('demoError');740    const demoCharCount = document.getElementById('demoCharCount');741    const demoCharCounter = document.getElementById('demoCharCounter');742    const demoCharWarning = document.getElementById('demoCharWarning');743 744    // Text validation constants745    const MIN_CHARS = 10;746    const MAX_CHUNK_LENGTH_DEFAULT = 300; // Maximum length for each chunk (default)747    const MAX_CHUNK_LENGTH_KO = 120; // Maximum length for Korean748    function getMaxChunkLength() {749        return currentLanguage === 'ko' ? MAX_CHUNK_LENGTH_KO : MAX_CHUNK_LENGTH_DEFAULT;750    }751    752    // Custom audio player state (shared across generations)753    let audioContext = null;754    let scheduledSources = [];755    let audioChunks = [];756    let totalDuration = 0;757    let startTime = 0;758    let pauseTime = 0;759    let isPaused = false;760    let isPlaying = false;761    let animationFrameId = null;762    let playPauseBtn = null;763    let progressBar = null;764    let currentTimeDisplay = null;765    let durationDisplay = null;766    let progressFill = null;767    let firstChunkGenerationTime = 0; // Processing time for first chunk768    let totalChunks = 0;769    let nextScheduledTime = 0; // Next time to schedule audio chunk770    let currentGenerationTextLength = 0;771    let supertonicPlayerRecord = null; // Supertonic player record for cross-player pause management772    let isGenerating = false; // Track if speech generation is in progress773    774    // Track all custom audio players775    let customAudioPlayers = [];776 777    const isMobileViewport = () => window.matchMedia('(max-width: 768px)').matches;778    // Check if device actually supports touch (not just viewport size)779    const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0;780    const trimDecimalsForMobile = (formatted) => {781        if (!formatted) return formatted;782        return isMobileViewport() ? formatted.replace(/\.\d{2}$/, '') : formatted;783    };784 785    function pauseAllPlayersExcept(currentPlayer) {786        customAudioPlayers.forEach(player => {787            if (player !== currentPlayer && player && typeof player.pausePlayback === 'function') {788                player.pausePlayback();789            }790        });791    }792 793 794    /**795     * Chunk text into smaller pieces based on sentence boundaries796     * @param {string} text - The text to chunk797     * @param {number} maxLen - Maximum length for each chunk798     * @returns {Array<string>} - Array of text chunks799     */800    function chunkText(text, maxLen = getMaxChunkLength()) {801        // Split by paragraph (two or more newlines)802        const paragraphs = text.trim().split(/\n\s*\n+/).filter(p => p.trim());803        804        const chunks = [];805        806        for (let paragraph of paragraphs) {807            paragraph = paragraph.trim();808            if (!paragraph) continue;809            810            // Split by sentence boundaries (period, question mark, exclamation mark followed by space)811            // But exclude common abbreviations like Mr., Mrs., Dr., etc. and single capital letters like F.812            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+/);813            814            let currentChunk = "";815            816            for (let sentence of sentences) {817                if (currentChunk.length + sentence.length + 1 <= maxLen) {818                    currentChunk += (currentChunk ? " " : "") + sentence;819                } else {820                    if (currentChunk) {821                        chunks.push(currentChunk.trim());822                    }823                    currentChunk = sentence;824                }825            }826            827            if (currentChunk) {828                chunks.push(currentChunk.trim());829            }830        }831        832        return chunks;833    }834 835    function showDemoStatus(message, type = 'info', progress = null) {836        demoStatusText.innerHTML = message;837        demoStatusBox.className = 'demo-status-box';838        demoStatusBox.style.removeProperty('--status-progress');839        demoStatusBox.style.display = ''; // Show the status box840        841        if (type === 'success') {842            demoStatusBox.classList.add('success');843        } else if (type === 'error') {844            demoStatusBox.classList.add('error');845        }846        847        // Update progress bar848        if (progress !== null && progress >= 0 && progress <= 100) {849            const clampedProgress = Math.max(0, Math.min(progress, 100));850            demoStatusBox.style.setProperty('--status-progress', `${clampedProgress}%`);851            demoStatusBox.classList.toggle('complete', clampedProgress >= 100);852        } else if (type === 'success' || type === 'error') {853            demoStatusBox.style.removeProperty('--status-progress');854            demoStatusBox.classList.remove('complete');855        } else {856            demoStatusBox.style.removeProperty('--status-progress');857            demoStatusBox.classList.remove('complete');858        }859    }860 861    function hideDemoStatus() {862        demoStatusBox.style.display = 'none';863    }864 865    function showDemoError(message) {866        demoError.textContent = message;867        demoError.classList.add('active');868    }869 870    function hideDemoError() {871        demoError.classList.remove('active');872    }873    874    // Language toast notification875    const languageToast = document.getElementById('languageToast');876    const languageToastMessage = document.getElementById('languageToastMessage');877    let languageToastTimeout = null;878    879    function showLanguageToast(fromLang, toLang) {880        if (!languageToast || !languageToastMessage) return;881        882        const fromName = LANGUAGE_NAMES[fromLang] || fromLang;883        const toName = LANGUAGE_NAMES[toLang] || toLang;884        885        languageToastMessage.innerHTML = `Language auto-detected: <strong>${toName}</strong>`;886        887        // Clear any existing timeout888        if (languageToastTimeout) {889            clearTimeout(languageToastTimeout);890        }891        892        // Show toast893        languageToast.classList.add('show');894        895        // Hide after 3 seconds896        languageToastTimeout = setTimeout(() => {897            languageToast.classList.remove('show');898        }, 3000);899    }900 901    function showWasmWarning() {902        if (wasmWarningBanner) {903            wasmWarningBanner.style.display = 'flex';904        }905    }906 907    // Validate characters in text908    function validateCharacters(text) {909        if (!processors || !processors.textProcessor) {910            return { valid: true, unsupportedChars: [] };911        }912        913        try {914            // Extract unique characters to minimize preprocessText calls915            const uniqueChars = [...new Set(text)];916            917            // Build mapping for unique chars only (much faster for long texts)918            // For example, Korean '간' -> 'ㄱㅏㄴ', so we map 'ㄱ','ㅏ','ㄴ' -> '간'919            const processedToOriginal = new Map();920            const charToProcessed = new Map();921            922            for (const char of uniqueChars) {923                const processedChar = preprocessText(char);924                charToProcessed.set(char, processedChar);925                926                // Map each processed character back to its original927                for (const pc of processedChar) {928                    if (!processedToOriginal.has(pc)) {929                        processedToOriginal.set(pc, new Set());930                    }931                    processedToOriginal.get(pc).add(char);932                }933            }934            935            // Build full processed text using cached mappings936            const fullProcessedText = Array.from(text).map(c => charToProcessed.get(c)).join('');937            938            // Check the entire processed text once (efficient)939            const { unsupportedChars } = processors.textProcessor.call([fullProcessedText]);940            941            // Map unsupported processed chars back to original chars942            const unsupportedOriginalChars = new Set();943            if (unsupportedChars && unsupportedChars.length > 0) {944                for (const unsupportedChar of unsupportedChars) {945                    const originalChars = processedToOriginal.get(unsupportedChar);946                    if (originalChars) {947                        originalChars.forEach(c => unsupportedOriginalChars.add(c));948                    }949                }950            }951            952            const unsupportedCharsArray = Array.from(unsupportedOriginalChars);953            return { 954                valid: unsupportedCharsArray.length === 0, 955                unsupportedChars: unsupportedCharsArray956            };957        } catch (error) {958            return { valid: true, unsupportedChars: [] };959        }960    }961 962    // Update character counter and validate text length963    function updateCharCounter() {964        const rawText = demoTextInput.textContent || demoTextInput.innerText || '';965        const text = rawText.replace(/\n$/g, ''); // Remove trailing newline that browsers may add966        const length = text.length;967        968        demoCharCount.textContent = length;969        970        // Get the actual width of the textarea971        const textareaWidth = demoTextInput.offsetWidth;972        // Max width reference: 1280px (container max-width) / 2 (grid column) - padding/gap ≈ 638px973        // Using 640px as reference for easier calculation974        const maxWidthRef = 640;975        976        // Calculate font size based on width ratio977        // Original rem values at max-width (640px):978        // 5rem = 80px @ 16px base → 80/640 = 12.5%979        // 4rem = 64px → 64/640 = 10%980        // 3rem = 48px → 48/640 = 7.5%981        // 2.5rem = 40px → 40/640 = 6.25%982        // 2rem = 32px → 32/640 = 5%983        // 1.5rem = 24px → 24/640 = 3.75%984        // 1rem = 16px → 16/640 = 2.5%985        986        // Check if mobile (572px or less) for 2x font size scaling987        const isMobile = window.innerWidth <= 572;988        const mobileMultiplier = isMobile ? 2 : 1;989        990        let fontSizeRatio;991        if (length <= 100) {992            fontSizeRatio = 0.055 * mobileMultiplier; // 5.5% of width993        } else if (length <= 200) {994            fontSizeRatio = 0.04 * mobileMultiplier; // 4% of width995        } else if (length < 240) {996            fontSizeRatio = 0.053125 * mobileMultiplier; // ~5.3125% of width (scaled from 2.5rem)997        } else if (length < 400) {998            fontSizeRatio = 0.0425 * mobileMultiplier; // ~4.25% of width (scaled from 2rem)999        } else if (length < 700) {1000            fontSizeRatio = 0.031875 * mobileMultiplier; // ~3.1875% of width (scaled from 1.5rem)1001        } else {1002            fontSizeRatio = 0.025 * mobileMultiplier; // 2.5% of width (minimum stays the same)1003        }1004        1005        // Calculate font size based on actual width1006        const fontSize = textareaWidth * fontSizeRatio;1007        demoTextInput.style.fontSize = `${fontSize}px`;1008        1009        // Remove all status classes1010        demoCharCounter.classList.remove('error', 'warning', 'valid');1011        1012        // Check for unsupported characters first (only if models are loaded)1013        let hasUnsupportedChars = false;1014        if (models && processors && length > 0) {1015            const validation = validateCharacters(text);1016            if (!validation.valid && validation.unsupportedChars.length > 0) {1017                hasUnsupportedChars = true;1018                const charList = validation.unsupportedChars.slice(0, 5).map(c => `"${c}"`).join(', ');1019                const moreChars = validation.unsupportedChars.length > 5 ? ` and ${validation.unsupportedChars.length - 5} more` : '';1020                showDemoError(`Unsupported characters detected: ${charList}${moreChars}. Please remove them before generating speech.`);1021            } else {1022                hideDemoError();1023            }1024        }1025        1026        // Update status based on length and character validation1027        if (length < MIN_CHARS) {1028            demoCharCounter.classList.add('error');1029            demoCharWarning.textContent = '(At least 10 characters)';1030            demoGenerateBtn.disabled = true;1031        } else if (hasUnsupportedChars) {1032            demoCharCounter.classList.add('error');1033            demoCharWarning.textContent = '(Unsupported characters)';1034            demoGenerateBtn.disabled = true;1035        } else {1036            demoCharCounter.classList.add('valid');1037            demoCharWarning.textContent = '';1038            // Enable only if models are loaded AND not currently generating1039            demoGenerateBtn.disabled = !models || isGenerating;1040        }1041    }1042 1043    // Validate text input1044    function validateTextInput(text) {1045        if (!text || text.trim().length === 0) {1046            return { valid: false, message: 'Please enter some text.' };1047        }1048        if (text.length < MIN_CHARS) {1049            return { valid: false, message: `Text must be at least ${MIN_CHARS} characters long. (Currently ${text.length})` };1050        }1051        return { valid: true };1052    }1053 1054    // Load pre-extracted style embeddings from JSON1055    async function loadStyleEmbeddings(voice) {1056        try {1057            // Check if already cached1058            if (refEmbeddingCache[voice]) {1059                return refEmbeddingCache[voice];1060            }1061            1062            const embeddingPath = REF_EMBEDDING_PATHS[voice];1063            if (!embeddingPath) {1064                throw new Error(`No embedding path configured for voice: ${voice}`);1065            }1066            1067            const response = await fetch(embeddingPath);1068            if (!response.ok) {1069                throw new Error(`Failed to fetch embedding: ${response.statusText}`);1070            }1071            1072            const embeddingData = await response.json();1073            1074            // Convert JSON data to ONNX tensors1075            // Flatten nested arrays before creating Float32Array1076            const styleTtlData = embeddingData.style_ttl.data.flat(Infinity);1077            const styleTtlTensor = new ort.Tensor(1078                embeddingData.style_ttl.type || 'float32',1079                Float32Array.from(styleTtlData),1080                embeddingData.style_ttl.dims1081            );1082            1083            const styleDpData = embeddingData.style_dp.data.flat(Infinity);1084            const styleDpTensor = new ort.Tensor(1085                embeddingData.style_dp.type || 'float32',1086                Float32Array.from(styleDpData),1087                embeddingData.style_dp.dims1088            );1089            1090            const embeddings = {1091                styleTtl: styleTtlTensor,1092                styleDp: styleDpTensor1093            };1094            1095            // Cache the embeddings1096            refEmbeddingCache[voice] = embeddings;1097            1098            return embeddings;1099        } catch (error) {1100            throw error;1101        }1102    }1103    1104    // Switch to a different voice1105    async function switchVoice(voice) {1106        try {1107            const embeddings = await loadStyleEmbeddings(voice);1108            1109            currentStyleTtlTensor = embeddings.styleTtl;1110            currentStyleDpTensor = embeddings.styleDp;1111            currentVoice = voice;1112            1113            // Update active speaker in UI1114            if (typeof window.updateActiveSpeaker === 'function') {1115                window.updateActiveSpeaker(voice);1116            }1117            1118            // Re-validate text after switching voice1119            updateCharCounter();1120        } catch (error) {1121            showDemoError(`Failed to load voice ${voice}: ${error.message}`);1122            throw error;1123        }1124    }1125 1126    // Check WebGPU support more thoroughly1127    async function checkWebGPUSupport() {1128        try {1129            // Detect iOS/Safari1130            const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || 1131                          (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);1132            const isSafari = /^((?!chrome|crios|android|edg|firefox).)*safari/i.test(navigator.userAgent);1133            1134            // iOS and Safari have incomplete WebGPU support1135            if (isIOS) {1136                return { supported: false, reason: 'iOS does not support the required WebGPU features' };1137            }1138            if (isSafari) {1139                // Desktop Safari might work, but check carefully1140               return { supported: false, reason: 'Safari does not support the required WebGPU features' };1141            }1142            1143            // Check if WebGPU is available in the browser1144            if (!navigator.gpu) {1145                return { supported: false, reason: 'WebGPU not available in this browser' };1146            }1147            1148            // Request adapter1149            const adapter = await navigator.gpu.requestAdapter();1150            if (!adapter) {1151                return { supported: false, reason: 'No WebGPU adapter found' };1152            }1153            1154            // Check adapter info1155            try {1156                const adapterInfo = await adapter.requestAdapterInfo();1157            } catch (infoError) {1158                // Ignore adapter info errors1159            }1160            1161            // Request device to test if it actually works1162            const device = await adapter.requestDevice();1163            if (!device) {1164                return { supported: false, reason: 'Failed to create WebGPU device' };1165            }1166            1167            return { supported: true, adapter, device };1168        } catch (error) {1169            // Handle specific iOS/Safari errors1170            const errorMsg = error.message || '';1171            if (errorMsg.includes('subgroupMinSize') || errorMsg.includes('subgroup')) {1172                return { supported: false, reason: 'iOS/Safari does not support required WebGPU features (subgroup operations)' };1173            }1174            return { supported: false, reason: error.message };1175        }1176    }1177 1178    // Warmup models with dummy inference (no audio playback, no UI updates)1179    async function warmupModels() {1180        try {1181            const dummyText = 'Looking to integrate Supertonic into your product? We offer customized on-device SDK solutions tailored to your business needs. Our lightweight, high-performance TTS technology can be seamlessly integrated into mobile apps, IoT devices, automotive systems, and more. Try it now, and enjoy its speed.';1182            const totalStep = 5; // Use minimal steps for faster warmup1183            const durationFactor = 1.0;1184            1185            const textList = [dummyText];1186            const bsz = 1;1187            1188            // Use pre-computed style embeddings1189            const styleTtlTensor = currentStyleTtlTensor;1190            const styleDpTensor = currentStyleDpTensor;1191            1192            // Step 1: Estimate duration1193            const { textIds, textMask } = processors.textProcessor.call(textList, currentLanguage);1194            1195            const textIdsShape = [bsz, textIds[0].length];1196            const textMaskShape = [bsz, 1, textMask[0][0].length];1197            const textMaskTensor = arrayToTensor(textMask, textMaskShape);1198            1199            const dpResult = await models.dpOrt.run({1200                text_ids: intArrayToTensor(textIds, textIdsShape),

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