<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: VoiceHue</title>
    <description>The latest articles on DEV Community by VoiceHue (@voicehue).</description>
    <link>https://dev.to/voicehue</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4228388%2F27dfeab9-744a-4fec-ada7-8d9ee8cd22f9.png</url>
      <title>DEV Community: VoiceHue</title>
      <link>https://dev.to/voicehue</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/voicehue"/>
    <language>en</language>
    <item>
      <title>Let your agent speak: wiring a remote MCP text-to-speech server</title>
      <dc:creator>VoiceHue</dc:creator>
      <pubDate>Thu, 05 Nov 2026 11:17:42 +0000</pubDate>
      <link>https://dev.to/voicehue/let-your-agent-speak-wiring-a-remote-mcp-text-to-speech-server-487n</link>
      <guid>https://dev.to/voicehue/let-your-agent-speak-wiring-a-remote-mcp-text-to-speech-server-487n</guid>
      <description>&lt;p&gt;Most of my agent tooling is text in, text out. That is fine until the thing I am&lt;br&gt;
building is audio: a product demo, a course module, a 40-second short. Then I end&lt;br&gt;
up copy-pasting the script back into a web UI, picking a voice, downloading an&lt;br&gt;
MP3, and repeating the whole loop for every language.&lt;/p&gt;

&lt;p&gt;This post walks through the setup I use now: a &lt;strong&gt;remote MCP server&lt;/strong&gt; that turns&lt;br&gt;
text into audio, wired into the assistant I already have open, plus a small&lt;br&gt;
Python pipeline for the batch case. All the numbers below come from the live&lt;br&gt;
endpoints and docs on 2026-11-05, not from a marketing page.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why remote MCP instead of one more API wrapper
&lt;/h3&gt;

&lt;p&gt;A remote MCP server is an HTTP endpoint your agent already knows how to call.&lt;br&gt;
There is no local process to keep alive, no &lt;code&gt;npx&lt;/code&gt; shim, no stdio session that&lt;br&gt;
dies when you close a terminal. You give the assistant a URL and a bearer token&lt;br&gt;
and it gains tools.&lt;/p&gt;

&lt;p&gt;That matters for TTS specifically because the useful call is boring and&lt;br&gt;
frequent: "read this text as this voice". You do not want to hand-write that&lt;br&gt;
request twenty times while iterating on a script.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 0: what the server exposes
&lt;/h3&gt;

&lt;p&gt;Two tools, which is all you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;list_voices&lt;/code&gt; — returns voice ids, names, and languages.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;text_to_speech&lt;/code&gt; — takes &lt;code&gt;text&lt;/code&gt; (up to 1,200 characters) and a &lt;code&gt;voice&lt;/code&gt; id,
returns the audio plus a short summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The transport is Streamable HTTP at &lt;code&gt;https://voicehue.com/mcp&lt;/code&gt;. A bare&lt;br&gt;
&lt;code&gt;initialize&lt;/code&gt; call answers without a key, which is a cheap way to check the&lt;br&gt;
endpoint is alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://voicehue.com/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json, text/event-stream'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18",
        "capabilities":{},
        "clientInfo":{"name":"curl","version":"0"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response negotiates &lt;code&gt;2025-06-18&lt;/code&gt; and reports &lt;code&gt;serverInfo.name = "voicehue"&lt;/code&gt;.&lt;br&gt;
If that comes back, your network path is fine and any later failure is auth or&lt;br&gt;
args, not transport.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: create a key
&lt;/h3&gt;

&lt;p&gt;Keys are created in the dashboard and &lt;strong&gt;shown once&lt;/strong&gt;. Put yours in the&lt;br&gt;
environment; never in a config file you might commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VOICEHUE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"vh_live_..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free plan is per key: 10,000 characters per month, 2,000 per day, 10&lt;br&gt;
requests per minute, 1,200 characters per request, standard voices only. An&lt;br&gt;
account can hold two free keys, so a side project and a demo can be metered&lt;br&gt;
separately.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: wire it into your assistant
&lt;/h3&gt;

&lt;p&gt;Codex CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add voicehue &lt;span class="nt"&gt;--url&lt;/span&gt; https://voicehue.com/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bearer-token-env-var&lt;/span&gt; VOICEHUE_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http voicehue https://voicehue.com/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$VOICEHUE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor (&lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"voicehue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://voicehue.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer ${env:VOICEHUE_API_KEY}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask the agent for something small: &lt;em&gt;"list the available voices"&lt;/em&gt;. If the&lt;br&gt;
tool list is populated but the call 401s, the env var is not reaching the&lt;br&gt;
process — a shell export in one terminal does not travel to a GUI app.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: the batch case, in Python
&lt;/h3&gt;

&lt;p&gt;Chat tools are great for one take. Assets need a script that can run twice and&lt;br&gt;
produce the same file names. The same account also exposes an&lt;br&gt;
OpenAI-compatible endpoint at &lt;code&gt;https://voicehue.com/v1&lt;/code&gt;, so the official SDK&lt;br&gt;
works with two changed lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://voicehue.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VOICEHUE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_streaming_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voicehue-tts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vh-en-sky&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from the pipeline.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_to_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;model&lt;/code&gt; is accepted for client compatibility and does not select the voice.&lt;br&gt;
&lt;code&gt;speed&lt;/code&gt; accepts 0.5–2, and &lt;code&gt;response_format&lt;/code&gt; accepts &lt;code&gt;mp3&lt;/code&gt; (default) or &lt;code&gt;wav&lt;/code&gt;&lt;br&gt;
when the chosen voice supports it.&lt;/p&gt;

&lt;p&gt;The six standard voice ids, one per language:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Voice id&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;en-US&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vh-en-sky&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sky&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zh-CN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vh-zh-qingyin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;清音&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;es-ES&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vh-es-alex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fr-FR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vh-fr-camille&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Camille&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ja-JP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vh-ja-hikari&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ひかり&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ko-KR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vh-ko-haneul&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;하늘&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can fetch the same list without a key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://voicehue.com/v1/voices?language=en"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: split on sentences, not characters
&lt;/h3&gt;

&lt;p&gt;The 1,200-character request cap is the one limit that actually shapes the code.&lt;br&gt;
Naive &lt;code&gt;text[i:i+1200]&lt;/code&gt; slicing cuts mid-word, and the seam is audible.&lt;br&gt;
Split on sentence boundaries first, and only hard-split a single sentence that&lt;br&gt;
is longer than the cap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?&amp;lt;=[.!?。！？\n])\s*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;sentence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then render one file per chunk and stitch with ffmpeg's concat demuxer rather&lt;br&gt;
than concatenating bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; concat &lt;span class="nt"&gt;-safe&lt;/span&gt; 0 &lt;span class="nt"&gt;-i&lt;/span&gt; list.txt &lt;span class="nt"&gt;-c&lt;/span&gt; copy narration.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gotchas worth knowing before you ship
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 401 usually means the client, not the key.&lt;/strong&gt; GUI apps started from the
Dock do not inherit variables exported in a terminal. Export the key in the
shell that launches the client, or paste the header into the client's own
config file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 429 can arrive while your own quota still shows headroom.&lt;/strong&gt; The service
also enforces a shared daily capacity, so a &lt;code&gt;429&lt;/code&gt; does not always mean &lt;em&gt;your&lt;/em&gt;
key is exhausted. The error body carries &lt;code&gt;limit&lt;/code&gt; and &lt;code&gt;resets_at&lt;/code&gt; (00:00 UTC);
branch on &lt;code&gt;code&lt;/code&gt;, not on the message text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages outside the seven have no cloud voice.&lt;/strong&gt; Cloud coverage is
English, Chinese, Spanish, Portuguese, Japanese, Korean, French. The web page
falls back to the browser's built-in voices for anything else, and those
sound very different from the cloud set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A large voice library is not a large free catalogue.&lt;/strong&gt; The library lists 138
voices; six are standard and free, the rest are premium and need a paid plan.
Read the &lt;code&gt;tier&lt;/code&gt; field before promising a client a specific voice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When this is the wrong tool
&lt;/h3&gt;

&lt;p&gt;If you need word-level timestamps for subtitles, or cloned voices, this is not&lt;br&gt;
it. MCP is the right layer for "make this paragraph audible" inside an agent&lt;br&gt;
loop, and the HTTP endpoint is the right layer for batch rendering. Neither&lt;br&gt;
replaces a proper audio editor for mixing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;The endpoint, the voice list, and the free key are all live; the 1,200-character&lt;br&gt;
cap and the six standard voices are documented, not discovered by trial.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://voicehue.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=voicehue_launch_20261106" rel="noopener noreferrer"&gt;VoiceHue — free text to speech, no sign-up&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on VoiceHue, the service used in the examples. The setup&lt;br&gt;
above is the same one I use for the project's own demo assets; the limits and&lt;br&gt;
error codes are quoted from the live docs and API on 2026-11-05.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>tts</category>
    </item>
  </channel>
</rss>
