diff --git a/bun.lock b/bun.lock
index 09471d5a7642..88b84957c2e4 100644
--- a/bun.lock
+++ b/bun.lock
@@ -292,6 +292,7 @@
"@gitlab/opencode-gitlab-auth": "1.3.2",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
+ "@huggingface/transformers": "3.8.1",
"@modelcontextprotocol/sdk": "1.25.2",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
@@ -328,12 +329,14 @@
"opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
+ "sharp": "0.34.5",
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"tree-sitter-bash": "0.25.0",
"turndown": "7.2.0",
"ulid": "catalog:",
"vscode-jsonrpc": "8.2.1",
+ "wavefile": "11.0.0",
"web-tree-sitter": "0.25.10",
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
@@ -496,6 +499,7 @@
},
},
"trustedDependencies": [
+ "protobufjs",
"esbuild",
"web-tree-sitter",
"tree-sitter-bash",
@@ -506,6 +510,7 @@
"overrides": {
"@types/bun": "catalog:",
"@types/node": "catalog:",
+ "sharp": "0.34.5",
},
"catalog": {
"@cloudflare/workers-types": "4.20251008.0",
@@ -995,47 +1000,63 @@
"@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],
+ "@huggingface/jinja": ["@huggingface/jinja@0.5.5", "", {}, "sha512-xRlzazC+QZwr6z4ixEqYHo9fgwhTZ3xNSdljlKfUFGZSdlvt166DljRELFUfFytlYOYvo3vTisA/AFOuOAzFQQ=="],
+
+ "@huggingface/transformers": ["@huggingface/transformers@3.8.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.3", "onnxruntime-node": "1.21.0", "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", "sharp": "^0.34.1" } }, "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA=="],
+
"@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="],
"@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="],
- "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
+ "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
+
+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
+
+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
+
+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
+
+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
- "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
- "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
- "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
+ "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
- "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
+ "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
- "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
+ "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
- "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="],
+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
- "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
- "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
- "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
- "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
- "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
+ "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
- "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="],
+ "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
- "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
+ "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
- "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
- "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
- "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="],
+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
- "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="],
+ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
- "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
+ "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
+
+ "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
+
+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@internationalized/date": ["@internationalized/date@3.10.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA=="],
@@ -1433,6 +1454,26 @@
"@protobuf-ts/runtime-rpc": ["@protobuf-ts/runtime-rpc@2.11.1", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1" } }, "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ=="],
+ "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
+
+ "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
+
+ "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
+
+ "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
+
+ "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
+
+ "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
+
+ "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
+
+ "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
+
+ "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
+
+ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
+
"@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="],
@@ -2153,6 +2194,8 @@
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
+ "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
+
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
"bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="],
@@ -2263,14 +2306,10 @@
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
- "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
-
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
- "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
-
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
@@ -2375,6 +2414,8 @@
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
+ "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
+
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="],
@@ -2471,6 +2512,8 @@
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
+ "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
+
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
@@ -2579,6 +2622,8 @@
"finity": ["finity@0.5.4", "", {}, "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA=="],
+ "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="],
+
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
@@ -2665,6 +2710,8 @@
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
+ "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
+
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"globby": ["globby@11.0.4", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.1.1", "ignore": "^5.1.4", "merge2": "^1.3.0", "slash": "^3.0.0" } }, "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg=="],
@@ -2685,6 +2732,8 @@
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
+ "guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="],
+
"h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
"happy-dom": ["happy-dom@20.4.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^4.5.0", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-RDeQm3dT9n0A5f/TszjUmNCLEuPnMGv3Tv4BmNINebz/h17PA6LMBcxJ5FrcqltNBMh9jA/8ufgDdBYUdBt+eg=="],
@@ -2817,8 +2866,6 @@
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
- "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
-
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
@@ -2949,6 +2996,8 @@
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
+ "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
+
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
@@ -3057,6 +3106,8 @@
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
+ "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"md-to-react-email": ["md-to-react-email@5.0.0", "", { "dependencies": { "marked": "7.0.4" }, "peerDependencies": { "react": "18.x" } }, "sha512-GdBrBUbAAJHypnuyofYGfVos8oUslxHx69hs3CW9P0L8mS1sT6GnJuMBTlz/Fw+2widiwdavcu9UwyLF/BzZ4w=="],
@@ -3297,6 +3348,12 @@
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
+ "onnxruntime-common": ["onnxruntime-common@1.21.0", "", {}, "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ=="],
+
+ "onnxruntime-node": ["onnxruntime-node@1.21.0", "", { "dependencies": { "global-agent": "^3.0.0", "onnxruntime-common": "1.21.0", "tar": "^7.0.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw=="],
+
+ "onnxruntime-web": ["onnxruntime-web@1.22.0-dev.20250409-89f8206ba4", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ=="],
+
"open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="],
"openai": ["openai@5.11.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg=="],
@@ -3415,6 +3472,8 @@
"planck": ["planck@1.4.2", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew=="],
+ "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
+
"playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
"playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
@@ -3463,6 +3522,8 @@
"proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="],
+ "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
@@ -3603,6 +3664,8 @@
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
+ "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
+
"rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="],
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
@@ -3641,10 +3704,14 @@
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+ "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
+
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
+ "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
+
"seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="],
"seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="],
@@ -3661,7 +3728,7 @@
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
- "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
+ "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -3685,8 +3752,6 @@
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
- "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
-
"simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
@@ -4049,6 +4114,8 @@
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
+ "wavefile": ["wavefile@11.0.0", "", { "bin": { "wavefile": "./bin/wavefile.js" } }, "sha512-/OBiAALgWU24IG7sC84cDO/KfFuvajWc5Uec0oV2zrpOOZZDgGdOwHwgEzOrwh8jkubBk7PtZfQBIcI1OaE5Ng=="],
+
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
@@ -4559,6 +4626,8 @@
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+ "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
@@ -4579,6 +4648,8 @@
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
+ "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.22.0-dev.20250409-89f8206ba4", "", {}, "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ=="],
+
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
@@ -4625,6 +4696,8 @@
"rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+ "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
+
"router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@@ -4635,6 +4708,8 @@
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
+ "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
+
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
diff --git a/package.json b/package.json
index 2c69f46d2993..ce932de3d017 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,8 @@
],
"overrides": {
"@types/bun": "catalog:",
- "@types/node": "catalog:"
+ "@types/node": "catalog:",
+ "sharp": "0.34.5"
},
"patchedDependencies": {
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch"
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 63dd0f18eca3..8769748f3f5c 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -91,7 +91,10 @@
"@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/scheduled": "1.5.2",
"@standard-schema/spec": "1.0.0",
+ "@huggingface/transformers": "3.8.1",
"@zip.js/zip.js": "2.7.62",
+ "sharp": "0.34.5",
+ "wavefile": "11.0.0",
"ai": "catalog:",
"ai-gateway-provider": "2.3.1",
"bonjour-service": "1.3.0",
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 0d5aefe7bc3b..b6f1017dc28e 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -12,6 +12,7 @@ import { SyncProvider, useSync } from "@tui/context/sync"
import { LocalProvider, useLocal } from "@tui/context/local"
import { DialogModel, useConnected } from "@tui/component/dialog-model"
import { DialogMcp } from "@tui/component/dialog-mcp"
+import { DialogVoice } from "@tui/component/dialog-voice"
import { DialogStatus } from "@tui/component/dialog-status"
import { DialogThemeList } from "@tui/component/dialog-theme-list"
import { DialogHelp } from "./ui/dialog-help"
@@ -423,6 +424,17 @@ function App() {
dialog.replace(() => )
},
},
+ {
+ title: "Voice settings",
+ value: "voice.settings",
+ category: "Agent",
+ slash: {
+ name: "voice",
+ },
+ onSelect: () => {
+ dialog.replace(() => )
+ },
+ },
{
title: "Agent cycle",
value: "agent.cycle",
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
index 3b6b5ef21827..31a1903ee8f1 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
@@ -162,6 +162,27 @@ export function DialogStatus() {
+
+
+ Voice
+
+
+ •
+
+
+ {sync.data.voice?.status === "ready" ? sync.data.voice.model : "..."}{" "}
+
+ {sync.data.voice?.status === "ready" ? "Ready" : "Loading model..."}
+
+
+
+
+
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-voice.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-voice.tsx
new file mode 100644
index 000000000000..8494568bd8de
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-voice.tsx
@@ -0,0 +1,125 @@
+import { createMemo, createSignal, For, Show } from "solid-js"
+import { useLocal } from "@tui/context/local"
+import { useSync } from "@tui/context/sync"
+import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
+import { useTheme } from "../context/theme"
+import { Keybind } from "@/util/keybind"
+import { TextAttributes } from "@opentui/core"
+
+function Status(props: { status: string; loading: boolean }) {
+ const { theme } = useTheme()
+ if (props.loading) {
+ return ⋯ Loading
+ }
+ if (props.status === "ready") {
+ return ✓ Ready
+ }
+ if (props.status === "downloading") {
+ return ⬇ Downloading
+ }
+ if (props.status === "loading") {
+ return ⋯ Loading
+ }
+ if (props.status === "disabled") {
+ return ○ Disabled
+ }
+ if (props.status === "idle") {
+ return ○ Idle
+ }
+ return ✗ Error
+}
+
+export function DialogVoice() {
+ const local = useLocal()
+ const sync = useSync()
+ const [, setRef] = createSignal>()
+ const [loading, setLoading] = createSignal(null)
+
+ const voiceData = () => sync.data.voice
+ const voiceStatus = () => (voiceData() as any)?.status ?? "disabled"
+ const voiceModel = () => (voiceData() as any)?.model
+
+ const options = createMemo(() => {
+ const loadingModel = loading()
+ const currentStatus = voiceStatus()
+
+ const result: DialogSelectOption[] = []
+
+ // Toggle voice on/off
+ result.push({
+ value: "toggle",
+ title: currentStatus === "disabled" ? "Enable Voice" : "Disable Voice",
+ description: "Toggle voice transcription",
+ footer: ,
+ category: "Control",
+ })
+
+ // Model selection
+ const models = [
+ { name: "tiny", size: "75 MB", description: "Fast, lower accuracy" },
+ { name: "base", size: "142 MB", description: "Balanced speed and accuracy" },
+ { name: "small", size: "466 MB", description: "Better accuracy, slower" },
+ ]
+
+ for (const model of models) {
+ const isCurrent = voiceModel() === model.name
+ result.push({
+ value: `model:${model.name}`,
+ title: `${model.name} (${model.size})`,
+ description: model.description,
+ footer: loadingModel === model.name ? ⋯ Loading : isCurrent ? ✓ Active : undefined,
+ category: "Models",
+ })
+ }
+
+ return result
+ })
+
+ const keybinds = createMemo(() => [
+ {
+ keybind: Keybind.parse("space")[0],
+ title: "select",
+ onTrigger: async (option: DialogSelectOption) => {
+ if (loading() !== null) return
+
+ const value = option.value
+
+ if (value === "toggle") {
+ setLoading("toggle")
+ try {
+ await local.voice.toggle()
+ } catch (error) {
+ console.error("Failed to toggle voice:", error)
+ } finally {
+ setLoading(null)
+ }
+ return
+ }
+
+ if (value.startsWith("model:")) {
+ const modelName = value.replace("model:", "") as "tiny" | "base" | "small"
+ setLoading(modelName)
+ try {
+ await local.voice.switchModel(modelName)
+ } catch (error) {
+ console.error("Failed to switch voice model:", error)
+ } finally {
+ setLoading(null)
+ }
+ }
+ },
+ },
+ ])
+
+ return (
+ {
+ // Don't close on select, only on escape
+ }}
+ />
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index cefef208de4a..8542bd732b43 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -31,6 +31,7 @@ import { DialogAlert } from "../../ui/dialog-alert"
import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
+import { VoiceRecorder, type VoiceRecorderStatus } from "@tui/util/voice-recorder"
import { DialogSkill } from "../dialog-skill"
export type PromptProps = {
@@ -135,6 +136,18 @@ export function Prompt(props: PromptProps) {
interrupt: 0,
})
+ // Voice recording state
+ const [voiceStatus, setVoiceStatus] = createSignal("idle")
+ let voiceRecorder: VoiceRecorder | null = null
+
+ onMount(() => {
+ voiceRecorder = new VoiceRecorder(sdk.client)
+ })
+
+ onCleanup(() => {
+ voiceRecorder?.cancel()
+ })
+
createEffect(
on(
() => props.sessionID,
@@ -834,6 +847,71 @@ export function Prompt(props: PromptProps) {
e.preventDefault()
return
}
+
+ // Handle voice input - toggle recording
+ if (keybind.match("voice_input", e)) {
+ // Prevent default to avoid inserting the keybind character
+ e.preventDefault()
+
+ // Only allow voice input if service is available
+ if (sync.data.voice?.status !== "ready") {
+ return
+ }
+
+ if (voiceStatus() === "idle" && voiceRecorder) {
+ // Start recording
+ try {
+ await voiceRecorder.startRecording()
+ setVoiceStatus(voiceRecorder.status)
+ } catch (err) {
+ toast.show({
+ variant: "error",
+ message: `Failed to start recording: ${err instanceof Error ? err.message : String(err)}`,
+ duration: 3000,
+ })
+ }
+ return
+ }
+
+ if (voiceStatus() === "recording" && voiceRecorder) {
+ // Stop recording and transcribe
+ setVoiceStatus("transcribing")
+
+ try {
+ const text = await voiceRecorder.stopRecordingAndTranscribe()
+ setVoiceStatus(voiceRecorder.status)
+
+ if (!text) {
+ toast.show({
+ variant: "warning",
+ message: "No speech detected",
+ duration: 3000,
+ })
+ return
+ }
+
+ // Insert transcribed text at cursor position
+ input.insertText(text)
+ setTimeout(() => {
+ input.getLayoutNode().markDirty()
+ renderer.requestRender()
+ }, 0)
+ } catch (err) {
+ setVoiceStatus("error")
+ toast.show({
+ variant: "error",
+ message: `Transcription failed: ${err instanceof Error ? err.message : String(err)}`,
+ duration: 5000,
+ })
+ // Reset status after error
+ setTimeout(() => setVoiceStatus("idle"), 100)
+ }
+ return
+ }
+
+ return
+ }
+
// Handle clipboard paste (Ctrl+V) - check for images first on Windows
// This is needed because Windows terminal doesn't properly send image data
// through bracketed paste, so we need to intercept the keypress and
@@ -1126,6 +1204,23 @@ export function Prompt(props: PromptProps) {
+
+
+
+ Recording... ({keybind.print("voice_input")} to stop)
+
+
+
+
+
+ Transcribing...
+
+
+
+
+ {keybind.print("voice_input")} voice
+
+
0}>
{keybind.print("variant_cycle")} variants
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index 72c72dc5bb3c..73162117106d 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx
@@ -381,6 +381,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
},
}
+ const voice = {
+ async toggle() {
+ const status = sync.data.voice
+ if (status?.status === "ready") {
+ await sdk.client.voice.disable()
+ } else {
+ await sdk.client.voice.enable()
+ }
+ },
+ async switchModel(model: "tiny" | "base" | "small") {
+ await sdk.client.voice.switchModel({ model })
+ },
+ }
+
// Automatically update model when agent changes
createEffect(() => {
const value = agent.current()
@@ -403,6 +417,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
model,
agent,
mcp,
+ voice,
}
return result
},
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
index eb8ed2d9bbad..640e0e98ba83 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
@@ -17,6 +17,7 @@ import type {
ProviderListResponse,
ProviderAuthMethod,
VcsInfo,
+ VoiceStatusResponse,
} from "@opencode-ai/sdk/v2"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
@@ -71,6 +72,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
[key: string]: McpResource
}
formatter: FormatterStatus[]
+ voice: VoiceStatusResponse | undefined
vcs: VcsInfo | undefined
path: Path
}>({
@@ -98,6 +100,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
mcp: {},
mcp_resource: {},
formatter: [],
+ voice: undefined,
vcs: undefined,
path: { state: "", config: "", worktree: "", directory: "" },
})
@@ -318,6 +321,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
+ case "voice.updated": {
+ sdk.client.voice
+ .status()
+ .then((x) => {
+ if (x.data) {
+ setStore("voice", reconcile(x.data))
+ }
+ })
+ .catch((e) => {
+ Log.Default.error("failed to fetch voice status", { error: e })
+ })
+ break
+ }
+
case "vcs.branch.updated": {
setStore("vcs", { branch: event.properties.branch })
break
@@ -389,6 +406,14 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))),
+ sdk.client.voice
+ .status()
+ .then((x) => {
+ if (x.data) setStore("voice", reconcile(x.data))
+ })
+ .catch(() => {
+ // Voice service might not be available - that's okay
+ }),
sdk.client.session.status().then((x) => {
setStore("session_status", reconcile(x.data!))
}),
diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
index c011f6c62468..5c5a39b321ec 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
@@ -132,6 +132,21 @@ export function Home() {
{connectedMcpCount()} MCP
+
+
+
+
+
+ ◉
+
+
+ ◉
+
+
+ Voice
+
+
+
/status
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
index 8ace2fff3725..d20f1753e7b7 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
@@ -82,6 +82,19 @@ export function Footer() {
{mcp()} MCP
+
+
+
+
+ ◉
+
+
+ ◉
+
+
+ Voice
+
+
/status
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 2ea49ff6b2b4..763e6e1d6085 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -142,6 +142,8 @@ export const TuiThreadCommand = cmd({
// Start HTTP server for external access
const server = await client.call("server", networkOpts)
url = server.url
+ customFetch = createWorkerFetch(client)
+ events = createEventSource(client)
} else {
// Use direct RPC communication (no HTTP)
url = "http://opencode.internal"
diff --git a/packages/opencode/src/cli/cmd/tui/util/voice-recorder.ts b/packages/opencode/src/cli/cmd/tui/util/voice-recorder.ts
new file mode 100644
index 000000000000..c3be7f9c06aa
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/util/voice-recorder.ts
@@ -0,0 +1,116 @@
+import { spawn, type ChildProcess } from "child_process"
+import { tmpdir } from "os"
+import { join } from "path"
+import { unlinkSync } from "fs"
+import type { OpencodeClient } from "@opencode-ai/sdk/v2"
+
+export type VoiceRecorderStatus = "idle" | "recording" | "transcribing" | "error"
+
+export class VoiceRecorder {
+ private process: ChildProcess | null = null
+ private tempFile: string | null = null
+ private client: OpencodeClient
+ status: VoiceRecorderStatus = "idle"
+
+ constructor(client: OpencodeClient) {
+ this.client = client
+ }
+
+ async startRecording(): Promise {
+ if (this.status !== "idle") {
+ throw new Error("Already recording or transcribing")
+ }
+
+ this.tempFile = join(tmpdir(), `voice-input-${Date.now()}.wav`)
+ this.status = "recording"
+
+ // Start sox recording process - will continue until stopped
+ this.process = spawn("sox", [
+ "-d", // default input device
+ "-r",
+ "16000", // sample rate
+ "-c",
+ "1", // mono
+ "-b",
+ "16", // 16-bit
+ this.tempFile,
+ ])
+
+ this.process.on("error", (err) => {
+ this.status = "error"
+ console.error("Recording error:", err)
+ })
+ }
+
+ async stopRecordingAndTranscribe(): Promise {
+ if (this.status !== "recording" || !this.process || !this.tempFile) {
+ throw new Error("Not currently recording")
+ }
+
+ const tempFile = this.tempFile
+
+ // Stop recording by killing sox
+ this.process.kill("SIGTERM")
+ this.process = null
+
+ // Wait a moment for file to be flushed
+ await new Promise((resolve) => setTimeout(resolve, 100))
+
+ this.status = "transcribing"
+
+ try {
+ // Read audio file
+ const audioFile = Bun.file(tempFile)
+ const audioBuffer = await audioFile.arrayBuffer()
+
+ if (audioBuffer.byteLength === 0) {
+ throw new Error("Audio file is empty - no audio was recorded")
+ }
+
+ const audioBase64 = Buffer.from(audioBuffer).toString("base64")
+
+ // Transcribe using SDK client
+ const result = await this.client.voice.transcribe({
+ audio: audioBase64,
+ timestamps: false,
+ })
+
+ if (!result.data) {
+ throw new Error("Transcription failed: No data returned")
+ }
+
+ const text = result.data.text?.trim() ?? ""
+
+ // Clean up temp file
+ try {
+ unlinkSync(tempFile)
+ } catch {
+ // Ignore cleanup errors
+ }
+
+ this.status = "idle"
+ this.tempFile = null
+ return text
+ } catch (err) {
+ this.status = "error"
+ // Don't delete file on error for debugging
+ throw err
+ }
+ }
+
+ cancel(): void {
+ if (this.process) {
+ this.process.kill("SIGTERM")
+ this.process = null
+ }
+ if (this.tempFile) {
+ try {
+ unlinkSync(this.tempFile)
+ } catch {
+ // Ignore
+ }
+ this.tempFile = null
+ }
+ this.status = "idle"
+ }
+}
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index e63f10ba80c9..e5f282e2b553 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -32,11 +32,6 @@ process.on("uncaughtException", (e) => {
})
})
-// Subscribe to global events and forward them via RPC
-GlobalBus.on("event", (event) => {
- Rpc.emit("global.event", event)
-})
-
let server: Bun.Server | undefined
const eventStream = {
@@ -65,14 +60,7 @@ const startEventStream = (directory: string) => {
;(async () => {
while (!signal.aborted) {
- const events = await Promise.resolve(
- sdk.event.subscribe(
- {},
- {
- signal,
- },
- ),
- ).catch(() => undefined)
+ const events = await Promise.resolve(sdk.global.event({ parseAs: "stream" })).catch(() => undefined)
if (!events) {
await Bun.sleep(250)
@@ -80,7 +68,7 @@ const startEventStream = (directory: string) => {
}
for await (const event of events.stream) {
- Rpc.emit("event", event as Event)
+ Rpc.emit("event", event.payload as Event)
}
if (!signal.aborted) {
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 8f0f583ea3d6..fad6856f8116 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -583,6 +583,25 @@ export namespace Config {
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
export type Mcp = z.infer
+ export const Voice = z
+ .object({
+ enabled: z.boolean().optional().describe("Enable or disable voice transcription"),
+ model: z
+ .enum(["tiny", "base", "small"])
+ .optional()
+ .default("base")
+ .describe("Whisper model size: tiny (75MB), base (142MB), or small (466MB)"),
+ device: z
+ .enum(["cpu", "gpu", "auto"])
+ .optional()
+ .default("auto")
+ .describe("Device to run the model on: cpu, gpu, or auto"),
+ })
+ .meta({
+ ref: "VoiceConfig",
+ })
+ export type Voice = z.infer
+
export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({
ref: "PermissionActionConfig",
})
@@ -822,6 +841,7 @@ export namespace Config {
variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
+ voice_input: z.string().optional().default("\\").describe("Voice input (tap to record, tap to stop)"),
input_submit: z.string().optional().default("return").describe("Submit input"),
input_newline: z
.string()
@@ -1098,6 +1118,7 @@ export namespace Config {
)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
+ voice: Voice.optional().describe("Voice transcription configuration"),
formatter: z
.union([
z.literal(false),
diff --git a/packages/opencode/src/server/routes/voice.ts b/packages/opencode/src/server/routes/voice.ts
new file mode 100644
index 000000000000..5ba935ed72c9
--- /dev/null
+++ b/packages/opencode/src/server/routes/voice.ts
@@ -0,0 +1,377 @@
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import { upgradeWebSocket } from "hono/bun"
+import z from "zod"
+import { VoiceService, Voice } from "../../voice/service"
+import { AudioBuffer } from "../../voice/audio-buffer"
+import { errors } from "../error"
+import { lazy } from "../../util/lazy"
+
+export const VoiceRoutes = lazy(() =>
+ new Hono()
+ .get(
+ "/status",
+ describeRoute({
+ summary: "Get voice service status",
+ description: "Check the current status of the voice transcription service",
+ operationId: "voice.status",
+ responses: {
+ 200: {
+ description: "Service status",
+ content: {
+ "application/json": {
+ schema: resolver(Voice.Status),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(VoiceService.getStatus())
+ },
+ )
+ .post(
+ "/enable",
+ describeRoute({
+ summary: "Enable voice transcription",
+ description: "Enable voice transcription with optional model selection",
+ operationId: "voice.enable",
+ responses: {
+ 200: {
+ description: "Enable result",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ success: z.boolean(),
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "json",
+ z.object({
+ model: z.enum(["tiny", "base", "small"]).optional(),
+ }),
+ ),
+ async (c) => {
+ const { model } = c.req.valid("json")
+ const success = await VoiceService.enable(model)
+ return c.json({ success })
+ },
+ )
+ .post(
+ "/disable",
+ describeRoute({
+ summary: "Disable voice transcription",
+ description: "Disable voice transcription service",
+ operationId: "voice.disable",
+ responses: {
+ 200: {
+ description: "Disabled successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.object({ success: z.boolean() })),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ await VoiceService.disable()
+ return c.json({ success: true })
+ },
+ )
+ .get(
+ "/models",
+ describeRoute({
+ summary: "List available models",
+ description: "Get list of available Whisper models",
+ operationId: "voice.models",
+ responses: {
+ 200: {
+ description: "Available models",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ available: z.array(
+ z.object({
+ name: z.enum(["tiny", "base", "small"]),
+ size: z.string(),
+ }),
+ ),
+ downloaded: z.array(z.enum(["tiny", "base", "small"])),
+ current: z.enum(["tiny", "base", "small"]),
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const available = await VoiceService.getAvailableModels()
+ const downloaded = await VoiceService.getDownloadedModels()
+ const current = VoiceService.getCurrentModel()
+ return c.json({ available, downloaded, current })
+ },
+ )
+ .post(
+ "/switch-model",
+ describeRoute({
+ summary: "Switch to a different model",
+ description: "Switch the voice transcription model",
+ operationId: "voice.switchModel",
+ responses: {
+ 200: {
+ description: "Model switch result",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ success: z.boolean(),
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ validator(
+ "json",
+ z.object({
+ model: z.enum(["tiny", "base", "small"]),
+ }),
+ ),
+ async (c) => {
+ const { model } = c.req.valid("json")
+ const success = await VoiceService.switchModel(model)
+ return c.json({ success })
+ },
+ )
+ .post(
+ "/transcribe",
+ describeRoute({
+ summary: "Transcribe audio file",
+ description: "Submit a base64-encoded audio file for transcription",
+ operationId: "voice.transcribe",
+ responses: {
+ 200: {
+ description: "Transcription result",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ text: z.string(),
+ chunks: z
+ .array(
+ z.object({
+ text: z.string(),
+ timestamp: z.tuple([z.number(), z.number()]),
+ }),
+ )
+ .optional(),
+ }),
+ ),
+ },
+ },
+ },
+ ...errors(503),
+ },
+ }),
+ validator(
+ "json",
+ z.object({
+ audio: z.string().describe("Base64-encoded WAV audio data"),
+ timestamps: z.boolean().optional().default(false),
+ }),
+ ),
+ async (c) => {
+ if (!VoiceService.isReady()) {
+ return c.json({ error: "Transcription service not ready" }, 503)
+ }
+
+ const { audio, timestamps } = c.req.valid("json")
+
+ try {
+ const audioBuffer = Buffer.from(audio, "base64")
+ const result = await VoiceService.transcribe(audioBuffer, timestamps)
+ return c.json(result)
+ } catch (error) {
+ console.error("[Transcription] Error:", error)
+ return c.json(
+ {
+ error: error instanceof Error ? error.message : "Transcription failed",
+ },
+ 500,
+ )
+ }
+ },
+ )
+ .get(
+ "/stream",
+ describeRoute({
+ summary: "Stream audio for transcription",
+ description: "Establish a WebSocket connection to stream audio chunks and receive real-time transcriptions",
+ operationId: "voice.stream",
+ responses: {
+ 200: {
+ description: "WebSocket connection established",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(503),
+ },
+ }),
+ upgradeWebSocket(() => {
+ if (!VoiceService.isReady()) {
+ throw new Error("Transcription service not ready")
+ }
+
+ const buffer = new AudioBuffer(16000, 1)
+ const maxDuration = 300
+ const chunkDuration = 3
+ let isProcessing = false
+ let isClosed = false
+
+ return {
+ onOpen(_event, ws) {
+ ws.send(
+ JSON.stringify({
+ type: "ready",
+ maxDuration,
+ }),
+ )
+ },
+
+ async onMessage(event, ws) {
+ if (isClosed || isProcessing) return
+
+ try {
+ const data = event.data
+
+ // Handle text messages (commands)
+ if (typeof data === "string") {
+ const msg = JSON.parse(data)
+
+ if (msg.type === "finalize") {
+ // Transcribe whatever we have buffered
+ isProcessing = true
+
+ if (!buffer.isEmpty()) {
+ try {
+ const wavBuffer = buffer.toWav()
+ const result = await VoiceService.transcribe(wavBuffer, msg.timestamps || false)
+
+ ws.send(
+ JSON.stringify({
+ type: "transcription",
+ text: result.text,
+ chunks: result.chunks,
+ final: true,
+ }),
+ )
+ } catch (error) {
+ ws.send(
+ JSON.stringify({
+ type: "error",
+ message: error instanceof Error ? error.message : "Transcription failed",
+ }),
+ )
+ }
+
+ buffer.clear()
+ }
+
+ ws.send(JSON.stringify({ type: "done" }))
+ isProcessing = false
+ return
+ }
+
+ if (msg.type === "clear") {
+ buffer.clear()
+ ws.send(JSON.stringify({ type: "cleared" }))
+ return
+ }
+ }
+
+ // Handle binary audio data
+ if (data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
+ const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data)
+ buffer.append(chunk)
+
+ // Check if we've exceeded max duration
+ if (buffer.getDuration() > maxDuration) {
+ ws.send(
+ JSON.stringify({
+ type: "error",
+ message: `Maximum recording duration (${maxDuration}s) exceeded`,
+ }),
+ )
+ ws.close()
+ return
+ }
+
+ // Send progress updates
+ ws.send(
+ JSON.stringify({
+ type: "progress",
+ duration: buffer.getDuration(),
+ }),
+ )
+
+ // Optional: Perform intermediate transcription every chunkDuration seconds
+ if (buffer.getDuration() >= chunkDuration && !isProcessing) {
+ isProcessing = true
+
+ try {
+ const wavBuffer = buffer.toWav()
+ const result = await VoiceService.transcribe(wavBuffer, false)
+
+ ws.send(
+ JSON.stringify({
+ type: "transcription",
+ text: result.text,
+ final: false,
+ }),
+ )
+
+ // Keep the buffer for the final transcription
+ } catch (error) {
+ console.error("[Transcription] Intermediate transcription error:", error)
+ // Don't fail the whole session on intermediate errors
+ }
+
+ isProcessing = false
+ }
+ }
+ } catch (error) {
+ console.error("[Transcription] Message handling error:", error)
+ ws.send(
+ JSON.stringify({
+ type: "error",
+ message: error instanceof Error ? error.message : "Unknown error",
+ }),
+ )
+ }
+ },
+
+ onClose() {
+ isClosed = true
+ buffer.clear()
+ },
+
+ onError(_ws, error) {
+ console.error("[Transcription] WebSocket error:", error)
+ },
+ }
+ }),
+ ),
+)
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 9fb5206551b6..3f7298efef84 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -39,6 +39,8 @@ import { errors } from "./error"
import { QuestionRoutes } from "./routes/question"
import { PermissionRoutes } from "./routes/permission"
import { GlobalRoutes } from "./routes/global"
+import { VoiceRoutes } from "./routes/voice"
+import { VoiceService } from "../voice/service"
import { MDNS } from "./mdns"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
@@ -232,6 +234,7 @@ export namespace Server {
.route("/permission", PermissionRoutes())
.route("/question", QuestionRoutes())
.route("/provider", ProviderRoutes())
+ .route("/voice", VoiceRoutes())
.route("/", FileRoutes())
.route("/mcp", McpRoutes())
.route("/tui", TuiRoutes())
@@ -580,6 +583,11 @@ export namespace Server {
}) {
_corsWhitelist = opts.cors ?? []
+ // Initialize transcription service (non-blocking)
+ VoiceService.initialize().catch((error) => {
+ log.warn("transcription service initialization failed", { error })
+ })
+
const args = {
hostname: opts.hostname,
idleTimeout: 0,
@@ -613,6 +621,7 @@ export namespace Server {
const originalStop = server.stop.bind(server)
server.stop = async (closeActiveConnections?: boolean) => {
if (shouldPublishMDNS) MDNS.unpublish()
+ await VoiceService.shutdown()
return originalStop(closeActiveConnections)
}
diff --git a/packages/opencode/src/voice/audio-buffer.ts b/packages/opencode/src/voice/audio-buffer.ts
new file mode 100644
index 000000000000..53ba1f1bd8b0
--- /dev/null
+++ b/packages/opencode/src/voice/audio-buffer.ts
@@ -0,0 +1,62 @@
+export class AudioBuffer {
+ private chunks: Buffer[] = []
+ private totalDuration = 0 // in seconds
+ private sampleRate: number
+ private channels: number
+
+ constructor(sampleRate = 16000, channels = 1) {
+ this.sampleRate = sampleRate
+ this.channels = channels
+ }
+
+ append(chunk: Buffer) {
+ this.chunks.push(chunk)
+ // Assuming 16-bit PCM audio
+ const samples = chunk.length / 2
+ this.totalDuration += samples / this.sampleRate
+ }
+
+ getDuration(): number {
+ return this.totalDuration
+ }
+
+ getBuffer(): Buffer {
+ return Buffer.concat(this.chunks)
+ }
+
+ clear() {
+ this.chunks = []
+ this.totalDuration = 0
+ }
+
+ isEmpty(): boolean {
+ return this.chunks.length === 0
+ }
+
+ toWav(): Buffer {
+ const audioData = this.getBuffer()
+ const dataLength = audioData.length
+ const header = Buffer.alloc(44)
+
+ // RIFF header
+ header.write("RIFF", 0)
+ header.writeUInt32LE(36 + dataLength, 4)
+ header.write("WAVE", 8)
+
+ // fmt chunk
+ header.write("fmt ", 12)
+ header.writeUInt32LE(16, 16) // chunk size
+ header.writeUInt16LE(1, 20) // audio format (PCM)
+ header.writeUInt16LE(this.channels, 22)
+ header.writeUInt32LE(this.sampleRate, 24)
+ header.writeUInt32LE(this.sampleRate * this.channels * 2, 28) // byte rate
+ header.writeUInt16LE(this.channels * 2, 32) // block align
+ header.writeUInt16LE(16, 34) // bits per sample
+
+ // data chunk
+ header.write("data", 36)
+ header.writeUInt32LE(dataLength, 40)
+
+ return Buffer.concat([header, audioData])
+ }
+}
diff --git a/packages/opencode/src/voice/event.ts b/packages/opencode/src/voice/event.ts
new file mode 100644
index 000000000000..08ad2f9b6783
--- /dev/null
+++ b/packages/opencode/src/voice/event.ts
@@ -0,0 +1,40 @@
+import { BusEvent } from "@/bus/bus-event"
+import z from "zod"
+
+export namespace Voice {
+ export const Status = z
+ .discriminatedUnion("status", [
+ z.object({
+ status: z.literal("disabled"),
+ }),
+ z.object({
+ status: z.literal("idle"),
+ }),
+ z.object({
+ status: z.literal("downloading"),
+ progress: z.number(),
+ }),
+ z.object({
+ status: z.literal("loading"),
+ }),
+ z.object({
+ status: z.literal("ready"),
+ model: z.string(),
+ }),
+ z.object({
+ status: z.literal("error"),
+ error: z.string(),
+ }),
+ ])
+ .meta({ ref: "VoiceStatus" })
+ export type Status = z.infer
+
+ export const Event = {
+ Updated: BusEvent.define(
+ "voice.updated",
+ z.object({
+ status: Status,
+ }),
+ ),
+ }
+}
diff --git a/packages/opencode/src/voice/service.ts b/packages/opencode/src/voice/service.ts
new file mode 100644
index 000000000000..198f941f3a5f
--- /dev/null
+++ b/packages/opencode/src/voice/service.ts
@@ -0,0 +1,204 @@
+import { WhisperEngine, type WhisperModelSize } from "./whisper-engine"
+import { GlobalBus } from "@/bus/global"
+import { Voice } from "./event"
+import { Log } from "@/util/log"
+import { Global } from "@/global"
+import path from "path"
+import { Config } from "@/config/config"
+
+export { Voice }
+
+class VoiceServiceImpl {
+ private engine: WhisperEngine | null = null
+ private log = Log.create({ service: "voice" })
+ private currentModel: WhisperModelSize = "base"
+ private enabled = false
+
+ private async saveToDisk() {
+ await Config.updateGlobal({
+ voice: {
+ enabled: this.enabled,
+ model: this.currentModel,
+ device: "auto",
+ },
+ })
+ this.log.debug("voice settings saved to config", { enabled: this.enabled, model: this.currentModel })
+ }
+
+ private publishStatus() {
+ const status = (() => {
+ if (!this.enabled) return { status: "disabled" as const }
+ if (!this.engine) return { status: "idle" as const }
+
+ const engineStatus = this.engine.getStatus()
+ if (engineStatus === "idle") return { status: "idle" as const }
+ if (engineStatus === "downloading") {
+ return { status: "downloading" as const, progress: this.engine.getDownloadProgress() }
+ }
+ if (engineStatus === "loading") return { status: "loading" as const }
+ if (engineStatus === "ready") return { status: "ready" as const, model: this.currentModel }
+ return { status: "error" as const, error: "Engine failed to initialize" }
+ })()
+
+ GlobalBus.emit("event", {
+ directory: "",
+ payload: {
+ type: Voice.Event.Updated.type,
+ properties: { status },
+ },
+ })
+ }
+
+ async initialize(): Promise {
+ const cfg = await Config.getGlobal()
+
+ this.enabled = cfg.voice?.enabled ?? false
+ this.currentModel = cfg.voice?.model ?? "base"
+
+ this.log.debug("voice service initialized", { enabled: this.enabled, model: this.currentModel })
+
+ this.publishStatus()
+
+ if (!this.enabled) {
+ return
+ }
+
+ await this.enable(this.currentModel)
+ }
+
+ async enable(model?: WhisperModelSize): Promise {
+ if (model) {
+ this.currentModel = model
+ }
+
+ this.enabled = true
+ await this.saveToDisk()
+ this.publishStatus()
+
+ if (this.engine) {
+ return this.engine.isReady()
+ }
+
+ this.log.debug("enabling voice engine", { model: this.currentModel })
+ this.engine = new WhisperEngine(this.currentModel, "auto")
+ this.publishStatus()
+
+ const started = await this.engine.start()
+ this.publishStatus()
+
+ if (!started) {
+ this.log.warn("voice engine failed to start")
+ return false
+ }
+
+ this.log.debug("voice service enabled successfully")
+ return true
+ }
+
+ async disable(): Promise {
+ this.enabled = false
+ await this.saveToDisk()
+ if (this.engine) {
+ await this.engine.stop()
+ this.engine = null
+ }
+ this.publishStatus()
+ this.log.debug("voice service disabled")
+ }
+
+ async switchModel(model: WhisperModelSize): Promise {
+ if (model === this.currentModel && this.engine?.isReady()) {
+ return true
+ }
+
+ this.log.debug("switching voice model", { from: this.currentModel, to: model })
+ this.currentModel = model
+ await this.saveToDisk()
+
+ if (this.engine) {
+ await this.engine.stop()
+ this.engine = null
+ }
+
+ if (!this.enabled) {
+ return true
+ }
+
+ return this.enable(model)
+ }
+
+ async transcribe(audioBuffer: Buffer, timestamps = false) {
+ if (!this.enabled) {
+ throw new Error("Voice transcription is disabled")
+ }
+
+ if (!this.engine) {
+ const started = await this.enable()
+ if (!started || !this.engine) {
+ throw new Error("Failed to start voice engine")
+ }
+ }
+
+ if (!this.engine.isReady()) {
+ throw new Error("Voice engine not ready")
+ }
+
+ return this.engine.transcribe(audioBuffer, timestamps)
+ }
+
+ async shutdown() {
+ await this.disable()
+ }
+
+ isEnabled(): boolean {
+ return this.enabled
+ }
+
+ isReady(): boolean {
+ return this.enabled && this.engine !== null && this.engine.isReady()
+ }
+
+ getStatus(): Voice.Status {
+ if (!this.enabled) return { status: "disabled" }
+ if (!this.engine) return { status: "idle" }
+
+ const engineStatus = this.engine.getStatus()
+ if (engineStatus === "idle") return { status: "idle" }
+ if (engineStatus === "downloading") {
+ return { status: "downloading", progress: this.engine.getDownloadProgress() }
+ }
+ if (engineStatus === "loading") return { status: "loading" }
+ if (engineStatus === "ready") return { status: "ready", model: this.currentModel }
+ return { status: "error", error: "Engine failed to initialize" }
+ }
+
+ getCurrentModel(): WhisperModelSize {
+ return this.currentModel
+ }
+
+ async getAvailableModels(): Promise> {
+ return [
+ { name: "tiny", size: "75 MB" },
+ { name: "base", size: "142 MB" },
+ { name: "small", size: "466 MB" },
+ ]
+ }
+
+ async getDownloadedModels(): Promise {
+ const cacheDir = path.join(Global.Path.cache, "models")
+ const downloaded: WhisperModelSize[] = []
+
+ const models: WhisperModelSize[] = ["tiny", "base", "small"]
+ for (const model of models) {
+ const modelPath = path.join(cacheDir, `whisper-${model}.en`)
+ const exists = await Bun.file(path.join(modelPath, "config.json")).exists()
+ if (exists) {
+ downloaded.push(model)
+ }
+ }
+
+ return downloaded
+ }
+}
+
+export const VoiceService = new VoiceServiceImpl()
diff --git a/packages/opencode/src/voice/whisper-engine.ts b/packages/opencode/src/voice/whisper-engine.ts
new file mode 100644
index 000000000000..10aa1a1831c2
--- /dev/null
+++ b/packages/opencode/src/voice/whisper-engine.ts
@@ -0,0 +1,141 @@
+import { pipeline } from "@huggingface/transformers"
+import { Log } from "@/util/log"
+import { Global } from "@/global"
+import path from "path"
+import fs from "fs/promises"
+import os from "os"
+import { WaveFile } from "wavefile"
+import { exec } from "child_process"
+import { promisify } from "util"
+
+const execAsync = promisify(exec)
+
+export type WhisperModelSize = "tiny" | "base" | "small"
+
+export type WhisperEngineStatus = "idle" | "downloading" | "loading" | "ready" | "error"
+
+export class WhisperEngine {
+ private transcriber: any = null
+ private status: WhisperEngineStatus = "idle"
+ private log = Log.create({ service: "voice-whisper" })
+ private downloadProgress = 0
+
+ constructor(
+ private modelSize: WhisperModelSize = "base",
+ private device: "cpu" | "gpu" | "auto" = "auto",
+ ) {}
+
+ async start(): Promise {
+ if (this.status === "ready") return true
+ if (this.status === "downloading" || this.status === "loading") return false
+
+ this.status = "downloading"
+ this.log.debug("initializing whisper engine", { modelSize: this.modelSize, device: this.device })
+
+ const modelId = `whisper-${this.modelSize}.en`
+ const cacheDir = path.join(Global.Path.cache, "models")
+
+ try {
+ this.status = "loading"
+
+ this.transcriber = await pipeline("automatic-speech-recognition", modelId, {
+ session_options: {
+ log_severity_level: 4,
+ },
+ dtype: "fp32",
+ quantized: true,
+ device: this.device === "auto" ? undefined : this.device,
+ cache_dir: cacheDir,
+ progress_callback: (progress: any) => {
+ if (progress.status === "downloading") {
+ const percent = progress.progress ? Math.round(progress.progress) : 0
+ if (percent !== this.downloadProgress) {
+ this.downloadProgress = percent
+ this.log.debug("model download progress", { percent })
+ }
+ }
+ },
+ } as any)
+
+ this.status = "ready"
+ this.log.debug("whisper engine ready", { modelSize: this.modelSize })
+ return true
+ } catch (error) {
+ this.status = "error"
+ this.log.error("failed to initialize whisper engine", {
+ error: error instanceof Error ? error.message : String(error),
+ })
+ return false
+ }
+ }
+
+ async transcribe(
+ audioBuffer: Buffer,
+ timestamps = false,
+ ): Promise<{ text: string; chunks?: Array<{ text: string; timestamp: [number, number] }> }> {
+ if (!this.isReady()) {
+ throw new Error("Whisper engine not ready")
+ }
+
+ const tempInput = path.join(os.tmpdir(), `opencode-audio-${Date.now()}.webm`)
+ const tempWav = path.join(os.tmpdir(), `opencode-audio-${Date.now()}.wav`)
+
+ try {
+ await fs.writeFile(tempInput, audioBuffer)
+
+ await execAsync(`ffmpeg -i "${tempInput}" -ar 16000 -ac 1 -f wav "${tempWav}" -y -loglevel quiet`)
+
+ const wavBuffer = await fs.readFile(tempWav)
+ const wav = new WaveFile(wavBuffer)
+
+ wav.toBitDepth("32f")
+ wav.toSampleRate(16000)
+
+ const rawAudioData = wav.getSamples()
+ const audioData = (() => {
+ if (!Array.isArray(rawAudioData)) return rawAudioData
+
+ if (rawAudioData.length === 1) return rawAudioData[0]
+
+ // Mix stereo to mono
+ const SCALING_FACTOR = Math.sqrt(2)
+ for (let i = 0; i < rawAudioData[0].length; ++i) {
+ rawAudioData[0][i] = (SCALING_FACTOR * (rawAudioData[0][i] + rawAudioData[1][i])) / 2
+ }
+ return rawAudioData[0]
+ })()
+
+ const result = await this.transcriber(audioData, {
+ return_timestamps: timestamps,
+ chunk_length_s: 30,
+ stride_length_s: 5,
+ })
+
+ return {
+ text: result.text.trim(),
+ ...(timestamps && result.chunks ? { chunks: result.chunks } : {}),
+ }
+ } finally {
+ await fs.unlink(tempInput).catch(() => {})
+ await fs.unlink(tempWav).catch(() => {})
+ }
+ }
+
+ async stop() {
+ this.transcriber = null
+ this.status = "idle"
+ this.log.info("whisper engine stopped")
+ }
+
+ isReady(): boolean {
+ return this.status === "ready" && this.transcriber !== null
+ }
+
+ getStatus(): WhisperEngineStatus {
+ return this.status
+ }
+
+ getDownloadProgress(): number {
+ return this.downloadProgress
+ }
+}
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index b757b7535075..23930803e39b 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -163,6 +163,13 @@ import type {
TuiShowToastResponses,
TuiSubmitPromptResponses,
VcsGetResponses,
+ VoiceDisableResponses,
+ VoiceEnableResponses,
+ VoiceModelsResponses,
+ VoiceStatusResponses,
+ VoiceStreamResponses,
+ VoiceSwitchModelResponses,
+ VoiceTranscribeResponses,
WorktreeCreateErrors,
WorktreeCreateInput,
WorktreeCreateResponses,
@@ -2161,6 +2168,191 @@ export class Provider extends HeyApiClient {
}
}
+export class Voice extends HeyApiClient {
+ /**
+ * Get voice service status
+ *
+ * Check the current status of the voice transcription service
+ */
+ public status(
+ parameters?: {
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
+ return (options?.client ?? this.client).get({
+ url: "/voice/status",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Enable voice transcription
+ *
+ * Enable voice transcription with optional model selection
+ */
+ public enable(
+ parameters?: {
+ directory?: string
+ model?: "tiny" | "base" | "small"
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "body", key: "model" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post({
+ url: "/voice/enable",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+
+ /**
+ * Disable voice transcription
+ *
+ * Disable voice transcription service
+ */
+ public disable(
+ parameters?: {
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
+ return (options?.client ?? this.client).post({
+ url: "/voice/disable",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * List available models
+ *
+ * Get list of available Whisper models
+ */
+ public models(
+ parameters?: {
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
+ return (options?.client ?? this.client).get({
+ url: "/voice/models",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * Switch to a different model
+ *
+ * Switch the voice transcription model
+ */
+ public switchModel(
+ parameters?: {
+ directory?: string
+ model?: "tiny" | "base" | "small"
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "body", key: "model" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post({
+ url: "/voice/switch-model",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+
+ /**
+ * Transcribe audio file
+ *
+ * Submit a base64-encoded audio file for transcription
+ */
+ public transcribe(
+ parameters?: {
+ directory?: string
+ audio?: string
+ timestamps?: boolean
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "body", key: "audio" },
+ { in: "body", key: "timestamps" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post({
+ url: "/voice/transcribe",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+
+ /**
+ * Stream audio for transcription
+ *
+ * Establish a WebSocket connection to stream audio chunks and receive real-time transcriptions
+ */
+ public stream(
+ parameters?: {
+ directory?: string
+ },
+ options?: Options,
+ ) {
+ const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
+ return (options?.client ?? this.client).get({
+ url: "/voice/stream",
+ ...options,
+ ...params,
+ })
+ }
+}
+
export class Find extends HeyApiClient {
/**
* Find text
@@ -3251,6 +3443,11 @@ export class OpencodeClient extends HeyApiClient {
return (this._provider ??= new Provider({ client: this.client }))
}
+ private _voice?: Voice
+ get voice(): Voice {
+ return (this._voice ??= new Voice({ client: this.client }))
+ }
+
private _find?: Find
get find(): Find {
return (this._find ??= new Find({ client: this.client }))
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 07ce5c2b05cd..1d0e18fa9951 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -907,6 +907,36 @@ export type EventWorktreeFailed = {
}
}
+export type VoiceStatus =
+ | {
+ status: "disabled"
+ }
+ | {
+ status: "idle"
+ }
+ | {
+ status: "downloading"
+ progress: number
+ }
+ | {
+ status: "loading"
+ }
+ | {
+ status: "ready"
+ model: string
+ }
+ | {
+ status: "error"
+ error: string
+ }
+
+export type EventVoiceUpdated = {
+ type: "voice.updated"
+ properties: {
+ status: VoiceStatus
+ }
+}
+
export type Event =
| EventInstallationUpdated
| EventInstallationUpdateAvailable
@@ -950,6 +980,7 @@ export type Event =
| EventPtyDeleted
| EventWorktreeReady
| EventWorktreeFailed
+ | EventVoiceUpdated
export type GlobalEvent = {
directory: string
@@ -1160,6 +1191,10 @@ export type KeybindsConfig = {
* Paste from clipboard
*/
input_paste?: string
+ /**
+ * Voice input (tap to record, tap to stop)
+ */
+ voice_input?: string
/**
* Submit input
*/
@@ -1626,6 +1661,24 @@ export type McpRemoteConfig = {
timeout?: number
}
+/**
+ * Voice transcription configuration
+ */
+export type VoiceConfig = {
+ /**
+ * Enable or disable voice transcription
+ */
+ enabled?: boolean
+ /**
+ * Whisper model size: tiny (75MB), base (142MB), or small (466MB)
+ */
+ model?: "tiny" | "base" | "small"
+ /**
+ * Device to run the model on: cpu, gpu, or auto
+ */
+ device?: "cpu" | "gpu" | "auto"
+}
+
/**
* @deprecated Always uses stretch layout.
*/
@@ -1769,6 +1822,7 @@ export type Config = {
enabled: boolean
}
}
+ voice?: VoiceConfig
formatter?:
| false
| {
@@ -4154,6 +4208,161 @@ export type ProviderOauthCallbackResponses = {
export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses]
+export type VoiceStatusData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/voice/status"
+}
+
+export type VoiceStatusResponses = {
+ /**
+ * Service status
+ */
+ 200: VoiceStatus
+}
+
+export type VoiceStatusResponse = VoiceStatusResponses[keyof VoiceStatusResponses]
+
+export type VoiceEnableData = {
+ body?: {
+ model?: "tiny" | "base" | "small"
+ }
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/voice/enable"
+}
+
+export type VoiceEnableResponses = {
+ /**
+ * Enable result
+ */
+ 200: {
+ success: boolean
+ }
+}
+
+export type VoiceEnableResponse = VoiceEnableResponses[keyof VoiceEnableResponses]
+
+export type VoiceDisableData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/voice/disable"
+}
+
+export type VoiceDisableResponses = {
+ /**
+ * Disabled successfully
+ */
+ 200: {
+ success: boolean
+ }
+}
+
+export type VoiceDisableResponse = VoiceDisableResponses[keyof VoiceDisableResponses]
+
+export type VoiceModelsData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/voice/models"
+}
+
+export type VoiceModelsResponses = {
+ /**
+ * Available models
+ */
+ 200: {
+ available: Array<{
+ name: "tiny" | "base" | "small"
+ size: string
+ }>
+ downloaded: Array<"tiny" | "base" | "small">
+ current: "tiny" | "base" | "small"
+ }
+}
+
+export type VoiceModelsResponse = VoiceModelsResponses[keyof VoiceModelsResponses]
+
+export type VoiceSwitchModelData = {
+ body?: {
+ model: "tiny" | "base" | "small"
+ }
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/voice/switch-model"
+}
+
+export type VoiceSwitchModelResponses = {
+ /**
+ * Model switch result
+ */
+ 200: {
+ success: boolean
+ }
+}
+
+export type VoiceSwitchModelResponse = VoiceSwitchModelResponses[keyof VoiceSwitchModelResponses]
+
+export type VoiceTranscribeData = {
+ body?: {
+ /**
+ * Base64-encoded WAV audio data
+ */
+ audio: string
+ timestamps?: boolean
+ }
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/voice/transcribe"
+}
+
+export type VoiceTranscribeResponses = {
+ /**
+ * Transcription result
+ */
+ 200: {
+ text: string
+ chunks?: Array<{
+ text: string
+ timestamp: [number, number]
+ }>
+ }
+}
+
+export type VoiceTranscribeResponse = VoiceTranscribeResponses[keyof VoiceTranscribeResponses]
+
+export type VoiceStreamData = {
+ body?: never
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/voice/stream"
+}
+
+export type VoiceStreamResponses = {
+ /**
+ * WebSocket connection established
+ */
+ 200: boolean
+}
+
+export type VoiceStreamResponse = VoiceStreamResponses[keyof VoiceStreamResponses]
+
export type FindTextData = {
body?: never
path?: never
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index dd118a3ae5b3..57770a4291d5 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -4040,6 +4040,372 @@
]
}
},
+ "/voice/status": {
+ "get": {
+ "operationId": "voice.status",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Get voice service status",
+ "description": "Check the current status of the voice transcription service",
+ "responses": {
+ "200": {
+ "description": "Service status",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoiceStatus"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.voice.status({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/voice/enable": {
+ "post": {
+ "operationId": "voice.enable",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Enable voice transcription",
+ "description": "Enable voice transcription with optional model selection",
+ "responses": {
+ "200": {
+ "description": "Enable result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ }
+ },
+ "required": ["success"]
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "model": {
+ "type": "string",
+ "enum": ["tiny", "base", "small"]
+ }
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.voice.enable({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/voice/disable": {
+ "post": {
+ "operationId": "voice.disable",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Disable voice transcription",
+ "description": "Disable voice transcription service",
+ "responses": {
+ "200": {
+ "description": "Disabled successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ }
+ },
+ "required": ["success"]
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.voice.disable({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/voice/models": {
+ "get": {
+ "operationId": "voice.models",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "List available models",
+ "description": "Get list of available Whisper models",
+ "responses": {
+ "200": {
+ "description": "Available models",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "available": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "enum": ["tiny", "base", "small"]
+ },
+ "size": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "size"]
+ }
+ },
+ "downloaded": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["tiny", "base", "small"]
+ }
+ },
+ "current": {
+ "type": "string",
+ "enum": ["tiny", "base", "small"]
+ }
+ },
+ "required": ["available", "downloaded", "current"]
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.voice.models({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/voice/switch-model": {
+ "post": {
+ "operationId": "voice.switchModel",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Switch to a different model",
+ "description": "Switch the voice transcription model",
+ "responses": {
+ "200": {
+ "description": "Model switch result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ }
+ },
+ "required": ["success"]
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "model": {
+ "type": "string",
+ "enum": ["tiny", "base", "small"]
+ }
+ },
+ "required": ["model"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.voice.switchModel({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/voice/transcribe": {
+ "post": {
+ "operationId": "voice.transcribe",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Transcribe audio file",
+ "description": "Submit a base64-encoded audio file for transcription",
+ "responses": {
+ "200": {
+ "description": "Transcription result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ },
+ "chunks": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "array",
+ "prefixItems": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ }
+ },
+ "required": ["text", "timestamp"]
+ }
+ }
+ },
+ "required": ["text"]
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "audio": {
+ "description": "Base64-encoded WAV audio data",
+ "type": "string"
+ },
+ "timestamps": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "required": ["audio"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.voice.transcribe({\n ...\n})"
+ }
+ ]
+ }
+ },
+ "/voice/stream": {
+ "get": {
+ "operationId": "voice.stream",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Stream audio for transcription",
+ "description": "Establish a WebSocket connection to stream audio chunks and receive real-time transcriptions",
+ "responses": {
+ "200": {
+ "description": "WebSocket connection established",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.voice.stream({\n ...\n})"
+ }
+ ]
+ }
+ },
"/find": {
"get": {
"operationId": "find.text",
@@ -8310,6 +8676,98 @@
},
"required": ["type", "properties"]
},
+ "VoiceStatus": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "disabled"
+ }
+ },
+ "required": ["status"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "idle"
+ }
+ },
+ "required": ["status"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "downloading"
+ },
+ "progress": {
+ "type": "number"
+ }
+ },
+ "required": ["status", "progress"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "loading"
+ }
+ },
+ "required": ["status"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "ready"
+ },
+ "model": {
+ "type": "string"
+ }
+ },
+ "required": ["status", "model"]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "const": "error"
+ },
+ "error": {
+ "type": "string"
+ }
+ },
+ "required": ["status", "error"]
+ }
+ ]
+ },
+ "Event.voice.updated": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "voice.updated"
+ },
+ "properties": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "$ref": "#/components/schemas/VoiceStatus"
+ }
+ },
+ "required": ["status"]
+ }
+ },
+ "required": ["type", "properties"]
+ },
"Event": {
"anyOf": [
{
@@ -8437,6 +8895,9 @@
},
{
"$ref": "#/components/schemas/Event.worktree.failed"
+ },
+ {
+ "$ref": "#/components/schemas/Event.voice.updated"
}
]
},
@@ -8706,6 +9167,11 @@
"default": "ctrl+v",
"type": "string"
},
+ "voice_input": {
+ "description": "Voice input (tap to record, tap to stop)",
+ "default": "\\",
+ "type": "string"
+ },
"input_submit": {
"description": "Submit input",
"default": "return",
@@ -9494,6 +9960,29 @@
"required": ["type", "url"],
"additionalProperties": false
},
+ "VoiceConfig": {
+ "description": "Voice transcription configuration",
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "description": "Enable or disable voice transcription",
+ "type": "boolean"
+ },
+ "model": {
+ "description": "Whisper model size: tiny (75MB), base (142MB), or small (466MB)",
+ "default": "base",
+ "type": "string",
+ "enum": ["tiny", "base", "small"]
+ },
+ "device": {
+ "description": "Device to run the model on: cpu, gpu, or auto",
+ "default": "auto",
+ "type": "string",
+ "enum": ["cpu", "gpu", "auto"]
+ }
+ },
+ "additionalProperties": false
+ },
"LayoutConfig": {
"description": "@deprecated Always uses stretch layout.",
"type": "string",
@@ -9751,6 +10240,9 @@
]
}
},
+ "voice": {
+ "$ref": "#/components/schemas/VoiceConfig"
+ },
"formatter": {
"anyOf": [
{