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": [ {