├── .gitignore ├── .vscode └── extensions.json ├── README-zh.md ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── py ├── convert.py └── main.py ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── 64x64.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon-template.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── model │ ├── tokenizer-marian-base-en.json │ └── tokenizer-marian-base-zh.json ├── src │ ├── audio.rs │ ├── lib.rs │ ├── main.rs │ ├── translate.rs │ └── whisper.rs └── tauri.conf.json ├── src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── Lyrics.tsx │ ├── Settings.css │ └── Settings.tsx ├── main.tsx └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | __pycache__/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 |

2 | Vibe logo 7 |

8 | 9 |

Peeches

10 |

实时系统音频转录和翻译

11 | 12 |

13 | English | 中文 14 |

15 | 16 | ![Image](https://github.com/user-attachments/assets/b5b0692b-bde5-4c3f-8284-7545f0846333) 17 | 18 | # 功能 19 | 20 | - 🎙️ 实时转录系统音频 21 | - 💻 支持 macOS 和 Windows 22 | - 🤖 完全本地的 AI 模型 23 | - 🎵 歌词样式的文本显示 24 | - 🦀 用纯 Rust 编写 25 | - 🌐 目前仅支持英语到中文的翻译 26 | 27 | # 模型 28 | 29 | - whisper: https://huggingface.co/ggerganov/whisper.cpp 30 | - opus-mt-en-zh: https://huggingface.co/Helsinki-NLP/opus-mt-en-zh 31 | 32 | # 致谢 33 | 34 | - [tauri](https://tauri.app/): 使用 Web 前端构建更小、更快、更安全的桌面和移动应用程序。 35 | - [whisper-rs](https://github.com/tazz4843/whisper-rs): Rust 绑定到 https://github.com/ggerganov/whisper.cpp 36 | - [candle](https://github.com/huggingface/candle): Rust 的极简主义 ML 框架 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Vibe logo 7 |

8 | 9 |

Peeches

10 |

Real-time system audio transcription and translation

11 | 12 |

13 | English | 中文 14 |

15 | 16 | ![Image](https://github.com/user-attachments/assets/b5b0692b-bde5-4c3f-8284-7545f0846333) 17 | 18 | # Features 19 | 20 | - 🎙️ Transcribe system audio in real-time 21 | - 💻 Supports macOS and Windows 22 | - 🤖 Fully local AI model 23 | - 🎵 Lyrics-style text display 24 | - 🦀 Written in pure Rust 25 | - 🌐 Currently English to Chinese translation only 26 | 27 | # Model 28 | 29 | - whisper: https://huggingface.co/ggerganov/whisper.cpp 30 | - opus-mt-en-zh: https://huggingface.co/Helsinki-NLP/opus-mt-en-zh 31 | 32 | # Credits 33 | 34 | - [tauri](https://tauri.app/): Build smaller, faster, and more secure desktop and mobile applications with a web frontend. 35 | - [whisper-rs](https://github.com/tazz4843/whisper-rs): Rust bindings to https://github.com/ggerganov/whisper.cpp 36 | - [candle](https://github.com/huggingface/candle): Minimalist ML framework for Rust 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + React + Typescript 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "desktop-lyric", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@tauri-apps/api": "^2", 14 | "@tauri-apps/plugin-log": "~2", 15 | "@tauri-apps/plugin-shell": "^2", 16 | "@tauri-apps/plugin-store": "~2", 17 | "lucide-react": "^0.474.0", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-router-dom": "^7.1.5" 21 | }, 22 | "devDependencies": { 23 | "@tauri-apps/cli": "^2", 24 | "@types/react": "^18.2.15", 25 | "@types/react-dom": "^18.2.7", 26 | "@vitejs/plugin-react": "^4.2.1", 27 | "typescript": "^5.2.2", 28 | "vite": "^5.3.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@tauri-apps/api': 9 | specifier: ^2 10 | version: 2.2.0 11 | '@tauri-apps/plugin-log': 12 | specifier: ~2 13 | version: 2.2.1 14 | '@tauri-apps/plugin-shell': 15 | specifier: ^2 16 | version: 2.2.0 17 | '@tauri-apps/plugin-store': 18 | specifier: ~2 19 | version: 2.2.0 20 | lucide-react: 21 | specifier: ^0.474.0 22 | version: 0.474.0(react@18.3.1) 23 | react: 24 | specifier: ^18.2.0 25 | version: 18.3.1 26 | react-dom: 27 | specifier: ^18.2.0 28 | version: 18.3.1(react@18.3.1) 29 | react-router-dom: 30 | specifier: ^7.1.5 31 | version: 7.1.5(react-dom@18.3.1)(react@18.3.1) 32 | 33 | devDependencies: 34 | '@tauri-apps/cli': 35 | specifier: ^2 36 | version: 2.2.7 37 | '@types/react': 38 | specifier: ^18.2.15 39 | version: 18.3.18 40 | '@types/react-dom': 41 | specifier: ^18.2.7 42 | version: 18.3.5(@types/react@18.3.18) 43 | '@vitejs/plugin-react': 44 | specifier: ^4.2.1 45 | version: 4.3.4(vite@5.4.14) 46 | typescript: 47 | specifier: ^5.2.2 48 | version: 5.7.3 49 | vite: 50 | specifier: ^5.3.1 51 | version: 5.4.14 52 | 53 | packages: 54 | 55 | /@ampproject/remapping@2.3.0: 56 | resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 57 | engines: {node: '>=6.0.0'} 58 | dependencies: 59 | '@jridgewell/gen-mapping': 0.3.8 60 | '@jridgewell/trace-mapping': 0.3.25 61 | dev: true 62 | 63 | /@babel/code-frame@7.26.2: 64 | resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} 65 | engines: {node: '>=6.9.0'} 66 | dependencies: 67 | '@babel/helper-validator-identifier': 7.25.9 68 | js-tokens: 4.0.0 69 | picocolors: 1.1.1 70 | dev: true 71 | 72 | /@babel/compat-data@7.26.8: 73 | resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} 74 | engines: {node: '>=6.9.0'} 75 | dev: true 76 | 77 | /@babel/core@7.26.9: 78 | resolution: {integrity: sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==} 79 | engines: {node: '>=6.9.0'} 80 | dependencies: 81 | '@ampproject/remapping': 2.3.0 82 | '@babel/code-frame': 7.26.2 83 | '@babel/generator': 7.26.9 84 | '@babel/helper-compilation-targets': 7.26.5 85 | '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.9) 86 | '@babel/helpers': 7.26.9 87 | '@babel/parser': 7.26.9 88 | '@babel/template': 7.26.9 89 | '@babel/traverse': 7.26.9 90 | '@babel/types': 7.26.9 91 | convert-source-map: 2.0.0 92 | debug: 4.4.0 93 | gensync: 1.0.0-beta.2 94 | json5: 2.2.3 95 | semver: 6.3.1 96 | transitivePeerDependencies: 97 | - supports-color 98 | dev: true 99 | 100 | /@babel/generator@7.26.9: 101 | resolution: {integrity: sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==} 102 | engines: {node: '>=6.9.0'} 103 | dependencies: 104 | '@babel/parser': 7.26.9 105 | '@babel/types': 7.26.9 106 | '@jridgewell/gen-mapping': 0.3.8 107 | '@jridgewell/trace-mapping': 0.3.25 108 | jsesc: 3.1.0 109 | dev: true 110 | 111 | /@babel/helper-compilation-targets@7.26.5: 112 | resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} 113 | engines: {node: '>=6.9.0'} 114 | dependencies: 115 | '@babel/compat-data': 7.26.8 116 | '@babel/helper-validator-option': 7.25.9 117 | browserslist: 4.24.4 118 | lru-cache: 5.1.1 119 | semver: 6.3.1 120 | dev: true 121 | 122 | /@babel/helper-module-imports@7.25.9: 123 | resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} 124 | engines: {node: '>=6.9.0'} 125 | dependencies: 126 | '@babel/traverse': 7.26.9 127 | '@babel/types': 7.26.9 128 | transitivePeerDependencies: 129 | - supports-color 130 | dev: true 131 | 132 | /@babel/helper-module-transforms@7.26.0(@babel/core@7.26.9): 133 | resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} 134 | engines: {node: '>=6.9.0'} 135 | peerDependencies: 136 | '@babel/core': ^7.0.0 137 | dependencies: 138 | '@babel/core': 7.26.9 139 | '@babel/helper-module-imports': 7.25.9 140 | '@babel/helper-validator-identifier': 7.25.9 141 | '@babel/traverse': 7.26.9 142 | transitivePeerDependencies: 143 | - supports-color 144 | dev: true 145 | 146 | /@babel/helper-plugin-utils@7.26.5: 147 | resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} 148 | engines: {node: '>=6.9.0'} 149 | dev: true 150 | 151 | /@babel/helper-string-parser@7.25.9: 152 | resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} 153 | engines: {node: '>=6.9.0'} 154 | dev: true 155 | 156 | /@babel/helper-validator-identifier@7.25.9: 157 | resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} 158 | engines: {node: '>=6.9.0'} 159 | dev: true 160 | 161 | /@babel/helper-validator-option@7.25.9: 162 | resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} 163 | engines: {node: '>=6.9.0'} 164 | dev: true 165 | 166 | /@babel/helpers@7.26.9: 167 | resolution: {integrity: sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==} 168 | engines: {node: '>=6.9.0'} 169 | dependencies: 170 | '@babel/template': 7.26.9 171 | '@babel/types': 7.26.9 172 | dev: true 173 | 174 | /@babel/parser@7.26.9: 175 | resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} 176 | engines: {node: '>=6.0.0'} 177 | hasBin: true 178 | dependencies: 179 | '@babel/types': 7.26.9 180 | dev: true 181 | 182 | /@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.9): 183 | resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} 184 | engines: {node: '>=6.9.0'} 185 | peerDependencies: 186 | '@babel/core': ^7.0.0-0 187 | dependencies: 188 | '@babel/core': 7.26.9 189 | '@babel/helper-plugin-utils': 7.26.5 190 | dev: true 191 | 192 | /@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.9): 193 | resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} 194 | engines: {node: '>=6.9.0'} 195 | peerDependencies: 196 | '@babel/core': ^7.0.0-0 197 | dependencies: 198 | '@babel/core': 7.26.9 199 | '@babel/helper-plugin-utils': 7.26.5 200 | dev: true 201 | 202 | /@babel/template@7.26.9: 203 | resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} 204 | engines: {node: '>=6.9.0'} 205 | dependencies: 206 | '@babel/code-frame': 7.26.2 207 | '@babel/parser': 7.26.9 208 | '@babel/types': 7.26.9 209 | dev: true 210 | 211 | /@babel/traverse@7.26.9: 212 | resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} 213 | engines: {node: '>=6.9.0'} 214 | dependencies: 215 | '@babel/code-frame': 7.26.2 216 | '@babel/generator': 7.26.9 217 | '@babel/parser': 7.26.9 218 | '@babel/template': 7.26.9 219 | '@babel/types': 7.26.9 220 | debug: 4.4.0 221 | globals: 11.12.0 222 | transitivePeerDependencies: 223 | - supports-color 224 | dev: true 225 | 226 | /@babel/types@7.26.9: 227 | resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} 228 | engines: {node: '>=6.9.0'} 229 | dependencies: 230 | '@babel/helper-string-parser': 7.25.9 231 | '@babel/helper-validator-identifier': 7.25.9 232 | dev: true 233 | 234 | /@esbuild/aix-ppc64@0.21.5: 235 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 236 | engines: {node: '>=12'} 237 | cpu: [ppc64] 238 | os: [aix] 239 | requiresBuild: true 240 | dev: true 241 | optional: true 242 | 243 | /@esbuild/android-arm64@0.21.5: 244 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 245 | engines: {node: '>=12'} 246 | cpu: [arm64] 247 | os: [android] 248 | requiresBuild: true 249 | dev: true 250 | optional: true 251 | 252 | /@esbuild/android-arm@0.21.5: 253 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 254 | engines: {node: '>=12'} 255 | cpu: [arm] 256 | os: [android] 257 | requiresBuild: true 258 | dev: true 259 | optional: true 260 | 261 | /@esbuild/android-x64@0.21.5: 262 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 263 | engines: {node: '>=12'} 264 | cpu: [x64] 265 | os: [android] 266 | requiresBuild: true 267 | dev: true 268 | optional: true 269 | 270 | /@esbuild/darwin-arm64@0.21.5: 271 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 272 | engines: {node: '>=12'} 273 | cpu: [arm64] 274 | os: [darwin] 275 | requiresBuild: true 276 | dev: true 277 | optional: true 278 | 279 | /@esbuild/darwin-x64@0.21.5: 280 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 281 | engines: {node: '>=12'} 282 | cpu: [x64] 283 | os: [darwin] 284 | requiresBuild: true 285 | dev: true 286 | optional: true 287 | 288 | /@esbuild/freebsd-arm64@0.21.5: 289 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 290 | engines: {node: '>=12'} 291 | cpu: [arm64] 292 | os: [freebsd] 293 | requiresBuild: true 294 | dev: true 295 | optional: true 296 | 297 | /@esbuild/freebsd-x64@0.21.5: 298 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 299 | engines: {node: '>=12'} 300 | cpu: [x64] 301 | os: [freebsd] 302 | requiresBuild: true 303 | dev: true 304 | optional: true 305 | 306 | /@esbuild/linux-arm64@0.21.5: 307 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 308 | engines: {node: '>=12'} 309 | cpu: [arm64] 310 | os: [linux] 311 | requiresBuild: true 312 | dev: true 313 | optional: true 314 | 315 | /@esbuild/linux-arm@0.21.5: 316 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 317 | engines: {node: '>=12'} 318 | cpu: [arm] 319 | os: [linux] 320 | requiresBuild: true 321 | dev: true 322 | optional: true 323 | 324 | /@esbuild/linux-ia32@0.21.5: 325 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 326 | engines: {node: '>=12'} 327 | cpu: [ia32] 328 | os: [linux] 329 | requiresBuild: true 330 | dev: true 331 | optional: true 332 | 333 | /@esbuild/linux-loong64@0.21.5: 334 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 335 | engines: {node: '>=12'} 336 | cpu: [loong64] 337 | os: [linux] 338 | requiresBuild: true 339 | dev: true 340 | optional: true 341 | 342 | /@esbuild/linux-mips64el@0.21.5: 343 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 344 | engines: {node: '>=12'} 345 | cpu: [mips64el] 346 | os: [linux] 347 | requiresBuild: true 348 | dev: true 349 | optional: true 350 | 351 | /@esbuild/linux-ppc64@0.21.5: 352 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 353 | engines: {node: '>=12'} 354 | cpu: [ppc64] 355 | os: [linux] 356 | requiresBuild: true 357 | dev: true 358 | optional: true 359 | 360 | /@esbuild/linux-riscv64@0.21.5: 361 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 362 | engines: {node: '>=12'} 363 | cpu: [riscv64] 364 | os: [linux] 365 | requiresBuild: true 366 | dev: true 367 | optional: true 368 | 369 | /@esbuild/linux-s390x@0.21.5: 370 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 371 | engines: {node: '>=12'} 372 | cpu: [s390x] 373 | os: [linux] 374 | requiresBuild: true 375 | dev: true 376 | optional: true 377 | 378 | /@esbuild/linux-x64@0.21.5: 379 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 380 | engines: {node: '>=12'} 381 | cpu: [x64] 382 | os: [linux] 383 | requiresBuild: true 384 | dev: true 385 | optional: true 386 | 387 | /@esbuild/netbsd-x64@0.21.5: 388 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 389 | engines: {node: '>=12'} 390 | cpu: [x64] 391 | os: [netbsd] 392 | requiresBuild: true 393 | dev: true 394 | optional: true 395 | 396 | /@esbuild/openbsd-x64@0.21.5: 397 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 398 | engines: {node: '>=12'} 399 | cpu: [x64] 400 | os: [openbsd] 401 | requiresBuild: true 402 | dev: true 403 | optional: true 404 | 405 | /@esbuild/sunos-x64@0.21.5: 406 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 407 | engines: {node: '>=12'} 408 | cpu: [x64] 409 | os: [sunos] 410 | requiresBuild: true 411 | dev: true 412 | optional: true 413 | 414 | /@esbuild/win32-arm64@0.21.5: 415 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 416 | engines: {node: '>=12'} 417 | cpu: [arm64] 418 | os: [win32] 419 | requiresBuild: true 420 | dev: true 421 | optional: true 422 | 423 | /@esbuild/win32-ia32@0.21.5: 424 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 425 | engines: {node: '>=12'} 426 | cpu: [ia32] 427 | os: [win32] 428 | requiresBuild: true 429 | dev: true 430 | optional: true 431 | 432 | /@esbuild/win32-x64@0.21.5: 433 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 434 | engines: {node: '>=12'} 435 | cpu: [x64] 436 | os: [win32] 437 | requiresBuild: true 438 | dev: true 439 | optional: true 440 | 441 | /@jridgewell/gen-mapping@0.3.8: 442 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 443 | engines: {node: '>=6.0.0'} 444 | dependencies: 445 | '@jridgewell/set-array': 1.2.1 446 | '@jridgewell/sourcemap-codec': 1.5.0 447 | '@jridgewell/trace-mapping': 0.3.25 448 | dev: true 449 | 450 | /@jridgewell/resolve-uri@3.1.2: 451 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 452 | engines: {node: '>=6.0.0'} 453 | dev: true 454 | 455 | /@jridgewell/set-array@1.2.1: 456 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 457 | engines: {node: '>=6.0.0'} 458 | dev: true 459 | 460 | /@jridgewell/sourcemap-codec@1.5.0: 461 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 462 | dev: true 463 | 464 | /@jridgewell/trace-mapping@0.3.25: 465 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 466 | dependencies: 467 | '@jridgewell/resolve-uri': 3.1.2 468 | '@jridgewell/sourcemap-codec': 1.5.0 469 | dev: true 470 | 471 | /@rollup/rollup-android-arm-eabi@4.34.7: 472 | resolution: {integrity: sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==} 473 | cpu: [arm] 474 | os: [android] 475 | requiresBuild: true 476 | dev: true 477 | optional: true 478 | 479 | /@rollup/rollup-android-arm64@4.34.7: 480 | resolution: {integrity: sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==} 481 | cpu: [arm64] 482 | os: [android] 483 | requiresBuild: true 484 | dev: true 485 | optional: true 486 | 487 | /@rollup/rollup-darwin-arm64@4.34.7: 488 | resolution: {integrity: sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==} 489 | cpu: [arm64] 490 | os: [darwin] 491 | requiresBuild: true 492 | dev: true 493 | optional: true 494 | 495 | /@rollup/rollup-darwin-x64@4.34.7: 496 | resolution: {integrity: sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==} 497 | cpu: [x64] 498 | os: [darwin] 499 | requiresBuild: true 500 | dev: true 501 | optional: true 502 | 503 | /@rollup/rollup-freebsd-arm64@4.34.7: 504 | resolution: {integrity: sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==} 505 | cpu: [arm64] 506 | os: [freebsd] 507 | requiresBuild: true 508 | dev: true 509 | optional: true 510 | 511 | /@rollup/rollup-freebsd-x64@4.34.7: 512 | resolution: {integrity: sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==} 513 | cpu: [x64] 514 | os: [freebsd] 515 | requiresBuild: true 516 | dev: true 517 | optional: true 518 | 519 | /@rollup/rollup-linux-arm-gnueabihf@4.34.7: 520 | resolution: {integrity: sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==} 521 | cpu: [arm] 522 | os: [linux] 523 | requiresBuild: true 524 | dev: true 525 | optional: true 526 | 527 | /@rollup/rollup-linux-arm-musleabihf@4.34.7: 528 | resolution: {integrity: sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==} 529 | cpu: [arm] 530 | os: [linux] 531 | requiresBuild: true 532 | dev: true 533 | optional: true 534 | 535 | /@rollup/rollup-linux-arm64-gnu@4.34.7: 536 | resolution: {integrity: sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==} 537 | cpu: [arm64] 538 | os: [linux] 539 | requiresBuild: true 540 | dev: true 541 | optional: true 542 | 543 | /@rollup/rollup-linux-arm64-musl@4.34.7: 544 | resolution: {integrity: sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==} 545 | cpu: [arm64] 546 | os: [linux] 547 | requiresBuild: true 548 | dev: true 549 | optional: true 550 | 551 | /@rollup/rollup-linux-loongarch64-gnu@4.34.7: 552 | resolution: {integrity: sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==} 553 | cpu: [loong64] 554 | os: [linux] 555 | requiresBuild: true 556 | dev: true 557 | optional: true 558 | 559 | /@rollup/rollup-linux-powerpc64le-gnu@4.34.7: 560 | resolution: {integrity: sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==} 561 | cpu: [ppc64] 562 | os: [linux] 563 | requiresBuild: true 564 | dev: true 565 | optional: true 566 | 567 | /@rollup/rollup-linux-riscv64-gnu@4.34.7: 568 | resolution: {integrity: sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==} 569 | cpu: [riscv64] 570 | os: [linux] 571 | requiresBuild: true 572 | dev: true 573 | optional: true 574 | 575 | /@rollup/rollup-linux-s390x-gnu@4.34.7: 576 | resolution: {integrity: sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==} 577 | cpu: [s390x] 578 | os: [linux] 579 | requiresBuild: true 580 | dev: true 581 | optional: true 582 | 583 | /@rollup/rollup-linux-x64-gnu@4.34.7: 584 | resolution: {integrity: sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==} 585 | cpu: [x64] 586 | os: [linux] 587 | requiresBuild: true 588 | dev: true 589 | optional: true 590 | 591 | /@rollup/rollup-linux-x64-musl@4.34.7: 592 | resolution: {integrity: sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==} 593 | cpu: [x64] 594 | os: [linux] 595 | requiresBuild: true 596 | dev: true 597 | optional: true 598 | 599 | /@rollup/rollup-win32-arm64-msvc@4.34.7: 600 | resolution: {integrity: sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==} 601 | cpu: [arm64] 602 | os: [win32] 603 | requiresBuild: true 604 | dev: true 605 | optional: true 606 | 607 | /@rollup/rollup-win32-ia32-msvc@4.34.7: 608 | resolution: {integrity: sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==} 609 | cpu: [ia32] 610 | os: [win32] 611 | requiresBuild: true 612 | dev: true 613 | optional: true 614 | 615 | /@rollup/rollup-win32-x64-msvc@4.34.7: 616 | resolution: {integrity: sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==} 617 | cpu: [x64] 618 | os: [win32] 619 | requiresBuild: true 620 | dev: true 621 | optional: true 622 | 623 | /@tauri-apps/api@2.2.0: 624 | resolution: {integrity: sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==} 625 | dev: false 626 | 627 | /@tauri-apps/cli-darwin-arm64@2.2.7: 628 | resolution: {integrity: sha512-54kcpxZ3X1Rq+pPTzk3iIcjEVY4yv493uRx/80rLoAA95vAC0c//31Whz75UVddDjJfZvXlXZ3uSZ+bnCOnt0A==} 629 | engines: {node: '>= 10'} 630 | cpu: [arm64] 631 | os: [darwin] 632 | requiresBuild: true 633 | dev: true 634 | optional: true 635 | 636 | /@tauri-apps/cli-darwin-x64@2.2.7: 637 | resolution: {integrity: sha512-Vgu2XtBWemLnarB+6LqQeLanDlRj7CeFN//H8bVVdjbNzxcSxsvbLYMBP8+3boa7eBnjDrqMImRySSgL6IrwTw==} 638 | engines: {node: '>= 10'} 639 | cpu: [x64] 640 | os: [darwin] 641 | requiresBuild: true 642 | dev: true 643 | optional: true 644 | 645 | /@tauri-apps/cli-linux-arm-gnueabihf@2.2.7: 646 | resolution: {integrity: sha512-+Clha2iQAiK9zoY/KKW0KLHkR0k36O78YLx5Sl98tWkwI3OBZFg5H5WT1plH/4sbZIS2aLFN6dw58/JlY9Bu/g==} 647 | engines: {node: '>= 10'} 648 | cpu: [arm] 649 | os: [linux] 650 | requiresBuild: true 651 | dev: true 652 | optional: true 653 | 654 | /@tauri-apps/cli-linux-arm64-gnu@2.2.7: 655 | resolution: {integrity: sha512-Z/Lp4SQe6BUEOays9BQAEum2pvZF4w9igyXijP+WbkOejZx4cDvarFJ5qXrqSLmBh7vxrdZcLwoLk9U//+yQrg==} 656 | engines: {node: '>= 10'} 657 | cpu: [arm64] 658 | os: [linux] 659 | requiresBuild: true 660 | dev: true 661 | optional: true 662 | 663 | /@tauri-apps/cli-linux-arm64-musl@2.2.7: 664 | resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==} 665 | engines: {node: '>= 10'} 666 | cpu: [arm64] 667 | os: [linux] 668 | requiresBuild: true 669 | dev: true 670 | optional: true 671 | 672 | /@tauri-apps/cli-linux-x64-gnu@2.2.7: 673 | resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==} 674 | engines: {node: '>= 10'} 675 | cpu: [x64] 676 | os: [linux] 677 | requiresBuild: true 678 | dev: true 679 | optional: true 680 | 681 | /@tauri-apps/cli-linux-x64-musl@2.2.7: 682 | resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==} 683 | engines: {node: '>= 10'} 684 | cpu: [x64] 685 | os: [linux] 686 | requiresBuild: true 687 | dev: true 688 | optional: true 689 | 690 | /@tauri-apps/cli-win32-arm64-msvc@2.2.7: 691 | resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==} 692 | engines: {node: '>= 10'} 693 | cpu: [arm64] 694 | os: [win32] 695 | requiresBuild: true 696 | dev: true 697 | optional: true 698 | 699 | /@tauri-apps/cli-win32-ia32-msvc@2.2.7: 700 | resolution: {integrity: sha512-EiJ5/25tLSQOSGvv+t6o3ZBfOTKB5S3vb+hHQuKbfmKdRF0XQu2YPdIi1CQw1DU97ZAE0Dq4frvnyYEKWgMzVQ==} 701 | engines: {node: '>= 10'} 702 | cpu: [ia32] 703 | os: [win32] 704 | requiresBuild: true 705 | dev: true 706 | optional: true 707 | 708 | /@tauri-apps/cli-win32-x64-msvc@2.2.7: 709 | resolution: {integrity: sha512-ZB8Kw90j8Ld+9tCWyD2fWCYfIrzbQohJ4DJSidNwbnehlZzP7wAz6Z3xjsvUdKtQ3ibtfoeTqVInzCCEpI+pWg==} 710 | engines: {node: '>= 10'} 711 | cpu: [x64] 712 | os: [win32] 713 | requiresBuild: true 714 | dev: true 715 | optional: true 716 | 717 | /@tauri-apps/cli@2.2.7: 718 | resolution: {integrity: sha512-ZnsS2B4BplwXP37celanNANiIy8TCYhvg5RT09n72uR/o+navFZtGpFSqljV8fy1Y4ixIPds8FrGSXJCN2BerA==} 719 | engines: {node: '>= 10'} 720 | hasBin: true 721 | optionalDependencies: 722 | '@tauri-apps/cli-darwin-arm64': 2.2.7 723 | '@tauri-apps/cli-darwin-x64': 2.2.7 724 | '@tauri-apps/cli-linux-arm-gnueabihf': 2.2.7 725 | '@tauri-apps/cli-linux-arm64-gnu': 2.2.7 726 | '@tauri-apps/cli-linux-arm64-musl': 2.2.7 727 | '@tauri-apps/cli-linux-x64-gnu': 2.2.7 728 | '@tauri-apps/cli-linux-x64-musl': 2.2.7 729 | '@tauri-apps/cli-win32-arm64-msvc': 2.2.7 730 | '@tauri-apps/cli-win32-ia32-msvc': 2.2.7 731 | '@tauri-apps/cli-win32-x64-msvc': 2.2.7 732 | dev: true 733 | 734 | /@tauri-apps/plugin-log@2.2.1: 735 | resolution: {integrity: sha512-bOz9w0hhlXLGLc1ZR37GqkXvTqkykl4A3GEKLjRIs0dq3n0BzLyoRDMPcpt7PdUHqaq6WISME+zEX2bqjSbJ2A==} 736 | dependencies: 737 | '@tauri-apps/api': 2.2.0 738 | dev: false 739 | 740 | /@tauri-apps/plugin-shell@2.2.0: 741 | resolution: {integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==} 742 | dependencies: 743 | '@tauri-apps/api': 2.2.0 744 | dev: false 745 | 746 | /@tauri-apps/plugin-store@2.2.0: 747 | resolution: {integrity: sha512-hJTRtuJis4w5fW1dkcgftsYxKXK0+DbAqurZ3CURHG5WkAyyZgbxpeYctw12bbzF9ZbZREXZklPq8mocCC3Sgg==} 748 | dependencies: 749 | '@tauri-apps/api': 2.2.0 750 | dev: false 751 | 752 | /@types/babel__core@7.20.5: 753 | resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 754 | dependencies: 755 | '@babel/parser': 7.26.9 756 | '@babel/types': 7.26.9 757 | '@types/babel__generator': 7.6.8 758 | '@types/babel__template': 7.4.4 759 | '@types/babel__traverse': 7.20.6 760 | dev: true 761 | 762 | /@types/babel__generator@7.6.8: 763 | resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} 764 | dependencies: 765 | '@babel/types': 7.26.9 766 | dev: true 767 | 768 | /@types/babel__template@7.4.4: 769 | resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 770 | dependencies: 771 | '@babel/parser': 7.26.9 772 | '@babel/types': 7.26.9 773 | dev: true 774 | 775 | /@types/babel__traverse@7.20.6: 776 | resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} 777 | dependencies: 778 | '@babel/types': 7.26.9 779 | dev: true 780 | 781 | /@types/cookie@0.6.0: 782 | resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} 783 | dev: false 784 | 785 | /@types/estree@1.0.6: 786 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 787 | dev: true 788 | 789 | /@types/prop-types@15.7.14: 790 | resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} 791 | dev: true 792 | 793 | /@types/react-dom@18.3.5(@types/react@18.3.18): 794 | resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} 795 | peerDependencies: 796 | '@types/react': ^18.0.0 797 | dependencies: 798 | '@types/react': 18.3.18 799 | dev: true 800 | 801 | /@types/react@18.3.18: 802 | resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} 803 | dependencies: 804 | '@types/prop-types': 15.7.14 805 | csstype: 3.1.3 806 | dev: true 807 | 808 | /@vitejs/plugin-react@4.3.4(vite@5.4.14): 809 | resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} 810 | engines: {node: ^14.18.0 || >=16.0.0} 811 | peerDependencies: 812 | vite: ^4.2.0 || ^5.0.0 || ^6.0.0 813 | dependencies: 814 | '@babel/core': 7.26.9 815 | '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.9) 816 | '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) 817 | '@types/babel__core': 7.20.5 818 | react-refresh: 0.14.2 819 | vite: 5.4.14 820 | transitivePeerDependencies: 821 | - supports-color 822 | dev: true 823 | 824 | /browserslist@4.24.4: 825 | resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} 826 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 827 | hasBin: true 828 | dependencies: 829 | caniuse-lite: 1.0.30001699 830 | electron-to-chromium: 1.5.100 831 | node-releases: 2.0.19 832 | update-browserslist-db: 1.1.2(browserslist@4.24.4) 833 | dev: true 834 | 835 | /caniuse-lite@1.0.30001699: 836 | resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==} 837 | dev: true 838 | 839 | /convert-source-map@2.0.0: 840 | resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 841 | dev: true 842 | 843 | /cookie@1.0.2: 844 | resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} 845 | engines: {node: '>=18'} 846 | dev: false 847 | 848 | /csstype@3.1.3: 849 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 850 | dev: true 851 | 852 | /debug@4.4.0: 853 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 854 | engines: {node: '>=6.0'} 855 | peerDependencies: 856 | supports-color: '*' 857 | peerDependenciesMeta: 858 | supports-color: 859 | optional: true 860 | dependencies: 861 | ms: 2.1.3 862 | dev: true 863 | 864 | /electron-to-chromium@1.5.100: 865 | resolution: {integrity: sha512-u1z9VuzDXV86X2r3vAns0/5ojfXBue9o0+JDUDBKYqGLjxLkSqsSUoPU/6kW0gx76V44frHaf6Zo+QF74TQCMg==} 866 | dev: true 867 | 868 | /esbuild@0.21.5: 869 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 870 | engines: {node: '>=12'} 871 | hasBin: true 872 | requiresBuild: true 873 | optionalDependencies: 874 | '@esbuild/aix-ppc64': 0.21.5 875 | '@esbuild/android-arm': 0.21.5 876 | '@esbuild/android-arm64': 0.21.5 877 | '@esbuild/android-x64': 0.21.5 878 | '@esbuild/darwin-arm64': 0.21.5 879 | '@esbuild/darwin-x64': 0.21.5 880 | '@esbuild/freebsd-arm64': 0.21.5 881 | '@esbuild/freebsd-x64': 0.21.5 882 | '@esbuild/linux-arm': 0.21.5 883 | '@esbuild/linux-arm64': 0.21.5 884 | '@esbuild/linux-ia32': 0.21.5 885 | '@esbuild/linux-loong64': 0.21.5 886 | '@esbuild/linux-mips64el': 0.21.5 887 | '@esbuild/linux-ppc64': 0.21.5 888 | '@esbuild/linux-riscv64': 0.21.5 889 | '@esbuild/linux-s390x': 0.21.5 890 | '@esbuild/linux-x64': 0.21.5 891 | '@esbuild/netbsd-x64': 0.21.5 892 | '@esbuild/openbsd-x64': 0.21.5 893 | '@esbuild/sunos-x64': 0.21.5 894 | '@esbuild/win32-arm64': 0.21.5 895 | '@esbuild/win32-ia32': 0.21.5 896 | '@esbuild/win32-x64': 0.21.5 897 | dev: true 898 | 899 | /escalade@3.2.0: 900 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 901 | engines: {node: '>=6'} 902 | dev: true 903 | 904 | /fsevents@2.3.3: 905 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 906 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 907 | os: [darwin] 908 | requiresBuild: true 909 | dev: true 910 | optional: true 911 | 912 | /gensync@1.0.0-beta.2: 913 | resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 914 | engines: {node: '>=6.9.0'} 915 | dev: true 916 | 917 | /globals@11.12.0: 918 | resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 919 | engines: {node: '>=4'} 920 | dev: true 921 | 922 | /js-tokens@4.0.0: 923 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 924 | 925 | /jsesc@3.1.0: 926 | resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 927 | engines: {node: '>=6'} 928 | hasBin: true 929 | dev: true 930 | 931 | /json5@2.2.3: 932 | resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 933 | engines: {node: '>=6'} 934 | hasBin: true 935 | dev: true 936 | 937 | /loose-envify@1.4.0: 938 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 939 | hasBin: true 940 | dependencies: 941 | js-tokens: 4.0.0 942 | dev: false 943 | 944 | /lru-cache@5.1.1: 945 | resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 946 | dependencies: 947 | yallist: 3.1.1 948 | dev: true 949 | 950 | /lucide-react@0.474.0(react@18.3.1): 951 | resolution: {integrity: sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==} 952 | peerDependencies: 953 | react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 954 | dependencies: 955 | react: 18.3.1 956 | dev: false 957 | 958 | /ms@2.1.3: 959 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 960 | dev: true 961 | 962 | /nanoid@3.3.8: 963 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 964 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 965 | hasBin: true 966 | dev: true 967 | 968 | /node-releases@2.0.19: 969 | resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} 970 | dev: true 971 | 972 | /picocolors@1.1.1: 973 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 974 | dev: true 975 | 976 | /postcss@8.5.2: 977 | resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} 978 | engines: {node: ^10 || ^12 || >=14} 979 | dependencies: 980 | nanoid: 3.3.8 981 | picocolors: 1.1.1 982 | source-map-js: 1.2.1 983 | dev: true 984 | 985 | /react-dom@18.3.1(react@18.3.1): 986 | resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 987 | peerDependencies: 988 | react: ^18.3.1 989 | dependencies: 990 | loose-envify: 1.4.0 991 | react: 18.3.1 992 | scheduler: 0.23.2 993 | dev: false 994 | 995 | /react-refresh@0.14.2: 996 | resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} 997 | engines: {node: '>=0.10.0'} 998 | dev: true 999 | 1000 | /react-router-dom@7.1.5(react-dom@18.3.1)(react@18.3.1): 1001 | resolution: {integrity: sha512-/4f9+up0Qv92D3bB8iN5P1s3oHAepSGa9h5k6tpTFlixTTskJZwKGhJ6vRJ277tLD1zuaZTt95hyGWV1Z37csQ==} 1002 | engines: {node: '>=20.0.0'} 1003 | peerDependencies: 1004 | react: '>=18' 1005 | react-dom: '>=18' 1006 | dependencies: 1007 | react: 18.3.1 1008 | react-dom: 18.3.1(react@18.3.1) 1009 | react-router: 7.1.5(react-dom@18.3.1)(react@18.3.1) 1010 | dev: false 1011 | 1012 | /react-router@7.1.5(react-dom@18.3.1)(react@18.3.1): 1013 | resolution: {integrity: sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==} 1014 | engines: {node: '>=20.0.0'} 1015 | peerDependencies: 1016 | react: '>=18' 1017 | react-dom: '>=18' 1018 | peerDependenciesMeta: 1019 | react-dom: 1020 | optional: true 1021 | dependencies: 1022 | '@types/cookie': 0.6.0 1023 | cookie: 1.0.2 1024 | react: 18.3.1 1025 | react-dom: 18.3.1(react@18.3.1) 1026 | set-cookie-parser: 2.7.1 1027 | turbo-stream: 2.4.0 1028 | dev: false 1029 | 1030 | /react@18.3.1: 1031 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 1032 | engines: {node: '>=0.10.0'} 1033 | dependencies: 1034 | loose-envify: 1.4.0 1035 | dev: false 1036 | 1037 | /rollup@4.34.7: 1038 | resolution: {integrity: sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==} 1039 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1040 | hasBin: true 1041 | dependencies: 1042 | '@types/estree': 1.0.6 1043 | optionalDependencies: 1044 | '@rollup/rollup-android-arm-eabi': 4.34.7 1045 | '@rollup/rollup-android-arm64': 4.34.7 1046 | '@rollup/rollup-darwin-arm64': 4.34.7 1047 | '@rollup/rollup-darwin-x64': 4.34.7 1048 | '@rollup/rollup-freebsd-arm64': 4.34.7 1049 | '@rollup/rollup-freebsd-x64': 4.34.7 1050 | '@rollup/rollup-linux-arm-gnueabihf': 4.34.7 1051 | '@rollup/rollup-linux-arm-musleabihf': 4.34.7 1052 | '@rollup/rollup-linux-arm64-gnu': 4.34.7 1053 | '@rollup/rollup-linux-arm64-musl': 4.34.7 1054 | '@rollup/rollup-linux-loongarch64-gnu': 4.34.7 1055 | '@rollup/rollup-linux-powerpc64le-gnu': 4.34.7 1056 | '@rollup/rollup-linux-riscv64-gnu': 4.34.7 1057 | '@rollup/rollup-linux-s390x-gnu': 4.34.7 1058 | '@rollup/rollup-linux-x64-gnu': 4.34.7 1059 | '@rollup/rollup-linux-x64-musl': 4.34.7 1060 | '@rollup/rollup-win32-arm64-msvc': 4.34.7 1061 | '@rollup/rollup-win32-ia32-msvc': 4.34.7 1062 | '@rollup/rollup-win32-x64-msvc': 4.34.7 1063 | fsevents: 2.3.3 1064 | dev: true 1065 | 1066 | /scheduler@0.23.2: 1067 | resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} 1068 | dependencies: 1069 | loose-envify: 1.4.0 1070 | dev: false 1071 | 1072 | /semver@6.3.1: 1073 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1074 | hasBin: true 1075 | dev: true 1076 | 1077 | /set-cookie-parser@2.7.1: 1078 | resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} 1079 | dev: false 1080 | 1081 | /source-map-js@1.2.1: 1082 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1083 | engines: {node: '>=0.10.0'} 1084 | dev: true 1085 | 1086 | /turbo-stream@2.4.0: 1087 | resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} 1088 | dev: false 1089 | 1090 | /typescript@5.7.3: 1091 | resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 1092 | engines: {node: '>=14.17'} 1093 | hasBin: true 1094 | dev: true 1095 | 1096 | /update-browserslist-db@1.1.2(browserslist@4.24.4): 1097 | resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} 1098 | hasBin: true 1099 | peerDependencies: 1100 | browserslist: '>= 4.21.0' 1101 | dependencies: 1102 | browserslist: 4.24.4 1103 | escalade: 3.2.0 1104 | picocolors: 1.1.1 1105 | dev: true 1106 | 1107 | /vite@5.4.14: 1108 | resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==} 1109 | engines: {node: ^18.0.0 || >=20.0.0} 1110 | hasBin: true 1111 | peerDependencies: 1112 | '@types/node': ^18.0.0 || >=20.0.0 1113 | less: '*' 1114 | lightningcss: ^1.21.0 1115 | sass: '*' 1116 | sass-embedded: '*' 1117 | stylus: '*' 1118 | sugarss: '*' 1119 | terser: ^5.4.0 1120 | peerDependenciesMeta: 1121 | '@types/node': 1122 | optional: true 1123 | less: 1124 | optional: true 1125 | lightningcss: 1126 | optional: true 1127 | sass: 1128 | optional: true 1129 | sass-embedded: 1130 | optional: true 1131 | stylus: 1132 | optional: true 1133 | sugarss: 1134 | optional: true 1135 | terser: 1136 | optional: true 1137 | dependencies: 1138 | esbuild: 0.21.5 1139 | postcss: 8.5.2 1140 | rollup: 4.34.7 1141 | optionalDependencies: 1142 | fsevents: 2.3.3 1143 | dev: true 1144 | 1145 | /yallist@3.1.1: 1146 | resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1147 | dev: true 1148 | -------------------------------------------------------------------------------- /py/convert.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Copyright 2018 The HuggingFace Inc. team. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # https://github.com/huggingface/candle/blob/main/candle-examples/examples/marian-mt/convert_slow_tokenizer.py 16 | """ 17 | Utilities to convert slow tokenizers in their fast tokenizers counterparts. 18 | 19 | All the conversions are grouped here to gather SentencePiece dependencies outside of the fast tokenizers files and 20 | allow to make our dependency on SentencePiece optional. 21 | """ 22 | 23 | import warnings 24 | from typing import Dict, List, Tuple 25 | 26 | from packaging import version 27 | from pathlib import Path 28 | from tokenizers import AddedToken, Regex, Tokenizer, decoders, normalizers, pre_tokenizers, processors 29 | from tokenizers.models import BPE, Unigram, WordPiece 30 | 31 | from transformers.utils import is_protobuf_available, requires_backends 32 | from transformers.utils.import_utils import PROTOBUF_IMPORT_ERROR 33 | 34 | 35 | def import_protobuf(error_message=""): 36 | if is_protobuf_available(): 37 | import google.protobuf 38 | 39 | if version.parse(google.protobuf.__version__) < version.parse("4.0.0"): 40 | from transformers.utils import sentencepiece_model_pb2 41 | else: 42 | from transformers.utils import sentencepiece_model_pb2_new as sentencepiece_model_pb2 43 | return sentencepiece_model_pb2 44 | else: 45 | raise ImportError(PROTOBUF_IMPORT_ERROR.format(error_message)) 46 | 47 | def _get_prepend_scheme(add_prefix_space: bool, original_tokenizer) -> str: 48 | if add_prefix_space: 49 | prepend_scheme = "always" 50 | if hasattr(original_tokenizer, "legacy") and not original_tokenizer.legacy: 51 | prepend_scheme = "first" 52 | else: 53 | prepend_scheme = "never" 54 | return prepend_scheme 55 | 56 | class SentencePieceExtractor: 57 | """ 58 | Extractor implementation for SentencePiece trained models. https://github.com/google/sentencepiece 59 | """ 60 | 61 | def __init__(self, model: str): 62 | requires_backends(self, "sentencepiece") 63 | from sentencepiece import SentencePieceProcessor 64 | 65 | self.sp = SentencePieceProcessor() 66 | self.sp.Load(model) 67 | 68 | def extract(self, vocab_scores=None) -> Tuple[Dict[str, int], List[Tuple]]: 69 | """ 70 | By default will return vocab and merges with respect to their order, by sending `vocab_scores` we're going to 71 | order the merges with respect to the piece scores instead. 72 | """ 73 | sp = self.sp 74 | vocab = {sp.id_to_piece(index): index for index in range(sp.GetPieceSize())} 75 | if vocab_scores is not None: 76 | vocab_scores, reverse = dict(vocab_scores), True 77 | else: 78 | vocab_scores, reverse = vocab, False 79 | 80 | # Merges 81 | merges = [] 82 | for merge, piece_score in vocab_scores.items(): 83 | local = [] 84 | for index in range(1, len(merge)): 85 | piece_l, piece_r = merge[:index], merge[index:] 86 | if piece_l in vocab and piece_r in vocab: 87 | local.append((piece_l, piece_r, piece_score)) 88 | local = sorted(local, key=lambda x: (vocab[x[0]], vocab[x[1]])) 89 | merges.extend(local) 90 | 91 | merges = sorted(merges, key=lambda val: val[2], reverse=reverse) 92 | merges = [(val[0], val[1]) for val in merges] 93 | return vocab, merges 94 | 95 | 96 | def check_number_comma(piece: str) -> bool: 97 | return len(piece) < 2 or piece[-1] != "," or not piece[-2].isdigit() 98 | 99 | 100 | class Converter: 101 | def __init__(self, original_tokenizer): 102 | self.original_tokenizer = original_tokenizer 103 | 104 | def converted(self) -> Tokenizer: 105 | raise NotImplementedError() 106 | 107 | 108 | class SpmConverter(Converter): 109 | def __init__(self, *args): 110 | requires_backends(self, "protobuf") 111 | 112 | super().__init__(*args) 113 | 114 | # from .utils import sentencepiece_model_pb2 as model_pb2 115 | model_pb2 = import_protobuf() 116 | 117 | m = model_pb2.ModelProto() 118 | with open(self.original_tokenizer.vocab_file, "rb") as f: 119 | m.ParseFromString(f.read()) 120 | self.proto = m 121 | 122 | if self.proto.trainer_spec.byte_fallback: 123 | if not getattr(self, "handle_byte_fallback", None): 124 | warnings.warn( 125 | "The sentencepiece tokenizer that you are converting to a fast tokenizer uses the byte fallback option" 126 | " which is not implemented in the fast tokenizers. In practice this means that the fast version of the" 127 | " tokenizer can produce unknown tokens whereas the sentencepiece version would have converted these " 128 | "unknown tokens into a sequence of byte tokens matching the original piece of text." 129 | ) 130 | 131 | def vocab(self, proto): 132 | return [(piece.piece, piece.score) for piece in proto.pieces] 133 | 134 | def unk_id(self, proto): 135 | return proto.trainer_spec.unk_id 136 | 137 | def tokenizer(self, proto): 138 | model_type = proto.trainer_spec.model_type 139 | vocab_scores = self.vocab(proto) 140 | unk_id = self.unk_id(proto) 141 | 142 | if model_type == 1: 143 | tokenizer = Tokenizer(Unigram(vocab_scores, unk_id)) 144 | elif model_type == 2: 145 | _, merges = SentencePieceExtractor(self.original_tokenizer.vocab_file).extract() 146 | bpe_vocab = {word: i for i, (word, score) in enumerate(vocab_scores)} 147 | tokenizer = Tokenizer( 148 | BPE( 149 | bpe_vocab, 150 | merges, 151 | unk_token=proto.trainer_spec.unk_piece, 152 | fuse_unk=True, 153 | ) 154 | ) 155 | else: 156 | raise Exception( 157 | "You're trying to run a `Unigram` model but you're file was trained with a different algorithm" 158 | ) 159 | 160 | return tokenizer 161 | 162 | def normalizer(self, proto): 163 | precompiled_charsmap = proto.normalizer_spec.precompiled_charsmap 164 | if not precompiled_charsmap: 165 | return normalizers.Sequence([normalizers.Replace(Regex(" {2,}"), " ")]) 166 | else: 167 | return normalizers.Sequence( 168 | [normalizers.Precompiled(precompiled_charsmap), normalizers.Replace(Regex(" {2,}"), " ")] 169 | ) 170 | 171 | def pre_tokenizer(self, replacement, add_prefix_space): 172 | prepend_scheme = _get_prepend_scheme(add_prefix_space, self.original_tokenizer) 173 | return pre_tokenizers.Metaspace(replacement=replacement, prepend_scheme=prepend_scheme) 174 | 175 | def post_processor(self): 176 | return None 177 | 178 | def decoder(self, replacement, add_prefix_space): 179 | prepend_scheme = _get_prepend_scheme(add_prefix_space, self.original_tokenizer) 180 | return decoders.Metaspace(replacement=replacement, prepend_scheme=prepend_scheme) 181 | 182 | def converted(self) -> Tokenizer: 183 | tokenizer = self.tokenizer(self.proto) 184 | 185 | # Tokenizer assemble 186 | normalizer = self.normalizer(self.proto) 187 | if normalizer is not None: 188 | tokenizer.normalizer = normalizer 189 | 190 | replacement = "▁" 191 | add_prefix_space = True 192 | pre_tokenizer = self.pre_tokenizer(replacement, add_prefix_space) 193 | if pre_tokenizer is not None: 194 | tokenizer.pre_tokenizer = pre_tokenizer 195 | 196 | tokenizer.decoder = self.decoder(replacement, add_prefix_space) 197 | post_processor = self.post_processor() 198 | if post_processor: 199 | tokenizer.post_processor = post_processor 200 | 201 | return tokenizer 202 | 203 | class MarianConverter(SpmConverter): 204 | def __init__(self, *args, index: int = 0): 205 | requires_backends(self, "protobuf") 206 | 207 | super(SpmConverter, self).__init__(*args) 208 | 209 | # from .utils import sentencepiece_model_pb2 as model_pb2 210 | model_pb2 = import_protobuf() 211 | 212 | m = model_pb2.ModelProto() 213 | print(self.original_tokenizer.spm_files) 214 | with open(self.original_tokenizer.spm_files[index], "rb") as f: 215 | m.ParseFromString(f.read()) 216 | self.proto = m 217 | print(self.original_tokenizer) 218 | #with open(self.original_tokenizer.vocab_path, "r") as f: 219 | dir_path = Path(self.original_tokenizer.spm_files[0]).parents[0] 220 | with open(dir_path / "vocab.json", "r") as f: 221 | import json 222 | self._vocab = json.load(f) 223 | 224 | if self.proto.trainer_spec.byte_fallback: 225 | if not getattr(self, "handle_byte_fallback", None): 226 | warnings.warn( 227 | "The sentencepiece tokenizer that you are converting to a fast tokenizer uses the byte fallback option" 228 | " which is not implemented in the fast tokenizers. In practice this means that the fast version of the" 229 | " tokenizer can produce unknown tokens whereas the sentencepiece version would have converted these " 230 | "unknown tokens into a sequence of byte tokens matching the original piece of text." 231 | ) 232 | 233 | def vocab(self, proto): 234 | vocab_size = max(self._vocab.values()) + 1 235 | vocab = [("", -100) for _ in range(vocab_size)] 236 | for piece in proto.pieces: 237 | try: 238 | index = self._vocab[piece.piece] 239 | except Exception: 240 | print(f"Ignored missing piece {piece.piece}") 241 | vocab[index] = (piece.piece, piece.score) 242 | return vocab 243 | -------------------------------------------------------------------------------- /py/main.py: -------------------------------------------------------------------------------- 1 | from convert import MarianConverter 2 | from transformers import AutoTokenizer 3 | 4 | 5 | tokenizer = AutoTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-zh", use_fast=False) 6 | fast_tokenizer = MarianConverter(tokenizer, index=0).converted() 7 | fast_tokenizer.save(f"tokenizer-marian-base-en.json") 8 | fast_tokenizer = MarianConverter(tokenizer, index=1).converted() 9 | fast_tokenizer.save(f"tokenizer-marian-base-zh.json") -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "peeches" 3 | version = "0.2.0" 4 | description = "Real-time system audio whisper and translation" 5 | authors = ["leon7hao"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "peeches_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [profile.release] 18 | codegen-units = 1 19 | lto = true 20 | opt-level = "s" 21 | panic = "abort" 22 | strip = true 23 | 24 | [build-dependencies] 25 | tauri-build = { version = "2", features = [] } 26 | 27 | [target.'cfg(target_os = "macos")'.dependencies] 28 | cidre = { git = "https://github.com/yury/cidre.git", branch = "main", default-features = false, features = [ 29 | "sc", 30 | "dispatch", 31 | "av", 32 | "cv", 33 | "async", 34 | "macos_15_0", 35 | ] } 36 | tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } 37 | candle-core = { version = "0.8.2", features = ["metal"] } 38 | candle-transformers = { version = "0.8.2", features = ["metal"] } 39 | candle-nn = { version = "0.8.2", features = ["metal"] } 40 | whisper-rs = { git="https://github.com/Leeeon233/whisper-rs.git", features = ["metal"] } 41 | 42 | [target.'cfg(target_os = "windows")'.dependencies] 43 | cpal = "^0.15.3" 44 | candle-core = { version = "0.8.2", features = ["cuda"] } 45 | candle-transformers = { version = "0.8.2", features = ["cuda"] } 46 | candle-nn = { version = "0.8.2", features = ["cuda"] } 47 | whisper-rs = { git="https://github.com/Leeeon233/whisper-rs.git", features = ["cuda"] } 48 | 49 | [patch.crates-io] 50 | esaxx-rs = { git = "https://github.com/thewh1teagle/esaxx-rs.git", branch = "feat/dynamic-msvc-link" } 51 | 52 | [dependencies] 53 | tauri = { version = "2", features = [ 54 | "macos-private-api", 55 | "tray-icon", 56 | "unstable", 57 | ] } 58 | serde = { version = "1", features = ["derive"] } 59 | serde_json = "1" 60 | tokio = { version = "1", features = ["full"] } 61 | anyhow = "1.0" 62 | ringbuffer = "0.15.0" 63 | # vad-rs = "0.1.5" 64 | samplerate = "0.2.4" 65 | futures = "^0.3" 66 | tokenizers = { version = "0.21" } 67 | tauri-plugin-log = "2" 68 | tauri-plugin-store = "2" 69 | reqwest = { version = "0.11", features = ["json", "stream"] } 70 | futures-util = "0.3" 71 | log = "^0.4" 72 | 73 | # https://github.com/tazz4843/whisper-rs/blob/master/BUILDING.md 74 | [target.aarch64-apple-darwin] 75 | rustflags = "-lc++ -l framework=Accelerate" 76 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main", 7 | "settings" 8 | ], 9 | "permissions": [ 10 | "core:default", 11 | "core:window:allow-start-dragging", 12 | "core:window:allow-set-ignore-cursor-events", 13 | "log:default", 14 | "store:default" 15 | ] 16 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/64x64.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/icon-template.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Leeeon233/peeches/c934a2a4b70ee6214d8fe57df8b4937dff7bcd12/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/audio.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicUsize, Ordering}, 3 | mpsc, Arc, Mutex, 4 | }; 5 | 6 | use ringbuffer::{AllocRingBuffer, RingBuffer}; 7 | 8 | pub struct AudioOutput { 9 | #[cfg(target_os = "macos")] 10 | inner: macos::MacAudioOutput, 11 | #[cfg(target_os = "windows")] 12 | inner: win::WinAudioOutput, 13 | } 14 | 15 | unsafe impl Send for AudioOutput {} 16 | unsafe impl Sync for AudioOutput {} 17 | 18 | impl AudioOutput { 19 | pub fn new(sender: mpsc::Sender>) -> anyhow::Result { 20 | let speech_buf = Arc::new(Mutex::new(AllocRingBuffer::new(16000 * 3))); 21 | let counter = Arc::new(AtomicUsize::new(0)); 22 | let cb = Box::new(move |data| { 23 | let mut buf = speech_buf.lock().unwrap(); 24 | buf.extend(data); 25 | counter.fetch_add(1, Ordering::SeqCst); 26 | if counter.load(Ordering::SeqCst) > (16000.0 / 320.0 * 0.6) as usize 27 | && buf.len() as f64 > 1.1 * 16000.0 28 | { 29 | let samples = buf.to_vec(); 30 | drop(buf); 31 | sender.send(samples).unwrap(); 32 | counter.store(0, Ordering::SeqCst); 33 | } 34 | }); 35 | #[cfg(target_os = "macos")] 36 | { 37 | Ok(Self { 38 | inner: macos::MacAudioOutput::new(cb), 39 | }) 40 | } 41 | #[cfg(target_os = "windows")] 42 | { 43 | Ok(Self { 44 | inner: win::WinAudioOutput::new(cb)?, 45 | }) 46 | } 47 | } 48 | 49 | pub fn start_recording(&self) -> anyhow::Result<()> { 50 | self.inner.start_recording() 51 | } 52 | 53 | pub fn stop_recording(&self) { 54 | self.inner.stop_recording() 55 | } 56 | } 57 | 58 | #[cfg(target_os = "windows")] 59 | mod win { 60 | use std::sync::atomic::AtomicBool; 61 | use std::sync::atomic::Ordering; 62 | use std::sync::Arc; 63 | 64 | use cpal::traits::DeviceTrait; 65 | use cpal::traits::HostTrait; 66 | use cpal::traits::StreamTrait; 67 | 68 | use super::audio_resample; 69 | use super::stereo_to_mono; 70 | 71 | pub struct WinAudioOutput { 72 | stream: cpal::Stream, 73 | } 74 | 75 | impl WinAudioOutput { 76 | pub fn new(on_data: Box) + Send>) -> anyhow::Result { 77 | let err_fn = move |err| { 78 | eprintln!("an error occurred on stream: {}", err); 79 | }; 80 | let host = cpal::default_host(); 81 | let device = host.default_output_device().unwrap(); 82 | let config = device.default_output_config().unwrap(); 83 | let stream = match config.sample_format() { 84 | cpal::SampleFormat::F32 => device.build_input_stream::( 85 | &config.into(), 86 | move |data, _: &_| { 87 | // TODO: assume 2 channels 88 | let mut resampled: Vec = audio_resample(data, 48000, 16000, 2); 89 | resampled = stereo_to_mono(&resampled).unwrap(); 90 | on_data(resampled); 91 | }, 92 | err_fn, 93 | None, 94 | )?, 95 | sample_format => { 96 | return Err(anyhow::Error::msg(format!( 97 | "Unsupported sample format '{sample_format}'" 98 | ))) 99 | } 100 | }; 101 | Ok(WinAudioOutput { stream }) 102 | } 103 | 104 | pub fn start_recording(&self) -> anyhow::Result<()> { 105 | self.stream.play()?; 106 | 107 | Ok(()) 108 | } 109 | 110 | pub fn stop_recording(&self) { 111 | self.stream.pause(); 112 | } 113 | } 114 | } 115 | 116 | pub fn audio_resample( 117 | data: &[f32], 118 | sample_rate0: u32, 119 | sample_rate: u32, 120 | channels: u16, 121 | ) -> Vec { 122 | use samplerate::{convert, ConverterType}; 123 | convert( 124 | sample_rate0 as _, 125 | sample_rate as _, 126 | channels as _, 127 | ConverterType::SincBestQuality, 128 | data, 129 | ) 130 | .unwrap_or_default() 131 | } 132 | 133 | #[allow(dead_code)] 134 | pub fn stereo_to_mono(stereo_data: &[f32]) -> anyhow::Result> { 135 | let mut mono_data = Vec::with_capacity(stereo_data.len() / 2); 136 | 137 | for chunk in stereo_data.chunks_exact(2) { 138 | let average = (chunk[0] + chunk[1]) / 2.0; 139 | mono_data.push(average); 140 | } 141 | 142 | Ok(mono_data) 143 | } 144 | 145 | #[cfg(target_os = "macos")] 146 | mod macos { 147 | 148 | use cidre::{ 149 | arc::Retained, 150 | cm::{self}, 151 | define_obj_type, dispatch, objc, 152 | sc::{ 153 | self, 154 | stream::{Output, OutputImpl}, 155 | }, 156 | }; 157 | use futures::executor::block_on; 158 | 159 | use super::audio_resample; 160 | 161 | struct StreamOutputInner { 162 | on_data: Box) + Send>, 163 | } 164 | 165 | impl StreamOutputInner { 166 | fn handle_audio(&mut self, sample_buf: &mut cm::SampleBuf) { 167 | let audio_buf_list = sample_buf.audio_buf_list::<2>().unwrap(); 168 | let buffer_list = audio_buf_list.list(); 169 | let samples = unsafe { 170 | let buffer = buffer_list.buffers[0]; 171 | std::slice::from_raw_parts( 172 | buffer.data as *const f32, 173 | buffer.data_bytes_size as usize / std::mem::size_of::(), 174 | ) 175 | }; 176 | let resampled: Vec = audio_resample(samples, 48000, 16000, 1); 177 | (self.on_data)(resampled); 178 | } 179 | } 180 | 181 | define_obj_type!(StreamOutput + OutputImpl, StreamOutputInner, STREAM_OUTPUT); 182 | 183 | impl Output for StreamOutput {} 184 | #[objc::add_methods] 185 | impl OutputImpl for StreamOutput { 186 | extern "C" fn impl_stream_did_output_sample_buf( 187 | &mut self, 188 | _cmd: Option<&cidre::objc::Sel>, 189 | _stream: &sc::Stream, 190 | sample_buf: &mut cm::SampleBuf, 191 | kind: sc::OutputType, 192 | ) { 193 | match kind { 194 | sc::OutputType::Screen => {} 195 | sc::OutputType::Audio => self.inner_mut().handle_audio(sample_buf), 196 | sc::OutputType::Mic => {} 197 | } 198 | } 199 | } 200 | 201 | pub struct MacAudioOutput { 202 | _output: Retained, 203 | stream: Retained, 204 | } 205 | 206 | unsafe impl Send for MacAudioOutput {} 207 | unsafe impl Sync for MacAudioOutput {} 208 | 209 | impl MacAudioOutput { 210 | pub fn new(on_data: Box) + Send>) -> Self { 211 | let inner = StreamOutputInner { on_data }; 212 | let delegate = StreamOutput::with(inner); 213 | let content = block_on(sc::ShareableContent::current()).unwrap(); 214 | let displays = content.displays().clone(); 215 | let display = displays.first().expect("No display found"); 216 | let filter = sc::ContentFilter::with_display_excluding_windows( 217 | display, 218 | &cidre::ns::Array::new(), 219 | ); 220 | 221 | let queue = dispatch::Queue::serial_with_ar_pool(); 222 | let mut cfg = sc::StreamCfg::new(); 223 | cfg.set_captures_audio(true); 224 | cfg.set_excludes_current_process_audio(false); 225 | 226 | let stream = sc::Stream::new(&filter, &cfg); 227 | stream 228 | .add_stream_output(delegate.as_ref(), sc::OutputType::Audio, Some(&queue)) 229 | .expect("Failed to add stream output"); 230 | Self { 231 | _output: delegate, 232 | stream, 233 | } 234 | } 235 | 236 | pub fn start_recording(&self) -> anyhow::Result<()> { 237 | block_on(self.stream.start())?; 238 | log::info!("stream started"); 239 | Ok(()) 240 | } 241 | 242 | pub fn stop_recording(&self) { 243 | block_on(self.stream.stop()).unwrap(); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::PathBuf; 3 | use std::sync::mpsc; 4 | use std::{ 5 | fs, 6 | sync::{Arc, Mutex}, 7 | }; 8 | 9 | use audio::AudioOutput; 10 | use serde::{Deserialize, Serialize}; 11 | use tauri::{ 12 | menu::{Menu, MenuItem}, 13 | AppHandle, Emitter, Manager, WebviewWindowBuilder, 14 | }; 15 | use tauri_plugin_store::StoreExt as _; 16 | use translate::Translator; 17 | use whisper::Whisper; 18 | 19 | mod audio; 20 | mod translate; 21 | mod whisper; 22 | 23 | #[derive(Serialize, Deserialize, Clone)] 24 | struct ModelInfo { 25 | name: String, 26 | #[serde(rename = "fileName")] 27 | file_name: String, 28 | status: String, 29 | } 30 | 31 | #[derive(Clone)] 32 | struct AppState { 33 | audio_output: Arc>, 34 | whisper: Arc>>, 35 | translator: Arc>>, 36 | } 37 | 38 | impl AppState { 39 | pub fn new(app: AppHandle) -> anyhow::Result { 40 | let (audio_sender, audio_receiver) = mpsc::channel(); 41 | let (transcript_sender, transcript_receiver) = mpsc::channel(); 42 | let audio_output = AudioOutput::new(audio_sender)?; 43 | let whisper = Arc::new(Mutex::new(None::)); 44 | let whisper_arc = whisper.clone(); 45 | let translator = Arc::new(Mutex::new(None::)); 46 | let translator_arc = translator.clone(); 47 | 48 | std::thread::spawn(move || { 49 | while let Ok(samples) = audio_receiver.recv() { 50 | let mut whisper = whisper_arc.lock().unwrap(); 51 | if whisper.is_none() { 52 | continue; 53 | } 54 | let text = whisper.as_mut().unwrap().transcribe(samples).unwrap(); 55 | transcript_sender.send(text).unwrap(); 56 | } 57 | }); 58 | 59 | std::thread::spawn(move || { 60 | while let Ok(text) = transcript_receiver.recv() { 61 | let mut translator = translator_arc.lock().unwrap(); 62 | if translator.is_none() { 63 | continue; 64 | } 65 | let translated_text = translator.as_mut().unwrap().translate(&text).unwrap(); 66 | if text == " [BLANK_AUDIO]" { 67 | app.emit( 68 | "event", 69 | Event { 70 | original_text: "BLANK_AUDIO".to_string(), 71 | translated_text: "空白".to_string(), 72 | }, 73 | ) 74 | .unwrap(); 75 | continue; 76 | } 77 | log::debug!("original_text: {}", text); 78 | log::debug!("translated_text: {}", translated_text); 79 | app.emit( 80 | "event", 81 | Event { 82 | original_text: text, 83 | translated_text, 84 | }, 85 | ) 86 | .unwrap(); 87 | } 88 | }); 89 | 90 | Ok(Self { 91 | audio_output: Arc::new(Mutex::new(audio_output)), 92 | whisper, 93 | translator, 94 | }) 95 | } 96 | 97 | fn is_ready(&self) -> bool { 98 | self.whisper.lock().unwrap().is_some() && self.translator.lock().unwrap().is_some() 99 | } 100 | 101 | fn set_model(&self, app: &AppHandle, file_name: &str) -> Result<(), String> { 102 | log::info!("create model: {}", file_name); 103 | match file_name { 104 | "ggml-base-q5_1.bin" => { 105 | self.whisper 106 | .lock() 107 | .unwrap() 108 | .replace(Self::create_whisper(app, file_name)?); 109 | } 110 | "opus-mt-en-zh.bin" => { 111 | self.translator 112 | .lock() 113 | .unwrap() 114 | .replace(Self::create_translator(app, file_name)?); 115 | } 116 | _ => unreachable!(), 117 | } 118 | Ok(()) 119 | } 120 | 121 | fn create_whisper(app: &AppHandle, file_name: &str) -> Result { 122 | let model_dir = model_dir(app)?; 123 | let whisper = Whisper::new(model_dir.join(file_name).to_str().unwrap()); 124 | Ok(whisper) 125 | } 126 | 127 | fn create_translator(app: &AppHandle, file_name: &str) -> Result { 128 | let model_dir = model_dir(app)?; 129 | let (en_token, zh_token) = get_token_path(app); 130 | Translator::new( 131 | model_dir.join(file_name).to_str().unwrap(), 132 | en_token.to_str().unwrap(), 133 | zh_token.to_str().unwrap(), 134 | ) 135 | .map_err(|e| e.to_string()) 136 | } 137 | } 138 | 139 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 140 | #[tauri::command] 141 | fn close_app(app: AppHandle) -> Result<(), String> { 142 | app.exit(0); 143 | Ok(()) 144 | } 145 | 146 | #[derive(Serialize, Clone)] 147 | struct Event { 148 | #[serde(rename = "originalText")] 149 | original_text: String, 150 | #[serde(rename = "translatedText")] 151 | translated_text: String, 152 | } 153 | 154 | #[derive(Serialize, Clone)] 155 | pub struct DownloadProgress { 156 | #[serde(rename = "fileName")] 157 | file_name: String, 158 | progress: f32, 159 | total_size: u64, 160 | downloaded: u64, 161 | } 162 | 163 | #[tauri::command] 164 | async fn start_recording( 165 | app: AppHandle, 166 | state: tauri::State<'_, AppState>, 167 | ) -> Result { 168 | log::info!("start_recording"); 169 | if !state.is_ready() { 170 | open_settings(app).await?; 171 | return Ok(false); 172 | } 173 | app.emit( 174 | "event", 175 | Event { 176 | original_text: "wait for audio".to_string(), 177 | translated_text: "等待音频".to_string(), 178 | }, 179 | ) 180 | .unwrap(); 181 | 182 | state 183 | .audio_output 184 | .lock() 185 | .unwrap() 186 | .start_recording() 187 | .map_err(|e| e.to_string())?; 188 | Ok(true) 189 | } 190 | 191 | #[tauri::command] 192 | fn stop_recording(app: AppHandle, state: tauri::State<'_, AppState>) -> Result<(), String> { 193 | log::info!("stop_recording"); 194 | state.audio_output.lock().unwrap().stop_recording(); 195 | app.emit( 196 | "event", 197 | Event { 198 | original_text: "已暂停".to_string(), 199 | translated_text: "".to_string(), 200 | }, 201 | ) 202 | .unwrap(); 203 | Ok(()) 204 | } 205 | 206 | #[tauri::command] 207 | async fn open_settings(app: AppHandle) -> Result<(), String> { 208 | // Check if settings window already exists and focus it 209 | if let Some(settings_window) = app.get_webview_window("settings") { 210 | settings_window.set_focus().map_err(|e| e.to_string())?; 211 | Ok(()) 212 | } else { 213 | let mut builder = WebviewWindowBuilder::new( 214 | &app, 215 | "settings", 216 | tauri::WebviewUrl::App("/#/settings".into()), 217 | ) 218 | .inner_size(400.0, 300.0) 219 | .resizable(false) 220 | .title("Model Download") 221 | .center(); 222 | 223 | #[cfg(target_os = "macos")] 224 | { 225 | builder = builder 226 | .title_bar_style(tauri::TitleBarStyle::Overlay) 227 | .hidden_title(true); 228 | } 229 | 230 | let settings = builder.build(); 231 | match settings { 232 | Ok(_) => Ok(()), 233 | Err(e) => Err(e.to_string()), 234 | } 235 | } 236 | } 237 | 238 | #[tauri::command] 239 | async fn download_model( 240 | app: AppHandle, 241 | url: String, 242 | file_name: String, 243 | state: tauri::State<'_, AppState>, 244 | ) -> Result<(), String> { 245 | let model_dir = model_dir(&app)?; 246 | let file_path = model_dir.join(&file_name); 247 | log::info!("download model save to: {}", file_path.to_str().unwrap()); 248 | let mut total_size = 0; 249 | let mut downloaded = 0; 250 | 251 | if file_path.exists() { 252 | log::info!("model already exists: {}", file_path.to_str().unwrap()); 253 | // TODO: check file md5 254 | } else { 255 | // Download the file 256 | let response = reqwest::get(&url).await.map_err(|e| e.to_string())?; 257 | 258 | total_size = response 259 | .content_length() 260 | .ok_or_else(|| "Failed to get content length".to_string())?; 261 | let mut file = fs::File::create(&file_path).map_err(|e| e.to_string())?; 262 | let mut stream = response.bytes_stream(); 263 | 264 | use futures_util::StreamExt; 265 | let mut last_update = std::time::Instant::now(); 266 | while let Some(item) = stream.next().await { 267 | let chunk = item.map_err(|e| e.to_string())?; 268 | std::io::copy(&mut chunk.as_ref(), &mut file).map_err(|e| e.to_string())?; 269 | downloaded += chunk.len() as u64; 270 | 271 | // Only update progress every 200ms 272 | let now = std::time::Instant::now(); 273 | if now.duration_since(last_update).as_millis() >= 200 { 274 | let progress = (downloaded as f32 / total_size as f32) * 100.0; 275 | log::info!("download progress: {} %", progress); 276 | app.emit( 277 | "download-progress", 278 | DownloadProgress { 279 | file_name: file_name.clone(), 280 | progress, 281 | total_size, 282 | downloaded, 283 | }, 284 | ) 285 | .map_err(|e| e.to_string())?; 286 | last_update = now; 287 | } 288 | } 289 | } 290 | state.set_model(&app, &file_name)?; 291 | log::debug!("download model: {} completed", file_name); 292 | app.emit( 293 | "download-progress", 294 | DownloadProgress { 295 | file_name, 296 | progress: 100.0, 297 | total_size, 298 | downloaded, 299 | }, 300 | ) 301 | .map_err(|e| e.to_string())?; 302 | 303 | Ok(()) 304 | } 305 | 306 | fn get_token_path(app: &AppHandle) -> (PathBuf, PathBuf) { 307 | let resource_dir = app.path().resource_dir().unwrap(); 308 | let model_dir = resource_dir.join("model"); 309 | let en_token = model_dir.join("tokenizer-marian-base-en.json"); 310 | let zh_token = model_dir.join("tokenizer-marian-base-zh.json"); 311 | (en_token, zh_token) 312 | } 313 | 314 | fn model_dir(app: &AppHandle) -> Result { 315 | let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; 316 | let model_dir = app_dir.join("model"); 317 | fs::create_dir_all(&model_dir).map_err(|e| e.to_string())?; 318 | Ok(model_dir) 319 | } 320 | 321 | #[tauri::command] 322 | async fn show_main_window(window: tauri::Window) { 323 | window.get_window("main").unwrap().show().unwrap(); 324 | } 325 | 326 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 327 | pub fn run() { 328 | tauri::Builder::default() 329 | .plugin(tauri_plugin_log::Builder::new().build()) 330 | .plugin(tauri_plugin_store::Builder::new().build()) 331 | .plugin( 332 | tauri_plugin_log::Builder::new() 333 | .level(log::LevelFilter::Debug) 334 | .targets(vec![ 335 | tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview), 336 | tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), 337 | tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { 338 | file_name: Some("Peeches".to_string()), 339 | }) 340 | .filter(|meta| { 341 | matches!(meta.level(), log::Level::Info) 342 | || matches!(meta.level(), log::Level::Error) 343 | || matches!(meta.level(), log::Level::Warn) 344 | }), 345 | ]) 346 | .build(), 347 | ) 348 | .setup(|app| { 349 | if let Some(tray_icon) = app.tray_by_id("tray") { 350 | let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; 351 | let menu = Menu::with_items(app, &[&quit_i])?; 352 | tray_icon.set_menu(Some(menu))?; 353 | tray_icon.on_menu_event(|app, e| { 354 | if e.id() == "quit" { 355 | app.exit(0); 356 | } 357 | }); 358 | } 359 | // Get the main window 360 | let window = app.get_webview_window("main").unwrap(); 361 | #[cfg(target_os = "macos")] 362 | { 363 | // Hide dock icon 364 | app.set_activation_policy(tauri::ActivationPolicy::Accessory); 365 | } 366 | window.set_shadow(false)?; 367 | 368 | if let Some(monitor) = window.primary_monitor().unwrap() { 369 | window.set_min_size(Some(tauri::Size::Physical(tauri::PhysicalSize { 370 | width: 300 * monitor.scale_factor() as u32, 371 | height: 120 * monitor.scale_factor() as u32, 372 | })))?; 373 | let screen_size = monitor.size(); 374 | // Calculate window width (60% of screen width) 375 | let window_width = (screen_size.width as f64 * 0.4) as u32; 376 | 377 | // Calculate x position to center window 378 | let x = (screen_size.width - window_width) / 2; 379 | 380 | // Set window position at bottom center 381 | // Leave 20px margin from bottom 382 | let y = screen_size.height - (320_f64 * monitor.scale_factor()) as u32; 383 | // Update window size and position 384 | window 385 | .set_size(tauri::Size::Physical(tauri::PhysicalSize { 386 | width: window_width, 387 | height: (148_f64 * monitor.scale_factor()) as u32, 388 | })) 389 | .unwrap(); 390 | 391 | window 392 | .set_position(tauri::Position::Physical(tauri::PhysicalPosition { 393 | x: x as i32, 394 | y: y as i32, 395 | })) 396 | .unwrap(); 397 | } 398 | 399 | let app_state = AppState::new(app.handle().clone())?; 400 | 401 | let model_dir = model_dir(app.handle())?; 402 | 403 | // Get model store state 404 | let store = app.store("models.dat")?; 405 | let models = store 406 | .get("models") 407 | .unwrap_or(serde_json::Value::Array(vec![])); 408 | let mut models: HashMap = 409 | serde_json::from_value(models).unwrap_or_default(); 410 | 411 | if let Some(info) = models.get("ggml-base-q5_1.bin") { 412 | let model_path = model_dir.join(&info.file_name); 413 | if info.status == "completed" && model_path.exists() { 414 | let whisper = Whisper::new(model_path.to_str().unwrap()); 415 | app_state.whisper.lock().unwrap().replace(whisper); 416 | } else { 417 | models.remove("ggml-base-q5_1.bin"); 418 | store.set("models", serde_json::to_value(&models).unwrap()); 419 | } 420 | }; 421 | 422 | if let Some(info) = models.get("opus-mt-en-zh.bin") { 423 | let model_path = model_dir.join(&info.file_name); 424 | if info.status == "completed" && model_path.exists() { 425 | let (en_token, zh_token) = get_token_path(app.handle()); 426 | let translator = Translator::new( 427 | model_path.to_str().unwrap(), 428 | en_token.to_str().unwrap(), 429 | zh_token.to_str().unwrap(), 430 | )?; 431 | app_state.translator.lock().unwrap().replace(translator); 432 | } else { 433 | models.remove("opus-mt-en-zh.bin"); 434 | store.set("models", serde_json::to_value(&models).unwrap()); 435 | } 436 | }; 437 | app.manage(app_state); 438 | Ok(()) 439 | }) 440 | .invoke_handler(tauri::generate_handler![ 441 | close_app, 442 | start_recording, 443 | stop_recording, 444 | open_settings, 445 | download_model, 446 | show_main_window 447 | ]) 448 | .run(tauri::generate_context!()) 449 | .expect("error while running tauri application"); 450 | } 451 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | peeches_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/src/translate.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error as E; 2 | use candle_core::{DType, Tensor}; 3 | use candle_nn::VarBuilder; 4 | use candle_transformers::models::marian::{self, MTModel}; 5 | use tokenizers::Tokenizer; 6 | 7 | pub struct Translator { 8 | model: MTModel, 9 | config: marian::Config, 10 | tokenizer: Tokenizer, 11 | tokenizer_dec: Tokenizer, 12 | device: candle_core::Device, 13 | } 14 | 15 | impl Translator { 16 | pub fn new(model: &str, en_token: &str, zh_token: &str) -> anyhow::Result { 17 | let tokenizer = Tokenizer::from_file(en_token).map_err(E::msg)?; 18 | let tokenizer_dec = Tokenizer::from_file(zh_token).map_err(E::msg)?; 19 | // let tokenizer_dec = TokenOutputStream::new(tokenizer_dec); 20 | let device = if cfg!(target_os = "macos") { 21 | candle_core::Device::new_metal(0)? 22 | } else { 23 | candle_core::Device::new_cuda(0)? 24 | }; 25 | let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[&model], DType::F32, &device)? }; 26 | // https://huggingface.co/Helsinki-NLP/opus-mt-en-zh/blob/main/config.json 27 | let config = marian::Config { 28 | vocab_size: 65001, 29 | decoder_vocab_size: Some(65001), 30 | max_position_embeddings: 512, 31 | encoder_layers: 6, 32 | encoder_ffn_dim: 2048, 33 | encoder_attention_heads: 8, 34 | decoder_layers: 6, 35 | decoder_ffn_dim: 2048, 36 | decoder_attention_heads: 8, 37 | use_cache: true, 38 | is_encoder_decoder: true, 39 | activation_function: candle_nn::Activation::Swish, 40 | d_model: 512, 41 | decoder_start_token_id: 65000, 42 | scale_embedding: true, 43 | pad_token_id: 65000, 44 | eos_token_id: 0, 45 | forced_eos_token_id: 0, 46 | share_encoder_decoder_embeddings: true, 47 | }; 48 | let model = marian::MTModel::new(&config, vb)?; 49 | Ok(Self { 50 | model, 51 | config, 52 | tokenizer, 53 | tokenizer_dec, 54 | device, 55 | }) 56 | } 57 | 58 | pub fn translate(&mut self, text: &str) -> anyhow::Result { 59 | let mut logits_processor = 60 | candle_transformers::generation::LogitsProcessor::new(1337, None, None); 61 | let encoder_xs = { 62 | let mut tokens = self 63 | .tokenizer 64 | .encode(text, true) 65 | .map_err(E::msg)? 66 | .get_ids() 67 | .to_vec(); 68 | tokens.push(self.config.eos_token_id); 69 | let tokens = Tensor::new(tokens.as_slice(), &self.device)?.unsqueeze(0)?; 70 | self.model.encoder().forward(&tokens, 0)? 71 | }; 72 | let mut token_ids = vec![self.config.decoder_start_token_id]; 73 | for index in 0..1000 { 74 | let context_size = if index >= 1 { 1 } else { token_ids.len() }; 75 | let start_pos = token_ids.len().saturating_sub(context_size); 76 | let input_ids = Tensor::new(&token_ids[start_pos..], &self.device)?.unsqueeze(0)?; 77 | let logits = self.model.decode(&input_ids, &encoder_xs, start_pos)?; 78 | let logits = logits.squeeze(0)?; 79 | let logits = logits.get(logits.dim(0)? - 1)?; 80 | let token = logits_processor.sample(&logits)?; 81 | if token == self.config.eos_token_id || token == self.config.forced_eos_token_id { 82 | break; 83 | } 84 | token_ids.push(token); 85 | // if let Some(t) = self.tokenizer_dec.next_token(token)? { 86 | // ans.push_str(&t); 87 | // } 88 | } 89 | // if let Some(rest) = self.tokenizer_dec.decode_rest().map_err(E::msg)? { 90 | // ans.push_str(&rest); 91 | // } 92 | let ans = self 93 | .tokenizer_dec 94 | .decode(&token_ids[1..], true) 95 | .map_err(E::msg)?; 96 | self.model.reset_kv_cache(); 97 | Ok(ans) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src-tauri/src/whisper.rs: -------------------------------------------------------------------------------- 1 | // https://github.com/thewh1teagle/vad-rs/blob/main/examples/whisper/src/main.rs 2 | 3 | use whisper_rs::{ 4 | FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters, WhisperState, 5 | }; 6 | 7 | pub struct Whisper { 8 | // vad: Arc>, 9 | whisper_ctx: WhisperState, 10 | // normalizer: Arc>, 11 | } 12 | 13 | impl Whisper { 14 | pub fn new(whisper_model_path: &str) -> Self { 15 | // let vad = Vad::new(vad_model_path, 16000).unwrap(); 16 | // let normalizer = Normalizer::new(1, 16000); 17 | let ctx = WhisperContext::new_with_params( 18 | whisper_model_path, 19 | WhisperContextParameters { 20 | use_gpu: true, 21 | flash_attn: false, 22 | ..Default::default() 23 | }, 24 | ) 25 | .unwrap(); 26 | let state = ctx.create_state().expect("failed to create key"); 27 | 28 | Self { 29 | // vad: Arc::new(Mutex::new(vad)), 30 | whisper_ctx: state, 31 | // params: Arc::new(Mutex::new(params)), 32 | // normalizer: Arc::new(Mutex::new(normalizer)), 33 | } 34 | } 35 | 36 | pub fn transcribe(&mut self, samples: Vec) -> anyhow::Result { 37 | let mut params = FullParams::new(SamplingStrategy::default()); 38 | params.set_print_progress(false); 39 | params.set_print_realtime(false); 40 | params.set_print_special(false); 41 | params.set_print_timestamps(false); 42 | params.set_debug_mode(false); 43 | params.set_language(Some("en")); 44 | // params.set_duration_ms(3000); 45 | params.set_logprob_thold(-2.0); 46 | params.set_temperature(0.0); 47 | self.whisper_ctx.full(params, &samples)?; 48 | let text = self.whisper_ctx.full_get_segment_text_lossy(0)?; 49 | Ok(text) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Peeches", 4 | "version": "0.2.0", 5 | "identifier": "Peeches", 6 | "build": { 7 | "beforeDevCommand": "pnpm dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "pnpm build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "macOSPrivateApi": true, 14 | "windows": [ 15 | { 16 | "fullscreen": false, 17 | "resizable": true, 18 | "transparent": true, 19 | "alwaysOnTop": true, 20 | "decorations": false, 21 | "acceptFirstMouse": true, 22 | "visible": false 23 | } 24 | ], 25 | "security": { 26 | "capabilities": [ 27 | "default" 28 | ], 29 | "csp": null 30 | }, 31 | "trayIcon": { 32 | "iconAsTemplate": false, 33 | "iconPath": "./icons/icon-template.png", 34 | "id": "tray" 35 | } 36 | }, 37 | "bundle": { 38 | "active": true, 39 | "targets": "all", 40 | "icon": [ 41 | "icons/32x32.png", 42 | "icons/128x128.png", 43 | "icons/128x128@2x.png", 44 | "icons/icon.icns", 45 | "icons/icon.ico" 46 | ], 47 | "resources": [ 48 | "model/*" 49 | ] 50 | } 51 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | font-synthesis: none; 8 | text-rendering: optimizeLegibility; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-text-size-adjust: 100%; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | padding: 0; 17 | background: transparent !important; 18 | overflow: hidden; 19 | } 20 | 21 | a { 22 | font-weight: 500; 23 | color: #646cff; 24 | text-decoration: inherit; 25 | } 26 | 27 | a:hover { 28 | color: #535bf2; 29 | } 30 | 31 | h1 { 32 | text-align: center; 33 | } 34 | 35 | input, 36 | button { 37 | border-radius: 8px; 38 | border: 1px solid transparent; 39 | padding: 0.6em 1.2em; 40 | font-size: 1em; 41 | font-weight: 500; 42 | font-family: inherit; 43 | color: #0f0f0f; 44 | background-color: #ffffff; 45 | transition: border-color 0.25s; 46 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 47 | } 48 | 49 | button { 50 | cursor: pointer; 51 | } 52 | 53 | button:hover { 54 | border-color: #396cd8; 55 | } 56 | button:active { 57 | border-color: #396cd8; 58 | background-color: #e8e8e8; 59 | } 60 | 61 | input, 62 | button { 63 | outline: none; 64 | } 65 | 66 | #greet-input { 67 | margin-right: 5px; 68 | } 69 | 70 | .translation-container { 71 | display: flex; 72 | flex-direction: column; 73 | align-items: center; 74 | justify-content: center; 75 | min-height: 100vh; 76 | background: transparent; 77 | cursor: move; 78 | user-select: none; 79 | margin: 8px; 80 | -webkit-user-select: none; 81 | } 82 | 83 | .text-display { 84 | width: 100%; 85 | /* padding: 2rem; */ 86 | /* max-width: 800px; */ 87 | background: transparent; 88 | display: flex; 89 | flex-direction: column; 90 | gap: 0.5rem; 91 | transition: all 0.3s ease; 92 | position: relative; 93 | background: rgba(0, 0, 0, 0.1); 94 | border-radius: 12px; 95 | } 96 | 97 | .text-display.show-hover-bg { 98 | background: rgba(0, 0, 0, 0.3); 99 | backdrop-filter: blur(8px); 100 | border-radius: 12px; 101 | margin: 0; 102 | padding: 8px; 103 | } 104 | 105 | .original-text, 106 | .translated-text { 107 | font-size: 24px; 108 | line-height: 1.5; 109 | padding: 0.5rem; 110 | text-align: center; 111 | background: transparent; 112 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); 113 | transition: all 0.3s ease; 114 | opacity: 0.8; 115 | } 116 | 117 | .text-display.show-hover-bg .original-text, 118 | .text-display.show-hover-bg .translated-text { 119 | opacity: 1; 120 | } 121 | 122 | .original-text { 123 | color: #FFFFFF; 124 | font-weight: 500; 125 | } 126 | 127 | .translated-text { 128 | color: #00FFBB; 129 | font-weight: 400; 130 | } 131 | 132 | /* Add some basic animations */ 133 | /* .original-text, 134 | .translated-text { 135 | transition: all 0.3s ease; 136 | } */ 137 | 138 | .original-text:empty, 139 | .translated-text:empty { 140 | opacity: 0.5; 141 | } 142 | 143 | .button-group { 144 | position: absolute; 145 | top: 0.5rem; 146 | right: 1.5rem; 147 | display: flex; 148 | gap: 0.5rem; 149 | opacity: 0; 150 | transition: opacity 0.3s ease; 151 | pointer-events: none; 152 | z-index: 10; 153 | } 154 | 155 | .text-display.show-buttons .button-group { 156 | opacity: 1; 157 | pointer-events: all; 158 | } 159 | 160 | .button-group button { 161 | width: 32px; 162 | height: 32px; 163 | padding: 0; 164 | display: flex; 165 | align-items: center; 166 | justify-content: center; 167 | border-radius: 50%; 168 | background: rgba(0, 0, 0, 0.6); 169 | border: none; 170 | color: white; 171 | cursor: pointer; 172 | font-size: 16px; 173 | transition: all 0.2s ease; 174 | } 175 | 176 | .button-group button:hover { 177 | background: rgba(0, 0, 0, 0.8); 178 | /* transform: scale(1.1); */ 179 | } 180 | 181 | .button-group .pin-button.active { 182 | background: rgba(0, 150, 255, 0.8); 183 | } 184 | 185 | .button-group .close-button:hover { 186 | background: rgba(255, 0, 0, 0.8); 187 | } 188 | 189 | .settings-button { 190 | background: none; 191 | border: none; 192 | color: #666; 193 | cursor: pointer; 194 | padding: 4px; 195 | border-radius: 4px; 196 | display: flex; 197 | align-items: center; 198 | justify-content: center; 199 | transition: background-color 0.3s; 200 | } 201 | 202 | .settings-button:hover { 203 | background-color: rgba(0, 0, 0, 0.1); 204 | } 205 | 206 | .settings-overlay { 207 | position: fixed; 208 | top: 0; 209 | left: 0; 210 | right: 0; 211 | bottom: 0; 212 | background: rgba(0, 0, 0, 0.5); 213 | display: flex; 214 | align-items: center; 215 | justify-content: center; 216 | z-index: 1000; 217 | animation: fade-in 0.3s ease; 218 | } 219 | 220 | .settings-modal { 221 | background: white; 222 | border-radius: 8px; 223 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 224 | animation: slide-up 0.3s ease; 225 | } 226 | 227 | @keyframes fade-in { 228 | from { 229 | opacity: 0; 230 | } 231 | to { 232 | opacity: 1; 233 | } 234 | } 235 | 236 | @keyframes slide-up { 237 | from { 238 | transform: translateY(20px); 239 | opacity: 0; 240 | } 241 | to { 242 | transform: translateY(0); 243 | opacity: 1; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | import "./App.css"; 3 | import Lyrics from "./components/Lyrics"; 4 | import Settings from "./components/Settings"; 5 | 6 | function App() { 7 | return ( 8 | 9 | } /> 10 | } /> 11 | 12 | ); 13 | } 14 | 15 | export default App; 16 | 17 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Lyrics.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { listen } from "@tauri-apps/api/event"; 3 | import { invoke } from "@tauri-apps/api/core"; 4 | import { Pause, Pin, PinOff, Play, Settings, X } from "lucide-react"; 5 | 6 | type Event = { 7 | originalText: string; 8 | translatedText: string; 9 | }; 10 | 11 | function Lyrics() { 12 | const [originalText, setOriginalText] = useState(""); 13 | const [translatedText, setTranslatedText] = useState(""); 14 | const [isPinned, setIsPinned] = useState(false); 15 | const [isHovered, setIsHovered] = useState(false); 16 | const [isRecording, setIsRecording] = useState(false); 17 | 18 | useEffect(() => { 19 | invoke("show_main_window"); 20 | const unlisten = listen("event", (event) => { 21 | const { originalText, translatedText } = event.payload; 22 | setOriginalText(originalText); 23 | setTranslatedText(translatedText); 24 | }); 25 | 26 | return () => { 27 | unlisten.then((f) => f()); 28 | }; 29 | }, []); 30 | 31 | const handlePin = async () => { 32 | setIsPinned(!isPinned); 33 | }; 34 | 35 | const handleRecording = async () => { 36 | if (isRecording) { 37 | await invoke("stop_recording"); 38 | setOriginalText(""); 39 | setTranslatedText(""); 40 | setIsRecording(false); 41 | } else { 42 | if (await invoke("start_recording")) { 43 | setIsRecording(true); 44 | } 45 | } 46 | }; 47 | 48 | const handleMouseEnter = () => { 49 | setIsHovered(true); 50 | }; 51 | 52 | const handleMouseLeave = () => { 53 | setIsHovered(false); 54 | }; 55 | 56 | // 计算 CSS 类名 57 | const textDisplayClasses = [ 58 | "text-display", 59 | // 只在非固定状态下显示悬停背景 60 | !isPinned && isHovered ? "show-hover-bg" : "", 61 | // 只在鼠标悬停时显示按钮组 62 | isHovered ? "show-buttons" : "", 63 | ] 64 | .filter(Boolean) 65 | .join(" "); 66 | 67 | return ( 68 |
73 |
80 |
{ 83 | e.stopPropagation(); 84 | setIsHovered(true); 85 | }} 86 | onMouseLeave={(e) => { 87 | e.stopPropagation(); 88 | setIsHovered(false); 89 | }} 90 | > 91 | 100 | 107 | 115 | 124 |
125 |
129 | {originalText || "等待输入..."} 130 |
131 |
135 | {translatedText || "等待翻译..."} 136 |
137 |
138 |
139 | ); 140 | } 141 | 142 | export default Lyrics; 143 | -------------------------------------------------------------------------------- /src/components/Settings.css: -------------------------------------------------------------------------------- 1 | .settings-container { 2 | width: 400px; 3 | height: 300px; 4 | padding-top: 24px; 5 | background: rgba(15, 15, 15, 0.95); 6 | border-radius: 8px; 7 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 8 | overflow-y: auto; 9 | } 10 | 11 | .model-item { 12 | margin: 24px 16px; 13 | padding: 16px; 14 | border: 1px solid rgba(255, 255, 255, 0.1); 15 | border-radius: 6px; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | background: rgba(30, 30, 30, 0.5); 20 | } 21 | 22 | .model-info { 23 | flex: 1; 24 | margin-right: 16px; 25 | } 26 | 27 | .model-info h3 { 28 | margin: 0; 29 | font-size: 16px; 30 | color: #ffffff; 31 | } 32 | 33 | .model-description { 34 | margin: 4px 0; 35 | font-size: 14px; 36 | color: rgba(255, 255, 255, 0.7); 37 | } 38 | 39 | .error-message { 40 | color: #ff6b6b; 41 | font-size: 12px; 42 | margin: 4px 0 0; 43 | } 44 | 45 | .download-section { 46 | min-width: 80px; 47 | height: 32px; 48 | display: flex; 49 | align-items: center; 50 | justify-content: center; 51 | } 52 | 53 | .download-button { 54 | width: 100%; 55 | height: 100%; 56 | padding: 6px 16px; 57 | background: #646cff; 58 | color: white; 59 | border: none; 60 | border-radius: 4px; 61 | cursor: pointer; 62 | transition: background 0.3s; 63 | text-align: center; 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | } 68 | 69 | .download-button:hover { 70 | background: #535bf2; 71 | } 72 | 73 | .progress-wrapper { 74 | width: 100%; 75 | height: 100%; 76 | display: flex; 77 | flex-direction: column; 78 | align-items: center; 79 | justify-content: center; 80 | position: relative; 81 | } 82 | 83 | .progress-container { 84 | width: 100%; 85 | height: 4px; 86 | background: rgba(255, 255, 255, 0.1); 87 | border-radius: 2px; 88 | overflow: visible; 89 | } 90 | 91 | .progress-bar { 92 | height: 100%; 93 | background: #646cff; 94 | transition: width 0.3s ease; 95 | } 96 | 97 | .progress-text { 98 | font-size: 12px; 99 | color: rgba(255, 255, 255, 0.7); 100 | white-space: nowrap; 101 | } 102 | 103 | .check-mark { 104 | color: #00FFBB; 105 | font-size: 24px; 106 | animation: scale-in 0.3s ease; 107 | } 108 | 109 | @keyframes scale-in { 110 | 0% { 111 | transform: scale(0); 112 | opacity: 0; 113 | } 114 | 100% { 115 | transform: scale(1); 116 | opacity: 1; 117 | } 118 | } -------------------------------------------------------------------------------- /src/components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { invoke } from "@tauri-apps/api/core"; 3 | import { listen } from "@tauri-apps/api/event"; 4 | import "./Settings.css"; 5 | import { Store } from "@tauri-apps/plugin-store"; 6 | import { error as logError } from "@tauri-apps/plugin-log"; 7 | 8 | interface ModelInfo { 9 | name: string; 10 | fileName: string; 11 | description: string; 12 | status: "idle" | "downloading" | "completed" | "error"; 13 | url: string; 14 | progress: number; 15 | error?: string; 16 | } 17 | 18 | type ModelsRecord = Record; 19 | 20 | const defaultModels: ModelsRecord = { 21 | "ggml-base-q5_1.bin": { 22 | name: "转录模型", 23 | fileName: "ggml-base-q5_1.bin", 24 | description: "whisper ggml base-q5_1", 25 | status: "idle", 26 | progress: 0, 27 | url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base-q5_1.bin", 28 | }, 29 | "opus-mt-en-zh.bin": { 30 | name: "翻译模型", 31 | fileName: "opus-mt-en-zh.bin", 32 | description: "opus-mt-en-zh", 33 | status: "idle", 34 | progress: 0, 35 | url: "https://huggingface.co/Helsinki-NLP/opus-mt-en-zh/resolve/refs%2Fpr%2F26/model.safetensors", 36 | }, 37 | }; 38 | 39 | function Settings() { 40 | const [models, setModels] = useState({}); 41 | 42 | const listenDownloadProgress = async () => { 43 | const store = await Store.load("models.dat"); 44 | await listen("download-progress", async (event: any) => { 45 | const { progress, fileName } = event.payload; 46 | console.log("progress: ", progress, "fileName: ", fileName); 47 | setModels((prev) => { 48 | const newModels = { ...prev }; 49 | newModels[fileName] = { ...newModels[fileName], progress }; 50 | if (progress === 100) { 51 | newModels[fileName] = { 52 | ...newModels[fileName], 53 | status: "completed", 54 | }; 55 | store.set("models", newModels); 56 | } 57 | return newModels; 58 | }); 59 | }); 60 | }; 61 | 62 | useEffect(() => { 63 | let needUpdate = false; 64 | Store.load("models.dat").then((store) => { 65 | store.get("models").then((value) => { 66 | if (value) { 67 | Object.values(value).forEach((model) => { 68 | if (model.status !== "completed") { 69 | needUpdate = true; 70 | } 71 | }); 72 | if (needUpdate) { 73 | listenDownloadProgress(); 74 | } 75 | setModels({ ...defaultModels, ...value }); 76 | } else { 77 | setModels(defaultModels); 78 | listenDownloadProgress(); 79 | } 80 | }); 81 | }); 82 | }, []); 83 | 84 | const handleDownload = async (fileName: string) => { 85 | try { 86 | setModels((prev) => ({ 87 | ...prev, 88 | [fileName]: { 89 | ...prev[fileName], 90 | status: "downloading", 91 | progress: 0, 92 | error: undefined, 93 | }, 94 | })); 95 | 96 | // Start the download 97 | await invoke("download_model", { url: models[fileName].url, fileName }); 98 | } catch (error) { 99 | logError(`Download error: ${error}`); 100 | setModels((prev) => ({ 101 | ...prev, 102 | [fileName]: { 103 | ...prev[fileName], 104 | status: "error", 105 | error: "下载失败,请重试", 106 | }, 107 | })); 108 | } 109 | }; 110 | 111 | return ( 112 |
113 | {Object.values(models).map((model) => ( 114 |
115 |
116 |

{model.name}

117 |

{model.description}

118 | {model.error &&

{model.error}

} 119 |
120 |
121 | {(model.status === "idle" || model.status === "error") && ( 122 | 128 | )} 129 | {model.status === "downloading" && ( 130 |
140 |
141 |
145 |
146 | 147 | {model.progress.toFixed(2)}% 148 | 149 |
150 | )} 151 | {model.status === "completed" && ( 152 |
153 | )} 154 |
155 |
156 | ))} 157 |
158 | ); 159 | } 160 | 161 | export default Settings; 162 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { HashRouter } from "react-router-dom"; 4 | import App from "./App"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // @ts-expect-error process is a nodejs global 5 | const host = process.env.TAURI_DEV_HOST; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(async () => ({ 9 | plugins: [react()], 10 | 11 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 12 | // 13 | // 1. prevent vite from obscuring rust errors 14 | clearScreen: false, 15 | // 2. tauri expects a fixed port, fail if that port is not available 16 | server: { 17 | port: 1420, 18 | strictPort: true, 19 | host: host || false, 20 | hmr: host 21 | ? { 22 | protocol: "ws", 23 | host, 24 | port: 1421, 25 | } 26 | : undefined, 27 | watch: { 28 | // 3. tell vite to ignore watching `src-tauri` 29 | ignored: ["**/src-tauri/**"], 30 | }, 31 | }, 32 | })); 33 | --------------------------------------------------------------------------------