<?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: C. Wheatley</title>
    <description>The latest articles on DEV Community by C. Wheatley (@bsymbolic).</description>
    <link>https://dev.to/bsymbolic</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%2F3982019%2F8beb5d78-be18-44b4-b2f8-1a9e798a54e2.jpeg</url>
      <title>DEV Community: C. Wheatley</title>
      <link>https://dev.to/bsymbolic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bsymbolic"/>
    <language>en</language>
    <item>
      <title>AI This Week: Distillation Wars Go Geopolitical</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Thu, 23 Jul 2026 13:12:26 +0000</pubDate>
      <link>https://dev.to/bsymbolic/ai-this-week-distillation-wars-go-geopolitical-4mhl</link>
      <guid>https://dev.to/bsymbolic/ai-this-week-distillation-wars-go-geopolitical-4mhl</guid>
      <description>&lt;p&gt;This was the week AI stopped being a product story and became a foreign-policy one. A White House official publicly accused a Chinese lab of copying Anthropic's frontier model, OpenAI put more than $30 billion behind a single data center campus, and four separate enterprise "agent platforms" landed within weeks of each other. The through-line: capability is now cheap enough, and strategically important enough, that governments are treating model weights the way they once treated enriched uranium.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Releases &amp;amp; the Distillation Fight
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-23-2026" rel="noopener noreferrer"&gt;White House accuses Moonshot of distilling Anthropic's Fable model&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
OSTP Director Michael Kratsios publicly alleged that Moonshot AI distilled Anthropic's Fable model to build Kimi K3 — a 2.8-trillion-parameter model claiming a 76% win rate on Frontend Code Arena and an 88.3 Terminal-Bench score, with open weights due July 27. Kratsios called it "large-scale covert industrial distillation." It's the first time a US official has named a specific model as stolen IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-23-2026" rel="noopener noreferrer"&gt;Independent analysis adds fuel to the claim&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Redwood Research chief scientist Ryan Greenblatt published a cross-entropy analysis finding that Kimi K3 identifies itself as "Claude" disproportionately often across many prompts — the kind of statistical fingerprint that distillation tends to leave behind. Kratsios separately alleged Moonshot accessed export-restricted Nvidia GB300 chips through servers in Thailand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-23-2026" rel="noopener noreferrer"&gt;DeepSeek V4 and Kimi K3 land within days of each other&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The Chinese frontier is shipping fast and cheap: DeepSeek V4's stable release arrived July 24 at $0.44 per million output tokens, with Kimi K3's open weights following July 27. Both continue to narrow the capability gap with US labs at a fraction of the cost — the exact dynamic that made this week's accusations so charged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic AI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-23-2026" rel="noopener noreferrer"&gt;OpenAI launches Presence, its enterprise agent platform&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
On July 22, OpenAI shipped Presence, a platform for deploying agents into customer support, sales, and other high-risk workflows, with BBVA, SoftBank, and IAG as early adopters. It joins a suddenly crowded field: Google Gemini Enterprise, Meta's Business Agent Platform, and the NVIDIA/ServiceNow Project Arc all launched in the same window. The enterprise agent war is officially on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure &amp;amp; Business
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-23-2026" rel="noopener noreferrer"&gt;OpenAI breaks ground on Project Camellia, a $30B+ data center&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Announced July 22, Camellia is a 3.2-gigawatt campus across 1,400 acres in Effingham County, Georgia, with spending exceeding $30 billion and power delivery staged from 2028 to 2032. OpenAI paired it with $80 million in community benefits plus $71 million in Codex credits — a reminder that the constraint on frontier AI is now electricity and concrete, not algorithms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://techstartups.com/2026/07/21/top-tech-news-today-july-21-2026-anthropic-blackrock-tesla/" rel="noopener noreferrer"&gt;BlackRock and MGX pour another $5B into data centers&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
BlackRock and Abu Dhabi's MGX committed an additional $5 billion to Aligned Data Centers on top of a $40 billion acquisition, with total potential deployment reaching $100 billion including debt. Meanwhile, the Albany NanoTech Complex received components of a $400 million ASML High-NA EUV lithography system expected to be operational by year-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://techstartups.com/2026/07/21/top-tech-news-today-july-21-2026-anthropic-blackrock-tesla/" rel="noopener noreferrer"&gt;A federal judge approves Anthropic's $1.5B copyright settlement&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A San Francisco federal judge signed off on Anthropic's $1.5 billion settlement with authors over pirated books used to train Claude. Over 91% of eligible authors and publishers filed claims, with $101 million set aside for attorney fees — one of the largest resolutions yet in the AI-training copyright fights.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-21-2026" rel="noopener noreferrer"&gt;Defense AI clears $3B in disclosed July funding&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Defense-focused AI attracted well over $3 billion in disclosed funding this month alone, led by Shield AI's $1.5 billion Series G and Helsing's earlier €1.8 billion round, alongside a new Anduril–Archer partnership. Capital is following the same geopolitical logic driving the week's headlines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Science &amp;amp; Healthcare
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.globenewswire.com/news-release/2026/07/14/3327157/0/en/AI-Drug-Discovery-Investment-Surges-to-2-Billion-as-Technology-Cuts-Development-Timelines-by-70-Driven-by-Breakthrough-Clinical-Success-Rates.html" rel="noopener noreferrer"&gt;AI drug discovery investment tops $2B as timelines collapse&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
AI-driven drug discovery pulled in more than $2 billion in recent investment, with technology cutting development timelines from 4–5 years to 12–18 months while roughly doubling clinical success rates. About 175 AI-originated drug programs have entered human trials since 2019, and 15–20 could reach pivotal Phase III this year. The FDA is expected to finalize guidance on AI in drug development during 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy &amp;amp; Regulation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://techstartups.com/2026/07/21/top-tech-news-today-july-21-2026-anthropic-blackrock-tesla/" rel="noopener noreferrer"&gt;The US and China schedule formal AI talks for September&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Washington and Beijing are preparing formal AI negotiations in September — before President Xi Jinping's planned September 24 US visit — covering military AI, cyberattacks, model access, and open-weight releases. After a week of distillation accusations and chip-smuggling allegations, the diplomatic channel suddenly looks less like a formality and more like a pressure valve.&lt;/p&gt;

&lt;p&gt;If there's a single takeaway, it's that the interesting AI questions are no longer just "which model scores highest." They're "who trained on what," "who's allowed to buy which chips," and "who pays for the gigawatts." The benchmarks are converging; the politics are diverging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-23-2026" rel="noopener noreferrer"&gt;buildfast — AI News Today July 23, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-21-2026" rel="noopener noreferrer"&gt;buildfast — AI News Today July 21, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://techstartups.com/2026/07/21/top-tech-news-today-july-21-2026-anthropic-blackrock-tesla/" rel="noopener noreferrer"&gt;Tech Startups — Top Tech News, July 21, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.globenewswire.com/news-release/2026/07/14/3327157/0/en/AI-Drug-Discovery-Investment-Surges-to-2-Billion-as-Technology-Cuts-Development-Timelines-by-70-Driven-by-Breakthrough-Clinical-Success-Rates.html" rel="noopener noreferrer"&gt;GlobeNewswire — AI Drug Discovery Investment Surges Past $2 Billion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://llm-stats.com/llm-updates" rel="noopener noreferrer"&gt;llm-stats — Latest AI Model Releases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>weeklydigest</category>
    </item>
    <item>
      <title>AI This Week: Compute Becomes the Bottleneck</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Thu, 16 Jul 2026 14:07:05 +0000</pubDate>
      <link>https://dev.to/bsymbolic/ai-this-week-compute-becomes-the-bottleneck-33cf</link>
      <guid>https://dev.to/bsymbolic/ai-this-week-compute-becomes-the-bottleneck-33cf</guid>
      <description>&lt;p&gt;The story this week wasn't a smarter model — it was the realization that raw capability is no longer the thing that's scarce. Frontier labs shipped cheaper, faster tiers instead of bigger ones, Google literally started rationing GPU access to a competitor, and the money moving around the industry began to dwarf the technical headlines. Compute and capital, not IQ, are now the binding constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Releases
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.aiapps.com/blog/july-ai-mega-update-major-breakthroughs-launches/" rel="noopener noreferrer"&gt;OpenAI ships the GPT-5.6 suite&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
GPT-5.6 went live on July 9 as a three-tier lineup: Sol for high-end reasoning, coding, and science at $5.00 / $30.00 per 1M input/output tokens; Terra, targeting GPT-5.5-level quality at roughly half Sol's cost; and Luna for fast, high-volume work. The split is the whole point — OpenAI is selling price/latency tiers, not one monolithic model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.aiapps.com/blog/july-ai-mega-update-major-breakthroughs-launches/" rel="noopener noreferrer"&gt;Meta's Muse Spark 1.1 undercuts everyone&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Meta's agentic model arrived with a 1-million-token context window at $1.25 / $4.25 per 1M tokens, ranked first on JobBench and Finance Agent V2, and shipped with Meta's first-ever paid developer API ($20 in free credits, US-only). It adds parallel subagent delegation plus computer use across desktop, browser, and mobile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.aiapps.com/blog/july-ai-mega-update-major-breakthroughs-launches/" rel="noopener noreferrer"&gt;Grok 4.5 and Claude Fable 5 round out the frontier&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
xAI's Grok 4.5 is a 1.5-trillion-parameter Mixture-of-Experts model scoring 83.3% on Terminal-Bench 2.1 at $2.00 / $6.00 per 1M tokens, and reportedly burns about 25% as many output tokens as comparable models. Anthropic's Claude Fable 5 returned July 1 after a 19-day pause, retaking the coding crown at 80.3% on SWE-Bench Pro.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-14-2026" rel="noopener noreferrer"&gt;Gemini 3.5 Pro is on deck&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Google delayed Gemini 3.5 Pro from June into July while tuning it on early feedback, with a launch expected around July 17 — reportedly a 2-million-token context window at $1.25 input / $10 output per 1M tokens. The current &lt;a href="https://felloai.com/best-ai-models/" rel="noopener noreferrer"&gt;Gemini 3.1 Pro&lt;/a&gt; already posts 94.3% on GPQA Diamond and 77.1% on ARC-AGI-2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business &amp;amp; Industry
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-14-2026" rel="noopener noreferrer"&gt;Google caps Meta's access to Gemini&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In the clearest sign of where the real constraint sits, Google limited Meta's access to its Gemini models, citing insufficient compute to meet Meta's requests. When one of the largest infrastructure owners on Earth has to turn a paying customer away, the bottleneck is silicon, not smarts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-14-2026" rel="noopener noreferrer"&gt;OpenAI floats a 5% government stake&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
OpenAI proposed handing the US government a 5% equity stake worth about $42.6 billion at an $852 billion valuation, part of a broader pitch for leading AI firms to seed a public sovereign wealth fund modeled on Alaska's permanent fund. A poll cited alongside it found 69% of US workers support requiring such transfers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-14-2026" rel="noopener noreferrer"&gt;Anthropic preps an October IPO&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Anthropic is reportedly preparing an S-1 for an October 2026 IPO on roughly $47 billion in annualized revenue and 2026 profitability, while negotiating with Samsung for a custom AI chip. Meanwhile TSMC posted Q2 revenue of NT$1.27 trillion ($39.62 billion), up 36% year-over-year on AI chip demand, and global startups raised $510 billion in H1 2026 — most of it AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic AI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-14-2026" rel="noopener noreferrer"&gt;Enterprise agent platforms go head-to-head&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Google unveiled an expanded Gemini Enterprise portfolio for building, orchestrating, and governing fleets of agents across an organization — squaring off directly against OpenAI's ChatGPT Work (launched July 9, pairing ChatGPT with Codex so non-technical staff can build docs, sheets, and apps) and &lt;a href="https://aiweekly.co/ai-news-today/anthropic-news" rel="noopener noreferrer"&gt;Anthropic's Claude Cowork&lt;/a&gt;, which handles long-running email, calendar, and file tasks that keep going even when your device is offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-14-2026" rel="noopener noreferrer"&gt;Gemini reasoning lands inside Boston Dynamics' Spot&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Boston Dynamics integrated Gemini Robotics-ER 1.6 into its Spot robots, giving them autonomous spatial reasoning and on-the-fly decision-making for industrial inspection — agents stepping off the screen and onto four legs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Science &amp;amp; Healthcare
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thisweekinsciencenews.com/blog/2026/07/06/breaking-barriers-in-cancer-care-and-beyond-innovations-in-health-and-science-from-july-2026/" rel="noopener noreferrer"&gt;A pan-cancer AI predicts immunotherapy response&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A model called COMPASS, published in Nature Medicine, analyzes tumor gene expression to forecast immunotherapy success across 33 cancer types and multiple checkpoint inhibitors. Trained on over 10,000 tumors, it outperformed existing methods and generalized to cancers it had never seen during training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://thisweekinsciencenews.com/blog/2026/07/06/breaking-barriers-in-cancer-care-and-beyond-innovations-in-health-and-science-from-july-2026/" rel="noopener noreferrer"&gt;The FDA clears its first patient-facing generative AI&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The clearance is deliberately narrow — the tool is bound to non-diagnostic tasks and does not write prescriptions — but it's a first. Alongside it, Evernorth placed a $100 million bet on AI pharmacy automation and roughly $95 million flowed into clinical trial automation, signaling that regulatory and pharmacy workflows are the next high-value frontier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy &amp;amp; Regulation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.ftc.gov/news-events/news/press-releases/2026/07/ftc-seeks-public-comment-policy-statement-addressing-ai-accuracy" rel="noopener noreferrer"&gt;The FTC targets "AI accuracy suppression"&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The Federal Trade Commission opened public comment (due July 31) on a policy statement arguing that state laws pressuring companies to alter model outputs — Colorado's AI Act is named — may be impliedly preempted where they conflict with federal rules. It frames tampering with AI outputs as a potential deceptive practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.insideglobaltech.com/2026/07/13/u-s-tech-legislative-regulatory-update-second-quarter-2026/" rel="noopener noreferrer"&gt;The EU fuses cybersecurity and AI oversight&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
On July 7 the European Commission presented an Action Plan on Cybersecurity and Artificial Intelligence, merging two previously separate regulatory regimes. Stateside, at least 35 AI-related bills have now been enacted across states, with California, Texas, Illinois, and Utah duties already in force and Colorado's replacement framework arriving January 2027.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.aiapps.com/blog/july-ai-mega-update-major-breakthroughs-launches/" rel="noopener noreferrer"&gt;AIapps — July 2026 AI Mega-Update: Every Major Breakthrough &amp;amp; Launch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.buildfastwithai.com/blogs/ai-news-today-july-14-2026" rel="noopener noreferrer"&gt;BuildFastWithAI — AI News Today July 14, 2026: 15 Biggest Stories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://felloai.com/best-ai-models/" rel="noopener noreferrer"&gt;Fello AI — Best AI Models in July 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aiweekly.co/ai-news-today/anthropic-news" rel="noopener noreferrer"&gt;AI Weekly — Anthropic AI News Tracker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thisweekinsciencenews.com/blog/2026/07/06/breaking-barriers-in-cancer-care-and-beyond-innovations-in-health-and-science-from-july-2026/" rel="noopener noreferrer"&gt;This Week in Science — Innovations in Health and Science from July 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ftc.gov/news-events/news/press-releases/2026/07/ftc-seeks-public-comment-policy-statement-addressing-ai-accuracy" rel="noopener noreferrer"&gt;FTC — Seeks Public Comment on Policy Statement Addressing AI Accuracy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.insideglobaltech.com/2026/07/13/u-s-tech-legislative-regulatory-update-second-quarter-2026/" rel="noopener noreferrer"&gt;Inside Global Tech — U.S. Tech Legislative &amp;amp; Regulatory Update, Q2 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.marketingprofs.com/opinions/2026/55247/ai-update-july-10-2026-ai-news-and-views-from-the-past-week" rel="noopener noreferrer"&gt;MarketingProfs — AI Update, July 10, 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>weeklydigest</category>
    </item>
    <item>
      <title>Building LibreSprite From Source: The Free GPL Fork, Built With an All-Open Toolchain</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Tue, 14 Jul 2026 02:45:24 +0000</pubDate>
      <link>https://dev.to/bsymbolic/building-libresprite-from-source-the-free-gpl-fork-built-with-an-all-open-toolchain-3pp0</link>
      <guid>https://dev.to/bsymbolic/building-libresprite-from-source-the-free-gpl-fork-built-with-an-all-open-toolchain-3pp0</guid>
      <description>&lt;p&gt;A while back I compiled Aseprite from source — a source-available, paid editor I built for myself with Visual Studio and a prebuilt Skia. This is the other half of that story. LibreSprite is the &lt;strong&gt;free, GPL&lt;/strong&gt; fork of Aseprite, and I wanted to build it the way it's meant to be built: with a fully open toolchain, no proprietary compiler, no pinned vendor binary. I cloned the repo, installed a pile of dependencies through a package manager, and ended up with a working &lt;code&gt;libresprite.exe&lt;/code&gt; (LibreSprite 1.2-dev) that launches with the full GUI — drawing canvas, palette, animation preview, the lot. Claude was my pair programmer for it, and most of the work was figuring out that the project's own official Windows instructions no longer point at anything that exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;LibreSprite is a free and open-source pixel-art and sprite-animation editor: layers and frames, onion skinning, real-time animation preview, tiled drawing, palette management, the usual pixel-precise toolset. If that sounds exactly like Aseprite, that's because it is one — or was. LibreSprite started as a fork of Aseprite back when Aseprite was distributed under the GNU General Public License v2. On August 26th, 2016, Aseprite moved to a proprietary license; LibreSprite forked from the last GPL commit and has been maintained as a free, GPL project by its community ever since.&lt;/p&gt;

&lt;p&gt;That history is the whole reason this post exists as a companion to my Aseprite build, not a rerun of it. Aseprite today is source-available under its own EULA — you can compile it for yourself, but you can't redistribute the binary, and the canonical build uses Visual Studio plus a prebuilt Skia. LibreSprite is genuinely free software under the GPL, and its build leans on an all-open toolchain: an open compiler (g++ via MSYS2), open dependencies installed from a package repository, and an open build system (CMake + Ninja). Same lineage, opposite license, opposite toolchain. Full credit for the editor goes to the LibreSprite project and its contributors — and, further back, to David Capello and the original Aseprite authors. My work here was purely getting it to compile on Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The supported Windows toolchain for LibreSprite is MSYS2 — the project ships with no Visual Studio solution you'd actually want to use today, and the real path runs through MinGW. The toolchain that worked for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MSYS2 with the &lt;code&gt;ucrt64&lt;/code&gt; environment&lt;/strong&gt; — using &lt;strong&gt;g++ 16.1.0&lt;/strong&gt;. This is far newer than the codebase targets, but it built clean anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies installed via pacman&lt;/strong&gt;, all from the &lt;code&gt;mingw-w64-ucrt-x86_64-*&lt;/code&gt; set: &lt;code&gt;cmake&lt;/code&gt;, &lt;code&gt;ninja&lt;/code&gt;, &lt;code&gt;pkgconf&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;freetype&lt;/code&gt;, &lt;code&gt;giflib&lt;/code&gt;, &lt;code&gt;libjpeg-turbo&lt;/code&gt;, &lt;code&gt;libpng&lt;/code&gt;, &lt;code&gt;libwebp&lt;/code&gt;, &lt;code&gt;pixman&lt;/code&gt;, &lt;code&gt;SDL2&lt;/code&gt;, &lt;code&gt;SDL2_image&lt;/code&gt;, &lt;code&gt;tinyxml2&lt;/code&gt;, &lt;code&gt;zlib&lt;/code&gt;, and &lt;code&gt;libarchive&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CMake + Ninja&lt;/strong&gt; as the configure-and-build pair: from a ucrt64 shell, &lt;code&gt;cmake -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo ..&lt;/code&gt; followed by &lt;code&gt;ninja libresprite&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That build ran 1351 steps to completion with zero errors. The one thing that didn't resolve was V8 — the project couldn't find it, so the scripting engine fell back to the bundled duktape interpreter, which is a perfectly fine default and cost nothing. SDL2 and freetype do the windowing and font rendering, the image libraries handle the file formats, and the whole thing links against the MSYS2 runtime rather than the MSVC one.&lt;/p&gt;

&lt;p&gt;The contrast with the Aseprite build is the point. There, the hard part was a proprietary-adjacent stack: a specific Visual Studio version and a prebuilt Skia binary pinned to an exact revision. Here, every piece is open and installable from a package repo. The pain moved somewhere else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INSTALL.md's official Windows path is dead — &lt;code&gt;mingw32&lt;/code&gt;/&lt;code&gt;i686&lt;/code&gt; no longer exists in modern MSYS2.&lt;/strong&gt; This is the one to know before you start. LibreSprite's &lt;code&gt;INSTALL.md&lt;/code&gt; lists Windows 10 + VS2015 at the top, then gives the real instructions as a &lt;code&gt;pacman&lt;/code&gt; command to run "in mingw32" using the 32-bit i686 dependency set (&lt;code&gt;mingw-w64-i686-gcc&lt;/code&gt;, &lt;code&gt;mingw-w64-i686-cmake&lt;/code&gt;, and so on). That environment was removed from MSYS2 in 2023. If you follow the documented steps on a current MSYS2 install, the i686 packages simply won't resolve and there's no mingw32 shell to run them in. The fix is to use the &lt;strong&gt;&lt;code&gt;ucrt64&lt;/code&gt;&lt;/strong&gt; environment instead and substitute the &lt;code&gt;ucrt-x86_64&lt;/code&gt; package names for every &lt;code&gt;i686&lt;/code&gt; one in the list. That's the entire difference between "the official instructions fail immediately" and "1351 steps, zero errors." On this machine ucrt64 was the only environment with a working toolchain at all — the &lt;code&gt;mingw64&lt;/code&gt; directory existed but was empty — so ucrt64 wasn't just the modern choice, it was the only one available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The finished &lt;code&gt;.exe&lt;/code&gt; needs &lt;code&gt;C:\msys64\ucrt64\bin&lt;/code&gt; on &lt;code&gt;PATH&lt;/code&gt; to run.&lt;/strong&gt; Because LibreSprite is built against the MSYS2 runtime, the binary dynamically links a stack of DLLs that live in the ucrt64 bin directory — &lt;code&gt;libstdc++&lt;/code&gt;, SDL2, freetype, and friends. Double-clicking &lt;code&gt;libresprite.exe&lt;/code&gt; from a plain Windows shell that doesn't have that directory on &lt;code&gt;PATH&lt;/code&gt; gets you a missing-DLL error, not the editor. The fix is to make sure &lt;code&gt;C:\msys64\ucrt64\bin&lt;/code&gt; is on &lt;code&gt;PATH&lt;/code&gt; when you launch it (or to ship those DLLs alongside the exe for a standalone build). This is the open-toolchain tax: a Visual Studio build statically pulls in its runtime, but a MinGW build expects its runtime to be findable. Worth knowing before you conclude a successful build is "broken."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A GUI-subsystem binary prints nothing to &lt;code&gt;--version&lt;/code&gt;, which looks like a failure but isn't.&lt;/strong&gt; &lt;code&gt;libresprite.exe&lt;/code&gt; is compiled as a Windows GUI-subsystem program, so it has no console attached. Run &lt;code&gt;libresprite.exe --version&lt;/code&gt; from a terminal and you get... nothing — it prints no text and exits cleanly. The first time, that reads as a silent crash. It isn't: the binary is fine, it just has no stdout to write to. The way to actually verify the build is to launch it and look at the window — which is exactly what the screenshot at the top of this post is: LibreSprite 1.2-dev running, with a sprite open on the canvas, the palette docked on the left, and the animation preview floating over it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;A working &lt;code&gt;build/bin/libresprite.exe&lt;/code&gt; (LibreSprite 1.2-dev), built entirely from source on Windows with a fully open toolchain: MSYS2 ucrt64, g++ 16, pacman-installed dependencies, and CMake driving Ninja. I verified it the only way a GUI binary lets you — by running it and confirming the editor comes up live, drawing canvas and palette and preview all working. The whole build is reproducible from a clean checkout once you know to reach for ucrt64 instead of the dead mingw32 path the project's INSTALL.md still advertises.&lt;/p&gt;

&lt;p&gt;To be clear about what this is: LibreSprite is someone else's software — a free, GPL project built and maintained by its contributors. My contribution was narrow: getting it to compile on a modern Windows machine, and pinning down that the official instructions had quietly rotted. Set next to the Aseprite build, it's a neat study in contrasts — same family of editor, but one is paid and source-available with a proprietary-flavored toolchain, and the other is free, GPL, and built with nothing but open parts.&lt;/p&gt;

&lt;p&gt;It's one of a series of projects I've been building this way; the running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>msys2</category>
      <category>pixelart</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Aseprite From Source: A Paid Pixel-Art Editor, Free If You Compile It</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Tue, 14 Jul 2026 02:45:06 +0000</pubDate>
      <link>https://dev.to/bsymbolic/building-aseprite-from-source-a-paid-pixel-art-editor-free-if-you-compile-it-44h3</link>
      <guid>https://dev.to/bsymbolic/building-aseprite-from-source-a-paid-pixel-art-editor-free-if-you-compile-it-44h3</guid>
      <description>&lt;p&gt;Aseprite is a pixel-art editor and animation tool that costs about twenty dollars on Steam and itch.io. It's also source-available: the full source lives on GitHub, and the project explicitly allows you to compile it yourself for your own use. So I did. I cloned the repo, fought the toolchain, and ended up with a working &lt;code&gt;aseprite.exe&lt;/code&gt; (~20.6 MB) that launches and reports "Aseprite 1.x-dev" — the same editor, built by me, on my own machine. I did it with Claude as a pair programmer, and most of the work was untangling exactly one non-obvious trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Aseprite is the editor a lot of pixel artists reach for first: a tight tool for drawing sprites, tilesets, and frame-by-frame animation, with onion skinning, palette management, and sprite-sheet export. The binaries are paid, but the source is genuinely public — not open-source in the OSI sense, but source-available under its own EULA. The distinction matters: building it for personal use is explicitly fine, while redistributing the binaries you produce is not. So this is a build-it-yourself story, not a fork-and-republish one. Full credit for the editor belongs to Igara Studio S.A. — David Capello and contributors; my work here was purely getting it to compile on Windows.&lt;/p&gt;

&lt;p&gt;Why bother compiling something you can buy? Partly because the option exists and it's a clean test of a real toolchain — Aseprite is a substantial C++ codebase with a pile of vendored dependencies and a custom graphics backend. If you can build it, you can build almost anything. And partly because a build you control is a build you can patch, instrument, or pin to a specific commit. My checkout sits at &lt;code&gt;7063d5362&lt;/code&gt;, near the &lt;code&gt;v1.3.18-beta3&lt;/code&gt; tag on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The division of labor was the usual one for these projects: I decided what I wanted and made the judgment calls, and Claude wrote the scripts, read the error logs, and worked out which knob to turn next.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Aseprite's renderer is built on &lt;a href="https://skia.org/" rel="noopener noreferrer"&gt;Skia&lt;/a&gt;, Google's 2D graphics library — the same one behind Chrome's canvas. Skia is famously painful to compile from scratch, so the good news is you don't have to. Aseprite's own &lt;code&gt;build.sh --auto&lt;/code&gt; downloads a &lt;strong&gt;prebuilt Skia&lt;/strong&gt; matching the version the project expects, which is &lt;code&gt;m124-08a5439a6b&lt;/code&gt; (the exact tag is pinned in &lt;code&gt;laf/misc/skia-tag.txt&lt;/code&gt;). That one decision removes the single biggest source of pain from the whole build.&lt;/p&gt;

&lt;p&gt;The toolchain that worked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Studio 2022 Build Tools&lt;/strong&gt; for the compiler (&lt;code&gt;cl&lt;/code&gt; 14.44, x64). My machine also has VS 2026, but Aseprite officially targets VS 2022, so I pointed the build at the 2022 Build Tools deliberately rather than letting it find the newer one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CMake 3.31.6, portable&lt;/strong&gt; — unzipped into &lt;code&gt;.deps/&lt;/code&gt;, used instead of the CMake 4.x I had installed via winget. This is the trap; more on it below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ninja 1.13.2&lt;/strong&gt; as the build generator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prebuilt Skia m124&lt;/strong&gt;, auto-downloaded into &lt;code&gt;.deps/skia-m124/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo ships a &lt;code&gt;build.cmd&lt;/code&gt; for Windows, but it's not usable as-is on my setup: it hardcodes a Visual Studio 2022 &lt;strong&gt;Community&lt;/strong&gt; vcvars path I don't have (I have the Build Tools, not the full IDE), and it ends in a &lt;code&gt;pause&lt;/code&gt; that hangs any non-interactive run. So I wrote a thin launcher, &lt;code&gt;build-everything.bat&lt;/code&gt;, that does four things in order: prepends the portable CMake and Ninja to &lt;code&gt;PATH&lt;/code&gt;, calls the VS 2022 Build Tools &lt;code&gt;vcvars64.bat&lt;/code&gt; to set up the compiler environment, prints a tool-check so I can see exactly which &lt;code&gt;cl&lt;/code&gt;, &lt;code&gt;cmake&lt;/code&gt;, and &lt;code&gt;ninja&lt;/code&gt; are about to run, and then hands off to the official &lt;code&gt;build.sh --auto --norun&lt;/code&gt; through Git's bundled &lt;code&gt;sh.exe&lt;/code&gt;. The &lt;code&gt;--auto&lt;/code&gt; flag does the Skia download and dependency wiring; &lt;code&gt;--norun&lt;/code&gt; keeps it from launching the editor when it finishes.&lt;/p&gt;

&lt;p&gt;To rebuild after a source change, I just re-run that one batch file — Ninja handles the incremental build from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CMake 4.x refuses to configure the build, and "newer" is the problem, not the fix.&lt;/strong&gt; This is the one that cost the most time and is the single most important thing to know if you try this yourself. CMake 4.x removed support for old &lt;code&gt;cmake_minimum_required&lt;/code&gt; declarations: if a project (or any library it pulls in) asks for a minimum CMake version below 3.5, CMake 4.x treats it as a hard error and stops. Aseprite vendors a stack of dependencies, several of which still declare ancient minimums — 2.4, 2.8, 3.0, 3.2, 3.4. With the winget-installed CMake 4.3.3, configuration dies on the first one it hits. The fix is counterintuitive: install an &lt;em&gt;older&lt;/em&gt; CMake. I dropped a portable &lt;strong&gt;CMake 3.31.6&lt;/strong&gt; into &lt;code&gt;.deps/&lt;/code&gt; and put it ahead of everything on &lt;code&gt;PATH&lt;/code&gt;. CMake 3.31 only &lt;em&gt;warns&lt;/em&gt; about those old minimums instead of erroring, so the configure step completes. If you take one thing from this post: when a C++ project with vendored libraries won't configure, check your CMake version before you change anything else — bleeding-edge CMake is often the cause, not the cure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The repo's own &lt;code&gt;build.cmd&lt;/code&gt; assumes a setup you probably don't have.&lt;/strong&gt; It's written for someone running full Visual Studio 2022 Community, so its vcvars path is wrong for a Build Tools install, and the trailing &lt;code&gt;pause&lt;/code&gt; means it can't be driven by a script or an agent. Rather than patch it in place, I left it alone and wrote a small launcher that sets the environment explicitly and calls the cross-platform &lt;code&gt;build.sh&lt;/code&gt; underneath. The lesson that generalizes: when a project's convenience script doesn't match your environment, the official &lt;code&gt;build.sh --auto&lt;/code&gt; is usually the more honest path — it's what the project actually maintains, and it does the dependency fetching for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't try to build Skia yourself — let &lt;code&gt;--auto&lt;/code&gt; fetch the pinned binary.&lt;/strong&gt; It's tempting to think "build from source" means building &lt;em&gt;everything&lt;/em&gt; from source, including Skia. Resist it. Aseprite pins an exact Skia revision (&lt;code&gt;m124-08a5439a6b&lt;/code&gt;) because its renderer depends on that specific API surface, and &lt;code&gt;build.sh --auto&lt;/code&gt; downloads a matching prebuilt artifact for you. Compiling Skia by hand would mean reproducing Google's depot_tools-based build and then hoping the version matched — hours of work to arrive at the same binary the script grabs in seconds. The pin in &lt;code&gt;laf/misc/skia-tag.txt&lt;/code&gt; is the source of truth if you ever need to know which revision your build expects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;A working &lt;code&gt;build/bin/aseprite.exe&lt;/code&gt;, ~20.6 MB, that launches and runs as the real editor — drawing, palettes, animation timeline, the lot — built entirely from source on Windows. Re-running &lt;code&gt;build-everything.bat&lt;/code&gt; does an incremental rebuild, so the whole thing is reproducible from a clean checkout: portable CMake 3.31, VS 2022 Build Tools, Ninja, and the auto-downloaded Skia m124.&lt;/p&gt;

&lt;p&gt;To be clear about what this is and isn't: it's a personal build of someone else's software, allowed under Aseprite's source-available license for exactly this kind of self-compiling. There's no binary to hand out and no fork to link — the value was entirely in getting a large, real C++ project to compile cleanly, and in pinning down that the CMake version was the thing standing in the way the whole time.&lt;/p&gt;

&lt;p&gt;It's one of a series of projects I've been building this way; the running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>cmake</category>
      <category>windows</category>
      <category>pixelart</category>
    </item>
    <item>
      <title>Blender Planets: A Reusable Skill for Building Worlds from Real NASA Maps</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Thu, 09 Jul 2026 17:46:42 +0000</pubDate>
      <link>https://dev.to/bsymbolic/blender-planets-a-reusable-skill-for-building-worlds-from-real-nasa-maps-1o8b</link>
      <guid>https://dev.to/bsymbolic/blender-planets-a-reusable-skill-for-building-worlds-from-real-nasa-maps-1o8b</guid>
      <description>&lt;p&gt;Most of the projects I write up here are &lt;em&gt;things&lt;/em&gt; — a game, a bridge, a tool. This one is a little different. &lt;strong&gt;Blender Planets&lt;/strong&gt; is a reusable Claude Code skill: a written capability that Claude loads on demand, encoding how to build a believable planet, moon, or gas giant in Blender from real NASA-derived map data instead of procedural noise. It isn't a single scene I rendered once. It's the recipe, the gotchas, and a set of helper functions, packaged so that the next time I say "drop Mars into this shot," Claude already knows how to do it well.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ixdunta740289f5awln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ixdunta740289f5awln.png" alt="Earth, Mars, the cratered Moon, and a ringed gas giant in a single Blender scene, with an orbital station for scale — the production render that validated the skill" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A skill, in Claude Code terms, is a Markdown file (plus optional references) that gets injected into Claude's context when the work matches its trigger. This one triggers whenever I'm "placing a textured sphere into a scene, adding an atmosphere glow, planetary rings, or real terrain relief, or when a planet looks like a flat red/blue decal." That last clause is the honest origin story: the first time I asked for a planet, I got a flat-shaded ball that read like a sticker. The skill exists so that never happens again.&lt;/p&gt;

&lt;p&gt;What it encodes is general, not one-off. It doesn't say "here's how I made &lt;em&gt;this&lt;/em&gt; Mars." It says: here's how to build &lt;em&gt;any&lt;/em&gt; rocky body, &lt;em&gt;any&lt;/em&gt; gas giant, &lt;em&gt;any&lt;/em&gt; moon — where to get the real imagery, when to add true displacement versus just a bump, how to make the atmosphere read as haze rather than a hard ring, and how to land the body precisely in frame next to other objects without it floating off or hiding behind something. The hero image above is the scene that validated all of it: Earth with real continents, Mars with its rust surface and a thin atmosphere halo, the cratered Moon, and a ringed gas giant, with an orbital station thrown in for scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The whole philosophy is &lt;strong&gt;real maps, never procedural noise&lt;/strong&gt;. Procedural planets always look fake up close, because real planets aren't noise — they're continents and craters and cloud bands that we recognize. So the skill sources actual equirectangular imagery and wraps it on a UV sphere whose default UVs already map equirectangular projection correctly.&lt;/p&gt;

&lt;p&gt;The map data comes from two NASA-derived sources, both credited in the skill:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.solarsystemscope.com/textures/" rel="noopener noreferrer"&gt;Solar System Scope&lt;/a&gt;&lt;/strong&gt; for the color maps — equirectangular color maps (up to 8K) of Mercury through Neptune (CC BY 4.0, Viking/MOLA-derived), plus a Saturn ring alpha map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://planetpixelemporium.com/" rel="noopener noreferrer"&gt;Planet Pixel Emporium&lt;/a&gt;&lt;/strong&gt; for grayscale elevation/bump maps — &lt;code&gt;marsbump&lt;/code&gt;, &lt;code&gt;moonbump&lt;/code&gt;, &lt;code&gt;earthbump&lt;/code&gt;. These are the &lt;strong&gt;MOLA&lt;/strong&gt; (Mars Orbiter Laser Altimeter) and &lt;strong&gt;LOLA&lt;/strong&gt; (Lunar Orbiter Laser Altimeter) datasets, the actual laser-altimetry surveys of Mars and the Moon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there the recipe forks by body type. For a &lt;strong&gt;rocky body&lt;/strong&gt;, the elevation map does double duty: it feeds a Bump node for fine shading detail &lt;em&gt;and&lt;/em&gt; a real Displace modifier (over a simple subdivision) so the terrain physically breaks the silhouette — Olympus Mons actually pokes out, craters are real geometry, not a painted illusion. For a &lt;strong&gt;gas giant&lt;/strong&gt;, there's no elevation at all; the bands live entirely in the color map, so displacement is skipped and Saturn just gets its rings.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;atmosphere&lt;/strong&gt; is a separate sphere a few percent larger than the planet, with a material that's transparent except at the grazing angle. A &lt;code&gt;LayerWeight → Facing&lt;/code&gt; output drives a color ramp into a mix between a transparent and an emission shader, so the glow only appears at the limb — a Fresnel rim halo. That's what sells "this body has air" without a volumetric.&lt;/p&gt;

&lt;p&gt;The part I think makes it a real skill rather than a snippet is the bundle of reusable helpers in &lt;code&gt;references/planet-toolkit.md&lt;/code&gt;: &lt;code&gt;download&lt;/code&gt;, &lt;code&gt;build_planet&lt;/code&gt;, &lt;code&gt;add_atmosphere&lt;/code&gt;, &lt;code&gt;add_rings&lt;/code&gt;, &lt;code&gt;place_in_frame&lt;/code&gt;, and &lt;code&gt;closeup&lt;/code&gt;. They're paste-into-&lt;code&gt;execute_blender_code&lt;/code&gt; functions, plus a per-body cheat sheet — Mars gets thin rust atmosphere and MOLA relief, Saturn gets pale-gold haze and a ~26.7° tilt, Uranus rolls on its side at ~98°, and so on. Claude doesn't reinvent any of this per scene; it fills in the parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;These are the ones the skill captures because they cost real time the first time around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Height maps have to be loaded as Non-Color, or the relief comes out wrong.&lt;/strong&gt; This is the quiet one. If you load a grayscale elevation map as a normal sRGB color texture, Blender applies the color transform to it and your bump and displacement both go muddy and inaccurate — the elevations are simply wrong. The fix is a one-liner: set the image's &lt;code&gt;colorspace_settings.name = 'Non-Color'&lt;/code&gt; before wiring it into the Bump and Displace nodes. The skill flags it twice because it's invisible until you notice the terrain doesn't match the real planet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong displacement makes peaks punch through the atmosphere shell.&lt;/strong&gt; Once the Mars relief is real geometry, the tallest features can poke &lt;em&gt;out&lt;/em&gt; past the atmosphere sphere, leaving an ugly hard ring where terrain intersects haze. Two fixes, and the skill gives both: size the shell above the maximum displaced radius (&lt;code&gt;base*scale + strength*0.5*scale&lt;/code&gt;), or widen the Fresnel color ramp so the halo reads as soft haze that swallows the intrusion rather than a thin detached band. The Mars closeup below is what it looks like when that's tuned right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynzll28czia200wwjgct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynzll28czia200wwjgct.png" alt="Close-up of Mars showing real MOLA elevation relief breaking the silhouette and a soft rust-colored Fresnel atmosphere halo at the limb" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting a planet precisely "in frame" is harder than it looks — don't hand-roll camera vectors.&lt;/strong&gt; My instinct was to compute the camera's right and up vectors and offset from there. That breaks the moment the camera has any roll. The skill's &lt;code&gt;place_in_frame&lt;/code&gt; instead uses &lt;code&gt;cam.data.view_frame(scene)&lt;/code&gt; to get the four actual frustum corners, interpolates between them &lt;strong&gt;bilinearly&lt;/strong&gt; for the target screen position, and then &lt;em&gt;verifies&lt;/em&gt; the result with &lt;code&gt;world_to_camera_view&lt;/code&gt; — which returns normalized device coordinates where x and y should land in [0, 1] and z is depth. That depth value also tells you whether the body is hidden behind foreground geometry. The rule the skill states plainly: never assume a planet sits in frame — check the NDC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a skill
&lt;/h2&gt;

&lt;p&gt;I could have just kept the Blender Python for that one Earth/Moon/Mars scene lying around in a file. The reason to make it a skill instead is that the knowledge is reusable and the per-scene work is not. The hard-won parts — that elevation maps are Non-Color, that the atmosphere shell has to clear the displaced peaks, that camera placement needs frustum corners rather than basis vectors, that gas giants get no displacement — are the same for every planet I'll ever build. Encoding them once means every future scene starts from the validated version instead of from my first flat-decal mistake.&lt;/p&gt;

&lt;p&gt;It's also built on top of a more general &lt;code&gt;blender-scripting&lt;/code&gt; skill — probe the API instead of assuming it, isolate work in its own scene, build then render then look. Blender Planets is the planet-specific layer on that foundation, which is itself a small argument for how these skills compose: a general discipline at the base, a domain on top.&lt;/p&gt;

&lt;p&gt;The skill is validated in production on the scene at the top of this post — Earth, Moon, Mars, and a ringed giant, all from real maps, with real relief and real atmosphere halos. Credit where it's due: the color imagery is from &lt;a href="https://www.solarsystemscope.com/textures/" rel="noopener noreferrer"&gt;Solar System Scope&lt;/a&gt; and the elevation data from &lt;a href="https://planetpixelemporium.com/" rel="noopener noreferrer"&gt;Planet Pixel Emporium&lt;/a&gt;, which packages NASA's MOLA and LOLA altimetry. The skill's job is just to put that data on a sphere correctly, every time.&lt;/p&gt;

&lt;p&gt;This is one of a series of posts on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blender</category>
      <category>3d</category>
      <category>space</category>
      <category>claude</category>
    </item>
    <item>
      <title>Building Plato's Atlantis in Blender — a Parametric Ring City</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Thu, 09 Jul 2026 17:46:22 +0000</pubDate>
      <link>https://dev.to/bsymbolic/building-platos-atlantis-in-blender-a-parametric-ring-city-1fn2</link>
      <guid>https://dev.to/bsymbolic/building-platos-atlantis-in-blender-a-parametric-ring-city-1fn2</guid>
      <description>&lt;p&gt;In Plato's &lt;em&gt;Critias&lt;/em&gt;, Atlantis is laid out as a set of concentric rings — a central island crowned by the Temple of Poseidon, wrapped by alternating bands of water and land joined by bridges, sealed by a great outer wall, and pierced by a canal running out to the open sea. It's one of those descriptions that's basically a build spec already. So I had Claude write a single parametric Blender script that generates the whole city from that geometry, and rendered it a few different ways. This is a post about what the script does and how the renders turned out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfshwacqt6n8ostq5j01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfshwacqt6n8ostq5j01.png" alt="Three-quarter view of the Atlantis ring city at sunset — concentric stone rings of water and land around a central gold-domed temple, ringed by mountains" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The scene is the city as Plato describes it, built from the centre out. A central acropolis carries the Temple of Poseidon. Around it run three pairs of rings — water, then land, then water, then land — each land ring scattered with buildings, all of it enclosed by a defensive wall and ringed by mountains. A canal cuts straight out through the rings to the sea.&lt;/p&gt;

&lt;p&gt;The look is clean and stylized rather than photoreal — smooth marble-white platforms, a gold dome, red-and-gold roofs on the scattered structures, simple conical mountains. It reads more like an architectural model or a diorama than a photograph, and I think that suits a half-legendary city. Everything is driven by a config block at the top of the script, so the island radius, the number and width of the rings, the building count, and the direction of the canal are all just numbers you can change.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The split of work was the usual one for these sessions: I decided what to build and judged whether each render looked right, and Claude wrote the Blender Python. The whole thing is one file, &lt;code&gt;atlantis.py&lt;/code&gt;, that runs headless (&lt;code&gt;blender --background --python atlantis.py&lt;/code&gt;) or from Blender's text editor, and writes a still at the end.&lt;/p&gt;

&lt;p&gt;The geometry is generated procedurally, not modeled by hand. The rings and platforms are built with &lt;code&gt;bmesh&lt;/code&gt; rather than primitives — there's a &lt;code&gt;disc&lt;/code&gt; helper that lays down a solid cylinder for the central island, and a &lt;code&gt;ring&lt;/code&gt; helper that builds a hollow annular band for each ring of land, both walked vertex by vertex around 96–140 segments. The script plans the concentric layout first, marching a radius outward and appending a &lt;code&gt;(water, land)&lt;/code&gt; pair for each ring, then builds each band at the right inner and outer radius. A single huge sea disc underneath fills every water ring and extends out as the open ocean.&lt;/p&gt;

&lt;p&gt;The buildings are scattered, not placed. For each land ring the script drops a set number of structures at random angles and radii — sometimes a round tower with a conical roof, sometimes a rectangular hall — picking from a small palette of marble, stone, and wall materials, with roofs in red tile or gold. It deliberately keeps a wedge clear where the canal runs so nothing gets built across the channel. Because it's seeded (the config fixes the random seed), the same layout comes back every run.&lt;/p&gt;

&lt;p&gt;The centrepiece is the Temple of Poseidon, built explicitly rather than scattered: a stepped circular marble base, a ring of sixteen columns, an entablature and a rounded gold dome, topped with a small golden finial. The other set pieces follow Plato's text too — bridges span each water ring along the cardinal axes (skipping the one direction the canal occupies), and the great outer wall is a ring of stone capped and studded with towers, tipped in gold to stand in for &lt;em&gt;orichalcum&lt;/em&gt;, the legendary metal Plato says sheathed Atlantis. A band of conical mountains encircles the whole thing.&lt;/p&gt;

&lt;p&gt;Materials are plain Principled BSDF shaders set up in code — a palette of sea, land, stone, marble, gold, roof-red, wall, and mountain, each just a base colour with roughness and the odd metallic or transmission value. There are no image textures and no procedural node graphs; the clean diorama look comes entirely from flat-coloured materials under a single sun. The render is EEVEE (the script asks for EEVEE Next and falls back to plain EEVEE), at 1600×1000, with a single sun, a blue-tinted world for ambient light, and an elevated three-quarter camera tracked to the centre of the city. For the hero pass I dropped the sun low and warm to get the sunset light; that's a lighting choice for the shot, not something the script hardcodes.&lt;/p&gt;

&lt;p&gt;One small thing worth noting from the code: the material socket for transmission got renamed across Blender versions, so the helper tries &lt;code&gt;Transmission Weight&lt;/code&gt; first and falls back to &lt;code&gt;Transmission&lt;/code&gt;. That's the kind of version drift that quietly breaks Blender scripts, and it's handled here by probing for whichever socket name exists rather than assuming one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The renders
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8nmhscqxj7ndtqhnb34.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8nmhscqxj7ndtqhnb34.png" alt="Overhead daylight view of the full Atlantis plan — the complete concentric ring layout under a blue sky, mountains all around" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I rendered the scene a few ways by varying the camera and the lighting. The hero above is the sunset pass: a low warm sun raking across the rings, the gold dome catching the light, long soft shadows. It's the most flattering angle — close enough to read the temple and the red-roofed towers, low enough to feel like you're standing on the outer wall.&lt;/p&gt;

&lt;p&gt;The daylight render here is the plan view — a higher, wider three-quarter angle under a plain blue sky that shows the whole concentric layout at once: the temple at dead centre, the three rings of water and land, the wall, and the mountains closing the ring. It's the shot that makes the &lt;em&gt;Critias&lt;/em&gt; geometry obvious.&lt;/p&gt;

&lt;p&gt;The third pass is an "underwater" variant — the same city under a heavy teal-green cast, as if the legend had played out and Atlantis had sunk. I should be honest about what that is: it's a colour-and-lighting grade of the same geometry, not a true underwater scene. There's no volumetric water, no caustics, no floating particulate — just the green tint and flatter light selling the idea. It works as a mood, but it isn't a simulation, and I'd rather say so than overclaim it. There's also a turntable that spins the city for a full rotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;p&gt;This one is mostly a geometry exercise: take a 2,000-year-old text that happens to read like a spec, turn it into a parametric generator, and see the legendary city come out the other end with its rings and bridges and gold-roofed temple in the right places. Because every dimension is a config value, it's less a single render than a little machine for making Atlantises — more rings, a wider canal, a different seed, and you get a different city that still obeys Plato. The clean stylized look is a choice the materials make for me, and the sunset pass is the one I'd hang on the wall.&lt;/p&gt;

&lt;p&gt;This is one of a series of posts on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blender</category>
      <category>3d</category>
      <category>render</category>
      <category>art</category>
    </item>
    <item>
      <title>Atlases: 16 Interactive Learning Guides That Run Code in Your Browser</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Tue, 07 Jul 2026 19:40:22 +0000</pubDate>
      <link>https://dev.to/bsymbolic/atlases-16-interactive-learning-guides-that-run-code-in-your-browser-5650</link>
      <guid>https://dev.to/bsymbolic/atlases-16-interactive-learning-guides-that-run-code-in-your-browser-5650</guid>
      <description>&lt;p&gt;Atlases is a learning site: 16 long-form technical guides, each one a 12-chapter deep dive that you read in a browser tab while running the actual thing in another pane of the same tab. The databases atlas ships a real SQLite engine. The Python atlas runs CPython. The C++ atlas compiles C++. Nothing is a screenshot or an animated GIF pretending to be a terminal — it's the real interpreter, downloaded to your browser and running locally. It's live at &lt;a href="https://atlases.vercel.app/" rel="noopener noreferrer"&gt;atlases.vercel.app&lt;/a&gt;, and I built it with Claude as a pair programmer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;The pitch is "pick a topic, build the intuition." There are 16 atlases: Databases, Networking, Linux, Cryptography, Compilers, Observability, AI/LLM Engineering, FiveM/Lua/QBCore, Encoding &amp;amp; Wire Formats, Python, JavaScript, C++, C, Docker, n8n, and Coolify. That spread is deliberately personal — it's the stack I actually touch, from running a FiveM RP server to wiring up n8n workflows to deploying with Coolify, so the topics are the things I wanted a good reference for and couldn't find in one place.&lt;/p&gt;

&lt;p&gt;Each atlas has the same 12-chapter spine: origin story, toolchain, the bedrock concepts, a working snippet library, a triage/troubleshooting section, and a roadmap of where to go next. That's 192 chapters across the site. What makes it more than a long blog is the interactive parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real in-browser sandboxes.&lt;/strong&gt; SQLite via sql.js, CPython via Pyodide, C and C++ via JSCPP, a JavaScript REPL, a working mini-shell, a Lua interpreter for the FiveM atlas, and a live PromQL playground for observability. All client-side — there is no backend, no code execution server, nothing to attack. Your code never leaves the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curated snippet libraries&lt;/strong&gt; organized by what you're trying to do, not alphabetically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive troubleshooting trees&lt;/strong&gt; that walk you from symptom to diagnosis to fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A quiz at the end of every chapter,&lt;/strong&gt; with explanations rather than just a right/wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack profiles&lt;/strong&gt; — pick your context and the examples adapt as you read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No signup, no ads, no tracking. Progress saves to localStorage, so the site doesn't need to know who you are to remember where you were.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The stack is intentionally boring: Vite + React 18 + Tailwind, with &lt;code&gt;react-router-dom&lt;/code&gt; for routing and &lt;code&gt;lucide-react&lt;/code&gt; for icons. That's almost the whole runtime dependency list. Each atlas is a single self-contained &lt;code&gt;.jsx&lt;/code&gt; file in &lt;code&gt;src/atlases/&lt;/code&gt; — &lt;code&gt;db-atlas.jsx&lt;/code&gt;, &lt;code&gt;python-atlas.jsx&lt;/code&gt;, and so on — lazy-imported and code-split in &lt;code&gt;App.jsx&lt;/code&gt; so you only download the atlas you open. The landing page in &lt;code&gt;src/pages/Landing.jsx&lt;/code&gt; just lists them. Adding a new atlas is a three-step move: drop the file in &lt;code&gt;src/atlases/&lt;/code&gt;, add a lazy route in &lt;code&gt;App.jsx&lt;/code&gt;, add a card on the landing page.&lt;/p&gt;

&lt;p&gt;The content pipeline matches the pattern I use for everything: I decided what each atlas should cover and how it should feel, Claude drafted the chapters, and then every claim got a human fact-check pass before it shipped. The sandbox engines are off-the-shelf WASM/JS interpreters wired into React components, so the heavy lifting of "actually run SQLite in a browser" is sql.js doing its job — my work was the glue and the teaching around it.&lt;/p&gt;

&lt;p&gt;Deployment is the easy part. The repo is on GitHub at &lt;a href="https://github.com/denrod25-del/atlases" rel="noopener noreferrer"&gt;denrod25-del/atlases&lt;/a&gt;, and Vercel auto-deploys every push to &lt;code&gt;main&lt;/code&gt;. A small &lt;code&gt;vercel.json&lt;/code&gt; handles the SPA rewrites so deep links to a specific atlas resolve correctly. &lt;code&gt;npm run build&lt;/code&gt; produces a &lt;code&gt;dist/&lt;/code&gt; folder of static files that would deploy anywhere — Vercel just happens to be the zero-config option.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Facts about a fast-moving industry go stale, and a learning site that's wrong is worse than no learning site.&lt;/strong&gt; Several atlases make time-sensitive claims — the current frontier model lineup, the latest C++ and C standards, the current PostgreSQL and LLVM versions, whether OpenTelemetry's GenAI conventions are stable yet. Those were correct when written and rot quietly. My fix is twofold. First, every fast-moving claim carries an explicit "current as of June 2026"-style stamp right in the text, so a reader can see exactly how fresh the fact is instead of trusting it blindly. Second, refreshing those stamps is now a recurring maintenance task rather than a one-time thing. A single pass already caught real drift: the frontier model lineup needed updating to the current Opus / GPT-5.5 / Gemini lineup, OpenTelemetry's GenAI semantic conventions were still experimental (not stable, as an earlier draft implied), and the FiveM ecosystem's "Overextended" had been renamed to CommunityOx. The lesson: date your claims so the rot is visible, and treat the dates as a checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The site started out written for an audience of one.&lt;/strong&gt; The first versions were personalized to me — examples referenced my Claw World RP server by name, the Linux sandbox had a hardcoded username from my own setup, and asides assumed you were, well, me. Great for a private reference, useless as a public site. Before launch I did a de-personalization pass: pulled the RP-server references, renamed the Linux sandbox's example user (and updated the challenge checkers that validated against that username — easy to miss, would have broken the exercises), and rewrote the personal asides into something a stranger could follow. If you build a tool for yourself and later want to share it, budget real time for stripping out the assumptions you didn't know you'd baked in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep links break on static hosts unless you tell the host about your router.&lt;/strong&gt; Because each atlas is a client-side route, hitting &lt;code&gt;atlases.vercel.app/db&lt;/code&gt; directly — or refreshing on it — asks Vercel for a file that doesn't exist on disk, which is a 404. The fix is the SPA rewrite in &lt;code&gt;vercel.json&lt;/code&gt; that points every unknown path back at &lt;code&gt;index.html&lt;/code&gt; and lets React Router sort it out. It's a one-liner, but it's invisible until someone shares a deep link and it 404s for everyone who clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;Atlases is live and public at &lt;a href="https://atlases.vercel.app/" rel="noopener noreferrer"&gt;atlases.vercel.app&lt;/a&gt;, with the source on GitHub at &lt;a href="https://github.com/denrod25-del/atlases" rel="noopener noreferrer"&gt;denrod25-del/atlases&lt;/a&gt; under an MIT license. Sixteen atlases, 192 chapters, real in-browser sandboxes for SQLite, CPython, C/C++, JavaScript, a shell, Lua, and PromQL, a quiz per chapter, troubleshooting trees, and dated facts you can audit. No signup, no ads, no tracking — just open a topic and start reading.&lt;/p&gt;

&lt;p&gt;This is another entry in the series on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>education</category>
      <category>react</category>
      <category>vercel</category>
      <category>ai</category>
    </item>
    <item>
      <title>Scrapling MCP: Giving Claude Code a Real Web Scraper</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Tue, 07 Jul 2026 19:40:04 +0000</pubDate>
      <link>https://dev.to/bsymbolic/scrapling-mcp-giving-claude-code-a-real-web-scraper-3j04</link>
      <guid>https://dev.to/bsymbolic/scrapling-mcp-giving-claude-code-a-real-web-scraper-3j04</guid>
      <description>&lt;p&gt;Claude Code can read my files and run my shell, but out of the box it can't actually go &lt;em&gt;get&lt;/em&gt; a page from the live web in a way that survives modern anti-bot defenses. A &lt;code&gt;curl&lt;/code&gt; from the Bash tool gets you a 403 from anything behind Cloudflare. So I took an existing open-source scraper — &lt;a href="https://github.com/D4Vinci/Scrapling" rel="noopener noreferrer"&gt;D4Vinci/Scrapling&lt;/a&gt; — installed it locally, and registered its built-in MCP server with Claude Code. Now the agent has ten tools for pulling the real web: plain fetches, headless-browser fetches, stealth fetches that solve Cloudflare, and screenshots. The scraper isn't mine — I want to be clear about that. What I built is the local install and the MCP integration that hands those capabilities to the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Scrapling is an adaptive web-scraping framework. The part I cared about for this is its fetchers: a fast HTTP client built on &lt;code&gt;curl_cffi&lt;/code&gt; that impersonates real browser TLS fingerprints, a Playwright-backed fetcher for pages that need a real browser to render, and a "stealthy" fetcher built on patchright that bypasses anti-bot systems like Cloudflare Turnstile out of the box. It also ships a parser that "learns" a page's structure so your selectors keep working when a site redesigns. That whole engine is the maintainer's work, not mine.&lt;/p&gt;

&lt;p&gt;What turns that library into something Claude Code can use is the MCP server Scrapling already bundles. MCP — Model Context Protocol — is the standard way an agent gets extra tools. Register an MCP server and its tools show up in the agent's toolbox alongside the built-in ones. Scrapling's server exposes ten:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get&lt;/code&gt; and &lt;code&gt;bulk_get&lt;/code&gt; — fast HTTP fetches via &lt;code&gt;curl_cffi&lt;/code&gt;, single or batched&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;bulk_fetch&lt;/code&gt; — full headless-browser fetches via Playwright, for JS-rendered pages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stealthy_fetch&lt;/code&gt; and &lt;code&gt;bulk_stealthy_fetch&lt;/code&gt; — patchright stealth fetches that get past Cloudflare&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;open_session&lt;/code&gt;, &lt;code&gt;close_session&lt;/code&gt;, &lt;code&gt;list_sessions&lt;/code&gt; — persistent browser sessions across calls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;screenshot&lt;/code&gt; — returns an actual image block of the rendered page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design is deliberately token-conscious, which matters when the consumer is an LLM and every byte of the response eats context. The tools default to returning markdown with &lt;code&gt;main_content_only=True&lt;/code&gt;, which strips the page chrome down to the article body (and sanitizes hidden elements, which doubles as a guard against prompt injection hiding in the DOM). You can pass a &lt;code&gt;css_selector&lt;/code&gt; to narrow the result before it ever reaches the model — in my testing that cut the payload by roughly 71% on a content-heavy page.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;There wasn't much &lt;em&gt;code&lt;/em&gt; to write here — the value was in the install and the wiring, which on Windows had a few sharp edges.&lt;/p&gt;

&lt;p&gt;I cloned the repo to &lt;code&gt;claude/Scrapling/&lt;/code&gt; and installed &lt;code&gt;scrapling[ai]&lt;/code&gt; into its own virtualenv at &lt;code&gt;claude/Scrapling/.venv&lt;/code&gt;, on Python 3.14. That last part was a small gamble — 3.14 is new enough that I half-expected a dependency to have no wheel — but every package resolved, including &lt;code&gt;curl_cffi&lt;/code&gt;, which shipped an abi3 wheel that works across Python versions. Then &lt;code&gt;scrapling install&lt;/code&gt; pulled down the browser binaries: Playwright's Chromium and a headless shell into &lt;code&gt;~/AppData/Local/ms-playwright&lt;/code&gt;, with patchright reusing the same cache. That's roughly 700 MB of browser per Chromium build (more on disk once Playwright and patchright pull separate versions), which is the real cost of "can render and stealth real pages."&lt;/p&gt;

&lt;p&gt;The integration itself is one block of JSON. MCP servers in Claude Code live in &lt;code&gt;~/.claude.json&lt;/code&gt;, and I added &lt;code&gt;scrapling&lt;/code&gt; to the global &lt;code&gt;mcpServers&lt;/code&gt; block — the same place the blender, unity, and pixellab servers already lived. It's a stdio server, so the config is just a command and args: the command points straight at the venv's &lt;code&gt;scrapling.exe&lt;/code&gt; (&lt;code&gt;C:\Users\skyea\claude\Scrapling\.venv\Scripts\scrapling.exe&lt;/code&gt;) with args &lt;code&gt;["mcp"]&lt;/code&gt;. Registering it globally rather than per-project means the scraper is available in every Claude Code session, not just one repo — which is what I wanted, since "go grab this page" is a need that doesn't belong to any single project.&lt;/p&gt;

&lt;p&gt;The one non-obvious step: MCP servers are loaded when a session starts, so adding the block does nothing until you restart Claude Code (or re-run &lt;code&gt;/mcp&lt;/code&gt;). After the restart it connected, and I ran every tool against &lt;code&gt;quotes.toscrape.com&lt;/code&gt; to confirm — &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt; returned content, &lt;code&gt;stealthy_fetch&lt;/code&gt; came up clean, &lt;code&gt;screenshot&lt;/code&gt; returned a real image block, and the session tools opened and closed a browser session as expected. All ten verified live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Three real ones, each of which would have cost time if I'd skipped it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editing &lt;code&gt;~/.claude.json&lt;/code&gt; by hand is editing the file that runs Claude Code, so back it up first.&lt;/strong&gt; That file holds the entire harness config — every MCP server, project history, the lot. A stray comma turns it into invalid JSON and the next launch is a bad time. Before touching it I copied it to &lt;code&gt;~/.claude.json.bak-scrapling&lt;/code&gt;, then made the edit. If the edit had broken anything, recovery was a one-line file restore instead of a debugging session. When the config file &lt;em&gt;is&lt;/em&gt; the thing you depend on to debug, you don't get to debug your way out of corrupting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server won't appear until you restart — adding the JSON isn't enough.&lt;/strong&gt; MCP servers are read at session startup, so right after saving the config the &lt;code&gt;scrapling&lt;/code&gt; tools simply weren't there, which looks exactly like a broken registration. The fix is just to restart Claude Code (or run &lt;code&gt;/mcp&lt;/code&gt;), but knowing that up front saves you from "re-checking" a config that was already correct. I lost a minute to this before remembering it's load-at-startup, not hot-reload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python 3.14 in an isolated venv was a calculated risk that happened to pay off.&lt;/strong&gt; Installing into a brand-new interpreter version is where dependency wheels go to die — a single package without a 3.14 build would have meant compiling from source on Windows, which is its own afternoon. It worked because the dependency tree had 3.14 wheels (and &lt;code&gt;curl_cffi&lt;/code&gt;'s abi3 wheel sidestepped the version question entirely). The thing that made the gamble safe wasn't luck, though — it was the dedicated venv. Because Scrapling lives in &lt;code&gt;claude/Scrapling/.venv&lt;/code&gt; and nothing else, a failed or weird install couldn't contaminate any other project's Python, and the MCP command points at that exact interpreter, so there's no ambiguity about which environment the agent is launching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does now
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;scrapling&lt;/code&gt; MCP server is registered globally and connected, and all ten tools are live-verified: &lt;code&gt;get&lt;/code&gt; / &lt;code&gt;bulk_get&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt; / &lt;code&gt;bulk_fetch&lt;/code&gt;, &lt;code&gt;stealthy_fetch&lt;/code&gt; / &lt;code&gt;bulk_stealthy_fetch&lt;/code&gt;, the three session tools, and &lt;code&gt;screenshot&lt;/code&gt;. In practice that means inside any Claude Code session I can ask the agent to pull a live page and it actually can — fast HTTP for simple sites, a real headless browser for JS-heavy ones, stealth mode for the ones hiding behind Cloudflare, and a screenshot when I want to &lt;em&gt;see&lt;/em&gt; the page rather than read it. The same capabilities are reachable from the shell too, via Scrapling's own CLI (&lt;code&gt;scrapling extract get|fetch|stealthy_fetch URL out.md&lt;/code&gt;), which is handy when I'm not in an agent loop.&lt;/p&gt;

&lt;p&gt;To be clear about the division of credit one more time: the scraper, the stealth, the adaptive parser, and the MCP server are all D4Vinci's Scrapling. My work was the local install on Windows/Python 3.14, the global stdio registration in &lt;code&gt;~/.claude.json&lt;/code&gt;, the backup-and-restart discipline around it, and verifying that all ten tools actually work end to end. The result is that the agent I already use every day can now reach out and read the modern web instead of bouncing off it.&lt;/p&gt;

&lt;p&gt;This is one post in a series on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>python</category>
      <category>webscraping</category>
      <category>claude</category>
    </item>
    <item>
      <title>Rebuilding Space Invaders in Unity, One Verified Milestone at a Time</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:49:33 +0000</pubDate>
      <link>https://dev.to/bsymbolic/rebuilding-space-invaders-in-unity-one-verified-milestone-at-a-time-1f</link>
      <guid>https://dev.to/bsymbolic/rebuilding-space-invaders-in-unity-one-verified-milestone-at-a-time-1f</guid>
      <description>&lt;p&gt;Space Invaders is the one almost everyone can picture: a grid of aliens marching side to side, dropping a row when they hit the wall, and speeding up to a heart-attack tempo as you thin them out. Tomohiro Nishikado built that for Taito in 1978, and the acceleration was famously a happy accident — the hardware just ran faster with fewer sprites to draw. I wanted to rebuild the whole thing in Unity, faithfully, and decided on purpose rather than by accident. I built it with Claude as a pair programmer, driving Unity live through the Unity MCP bridge, and shipped v1 with all nine milestones verified in Play mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;This is a faithful recreation of the classic arcade loop, not a reimagining. A 5×11 grid of invaders marches in lockstep, drops and reverses at the screen edges, and accelerates as its numbers fall. You get one cannon, three lives, and the original one-bullet-on-screen rule — you can't spam shots, so every trigger pull is a small decision (the Rapid Fire and Twin Shot power-up capsules can lift that limit for a while, but the default is one shot at a time). Four bunkers sit between you and the swarm and erode from both sides as bullets and bombs chew through them. A mystery UFO drifts across the top on a long timer for bonus points, and clearing the grid starts a new, faster wave that begins one row lower. Lose all your lives, or let the swarm reach your row, and it's game over.&lt;/p&gt;

&lt;p&gt;Around that core I allowed only modern game-feel polish — no new mechanics. There's a title screen and a game-over flow, a high score that persists to disk between sessions, screen shake, particle bursts on every kill, and a synthesized four-note marching bass that speeds up in lockstep with the swarm. The whole thing runs in Unity 6 (2D URP) and lives entirely under &lt;code&gt;Assets/_Invaders/&lt;/code&gt; with its own assembly definition and &lt;code&gt;Invaders&lt;/code&gt; namespace, because it shares a Unity project with a completely separate game (a metroidvania called Pipe Knight) and the two had to never touch each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The division of labor was the same one I've settled into: I decided what the game should be and verified each build by playing it; Claude wrote essentially all of the C#. We started with a brainstorming pass that became a design spec, then a plan that broke v1 into nine milestones, M0 through M8 — scaffolding, the swarm math, the game manager and UI, the player, the alien swarm, bunkers, the UFO, juice and audio, and a final flow-polish pass. Each milestone was implemented, then verified live in Play mode before moving on. The project grew to 26 scripts across that run plus several enhancement passes.&lt;/p&gt;

&lt;p&gt;The piece I care most about is the swarm. The trickiest, most failure-prone logic — the acceleration curve and the edge-turn decision — got pulled out into a pure, dependency-free &lt;code&gt;SwarmMath&lt;/code&gt; class so it could be unit-tested in EditMode with no Unity runtime at all. The speed-up is a single readable line: the step interval lerps from a slow &lt;code&gt;maxInterval&lt;/code&gt; when all aliens are alive down to a fast &lt;code&gt;minInterval&lt;/code&gt; when one remains, so fewer invaders literally means faster steps. That's the 1978 acceleration made deliberate. &lt;code&gt;ResolveEdge&lt;/code&gt; handles the wall: it looks one step ahead, and if the leading alien would cross a limit, it signals a drop and flips the direction for the whole grid. The marching bass cadence is driven off the same step timer, so the music tightens up exactly in time with the swarm.&lt;/p&gt;

&lt;p&gt;The second decision that mattered was making projectiles &lt;strong&gt;raycast&lt;/strong&gt; rather than rely on collider overlaps. A player bullet travels at 18 units per second, and the bunker chunks are only 0.25 units thick. At that ratio, a fast-moving trigger collider can leap clean over a thin chunk between two physics frames and never register a hit. So &lt;code&gt;PlayerBullet&lt;/code&gt; and &lt;code&gt;AlienBomb&lt;/code&gt; don't translate-then-collide; each frame they cast a ray along the exact distance they're about to move, hit the first thing on the Alien or Bunker layer, and resolve through a small &lt;code&gt;IHittable&lt;/code&gt; interface. No tunnelling, and it made alien hits robust as a bonus.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Three that cost real debugging time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unity freezes the game loop when its window isn't focused, which silently invalidates every runtime test.&lt;/strong&gt; Driving Unity through MCP, I'd start Play mode, alt-tab to issue commands, and read back results — except &lt;code&gt;frameCount&lt;/code&gt; had stopped advancing the moment Unity lost focus, so the swarm wasn't actually marching and every "verification" was a snapshot of a frozen frame. The fix is one line at the start of each play session: set &lt;code&gt;Application.runInBackground = true&lt;/code&gt;. Without it, you're testing a paused game and don't know it. A related trap: script recompiles are deferred until you exit Play mode, so editing a script mid-session does nothing until you stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The swarm refused to spawn because of subscription timing.&lt;/strong&gt; &lt;code&gt;AlienSwarm&lt;/code&gt; listened for the game's Playing state in &lt;code&gt;OnEnable&lt;/code&gt;, but that ran before &lt;code&gt;GameManager.Awake&lt;/code&gt; had assigned its singleton, so the subscription either missed the event or hit a null. Moving it to &lt;code&gt;Start&lt;/code&gt; helped, but the real fix was architectural: I handed all (re)start responsibility to a single &lt;code&gt;LevelReset&lt;/code&gt; object that owns the clean slate — it clears projectiles, rebuilds the bunkers, recenters the cannon, and spawns the wave. The swarm no longer spawns itself, which also killed a double-spawn bug on restart. One object owning "what a fresh round looks like" beat sprinkling that logic across every component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The HUD piled up in the center of the screen.&lt;/strong&gt; All the score, high-score, and lives text stacked on top of each other in the middle instead of sitting in the corners. The cause was a zero-size RectTransform on the HUD parent: a center-anchored rect with no width or height, so every child landed at the same point. Stretching that parent to fill the canvas fixed it. The same family of bug bit again later — when I added a letterbox camera to frame the portrait-ish playfield on my very wide monitor, the HUD ended up out in the black side bars, because it was a Screen-Space-Overlay canvas spanning the whole window. Switching it to Screen-Space-Camera pinned the UI inside the pillarboxed game region. Canvas render mode is one of those settings that's invisible until it's suddenly very visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;v1 is complete and verified: title, play, game-over, and restart all work; the swarm marches, drops, reverses, and visibly accelerates as you thin it; the cannon fires one bullet at a time; aliens drop bombs; bunkers erode from both sides; the UFO awards its bonus; and the high score survived a full Unity restart (it loaded 1500 from disk on relaunch). On top of that I ran several enhancement passes — a player death sequence with hitstop, lives drawn as cannon icons, floating score popups, a CRT scanline overlay, pause and mute, on-screen touch controls so it's playable on an iPad, a top-five leaderboard with name entry, and weapon-drop power-ups (Rapid Fire and Twin Shot capsules that fall from killed aliens). Later I swapped the generated placeholder art for free Creative Commons sprites and audio from the open-source pivaders project, with attribution, so the final build reads like a proper space shooter rather than colored blocks.&lt;/p&gt;

&lt;p&gt;The hero shot above is that final build — full swarm, eroding bunkers, the planet-and-stars background, and the touch buttons along the bottom. The credit for the design goes to Nishikado and the 1978 original; the work here is the faithful modern recreation of it, built one verified milestone at a time. This is part of an ongoing series on projects built this way — the running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>csharp</category>
      <category>arcade</category>
    </item>
    <item>
      <title>MoneyPrinterTurbo, Locally: An AI Short-Video Generator on My Own Hardware</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:49:16 +0000</pubDate>
      <link>https://dev.to/bsymbolic/moneyprinterturbo-locally-an-ai-short-video-generator-on-my-own-hardware-24f9</link>
      <guid>https://dev.to/bsymbolic/moneyprinterturbo-locally-an-ai-short-video-generator-on-my-own-hardware-24f9</guid>
      <description>&lt;p&gt;I wanted to see how far an AI short-video pipeline could run on my own machine: type in a topic, get a finished vertical MP4 out the other end, with no cloud LLM bill and no proprietary API keys. The answer is "all the way." I took the open-source &lt;a href="https://github.com/harry0703/MoneyPrinterTurbo" rel="noopener noreferrer"&gt;MoneyPrinterTurbo&lt;/a&gt; project, got it running locally on Windows with Claude as a pair, swapped its language model over to a local Ollama instance, and generated a real 1080x1920, 27-second clip about the benefits of drinking water — script, voiceover, subtitles, stock footage, and all.&lt;/p&gt;

&lt;p&gt;The project itself is &lt;code&gt;harry0703/MoneyPrinterTurbo&lt;/code&gt;. None of the pipeline is mine. What's mine is getting it to run end-to-end on local hardware and working through the handful of things that broke along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;MoneyPrinterTurbo is a one-click AI short-video generator. You give it a subject or a keyword and it produces a finished short: it writes a script with an LLM, derives search terms from that script, pulls matching stock footage, synthesizes a voiceover, generates timed subtitles, and stitches the whole thing together with ffmpeg into a 1080p vertical MP4.&lt;/p&gt;

&lt;p&gt;Concretely, the pipeline that ran on my machine looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Script&lt;/strong&gt; — the LLM takes the topic ("The benefits of drinking water") and writes a short narration plus a list of visual search terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Footage&lt;/strong&gt; — those search terms hit a stock-video source (I used Pexels) and the matching clips get downloaded into a local cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voiceover&lt;/strong&gt; — the narration is read aloud by a TTS engine (edge-tts, the free Microsoft Edge voices).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subtitles&lt;/strong&gt; — the audio is turned into a timed &lt;code&gt;.srt&lt;/code&gt; that's burned onto the video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt; — ffmpeg concatenates the footage, lays the voiceover and optional background music underneath, overlays the subtitles, and outputs the final MP4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's a Python MVC app. There are two ways in: a Streamlit WebUI on port 8501, and a FastAPI service on port 8080 (with Swagger docs at &lt;code&gt;/docs&lt;/code&gt;). I drove it almost entirely through the WebUI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Getting it running locally was the whole exercise, so the "build" is really a setup-and-port story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The environment.&lt;/strong&gt; My default Python is 3.14, which the project rejects — it wants &lt;code&gt;&amp;gt;=3.11,&amp;lt;3.13&lt;/code&gt;. &lt;code&gt;uv sync&lt;/code&gt; solved that cleanly by provisioning its own Python 3.11.15 into a project-local &lt;code&gt;.venv&lt;/code&gt;. ffmpeg was already on my PATH from a winget install. From there, the WebUI launches via &lt;code&gt;.\webui.bat&lt;/code&gt;, which auto-selects the &lt;code&gt;.venv&lt;/code&gt; Python and finds a free port in the 8501–8599 range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The language model.&lt;/strong&gt; This is the part I most wanted to keep local. The project supports a long list of LLM providers, but I didn't want to pay for any of them or hand over keys. So I pointed it at a native Windows &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; instance running &lt;code&gt;qwen2.5:7b&lt;/code&gt; on &lt;code&gt;http://127.0.0.1:11434/v1&lt;/code&gt; — free, unlimited, and entirely on my own RTX 3070. Script generation on the 7B model took roughly three and a half minutes per run. That's slow compared to a hosted API, but it's free and it never rate-limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The footage.&lt;/strong&gt; Stock video is the one piece that genuinely needs an external service, since you can't synthesize generic B-roll locally. I used Pexels with a free API key. Everything else in the chain — the voice (edge-tts), the subtitles (edge), the render (ffmpeg) — is free and needs no key.&lt;/p&gt;

&lt;p&gt;So the final working configuration is: local Ollama for the script, Pexels for the footage, edge-tts for the voice, edge for subtitles, ffmpeg for the render. The only thing leaving my machine is the footage search.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Three things cost real debugging time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Pollinations free tier is effectively unusable, so I switched to local Ollama.&lt;/strong&gt; The config template's default pointed at Pollinations as a free LLM (my own &lt;code&gt;config.toml&lt;/code&gt; has since been corrected to local Ollama, but a fresh clone still starts from the template default), and that default is broken two ways. The shipped &lt;code&gt;pollinations_base_url&lt;/code&gt; (&lt;code&gt;https://pollinations.ai/api/v1&lt;/code&gt;) returns a 405, and even after fixing it to the correct endpoint the code already knows about (&lt;code&gt;https://text.pollinations.ai/openai&lt;/code&gt;), the anonymous free tier just returns &lt;code&gt;429 Too Many Requests&lt;/code&gt; on essentially every call. There's no usable free LLM hiding in there. The fix was to stop fighting it and run the model myself: local Ollama with &lt;code&gt;qwen2.5:7b&lt;/code&gt;. Slower per request, but it actually completes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The WebUI rewrites &lt;code&gt;config.toml&lt;/code&gt; from memory, so you edit the disk file &lt;em&gt;then&lt;/em&gt; restart.&lt;/strong&gt; This one is a genuine trap. The running Streamlit app holds its startup config in memory and rewrites &lt;code&gt;config.toml&lt;/code&gt; on every rerun, reverting any hand-edits you make to the file while it's running. I'd change the LLM provider on disk, the app would quietly stomp it back, and I'd wonder why my change had no effect. The fix is the right order of operations: stop the WebUI, edit &lt;code&gt;config.toml&lt;/code&gt; on disk, &lt;em&gt;then&lt;/em&gt; restart &lt;code&gt;webui.bat&lt;/code&gt; so it reads the new values into memory on startup. (The rewrite itself isn't corruption — it's the app persisting UI defaults. It just fights you if you edit out of band.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streamlit's first-run email prompt blocks a headless launch.&lt;/strong&gt; On its first run, Streamlit prints an interactive "enter your email" prompt, which hangs &lt;code&gt;webui.bat&lt;/code&gt; when there's no one to answer it. The fix is to pre-create &lt;code&gt;~/.streamlit/credentials.toml&lt;/code&gt; with an empty email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[general]&lt;/span&gt;
&lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that file in place, the prompt is satisfied and the WebUI boots straight through.&lt;/p&gt;

&lt;p&gt;(One more cosmetic one: during the render the Windows console spews &lt;code&gt;UnicodeEncodeError: charmap can't encode ①&lt;/code&gt; lines. That's just loguru failing to print circled-number glyphs to the cp1252 console — the video renders fine regardless.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;It works end-to-end, locally. The verification run produced a real &lt;code&gt;final-1.mp4&lt;/code&gt;: 1080x1920, H.264 video with AAC audio, 27.0 seconds long, on the topic "The benefits of drinking water." The script was written by the local &lt;code&gt;qwen2.5:7b&lt;/code&gt; model, the voice is edge-tts (Jenny), the subtitles are a generated &lt;code&gt;.srt&lt;/code&gt;, and the footage came from Pexels — assembled by ffmpeg into a finished vertical short. Nothing in that chain cost a cent or required a proprietary LLM key.&lt;/p&gt;

&lt;p&gt;All credit for the generator goes to the upstream &lt;a href="https://github.com/harry0703/MoneyPrinterTurbo" rel="noopener noreferrer"&gt;MoneyPrinterTurbo&lt;/a&gt; project. My contribution was narrow but specific: getting it to run entirely on local hardware on Windows, replacing the LLM with a local Ollama model, and documenting the fixes — the Pollinations dead end, the config-rewrite ordering, and the Streamlit email prompt — so the next person can skip the part where I lost an afternoon.&lt;/p&gt;

</description>
      <category>aivideo</category>
      <category>ollama</category>
      <category>python</category>
      <category>tts</category>
    </item>
    <item>
      <title>NemoClaw on WSL: Running NVIDIA's Sandboxed Agent Stack Fully Local</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:42:25 +0000</pubDate>
      <link>https://dev.to/bsymbolic/nemoclaw-on-wsl-running-nvidias-sandboxed-agent-stack-fully-local-100e</link>
      <guid>https://dev.to/bsymbolic/nemoclaw-on-wsl-running-nvidias-sandboxed-agent-stack-fully-local-100e</guid>
      <description>&lt;p&gt;NVIDIA's &lt;a href="https://github.com/NVIDIA/NemoClaw" rel="noopener noreferrer"&gt;NemoClaw&lt;/a&gt; is a reference stack for running always-on AI agents inside hardened sandboxes. The examples all point at remote GPU instances and cloud inference. I wanted the opposite: the whole thing on my own desk — WSL, my RTX 3070, a local Ollama model, no cloud and no API keys anywhere in the loop. That took some doing, and the last mile came down to a self-signed certificate and a context-window budget. It now works end to end: I can chat with a sandboxed OpenClaw agent through the dashboard at &lt;code&gt;localhost:18789&lt;/code&gt;, and every token of inference is served by a model running on my own GPU.&lt;/p&gt;

&lt;p&gt;To be clear up front about credit: NemoClaw is NVIDIA's stack, Apache-2.0 licensed. The sandbox, the blueprint, the routed inference, the network policy — all theirs. What I did was get it running fully local on WSL and work through the two things that stood between "installed" and "actually answering."&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;NemoClaw runs an agent — &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; by default — inside an &lt;a href="https://github.com/NVIDIA/OpenShell" rel="noopener noreferrer"&gt;NVIDIA OpenShell&lt;/a&gt; sandbox. OpenShell is the container layer that gives the agent a locked-down environment: an L7 egress proxy, a network policy that controls what it can reach, capability drops, and a routed inference path so the agent talks to a model through a managed hostname rather than punching straight out to the internet. NemoClaw wraps all of that in a single CLI that handles onboarding, lifecycle, and the blueprint that defines the sandbox.&lt;/p&gt;

&lt;p&gt;The intended deployment is a remote GPU box with a hosted model behind it. My goal was to collapse that to one machine. Inference shouldn't leave the box; the model should be something I run myself; and the sandbox's safety properties should stay intact while I do it. On a Windows machine with an 8 GB GPU, that meant WSL2 for the Linux container host, Docker Desktop's WSL integration for the runtime, GPU passthrough so the sandbox can see the card, and &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; on the host serving the model that OpenShell's routed inference points at.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The host is WSL2 running Ubuntu 24.04, with Docker Desktop's WSL integration enabled for that distro. GPU passthrough works through the WSL2 CUDA path, so the sandbox sees the RTX 3070 — when it comes up it reports the card detected and the GPU policy preset active.&lt;/p&gt;

&lt;p&gt;Installation is a script NVIDIA hosts. One wrinkle I hit: the piped &lt;code&gt;curl … | bash&lt;/code&gt; form quietly no-ops when it ends up in a detached process, so I downloaded the installer to &lt;code&gt;/tmp&lt;/code&gt; and ran it directly with the non-interactive and third-party-acceptance flags. The installer brings up Node 22 through nvm inside WSL, which sidesteps any Windows Node leaking onto the PATH. Provider choice doesn't happen during the install stage — you run &lt;code&gt;nemoclaw onboard&lt;/code&gt; separately with &lt;code&gt;NEMOCLAW_PROVIDER=ollama&lt;/code&gt; to point it at the local server.&lt;/p&gt;

&lt;p&gt;The model is the interesting constraint. Stock Ollama loaded &lt;code&gt;qwen2.5:7b&lt;/code&gt; at a 4096-token context, and the sandboxed OpenClaw agent needs roughly 7k just for its system prompt and tool definitions before it does anything useful — so a fresh session couldn't even start. The &lt;code&gt;OLLAMA_CONTEXT_LENGTH&lt;/code&gt; environment variable wasn't honored by the Ollama version I had, so I built a derived model from a Modelfile with a &lt;code&gt;PARAMETER num_ctx&lt;/code&gt; override. My first attempt aimed low to save VRAM: a derived model at &lt;code&gt;num_ctx 24576&lt;/code&gt;, re-onboarded with &lt;code&gt;NEMOCLAW_CONTEXT_WINDOW=24576&lt;/code&gt; so the sandbox's config agreed with it. That failed — the agent's own reserve left too small a usable budget, and a fresh session died on a preflight-compaction error before the first message (more on that below). So I bumped both sides to qwen2.5's maximum: a derived model — &lt;code&gt;qwen2.5-claw:7b&lt;/code&gt; — at &lt;code&gt;PARAMETER num_ctx 32768&lt;/code&gt;, re-onboarded with &lt;code&gt;NEMOCLAW_CONTEXT_WINDOW=32768&lt;/code&gt;. qwen2.5 uses grouped-query attention, which keeps the KV cache small enough that a 32k window still fits in 8 GB of VRAM (it sits around 5.5 GB). NemoClaw's start script supports that exact override — it's a documented env var in the entrypoint.&lt;/p&gt;

&lt;p&gt;What you end up with is a sandbox (&lt;code&gt;my-claw&lt;/code&gt; in my setup) in the Ready phase, running OpenClaw with its full tool set and the RTX 3070 passed through, with a dashboard forwarded to &lt;code&gt;localhost:18789&lt;/code&gt; on the Windows side via WSL2's localhost forwarding. If the gateway forward ever drops, &lt;code&gt;nemoclaw my-claw recover&lt;/code&gt; brings it back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Two real ones stood between "the sandbox is up" and "the agent actually replies." Both took real digging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The self-signed &lt;code&gt;inference.local&lt;/code&gt; certificate broke every agent turn.&lt;/strong&gt; This was the big one. OpenShell signs its managed inference route — &lt;code&gt;https://inference.local&lt;/code&gt;, the hostname the agent's traffic is routed through — with its own "OpenShell Sandbox CA." A plain &lt;code&gt;curl&lt;/code&gt; or a standalone Node process validates that fine, because they honor &lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt; and the CA bundle is mounted in the sandbox. But OpenClaw's provider transport uses a custom undici dispatcher that does &lt;em&gt;not&lt;/em&gt; honor &lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt;, so every agent turn died with &lt;code&gt;SELF_SIGNED_CERT_IN_CHAIN&lt;/code&gt;. That dispatcher does respect the global &lt;code&gt;rejectUnauthorized&lt;/code&gt; flag, so the fix is &lt;code&gt;NODE_TLS_REJECT_UNAUTHORIZED=0&lt;/code&gt;. The catch: NemoClaw's environment allowlist passes &lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt; through to the gateway but filters &lt;code&gt;NODE_TLS_REJECT_UNAUTHORIZED&lt;/code&gt; out, so setting it from outside does nothing — it has to be exported inside the container's &lt;code&gt;nemoclaw-start&lt;/code&gt; entrypoint, before that filtering happens. So that's where it goes, gated behind a &lt;code&gt;NEMOCLAW_TRUST_INFERENCE_TLS&lt;/code&gt; flag that defaults to &lt;code&gt;1&lt;/code&gt; — so it's on by default (opt-out), and you set &lt;code&gt;NEMOCLAW_TRUST_INFERENCE_TLS=0&lt;/code&gt; to turn it off. This is acceptable precisely because of the sandbox's design: all the gateway's egress already runs through the OpenShell L7 proxy and the network policy, so the agent can't reach anything the policy doesn't allow regardless of TLS verification. The proper upstream fix is OpenClaw's dispatcher learning to trust &lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt;; until then, the entrypoint export is the local workaround.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The context window had to clear the agent's reserve, not just fit the model.&lt;/strong&gt; This was the failure behind my first context attempt. With both the model and the sandbox set to a 24576 window, a fresh session died with "Preflight compaction required but failed." OpenClaw reserves a large slice of the context window for itself — at 24576, the reserve left only about 8000 tokens for a system-plus-tools prompt that was itself around 8090 tokens, so a brand-new conversation was already over budget before the first message. The fix was to stop being clever about fitting and go straight to qwen2.5's maximum: bump both the model's &lt;code&gt;num_ctx&lt;/code&gt; and the sandbox's &lt;code&gt;contextWindow&lt;/code&gt; to 32768. The rough rule I took away is that the usable budget is roughly half the configured window, so size the window for double what the agent's system prompt and tools actually cost.&lt;/p&gt;

&lt;p&gt;One persistence note worth recording: the TLS export survives a container restart because it lives in the entrypoint, and the model's &lt;code&gt;num_ctx&lt;/code&gt; survives because it's baked into the host Ollama model — but a full &lt;code&gt;nemoclaw rebuild&lt;/code&gt; or re-onboard reverts the config side, so the entrypoint line and the &lt;code&gt;NEMOCLAW_CONTEXT_WINDOW=32768&lt;/code&gt; onboard both have to be re-applied after a rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What works now
&lt;/h2&gt;

&lt;p&gt;The sandbox is up, the agent answers, and the inference is entirely local. I confirmed the dashboard chat end to end with a headless browser driving the real UI at &lt;code&gt;localhost:18789&lt;/code&gt; — sent a prompt, the sandboxed OpenClaw agent replied through the browser, with the completion served by &lt;code&gt;qwen2.5-claw:7b&lt;/code&gt; on my own GPU. No cloud endpoint, no API key, nothing leaving the machine for inference. The agent's full tool set is available and the sandbox's network policy and proxy are doing their job around it.&lt;/p&gt;

&lt;p&gt;One honest caveat: the CLI &lt;code&gt;openclaw agent&lt;/code&gt; path still falls back to an embedded mode rather than connecting to the gateway over its websocket, so the browser dashboard is the way in that actually works. For a local single-user setup that's fine — the dashboard is where I'd be anyway.&lt;/p&gt;

&lt;p&gt;The credit split stays the same as where I started: NemoClaw, OpenShell, and OpenClaw are NVIDIA's and the OpenClaw maintainers' work. Mine was getting the whole stack running on one Windows machine through WSL — local Ollama, GPU passthrough, the right context budget — and tracking the self-signed-cert failure down to the one entrypoint line that fixes it. The payoff is a hardened, always-on agent sandbox that runs start to finish on hardware I own.&lt;/p&gt;

&lt;p&gt;This is one post in a series on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wsl</category>
      <category>ollama</category>
      <category>localllm</category>
      <category>agents</category>
    </item>
    <item>
      <title>PDF to Podcast, Locally: Running NVIDIA's Blueprint Without Their Keys</title>
      <dc:creator>C. Wheatley</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:42:08 +0000</pubDate>
      <link>https://dev.to/bsymbolic/pdf-to-podcast-locally-running-nvidias-blueprint-without-their-keys-434b</link>
      <guid>https://dev.to/bsymbolic/pdf-to-podcast-locally-running-nvidias-blueprint-without-their-keys-434b</guid>
      <description>&lt;p&gt;NVIDIA shipped a neat blueprint called &lt;strong&gt;pdf-to-podcast&lt;/strong&gt;: feed it a PDF and it generates a two-host podcast — a real audio file of two voices discussing the document, complete with a planned outline and back-and-forth dialogue. The catch is that out of the box it wants NVIDIA NIM endpoints for the language model and ElevenLabs for the voices, both of which mean API keys and per-call billing. I wanted to know whether the pipeline itself was good, and then whether I could run the whole thing on my own machine with no NVIDIA or ElevenLabs accounts at all. So I cloned it and built a local-stack overlay. Credit where it's due: the pipeline, the agent flow, the whole "outline then dialogue then audio" shape is NVIDIA's work. What I did was swap the cloud pieces out and fix the things that broke when I did. I built the overlay with Claude as a pair programmer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;The original is a multi-service blueprint. A document goes in, a Docling-based service extracts the text, an agent service summarizes it and plans an outline, then turns that outline into a two-speaker conversation, and a TTS service renders the conversation to audio. The services talk over Redis and store artifacts in MinIO. You POST a PDF, poll a status endpoint, and eventually pull a finished audio file. It's a genuinely good pipeline — the part I wanted to keep.&lt;/p&gt;

&lt;p&gt;My goal was narrow: run that exact pipeline with zero NVIDIA and zero ElevenLabs dependencies. The overlay is seven files. The language-model layer gets a &lt;code&gt;provider&lt;/code&gt; field so each role can route to Anthropic, OpenAI, Ollama, or the original NVIDIA NIM, and &lt;code&gt;models.claude.json&lt;/code&gt; wires the three roles the blueprint already splits work across — &lt;code&gt;reasoning&lt;/code&gt;, &lt;code&gt;iteration&lt;/code&gt;, and &lt;code&gt;json&lt;/code&gt; — to tiered Claude models. The TTS service gets its ElevenLabs calls replaced with &lt;a href="https://github.com/hexgrad/kokoro" rel="noopener noreferrer"&gt;Kokoro&lt;/a&gt;, an open TTS model that runs locally and renders 24 kHz mono audio. The result is the same blueprint, the same API, but every external call now hits either the Anthropic API or a model running on my own machine. With the right &lt;code&gt;.env&lt;/code&gt;, a POST of a PDF comes back as a playable WAV and nothing went to NVIDIA or ElevenLabs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The script side uses &lt;strong&gt;tiered Claude&lt;/strong&gt;, and the tiering isn't something I invented — the blueprint already separates its LLM calls into three roles, presumably because the authors knew not every step needs the same horsepower. The overlay just maps those roles to Claude models: Sonnet for the heavy reasoning (summarizing the document, planning the outline), Haiku for the cheap iterative folding steps, and Sonnet again — at a larger token budget — for the final structured-JSON step that emits the actual dialogue. That last assignment was a lesson, not a default; more on that below.&lt;/p&gt;

&lt;p&gt;The voice side is &lt;strong&gt;Kokoro&lt;/strong&gt;. The TTS service loads one Kokoro pipeline once, maps two speaker slots to two Kokoro voices (the defaults are &lt;code&gt;af_heart&lt;/code&gt; and &lt;code&gt;am_michael&lt;/code&gt;), and renders each dialogue turn. The two voices alternate so the output sounds like a conversation, not a monologue. The audio is written out as a single 16-bit PCM WAV through &lt;code&gt;soundfile&lt;/code&gt;, which matters because of a bug I'll get to.&lt;/p&gt;

&lt;p&gt;For scanned PDFs there's an &lt;strong&gt;easyocr&lt;/strong&gt; path. Plenty of the documents I actually wanted to feed it are image-only scans with no text layer — the extractor would otherwise grab nothing useful (or worse, grab a watermark). The OCR workflow renders the target pages to images, runs easyocr over them, rebuilds a clean text-layer PDF, and feeds &lt;em&gt;that&lt;/em&gt; into the pipeline. It's slow on CPU and it reads code and symbols poorly, but for prose it produces text clean enough that the summarizer is happy.&lt;/p&gt;

&lt;p&gt;The division of labor was the same as my other projects: I decided what the overlay should do and made the calls about which model goes where, and Claude wrote essentially all of the code and the tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas
&lt;/h2&gt;

&lt;p&gt;Three real ones, each of which cost actual debugging time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A "3-minute podcast" came out 42 minutes long.&lt;/strong&gt; This was the one that genuinely surprised me. The outline step plans segment durations in &lt;em&gt;seconds&lt;/em&gt; — a sensible two-minute plan might be segments of 20, 30, 30, 25, and 15 seconds. But a downstream step stored those numbers into a field that the rest of the pipeline treats as &lt;em&gt;minutes&lt;/em&gt;, multiplying by a per-minute word budget. So a 2-minute request quietly became a 21,600-word target and produced a 42-minute monster. The fix is a &lt;code&gt;normalize_segment_durations&lt;/code&gt; step that rescales whatever numbers the model emits so they sum to the requested total, unit-agnostic, before anything downstream reads them. On top of that there's a hard cap, &lt;code&gt;cap_dialogue_words&lt;/code&gt;, set at &lt;code&gt;WORDS_PER_MINUTE = 130&lt;/code&gt; (Kokoro's real speaking rate), that trims the generated dialogue down to the word budget while always keeping the final sign-off turn. After both fixes, in my testing a 3-minute request lands at about 3.04 minutes regardless of how verbose the source document is. The tradeoff is that very wordy content generates-then-trims, which wastes some tokens, but a request for N minutes now reliably yields roughly N minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model kept emitting JSON that wouldn't parse — so I stopped trusting it to.&lt;/strong&gt; The final step asks the model to re-emit the entire conversation as one strict JSON object, and that turned out to be the architecturally fragile part of the whole pipeline. It would return the dialogue as a JSON &lt;em&gt;string&lt;/em&gt; (double-encoded), and that inner string was itself malformed — unescaped quotes inside the text, mangled unicode, the occasional &lt;code&gt;"K&amp;amp;R"&lt;/code&gt; breaking the parse. A strict &lt;code&gt;json.loads&lt;/code&gt; rejects all of it. The fix came in layers: first bump that role to Sonnet (Haiku failed it outright, returning a scratchpad with the dialogue field missing); then, when the string is still malformed, fall back to &lt;code&gt;json-repair&lt;/code&gt;; and finally an entry sanitizer that rebuilds the dialogue list by hand — keeping only entries with real text, coercing a missing speaker by alternating from the previous turn, and unescaping the text. After the sanitizer the step is structurally unable to fail schema validation no matter what the model hands back. Verified on real failing data in my run: 51 entries, zero invalid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key wiring resolved to empty even though it was set.&lt;/strong&gt; The compose file originally interpolated &lt;code&gt;ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}&lt;/code&gt; from the shell. My shell exports an empty &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;, and Docker Compose lets a shell variable shadow the one in &lt;code&gt;.env&lt;/code&gt; — so the value resolved to empty string and every Claude call failed for no obvious reason. The fix was to give the agent service &lt;code&gt;env_file: [.env]&lt;/code&gt; and delete the interpolation line entirely, so the key is read straight from the file. I confirmed it by checking the key length inside the running container (108 characters, not zero). It's the kind of bug that looks like an auth problem and is actually a config-precedence problem.&lt;/p&gt;

&lt;p&gt;(There was also a smaller one worth flagging: the original TTS service concatenated audio at the byte level, which corrupts the WAV because each turn carries its own header. Rendering all the samples and writing a single WAV header — with 350 ms of silence inserted between turns — fixed both the corruption and the pacing.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What works now
&lt;/h2&gt;

&lt;p&gt;It runs end-to-end on a stack with zero NVIDIA and zero ElevenLabs keys. A smoke test against a sample PDF produced a valid 24 kHz mono 16-bit WAV through the API — POST the PDF, poll status, GET the audio — using only an Anthropic key and local Kokoro. The duration normalization and the hard cap mean a requested length actually maps to roughly that length: the C++ chapter that once overshot to nearly 8 minutes now lands at 3.04 minutes for a 3-minute request in my run. Both text PDFs and scanned image-only PDFs work, the latter via the easyocr path. The JSON-repair-plus-sanitizer chain holds up against the malformed output the model genuinely produces.&lt;/p&gt;

&lt;p&gt;It's not perfect. The fragile part is still the final "re-emit everything as strict JSON" step — I made it un-failable, but un-failable is not the same as elegant, and a future version should probably stop asking the model to round-trip the whole conversation through JSON at all. TTS on CPU is slow (no GPU is wired into that container yet), and the OCR path reads code and symbols badly. The large-document story is also weak: the pipeline is built for short documents and sends the whole text in one summarize call, so a 900-page reference blows past the context window — for now the answer is to trim to the chapter you actually want. But for the thing I set out to prove — that NVIDIA's pdf-to-podcast blueprint can run, end to end, on a fully local stack with none of the keys it asks for — it delivers, and it produces a real audio file at the length I asked for.&lt;/p&gt;

&lt;p&gt;This is one post in a series on projects built this way. The running list is on the &lt;a href="https://dev.to/projects/"&gt;projects page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tts</category>
      <category>localllm</category>
      <category>python</category>
      <category>audio</category>
    </item>
  </channel>
</rss>
