From 44700b152a3a5015022fcc312f00033f695acdfa Mon Sep 17 00:00:00 2001 From: Paul Peavyhouse Date: Mon, 20 Jul 2026 14:12:36 -0700 Subject: [PATCH 1/2] WIP any-tts kokoro code --- .gitmodules | 3 + package.json | 2 +- scripts/download-assets.cjs | 251 +++++ src-tauri/.gitignore | 6 +- src-tauri/Cargo.lock | 1481 +++++++++++++++++++++++++-- src-tauri/Cargo.toml | 2 + src-tauri/any-tts | 1 + src-tauri/src/commands.rs | 28 + src-tauri/src/lib.rs | 3 + src-tauri/src/tts_engine.rs | 167 +++ src-tauri/tauri.conf.json | 4 +- src/components/Dashboard.tsx | 13 +- src/components/DetailPane.tsx | 28 +- src/components/MarkdownRenderer.tsx | 206 +++- src/components/SettingsDialog.tsx | 316 +++++- src/components/TitleBar.tsx | 4 +- src/i18n/locales/ar.json | 21 +- src/i18n/locales/de.json | 21 +- src/i18n/locales/en.json | 21 +- src/i18n/locales/es.json | 21 +- src/i18n/locales/fr.json | 21 +- src/i18n/locales/it.json | 21 +- src/i18n/locales/ja.json | 23 +- src/i18n/locales/ko.json | 21 +- src/i18n/locales/nl.json | 21 +- src/i18n/locales/pt.json | 21 +- src/i18n/locales/ru.json | 21 +- src/i18n/locales/zh-TW.json | 21 +- src/i18n/locales/zh.json | 21 +- src/utils/errorHelper.ts | 6 + src/utils/kokoro_voices.json | 218 ++++ src/utils/useSpeech.ts | 912 +++++++++++++++-- 32 files changed, 3693 insertions(+), 233 deletions(-) create mode 100644 .gitmodules create mode 100644 scripts/download-assets.cjs create mode 160000 src-tauri/any-tts create mode 100644 src-tauri/src/tts_engine.rs create mode 100644 src/utils/kokoro_voices.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..41a1e07 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src-tauri/any-tts"] + path = src-tauri/any-tts + url = https://github.com/LookAtWhatAiCanDo/any-tts diff --git a/package.json b/package.json index e96383c..0db7806 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "node scripts/download-privacy.cjs && tsc && vite build", + "build": "node scripts/download-privacy.cjs && node scripts/download-assets.cjs && tsc && vite build", "preview": "vite preview", "tauri": "node scripts/tauri.cjs", "build:local": "node scripts/tauri.cjs build --config \"{\\\"bundle\\\": {\\\"createUpdaterArtifacts\\\": false, \\\"targets\\\": [\\\"app\\\"]}}\"", diff --git a/scripts/download-assets.cjs b/scripts/download-assets.cjs new file mode 100644 index 0000000..d35f66a --- /dev/null +++ b/scripts/download-assets.cjs @@ -0,0 +1,251 @@ +const fs = require('fs'); +const path = require('path'); + +const resourcesDir = path.join(__dirname, '..', 'src-tauri', 'resources'); +const ttsDir = path.join(resourcesDir, 'tts'); +const voicesDir = path.join(ttsDir, 'voices'); +const onnxDir = path.join(resourcesDir, 'onnx'); + +// Hugging Face base URLs for assets +const HF_ONNX_BASE_URL = 'https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main'; +const HF_KOKORO_BASE_URL = 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/main'; + +async function fetchVoicesFromHF() { + console.log("Fetching voices list from Hugging Face Model API..."); + try { + const response = await fetch("https://huggingface.co/api/models/hexgrad/Kokoro-82M"); + if (!response.ok) { + throw new Error(`HF API HTTP status ${response.status}`); + } + const data = await response.json(); + const voiceFiles = data.siblings + .map(s => s.rfilename) + .filter(f => f.startsWith("voices/") && f.endsWith(".pt")); + + if (voiceFiles.length === 0) { + throw new Error("No voices found in Hugging Face repository siblings list."); + } + + const mappedVoices = voiceFiles.map(f => { + const name = f.substring("voices/".length, f.length - ".pt".length); + // Map name prefix to readable label + let labelSuffix = ""; + if (name.startsWith("af_")) labelSuffix = "en-us female"; + else if (name.startsWith("am_")) labelSuffix = "en-us male"; + else if (name.startsWith("bf_")) labelSuffix = "en-gb female"; + else if (name.startsWith("bm_")) labelSuffix = "en-gb male"; + else if (name.startsWith("zf_")) labelSuffix = "zh-cn female"; + else if (name.startsWith("zm_")) labelSuffix = "zh-cn male"; + else if (name.startsWith("jf_")) labelSuffix = "ja-jp female"; + else if (name.startsWith("jm_")) labelSuffix = "ja-jp male"; + else if (name.startsWith("ef_")) labelSuffix = "es-es female"; + else if (name.startsWith("em_")) labelSuffix = "es-es male"; + else if (name.startsWith("ff_")) labelSuffix = "fr-fr female"; + else if (name.startsWith("fm_")) labelSuffix = "fr-fr male"; + else if (name.startsWith("if_")) labelSuffix = "it-it female"; + else if (name.startsWith("im_")) labelSuffix = "it-it male"; + else if (name.startsWith("pf_")) labelSuffix = "pt-br female"; + else if (name.startsWith("pm_")) labelSuffix = "pt-br male"; + else if (name.startsWith("hf_")) labelSuffix = "hi-in female"; + else if (name.startsWith("hm_")) labelSuffix = "hi-in male"; + else { + // Fallback gender based on 'f' or 'm' in prefix + const secondChar = name.split('_')[0]?.charAt(1) || ''; + labelSuffix = secondChar === 'f' ? 'female' : 'male'; + } + return { + name, + label: `${name} (${labelSuffix})` + }; + }); + return mappedVoices; + } catch (err) { + console.warn("Failed to fetch voices list from Hugging Face, falling back to local/default seed. Error:", err); + return null; + } +} + +function getFallbackVoices() { + const jsonPath = path.join(__dirname, '..', 'src', 'utils', 'kokoro_voices.json'); + if (fs.existsSync(jsonPath)) { + try { + return JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + } catch (e) { + console.warn("Failed to parse local voices fallback json:", e); + } + } + return [ + { name: "af_heart", label: "af_heart (en-us female)" } + ]; +} + +async function downloadFile(url, destPath) { + const tempPath = destPath + '.tmp'; + console.log(`Downloading ${url} -> ${destPath}...`); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download ${url}: HTTP status ${response.status}`); + } + const fileStream = fs.createWriteStream(tempPath); + const reader = response.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + fileStream.write(Buffer.from(value)); + } + fileStream.end(); + + // Clean up and rename + if (fs.existsSync(destPath)) { + fs.unlinkSync(destPath); + } + fs.renameSync(tempPath, destPath); + console.log(`Successfully saved: ${destPath}`); +} + +async function downloadFileIfModified(url, destPath) { + if (!fs.existsSync(destPath)) { + await downloadFile(url, destPath); + return; + } + + const headers = {}; + const stats = fs.statSync(destPath); + headers['If-Modified-Since'] = stats.mtime.toUTCString(); + + console.log(`Checking update for ${url} -> ${destPath}...`); + try { + const response = await fetch(url, { headers }); + if (response.status === 304) { + console.log(`${path.basename(destPath)} is up to date (304 Not Modified).`); + return; + } + if (!response.ok) { + throw new Error(`HTTP status ${response.status}`); + } + + const tempPath = destPath + '.tmp'; + console.log(`Downloading update: ${url} -> ${destPath}...`); + const fileStream = fs.createWriteStream(tempPath); + const reader = response.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + fileStream.write(Buffer.from(value)); + } + fileStream.end(); + + if (fs.existsSync(destPath)) { + fs.unlinkSync(destPath); + } + fs.renameSync(tempPath, destPath); + console.log(`Successfully updated: ${destPath}`); + } catch (err) { + console.warn(`[Asset Cache] Failed to check modification status for ${url}, keeping existing file. Error:`, err); + } +} + +function isPlaceholder(filePath, minSize = 1024) { + if (!fs.existsSync(filePath)) { + return true; + } + const stats = fs.statSync(filePath); + if (stats.size < minSize) { + return true; + } + try { + const content = fs.readFileSync(filePath, 'utf8'); + if (content.includes('placeholder')) { + return true; + } + } catch (e) { + // If it's a large binary file, readFileSync might fail or not be UTF-8 + } + return false; +} + +async function main() { + // Ensure directories exist + if (!fs.existsSync(resourcesDir)) { + fs.mkdirSync(resourcesDir, { recursive: true }); + } + if (!fs.existsSync(ttsDir)) { + fs.mkdirSync(ttsDir, { recursive: true }); + } + if (!fs.existsSync(voicesDir)) { + fs.mkdirSync(voicesDir, { recursive: true }); + } + if (!fs.existsSync(onnxDir)) { + fs.mkdirSync(onnxDir, { recursive: true }); + } + + // Discover and configure voices dynamically + let kokoroVoices = await fetchVoicesFromHF(); + if (!kokoroVoices) { + kokoroVoices = getFallbackVoices(); + } else { + const jsonPath = path.join(__dirname, '..', 'src', 'utils', 'kokoro_voices.json'); + try { + fs.writeFileSync(jsonPath, JSON.stringify(kokoroVoices, null, 2), 'utf8'); + console.log(`Saved dynamic voices configuration file to: ${jsonPath}`); + } catch (err) { + console.error("Failed to write dynamic voices config file:", err); + } + } + + const voicesToDownload = kokoroVoices.map(v => v.name); + + // === 1. ONNX Semantic Search Model === + console.log('Checking ONNX semantic search model...'); + const onnxModelPath = path.join(onnxDir, 'model.onnx'); + if (isPlaceholder(onnxModelPath, 1024 * 1024 * 10)) { // 10MB minimum + await downloadFile(`${HF_ONNX_BASE_URL}/onnx/model.onnx`, onnxModelPath); + } else { + console.log('ONNX search model.onnx is already present and valid.'); + } + + const onnxVocabPath = path.join(onnxDir, 'vocab.txt'); + if (isPlaceholder(onnxVocabPath, 1024 * 10)) { // 10KB minimum + await downloadFile(`${HF_ONNX_BASE_URL}/vocab.txt`, onnxVocabPath); + } else { + console.log('ONNX search vocab.txt is already present and valid.'); + } + + // === 2. Offline TTS Weights === + console.log('Checking Offline TTS model...'); + const dummySafetensors = path.join(ttsDir, 'model.safetensors'); + if (isPlaceholder(dummySafetensors, 1024 * 1024 * 10)) { + if (fs.existsSync(dummySafetensors)) { + console.log('Removing dummy model.safetensors placeholder...'); + fs.unlinkSync(dummySafetensors); + } + } + + const configPath = path.join(ttsDir, 'config.json'); + await downloadFileIfModified(`${HF_KOKORO_BASE_URL}/config.json`, configPath); + + const modelPath = path.join(ttsDir, 'kokoro-v1_0.pth'); + if (isPlaceholder(modelPath, 1024 * 1024 * 10)) { // 10MB minimum + await downloadFile(`${HF_KOKORO_BASE_URL}/kokoro-v1_0.pth`, modelPath); + } else { + console.log('kokoro-v1_0.pth is already present and valid.'); + } + + // === 3. Selected Voice Files === + console.log('Checking Selected TTS voices...'); + for (const voice of voicesToDownload) { + const voicePath = path.join(voicesDir, `${voice}.pt`); + if (isPlaceholder(voicePath, 1024 * 50)) { // 50KB minimum + await downloadFile(`${HF_KOKORO_BASE_URL}/voices/${voice}.pt`, voicePath); + } else { + console.log(`Voice ${voice}.pt is already present and valid.`); + } + } + + console.log('All model assets downloaded and configured successfully.'); +} + +main().catch(err => { + console.error('Fatal error during asset download:', err); + process.exit(1); +}); diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index b21bd68..13a9854 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -4,4 +4,8 @@ # Generated by Tauri # will have schema files for capabilities auto-completion -/gen/schemas +/gen/schemas/ + +# Large offline TTS weights and resources +/resources/tts/ +/resources/onnx/ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fe155c1..24eadfa 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -116,6 +116,32 @@ dependencies = [ "libc", ] +[[package]] +name = "any-tts" +version = "0.1.2" +dependencies = [ + "base64 0.22.1", + "candle-core", + "candle-nn", + "candle-transformers", + "kana2phone", + "lindera", + "nanomp3", + "pinyin", + "reqwest 0.12.28", + "rustc-hash 1.1.0", + "rustfft", + "safetensors 0.7.0", + "serde", + "serde_json", + "thiserror 2.0.18", + "tiktoken-rs", + "tokenizers 0.22.2", + "tracing", + "voice-g2p", + "zip 7.2.0", +] + [[package]] name = "anyhow" version = "1.0.103" @@ -355,6 +381,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -373,6 +408,12 @@ dependencies = [ "bit-vec 0.9.1", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -488,6 +529,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bstr" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -497,11 +549,48 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] [[package]] name = "byteorder" @@ -552,6 +641,96 @@ dependencies = [ "serde_core", ] +[[package]] +name = "candle-core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd9895436c1ba5dc1037a19935d084b838db066ff4e15ef7dded020b7c12a4a" +dependencies = [ + "byteorder", + "candle-metal-kernels", + "candle-ug", + "float8", + "gemm 0.19.0", + "half", + "libm", + "memmap2", + "num-traits", + "num_cpus", + "objc2-foundation", + "objc2-metal", + "rand 0.9.4", + "rand_distr 0.5.1", + "rayon", + "safetensors 0.7.0", + "thiserror 2.0.18", + "tokenizers 0.22.2", + "yoke 0.8.3", + "zip 7.2.0", +] + +[[package]] +name = "candle-metal-kernels" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6b5a4cae6b4e1ab0efcee4dc05272d11b374a3d1ba121b3a961e36be54ab60" +dependencies = [ + "half", + "objc2", + "objc2-foundation", + "objc2-metal", + "once_cell", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "candle-nn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9317a09d6530b758990ed7f625ac69ff43653bc9ee28b0464644ad1169ada87" +dependencies = [ + "candle-core", + "candle-metal-kernels", + "half", + "libc", + "num-traits", + "objc2-metal", + "rayon", + "safetensors 0.7.0", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "candle-transformers" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59d08c89e9f4af9c464e2f3a8e16199e7cc601e6f34538c2cfbb42b623b1783" +dependencies = [ + "byteorder", + "candle-core", + "candle-nn", + "fancy-regex 0.17.0", + "num-traits", + "rand 0.9.4", + "rayon", + "serde", + "serde_json", + "serde_plain", + "tracing", +] + +[[package]] +name = "candle-ug" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0fc3167cbc99c8ec1be618cb620aa21dca95038f118c3579a79370e3dc5f77" +dependencies = [ + "ug", + "ug-metal", +] + [[package]] name = "cargo-platform" version = "0.1.9" @@ -759,6 +938,7 @@ name = "codeoba" version = "0.8.9" dependencies = [ "aes-gcm", + "any-tts", "base64 0.22.1", "chrono", "dirs", @@ -774,7 +954,7 @@ dependencies = [ "percent-encoding", "rayon", "regex", - "reqwest", + "reqwest 0.13.4", "rusqlite", "serde", "serde_json", @@ -788,7 +968,7 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-window-state", "tempfile", - "tokenizers", + "tokenizers 0.23.1", "tokio", "tract-onnx", "url", @@ -1026,7 +1206,7 @@ dependencies = [ "postcard", "pulley-interpreter", "regalloc2", - "rustc-hash", + "rustc-hash 2.1.3", "serde", "serde_derive", "sha2 0.10.9", @@ -1190,7 +1370,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf", + "phf 0.13.1", "smallvec", ] @@ -1204,6 +1384,27 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "ctor" version = "0.8.0" @@ -1661,6 +1862,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fdab65db9274e0168143841eb8f864a0a21f8b1b8d2ba6812bbe6024346e99e" +[[package]] +name = "dyn-stack" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" +dependencies = [ + "bytemuck", + "dyn-stack-macros", +] + +[[package]] +name = "dyn-stack-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" + [[package]] name = "ed25519" version = "3.0.0" @@ -1727,6 +1944,70 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1736,12 +2017,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + [[package]] name = "endi" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -1831,6 +2133,28 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set 0.5.3", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fancy-regex" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" +dependencies = [ + "bit-set 0.8.0", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1900,6 +2224,18 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" +[[package]] +name = "float8" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d1f04709a8ac06e8e8042875a3c466cc4832d3c1a18dbcb9dba3c6e83046bc" +dependencies = [ + "half", + "num-traits", + "rand 0.9.4", + "rand_distr 0.5.1", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2086,7 +2422,7 @@ checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" dependencies = [ "bitflags 2.13.0", "debugid", - "rustc-hash", + "rustc-hash 2.1.3", "serde", "serde_derive", "serde_json", @@ -2192,50 +2528,299 @@ dependencies = [ ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "gemm" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" dependencies = [ - "typenum", - "version_check", + "dyn-stack", + "gemm-c32 0.18.2", + "gemm-c64 0.18.2", + "gemm-common 0.18.2", + "gemm-f16 0.18.2", + "gemm-f32 0.18.2", + "gemm-f64 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "gemm" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", + "dyn-stack", + "gemm-c32 0.19.0", + "gemm-c64 0.19.0", + "gemm-common 0.19.0", + "gemm-f16 0.19.0", + "gemm-f32 0.19.0", + "gemm-f64 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "gemm-c32" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", ] [[package]] -name = "getrandom" -version = "0.4.3" +name = "gemm-c32" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-common" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" +dependencies = [ + "bytemuck", + "dyn-stack", + "half", + "libm", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.21.5", + "raw-cpuid", + "rayon", + "seq-macro", + "sysctl", +] + +[[package]] +name = "gemm-common" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e" +dependencies = [ + "bytemuck", + "dyn-stack", + "half", + "libm", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.22.3", + "raw-cpuid", + "rayon", + "seq-macro", + "sysctl", +] + +[[package]] +name = "gemm-f16" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "gemm-f32 0.18.2", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "rayon", + "seq-macro", +] + +[[package]] +name = "gemm-f16" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "gemm-f32 0.19.0", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "rayon", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] @@ -2430,13 +3015,17 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ + "bytemuck", "cfg-if", "crunchy", "num-traits", + "rand 0.9.4", + "rand_distr 0.5.1", + "zerocopy", ] [[package]] @@ -2612,6 +3201,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -2682,7 +3272,7 @@ dependencies = [ "displaydoc", "potential_utf", "utf8_iter", - "yoke", + "yoke 0.8.3", "zerofrom", "zerovec", ] @@ -2749,7 +3339,7 @@ dependencies = [ "displaydoc", "icu_locale_core", "writeable", - "yoke", + "yoke 0.8.3", "zerofrom", "zerotrie", "zerovec", @@ -3081,6 +3671,25 @@ dependencies = [ "serde_json", ] +[[package]] +name = "kana2phone" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6c6e71a984e88de113a6e693e39236c3232b438642ed115463a39f788d1358" +dependencies = [ + "phf 0.8.0", + "regex", +] + +[[package]] +name = "kanaria" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f9d9652540055ac4fded998a73aca97d965899077ab1212587437da44196ff" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "keyboard-types" version = "0.7.0" @@ -3165,7 +3774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -3194,6 +3803,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.16" @@ -3220,6 +3839,84 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "lindera" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb784bf51d22be2350667c35c0796250ca4dd6888ee98fe8a41f16f400f6c9b" +dependencies = [ + "anyhow", + "byteorder", + "csv", + "daachorse", + "kanaria", + "lindera-dictionary", + "lindera-ipadic", + "log", + "once_cell", + "percent-encoding", + "regex", + "serde", + "serde_json", + "serde_yaml_ng", + "strum", + "strum_macros", + "unicode-blocks", + "unicode-normalization", + "unicode-segmentation", + "url", +] + +[[package]] +name = "lindera-dictionary" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0aa640fbefa4b4251f8ab001e47f213fdecf7bbbd0ee4a17a27b2cbff5ef1" +dependencies = [ + "anyhow", + "byteorder", + "csv", + "daachorse", + "derive_builder", + "encoding", + "encoding_rs", + "encoding_rs_io", + "flate2", + "glob", + "log", + "md5", + "memmap2", + "num_cpus", + "once_cell", + "rand 0.10.2", + "regex", + "reqwest 0.13.4", + "rkyv", + "serde", + "serde_json", + "strum", + "strum_macros", + "tar", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "lindera-ipadic" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c8eebbfaee36d6ebd674aa796decbbb186e42c8b7a919f3495ba610cc8fcb6" +dependencies = [ + "anyhow", + "byteorder", + "csv", + "lindera-dictionary", + "once_cell", + "rkyv", + "serde_json", + "tokio", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -3349,6 +4046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", + "stable_deref_trait", ] [[package]] @@ -3366,6 +4064,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.13.0", + "block", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + [[package]] name = "mime" version = "0.3.17" @@ -3412,7 +4125,7 @@ checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -3459,6 +4172,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "nanomp3" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69bdf7e634dc76798adc292ebd4f6e8e125cde6843a264fe398c52c3f7e8541" + [[package]] name = "ndarray" version = "0.17.2" @@ -3589,6 +4328,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ + "bytemuck", "num-traits", ] @@ -3638,6 +4378,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.6" @@ -3808,6 +4558,20 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.0", + "block2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "objc2-osa-kit" version = "0.3.2" @@ -4046,14 +4810,25 @@ dependencies = [ "indexmap 2.14.0", ] +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros", - "phf_shared", + "phf_macros 0.13.1", + "phf_shared 0.13.1", "serde", ] @@ -4063,8 +4838,18 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", ] [[package]] @@ -4074,7 +4859,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -4083,20 +4882,29 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.118", ] +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + [[package]] name = "phf_shared" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher", + "siphasher 1.0.3", ] [[package]] @@ -4105,6 +4913,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pinyin" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e225f595052d9c46045755be4b8d7950b6d9f3c33e0c0b74ba58f11bbfa8c64b" + [[package]] name = "piper" version = "0.2.5" @@ -4304,6 +5118,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -4336,6 +5156,26 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "pulley-interpreter" version = "46.0.1" @@ -4359,6 +5199,43 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "pulp" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + [[package]] name = "quick-xml" version = "0.41.0" @@ -4379,7 +5256,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror 2.0.18", @@ -4399,9 +5276,9 @@ dependencies = [ "getrandom 0.4.3", "lru-slab", "rand 0.10.2", - "rand_pcg", + "rand_pcg 0.10.2", "ring", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", @@ -4446,13 +5323,36 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rancor" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daff8b7b3ccf5f7ba270b3e7a0a4d4c701c5797e38dec27c7e2c3dbb830fed1c" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg 0.2.1", +] + [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] @@ -4467,6 +5367,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -4477,6 +5387,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.9.5" @@ -4492,6 +5411,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand 0.9.4", +] + [[package]] name = "rand_distr" version = "0.6.0" @@ -4502,6 +5431,24 @@ dependencies = [ "rand 0.10.2", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_pcg" version = "0.10.2" @@ -4511,6 +5458,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -4554,6 +5510,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4615,7 +5577,7 @@ dependencies = [ "bumpalo", "hashbrown 0.17.1", "log", - "rustc-hash", + "rustc-hash 2.1.3", "serde", "smallvec", ] @@ -4643,12 +5605,61 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rend" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.4" @@ -4706,6 +5717,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815cc8a37159a463064825246cadb07961e25cd9885908606f6d08a98d8f8874" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "rsqlite-vfs" version = "0.1.1" @@ -4737,6 +5778,12 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -4867,6 +5914,27 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "safetensors" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5" +dependencies = [ + "hashbrown 0.16.1", + "serde", + "serde_json", +] + [[package]] name = "safetensors" version = "0.8.0" @@ -5017,10 +6085,10 @@ dependencies = [ "derive_more", "log", "new_debug_unreachable", - "phf", + "phf 0.13.1", "phf_codegen", "precomputed-hash", - "rustc-hash", + "rustc-hash 2.1.3", "servo_arc", "smallvec", ] @@ -5035,6 +6103,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + [[package]] name = "serde" version = "1.0.228" @@ -5101,6 +6175,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -5130,6 +6213,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.21.0" @@ -5162,6 +6257,19 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "serde_yaml_ng" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -5265,6 +6373,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "siphasher" version = "1.0.3" @@ -5421,7 +6535,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared", + "phf_shared 0.13.1", "precomputed-hash", ] @@ -5431,8 +6545,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", ] @@ -5443,6 +6557,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5467,6 +6602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -5501,6 +6637,20 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "sysctl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" +dependencies = [ + "bitflags 2.13.0", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -5639,7 +6789,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -5802,7 +6952,7 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest", + "reqwest 0.13.4", "rustls", "semver", "serde", @@ -5816,7 +6966,7 @@ dependencies = [ "tokio", "url", "windows-sys 0.60.2", - "zip", + "zip 4.6.1", ] [[package]] @@ -5903,7 +7053,7 @@ dependencies = [ "json-patch", "log", "memchr", - "phf", + "phf 0.13.1", "plist", "proc-macro2", "quote", @@ -6005,6 +7155,21 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "tiktoken-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a19830747d9034cd9da43a60eaa8e552dfda7712424aebf187b7a60126bae0d" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bstr", + "fancy-regex 0.13.0", + "lazy_static", + "regex", + "rustc-hash 1.1.0", +] + [[package]] name = "time" version = "0.3.53" @@ -6060,6 +7225,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokenizers" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" +dependencies = [ + "ahash", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "getrandom 0.3.4", + "indicatif", + "itertools", + "log", + "macro_rules_attribute", + "monostate", + "onig", + "paste", + "rand 0.9.4", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.18", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + [[package]] name = "tokenizers" version = "0.23.1" @@ -6105,9 +7304,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -6454,7 +7665,7 @@ dependencies = [ "minijinja", "nom 8.0.0", "nom-language", - "safetensors", + "safetensors 0.8.0", "serde", "serde_json", "simd-adler32", @@ -6494,7 +7705,7 @@ dependencies = [ "getrandom 0.4.3", "log", "rand 0.10.2", - "rand_distr", + "rand_distr 0.6.0", "rustfft", "tract-extra", "tract-nnef", @@ -6577,6 +7788,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typeid" version = "1.0.3" @@ -6600,6 +7817,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ug" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b761acf8af3494640d826a8609e2265e19778fb43306c7f15379c78c9b05b0" +dependencies = [ + "gemm 0.18.2", + "half", + "libloading 0.8.9", + "memmap2", + "num", + "num-traits", + "num_cpus", + "rayon", + "safetensors 0.4.5", + "serde", + "thiserror 1.0.69", + "tracing", + "yoke 0.7.5", +] + +[[package]] +name = "ug-metal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7adf545a99a086d362efc739e7cf4317c18cbeda22706000fd434d70ea3d95" +dependencies = [ + "half", + "metal", + "objc", + "serde", + "thiserror 1.0.69", + "ug", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -6641,12 +7893,27 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-blocks" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b12e05d9e06373163a9bb6bb8c263c261b396643a99445fe6b9811fd376581b" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-normalization-alignments" version = "0.1.12" @@ -6696,6 +7963,12 @@ dependencies = [ "ctutils", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -6765,6 +8038,19 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "voice-g2p" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bbb1055aed015e0534b155cd50f1f15cf06f4648593b44f2c1e5d1e42c52962" +dependencies = [ + "fancy-regex 0.17.0", + "serde", + "serde_json", + "thiserror 2.0.18", + "unicode-normalization", +] + [[package]] name = "vswhom" version = "0.1.0" @@ -6804,6 +8090,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -7247,7 +8539,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ - "phf", + "phf 0.13.1", "phf_codegen", "string_cache", "string_cache_codegen", @@ -7306,6 +8598,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -7952,6 +9253,18 @@ dependencies = [ "rustix", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.7.5", + "zerofrom", +] + [[package]] name = "yoke" version = "0.8.3" @@ -7959,10 +9272,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.8.2", "zerofrom", ] +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + [[package]] name = "yoke-derive" version = "0.8.2" @@ -8101,7 +9426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", - "yoke", + "yoke 0.8.3", "zerofrom", ] @@ -8111,7 +9436,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "yoke", + "yoke 0.8.3", "zerofrom", "zerovec-derive", ] @@ -8139,6 +9464,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "zip" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +dependencies = [ + "crc32fast", + "indexmap 2.14.0", + "memchr", + "typed-path", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d45c199..a854d23 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -60,6 +60,8 @@ lazy_static = "1.5" base64 = "0.22" rayon = "1.12.0" souvlaki = "0.7.3" +any-tts = { path = "any-tts", features = ["kokoro", "metal"] } + [dev-dependencies] tauri = { version = "2", features = ["test"] } diff --git a/src-tauri/any-tts b/src-tauri/any-tts new file mode 160000 index 0000000..a4fd74b --- /dev/null +++ b/src-tauri/any-tts @@ -0,0 +1 @@ +Subproject commit a4fd74b375aadd3a3a0ec3b55aa5488a8096f9e0 diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index ad3a759..c21ad28 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -38,6 +38,8 @@ pub const ERR_MANIFEST_WRITE: u32 = 1054; // General/Session Errors (2000 - 2099) pub const ERR_ADAPTER_NOT_FOUND: u32 = 2001; pub const ERR_SESSION_READ_LOCK: u32 = 2002; +pub const ERR_TTS_MODEL_LOAD: u32 = 2003; +pub const ERR_TTS_SYNTHESIS: u32 = 2004; // Search & Index Errors (2100 - 2199) pub const ERR_ONNX_NOT_FOUND: u32 = 2101; @@ -1516,6 +1518,32 @@ mod trusted_root_tests { } } +#[tauri::command] +pub async fn prewarm_offline_speech(app_handle: tauri::AppHandle) -> Result<(), AppErrorPayload> { + tokio::task::spawn_blocking(move || { + crate::tts_engine::prewarm(&app_handle) + .map_err(|e| AppErrorPayload::with_msg(ERR_TTS_SYNTHESIS, e)) + }) + .await + .map_err(|e| AppErrorPayload::with_msg(ERR_GENERIC, format!("Thread join error: {}", e)))? +} + +#[tauri::command] +pub async fn generate_offline_speech( + app_handle: tauri::AppHandle, + text: String, + voice: String, +) -> Result { + use base64::Engine as _; + tokio::task::spawn_blocking(move || { + crate::tts_engine::synthesize_offline(&app_handle, &text, &voice) + .map(|wav_bytes| base64::engine::general_purpose::STANDARD.encode(&wav_bytes)) + .map_err(|e| AppErrorPayload::with_msg(ERR_TTS_SYNTHESIS, e)) + }) + .await + .map_err(|e| AppErrorPayload::with_msg(ERR_GENERIC, format!("Thread join error: {}", e)))? +} + #[cfg(test)] mod get_all_sessions_tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0e112cd..821860a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ pub mod parsers; pub mod premium; pub mod search; pub mod tokenizer; +pub mod tts_engine; pub mod watcher; #[cfg(test)] @@ -417,6 +418,8 @@ pub fn run() { commands::get_index_subagents, commands::save_index_subagents, commands::update_playback_metadata, + commands::prewarm_offline_speech, + commands::generate_offline_speech, menu::update_scroll_menu_labels, menu::set_menu_item_text ]) diff --git a/src-tauri/src/tts_engine.rs b/src-tauri/src/tts_engine.rs new file mode 100644 index 0000000..6cceeca --- /dev/null +++ b/src-tauri/src/tts_engine.rs @@ -0,0 +1,167 @@ +use any_tts::{load_model, ModelType, SynthesisRequest, TtsConfig, TtsModel}; +use lazy_static::lazy_static; +use std::sync::Mutex; +use tauri::{path::BaseDirectory, AppHandle, Manager}; + +lazy_static! { + static ref MODEL: Mutex>> = Mutex::new(None); +} + +/// Prewarm the TTS model by loading it into memory if not already loaded. +pub fn prewarm(app_handle: &AppHandle) -> Result<(), String> { + let mut lock = MODEL + .lock() + .map_err(|e| format!("Failed to lock TTS engine: {}", e))?; + + if lock.is_none() { + let tts_dir = app_handle + .path() + .resolve("resources/tts", BaseDirectory::Resource) + .map_err(|e| { + let err_msg = format!("Failed to resolve tts path: {}", e); + eprintln!("[TTS Engine] {}", err_msg); + err_msg + })?; + + eprintln!("[TTS Engine] Prewarming: Resolved tts_dir: {:?}", tts_dir); + + if !tts_dir.exists() { + let err_msg = format!("Offline TTS resources not found at {:?}", tts_dir); + eprintln!("[TTS Engine] Prewarming: {}", err_msg); + return Err(err_msg); + } + + let config = TtsConfig::new(ModelType::Kokoro) + .with_model_path(tts_dir.to_string_lossy().to_string()); + + let loaded = load_model(config).map_err(|e| { + let err_msg = format!("Failed to load Kokoro model: {}", e); + eprintln!("[TTS Engine] Prewarming: {}", err_msg); + err_msg + })?; + + *lock = Some(loaded); + } + Ok(()) +} + +/// Helper function to synthesize speech offline using embedded Kokoro-82M model. +/// Returns WAV file bytes on success. +pub fn synthesize_offline( + app_handle: &AppHandle, + text: &str, + voice: &str, +) -> Result, String> { + let mut lock = MODEL + .lock() + .map_err(|e| format!("Failed to lock TTS engine: {}", e))?; + + if lock.is_none() { + let tts_dir = app_handle + .path() + .resolve("resources/tts", BaseDirectory::Resource) + .map_err(|e| { + let err_msg = format!("Failed to resolve tts path: {}", e); + eprintln!("[TTS Engine] {}", err_msg); + err_msg + })?; + + eprintln!("[TTS Engine] Resolved tts_dir: {:?}", tts_dir); + + if !tts_dir.exists() { + let err_msg = format!("Offline TTS resources not found at {:?}", tts_dir); + eprintln!("[TTS Engine] {}", err_msg); + return Err(err_msg); + } + + let config = TtsConfig::new(ModelType::Kokoro) + .with_model_path(tts_dir.to_string_lossy().to_string()); + + let loaded = load_model(config).map_err(|e| { + let err_msg = format!("Failed to load Kokoro model: {}", e); + eprintln!("[TTS Engine] {}", err_msg); + err_msg + })?; + + *lock = Some(loaded); + } + + if let Some(ref model) = *lock { + // Map voice prefixes to ISO language codes for Kokoro + let lang = if voice.starts_with("af_") || voice.starts_with("am_") { + "en" + } else if voice.starts_with("bf_") || voice.starts_with("bm_") { + "en-gb" + } else if voice.starts_with("jf_") || voice.starts_with("jm_") { + "ja" + } else if voice.starts_with("zf_") || voice.starts_with("zm_") { + "zh" + } else if voice.starts_with("kf_") || voice.starts_with("km_") { + "ko" + } else if voice.starts_with("ff_") || voice.starts_with("fm_") { + "fr" + } else if voice.starts_with("df_") || voice.starts_with("dm_") { + "de" + } else if voice.starts_with("if_") || voice.starts_with("im_") { + "it" + } else if voice.starts_with("pf_") || voice.starts_with("pm_") { + "pt" + } else if voice.starts_with("es_") || voice.starts_with("es-") { + "es" + } else { + "en" // default fallback + }; + + let request = SynthesisRequest::new(text) + .with_language(lang) + .with_voice(voice); + + let mut audio = model.synthesize(&request).map_err(|e| { + let err_msg = format!("Failed to synthesize speech: {}", e); + eprintln!("[TTS Engine] {}", err_msg); + err_msg + })?; + + // Prepend 150ms of digital silence (zero samples) to prevent OS audio wake-up clipping. + // At 24000Hz sample rate, 150ms is 3600 samples. + let silence_len = (audio.sample_rate as f32 * 0.15) as usize; + let mut padded = vec![0.0f32; silence_len]; + padded.extend_from_slice(&audio.samples); + audio.samples = padded; + + Ok(audio.get_wav()) + } else { + let err_msg = "TTS engine not initialized".to_string(); + eprintln!("[TTS Engine] {}", err_msg); + Err(err_msg) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn test_load_and_synthesize() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let tts_dir = manifest_dir.join("resources").join("tts"); + if !tts_dir.join("kokoro-v1_0.pth").exists() { + println!("Skipping test: kokoro-v1_0.pth not found in resources"); + return; + } + let config = TtsConfig::new(ModelType::Kokoro) + .with_model_path(tts_dir.to_string_lossy().to_string()); + let model = load_model(config).expect("Failed to load Kokoro model"); + let test_txt = "WKWebView"; + + let request = SynthesisRequest::new(test_txt) + .with_language("en") + .with_voice("af_heart"); + let audio = model + .synthesize(&request) + .expect("Failed to synthesize speech"); + let wav = audio.get_wav(); + assert!(!wav.is_empty()); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 87fd739..88f8740 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -35,8 +35,8 @@ "bundle": { "active": true, "resources": [ - "resources/onnx/model.onnx", - "resources/onnx/vocab.txt" + "resources/onnx/**/*", + "resources/tts/**/*" ], "targets": "all", "publisher": "WhatAiCanDo", diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 604c515..f969d83 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -734,7 +734,18 @@ export const Dashboard = (props: DashboardProps) => { > } + fallback={ + } + > + + + } > diff --git a/src/components/DetailPane.tsx b/src/components/DetailPane.tsx index ee87969..e98773f 100644 --- a/src/components/DetailPane.tsx +++ b/src/components/DetailPane.tsx @@ -26,7 +26,7 @@ import { Volume2, } from "lucide-solid"; import { MarkdownRenderer } from "./MarkdownRenderer"; -import { useSpeech } from "../utils/useSpeech"; +import { useSpeech, splitIntoLogicalBlocks, sanitizeBlockForSpeech } from "../utils/useSpeech"; import { useI18n } from "../i18n/i18n"; import { logFE } from "../utils/logger"; import { getStatusBadge } from "../utils/sessionStatus"; @@ -2267,9 +2267,24 @@ const AssistantMessageRenderer = (props: { const groupedParts = createMemo(() => { const list = parts(); const result: Array< - { type: "text"; content: string } | { type: "toolGroup"; tools: MessageToolPart[] } + | { type: "text"; content: string; startBlockIndex: number } + | { type: "toolGroup"; tools: MessageToolPart[] } > = []; let currentToolGroup: MessageToolPart[] = []; + let currentBlockCount = 0; + + const getBlockCount = (text: string) => { + const blocks = splitIntoLogicalBlocks(text); + let count = 0; + for (const rawBlock of blocks) { + if (/^[-*_]{3,}$/.test(rawBlock)) continue; + const sanitized = sanitizeBlockForSpeech(rawBlock); + if (sanitized && /\p{L}|\p{N}/u.test(sanitized)) { + count++; + } + } + return count; + }; for (const part of list) { if (part.type === "tool") { @@ -2279,7 +2294,13 @@ const AssistantMessageRenderer = (props: { result.push({ type: "toolGroup", tools: currentToolGroup }); currentToolGroup = []; } - result.push(part); + const blockCount = getBlockCount(part.content); + result.push({ + type: "text", + content: part.content, + startBlockIndex: currentBlockCount, + }); + currentBlockCount += blockCount; } } @@ -2317,6 +2338,7 @@ const AssistantMessageRenderer = (props: { turnIndex={props.turnIndex} sourceId={props.sourceId} filePath={props.filePath} + startBlockIndex={part.startBlockIndex} /> ); diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index 264e6f0..afb6f3c 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -19,7 +19,7 @@ import { openUrl } from "@tauri-apps/plugin-opener"; import DOMPurify from "dompurify"; import { highlightContainer } from "../utils/highlighter"; import { invoke } from "@tauri-apps/api/core"; -import { useSpeech } from "../utils/useSpeech"; +import { useSpeech, splitIntoLogicalBlocks, sanitizeBlockForSpeech } from "../utils/useSpeech"; const massageMermaidCode = (code: string): string => { const lines = code.split(/\r?\n/); @@ -155,14 +155,105 @@ interface MarkdownRendererProps { turnIndex?: number; sourceId?: string; filePath?: string; + startBlockIndex?: number; } +const wrapBrContent = (el: HTMLElement) => { + if (el.querySelector(".br-content-wrap") || el.classList.contains("br-content-wrap")) return; + + const childNodes = Array.from(el.childNodes); + let currentGroup: Node[] = []; + const groups: Node[][] = []; + + for (const node of childNodes) { + if (node.nodeType === 1 && (node as Element).tagName.toLowerCase() === "br") { + groups.push(currentGroup); + currentGroup = []; + } else { + currentGroup.push(node); + } + } + groups.push(currentGroup); + + if (groups.length <= 1) return; + + el.innerHTML = ""; + + for (let i = 0; i < groups.length; i++) { + const group = groups[i]!; + const hasText = group.some((n) => n.nodeType === 3 && (n.textContent?.trim().length ?? 0) > 0); + const hasElement = group.some((n) => n.nodeType === 1); + + if (group.length > 0 && (hasText || hasElement)) { + const wrapper = document.createElement("span"); + wrapper.className = "br-content-wrap inline-block w-full relative"; + for (const node of group) { + wrapper.appendChild(node); + } + el.appendChild(wrapper); + } + + if (i < groups.length - 1) { + el.appendChild(document.createElement("br")); + } + } +}; + +const wrapLiInlineContent = (li: HTMLElement) => { + if (li.querySelector(".li-content-wrap") || li.querySelector(".br-content-wrap")) return; + + const childNodes = Array.from(li.childNodes); + const inlineNodes: Node[] = []; + + for (const node of childNodes) { + if ( + node.nodeType === 1 && // Node.ELEMENT_NODE + /^(p|ul|ol|blockquote|div|h[1-6])$/i.test((node as Element).tagName) + ) { + break; + } + inlineNodes.push(node); + } + + const hasText = inlineNodes.some( + (n) => n.nodeType === 3 && (n.textContent?.trim().length ?? 0) > 0 // Node.TEXT_NODE + ); + const hasInlineElement = inlineNodes.some( + (n) => + n.nodeType === 1 && // Node.ELEMENT_NODE + !/^(p|ul|ol|blockquote|div|h[1-6])$/i.test((n as Element).tagName) + ); + + if (inlineNodes.length > 0 && (hasText || hasInlineElement)) { + const wrapper = document.createElement("span"); + wrapper.className = "li-content-wrap inline-block w-full relative"; + li.insertBefore(wrapper, inlineNodes[0]!); + for (const node of inlineNodes) { + wrapper.appendChild(node); + } + } +}; + export const MarkdownRenderer = (props: MarkdownRendererProps) => { const { t } = useI18n(); const speech = useSpeech(); const [container, setContainer] = createSignal(null); const [isExpanded, setIsExpanded] = createSignal(false); + const numBlocks = createMemo(() => { + if (props.startBlockIndex === undefined) return 100000; + const blocks = splitIntoLogicalBlocks(props.content); + let count = 0; + for (const rawBlock of blocks) { + if (/^[-*_]{3,}$/.test(rawBlock)) continue; + const sanitized = sanitizeBlockForSpeech(rawBlock); + if (sanitized && /\p{L}|\p{N}/u.test(sanitized)) { + count++; + } + } + return count; + }); + // Listen to custom toggle-mermaid-raw events to swap raw/diagram formats from the parent context menu createEffect(() => { const handleToggleEvent = (e: Event) => { @@ -676,16 +767,36 @@ export const MarkdownRenderer = (props: MarkdownRendererProps) => { setTimeout(() => { if (!containerRef) return; + // Wrap content separated by
in p and li elements to allow precise line-by-line highlighting + const brElements = containerRef.querySelectorAll("p, li, blockquote"); + brElements.forEach((el) => wrapBrContent(el as HTMLElement)); + + // Wrap inline content of list items to allow precise highlighting and play button alignment + const liElements = containerRef.querySelectorAll("li"); + liElements.forEach((li) => wrapLiInlineContent(li as HTMLElement)); + const elements = Array.from( - containerRef.querySelectorAll("p, li, blockquote, h1, h2, h3, h4, h5, h6") + containerRef.querySelectorAll( + "p, .li-content-wrap, .br-content-wrap, li, blockquote, h1, h2, h3, h4, h5, h6" + ) ); let narrativeElements = elements.filter((el) => !el.closest("pre") && !el.closest("code")); - // Filter out child paragraphs that are nested inside list items to avoid duplicate play buttons + // Discard parent elements that have a wrapped child to ensure we target the innermost element narrativeElements = narrativeElements.filter((el) => { - if (el.tagName.toLowerCase() === "p" && el.closest("li")) { + if (el.querySelector(".br-content-wrap") || el.querySelector(".li-content-wrap")) { return false; } + if (el.tagName.toLowerCase() === "p" && el.closest("li")) { + const parentLi = el.closest("li"); + if (parentLi) { + const liText = parentLi.textContent?.trim() || ""; + const pText = el.textContent?.trim() || ""; + if (liText === pText) { + return false; // Filter out if duplicate of parent + } + } + } return true; }); @@ -773,17 +884,55 @@ export const MarkdownRenderer = (props: MarkdownRendererProps) => { return; } + if (currentItem.blockIndex === undefined) { + return; + } + + if (props.startBlockIndex !== undefined) { + const relIdx = currentItem.blockIndex - props.startBlockIndex; + if (relIdx < 0 || relIdx >= numBlocks()) { + return; + } + } + + const targetBlockIndex = + props.startBlockIndex !== undefined + ? currentItem.blockIndex - props.startBlockIndex + : currentItem.blockIndex; + + // Wrap content separated by
in p and li elements to allow precise line-by-line highlighting + const brElements = containerRef.querySelectorAll("p, li, blockquote"); + brElements.forEach((el) => wrapBrContent(el as HTMLElement)); + + // Wrap inline content of list items to allow precise highlighting + const liElements = containerRef.querySelectorAll("li"); + liElements.forEach((li) => wrapLiInlineContent(li as HTMLElement)); + // Select text block elements in document order const elements = Array.from( - containerRef.querySelectorAll("p, li, blockquote, h1, h2, h3, h4, h5, h6") + containerRef.querySelectorAll( + "p, .li-content-wrap, .br-content-wrap, li, blockquote, h1, h2, h3, h4, h5, h6" + ) ); let narrativeElements = elements.filter((el) => !el.closest("pre") && !el.closest("code")); - // Filter out child paragraphs that are nested inside list items to avoid duplicate highlighting targets + // Discard parent elements that have a wrapped child to ensure we target the innermost element narrativeElements = narrativeElements.filter((el) => { - if (el.tagName.toLowerCase() === "p" && el.closest("li")) { + if (el.querySelector(".br-content-wrap") || el.querySelector(".li-content-wrap")) { return false; } + // Keep p elements if they are nested inside li, because we can target them individually using minimum-length matching. + // But if the parent li has no other text content besides this p, we can filter it to prevent double-matching. + if (el.tagName.toLowerCase() === "p" && el.closest("li")) { + const parentLi = el.closest("li"); + if (parentLi) { + const liText = parentLi.textContent?.trim() || ""; + const pText = el.textContent?.trim() || ""; + if (liText === pText) { + return false; // Filter out if duplicate of parent + } + } + } return true; }); @@ -792,23 +941,46 @@ export const MarkdownRenderer = (props: MarkdownRendererProps) => { // Try text-similarity matching first (highly robust against off-by-one formatting/tag gaps) const cleanSpeakText = currentItem.text.toLowerCase().replace(/[^\p{L}\p{N}]/gu, ""); if (cleanSpeakText) { - targetEl = - (narrativeElements.find((el) => { - const cleanElText = el.textContent?.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "") || ""; - return ( + logFE("info", `TTS HIGHLIGHT scan for speakText: "${cleanSpeakText}"`); + const matches = narrativeElements + .map((el, i) => { + let rawElText = el.textContent?.toLowerCase() || ""; + // Normalize angle brackets in the same way as speech text to ensure matching + rawElText = rawElText.replace(//g, " greater than "); + const cleanElText = rawElText.replace(/[^\p{L}\p{N}]/gu, "") || ""; + logFE("info", ` Element ${i} (${el.tagName}): "${cleanElText.substring(0, 60)}..."`); + const isMatch = cleanElText.length > 0 && - (cleanElText.includes(cleanSpeakText) || cleanSpeakText.includes(cleanElText)) - ); - }) as HTMLElement) || null; + (cleanElText.includes(cleanSpeakText) || cleanSpeakText.includes(cleanElText)); + return { el: el as HTMLElement, i, cleanElText, isMatch }; + }) + .filter((m) => m.isMatch); + + if (matches.length > 0) { + // Sort matches to find the best candidate: + // 1. Prefer smaller text length difference to match the innermost/most specific element + // 2. Prefer the element closest to the blockIndex position + matches.sort((a, b) => { + const lenDiffA = Math.abs(a.cleanElText.length - cleanSpeakText.length); + const lenDiffB = Math.abs(b.cleanElText.length - cleanSpeakText.length); + if (lenDiffA !== lenDiffB) { + return lenDiffA - lenDiffB; + } + const distA = Math.abs(a.i - (targetBlockIndex ?? 0)); + const distB = Math.abs(b.i - (targetBlockIndex ?? 0)); + return distA - distB; + }); + targetEl = matches[0]!.el; + } } // Fallback to positional index matching if ( !targetEl && - currentItem.blockIndex !== undefined && - currentItem.blockIndex < narrativeElements.length + targetBlockIndex !== undefined && + targetBlockIndex < narrativeElements.length ) { - targetEl = narrativeElements[currentItem.blockIndex] as HTMLElement; + targetEl = narrativeElements[targetBlockIndex] as HTMLElement; } if (targetEl) { diff --git a/src/components/SettingsDialog.tsx b/src/components/SettingsDialog.tsx index 3b1e4b9..17736a5 100644 --- a/src/components/SettingsDialog.tsx +++ b/src/components/SettingsDialog.tsx @@ -13,6 +13,9 @@ import { Globe, SlidersHorizontal, Volume2, + Edit2, + Play, + Check, } from "lucide-solid"; import { invoke } from "@tauri-apps/api/core"; @@ -23,6 +26,9 @@ import { useI18n, LOCALES, LOCALE_NAMES, Locale } from "../i18n/i18n"; import { getLocalizedAppError } from "../utils/errorHelper"; import { openUrl } from "@tauri-apps/plugin-opener"; import { SourceMetadata } from "../types"; +import { useSpeech, DEFAULT_PRONUNCIATIONS, DEFAULT_KOKORO_VOICE } from "../utils/useSpeech"; + +import KOKORO_VOICES from "../utils/kokoro_voices.json"; interface SettingsDialogProps { isOpen: boolean; @@ -125,9 +131,99 @@ export const SettingsDialog = (props: SettingsDialogProps) => { const [voices, setVoices] = createSignal([]); const [selectedVoiceName, setSelectedVoiceName] = createSignal(""); + const [selectedProvider, setSelectedProvider] = createSignal("system"); const [speechRate, setSpeechRate] = createSignal(1.0); const [speechPitch, setSpeechPitch] = createSignal(1.0); + const [rules, setRules] = createSignal>({}); + const [newWord, setNewWord] = createSignal(""); + const [newReplacement, setNewReplacement] = createSignal(""); + const [editingWord, setEditingWord] = createSignal(null); + const [editWordVal, setEditWordVal] = createSignal(""); + const [editRepVal, setEditRepVal] = createSignal(""); + + const handleSaveEdit = (oldWord: string) => { + const word = editWordVal().trim().toLowerCase(); + const rep = editRepVal().trim(); + if (!word || !rep) return; + const updated = { ...rules() }; + if (word !== oldWord) { + delete updated[oldWord]; + } + updated[word] = rep; + setRules(updated); + localStorage.setItem("codeoba-tts-pronunciations", JSON.stringify(updated)); + setEditingWord(null); + }; + + const loadRules = () => { + try { + const saved = localStorage.getItem("codeoba-tts-pronunciations"); + if (saved) { + const parsed = JSON.parse(saved); + // Merge missing defaults + let changed = false; + for (const key of Object.keys(DEFAULT_PRONUNCIATIONS)) { + if (!(key in parsed)) { + parsed[key] = DEFAULT_PRONUNCIATIONS[key]; + changed = true; + } + } + // Remove old auto-added default keys that we deprecated + if ("phonemizer" in parsed && parsed["phonemizer"] === "fohnemizer") { + delete parsed["phonemizer"]; + changed = true; + } + if ("phonemizers" in parsed && parsed["phonemizers"] === "fohnemizers") { + delete parsed["phonemizers"]; + changed = true; + } + if (changed) { + localStorage.setItem("codeoba-tts-pronunciations", JSON.stringify(parsed)); + } + setRules(parsed); + } else { + setRules(DEFAULT_PRONUNCIATIONS); + } + } catch (e) { + setRules(DEFAULT_PRONUNCIATIONS); + } + }; + + const handleAddRule = (e: Event) => { + e.preventDefault(); + const word = newWord().trim().toLowerCase(); + const rep = newReplacement().trim(); + if (!word || !rep) return; + const updated = { ...rules(), [word]: rep }; + setRules(updated); + localStorage.setItem("codeoba-tts-pronunciations", JSON.stringify(updated)); + setNewWord(""); + setNewReplacement(""); + }; + + const handleDeleteRule = (word: string) => { + const updated = { ...rules() }; + delete updated[word]; + setRules(updated); + localStorage.setItem("codeoba-tts-pronunciations", JSON.stringify(updated)); + }; + + const handleResetRules = () => { + setRules(DEFAULT_PRONUNCIATIONS); + localStorage.setItem("codeoba-tts-pronunciations", JSON.stringify(DEFAULT_PRONUNCIATIONS)); + }; + + const handleProviderChange = (val: string) => { + setSelectedProvider(val); + localStorage.setItem("codeoba-tts-provider", val); + if (val === "offline-kokoro") { + handleVoiceChange(DEFAULT_KOKORO_VOICE); + } else { + handleVoiceChange(""); + } + }; + const loadVoices = () => { if (typeof window !== "undefined" && window.speechSynthesis) { setVoices(window.speechSynthesis.getVoices()); @@ -151,13 +247,34 @@ export const SettingsDialog = (props: SettingsDialogProps) => { const handleResetReadAloudDefaults = () => { setSelectedVoiceName(""); + setSelectedProvider("system"); setSpeechRate(1.0); setSpeechPitch(1.0); localStorage.removeItem("codeoba-tts-voice"); + localStorage.removeItem("codeoba-tts-provider"); localStorage.removeItem("codeoba-tts-rate"); localStorage.removeItem("codeoba-tts-pitch"); }; + const speech = useSpeech(); + + const handleTestVoice = async () => { + const saying = t("settings.readAloud.testSaying"); + try { + await speech.speakDirectText(saying); + } catch (err) { + console.error("[TTS] Test voice failed:", err); + const provider = localStorage.getItem("codeoba-tts-provider") || "system"; + if (provider === "offline-kokoro") { + alert(t("errors.ttsModelLoad")); + } else { + alert( + "Failed to play system voice. Please check your system speech synthesizer configuration." + ); + } + } + }; + // Custom Theme HSL Adjusters const [activeColorIndex, setActiveColorIndex] = createSignal("bg"); @@ -313,6 +430,7 @@ export const SettingsDialog = (props: SettingsDialogProps) => { window.speechSynthesis.onvoiceschanged = loadVoices; } setSelectedVoiceName(localStorage.getItem("codeoba-tts-voice") || ""); + setSelectedProvider(localStorage.getItem("codeoba-tts-provider") || "system"); setSpeechRate( localStorage.getItem("codeoba-tts-rate") ? parseFloat(localStorage.getItem("codeoba-tts-rate")!) @@ -323,6 +441,7 @@ export const SettingsDialog = (props: SettingsDialogProps) => { ? parseFloat(localStorage.getItem("codeoba-tts-pitch")!) : 1.0 ); + loadRules(); }); // General Settings @@ -1008,6 +1127,28 @@ export const SettingsDialog = (props: SettingsDialogProps) => { {t("settings.readAloud.title")} + {/* Speech Engine Selector */} +
+
+

+ {t("settings.readAloud.provider")} +

+

+ {t("settings.readAloud.providerDesc")} +

+
+ +
+ {/* Voice Selector */}
@@ -1023,14 +1164,21 @@ export const SettingsDialog = (props: SettingsDialogProps) => { onChange={(e) => handleVoiceChange(e.currentTarget.value)} class="bg-background border border-border/80 rounded-xl px-3 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent font-medium cursor-pointer max-w-xs" > - - - {(voice) => ( - - )} - + + + + {(voice) => ( + + )} + + + + + {(voice) => } + +
@@ -1090,13 +1238,163 @@ export const SettingsDialog = (props: SettingsDialogProps) => {
-
+ {/* Actions: Reset and Test Voice */} +
+ +
+ + {/* Custom Pronunciations Editor */} +
+
+

+ {t("settings.readAloud.pronunciations")} +

+

+ {t("settings.readAloud.pronunciationsDesc")} +

+
+ +
+ + {(word) => { + const isEditing = () => editingWord() === word; + return ( +
+ +
+ + {word} + + + + {rules()[word]} + +
+
+ + + +
+ + } + > +
+ setEditWordVal(e.currentTarget.value)} + class="bg-background border border-border/80 rounded-xl px-2 py-1 text-xs text-text-primary focus:outline-none focus:border-accent w-24 font-mono font-medium" + /> + + setEditRepVal(e.currentTarget.value)} + class="bg-background border border-border/80 rounded-xl px-2 py-1 text-xs text-text-primary focus:outline-none focus:border-accent flex-1 font-medium" + /> +
+
+ + +
+
+
+ ); + }} +
+ +
+ No pronunciations defined. +
+
+
+ +
+ setNewWord(e.currentTarget.value)} + class="bg-background border border-border/80 rounded-xl px-3 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent flex-1 min-w-0 font-medium" + /> + setNewReplacement(e.currentTarget.value)} + class="bg-background border border-border/80 rounded-xl px-3 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent flex-1 min-w-0 font-medium" + /> + +
+ +
+ +
diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index ff9ee01..e918694 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -227,7 +227,9 @@ export const TitleBar = (props: TitleBarProps) => { } class="w-[30px] h-[30px] inline-flex items-center justify-center hover:bg-surface border border-transparent hover:border-border/60 hover:text-text-primary text-text-secondary rounded-lg transition-all cursor-pointer" > - {speech.isPlaying() && !speech.isPaused() ? ( + {speech.isPreparingSpeech && speech.isPreparingSpeech() ? ( + + ) : speech.isPlaying() && !speech.isPaused() ? ( ) : ( diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index e563fc5..b746901 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "القراءة بصوت عالٍ", + "provider": "محرك الصوت", + "providerDesc": "اختر بين نظام تحويل النص إلى كلام ومحرك Kokoro AI المحلي.", + "providerSystem": "مخلق صوت النظام", + "providerOffline": "محرك Kokoro AI المحلي (دون اتصال)", "voice": "صوت تحويل النص إلى كلام", "voiceDesc": "اختر محرك توليد الصوت لقراءة النص الوصفي.", "defaultVoice": "صوت النظام الافتراضي", @@ -299,7 +303,17 @@ "rateDesc": "اضبط السرعة التي يتم بها نطق النص.", "pitch": "طبقة الصوت", "pitchDesc": "اضبط نغمة الصوت المنطوق.", - "reset": "إعادة التعيين للافتراضي" + "reset": "إعادة التعيين للافتراضي", + "test": "تجربة الصوت", + "testSaying": "خيط حرير على حيط خليل.", + "pronunciations": "النطق المخصص", + "pronunciationsDesc": "تكوين بدائل إملائية مخصصة لاختصارات الأدلة أو اختصارات التعليمات البرمجية أو المصطلحات التقنية.", + "pronunciationsOriginal": "الكلمة / المطابقة", + "pronunciationsReplacement": "النطق المنطوق", + "pronunciationsPlaceholderWord": "مثال: src", + "pronunciationsPlaceholderReplacement": "مثال: source", + "pronunciationsAdd": "إضافة قاعدة", + "pronunciationsReset": "إعادة تعيين القواعد" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "لم يتم العثور على محول الوكيل '{sourceId}'.", "sessionReadLock": "فشل الحصول على قفل قراءة الجلسة.", + "ttsModelLoad": "فشل تحميل أوزان نموذج TTS المحلي دون اتصال.", + "ttsSynthesis": "فشل توليد الصوت محليًا: {reason}", "onnxNotFound": "البحث الدلالي غير متاح: لم يتم العثور على نموذج ONNX/المفردات. يرجى التحقق من حزمة التطبيق أو تنزيل النموذج.", "embedderCreation": "فشل تهيئة أداة التضمين الدلالية.", "embeddingsGeneration": "فشل إنشاء التضمينات.", @@ -445,6 +461,7 @@ "readSessionAloud": "قراءة الجلسة بصوت عالٍ", "stopReading": "إيقاف القراءة", "playFromHere": "اقرأ بصوت عالٍ من هنا", - "sessionTransition": "الجلسة {sessionTitle}. {text}" + "sessionTransition": "الجلسة {sessionTitle}. {text}", + "sessionLabel": "جلسة" } } diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 881c0a1..28e52f1 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "Vorlesen", + "provider": "Sprach-Engine", + "providerDesc": "Wählen Sie zwischen der System-Sprachsynthese und der Offline-Kokoro-KI.", + "providerSystem": "System-Sprachsynthesizer", + "providerOffline": "Offline-Kokoro-KI (Lokal)", "voice": "Text-to-Speech-Stimme", "voiceDesc": "Wählen Sie den Stimmensynthesizer zum Vorlesen von Transkripttexten.", "defaultVoice": "Standardsystemstimme", @@ -299,7 +303,17 @@ "rateDesc": "Passen Sie die Geschwindigkeit an, mit der Text gesprochen wird.", "pitch": "Stimmhöhe", "pitchDesc": "Passen Sie die Tonhöhe der gesprochenen Stimme an.", - "reset": "Auf Standardwerte zurücksetzen" + "reset": "Auf Standardwerte zurücksetzen", + "test": "Stimme testen", + "testSaying": "Fischers Fritze fischt frische Fische, frische Fische fischt Fischers Fritze.", + "pronunciations": "Benutzerdefinierte Aussprache", + "pronunciationsDesc": "Konfigurieren Sie benutzerdefinierte Schreibweisen für Verzeichnisverknüpfungen, Code-Abkürzungen oder Fachbegriffe.", + "pronunciationsOriginal": "Wort / Treffer", + "pronunciationsReplacement": "Gesprochene Aussprache", + "pronunciationsPlaceholderWord": "z. B. src", + "pronunciationsPlaceholderReplacement": "z. B. source", + "pronunciationsAdd": "Regel hinzufügen", + "pronunciationsReset": "Regeln zurücksetzen" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "Agentenadapter '{sourceId}' nicht gefunden.", "sessionReadLock": "Fehler beim Erwerb der Sitzungslesesperre.", + "ttsModelLoad": "Laden der lokalen Offline-TTS-Modellgewichte fehlgeschlagen.", + "ttsSynthesis": "Lokale Sprachsynthese fehlgeschlagen: {reason}", "onnxNotFound": "Semantische Suche ist nicht verfügbar: ONNX-Modell/Vokabular nicht gefunden. Bitte überprüfen Sie die Anwendung Verpackung oder laden Sie das Modell herunter.", "embedderCreation": "Fehler beim Initialisieren des semantischen Embedders.", "embeddingsGeneration": "Fehler beim Generieren von Embeddings.", @@ -445,6 +461,7 @@ "readSessionAloud": "Sitzung vorlesen", "stopReading": "Vorlesen stoppen", "playFromHere": "Von hier an vorlesen", - "sessionTransition": "Sitzung {sessionTitle}. {text}" + "sessionTransition": "Sitzung {sessionTitle}. {text}", + "sessionLabel": "Sitzung" } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9061ed7..41c5281 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -297,6 +297,10 @@ }, "readAloud": { "title": "Read Aloud", + "provider": "Speech Engine", + "providerDesc": "Choose between system text-to-speech and offline Kokoro AI.", + "providerSystem": "System Voice Synthesizer", + "providerOffline": "Offline Kokoro AI (Local)", "voice": "Text-to-Speech Voice", "voiceDesc": "Select the voice synthesizer for reading transcript narrative text.", "defaultVoice": "Default System Voice", @@ -304,7 +308,17 @@ "rateDesc": "Adjust the speed at which text is spoken.", "pitch": "Speech Pitch", "pitchDesc": "Adjust the tone pitch of the voice.", - "reset": "Reset to Defaults" + "reset": "Reset to Defaults", + "test": "Test Voice", + "testSaying": "How much wood would a woodchuck chuck if a woodchuck could chuck wood?", + "pronunciations": "Custom Pronunciations", + "pronunciationsDesc": "Configure custom spelling overrides for directory shortcuts, code abbreviations, or technical terms.", + "pronunciationsOriginal": "Word / Match", + "pronunciationsReplacement": "Spoken Pronunciation", + "pronunciationsPlaceholderWord": "e.g., src", + "pronunciationsPlaceholderReplacement": "e.g., source", + "pronunciationsAdd": "Add Override", + "pronunciationsReset": "Reset Mappings" } }, "updater": { @@ -414,6 +428,8 @@ "errors": { "adapterNotFound": "Agent adapter '{sourceId}' not found.", "sessionReadLock": "Failed to acquire session read lock.", + "ttsModelLoad": "Failed to load local offline TTS model weights.", + "ttsSynthesis": "Failed to synthesize speech locally: {reason}", "onnxNotFound": "Semantic search is unavailable: ONNX model/vocab not found. Please verify the application packaging or download the model.", "embedderCreation": "Failed to initialize semantic embedder.", "embeddingsGeneration": "Failed to generate embeddings.", @@ -451,6 +467,7 @@ "readSessionAloud": "Read Session Aloud", "stopReading": "Stop Reading", "playFromHere": "Read Aloud from here", - "sessionTransition": "Session {sessionTitle}. {text}" + "sessionTransition": "Session {sessionTitle}. {text}", + "sessionLabel": "Session" } } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 433f6b3..18b919c 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "Lectura en voz alta", + "provider": "Motor de voz", + "providerDesc": "Elija entre la síntesis de voz del sistema y Kokoro AI local.", + "providerSystem": "Sintetizador de voz del sistema", + "providerOffline": "Kokoro AI local (sin conexión)", "voice": "Voz de texto a voz", "voiceDesc": "Seleccione el sintetizador de voz para leer el texto descriptivo.", "defaultVoice": "Voz del sistema por defecto", @@ -299,7 +303,17 @@ "rateDesc": "Ajuste la velocidad con la que se habla el texto.", "pitch": "Tono de voz", "pitchDesc": "Ajuste el tono de la voz hablada.", - "reset": "Restablecer a valores predeterminados" + "reset": "Restablecer a valores predeterminados", + "test": "Probar voz", + "testSaying": "Tres tristes tigres tragan trigo en un trigal.", + "pronunciations": "Pronunciaciones personalizadas", + "pronunciationsDesc": "Configure sustituciones de ortografía personalizadas para atajos de directorio, abreviaturas de código o términos técnicos.", + "pronunciationsOriginal": "Palabra / Coincidencia", + "pronunciationsReplacement": "Pronunciación hablada", + "pronunciationsPlaceholderWord": "ej. src", + "pronunciationsPlaceholderReplacement": "ej. source", + "pronunciationsAdd": "Añadir regla", + "pronunciationsReset": "Restablecer reglas" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "No se encontró el adaptador de agente '{sourceId}'.", "sessionReadLock": "Error al adquirir el bloqueo de lectura de la sesión.", + "ttsModelLoad": "Error al cargar los pesos del modelo TTS local sin conexión.", + "ttsSynthesis": "Error al sintetizar voz localmente: {reason}", "onnxNotFound": "La búsqueda semántica no está disponible: no se encontró el modelo ONNX/vocabulario. Verifique el empaquetado de la aplicación o descargue el modelo.", "embedderCreation": "Error al inicializar el incorporador semántico.", "embeddingsGeneration": "Error al generar incorporaciones.", @@ -445,6 +461,7 @@ "readSessionAloud": "Leer sesión en voz alta", "stopReading": "Detener lectura", "playFromHere": "Leer en voz alta desde aquí", - "sessionTransition": "Sesión {sessionTitle}. {text}" + "sessionTransition": "Sesión {sessionTitle}. {text}", + "sessionLabel": "Sesión" } } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 028b6e7..2d2cfd1 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "Lecture à haute voix", + "provider": "Moteur de synthèse", + "providerDesc": "Choisissez entre la synthèse vocale du système et l'IA locale Kokoro.", + "providerSystem": "Synthétiseur de voix système", + "providerOffline": "IA hors ligne Kokoro (Local)", "voice": "Voix de synthèse vocale", "voiceDesc": "Sélectionnez le synthétiseur de voix pour lire le texte descriptif.", "defaultVoice": "Voix système par défaut", @@ -299,7 +303,17 @@ "rateDesc": "Ajustez la vitesse de lecture du texte.", "pitch": "Hauteur de la voix", "pitchDesc": "Ajustez la hauteur de ton de la voix parlée.", - "reset": "Réinitialiser" + "reset": "Réinitialiser", + "test": "Tester la voix", + "testSaying": "Un chasseur sachant chasser doit savoir chasser sans son chien.", + "pronunciations": "Prononciations personnalisées", + "pronunciationsDesc": "Configurez des règles d'orthographe personnalisées pour les raccourcis de répertoire, les abréviations de code ou les termes techniques.", + "pronunciationsOriginal": "Mot / Correspondance", + "pronunciationsReplacement": "Prononciation parlée", + "pronunciationsPlaceholderWord": "ex. src", + "pronunciationsPlaceholderReplacement": "ex. source", + "pronunciationsAdd": "Ajouter une règle", + "pronunciationsReset": "Réinitialiser les règles" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "Adaptateur d'agent '{sourceId}' introuvable.", "sessionReadLock": "Échec de l'acquisition du verrou de lecture de session.", + "ttsModelLoad": "Impossible de charger les poids du modèle TTS local hors ligne.", + "ttsSynthesis": "Échec de la synthèse vocale locale : {reason}", "onnxNotFound": "La recherche sémantique est indisponible : modèle ONNX/vocabulaire introuvable. Veuillez vérifier l'empaquetage de l'application ou télécharger le modèle.", "embedderCreation": "Échec de l'initialisation de l'embeddeur sémantique.", "embeddingsGeneration": "Échec de la génération des embeddings.", @@ -445,6 +461,7 @@ "readSessionAloud": "Lire la session à haute voix", "stopReading": "Arrêter la lecture", "playFromHere": "Lire à haute voix à partir d'ici", - "sessionTransition": "Session {sessionTitle}. {text}" + "sessionTransition": "Session {sessionTitle}. {text}", + "sessionLabel": "Session" } } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index f063614..3aca91b 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "Lettura ad alta voce", + "provider": "Motore Vocale", + "providerDesc": "Scegli tra la sintesi vocale di sistema e la tecnologia offline Kokoro AI.", + "providerSystem": "Sintetizzatore Vocale di Sistema", + "providerOffline": "Kokoro AI Offline (Locale)", "voice": "Voce di sintesi vocale", "voiceDesc": "Seleziona il sintetizzatore vocale per leggere il testo narrativo.", "defaultVoice": "Voce di sistema predefinita", @@ -299,7 +303,17 @@ "rateDesc": "Regola la velocità con cui viene letto il testo.", "pitch": "Tonalità della voce", "pitchDesc": "Regola la tonalità della voce parlata.", - "reset": "Ripristina predefiniti" + "reset": "Ripristina predefiniti", + "test": "Testa Voce", + "testSaying": "Trentotto trentini entrarono a Trento tutti e trentotto trotterellando.", + "pronunciations": "Pronunce Personalizzate", + "pronunciationsDesc": "Configura sostituzioni di pronuncia per scorciatoie di directory, abbreviazioni di codice o termini tecnici.", + "pronunciationsOriginal": "Parola / Corrispondenza", + "pronunciationsReplacement": "Pronuncia Parlata", + "pronunciationsPlaceholderWord": "es. src", + "pronunciationsPlaceholderReplacement": "es. source", + "pronunciationsAdd": "Aggiungi Regola", + "pronunciationsReset": "Ripristina Regole" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "Adattatore agente '{sourceId}' non trovato.", "sessionReadLock": "Impossibile acquisire il blocco di lettura della sessione.", + "ttsModelLoad": "Impossibile caricare i pesi del modello TTS offline locale.", + "ttsSynthesis": "Sintesi vocale locale fallita: {reason}", "onnxNotFound": "La ricerca semantica non è disponibile: modello ONNX/vocabolario non trovato. Verificare il pacchetto dell'applicazione o scaricare il modello.", "embedderCreation": "Impossibile inizializzare l'incorporatore semantico.", "embeddingsGeneration": "Impossibile generare le incorporazioni.", @@ -445,6 +461,7 @@ "readSessionAloud": "Leggi sessione ad alta voce", "stopReading": "Interrompi lettura", "playFromHere": "Leggi ad alta voce da qui", - "sessionTransition": "Sessione {sessionTitle}. {text}" + "sessionTransition": "Sessione {sessionTitle}. {text}", + "sessionLabel": "Sessione" } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 340600a..cbe7369 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -244,7 +244,7 @@ "confirmDeleteSource": "{source}のCodeobaのローカル検索インデックスとキャッシュされたセッションをクリアしてもよろしいですか?これにより検索結果からは削除されますが、実際のエージェントアプリケーション、会話、またはログファイルが削除されたり影響を受けたりすることはありません。", "detectedMultiPromptTitle": "ローカル検索の有効化", "detectedMultiPromptBadge": "100% ローカル&プライベート", - "detectedMultiPromptMessage": "Codeobaはシステム上で以下のエージェント의チャット履歴を検出しました。インデックスを作成するアプリを選択してください。会話データがコンピューターの外部に送信されることはありません。", + "detectedMultiPromptMessage": "Codeobaはシステム上で以下のエージェントのチャット履歴を検出しました。インデックスを作成するアプリを選択してください。会話データがコンピューターの外部に送信されることはありません。", "detectedMultiPromptDenyAll": "後で設定する", "detectedMultiPromptAllowSelected": "選択したアプリを有効にする", "detectedMultiPromptFootnotePrivate": "🔒 100% プライベート: すべての検索解析とインデックス構築はデバイス上で完全にオフラインで実行されます。オプションのクラウド機能(アップデート確認など)は、必要なメタデータのみをバックエンドに送信します。", @@ -292,6 +292,10 @@ }, "readAloud": { "title": "読み上げ", + "provider": "音声エンジン", + "providerDesc": "システム標準の音声合成とオフラインKokoro AIから選択します。", + "providerSystem": "システム音声合成", + "providerOffline": "オフラインKokoro AI (ローカル)", "voice": "音声合成の音声", "voiceDesc": "トランスクリプトのテキストを読み上げるための音声合成エンジンを選択します。", "defaultVoice": "システムのデフォルト音声", @@ -299,7 +303,17 @@ "rateDesc": "テキストを読み上げる速度を調整します。", "pitch": "音声のピッチ", "pitchDesc": "読み上げ音声のトーン(ピッチ)を調整します。", - "reset": "デフォルトに戻す" + "reset": "デフォルトに戻す", + "test": "音声をテスト", + "testSaying": "隣の客はよく柿食う客だ。", + "pronunciations": "カスタム発音", + "pronunciationsDesc": "ディレクトリのショートカット、コードの略称、または技術用語のカスタム表記発音を設定します。", + "pronunciationsOriginal": "単語 / マッチ", + "pronunciationsReplacement": "読み方(発音)", + "pronunciationsPlaceholderWord": "例: src", + "pronunciationsPlaceholderReplacement": "例: source", + "pronunciationsAdd": "ルールを追加", + "pronunciationsReset": "ルールをリセット" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "エージェントアダプター '{sourceId}' が見つかりません。", "sessionReadLock": "セッション読み取りロックの取得に失敗しました。", + "ttsModelLoad": "ローカルのオフラインTTSモデルウェイトの読み込みに失敗しました。", + "ttsSynthesis": "ローカルでの音声合成に失敗しました: {reason}", "onnxNotFound": "セマンティック検索は利用できません:ONNXモデル/ボキャブラリーが見つかりません。アプリパッケージを確認するか、モデルをダウンロードしてください。", "embedderCreation": "セマンティックエンベッダーの初期化に失敗しました。", "embeddingsGeneration": "埋め込みの生成に失敗しました。", @@ -445,6 +461,7 @@ "readSessionAloud": "セッションを読み上げる", "stopReading": "読み上げを停止", "playFromHere": "ここから朗読", - "sessionTransition": "セッション {sessionTitle}。{text}" + "sessionTransition": "セッション {sessionTitle}。{text}", + "sessionLabel": "セッション" } } diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index c06bf24..3041393 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "소리 내어 읽기", + "provider": "음성 엔진", + "providerDesc": "시스템 기본 음성 합성기와 오프라인 Kokoro AI 중에서 선택합니다.", + "providerSystem": "시스템 음성 합성기", + "providerOffline": "오프라인 Kokoro AI (로컬)", "voice": "음성 합성 목소리", "voiceDesc": "텍스트를 읽을 음성 합성 엔진을 선택합니다.", "defaultVoice": "시스템 기본 목소리", @@ -299,7 +303,17 @@ "rateDesc": "텍스트를 읽는 속도를 조절합니다.", "pitch": "음성 피치", "pitchDesc": "말하는 목소리의 톤(피치)을 조절합니다.", - "reset": "기본값으로 재설정" + "reset": "기본값으로 재설정", + "test": "음성 테스트", + "testSaying": "간장 공장 공장장은 강 공장장이고 된장 공장 공장장은 공 공장장이다.", + "pronunciations": "사용자 지정 발음", + "pronunciationsDesc": "디렉터리 단축키, 코드 약어 또는 기술 용어의 커스텀 발음 규칙을 설정합니다.", + "pronunciationsOriginal": "단어 / 일치", + "pronunciationsReplacement": "실제 발음", + "pronunciationsPlaceholderWord": "예: src", + "pronunciationsPlaceholderReplacement": "예: source", + "pronunciationsAdd": "규칙 추가", + "pronunciationsReset": "규칙 초기화" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "에이전트 어댑터 '{sourceId}'를 찾을 수 없습니다.", "sessionReadLock": "세션 읽기 잠금을 획득하지 못했습니다.", + "ttsModelLoad": "로컬 오프라인 TTS 모델 가중치를 로드하지 못했습니다.", + "ttsSynthesis": "로컬 음성 합성에 실패했습니다: {reason}", "onnxNotFound": "시맨틱 검색을 사용할 수 없습니다: ONNX 모델/어휘를 찾을 수 없습니다. 애플리케이션 패키징을 확인하거나 모델을 다운로드하십시오.", "embedderCreation": "시맨틱 임베더를 초기화하지 못했습니다.", "embeddingsGeneration": "임베딩을 생성하지 못했습니다.", @@ -445,6 +461,7 @@ "readSessionAloud": "세션 소리 내어 읽기", "stopReading": "읽기 중지", "playFromHere": "여기서부터 낭독", - "sessionTransition": "세션 {sessionTitle}. {text}" + "sessionTransition": "세션 {sessionTitle}. {text}", + "sessionLabel": "세션" } } diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index d62230a..d6ea0f2 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "Voorlezen", + "provider": "Spraak-engine", + "providerDesc": "Kies tussen systeem spraaksynthese en offline Kokoro AI.", + "providerSystem": "Systeem-spraaksynthesizer", + "providerOffline": "Offline Kokoro AI (Lokaal)", "voice": "Tekst-naar-spraakstem", "voiceDesc": "Selecteer de stemsynthesizer voor het voorlezen van tekst.", "defaultVoice": "Standaard systeemstem", @@ -299,7 +303,17 @@ "rateDesc": "Pas de snelheid aan waarmee tekst wordt uitgesproken.", "pitch": "Spraaktoonhoogte", "pitchDesc": "Pas de toonhoogte van de gesproken stem aan.", - "reset": "Standaardwaarden herstellen" + "reset": "Standaardwaarden herstellen", + "test": "Stem testen", + "testSaying": "De kat krabt de krullen van de trap.", + "pronunciations": "Aangepaste uitspraken", + "pronunciationsDesc": "Configureer spellingregels voor mapkoppelingen, afkortingen in code of technische termen.", + "pronunciationsOriginal": "Woord / Overeenkomst", + "pronunciationsReplacement": "Gesproken uitspraak", + "pronunciationsPlaceholderWord": "bijv. src", + "pronunciationsPlaceholderReplacement": "bijv. source", + "pronunciationsAdd": "Regel toevoegen", + "pronunciationsReset": "Regels herstellen" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "Agentadapter '{sourceId}' niet gevonden.", "sessionReadLock": "Verkrijgen van leesslot voor sessie mislukt.", + "ttsModelLoad": "Kan de lokale offline TTS-modelgewichten niet laden.", + "ttsSynthesis": "Lokale spraaksynthese mislukt: {reason}", "onnxNotFound": "Semantisch zoeken is niet beschikbaar: ONNX-model/vocabulaire niet gevonden. Controleer de applicatieverpakking of download het model.", "embedderCreation": "Initialiseren van semantische embedder mislukt.", "embeddingsGeneration": "Genereren van embeddings mislukt.", @@ -445,6 +461,7 @@ "readSessionAloud": "Sessie voorlezen", "stopReading": "Voorlezen stoppen", "playFromHere": "Vanaf hier voorlezen", - "sessionTransition": "Sessie {sessionTitle}. {text}" + "sessionTransition": "Sessie {sessionTitle}. {text}", + "sessionLabel": "Sessie" } } diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 6a59619..9887121 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "Leitura em voz alta", + "provider": "Mecanismo de Voz", + "providerDesc": "Escolha entre a síntese de voz do sistema e a IA offline Kokoro.", + "providerSystem": "Sintetizador de Voz do Sistema", + "providerOffline": "Kokoro AI Offline (Local)", "voice": "Voz de texto para fala", "voiceDesc": "Selecione o sintetizador de voz para ler o texto descritivo.", "defaultVoice": "Voz do sistema padrão", @@ -299,7 +303,17 @@ "rateDesc": "Ajuste a velocidade em que o texto é falado.", "pitch": "Tom da fala", "pitchDesc": "Ajuste o tom da voz falada.", - "reset": "Redefinir para padrões" + "reset": "Redefinir para padrões", + "test": "Testar Voz", + "testSaying": "O rato roeu a roupa do rei de Roma.", + "pronunciations": "Pronúncias Personalizadas", + "pronunciationsDesc": "Configure substituições ortográficas para atalhos de diretório, abreviações de código ou termos técnicos.", + "pronunciationsOriginal": "Palavra / Correspondência", + "pronunciationsReplacement": "Pronúncia Falada", + "pronunciationsPlaceholderWord": "ex: src", + "pronunciationsPlaceholderReplacement": "ex: source", + "pronunciationsAdd": "Adicionar Regra", + "pronunciationsReset": "Restaurar Regras" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "Adaptador de agente '{sourceId}' não encontrado.", "sessionReadLock": "Falha ao adquirir o bloqueio de leitura da sessão.", + "ttsModelLoad": "Falha ao carregar os pesos do modelo TTS offline local.", + "ttsSynthesis": "Falha ao sintetizar voz localmente: {reason}", "onnxNotFound": "A busca semântica não está disponível: modelo ONNX/vocabulário não encontrado. Verifique a embalagem do aplicativo ou faça o download do modelo.", "embedderCreation": "Falha ao inicializar o incorporador semântico.", "embeddingsGeneration": "Falha ao gerar as incorporações.", @@ -445,6 +461,7 @@ "readSessionAloud": "Ler sessão em voz alta", "stopReading": "Parar leitura", "playFromHere": "Ler em voz alta a partir daqui", - "sessionTransition": "Sessão {sessionTitle}. {text}" + "sessionTransition": "Sessão {sessionTitle}. {text}", + "sessionLabel": "Sessão" } } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index ce9d0bc..4f0fedc 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "Чтение вслух", + "provider": "Голосовой движок", + "providerDesc": "Выберите между системным синтезатором речи и автономным ИИ Kokoro.", + "providerSystem": "Системный синтезатор речи", + "providerOffline": "Автономный ИИ Kokoro (Локально)", "voice": "Голос синтезатора", "voiceDesc": "Выберите синтезатор речи для чтения описательного текста.", "defaultVoice": "Системный голос по умолчанию", @@ -299,7 +303,17 @@ "rateDesc": "Настройте скорость произнесения текста.", "pitch": "Высота голоса", "pitchDesc": "Настройте высоту тона произносимого голоса.", - "reset": "Сбросить по умолчанию" + "reset": "Сбросить по умолчанию", + "test": "Тест голоса", + "testSaying": "Шла Саша по шоссе и сосала сушку.", + "pronunciations": "Пользовательское произношение", + "pronunciationsDesc": "Настройте правила замены произношения для путей папок, сокращений кода или технических терминов.", + "pronunciationsOriginal": "Слово / Совпадение", + "pronunciationsReplacement": "Произношение речи", + "pronunciationsPlaceholderWord": "напр. src", + "pronunciationsPlaceholderReplacement": "напр. source", + "pronunciationsAdd": "Добавить правило", + "pronunciationsReset": "Сбросить правила" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "Адаптер агента '{sourceId}' не найден.", "sessionReadLock": "Не удалось получить блокировку чтения сессии.", + "ttsModelLoad": "Не удалось загрузить веса локальной автономной модели TTS.", + "ttsSynthesis": "Ошибка локального синтеза речи: {reason}", "onnxNotFound": "Семантический поиск недоступен: модель ONNX/словарь не найдены. Пожалуйста, проверьте сборку приложения или скачайте модель.", "embedderCreation": "Не удалось инициализировать семантический эмбеддер.", "embeddingsGeneration": "Не удалось создать эмбеддинги.", @@ -445,6 +461,7 @@ "readSessionAloud": "Прочитать сеанс вслух", "stopReading": "Остановить чтение", "playFromHere": "Читать вслух отсюда", - "sessionTransition": "Сессия {sessionTitle}. {text}" + "sessionTransition": "Сессия {sessionTitle}. {text}", + "sessionLabel": "Сессия" } } diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index c50fd59..ffc6125 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "朗讀", + "provider": "語音引擎", + "providerDesc": "在系統文字轉語音與離線 Kokoro AI 之間進行選擇。", + "providerSystem": "系統語音合成器", + "providerOffline": "離線 Kokoro AI (本地)", "voice": "語音合成聲音", "voiceDesc": "選擇朗讀轉錄文字的語音合成引擎。", "defaultVoice": "系統預設聲音", @@ -299,7 +303,17 @@ "rateDesc": "調整文字朗讀的速度。", "pitch": "朗讀音調", "pitchDesc": "調整說話聲音的音調。", - "reset": "重設為預設值" + "reset": "重設為預設值", + "test": "測試聲音", + "testSaying": "吃葡萄不吐葡萄皮,不吃葡萄倒吐葡萄皮。", + "pronunciations": "自訂發音", + "pronunciationsDesc": "為目錄快捷方式、程式碼縮寫或技術術語設定自訂拼寫替換規則。", + "pronunciationsOriginal": "單字 / 匹配", + "pronunciationsReplacement": "口頭發音", + "pronunciationsPlaceholderWord": "例如 src", + "pronunciationsPlaceholderReplacement": "例如 source", + "pronunciationsAdd": "新增規則", + "pronunciationsReset": "重設規則" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "找不到代理介面卡 '{sourceId}'。", "sessionReadLock": "未能獲取會話讀取鎖。", + "ttsModelLoad": "載入本地離線 TTS 模型權重失敗。", + "ttsSynthesis": "本地合成語音失敗: {reason}", "onnxNotFound": "語意搜尋不可用:找不到 ONNX 模型/詞表。請驗證應用程式封裝或下載模型。", "embedderCreation": "未能初始化語意嵌入器。", "embeddingsGeneration": "未能生成嵌入向量。", @@ -445,6 +461,7 @@ "readSessionAloud": "朗讀工作階段", "stopReading": "停止朗讀", "playFromHere": "從這裡開始朗讀", - "sessionTransition": "會話 {sessionTitle}。{text}" + "sessionTransition": "會話 {sessionTitle}。{text}", + "sessionLabel": "對話" } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 4d67cbd..e078291 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -292,6 +292,10 @@ }, "readAloud": { "title": "朗读", + "provider": "语音引擎", + "providerDesc": "在系统文本转语音和离线 Kokoro AI 之间进行选择。", + "providerSystem": "系统语音合成器", + "providerOffline": "离线 Kokoro AI (本地)", "voice": "语音合成声音", "voiceDesc": "选择朗读转录文本的语音合成引擎。", "defaultVoice": "系统默认声音", @@ -299,7 +303,17 @@ "rateDesc": "调整文本朗读的速度。", "pitch": "朗读音调", "pitchDesc": "调整说话声音的音调。", - "reset": "恢复默认值" + "reset": "恢复默认值", + "test": "测试声音", + "testSaying": "吃葡萄不吐葡萄皮,不吃葡萄倒吐葡萄皮。", + "pronunciations": "自定义发音", + "pronunciationsDesc": "为目录快捷方式、代码缩写或技术术语配置自定义拼写替换规则。", + "pronunciationsOriginal": "单词 / 匹配", + "pronunciationsReplacement": "口头读音", + "pronunciationsPlaceholderWord": "例如 src", + "pronunciationsPlaceholderReplacement": "例如 source", + "pronunciationsAdd": "添加规则", + "pronunciationsReset": "重置规则" } }, "updater": { @@ -408,6 +422,8 @@ "errors": { "adapterNotFound": "找不到智能体适配器 '{sourceId}'。", "sessionReadLock": "未能获取会话读取锁。", + "ttsModelLoad": "加载本地离线 TTS 模型权重失败。", + "ttsSynthesis": "本地合成语音失败: {reason}", "onnxNotFound": "语义搜索不可用:找不到 ONNX 模型/词表。请验证应用打包或下载模型。", "embedderCreation": "未能初始化语义嵌入器。", "embeddingsGeneration": "未能生成嵌入向量。", @@ -445,6 +461,7 @@ "readSessionAloud": "朗读会话", "stopReading": "停止朗读", "playFromHere": "从这里开始朗读", - "sessionTransition": "会话 {sessionTitle}。{text}" + "sessionTransition": "会话 {sessionTitle}。{text}", + "sessionLabel": "会话" } } diff --git a/src/utils/errorHelper.ts b/src/utils/errorHelper.ts index ee4ca45..39ce610 100644 --- a/src/utils/errorHelper.ts +++ b/src/utils/errorHelper.ts @@ -28,6 +28,8 @@ export const ERR_MANIFEST_WRITE = 1054; // General/Session Errors (2000 - 2099) export const ERR_ADAPTER_NOT_FOUND = 2001; export const ERR_SESSION_READ_LOCK = 2002; +export const ERR_TTS_MODEL_LOAD = 2003; +export const ERR_TTS_SYNTHESIS = 2004; // Search & Index Errors (2100 - 2199) export const ERR_ONNX_NOT_FOUND = 2101; @@ -141,6 +143,10 @@ export const getLocalizedAppError = ( return t("errors.adapterNotFound", params); case ERR_SESSION_READ_LOCK: return t("errors.sessionReadLock", params); + case ERR_TTS_MODEL_LOAD: + return t("errors.ttsModelLoad", params); + case ERR_TTS_SYNTHESIS: + return t("errors.ttsSynthesis", params); // Search & Index Errors (2100 - 2199) case ERR_ONNX_NOT_FOUND: diff --git a/src/utils/kokoro_voices.json b/src/utils/kokoro_voices.json new file mode 100644 index 0000000..5389056 --- /dev/null +++ b/src/utils/kokoro_voices.json @@ -0,0 +1,218 @@ +[ + { + "name": "af_alloy", + "label": "af_alloy (en-us female)" + }, + { + "name": "af_aoede", + "label": "af_aoede (en-us female)" + }, + { + "name": "af_bella", + "label": "af_bella (en-us female)" + }, + { + "name": "af_heart", + "label": "af_heart (en-us female)" + }, + { + "name": "af_jessica", + "label": "af_jessica (en-us female)" + }, + { + "name": "af_kore", + "label": "af_kore (en-us female)" + }, + { + "name": "af_nicole", + "label": "af_nicole (en-us female)" + }, + { + "name": "af_nova", + "label": "af_nova (en-us female)" + }, + { + "name": "af_river", + "label": "af_river (en-us female)" + }, + { + "name": "af_sarah", + "label": "af_sarah (en-us female)" + }, + { + "name": "af_sky", + "label": "af_sky (en-us female)" + }, + { + "name": "am_adam", + "label": "am_adam (en-us male)" + }, + { + "name": "am_echo", + "label": "am_echo (en-us male)" + }, + { + "name": "am_eric", + "label": "am_eric (en-us male)" + }, + { + "name": "am_fenrir", + "label": "am_fenrir (en-us male)" + }, + { + "name": "am_liam", + "label": "am_liam (en-us male)" + }, + { + "name": "am_michael", + "label": "am_michael (en-us male)" + }, + { + "name": "am_onyx", + "label": "am_onyx (en-us male)" + }, + { + "name": "am_puck", + "label": "am_puck (en-us male)" + }, + { + "name": "am_santa", + "label": "am_santa (en-us male)" + }, + { + "name": "bf_alice", + "label": "bf_alice (en-gb female)" + }, + { + "name": "bf_emma", + "label": "bf_emma (en-gb female)" + }, + { + "name": "bf_isabella", + "label": "bf_isabella (en-gb female)" + }, + { + "name": "bf_lily", + "label": "bf_lily (en-gb female)" + }, + { + "name": "bm_daniel", + "label": "bm_daniel (en-gb male)" + }, + { + "name": "bm_fable", + "label": "bm_fable (en-gb male)" + }, + { + "name": "bm_george", + "label": "bm_george (en-gb male)" + }, + { + "name": "bm_lewis", + "label": "bm_lewis (en-gb male)" + }, + { + "name": "ef_dora", + "label": "ef_dora (es-es female)" + }, + { + "name": "em_alex", + "label": "em_alex (es-es male)" + }, + { + "name": "em_santa", + "label": "em_santa (es-es male)" + }, + { + "name": "ff_siwis", + "label": "ff_siwis (fr-fr female)" + }, + { + "name": "hf_alpha", + "label": "hf_alpha (hi-in female)" + }, + { + "name": "hf_beta", + "label": "hf_beta (hi-in female)" + }, + { + "name": "hm_omega", + "label": "hm_omega (hi-in male)" + }, + { + "name": "hm_psi", + "label": "hm_psi (hi-in male)" + }, + { + "name": "if_sara", + "label": "if_sara (it-it female)" + }, + { + "name": "im_nicola", + "label": "im_nicola (it-it male)" + }, + { + "name": "jf_alpha", + "label": "jf_alpha (ja-jp female)" + }, + { + "name": "jf_gongitsune", + "label": "jf_gongitsune (ja-jp female)" + }, + { + "name": "jf_nezumi", + "label": "jf_nezumi (ja-jp female)" + }, + { + "name": "jf_tebukuro", + "label": "jf_tebukuro (ja-jp female)" + }, + { + "name": "jm_kumo", + "label": "jm_kumo (ja-jp male)" + }, + { + "name": "pf_dora", + "label": "pf_dora (pt-br female)" + }, + { + "name": "pm_alex", + "label": "pm_alex (pt-br male)" + }, + { + "name": "pm_santa", + "label": "pm_santa (pt-br male)" + }, + { + "name": "zf_xiaobei", + "label": "zf_xiaobei (zh-cn female)" + }, + { + "name": "zf_xiaoni", + "label": "zf_xiaoni (zh-cn female)" + }, + { + "name": "zf_xiaoxiao", + "label": "zf_xiaoxiao (zh-cn female)" + }, + { + "name": "zf_xiaoyi", + "label": "zf_xiaoyi (zh-cn female)" + }, + { + "name": "zm_yunjian", + "label": "zm_yunjian (zh-cn male)" + }, + { + "name": "zm_yunxi", + "label": "zm_yunxi (zh-cn male)" + }, + { + "name": "zm_yunxia", + "label": "zm_yunxia (zh-cn male)" + }, + { + "name": "zm_yunyang", + "label": "zm_yunyang (zh-cn male)" + } +] \ No newline at end of file diff --git a/src/utils/useSpeech.ts b/src/utils/useSpeech.ts index 5fd5551..43c8ca4 100644 --- a/src/utils/useSpeech.ts +++ b/src/utils/useSpeech.ts @@ -1,4 +1,4 @@ -import { createSignal, createMemo } from "solid-js"; +import { createSignal, createMemo, createEffect, untrack } from "solid-js"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { Session } from "../types"; @@ -15,8 +15,67 @@ export interface SpeechItem { sessionTitle?: string; } -// Global active speechSynthesis utterance reference to avoid overlapping playbacks let activeUtterance: SpeechSynthesisUtterance | null = null; +let activeAudio: HTMLAudioElement | null = null; + +function getSharedAudio(): HTMLAudioElement | null { + if (typeof Audio === "undefined") return null; + if (!activeAudio) { + activeAudio = new Audio(); + } + return activeAudio; +} + +// Permanent cache for the static "Session" label +let staticSessionLabelCache: string | null = null; +let cachedVoiceForLabel: string | null = null; +let cachedLanguageForLabel: string | null = null; + +// Keep a small look-ahead cache for pre-fetched offline TTS audio (Map of text -> base64 data) +const prefetchCache = new Map(); +const activeSynthesisPromises = new Map>(); + +function setInPrefetchCache(text: string, base64Data: string) { + if (prefetchCache.size >= 8) { + const oldestKey = prefetchCache.keys().next().value; + if (oldestKey !== undefined) { + prefetchCache.delete(oldestKey); + } + } + prefetchCache.set(text, base64Data); +} + +function getOfflineSpeech(text: string, voiceName: string): Promise { + const cached = prefetchCache.get(text); + if (cached) return Promise.resolve(cached); + + const existingPromise = activeSynthesisPromises.get(text); + if (existingPromise) return existingPromise; + + const promise = invoke("generate_offline_speech", { + text, + voice: voiceName, + }) + .then((base64Data) => { + setInPrefetchCache(text, base64Data); + activeSynthesisPromises.delete(text); + return base64Data; + }) + .catch((err) => { + activeSynthesisPromises.delete(text); + throw err; + }); + + activeSynthesisPromises.set(text, promise); + return promise; +} + +function triggerPrefetch(nextText: string, voiceName: string) { + if (!nextText || prefetchCache.has(nextText) || activeSynthesisPromises.has(nextText)) return; + getOfflineSpeech(nextText, voiceName).catch((err) => { + console.warn("[TTS Prefetch] Failed to pre-fetch next sentence:", err); + }); +} // Speech sanitation helper to strip markdown, inline code, bold/italics, blockquotes, list markers export function sanitizeBlockForSpeech(text: string): string { @@ -47,19 +106,171 @@ export function sanitizeBlockForSpeech(text: string): string { return clean.trim(); } +export const DEFAULT_KOKORO_VOICE = "af_heart"; + +export const DEFAULT_PRONUNCIATIONS: Record = { + newline: "new line", + newlines: "new lines", + codeoba: "code oh bu", + src: "source", + tauri: "tor ee", + tts: "tee tee ess", + rs: "are ess", + js: "jay ess", + ts: "tee ess", + tsx: "tee ess ex", + jsx: "jay ess ex", + css: "see ess ess", + html: "aitch tee em ell", + json: "jay son", + toml: "ta mul", + yml: "yeh mul", + yaml: "yeh mul", + md: "em dee", + gb: "gee bee", + npm: "en pee em", +}; + +export function splitCamelCase(word: string): string[] { + const parts: string[] = []; + let current = ""; + for (let i = 0; i < word.length; i++) { + const ch = word[i]; + if (i > 0) { + const prev = word[i - 1]; + const next = word[i + 1]; + const isLetter = /[a-zA-Z]/.test(ch) && /[a-zA-Z]/.test(prev); + const isNewWord = + isLetter && + ((prev === prev.toLowerCase() && ch === ch.toUpperCase()) || + (prev === prev.toUpperCase() && + ch === ch.toUpperCase() && + next && + next === next.toLowerCase())); + if (isNewWord) { + if (current) { + parts.push(current); + current = ""; + } + } + } + current += ch; + } + if (current) { + parts.push(current); + } + return parts; +} + +export function expandAcronym(part: string): string { + if (part.length >= 2 && /^[A-Z]+$/.test(part)) { + return part.split("").join(" "); + } + return part; +} + +export function processToken(token: string): string { + const parts = token.split(/[-_]+/); + const result: string[] = []; + for (const part of parts) { + const subParts = splitCamelCase(part); + for (const subPart of subParts) { + const expanded = expandAcronym(subPart); + result.push(expanded); + } + } + return result.join(" "); +} + +export function applyPronunciations(text: string): string { + let rules: Record = {}; + try { + const saved = localStorage.getItem("codeoba-tts-pronunciations"); + if (saved) { + rules = JSON.parse(saved); + // Merge missing defaults + let changed = false; + for (const key of Object.keys(DEFAULT_PRONUNCIATIONS)) { + if (!(key in rules)) { + rules[key] = DEFAULT_PRONUNCIATIONS[key]; + changed = true; + } + } + if (changed) { + localStorage.setItem("codeoba-tts-pronunciations", JSON.stringify(rules)); + } + } else { + rules = DEFAULT_PRONUNCIATIONS; + localStorage.setItem("codeoba-tts-pronunciations", JSON.stringify(DEFAULT_PRONUNCIATIONS)); + } + } catch (e) { + rules = DEFAULT_PRONUNCIATIONS; + } + + let result = text; + const sortedKeys = Object.keys(rules).sort((a, b) => b.length - a.length); + + for (const key of sortedKeys) { + const replacement = rules[key]; + if (!key || typeof replacement !== "string") continue; + const escapedKey = key.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + const regex = new RegExp(`\\b${escapedKey}\\b`, "gi"); + result = result.replace(regex, replacement); + } + + let output = ""; + let token = ""; + for (let i = 0; i < result.length; i++) { + const ch = result[i]; + if (/[a-zA-Z0-9'’\-_]/.test(ch)) { + token += ch; + } else { + if (token) { + output += processToken(token); + token = ""; + } + output += ch; + } + } + if (token) { + output += processToken(token); + } + + return output; +} + // Split narrative text into logical block-level chunks (newlines and HTML tags) export function splitIntoLogicalBlocks(text: string): string[] { // First, strip multi-line markdown code blocks from the raw narrative texts let clean = text.replace(/```[\s\S]*?```/g, ""); - // Second, convert any HTML tags (e.g.
, ,

) to newlines - clean = clean.replace(/<[^>]+>/g, "\n"); + // Second, convert block-level layout HTML tags to newlines + clean = clean.replace(/<\/?(div|p|blockquote|pre|br)(?:\s+[^>]*)?>/gi, "\n"); + + // Third, expand angle-bracketed words/tags to their inner word (e.g.
  • -> li,
  • -> li,