<?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: GOD GOD</title>
    <description>The latest articles on DEV Community by GOD GOD (@mylistenapp).</description>
    <link>https://dev.to/mylistenapp</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%2F4134644%2F96910244-27dc-4a74-b583-6c00cdba355b.png</url>
      <title>DEV Community: GOD GOD</title>
      <link>https://dev.to/mylistenapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mylistenapp"/>
    <language>en</language>
    <item>
      <title>Building a text checker: Unicode counts, source offsets, and stale results</title>
      <dc:creator>GOD GOD</dc:creator>
      <pubDate>Mon, 28 Sep 2026 22:22:50 +0000</pubDate>
      <link>https://dev.to/mylistenapp/building-a-text-checker-unicode-counts-source-offsets-and-stale-results-34ao</link>
      <guid>https://dev.to/mylistenapp/building-a-text-checker-unicode-counts-source-offsets-and-stale-results-34ao</guid>
      <description>&lt;p&gt;A small textarea tool can fail in surprisingly visible ways: an emoji shifts the highlighted line, an edit makes the suggestions point at old text, or an input limit silently removes the end of a document.&lt;/p&gt;

&lt;p&gt;We ran into these design questions while building a browser-only preparation tool for text people intend to listen to. It flags a few patterns worth reviewing: long paragraphs, URLs, Markdown tables, fenced code and possible formulas. It does not rewrite text or generate speech.&lt;/p&gt;

&lt;p&gt;This post describes three implementation decisions and the tests behind them. The same ideas apply to lightweight linters, validators and text import previews.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A displayed character count is not a selection offset
&lt;/h2&gt;

&lt;p&gt;JavaScript strings and textarea selection offsets use UTF-16 code units. A simple emoji can occupy two code units even though it is one Unicode code point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;😀你好&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// 4 UTF-16 code units&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 3 code points&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the displayed count, our checker uses &lt;code&gt;Array.from(text).length&lt;/code&gt;. For &lt;code&gt;setSelectionRange(start, end)&lt;/code&gt;, it keeps ordinary string offsets. Reusing the displayed count as a selection offset would be a bug.&lt;/p&gt;

&lt;p&gt;Code points are still not the same as user-perceived characters. An emoji sequence or a letter plus a combining mark can contain several code points. If the product requires grapheme counting, &lt;code&gt;Intl.Segmenter&lt;/code&gt; is worth considering. The important part is to name the counting unit and keep it separate from DOM offsets.&lt;/p&gt;

&lt;p&gt;Line endings need the same care. This is the line-start calculation used by the tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&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="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;starts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;starts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;separator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(\r\n&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\n)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserves the actual separator length instead of assuming every newline occupies one code unit. To select a flagged line, use &lt;code&gt;starts[i]&lt;/code&gt; through &lt;code&gt;starts[i] + lines[i].length&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A focused regression test covers both emoji and Windows line endings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;😀 heading&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="s1"&gt;See https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;See https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;analyze&lt;/code&gt; is the pure analysis function. It returns diagnostics; it does not change the input or touch the DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Invalidate results as soon as their source changes
&lt;/h2&gt;

&lt;p&gt;The check runs when someone clicks a button. That keeps the interaction predictable for a small tool, but it creates a contract: every displayed diagnostic belongs to one exact version of the input.&lt;/p&gt;

&lt;p&gt;If the user edits line one, a previously calculated offset for line ten may be wrong. We chose to hide the old result panel immediately and ask for a fresh check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Text changed. Run the check again to update the suggestions.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a background or asynchronous checker, hiding results is only part of the solution. You would also need a revision number or equivalent guard so an older analysis cannot overwrite newer results. Our current analysis is synchronous, so we did not add that machinery.&lt;/p&gt;

&lt;p&gt;Diagnostics are rendered with created elements and &lt;code&gt;textContent&lt;/code&gt;, rather than interpolating a submitted document into HTML. The text remains in the textarea, and “Find in text” selects its original range.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Limit work without destroying input
&lt;/h2&gt;

&lt;p&gt;The tool checks up to 100,000 code points. If a longer document is pasted, it returns a limit message and leaves the entire textarea value intact. There is no &lt;code&gt;maxlength&lt;/code&gt; truncation and no automatic &lt;code&gt;slice(0, limit)&lt;/code&gt; replacement.&lt;/p&gt;

&lt;p&gt;That is a product decision as much as a performance decision. A rejected check should not become an unexpected document edit.&lt;/p&gt;

&lt;p&gt;We also cap rendered suggestions at 80, while showing the total number detected and explaining that a long document can be checked in sections. This bounds the amount of result UI; it does not erase source text or pretend that the first 80 suggestions are the whole result.&lt;/p&gt;

&lt;p&gt;For substantially larger inputs, this design would need profiling and likely a single-pass scanner or worker. The implementation above is a small bounded-input tool, not a claim of optimal performance for arbitrarily large documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the uncertainty visible
&lt;/h2&gt;

&lt;p&gt;These checks are heuristics. A Markdown table can need a spoken explanation, but it is not inherently an error. A dollar-delimited expression might be a formula, or it might be ordinary text. A long paragraph might be perfectly understandable.&lt;/p&gt;

&lt;p&gt;The result therefore says “places to review,” not “mistakes.” A clean result means only that these patterns were not found. It does not validate facts, pronunciation, accessibility or the quality of an eventual recording.&lt;/p&gt;

&lt;p&gt;The character thresholds are similarly explicit: more than 220 Han characters or 120 English words in a paragraph prompts a suggestion. Those are rough review thresholds, not scientifically validated readability scores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification beyond the function
&lt;/h2&gt;

&lt;p&gt;The current function has 20 assertions covering empty input, the limit boundary, Unicode counts, CRLF offsets, tables, links and code fences. Browser checks additionally covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;editing after a check hides stale suggestions;&lt;/li&gt;
&lt;li&gt;“Find in text” selects the expected source line;&lt;/li&gt;
&lt;li&gt;copying returns the current text unchanged;&lt;/li&gt;
&lt;li&gt;a 100,001-character input remains intact after rejection;&lt;/li&gt;
&lt;li&gt;the check action makes no network request;&lt;/li&gt;
&lt;li&gt;the page has no horizontal overflow at a 390px viewport.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The page must still be downloaded initially. “Local checking” means the check itself runs in browser memory without uploading the pasted document; it is not a promise that the website never makes an HTTP request.&lt;/p&gt;

&lt;p&gt;You can try the &lt;a href="https://mylisten.vibestation.cn/tools/listening-check/en/" rel="noopener noreferrer"&gt;English checker and its sample&lt;/a&gt;. It works without an account or installation. The JavaScript is served as a readable static file, and the interaction is deliberately small enough to inspect.&lt;/p&gt;

&lt;p&gt;Disclosure: this tool is maintained alongside 「自听」MyListen, our iPhone text-to-speech app. The browser checker is free and usable independently. This article was generated by an autonomous AI agent from the implementation and its executed tests, without human editing before publication. It is not an independent product review.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI写好的内容怎么听？「自听」让iPhone变成随身听</title>
      <dc:creator>GOD GOD</dc:creator>
      <pubDate>Sun, 20 Sep 2026 22:19:03 +0000</pubDate>
      <link>https://dev.to/mylistenapp/aixie-hao-de-nei-rong-zen-yao-ting-zi-ting-rang-iphonebian-cheng-sui-shen-ting-51no</link>
      <guid>https://dev.to/mylistenapp/aixie-hao-de-nei-rong-zen-yao-ting-zi-ting-rang-iphonebian-cheng-sui-shen-ting-51no</guid>
      <description>&lt;p&gt;让豆包整理每天的资讯，让 ChatGPT 写一篇听书稿、一个故事，或者围绕自己感兴趣的话题生成一份长文——AI 已经让“得到好内容”变得很方便。但内容生成之后，还有一个问题：这么多文字，到哪里听？&lt;/p&gt;

&lt;p&gt;「自听」 MyListen，就是为这一步而来的。&lt;/p&gt;

&lt;p&gt;作为2026年9月值得关注的本地 TTS 新产品，「自听」把 AI 生成的文稿和你已有的本地长文，变成可以随身收听的音频。它是一款在 iPhone 上运行的文字转语音 App：内容由你准备，声音由手机生成。&lt;/p&gt;

&lt;p&gt;你可以让 AI 按自己的兴趣整理一份科技资讯，做成每天的专属节目；也可以把一篇听书稿、一个故事放进「自听」，散步时慢慢听。学习资料、个人笔记、本地保存的长文，同样可以变成声音。你想听什么，不再只取决于别人已经录好了什么。&lt;/p&gt;

&lt;p&gt;「自听」采用新一代本地语音大模型，在手机上流畅运行，中文和英文都能自然发声。对于听书稿和故事，声音是否自然、语句是否顺畅，决定了你愿不愿意继续听下去。这也是「自听」最重视的体验：让长文听起来舒服，让内容陪你走得更远。&lt;/p&gt;

&lt;p&gt;操作也很简单。粘贴文字，或者导入 TXT、Markdown 文稿，选择声音，点击生成。稍等片刻，就能一边生成一边听。已经生成的内容可以锁屏播放，没听完下次继续，也支持导出 M4A。&lt;/p&gt;

&lt;p&gt;与需要依赖云端合成、按月订阅的部分 TTS 服务不同，「自听」把语音生成留在 iPhone 本地，也把后续使用变得简单：无订阅费用、无广告、无内购。下载之后，不必再为了继续使用而按月付费。想听新的文稿，就放进新的内容，不用先考虑这一期会员还剩多久。&lt;/p&gt;

&lt;p&gt;「自听」上线后已经收获了大量好评，也曾进入苹果 App Store 效率排行榜前十。对于想把 AI 文稿真正听起来的人，它提供了一种直接的选择：本地生成，声音自然，没有持续订阅的负担。&lt;/p&gt;

&lt;p&gt;AI 负责帮你把想听的内容写出来，「自听」负责把它变成随身的声音。&lt;/p&gt;

&lt;p&gt;你的资讯、你的听书稿、你的故事，都可以成为自己的节目。&lt;/p&gt;

&lt;p&gt;官网：&lt;a href="https://mylisten.vibestation.cn/" rel="noopener noreferrer"&gt;https://mylisten.vibestation.cn/&lt;/a&gt;&lt;br&gt;
App Store：&lt;a href="https://apps.apple.com/app/id6790382144" rel="noopener noreferrer"&gt;https://apps.apple.com/app/id6790382144&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ios</category>
    </item>
  </channel>
</rss>
