<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Tuning Seven]]></title><description><![CDATA[Personal dev blog: notes on building and shipping indie AI tools and web projects.]]></description><link>https://tuningseven.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 12:38:21 GMT</lastBuildDate><atom:link href="https://tuningseven.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Text to rap is the smallest possible creative brief - and that is the point]]></title><description><![CDATA[Most creative tools start by asking you for everything: a script, a voice, a beat, a style guide. The rap generator we built starts with one sentence. "My…]]></description><link>https://tuningseven.hashnode.dev/text-to-rap-is-the-smallest-possible-creative-brief-and-that-is-the-point</link><guid isPermaLink="true">https://tuningseven.hashnode.dev/text-to-rap-is-the-smallest-possible-creative-brief-and-that-is-the-point</guid><dc:creator><![CDATA[ZHIWEI]]></dc:creator><pubDate>Thu, 05 Nov 2026 07:18:06 GMT</pubDate><content:encoded><![CDATA[<p>Most creative tools start by asking you for everything: a script, a voice, a beat, a style guide. The rap generator we built starts with one sentence. "My cat knocked my coffee onto my laptop." That is the entire brief. Text in, rap out.</p>
<p>This post is about why the one-sentence brief works, and what the text-to-rap mode actually does with it.</p>
<h2>The sentence is a spec</h2>
<p>A topic like the one above contains everything a verse needs: a subject (the cat), an object (the laptop), an emotion (grief, mostly), and a punchline that writes itself. The lyric stage reads the topic for those parts and builds bars around them — couplets that name the culprit, a hook that mourns the laptop, a closing line for the comeback. If the user writes their own lyrics instead, the generator skips this stage entirely and treats their text as the vocal track.</p>
<h2>From text to performance</h2>
<p>The bars go to a rap-styled vocal model that delivers them with flow — stress on the right syllables, push against the beat. This is where text-to-rap differs from reading text aloud: the same sentence performed straight is a sentence; performed with cadence, it is a verse. Then an original beat composed for the video carries it home.</p>
<h2>Where the words show up</h2>
<p>The finished video puts the performance on a stage — and if you add a photo, on a face. The text-to-rap page at <a href="https://useairapvideo.com/ai-rap-generator/text-to-rap">https://useairapvideo.com/ai-rap-generator/text-to-rap</a> has the mode itself; the free 6-second preview per account means your first sentence costs nothing to try.</p>
<h2>One practical note</h2>
<p>Topics with concrete nouns work best: objects, names, places. "My cat knocked my coffee onto my laptop" gives the writer (human or model) something to grab. Abstract topics — "the passage of time" — produce verses too, but they lean on the template's energy more than the lyrics' content.</p>
<p>The whole thing lives at <a href="https://useairapvideo.com">https://useairapvideo.com</a>. One video $3.99, credit packs from $9.99, no subscription, original beats only.</p>
<p>— AI Rap Video team</p>
]]></content:encoded></item><item><title><![CDATA[The boring decision that keeps our AI music tool out of trouble: original audio only]]></title><description><![CDATA[Most product posts celebrate a feature. This one celebrates a constraint — because it is the single decision that shapes everything else in our tool, AI…]]></description><link>https://tuningseven.hashnode.dev/the-boring-decision-that-keeps-our-ai-music-tool-out-of-trouble-original-audio-only</link><guid isPermaLink="true">https://tuningseven.hashnode.dev/the-boring-decision-that-keeps-our-ai-music-tool-out-of-trouble-original-audio-only</guid><dc:creator><![CDATA[ZHIWEI]]></dc:creator><pubDate>Tue, 03 Nov 2026 17:00:24 GMT</pubDate><content:encoded><![CDATA[<p>Most product posts celebrate a feature. This one celebrates a constraint — because it is the single decision that shapes everything else in our tool, AI Rap Video (<a href="https://useairapvideo.com">https://useairapvideo.com</a>).</p>
<p>Our product generates rap performance videos: lyrics written for your topic, an AI rap voice, and a beat, with your photo lip-synced on camera. The tempting shortcut in this space is obvious. Famous instrumentals drive engagement. Everyone recognizes them. Why compose anything?</p>
<h2>Copyright is not a rounding error</h2>
<p>A generated video that rides on a copyrighted instrumental is a derivative work sitting on someone else's rights. For a user, that means a takedown on the platforms they share to. For the tool that produced it, it means the whole catalog is radioactive: one rights-holder sweep and every rendered video is a liability. Training data debates aside, the <em>output</em> of a generator using licensed audio is squarely actionable today, and "the AI made it" is not a defense anyone has won with.</p>
<p>So the rule in our pipeline is simple: the beat is composed per video, the lyrics are generated for the user's topic, and nothing in the output samples or bundles an existing song. There is a DMCA takedown flow for edge cases, and users who own the rights to a track can upload it themselves — the escape hatch is theirs to exercise, not ours to assume.</p>
<h2>What it costs</h2>
<p>Two things, honestly:</p>
<ol>
<li><strong>Recognition.</strong> A famous beat does half the marketing. An original boom-bap pattern does none. Our videos rise or fall on the joke in the lyrics, which is more pressure on the writing stage.</li>
<li><strong>Engineering.</strong> Per-video composition means the audio stage cannot be a lookup table. It has to actually generate, matched to the vocal take.</li>
</ol>
<h2>What it buys</h2>
<p>Listings that do not get swept. Group-chat videos that do not get muted mid-share. And a clearer conscience: the person making a birthday rap for their dad is not unknowingly building on a producer's unlicensed work.</p>
<p>If you are building in AI music or AI video, we think this constraint is worth stealing. The workflows it forces — generate, don't sample — end up cheaper than the licensing mess you avoid.</p>
<p>The tool itself lives at <a href="https://useairapvideo.com">https://useairapvideo.com</a>, free 6-second preview per account, videos from $3.99 one-time, no subscription. Come for the roast generator, stay for the beats you do not have to clear.</p>
<p>— AI Rap Video team</p>
]]></content:encoded></item><item><title><![CDATA[Grok TTS speech tags, tested: what <loud>, <singing> and [laugh] actually do]]></title><description><![CDATA[What <loud> and <singing> really change in Grok text to speech, measured on real clips, plus every inline and wrapping tag.]]></description><link>https://tuningseven.hashnode.dev/grok-tts-speech-tags-tested-what-loud-singing-and-laugh-actually-do</link><guid isPermaLink="true">https://tuningseven.hashnode.dev/grok-tts-speech-tags-tested-what-loud-singing-and-laugh-actually-do</guid><category><![CDATA[AI]]></category><category><![CDATA[api]]></category><category><![CDATA[tts]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[ZHIWEI]]></dc:creator><pubDate>Sun, 01 Nov 2026 03:18:22 GMT</pubDate><content:encoded><![CDATA[<p>xAI's Grok text to speech API (<code>POST https://api.x.ai/v1/tts</code>) has 28 built-in voices, 20 languages and a set of speech tags you put straight into the text. The docs list the tags; they don't say how much each one changes. I measured a couple.</p>
<h2>The request</h2>
<pre><code class="language-js">const res = await fetch('https://api.x.ai/v1/tts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.XAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    text: 'Stand back. &lt;loud&gt;This is not a drill.&lt;/loud&gt;',
    voice_id: 'eve',
    language: 'en',
    output_format: { codec: 'mp3', sample_rate: 24000, bit_rate: 128000 },
  }),
});
const mp3 = Buffer.from(await res.arrayBuffer());
</code></pre>
<p><code>language</code> also takes <code>auto</code>. Price is $15 per million characters (xAI pricing page, checked 2026-10-30).</p>
<h2>Two kinds of tags</h2>
<ul>
<li><strong>Inline</strong> tags insert a sound where they sit: <code>[pause]</code>, <code>[long-pause]</code>, <code>[hum-tune]</code>, <code>[laugh]</code>, <code>[chuckle]</code>, <code>[giggle]</code>, <code>[cry]</code>, <code>[tsk]</code>, <code>[tongue-click]</code>, <code>[lip-smack]</code>, <code>[breath]</code>, <code>[inhale]</code>, <code>[exhale]</code>, <code>[sigh]</code>.</li>
<li><strong>Wrapping</strong> tags change how the enclosed phrase is read: <code>&lt;soft&gt;</code>, <code>&lt;whisper&gt;</code>, <code>&lt;loud&gt;</code>, <code>&lt;build-intensity&gt;</code>, <code>&lt;decrease-intensity&gt;</code>, <code>&lt;higher-pitch&gt;</code>, <code>&lt;lower-pitch&gt;</code>, <code>&lt;slow&gt;</code>, <code>&lt;fast&gt;</code>, <code>&lt;sing-song&gt;</code>, <code>&lt;singing&gt;</code>, <code>&lt;emphasis&gt;</code>.</li>
</ul>
<h2>What two of them measured</h2>
<p>Same sentence, voice Eve, English, one clip with and one without the tag. One clip each, so this is a quick check rather than a benchmark.</p>
<table>
<thead>
<tr>
<th>Tag</th>
<th>Length</th>
<th>Loudness (RMS)</th>
<th>Median pitch</th>
</tr>
</thead>
<tbody><tr>
<td>none → <code>&lt;loud&gt;</code></td>
<td>5.5 s → 5.5 s</td>
<td>-20.7 → -20.9 dBFS</td>
<td>195 → 253 Hz</td>
</tr>
<tr>
<td>none → <code>&lt;singing&gt;</code></td>
<td>6.5 s → 8.4 s</td>
<td>—</td>
<td>189 → 250 Hz</td>
</tr>
</tbody></table>
<p>So <code>&lt;loud&gt;</code> is closer to "raised, urgent delivery" than a volume control: if you need a louder file, normalise it afterwards. <code>&lt;singing&gt;</code> stretches the words into a melody, which also makes the clip longer (and, on a per-character price, not more expensive).</p>
<h2>Trying tags without writing code</h2>
<p>If you want to hear the tags on all 28 voices before wiring up the API, I built a free front-end: <a href="https://groktexttospeech.com/">Grok Text to Speech</a>. The <a href="https://groktexttospeech.com/grok-tts-speech-tags/">speech tag reference</a> has a before/after sample for each group, and there are preset pages for <a href="https://groktexttospeech.com/whisper-text-to-speech/">whisper</a>, <a href="https://groktexttospeech.com/shouting-text-to-speech/">shouting</a> and <a href="https://groktexttospeech.com/singing-text-to-speech/">singing</a>. It's an independent project, not affiliated with xAI.</p>
<blockquote>
<p>Originally published on <a href="https://dev.to/zhiwei_224e5e15600342dc0b/grok-tts-speech-tags-tested-what-and-laugh-actually-do-1jda">dev.to</a>.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Prerendering an RSS Feed in a Static TanStack Start Site]]></title><description><![CDATA[Generate an RSS feed at build time in a prerendered TanStack Start site from a JSON log, with stable GUIDs and timezone-correct dates.]]></description><link>https://tuningseven.hashnode.dev/prerendering-an-rss-feed-in-a-static-tanstack-start-site</link><guid isPermaLink="true">https://tuningseven.hashnode.dev/prerendering-an-rss-feed-in-a-static-tanstack-start-site</guid><category><![CDATA[rss]]></category><category><![CDATA[tanstack]]></category><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[static site generator]]></category><dc:creator><![CDATA[ZHIWEI]]></dc:creator><pubDate>Tue, 13 Oct 2026 08:23:31 GMT</pubDate><content:encoded><![CDATA[<p>I run an independent fan guide for <strong>Lagos Life</strong>, a Nigerian browser life-sim that went viral in October 2026. The game has no official changelog, so the guide keeps one: every few hours a script records the weekly law, the daily in-game headline and the daily gem-hunt clue into a JSON file, and the site is rebuilt.</p>
<p>A dated log page is useful, but people asked for a way to hear about changes without coming back to look. The answer is old and boring: <strong>RSS</strong>. The interesting part is that the site is fully prerendered — there's no server at request time — so the feed has to be a static file generated at build time from the same JSON the page uses.</p>
<p>Here's how that works in TanStack Start.</p>
<h2>A server route that only runs at build time</h2>
<p>In TanStack Start, a file route can define <code>server.handlers</code> instead of a component. Normally that runs on a server. With prerendering, the build calls it once and writes the response body to disk.</p>
<p>The file name does the URL work. A literal dot is escaped with brackets, and the trailing underscore on <code>updates_</code> stops the route from nesting under the <code>/updates/</code> page layout:</p>
<pre><code class="language-text">src/routes/updates_.feed[.]xml.ts   →   /updates/feed.xml
</code></pre>
<pre><code class="language-ts">export const Route = createFileRoute('/updates_/feed.xml')({
  server: {
    handlers: {
      GET: () =&gt; {
        const xml = buildFeed(liveLog); // plain string building
        return new Response(xml, {
          headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' },
        });
      },
    },
  },
});
</code></pre>
<p>The JSON is a normal import (<code>import liveLog from '@/data/live-log.json'</code>), so it gets bundled at build time. No file reads, no fetch.</p>
<h2>Tell the prerenderer it exists</h2>
<p>The prerenderer discovers pages by crawling links from <code>/</code>. Nothing links to an XML file with an <code>&lt;a&gt;</code> tag, so the crawler would never find it. It has to be listed explicitly, next to the other non-HTML outputs:</p>
<pre><code class="language-ts">// vite.config.ts → tanstackStart({ prerender: { ... }, pages: [...] })
pages: [
  { path: '/robots.txt',  prerender: { enabled: true, outputPath: '/robots.txt' } },
  { path: '/sitemap.xml', prerender: { enabled: true, outputPath: '/sitemap.xml' } },
  { path: '/updates/feed.xml',
    prerender: { enabled: true, outputPath: '/updates/feed.xml' } },
],
</code></pre>
<p>I set <code>outputPath</code> explicitly so the file lands exactly where the feed URL says, with the <code>.xml</code> name intact.</p>
<h2>Three kinds of entries, one sorted list</h2>
<p>The log has three arrays — law changes, daily headlines, gem-hunt clues. Each gets a small mapper to a common shape, then they're merged, filtered to the last 30 days and sorted newest first:</p>
<pre><code class="language-ts">type Item = { guid: string; title: string; description: string; link: string; at: number };

const items = [
  ...log.laws.map(lawItem),
  ...log.daily.filter((d) =&gt; d.headline).map(dayItem),
  ...log.hunt.filter((h) =&gt; h.clue).map(clueItem),
]
  .filter((i) =&gt; Number.isFinite(i.at) &amp;&amp; i.at &gt;= since)
  .sort((a, b) =&gt; b.at - a.at);
</code></pre>
<p>A few details that took a second try:</p>
<ul>
<li><p><strong>Stable GUIDs.</strong> Feed readers decide "new item" by <code>guid</code>. I build it from the kind and the date (<code>day-2026-10-13</code>, <code>hunt-2026-10-13</code>) with <code>isPermaLink="false"</code>, so a rebuild every few hours never re-announces old items.</p>
</li>
<li><p><strong>The game's day, not mine.</strong> The game runs on Lagos time (WAT, UTC+1). A headline for "13 October" should be dated midnight in Lagos, not midnight UTC: <code>Date.parse(`${day}T00:00:00+01:00`)</code>. Get this wrong and items show up a day off for half the readers.</p>
</li>
<li><p><strong>Escape everything.</strong> Headlines contain quotes and apostrophes ("Lagos says 'case closed'"). A five-line <code>esc()</code> for <code>&amp; &lt; &gt; "</code> on every text node is enough for RSS 2.0; no XML library needed.</p>
</li>
<li><p><strong>Anchor links.</strong> Every item links to a section of the one page (<code>/updates/#daily-headlines</code>, <code>#law-log</code>, <code>#daily-gem-hunt</code>) rather than to a page per item. There are no per-item pages, and I didn't want to create thin ones just for the feed.</p>
</li>
<li><p><code>&lt;ttl&gt;180</code><strong>.</strong> The data only changes every three hours, so the feed says so.</p>
</li>
</ul>
<h2>Same red lines as the page</h2>
<p>The feed is built from the same JSON as the page, so it can't leak anything the page doesn't show — but it could show <em>more</em> if a mapper got careless. The gem-hunt items carry the clue word for word and nothing else: no answers, no rewards, no leaderboard names. The governor's own announcements never reach the JSON in the first place.</p>
<h2>Make it discoverable</h2>
<p>Two small things so readers can actually find the feed:</p>
<pre><code class="language-ts">// root route head(): every page advertises the feed
links: [{ rel: 'alternate', type: 'application/rss+xml',
          title: 'Lagos Life updates (unofficial guide)',
          href: absoluteUrl('/updates/feed.xml') }],
</code></pre>
<p>…and a visible block on the page with the feed address and a copy button, because no major browser shows a feed button for <code>&lt;link rel="alternate"&gt;</code> anymore.</p>
<h2>The result</h2>
<p>The log page, with its feed linked at the bottom, is here: <a href="https://lagoslifegames.com/updates/"><strong>Lagos Life updates: this week's law, daily headlines and gem hunt clues</strong></a>. Each rebuild regenerates the page and the feed together from one JSON file, so they can't drift apart.</p>
<p>The game itself is at <a href="https://lagoslife.app/">lagoslife.app</a>; my site is an independent fan guide, not affiliated with the developers.</p>
<h2>Takeaways</h2>
<ul>
<li><p>A prerendered "server route" is a clean way to emit <strong>non-HTML files</strong> (RSS, sitemap, robots) from the same data as your pages.</p>
</li>
<li><p>List those routes in the prerender <code>pages</code> config with an explicit <code>outputPath</code> — the crawler won't find them.</p>
</li>
<li><p>Date items in the <strong>source's timezone</strong>, and make <strong>GUIDs deterministic</strong> so frequent rebuilds don't spam readers.</p>
</li>
</ul>
]]></content:encoded></item></channel></rss>