@markusylisiurunen/tau 0.3.76 → 0.3.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/agent/agent_runtime.js +6 -11
- package/dist/core/agent/agent_runtime.js.map +1 -1
- package/dist/core/cli.js +2 -5
- package/dist/core/cli.js.map +1 -1
- package/dist/core/commands/registry.js +2 -1
- package/dist/core/commands/registry.js.map +1 -1
- package/dist/core/config/builtin_themes.js +0 -30
- package/dist/core/config/builtin_themes.js.map +1 -1
- package/dist/core/config/content_loader.js +19 -308
- package/dist/core/config/content_loader.js.map +1 -1
- package/dist/core/config/index.js +1 -1
- package/dist/core/config/index.js.map +1 -1
- package/dist/core/config/paths.js +0 -1
- package/dist/core/config/paths.js.map +1 -1
- package/dist/core/config/runtime.js +2 -3
- package/dist/core/config/runtime.js.map +1 -1
- package/dist/core/config/runtime_config_snapshot.js +0 -1
- package/dist/core/config/runtime_config_snapshot.js.map +1 -1
- package/dist/core/config/schema.js +17 -234
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/config/skills_loader.js +0 -8
- package/dist/core/config/skills_loader.js.map +1 -1
- package/dist/core/config/theme_variants.js.map +1 -1
- package/dist/core/config/virtual_bundle.js +3 -9
- package/dist/core/config/virtual_bundle.js.map +1 -1
- package/dist/core/config/virtual_defaults.js +0 -2
- package/dist/core/config/virtual_defaults.js.map +1 -1
- package/dist/core/debug.js +8 -25
- package/dist/core/debug.js.map +1 -1
- package/dist/core/diff_review/bridge.js +2 -24
- package/dist/core/diff_review/bridge.js.map +1 -1
- package/dist/core/index.js +0 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/models/catalog.js +2 -3
- package/dist/core/models/catalog.js.map +1 -1
- package/dist/core/personas.js +7 -89
- package/dist/core/personas.js.map +1 -1
- package/dist/core/runtime/agent_model.js +0 -2
- package/dist/core/runtime/agent_model.js.map +1 -1
- package/dist/core/runtime/chat_runtime.js +4 -6
- package/dist/core/runtime/chat_runtime.js.map +1 -1
- package/dist/core/runtime/session_prompt_composer.js +12 -27
- package/dist/core/runtime/session_prompt_composer.js.map +1 -1
- package/dist/core/static/index.js +4 -7
- package/dist/core/static/index.js.map +1 -1
- package/dist/core/static/tau_docs/client-tools.md +1 -1
- package/dist/core/static/tau_docs/config-reference.md +30 -153
- package/dist/core/static/tau_docs/configuration.md +9 -21
- package/dist/core/static/tau_docs/credentials.md +9 -28
- package/dist/core/static/tau_docs/image-generation.md +1 -1
- package/dist/core/static/tau_docs/node-sdk.md +1 -1
- package/dist/core/static/tau_docs/ownership-and-scope.md +5 -5
- package/dist/core/static/tau_docs/pdf-unpacking.md +1 -1
- package/dist/core/static/tau_docs/personas.md +9 -20
- package/dist/core/static/tau_docs/prompts-and-project-context.md +1 -11
- package/dist/core/static/tau_docs/remote-sessions.md +6 -22
- package/dist/core/static/tau_docs/security.md +2 -2
- package/dist/core/static/tau_docs/session-protocol-methods.md +1 -7
- package/dist/core/static/tau_docs/session-protocol.md +2 -0
- package/dist/core/static/tau_docs/sessions.md +2 -14
- package/dist/core/static/tau_docs/skills.md +1 -23
- package/dist/core/static/tau_docs/speech-generation.md +1 -1
- package/dist/core/static/tau_docs/subagents.md +26 -99
- package/dist/core/static/tau_docs/telegram.md +14 -22
- package/dist/core/static/tau_docs/tools.md +6 -6
- package/dist/core/static/tau_docs/troubleshooting.md +19 -20
- package/dist/core/static/tau_docs/tui.md +18 -44
- package/dist/core/subagents/agent_supervisor.js +5 -6
- package/dist/core/subagents/agent_supervisor.js.map +1 -1
- package/dist/core/subagents/format.js +1 -1
- package/dist/core/subagents/format.js.map +1 -1
- package/dist/core/subagents/prompt.js +7 -0
- package/dist/core/subagents/prompt.js.map +1 -0
- package/dist/core/subagents/registry.js +14 -43
- package/dist/core/subagents/registry.js.map +1 -1
- package/dist/core/subagents/types.js +2 -2
- package/dist/core/subagents/types.js.map +1 -1
- package/dist/core/telegram/adapter.js +23 -30
- package/dist/core/telegram/adapter.js.map +1 -1
- package/dist/core/telegram/cli.js +4 -5
- package/dist/core/telegram/cli.js.map +1 -1
- package/dist/core/telegram/config.js +32 -57
- package/dist/core/telegram/config.js.map +1 -1
- package/dist/core/telegram/local_session_client.js +0 -3
- package/dist/core/telegram/local_session_client.js.map +1 -1
- package/dist/core/telegram/runtime.js +3 -5
- package/dist/core/telegram/runtime.js.map +1 -1
- package/dist/core/telegram/session_manager.js +29 -52
- package/dist/core/telegram/session_manager.js.map +1 -1
- package/dist/core/telegram/tts.js +11 -9
- package/dist/core/telegram/tts.js.map +1 -1
- package/dist/core/telegram/workspace.js +13 -18
- package/dist/core/telegram/workspace.js.map +1 -1
- package/dist/core/tool/image_generate.js +37 -14
- package/dist/core/tool/image_generate.js.map +1 -1
- package/dist/core/tool/media.js +84 -4
- package/dist/core/tool/media.js.map +1 -1
- package/dist/core/tool/pdf_unpack.js +17 -10
- package/dist/core/tool/pdf_unpack.js.map +1 -1
- package/dist/core/tool/speech_generate.js +30 -13
- package/dist/core/tool/speech_generate.js.map +1 -1
- package/dist/core/tools/catalog.js +10 -5
- package/dist/core/tools/catalog.js.map +1 -1
- package/dist/core/tools/send_input_to_agent.js +1 -1
- package/dist/core/tools/send_input_to_agent.js.map +1 -1
- package/dist/core/tools/spawn_agent.js +13 -30
- package/dist/core/tools/spawn_agent.js.map +1 -1
- package/dist/core/tools/view_image.js +12 -17
- package/dist/core/tools/view_image.js.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/usage/logs.js.map +1 -1
- package/dist/core/utils/gemini_transcription.js +5 -95
- package/dist/core/utils/gemini_transcription.js.map +1 -1
- package/dist/core/utils/{gemini_speech.js → speech.js} +167 -428
- package/dist/core/utils/speech.js.map +1 -0
- package/dist/core/utils/speech_to_text.js +24 -52
- package/dist/core/utils/speech_to_text.js.map +1 -1
- package/dist/core/utils/speech_to_text_keywords.js +65 -0
- package/dist/core/utils/speech_to_text_keywords.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/diff_tool/app/dist/assets/{angular-html-BjHlbeCk.js → angular-html-CnO8qept.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{angular-ts-5J5hsUV-.js → angular-ts-DPXUio9Z.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{apl-ChIs51_N.js → apl-X8YsymsQ.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{astro-RtGdbaIT.js → astro-BQ9k8AQ0.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{blade-Cuc0ZzS1.js → blade-DZ1LYIzB.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{c-IpjS3QEt.js → c-VLTwNh7z.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{chapel-BvHOmF40.js → chapel-5s-sGN8D.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{cobol-BWLkapNx.js → cobol-C9RzwrVu.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{coffee-lfdf_MuK.js → coffee-BHvjZ343.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{cpp-GvMfdutG.js → cpp-CBTVXbBp.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{crystal--kdmUeXB.js → crystal-DQJua4y4.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{css-CnlOazId.js → css-BQflXFb6.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{edge-CwnxQiFe.js → edge-BmdmXGH4.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{elixir-BVO19qQd.js → elixir-oiOLs98L.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{elm-BK3istEo.js → elm-BgYuiU4E.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{erb-CO4uKwKD.js → erb-CvFrXpyY.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{git-rebase-CDrrgbGV.js → git-rebase-D3Zo1BLH.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{glimmer-js-7CFSyo6c.js → glimmer-js-B4-8E3lJ.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{glimmer-ts-Ofllf2Yo.js → glimmer-ts-CtKkD0t_.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{glsl-K_36bq1K.js → glsl-BAqforx_.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{graphql-BMZnssUN.js → graphql-DuE1RMyL.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{hack-MG_t8QNs.js → hack-CHBcXjz1.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{haml-0hDK-uCt.js → haml-DAs2z3tD.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{handlebars-D4_MRJPB.js → handlebars-CfAla6QK.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{html-BCi--7qp.js → html-D5co0BLD.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{html-derivative-DYq2AWiD.js → html-derivative-BZaWEmAT.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{http-BmLFm2Pm.js → http-Cii5MjJE.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{hurl-CT71cK69.js → hurl-Bur-XdJO.js} +1 -1
- package/dist/diff_tool/app/dist/assets/index-CZUi_I-p.js +340 -0
- package/dist/diff_tool/app/dist/assets/{java-Bm2FCq-d.js → java-DN39wI4w.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{javascript-C7oEny21.js → javascript-VUReF1H4.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{jinja-igQBS-SS.js → jinja-BcrfV1JB.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{jison-BrOUp4rK.js → jison-Bez5Mtmy.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{json-DsuR7Nai.js → json-ChV8m0Le.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{jsx-BdomFXcA.js → jsx-D0SXNtM0.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{julia-DqOG4pbM.js → julia-DvhpcBwG.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{just-ZrSTuT0n.js → just-BGwe8S6x.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{latex-DbmtZIwN.js → latex-pWSag83P.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{liquid-DKnUlRIp.js → liquid-UdnqDjKz.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{lua-Cyq3f59a.js → lua-CjDHeHEr.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{marko-hmOsFPHQ.js → marko-yLX6iW68.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{mdc-BnnQmDC-.js → mdc-BOLj6Oyq.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{nginx-C0zLUSna.js → nginx-5r5xPc2T.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{nim-Oljl3c5n.js → nim-9exKQYMz.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{org-Cp4CA-0c.js → org-B2fpAlbt.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{perl-DstDVYmJ.js → perl-DV2fcwyY.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{php-Cc3bNZ7J.js → php-D-AthX_E.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{pug-B3xDUzl0.js → pug-DuKcQxFj.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{qml-B9iDahKk.js → qml-C16b4SYx.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{r-B7M5ZSBC.js → r-B1ZldnJf.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{razor-BlfV6bjQ.js → razor-TYmslaAB.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{regexp-BVT0NBj6.js → regexp-dWCffmDD.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{rst-Cq-Mcher.js → rst-DEkLr2YR.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{ruby-5uCK_s01.js → ruby-BJnJzr4u.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{sas-m4dYluKN.js → sas-DeRsDbUE.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{scss-DFiJcBQH.js → scss-SGV_CwKm.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{shellscript-DCtSkJJQ.js → shellscript-BI7khmmR.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{shellsession-_DntbpLM.js → shellsession-Nupw_Vvw.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{soy-Dd1fWsG8.js → soy-B96cBzxv.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{sql-Bygp5F-5.js → sql-CsIdKfaO.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{stata-CqIA1VQD.js → stata-Dnq48cE6.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{surrealql-C2-2C-z3.js → surrealql-D6lw4TiD.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{svelte-CrTy7VSb.js → svelte-ChVoq9tB.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{templ-B_h9AHFJ.js → templ-Dh1EXuHD.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{tex-AwlGuBDG.js → tex-KTYusNFe.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{ts-tags--oqU6stH.js → ts-tags-DC2RU1_R.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{tsx-Cx-oUPFA.js → tsx-DtIM39Jy.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{twig-DMQw3R9O.js → twig-C6D_TA3n.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{typescript-BCtu9W7E.js → typescript-BGAE5iuw.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{typst-jGrqJHSo.js → typst-nWoGZ22y.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{vue-html-DTMalvg7.js → vue-html-prNdIjLu.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{vue-GFZcsvXd.js → vue-iyHf-l4z.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{vue-vine-BwDeqLrM.js → vue-vine-KSSlnVeg.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{xml-CoXyuOzf.js → xml-DdCC83Xd.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{xsl-BQFm34CN.js → xsl-DSqZxsez.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{yaml-CbO2A_pF.js → yaml-CZcBUWvf.js} +1 -1
- package/dist/diff_tool/app/dist/index.html +1 -1
- package/dist/diff_tool/http_server.js +2 -9
- package/dist/diff_tool/http_server.js.map +1 -1
- package/dist/diff_tool/index.js +2 -2
- package/dist/diff_tool/index.js.map +1 -1
- package/dist/diff_tool/launcher.js +14 -8
- package/dist/diff_tool/launcher.js.map +1 -1
- package/dist/diff_tool/protocol_client.js +0 -7
- package/dist/diff_tool/protocol_client.js.map +1 -1
- package/dist/diff_tool/review_state.js +4 -10
- package/dist/diff_tool/review_state.js.map +1 -1
- package/dist/diff_tool/review_state_persistence.js +12 -7
- package/dist/diff_tool/review_state_persistence.js.map +1 -1
- package/dist/diff_tool/shared_types.js +0 -48
- package/dist/diff_tool/shared_types.js.map +1 -1
- package/dist/execution/fly_sprite_execution_environment.js +4 -2
- package/dist/execution/fly_sprite_execution_environment.js.map +1 -1
- package/dist/host/execution_runtime.js +1 -1
- package/dist/host/execution_runtime.js.map +1 -1
- package/dist/host/hosted_ephemeral_agent_session.js +1 -1
- package/dist/host/hosted_ephemeral_agent_session.js.map +1 -1
- package/dist/host/local_session_host.js +8 -18
- package/dist/host/local_session_host.js.map +1 -1
- package/dist/main.js +7 -93
- package/dist/main.js.map +1 -1
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/index.js.map +1 -1
- package/dist/protocol/session_protocol.d.ts +4 -23
- package/dist/protocol/session_protocol.js +3 -30
- package/dist/protocol/session_protocol.js.map +1 -1
- package/dist/sdk/local_client.js +0 -6
- package/dist/sdk/local_client.js.map +1 -1
- package/dist/store/session_snapshot_migrations.js +32 -1
- package/dist/store/session_snapshot_migrations.js.map +1 -1
- package/dist/tui/chat_controller/diff_review_service.js +0 -15
- package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
- package/dist/tui/chat_view.js +6 -4
- package/dist/tui/chat_view.js.map +1 -1
- package/dist/tui/listen_capture.js +7 -26
- package/dist/tui/listen_capture.js.map +1 -1
- package/dist/tui/session_chat_app.js +4 -21
- package/dist/tui/session_chat_app.js.map +1 -1
- package/dist/tui/session_chat_controller.js +81 -59
- package/dist/tui/session_chat_controller.js.map +1 -1
- package/dist/tui/speech_playback.js +10 -8
- package/dist/tui/speech_playback.js.map +1 -1
- package/dist/tui/ui/custom_editor.js +21 -15
- package/dist/tui/ui/custom_editor.js.map +1 -1
- package/dist/tui/ui/pending_messages.js +2 -1
- package/dist/tui/ui/pending_messages.js.map +1 -1
- package/dist/tui/ui/slash_autocomplete.js +1 -8
- package/dist/tui/ui/slash_autocomplete.js.map +1 -1
- package/dist/tui/ui/subagent_panel.js +1 -12
- package/dist/tui/ui/subagent_panel.js.map +1 -1
- package/package.json +3 -3
- package/dist/core/config/diff_tool.js +0 -51
- package/dist/core/config/diff_tool.js.map +0 -1
- package/dist/core/install/cli.js +0 -260
- package/dist/core/install/cli.js.map +0 -1
- package/dist/core/models/tau_model_overrides.js +0 -4
- package/dist/core/models/tau_model_overrides.js.map +0 -1
- package/dist/core/subagents/default.js +0 -8
- package/dist/core/subagents/default.js.map +0 -1
- package/dist/core/utils/gemini_speech.js.map +0 -1
- package/dist/core/utils/openai_transcription.js +0 -594
- package/dist/core/utils/openai_transcription.js.map +0 -1
- package/dist/diff_tool/app/dist/assets/index-DyaXBald.js +0 -340
- package/dist/execution/cloudflare_sandbox_execution_environment.js +0 -563
- package/dist/execution/cloudflare_sandbox_execution_environment.js.map +0 -1
- package/dist/execution/sandbox_tool_helpers.js +0 -30
- package/dist/execution/sandbox_tool_helpers.js.map +0 -1
- package/dist/starter/prompts/approval-review-branch.md +0 -7
- package/dist/starter/prompts/approval-review-current-changes.md +0 -7
- package/dist/starter/prompts/approval-review-last-commit.md +0 -7
- package/dist/starter/prompts/commit-all.md +0 -9
- package/dist/starter/prompts/commit-staged.md +0 -9
- package/dist/starter/prompts/guided-review-branch.md +0 -7
- package/dist/starter/prompts/guided-review-current-changes.md +0 -7
- package/dist/starter/prompts/guided-review-last-commit.md +0 -7
- package/dist/starter/prompts/plan.md +0 -7
- package/dist/starter/prompts/review-branch.md +0 -7
- package/dist/starter/prompts/review-current-changes.md +0 -7
- package/dist/starter/prompts/review-last-commit.md +0 -7
- package/dist/starter/prompts/rewrite-prompt.md +0 -89
- package/dist/starter/skills/approval-review/SKILL.md +0 -219
- package/dist/starter/skills/code-review/SKILL.md +0 -102
- package/dist/starter/skills/guided-review/SKILL.md +0 -152
- package/dist/starter/skills/plan/SKILL.md +0 -53
- /package/dist/core/static/prompts/{default-subagent-wrapper.md → subagent-wrapper.md} +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
const
|
|
3
|
-
export const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
2
|
+
const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
|
|
3
|
+
export const SPEECH_PLAYBACK_RATE = 1.15;
|
|
4
|
+
const SPEECH_REWRITE_MODEL = "gpt-6-luna";
|
|
5
|
+
const ELEVENLABS_BASE_URL = "https://api.elevenlabs.io/v1";
|
|
6
|
+
const SPEECH_MODEL = "eleven_v4_turbo";
|
|
7
|
+
const DEFAULT_VOICE_ID = "QtY3JBOUKEB5xzrRfOKc";
|
|
8
|
+
const FALLBACK_VOICE_ID = "AaOhDHYJ1XLZk74lXhdE";
|
|
9
|
+
const SPEECH_DELIVERY_NOTE = "[Brisk but relaxed, speaking naturally to a colleague] ";
|
|
10
|
+
const SPEECH_REQUEST_TIMEOUT_MS = 120_000;
|
|
11
|
+
export const SPEECH_SAMPLE_RATE_HZ = 24000;
|
|
12
|
+
export const SPEECH_CHANNEL_COUNT = 1;
|
|
13
|
+
export const SPEECH_BITS_PER_SAMPLE = 16;
|
|
12
14
|
const SPEECH_REWRITE_TIMEOUT_MS = 60_000;
|
|
13
15
|
const COMPLETE_TTS_CONCURRENCY = 3;
|
|
14
16
|
const MAX_SPEECH_SEGMENT_SECONDS = 120;
|
|
@@ -17,42 +19,18 @@ const MAX_SPEECH_SEGMENT_WEIGHT = MAX_SPEECH_SEGMENT_SECONDS * ESTIMATED_SPEECH_
|
|
|
17
19
|
const MAX_SPEECH_SOURCE_CHARACTERS = 10_000;
|
|
18
20
|
const MAX_SPOKEN_TEXT_CHARACTERS = 10_000;
|
|
19
21
|
const MAX_SPEECH_PCM_BYTES = 32 * 1024 * 1024;
|
|
20
|
-
const TTS_MAX_OUTPUT_TOKENS = 8192;
|
|
21
|
-
const RETRYABLE_TTS_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
22
22
|
const errorPayloadSchema = z.object({
|
|
23
23
|
error: z
|
|
24
24
|
.object({
|
|
25
25
|
message: z.string().trim().min(1).optional(),
|
|
26
|
-
status: z.string().trim().min(1).optional(),
|
|
27
|
-
code: z.number().int().optional(),
|
|
28
26
|
})
|
|
29
27
|
.optional(),
|
|
30
28
|
});
|
|
31
|
-
|
|
32
|
-
status;
|
|
33
|
-
constructor(message, status) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.name = "GeminiApiError";
|
|
36
|
-
this.status = status;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
class GeminiTtsResponseError extends Error {
|
|
40
|
-
constructor(message) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.name = "GeminiTtsResponseError";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
class GeminiTtsOutputLimitError extends Error {
|
|
46
|
-
constructor() {
|
|
47
|
-
super("Gemini TTS reached its output token limit");
|
|
48
|
-
this.name = "GeminiTtsOutputLimitError";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export async function* generateGeminiSpeechAudio(options) {
|
|
29
|
+
export async function* generateSpeechAudio(options) {
|
|
52
30
|
const abortController = createLinkedAbortController(options.signal);
|
|
53
31
|
let completed = false;
|
|
54
32
|
try {
|
|
55
|
-
const prepared = await
|
|
33
|
+
const prepared = await prepareSpeech(options, abortController.signal);
|
|
56
34
|
for await (const chunk of synthesizeSpeechAudioSegmentsInOrder({
|
|
57
35
|
...prepared,
|
|
58
36
|
signal: abortController.signal,
|
|
@@ -65,9 +43,9 @@ export async function* generateGeminiSpeechAudio(options) {
|
|
|
65
43
|
total: prepared.spokenSegments.length,
|
|
66
44
|
audio: encodeWaveFile({
|
|
67
45
|
pcmAudio: chunk.pcmAudio,
|
|
68
|
-
sampleRateHz:
|
|
69
|
-
channelCount:
|
|
70
|
-
bitsPerSample:
|
|
46
|
+
sampleRateHz: SPEECH_SAMPLE_RATE_HZ,
|
|
47
|
+
channelCount: SPEECH_CHANNEL_COUNT,
|
|
48
|
+
bitsPerSample: SPEECH_BITS_PER_SAMPLE,
|
|
71
49
|
}),
|
|
72
50
|
mimeType: "audio/wav",
|
|
73
51
|
};
|
|
@@ -81,12 +59,12 @@ export async function* generateGeminiSpeechAudio(options) {
|
|
|
81
59
|
abortController.dispose();
|
|
82
60
|
}
|
|
83
61
|
}
|
|
84
|
-
export async function*
|
|
62
|
+
export async function* streamSpeechPcm(options) {
|
|
85
63
|
const abortController = createLinkedAbortController(options.signal);
|
|
86
64
|
let completed = false;
|
|
87
65
|
let totalPcmBytes = 0;
|
|
88
66
|
try {
|
|
89
|
-
const prepared = await
|
|
67
|
+
const prepared = await prepareSpeech(options, abortController.signal);
|
|
90
68
|
const total = prepared.spokenSegments.length;
|
|
91
69
|
const accountAudio = (audio) => {
|
|
92
70
|
totalPcmBytes += audio.length;
|
|
@@ -101,23 +79,16 @@ export async function* streamGeminiSpeechPcm(options) {
|
|
|
101
79
|
accountAudio,
|
|
102
80
|
}).then((audio) => ({ audio }), (error) => ({ error }));
|
|
103
81
|
let ready = 0;
|
|
104
|
-
const firstStream =
|
|
82
|
+
const firstStream = streamSpeechSegment({
|
|
105
83
|
...prepared,
|
|
106
84
|
spokenText: prepared.spokenSegments[0],
|
|
107
85
|
signal: abortController.signal,
|
|
108
86
|
initialBufferBytes: Math.max(0, Math.trunc(options.initialBufferBytes ?? 0)),
|
|
109
87
|
});
|
|
110
|
-
const firstIterator = firstStream[Symbol.asyncIterator]();
|
|
111
|
-
let nextAudio = firstIterator.next();
|
|
112
88
|
let prefetched = total > 1 ? prefetch(1) : undefined;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
accountAudio(next.value);
|
|
119
|
-
yield { index: 0, total, audio: next.value };
|
|
120
|
-
nextAudio = firstIterator.next();
|
|
89
|
+
for await (const audio of firstStream) {
|
|
90
|
+
accountAudio(audio);
|
|
91
|
+
yield { index: 0, total, audio };
|
|
121
92
|
}
|
|
122
93
|
ready += 1;
|
|
123
94
|
await options.onSegmentProgress?.({ ready, total });
|
|
@@ -126,7 +97,7 @@ export async function* streamGeminiSpeechPcm(options) {
|
|
|
126
97
|
if ("error" in outcome) {
|
|
127
98
|
throw outcome.error instanceof Error
|
|
128
99
|
? outcome.error
|
|
129
|
-
: new Error("
|
|
100
|
+
: new Error("ElevenLabs speech request failed");
|
|
130
101
|
}
|
|
131
102
|
prefetched = index + 1 < total ? prefetch(index + 1) : undefined;
|
|
132
103
|
yield { index, total, audio: outcome.audio };
|
|
@@ -142,7 +113,7 @@ export async function* streamGeminiSpeechPcm(options) {
|
|
|
142
113
|
abortController.dispose();
|
|
143
114
|
}
|
|
144
115
|
}
|
|
145
|
-
async function
|
|
116
|
+
async function prepareSpeech(options, signal) {
|
|
146
117
|
const sourceText = options.sourceText.trim();
|
|
147
118
|
if (!sourceText) {
|
|
148
119
|
throw new Error("speech source text was empty");
|
|
@@ -150,9 +121,13 @@ async function prepareGeminiSpeech(options, signal) {
|
|
|
150
121
|
if (exceedsUnicodeCharacterLimit(sourceText, MAX_SPEECH_SOURCE_CHARACTERS)) {
|
|
151
122
|
throw new Error("speech source text exceeds 10,000 characters");
|
|
152
123
|
}
|
|
153
|
-
const apiKey = options.
|
|
154
|
-
|
|
155
|
-
|
|
124
|
+
const apiKey = options.elevenLabsApiKey.trim();
|
|
125
|
+
const openAIApiKey = options.openAIApiKey.trim();
|
|
126
|
+
if (!apiKey || !openAIApiKey) {
|
|
127
|
+
throw new Error("OpenAI and ElevenLabs API keys are required for speech");
|
|
128
|
+
}
|
|
129
|
+
if (options.voiceId !== undefined && !options.voiceId.trim()) {
|
|
130
|
+
throw new Error("speech voice ID must not be empty");
|
|
156
131
|
}
|
|
157
132
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
158
133
|
await options.onStageChange?.("rewriting");
|
|
@@ -162,8 +137,7 @@ async function prepareGeminiSpeech(options, signal) {
|
|
|
162
137
|
let spokenText;
|
|
163
138
|
try {
|
|
164
139
|
spokenText = await rewriteTextForSpeech({
|
|
165
|
-
apiKey,
|
|
166
|
-
model: options.rewriteModel ?? DEFAULT_GEMINI_SPEECH_REWRITE_MODEL,
|
|
140
|
+
apiKey: openAIApiKey,
|
|
167
141
|
sourceText,
|
|
168
142
|
fetchImpl,
|
|
169
143
|
signal: rewriteController.signal,
|
|
@@ -184,43 +158,62 @@ async function prepareGeminiSpeech(options, signal) {
|
|
|
184
158
|
}
|
|
185
159
|
const spokenSegments = splitSpeechSegments(spokenText);
|
|
186
160
|
await options.onStageChange?.("generating");
|
|
161
|
+
const voiceId = await resolveSpeechVoice({
|
|
162
|
+
apiKey,
|
|
163
|
+
voiceId: options.voiceId,
|
|
164
|
+
fetchImpl,
|
|
165
|
+
signal,
|
|
166
|
+
});
|
|
187
167
|
await options.onSegmentProgress?.({ ready: 0, total: spokenSegments.length });
|
|
188
168
|
return {
|
|
189
169
|
apiKey,
|
|
190
|
-
|
|
191
|
-
voiceName: options.voiceName ?? DEFAULT_GEMINI_TTS_VOICE_NAME,
|
|
170
|
+
voiceId,
|
|
192
171
|
spokenSegments,
|
|
193
172
|
fetchImpl,
|
|
194
|
-
maxAttempts: options.maxTtsAttempts ?? DEFAULT_TTS_MAX_ATTEMPTS,
|
|
195
173
|
};
|
|
196
174
|
}
|
|
197
175
|
async function rewriteTextForSpeech(args) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
body: {
|
|
204
|
-
contents: [
|
|
205
|
-
{
|
|
206
|
-
parts: [
|
|
207
|
-
{
|
|
208
|
-
text: buildSpeechRewritePrompt(args.sourceText),
|
|
209
|
-
},
|
|
210
|
-
],
|
|
211
|
-
},
|
|
212
|
-
],
|
|
213
|
-
generationConfig: {
|
|
214
|
-
thinkingConfig: {
|
|
215
|
-
thinkingLevel: DEFAULT_GEMINI_SPEECH_REWRITE_THINKING_LEVEL,
|
|
216
|
-
},
|
|
217
|
-
},
|
|
176
|
+
const response = await args.fetchImpl(OPENAI_RESPONSES_URL, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: {
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
Authorization: `Bearer ${args.apiKey}`,
|
|
218
181
|
},
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
model: SPEECH_REWRITE_MODEL,
|
|
184
|
+
input: buildSpeechRewritePrompt(args.sourceText),
|
|
185
|
+
reasoning: { effort: "none" },
|
|
186
|
+
store: false,
|
|
187
|
+
}),
|
|
188
|
+
signal: args.signal,
|
|
219
189
|
});
|
|
220
|
-
const
|
|
221
|
-
if (!
|
|
222
|
-
|
|
190
|
+
const payload = await response.json();
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const parsed = errorPayloadSchema.safeParse(payload);
|
|
193
|
+
throw new Error(parsed.success && parsed.data.error?.message
|
|
194
|
+
? parsed.data.error.message
|
|
195
|
+
: `OpenAI speech rewrite failed (HTTP ${response.status})`);
|
|
196
|
+
}
|
|
197
|
+
if (!isObject(payload) || payload.status !== "completed" || !Array.isArray(payload.output)) {
|
|
198
|
+
throw new Error("OpenAI speech rewrite did not complete");
|
|
199
|
+
}
|
|
200
|
+
const text = [];
|
|
201
|
+
for (const item of payload.output) {
|
|
202
|
+
if (!isObject(item) || item.type !== "message")
|
|
203
|
+
continue;
|
|
204
|
+
if (item.status !== "completed" || !Array.isArray(item.content)) {
|
|
205
|
+
throw new Error("OpenAI speech rewrite returned an incomplete message");
|
|
206
|
+
}
|
|
207
|
+
for (const part of item.content) {
|
|
208
|
+
if (!isObject(part) || part.type !== "output_text" || typeof part.text !== "string") {
|
|
209
|
+
throw new Error("OpenAI speech rewrite did not return text");
|
|
210
|
+
}
|
|
211
|
+
text.push(part.text);
|
|
212
|
+
}
|
|
223
213
|
}
|
|
214
|
+
const rewrittenText = text.join("").trim();
|
|
215
|
+
if (!rewrittenText)
|
|
216
|
+
throw new Error("OpenAI speech rewrite returned empty text");
|
|
224
217
|
return rewrittenText;
|
|
225
218
|
}
|
|
226
219
|
async function* synthesizeSpeechAudioSegmentsInOrder(args) {
|
|
@@ -299,7 +292,7 @@ async function* synthesizeSpeechAudioSegmentsInOrder(args) {
|
|
|
299
292
|
continue;
|
|
300
293
|
}
|
|
301
294
|
if (failure !== undefined) {
|
|
302
|
-
throw failure instanceof Error ? failure : new Error("
|
|
295
|
+
throw failure instanceof Error ? failure : new Error("ElevenLabs speech request failed");
|
|
303
296
|
}
|
|
304
297
|
await waitForWake();
|
|
305
298
|
}
|
|
@@ -310,323 +303,120 @@ async function* synthesizeSpeechAudioSegmentsInOrder(args) {
|
|
|
310
303
|
}
|
|
311
304
|
}
|
|
312
305
|
async function synthesizeSpeechAudioSegment(args) {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
306
|
+
const chunks = [];
|
|
307
|
+
for await (const audio of requestSpeechStream(args)) {
|
|
308
|
+
chunks.push(audio);
|
|
309
|
+
}
|
|
310
|
+
return Buffer.concat(chunks);
|
|
311
|
+
}
|
|
312
|
+
async function* streamSpeechSegment(args) {
|
|
313
|
+
const bufferedAudio = [];
|
|
314
|
+
let bufferedAudioBytes = 0;
|
|
315
|
+
let emittedAudio = false;
|
|
316
|
+
for await (const audio of requestSpeechStream(args)) {
|
|
317
|
+
if (!emittedAudio) {
|
|
318
|
+
bufferedAudio.push(audio);
|
|
319
|
+
bufferedAudioBytes += audio.length;
|
|
320
|
+
if (bufferedAudioBytes < args.initialBufferBytes) {
|
|
321
|
+
continue;
|
|
328
322
|
}
|
|
329
|
-
|
|
323
|
+
emittedAudio = true;
|
|
324
|
+
yield Buffer.concat(bufferedAudio);
|
|
325
|
+
bufferedAudio.length = 0;
|
|
330
326
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (args.signal?.aborted || !isRetryableTtsError(error) || attempt >= maxAttempts) {
|
|
334
|
-
throw error;
|
|
335
|
-
}
|
|
336
|
-
await waitForRetryDelay(attempt, args.signal);
|
|
327
|
+
else {
|
|
328
|
+
yield audio;
|
|
337
329
|
}
|
|
338
330
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
async function* streamSpeechSegmentWithRetries(args) {
|
|
342
|
-
const maxAttempts = Math.max(1, Math.trunc(args.maxAttempts));
|
|
343
|
-
let lastError;
|
|
344
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
345
|
-
const bufferedAudio = [];
|
|
346
|
-
let bufferedAudioBytes = 0;
|
|
347
|
-
let emittedAudio = false;
|
|
348
|
-
try {
|
|
349
|
-
for await (const audio of requestGeminiSpeechStream(args)) {
|
|
350
|
-
if (!emittedAudio && bufferedAudioBytes < args.initialBufferBytes) {
|
|
351
|
-
bufferedAudio.push(audio);
|
|
352
|
-
bufferedAudioBytes += audio.length;
|
|
353
|
-
if (bufferedAudioBytes < args.initialBufferBytes) {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
emittedAudio = true;
|
|
357
|
-
yield Buffer.concat(bufferedAudio);
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
emittedAudio = true;
|
|
361
|
-
yield audio;
|
|
362
|
-
}
|
|
363
|
-
if (bufferedAudio.length > 0 && !emittedAudio) {
|
|
364
|
-
emittedAudio = true;
|
|
365
|
-
yield Buffer.concat(bufferedAudio);
|
|
366
|
-
}
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
catch (error) {
|
|
370
|
-
lastError = error;
|
|
371
|
-
if (emittedAudio ||
|
|
372
|
-
args.signal?.aborted ||
|
|
373
|
-
!isRetryableTtsError(error) ||
|
|
374
|
-
attempt >= maxAttempts) {
|
|
375
|
-
throw error;
|
|
376
|
-
}
|
|
377
|
-
await waitForRetryDelay(attempt, args.signal);
|
|
378
|
-
}
|
|
331
|
+
if (!emittedAudio && bufferedAudio.length > 0) {
|
|
332
|
+
yield Buffer.concat(bufferedAudio);
|
|
379
333
|
}
|
|
380
|
-
throw lastError instanceof Error ? lastError : new Error("Gemini TTS request failed");
|
|
381
334
|
}
|
|
382
335
|
async function collectStreamingSpeechSegment(args) {
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
386
|
-
const chunks = [];
|
|
387
|
-
try {
|
|
388
|
-
for await (const audio of requestGeminiSpeechStream(args)) {
|
|
389
|
-
chunks.push(audio);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
catch (error) {
|
|
393
|
-
lastError = error;
|
|
394
|
-
if (args.signal?.aborted || !isRetryableTtsError(error) || attempt >= maxAttempts) {
|
|
395
|
-
throw error;
|
|
396
|
-
}
|
|
397
|
-
await waitForRetryDelay(attempt, args.signal);
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
const audio = Buffer.concat(chunks);
|
|
336
|
+
const chunks = [];
|
|
337
|
+
for await (const audio of requestSpeechStream(args)) {
|
|
401
338
|
args.accountAudio(audio);
|
|
402
|
-
|
|
339
|
+
chunks.push(audio);
|
|
403
340
|
}
|
|
404
|
-
|
|
341
|
+
return Buffer.concat(chunks);
|
|
405
342
|
}
|
|
406
|
-
async function
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
let completed = false;
|
|
420
|
-
for await (const payload of parseGeminiSse(response.body)) {
|
|
421
|
-
const finishReason = getGeminiFinishReason(payload);
|
|
422
|
-
if (finishReason) {
|
|
423
|
-
assertGeminiTtsFinishReason(finishReason);
|
|
424
|
-
completed = finishReason === "STOP";
|
|
343
|
+
async function resolveSpeechVoice(args) {
|
|
344
|
+
const voiceIds = args.voiceId === undefined ? [DEFAULT_VOICE_ID, FALLBACK_VOICE_ID] : [args.voiceId.trim()];
|
|
345
|
+
for (const voiceId of voiceIds) {
|
|
346
|
+
const response = await args.fetchImpl(`${ELEVENLABS_BASE_URL}/voices/${encodeURIComponent(voiceId)}`, {
|
|
347
|
+
headers: { "xi-api-key": args.apiKey },
|
|
348
|
+
signal: AbortSignal.any([args.signal, AbortSignal.timeout(SPEECH_REQUEST_TIMEOUT_MS)]),
|
|
349
|
+
});
|
|
350
|
+
if (response.ok) {
|
|
351
|
+
const payload = await response.json();
|
|
352
|
+
if (!isObject(payload) || payload.voice_id !== voiceId) {
|
|
353
|
+
throw new Error("ElevenLabs returned invalid voice metadata");
|
|
354
|
+
}
|
|
355
|
+
return voiceId;
|
|
425
356
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
357
|
+
const payload = await response.json().catch(() => undefined);
|
|
358
|
+
const detail = isObject(payload) && isObject(payload.detail) ? payload.detail : undefined;
|
|
359
|
+
const unavailable = (response.status === 400 || response.status === 404) && detail?.status === "voice_not_found";
|
|
360
|
+
if (!unavailable) {
|
|
361
|
+
throw new Error(`ElevenLabs voice lookup failed (HTTP ${response.status})`);
|
|
429
362
|
}
|
|
430
363
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (!completed) {
|
|
435
|
-
throw new GeminiTtsResponseError("Gemini TTS stream ended without a stop response");
|
|
436
|
-
}
|
|
364
|
+
throw new Error(args.voiceId === undefined
|
|
365
|
+
? "Neither Maisie nor Caleb is available from ElevenLabs"
|
|
366
|
+
: "The configured ElevenLabs speech voice is unavailable");
|
|
437
367
|
}
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
parts: [
|
|
443
|
-
{
|
|
444
|
-
text: buildSpeechSynthesisPrompt(spokenText),
|
|
445
|
-
},
|
|
446
|
-
],
|
|
447
|
-
},
|
|
448
|
-
],
|
|
449
|
-
generationConfig: {
|
|
450
|
-
responseModalities: ["AUDIO"],
|
|
451
|
-
maxOutputTokens: TTS_MAX_OUTPUT_TOKENS,
|
|
452
|
-
speechConfig: {
|
|
453
|
-
voiceConfig: {
|
|
454
|
-
prebuiltVoiceConfig: {
|
|
455
|
-
voiceName,
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
},
|
|
459
|
-
},
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
async function requestGeminiGenerateContent(args) {
|
|
463
|
-
const response = await requestGeminiResponse({ ...args, method: "generateContent" });
|
|
464
|
-
const responseText = await response.text();
|
|
465
|
-
try {
|
|
466
|
-
return responseText ? JSON.parse(responseText) : undefined;
|
|
467
|
-
}
|
|
468
|
-
catch {
|
|
469
|
-
return undefined;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
async function requestGeminiResponse(args) {
|
|
473
|
-
const response = await args.fetchImpl(`${GEMINI_GENERATE_CONTENT_BASE_URL}/${encodeURIComponent(args.model)}:${args.method}`, {
|
|
368
|
+
async function* requestSpeechStream(args) {
|
|
369
|
+
const timeout = AbortSignal.timeout(SPEECH_REQUEST_TIMEOUT_MS);
|
|
370
|
+
const signal = args.signal ? AbortSignal.any([args.signal, timeout]) : timeout;
|
|
371
|
+
const response = await args.fetchImpl(`${ELEVENLABS_BASE_URL}/text-to-speech/${encodeURIComponent(args.voiceId)}/stream?output_format=pcm_24000`, {
|
|
474
372
|
method: "POST",
|
|
475
|
-
headers: {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
signal
|
|
373
|
+
headers: { "Content-Type": "application/json", "xi-api-key": args.apiKey },
|
|
374
|
+
body: JSON.stringify({
|
|
375
|
+
model_id: SPEECH_MODEL,
|
|
376
|
+
text: SPEECH_DELIVERY_NOTE + args.spokenText,
|
|
377
|
+
}),
|
|
378
|
+
signal,
|
|
481
379
|
});
|
|
482
|
-
if (response.ok) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const fallbackMessage = responseText.trim() || `HTTP ${response.status}`;
|
|
495
|
-
const message = parsed.success
|
|
496
|
-
? (parsed.data.error?.message ?? fallbackMessage)
|
|
497
|
-
: fallbackMessage;
|
|
498
|
-
throw new GeminiApiError(message, response.status);
|
|
499
|
-
}
|
|
500
|
-
async function* parseGeminiSse(body) {
|
|
501
|
-
const reader = body.getReader();
|
|
502
|
-
const decoder = new TextDecoder();
|
|
503
|
-
let buffered = "";
|
|
380
|
+
if (!response.ok) {
|
|
381
|
+
await response.body?.cancel();
|
|
382
|
+
throw new Error(`ElevenLabs speech generation failed (HTTP ${response.status})`);
|
|
383
|
+
}
|
|
384
|
+
const contentType = response.headers.get("content-type")?.split(";")[0]?.trim();
|
|
385
|
+
if (contentType !== "audio/pcm" || !response.body) {
|
|
386
|
+
await response.body?.cancel();
|
|
387
|
+
throw new Error("ElevenLabs speech response did not include PCM audio");
|
|
388
|
+
}
|
|
389
|
+
const reader = response.body.getReader();
|
|
390
|
+
let totalBytes = 0;
|
|
391
|
+
let pendingByte = Buffer.alloc(0);
|
|
504
392
|
let completed = false;
|
|
505
393
|
try {
|
|
506
394
|
while (true) {
|
|
395
|
+
signal.throwIfAborted();
|
|
507
396
|
const { done, value } = await reader.read();
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const payload = parseSseEvent(event);
|
|
514
|
-
if (payload !== undefined) {
|
|
515
|
-
yield payload;
|
|
516
|
-
}
|
|
517
|
-
boundary = findSseEventBoundary(buffered);
|
|
518
|
-
}
|
|
519
|
-
if (done) {
|
|
520
|
-
const payload = parseSseEvent(buffered);
|
|
521
|
-
if (payload !== undefined) {
|
|
522
|
-
yield payload;
|
|
523
|
-
}
|
|
524
|
-
completed = true;
|
|
525
|
-
return;
|
|
397
|
+
if (done)
|
|
398
|
+
break;
|
|
399
|
+
totalBytes += value.length;
|
|
400
|
+
if (totalBytes > MAX_SPEECH_PCM_BYTES) {
|
|
401
|
+
throw new Error("generated speech audio exceeds the 32 MiB limit");
|
|
526
402
|
}
|
|
403
|
+
const audio = Buffer.concat([pendingByte, value]);
|
|
404
|
+
const alignedLength = audio.length - (audio.length % 2);
|
|
405
|
+
pendingByte = Buffer.from(audio.subarray(alignedLength));
|
|
406
|
+
if (alignedLength > 0)
|
|
407
|
+
yield audio.subarray(0, alignedLength);
|
|
408
|
+
}
|
|
409
|
+
if (totalBytes === 0 || pendingByte.length > 0) {
|
|
410
|
+
throw new Error("ElevenLabs returned empty or incomplete PCM audio");
|
|
527
411
|
}
|
|
412
|
+
completed = true;
|
|
528
413
|
}
|
|
529
414
|
finally {
|
|
530
|
-
if (!completed)
|
|
415
|
+
if (!completed)
|
|
531
416
|
await reader.cancel().catch(() => { });
|
|
532
|
-
}
|
|
533
417
|
reader.releaseLock();
|
|
534
418
|
}
|
|
535
419
|
}
|
|
536
|
-
function findSseEventBoundary(text) {
|
|
537
|
-
const match = /\r?\n\r?\n/u.exec(text);
|
|
538
|
-
return match ? { index: match.index, length: match[0].length } : undefined;
|
|
539
|
-
}
|
|
540
|
-
function parseSseEvent(event) {
|
|
541
|
-
const data = event
|
|
542
|
-
.split(/\r?\n/u)
|
|
543
|
-
.filter((line) => line.startsWith("data:"))
|
|
544
|
-
.map((line) => line.slice(5).trimStart())
|
|
545
|
-
.join("\n")
|
|
546
|
-
.trim();
|
|
547
|
-
if (!data || data === "[DONE]") {
|
|
548
|
-
return undefined;
|
|
549
|
-
}
|
|
550
|
-
try {
|
|
551
|
-
return JSON.parse(data);
|
|
552
|
-
}
|
|
553
|
-
catch {
|
|
554
|
-
throw new GeminiTtsResponseError("Gemini TTS returned malformed streaming data");
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
function extractGeminiText(payload) {
|
|
558
|
-
if (!isObject(payload) || !Array.isArray(payload.candidates)) {
|
|
559
|
-
return "";
|
|
560
|
-
}
|
|
561
|
-
for (const candidate of payload.candidates) {
|
|
562
|
-
if (!isObject(candidate) ||
|
|
563
|
-
!isObject(candidate.content) ||
|
|
564
|
-
!Array.isArray(candidate.content.parts)) {
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
const text = candidate.content.parts
|
|
568
|
-
.map((part) => (isObject(part) && typeof part.text === "string" ? part.text : ""))
|
|
569
|
-
.join("")
|
|
570
|
-
.trim();
|
|
571
|
-
if (text) {
|
|
572
|
-
return text;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return "";
|
|
576
|
-
}
|
|
577
|
-
function assertGeminiTtsCompleted(payload) {
|
|
578
|
-
const finishReason = getGeminiFinishReason(payload);
|
|
579
|
-
if (finishReason) {
|
|
580
|
-
assertGeminiTtsFinishReason(finishReason);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
function assertGeminiTtsFinishReason(finishReason) {
|
|
584
|
-
if (finishReason === "MAX_TOKENS") {
|
|
585
|
-
throw new GeminiTtsOutputLimitError();
|
|
586
|
-
}
|
|
587
|
-
if (finishReason !== "STOP") {
|
|
588
|
-
throw new GeminiTtsResponseError(`Gemini TTS stopped with finish reason '${finishReason}'`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
function getGeminiFinishReason(payload) {
|
|
592
|
-
if (!isObject(payload) || !Array.isArray(payload.candidates)) {
|
|
593
|
-
return undefined;
|
|
594
|
-
}
|
|
595
|
-
for (const candidate of payload.candidates) {
|
|
596
|
-
if (isObject(candidate) && typeof candidate.finishReason === "string") {
|
|
597
|
-
return candidate.finishReason;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return undefined;
|
|
601
|
-
}
|
|
602
|
-
function extractGeminiInlineAudioData(payload) {
|
|
603
|
-
return extractGeminiInlineAudioDataParts(payload)[0];
|
|
604
|
-
}
|
|
605
|
-
function extractGeminiInlineAudioDataParts(payload) {
|
|
606
|
-
if (!isObject(payload) || !Array.isArray(payload.candidates)) {
|
|
607
|
-
return [];
|
|
608
|
-
}
|
|
609
|
-
const audioData = [];
|
|
610
|
-
for (const candidate of payload.candidates) {
|
|
611
|
-
if (!isObject(candidate) ||
|
|
612
|
-
!isObject(candidate.content) ||
|
|
613
|
-
!Array.isArray(candidate.content.parts)) {
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
for (const part of candidate.content.parts) {
|
|
617
|
-
if (!isObject(part) ||
|
|
618
|
-
!isObject(part.inlineData) ||
|
|
619
|
-
typeof part.inlineData.data !== "string") {
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
const data = part.inlineData.data.trim();
|
|
623
|
-
if (data) {
|
|
624
|
-
audioData.push(data);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
return audioData;
|
|
629
|
-
}
|
|
630
420
|
function buildSpeechRewritePrompt(sourceText) {
|
|
631
421
|
return [
|
|
632
422
|
"Rewrite the assistant response below so it sounds natural when spoken aloud.",
|
|
@@ -640,7 +430,7 @@ function buildSpeechRewritePrompt(sourceText) {
|
|
|
640
430
|
"Rewrite a code identifier only when its literal form would be difficult to follow aloud, and preserve its exact meaning.",
|
|
641
431
|
"",
|
|
642
432
|
"Examples of good rewrites:",
|
|
643
|
-
'- `src/core/utils/
|
|
433
|
+
'- `src/core/utils/speech.ts:372` → "speech.ts, line 372"',
|
|
644
434
|
'- `src/tui/session_chat_controller.ts:1819-1855` → "session_chat_controller.ts, lines 1819 to 1855"',
|
|
645
435
|
'- `src/core/session/compaction.ts` → "compaction.ts"',
|
|
646
436
|
'- `/Users/markus/.config/tau/config.json` → "the tau config.json in your home directory"',
|
|
@@ -751,57 +541,6 @@ function closestSpeechBoundary(candidates, cumulativeWeights, idealWeight) {
|
|
|
751
541
|
function isSpeechSentenceBoundary(previous, next) {
|
|
752
542
|
return /[。!?]/u.test(previous) || (/[.!?;:]/u.test(previous) && /\s/u.test(next));
|
|
753
543
|
}
|
|
754
|
-
function buildSpeechSynthesisPrompt(spokenText) {
|
|
755
|
-
return [
|
|
756
|
-
"Synthesize speech audio for the labeled transcript below.",
|
|
757
|
-
"Speak only the transcript. Do not speak the instructions or section labels.",
|
|
758
|
-
"",
|
|
759
|
-
"### DIRECTOR'S NOTES",
|
|
760
|
-
"Style: Clear, natural, conversational.",
|
|
761
|
-
"Pacing: Brisk conversational speed. Keep it clear, confident, and energetic without sounding rushed.",
|
|
762
|
-
"",
|
|
763
|
-
"### TRANSCRIPT",
|
|
764
|
-
spokenText,
|
|
765
|
-
].join("\n");
|
|
766
|
-
}
|
|
767
|
-
function isRetryableTtsError(error) {
|
|
768
|
-
if (error instanceof GeminiTtsOutputLimitError) {
|
|
769
|
-
return false;
|
|
770
|
-
}
|
|
771
|
-
if (error instanceof GeminiTtsResponseError) {
|
|
772
|
-
return true;
|
|
773
|
-
}
|
|
774
|
-
if (error instanceof GeminiApiError) {
|
|
775
|
-
return error.status !== undefined && RETRYABLE_TTS_STATUS_CODES.has(error.status);
|
|
776
|
-
}
|
|
777
|
-
if (error instanceof Error) {
|
|
778
|
-
return error.name !== "AbortError";
|
|
779
|
-
}
|
|
780
|
-
return false;
|
|
781
|
-
}
|
|
782
|
-
async function waitForRetryDelay(attempt, signal) {
|
|
783
|
-
const durationMs = Math.min(1000, 150 * 2 ** (attempt - 1));
|
|
784
|
-
await new Promise((resolve, reject) => {
|
|
785
|
-
const timeout = setTimeout(() => {
|
|
786
|
-
signal?.removeEventListener("abort", onAbort);
|
|
787
|
-
resolve();
|
|
788
|
-
}, durationMs);
|
|
789
|
-
timeout.unref?.();
|
|
790
|
-
const onAbort = () => {
|
|
791
|
-
clearTimeout(timeout);
|
|
792
|
-
reject(abortError());
|
|
793
|
-
};
|
|
794
|
-
if (!signal) {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
if (signal.aborted) {
|
|
798
|
-
clearTimeout(timeout);
|
|
799
|
-
reject(abortError());
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
544
|
function createLinkedAbortController(parent) {
|
|
806
545
|
const controller = new AbortController();
|
|
807
546
|
const onAbort = () => controller.abort(parent?.reason);
|
|
@@ -846,4 +585,4 @@ function abortError() {
|
|
|
846
585
|
error.name = "AbortError";
|
|
847
586
|
return error;
|
|
848
587
|
}
|
|
849
|
-
//# sourceMappingURL=
|
|
588
|
+
//# sourceMappingURL=speech.js.map
|