<?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: gao chong</title>
    <description>The latest articles on DEV Community by gao chong (@gao_chong_480cf363fc961d4).</description>
    <link>https://dev.to/gao_chong_480cf363fc961d4</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%2F4230064%2F145b682b-83dd-40b6-bb2b-dbe3261136a5.png</url>
      <title>DEV Community: gao chong</title>
      <link>https://dev.to/gao_chong_480cf363fc961d4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gao_chong_480cf363fc961d4"/>
    <language>en</language>
    <item>
      <title>The AI speaking tutor is four API calls in a trenchcoat</title>
      <dc:creator>gao chong</dc:creator>
      <pubDate>Fri, 06 Nov 2026 09:19:42 +0000</pubDate>
      <link>https://dev.to/gao_chong_480cf363fc961d4/the-ai-speaking-tutor-is-four-api-calls-in-a-trenchcoat-5b01</link>
      <guid>https://dev.to/gao_chong_480cf363fc961d4/the-ai-speaking-tutor-is-four-api-calls-in-a-trenchcoat-5b01</guid>
      <description>&lt;p&gt;I build an English vocabulary app for Vietnamese learners. One feature — "speak, get scored, get a spoken reply" — turned out to be four sequential network hops, and most of the interesting bugs lived in the seams between them.&lt;/p&gt;

&lt;p&gt;I'll only publish numbers I actually measured in production. Where I don't have a number, I say so instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  One turn is four hops, not one request
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;record (client)
  -&amp;gt; upload the clip to temporary storage, get back an internal URL
  -&amp;gt; speech recognition: submit job -&amp;gt; poll -&amp;gt; download transcript
  -&amp;gt; LLM writes the reply, with the conversation history attached
  -&amp;gt; TTS renders the reply as audio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The surprise: &lt;strong&gt;the ASR model does not accept audio inlined in the request body.&lt;/strong&gt; It takes a URL. So the upload step isn't an optimisation, it's mandatory — and it comes with a short-lived link (48 hours in my config), which means every piece of processing has to close inside the session.&lt;/p&gt;

&lt;p&gt;The ASR call is also &lt;strong&gt;asynchronous&lt;/strong&gt;. There is no POST-and-get-your-transcript path. You submit, then poll until the result file exists. If you're used to writing synchronous request/response code, this polling loop feels like a wart. It buys you the ability to not hold an open connection for tens of seconds.&lt;/p&gt;

&lt;p&gt;Latency the user perceives is the sum of all four hops plus the network between them. Nobody experiences "the ASR step is slow"; they experience "the app is slow".&lt;/p&gt;

&lt;h2&gt;
  
  
  History length is a latency bug that never throws
&lt;/h2&gt;

&lt;p&gt;Because every turn re-sends the whole conversation, &lt;strong&gt;latency grows with turn count&lt;/strong&gt;. It degrades smoothly, which is exactly why it's hard to catch: no error, no timeout, just a gradually worse app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// was: 20&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="no"&gt;MAX_HISTORY_MESSAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I cut it to 10 messages for the free-form speaking tutor, and 16 for the scenario dialogues — a scripted scenario is usually over in four to six turns, and extra history past that mostly causes the model to restate points it already made. Same place, I cap &lt;code&gt;max_tokens&lt;/code&gt; at 1000 (speaking) and 600 (scenarios): long enough for a conversational reply, short enough that the model can't give a speech.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three small parameters with non-obvious behaviour
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;language_hints&lt;/code&gt; reads only the first element.&lt;/strong&gt; I used to send a two-language list and assumed the system would balance between them. It doesn't — the second value is silently ignored. If you want multi-language behaviour, you implement the selection logic yourself and send exactly one hint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;enable_thinking: false&lt;/code&gt;.&lt;/strong&gt; Turning off hidden reasoning cuts time-to-first-byte. Correct for short conversational turns; the trade is that answers are less "cooked". Don't leave it on by default just because the vendor's example does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;temperature: 0.8&lt;/code&gt;.&lt;/strong&gt; Higher than the safe textbook value. Language practice needs varied phrasing, not reproducible phrasing.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Please speak slowly and clearly" costs 15–19%
&lt;/h2&gt;

&lt;p&gt;I added an instruction to the TTS call asking for slower, clearer output. Measured effect: &lt;strong&gt;the same text produces 15–19% longer audio.&lt;/strong&gt; Users wait longer, the bill is bigger.&lt;/p&gt;

&lt;p&gt;I kept it, because comprehension improved enough to justify it. The point isn't the decision, it's that a single innocuous-looking instruction string is a cost and latency decision. If you use these, measure audio duration across a sample — don't eyeball one clip and call it fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoring model that graded silence
&lt;/h2&gt;

&lt;p&gt;This is the one I'd tell anyone choosing a speech-scoring model: &lt;strong&gt;one model returned high confidence scores for a silent recording.&lt;/strong&gt; It wasn't scoring pronunciation, it was guessing that a score should exist.&lt;/p&gt;

&lt;p&gt;If your model-selection process is "run some samples and see if the scores look reasonable", you will pick this one, because its output looks great. My selection criteria changed permanently: &lt;strong&gt;feed it garbage first, real speech second.&lt;/strong&gt; A silent file, a noise-only file, a recording of someone reading the wrong sentence. If it still hands out high scores, it's out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limits are a cost brake, not a "fair use policy"
&lt;/h2&gt;

&lt;p&gt;Real numbers, currently in production:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LLM reply generation&lt;/td&gt;
&lt;td&gt;3 req / min / user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speech recognition&lt;/td&gt;
&lt;td&gt;20 / min / user, 40 / min / IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-speech&lt;/td&gt;
&lt;td&gt;30 / min / user, 60 / min / IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pronunciation scoring&lt;/td&gt;
&lt;td&gt;6 / min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pronunciation scoring (HTTP ceiling)&lt;/td&gt;
&lt;td&gt;read 90s — measured p50 2.3s, worst ~3.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The per-IP numbers exist because per-user alone isn't enough: a lot of users sit behind one NAT (cafés, dorms) and would evict each other if I counted only by IP. I learned that expensively.&lt;/p&gt;

&lt;p&gt;For new users I chose a &lt;strong&gt;total turn allowance&lt;/strong&gt; rather than "N turns per day". A serious learner needs to talk a lot in the first few days to find out whether the app fits them; someone who bounces after three days isn't retained by a daily allowance anyway. The cost is that I lose the ability to nurture people slowly with a small daily drip. That's a business decision wearing an engineering costume, and you should know that before you copy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I still haven't measured
&lt;/h2&gt;

&lt;p&gt;So this isn't an advert with extra steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No per-region cost per turn — the vendor invoice aggregates, and I haven't split it.&lt;/li&gt;
&lt;li&gt;No peak-hour load test. The latency figures above come from normal usage.&lt;/li&gt;
&lt;li&gt;No assessment of ASR behaviour on weak connections, which is the common case for learning on a bus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building something like this, measure those three before writing a post about it.&lt;/p&gt;




&lt;p&gt;The original long version, with the full parameter dump: &lt;a href="https://thuoctu.91word.com/devlog-luyen-noi-ai.html?ref=devto" rel="noopener noreferrer"&gt;thuoctu.91word.com/devlog-luyen-noi-ai.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>english</category>
      <category>webdev</category>
      <category>android</category>
    </item>
  </channel>
</rss>
