<?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: Alain Chan</title>
    <description>The latest articles on DEV Community by Alain Chan (@alaindevs).</description>
    <link>https://dev.to/alaindevs</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%2F3957465%2F7da5c59b-d81d-4953-b1e2-f7cab087b424.png</url>
      <title>DEV Community: Alain Chan</title>
      <link>https://dev.to/alaindevs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alaindevs"/>
    <language>en</language>
    <item>
      <title>From Prototype to Polished: Reviving My Dart Personal Blog with GitHub Copilot</title>
      <dc:creator>Alain Chan</dc:creator>
      <pubDate>Sat, 06 Jun 2026 20:52:44 +0000</pubDate>
      <link>https://dev.to/alaindevs/from-prototype-to-polished-reviving-my-dart-personal-blog-with-github-copilot-1jkh</link>
      <guid>https://dev.to/alaindevs/from-prototype-to-polished-reviving-my-dart-personal-blog-with-github-copilot-1jkh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I revived &lt;code&gt;personal_blog&lt;/code&gt;, a server-rendered personal blogging platform built with Dart Shelf, PostgreSQL, Mustache templates, and Tailwind CSS.&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.amazonaws.com%2Fuploads%2Farticles%2Fej4y5iudjb82wqdq381l.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.amazonaws.com%2Fuploads%2Farticles%2Fej4y5iudjb82wqdq381l.png" alt="LandingPage" width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/AlainDevs/personal_blog" rel="noopener noreferrer"&gt;https://github.com/AlainDevs/personal_blog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project started as a bare-bones Dart web app and gradually became a small publishing system with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public blog pages and individual post pages&lt;/li&gt;
&lt;li&gt;User registration and login&lt;/li&gt;
&lt;li&gt;Admin-only pages for posts, categories, users, and settings&lt;/li&gt;
&lt;li&gt;PostgreSQL persistence&lt;/li&gt;
&lt;li&gt;Seeded demo content and demo users&lt;/li&gt;
&lt;li&gt;Tailwind-powered styling&lt;/li&gt;
&lt;li&gt;Docker Compose setup for one-command local deployment&lt;/li&gt;
&lt;li&gt;Automated tests for authentication, settings, comments, middleware, and blog routing&lt;/li&gt;
&lt;li&gt;A separate Docker-based performance testing stack using &lt;code&gt;wrk&lt;/code&gt; and Lua&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What makes this project meaningful to me is that it is intentionally simple. It is not trying to be another huge CMS. It is a calm, personal publishing space: fast to run, easy to understand, and small enough for one developer to confidently maintain.&lt;/p&gt;

&lt;p&gt;Before the comeback, it still felt like a rough prototype. It had pieces of a blog, but it was not easy enough to run, test, benchmark, or explain to another person. The Finish-Up-A-Thon gave me a reason to turn it into something I could actually hand to someone and say: clone it, run one command, log in, and start exploring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Project repository: &lt;a href="https://github.com/AlainDevs/personal_blog" rel="noopener noreferrer"&gt;https://github.com/AlainDevs/personal_blog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run the demo locally with Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AlainDevs/personal_blog.git
&lt;span class="nb"&gt;cd &lt;/span&gt;personal_blog
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Admin area:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8080/admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Performance report included in the repository:&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.amazonaws.com%2Fuploads%2Farticles%2Fl5nryrsshw0z2dei5alf.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.amazonaws.com%2Fuploads%2Farticles%2Fl5nryrsshw0z2dei5alf.png" alt="Performance" width="800" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The latest recorded smoke benchmark reported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,877 total requests in 2.01 seconds&lt;/li&gt;
&lt;li&gt;935.31 requests per second&lt;/li&gt;
&lt;li&gt;4.27ms average latency&lt;/li&gt;
&lt;li&gt;7.45ms 99th percentile latency&lt;/li&gt;
&lt;li&gt;No reported socket errors&lt;/li&gt;
&lt;li&gt;No reported non-2xx/3xx responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validation checks recorded in the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker compose -f docker-compose.performance.yml config --quiet&lt;/code&gt; passed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dart analyze&lt;/code&gt; passed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dart test --timeout=30s&lt;/code&gt; passed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;The project had eight commits in total. The first four commits created the original prototype: project structure, Tailwind setup, CRUD-oriented services, Docker scripts, and an early server implementation.&lt;/p&gt;

&lt;p&gt;The Finish-Up-A-Thon comeback happened in the top four commits. These were the commits where I used GitHub Copilot, together with my AI coding context and rules including ByteRover, DCM Flutter Guidelines, and AI rules for Flutter/Dart-style development, to push the project from “works on my machine prototype” toward “finished, runnable, documented project.”&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.amazonaws.com%2Fuploads%2Farticles%2Fk13ktp6wd5smzgm7nddb.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.amazonaws.com%2Fuploads%2Farticles%2Fk13ktp6wd5smzgm7nddb.png" alt="GitHub Copilot skills" width="676" height="240"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fdn52dbauvt9jbkv4etdv.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.amazonaws.com%2Fuploads%2Farticles%2Fdn52dbauvt9jbkv4etdv.png" alt="GitHub Copilot DCM skills" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Commit 1: &lt;code&gt;8345dbd&lt;/code&gt; — Docker infrastructure and service layer enhancements
&lt;/h3&gt;

&lt;p&gt;Commit message: &lt;code&gt;Add Docker infrastructure and service layer enhancements&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This was the biggest comeback commit. It changed &lt;strong&gt;44 files with 4,183 insertions and 1,577 deletions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before this commit, the project had useful pieces, but the architecture was still too tangled. Server setup, routing, database access, auth behavior, and page rendering were not cleanly separated enough for confident testing or future changes.&lt;/p&gt;

&lt;p&gt;What changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added a cleaner &lt;code&gt;createAppHandler()&lt;/code&gt; function so the Shelf app can be built both by the executable and by tests.&lt;/li&gt;
&lt;li&gt;Improved the middleware flow so JWT auth context is attached to requests.&lt;/li&gt;
&lt;li&gt;Protected admin pages and admin APIs more consistently.&lt;/li&gt;
&lt;li&gt;Added &lt;code&gt;DatabaseConnection&lt;/code&gt; as a shared PostgreSQL bootstrap layer.&lt;/li&gt;
&lt;li&gt;Added schema creation and seed data for users, categories, posts, comments, post categories, and application settings.&lt;/li&gt;
&lt;li&gt;Added an &lt;code&gt;AppSetting&lt;/code&gt; model.&lt;/li&gt;
&lt;li&gt;Added &lt;code&gt;SettingsService&lt;/code&gt; and &lt;code&gt;SettingsHandler&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Added admin settings UI for toggling public registration.&lt;/li&gt;
&lt;li&gt;Improved service-layer boundaries for users, posts, categories, comments, and settings.&lt;/li&gt;
&lt;li&gt;Used safer named SQL patterns through &lt;code&gt;Sql.named&lt;/code&gt; style interactions.&lt;/li&gt;
&lt;li&gt;Updated Docker Compose to use PostgreSQL 18 and a persistent database volume.&lt;/li&gt;
&lt;li&gt;Added tests for auth, registration settings, comments, and admin middleware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This commit was where the project stopped being a pile of working code and started feeling like an application with structure.&lt;/p&gt;

&lt;p&gt;Copilot helped here by accelerating the repetitive but important parts: service methods, handler wiring, model mapping, test doubles, and route refactors. The AI rules helped keep the generated code closer to consistent Dart conventions instead of random snippets.&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.amazonaws.com%2Fuploads%2Farticles%2Fz0azol9ok6v53izg0ace.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.amazonaws.com%2Fuploads%2Farticles%2Fz0azol9ok6v53izg0ace.png" alt="admin dashboard" width="799" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Commit 2: &lt;code&gt;d8832a9&lt;/code&gt; — Safer route parameter handling
&lt;/h3&gt;

&lt;p&gt;Commit message: &lt;code&gt;refactor: Replace direct access to request parameters with a utility function for safer path string retrieval&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This was a smaller but important hardening commit.&lt;/p&gt;

&lt;p&gt;Before this commit, route handlers accessed path parameters directly, for example by reading &lt;code&gt;request.params['slug']&lt;/code&gt; inline. That works, but it spreads low-level request handling across the codebase.&lt;/p&gt;

&lt;p&gt;What changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added &lt;code&gt;readPathString(Request request, String key)&lt;/code&gt; in &lt;code&gt;request_utils.dart&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Updated integer path parsing so &lt;code&gt;readPathInt()&lt;/code&gt; builds on top of &lt;code&gt;readPathString()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Updated the blog detail route to read the slug through the utility function.&lt;/li&gt;
&lt;li&gt;Added a test proving that &lt;code&gt;/blog/a-tiny-publishing-checklist&lt;/code&gt; renders the correct blog detail page.&lt;/li&gt;
&lt;li&gt;Added fake post/comment services so the page route can be tested without a real database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This commit represents the “finish-up” mindset: not just adding features, but reducing fragile patterns and locking behavior with tests.&lt;/p&gt;

&lt;p&gt;Copilot helped by suggesting the test structure and the fake service overrides. That let me quickly validate the route behavior rather than only eyeballing the refactor by using MCP.&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.amazonaws.com%2Fuploads%2Farticles%2F3ozzavnxk9h2zzs36pkr.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.amazonaws.com%2Fuploads%2Farticles%2F3ozzavnxk9h2zzs36pkr.png" alt="MCP" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Commit 3: &lt;code&gt;f52a043&lt;/code&gt; — Performance testing infrastructure
&lt;/h3&gt;

&lt;p&gt;Commit message: &lt;code&gt;feat: Add performance testing infrastructure with Docker and Lua scripts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This commit added 1,015 lines across seven files.&lt;/p&gt;

&lt;p&gt;Before this commit, I could run the app, but I did not have a repeatable way to answer a basic question: “How does it behave under load?”&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.amazonaws.com%2Fuploads%2Farticles%2F2y2d1p1425apxp9g2zw1.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.amazonaws.com%2Fuploads%2Farticles%2F2y2d1p1425apxp9g2zw1.png" alt="test result" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added &lt;code&gt;docker-compose.performance.yml&lt;/code&gt;, a separate performance testing stack.&lt;/li&gt;
&lt;li&gt;Added an Alpine-based performance Dockerfile that builds &lt;code&gt;wrk&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Added &lt;code&gt;request_mix.lua&lt;/code&gt; for weighted traffic across realistic routes:

&lt;ul&gt;
&lt;li&gt;homepage&lt;/li&gt;
&lt;li&gt;seeded blog detail pages&lt;/li&gt;
&lt;li&gt;generated CSS&lt;/li&gt;
&lt;li&gt;public JavaScript&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Added &lt;code&gt;generate_report.js&lt;/code&gt;, which runs Docker Compose, captures benchmark output, parses results, and writes a GitHub-ready Markdown report.&lt;/li&gt;

&lt;li&gt;Added &lt;code&gt;PERFORMANCE_RESULTS.md&lt;/code&gt; with the latest benchmark output.&lt;/li&gt;

&lt;li&gt;Added npm scripts:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm run performance:report&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm run performance:report:smoke&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Documented how to tune benchmark load with environment variables.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This was a big step toward making the project feel complete. A personal blog should not only have features; it should be easy to verify that pages respond quickly and that changes do not obviously break performance.&lt;/p&gt;

&lt;p&gt;Copilot was especially useful here because the work crossed several small domains: Docker Compose, shell readiness checks, Lua route selection for &lt;code&gt;wrk&lt;/code&gt;, Node.js process management, Markdown report generation, and benchmark parsing.&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.amazonaws.com%2Fuploads%2Farticles%2Fog9m2w754u77hhfz1gi0.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.amazonaws.com%2Fuploads%2Farticles%2Fog9m2w754u77hhfz1gi0.png" alt="agent in Copilot" width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Commit 4: &lt;code&gt;fdb4e92&lt;/code&gt; — README polish and beginner-friendly setup
&lt;/h3&gt;

&lt;p&gt;Commit message: &lt;code&gt;docs: Update README to enhance Docker Compose instructions and clarify setup process&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This final comeback commit focused on usability.&lt;/p&gt;

&lt;p&gt;Before this commit, the README still described the project like a basic Dart web app and told users to install WebDev manually. That no longer matched the revived project by creating our own agent - doc-reviewer.&lt;/p&gt;

&lt;p&gt;What changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rewrote the README around Docker Compose as the primary way to run the project.&lt;/li&gt;
&lt;li&gt;Explained that users do not need to install Dart, Node.js, PostgreSQL, or WebDev locally.&lt;/li&gt;
&lt;li&gt;Added step-by-step startup instructions.&lt;/li&gt;
&lt;li&gt;Added the local URL: &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Added seeded admin and reader accounts.&lt;/li&gt;
&lt;li&gt;Added the admin URL: &lt;code&gt;http://localhost:8080/admin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Added stop, restart, and database reset instructions.&lt;/li&gt;
&lt;li&gt;Kept the performance testing section so users can generate benchmark reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was the last mile of finishing the project. The app may be technically complete, but if another developer cannot run it easily, it still feels unfinished. This README update made the project approachable.&lt;/p&gt;

&lt;p&gt;Copilot helped turn rough notes into a clearer onboarding path and made the documentation more user-focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;GitHub Copilot helped me finish the project in the way I actually needed: not by replacing my decisions, but by keeping momentum while I worked through lots of small, connected tasks.&lt;/p&gt;

&lt;p&gt;I used Copilot with my AI development context, including ByteRover, DCM Flutter Guidelines, and AI rules for Flutter/Dart-style development. Even though this is a Dart Shelf web server rather than a Flutter UI app, those rules still helped encourage cleaner structure, explicit tests, safer utilities, and more maintainable code.&lt;/p&gt;

&lt;p&gt;The most useful parts of Copilot were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turning architectural intent into concrete Dart service and handler code.&lt;/li&gt;
&lt;li&gt;Helping refactor the Shelf server into a testable &lt;code&gt;createAppHandler()&lt;/code&gt; structure.&lt;/li&gt;
&lt;li&gt;Suggesting test cases and fake services for auth, settings, comments, and blog routes.&lt;/li&gt;
&lt;li&gt;Speeding up repetitive model and mapping work.&lt;/li&gt;
&lt;li&gt;Helping write Docker Compose and benchmark infrastructure without constantly switching mental context.&lt;/li&gt;
&lt;li&gt;Helping polish the README so the final project is easier for another person to run.&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2F1o1ca1yc90g9x3i1fdok.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.amazonaws.com%2Fuploads%2Farticles%2F1o1ca1yc90g9x3i1fdok.png" alt="post list" width="799" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The before-and-after arc is clear to me:&lt;/p&gt;

&lt;p&gt;Before, &lt;code&gt;personal_blog&lt;/code&gt; was a promising but unfinished side project. It had the shape of a blog, but it still required too much local setup knowledge, had less confidence around tests, and did not have a clear performance story.&lt;/p&gt;

&lt;p&gt;After, it is a Dockerized Dart personal blog with seeded content, admin flows, persistent PostgreSQL storage, application settings, automated tests, performance benchmarking, and beginner-friendly documentation.&lt;/p&gt;

&lt;p&gt;That is exactly what I wanted from this challenge: not to start something new, but to finally finish something I already cared about.&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.amazonaws.com%2Fuploads%2Farticles%2Fmsl4vgft1vkpk5ms7hzm.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.amazonaws.com%2Fuploads%2Farticles%2Fmsl4vgft1vkpk5ms7hzm.png" alt="post card" width="800" height="893"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
    </item>
    <item>
      <title>Breaking the Silence: Running Hermes Agent with Local C++ Voice Cloning (VoxCPM2) on ARM64</title>
      <dc:creator>Alain Chan</dc:creator>
      <pubDate>Fri, 29 May 2026 16:20:13 +0000</pubDate>
      <link>https://dev.to/alaindevs/breaking-the-silence-running-hermes-agent-with-local-c-voice-cloning-voxcpm2-on-arm64-1dfm</link>
      <guid>https://dev.to/alaindevs/breaking-the-silence-running-hermes-agent-with-local-c-voice-cloning-voxcpm2-on-arm64-1dfm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hermes-agent-2026-05-15"&gt;Hermes Agent Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking the Silence: Running Hermes Agent with Local C++ Voice Cloning (VoxCPM2) on ARM64
&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.amazonaws.com%2Fuploads%2Farticles%2F7tntao2ae49hlmf07vfc.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.amazonaws.com%2Fuploads%2Farticles%2F7tntao2ae49hlmf07vfc.png" alt="VPS" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most AI agents are deaf and mute, communicating solely through text or latency-heavy cloud TTS APIs. When I set out to build a fully autonomous morning assistant using &lt;strong&gt;Hermes Agent&lt;/strong&gt; hosted locally on my Debian ARM64 server, I wanted something different. I wanted a private, high-fidelity, cloned voice that could talk to me natively on WhatsApp every morning with custom-tailored weather briefings and diet-aware recommendations.&lt;/p&gt;

&lt;p&gt;To achieve this, I integrated Hermes with &lt;strong&gt;VoxCPM2&lt;/strong&gt;—a highly optimized multilingual speech-cloning model running in clean C++. Along the way, I hit some brutal low-level compilation blocks, model-packaging quirks, and real-time audio pipeline hurdles. &lt;/p&gt;

&lt;p&gt;Here is the exact blueprint of how I overcame these ARM64 limitations, patched GGML, and wired Hermes Agent to speak to me in a pristine, cloned voice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Vision: A Voice-First Private Daily Agent
&lt;/h2&gt;

&lt;p&gt;The goal was to leverage Hermes Agent's autonomous &lt;strong&gt;Cron&lt;/strong&gt; and &lt;strong&gt;Persistent Memory&lt;/strong&gt; systems. Every morning at a scheduled time, a cron job fires a custom Python script. Hermes gathers local weather forecasts, synthesizes them with personal preferences, and prepares a daily briefing.&lt;/p&gt;

&lt;p&gt;Instead of printing text, the agent passes the payload to a local C++ inference pipeline, clones a target voice, packages the audio, and sends it directly to my WhatsApp as an instant, native voice message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------+
|                         Hermes Agent                            |
|  [Cron Job (Scheduled)] -&amp;gt; [Weather/News Fetch] -&amp;gt; [Persist Mem]|
+-------------------------------+---------------------------------+
                                | (Text Payload)
                                v
+-----------------------------------------------------------------+
|                     VoxCPM2 C++ Engine                          |
|  [16kHz Reference WAV] -&amp;gt; [ggml.cpp] -&amp;gt; [High-Fid FP16 Voice]   |
+-------------------------------+---------------------------------+
                                | (Raw WAV Output)
                                v
+-----------------------------------------------------------------+
|                     Audio Pipeline &amp;amp; Delivery                   |
|  [FFmpeg (OGG/Opus)] -&amp;gt; [Local WA Bridge] -&amp;gt; [Native Voice Msg] |
+-----------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Hurdle 1: Bypassing the 64-Character GGML Tensor Limit
&lt;/h2&gt;

&lt;p&gt;VoxCPM2's C++ inference engine relies on a clean, local build of &lt;code&gt;ggml&lt;/code&gt;. When compilation finished and I attempted to load the larger, highly expressive GGUF models for multimodal/cloned speech, the engine crashed instantly with loading errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cause:
&lt;/h3&gt;

&lt;p&gt;GGML historically hardcodes &lt;code&gt;GGML_MAX_NAME&lt;/code&gt; (the maximum length of a tensor's name) to &lt;code&gt;64&lt;/code&gt; characters. Because high-fidelity speech models contain deep, hierarchical layers with descriptive naming schemes, their tensor names easily exceed 64 characters.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix:
&lt;/h3&gt;

&lt;p&gt;I had to patch the underlying GGML source before building. If you are running into this, navigate to &lt;code&gt;third_party/ggml/include/ggml.h&lt;/code&gt; and increase the limit to &lt;code&gt;128&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Locate in third_party/ggml/include/ggml.h&lt;/span&gt;
&lt;span class="c1"&gt;// Old definition:&lt;/span&gt;
&lt;span class="c1"&gt;// #define GGML_MAX_NAME 64&lt;/span&gt;

&lt;span class="c1"&gt;// New patched definition:&lt;/span&gt;
&lt;span class="cp"&gt;#define GGML_MAX_NAME 128
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After modifying this, re-running the C++ make pipeline allowed the GGUF loader to successfully parse the deep voice layers without truncation or memory segmentation faults.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hurdle 2: Untangling Model Packages for C++ Inference
&lt;/h2&gt;

&lt;p&gt;Many single-file GGUF packages available online (e.g., standard model merges) lack the necessary metadata required by the raw C++ inference binary of VoxCPM2. &lt;/p&gt;

&lt;p&gt;To run end-to-end voice cloning ("Ultimate Mode") successfully, I discovered that you must load separated model files that preserve explicit metadata structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;base_lm_q8_0.gguf&lt;/code&gt; (The quantized base language model weights)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;residual_lm_q8_0.gguf&lt;/code&gt; (The residual weights)&lt;/li&gt;
&lt;li&gt;Or verified unified packages such as &lt;code&gt;voxcpm2-q8_0-audiovae-f16.gguf&lt;/code&gt; from &lt;code&gt;bluryar/VoxCPM-GGUF&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By utilizing an FP16 high-fidelity model on an ARM64 CPU, we prioritize pristine vocal textures and rich tone over fast but robotic lower-precision modes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hurdle 3: Designing the Real-Time Audio &amp;amp; Delivery Pipeline
&lt;/h2&gt;

&lt;p&gt;Getting Hermes to talk natively on WhatsApp requires an exact, low-latency audio pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Format Reference Audio
&lt;/h3&gt;

&lt;p&gt;VoxCPM2 C++ cloning requires a pristine &lt;strong&gt;16kHz mono WAV&lt;/strong&gt; format reference file. Our utility script converts a standard MP3 sample to the exact format needed before running the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Conversion using FFmpeg in Python subprocess
&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffmpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ref_mp3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-ar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-ac&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ref_wav&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: C++ Inference
&lt;/h3&gt;

&lt;p&gt;The utility executes the C++ binary with customized parameters, leveraging multi-threading optimized for the server's ARM64 CPU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/home/debian/VoxCPM.cpp/build/examples/voxcpm_tts &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--model-path&lt;/span&gt; /path/to/voxcpm2-f16-audiovae-f16.gguf &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--prompt-audio&lt;/span&gt; /path/to/ref.wav &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--prompt-text&lt;/span&gt; &lt;span class="s2"&gt;"Reference voice transcript text."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--text&lt;/span&gt; &lt;span class="s2"&gt;"Target synthesis weather report text."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; /path/to/output.wav &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--backend&lt;/span&gt; cpu &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--threads&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cfg-value&lt;/span&gt; 2.0 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--inference-timesteps&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Low-Latency Encoding &amp;amp; WhatsApp Bridge Delivery
&lt;/h3&gt;

&lt;p&gt;Standard WAV files arrive on WhatsApp as document attachments. To deliver them as &lt;strong&gt;native, instant voice messages&lt;/strong&gt; (playable voice bubbles), we transcode them into &lt;code&gt;.ogg&lt;/code&gt; format using the highly compressed &lt;strong&gt;Opus&lt;/strong&gt; codec. &lt;/p&gt;

&lt;p&gt;We can also apply FFmpeg's &lt;code&gt;dynaudnorm&lt;/code&gt; (dynamic audio normalizer) filter to keep output volume levels consistent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; output.wav &lt;span class="nt"&gt;-filter&lt;/span&gt;:a dynaudnorm &lt;span class="nt"&gt;-c&lt;/span&gt;:a libopus output.ogg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fa1z4lifypgj33av2l46h.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.amazonaws.com%2Fuploads%2Farticles%2Fa1z4lifypgj33av2l46h.png" alt="WhatsApp" width="800" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the audio file is ready, the script programmatically makes an HTTP POST request to a local WhatsApp API bridge endpoint &lt;code&gt;/send-media&lt;/code&gt; with the payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chatId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_whatsapp_jid@lid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/path/to/output.ogg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mediaType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"audio"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This forces WhatsApp to render the media natively as a press-to-play instant voice message bubble!&lt;/p&gt;




&lt;h2&gt;
  
  
  Combining It All: The Self-improving Local Weather Scheduler
&lt;/h2&gt;

&lt;p&gt;The backbone of this workflow consists of two main Python components scheduled and triggered under Hermes Agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;cron_morning_weather.py&lt;/code&gt;: Fetches real-time JSON forecast from &lt;code&gt;wttr.in&lt;/code&gt; for the user's location, parses hourly temperatures, converts English weather descriptions into natural, expressive Cantonese, decides if an umbrella is needed, and outputs a cute morning briefing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run_clone.py&lt;/code&gt;: Receives the text payload, packages the model, compiles the C++ parameters, encodes the audio using &lt;code&gt;ffmpeg&lt;/code&gt; to &lt;code&gt;libopus&lt;/code&gt;, and delivers it to the local WhatsApp gateway bridge.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Magic of Hermes Agent: Memory and Location Privacy
&lt;/h3&gt;

&lt;p&gt;What makes this system genuinely &lt;em&gt;autonomous&lt;/em&gt; rather than a simple cron-bash script is &lt;strong&gt;Hermes's self-improving memory architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Persistent Memory (User Profile)&lt;/strong&gt;: Hermes maintains an ongoing log of user preferences across sessions. It remembers that Hermes follow user preferences for example like philosophy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context-Aware Briefings&lt;/strong&gt;: When generating the script text, Hermes synthesizes these facts from its memory. The morning weather update isn't just a reading of numbers; it dynamically adds philosophical thoughts suited to the day's weather.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timezone Synchronization&lt;/strong&gt;: Because scheduled cron tasks run in the server's UTC background, Hermes automatically calculates local offset (e.g. BST vs UTC) to ensure the morning briefing is delivered exactly at the user's local wake-up time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous Skill Management&lt;/strong&gt;: When there are path updates or script logic tweaks, Hermes adjusts its internal reference memory, avoiding stale or cached references during execution.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why Open-Source Agents Win
&lt;/h2&gt;

&lt;p&gt;Running Hermes Agent locally on an ARM64 server proved something crucial: &lt;strong&gt;We do not need to rely on proprietary or closed-source ecosystems to build delightful, highly personalized AI experiences.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With a 4-line patch to &lt;code&gt;ggml.h&lt;/code&gt;, an optimized C++ inference binary, and Hermes's robust multi-session persistent memory, I have a private, voice-cloning companion that knows my diet, my daily schedule, and my philosophical quirks—costing virtually nothing when idle.&lt;/p&gt;

&lt;p&gt;If you are building with Hermes, don't just stay in the terminal. Give your agent a voice, patch those C++ boundaries, and build something that feels alive!&lt;/p&gt;

</description>
      <category>hermesagentchallenge</category>
      <category>devchallenge</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
