<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Dean Hume's Blog]]></title><description><![CDATA[I'm a Technical Program Manager at Xbox Game Studios. I write about technical leadership, developer tools, and making great games. ]]></description><link>https://deanhume.com/</link><image><url>https://deanhume.com/favicon.png</url><title>Dean Hume&apos;s Blog</title><link>https://deanhume.com/</link></image><generator>Ghost 6.51</generator><lastBuildDate>Mon, 06 Jul 2026 12:07:27 GMT</lastBuildDate><atom:link href="https://deanhume.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[As Engineering Teams get smaller, is the Technical Program Manager the missing piece?]]></title><description><![CDATA[As AI shrinks engineering teams and the EM role splits in two, the Technical Program Manager could be the most overlooked answer hiding in plain sight.]]></description><link>https://deanhume.com/as-engineering-teams-get-smaller-is-the-technical-program-manager-the-missing-piece/</link><guid isPermaLink="false">6a2ad19ee1417c0001fea966</guid><category><![CDATA[Technical Leadership]]></category><category><![CDATA[Technical Program Manager]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Mon, 29 Jun 2026 10:27:25 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1748283373393-efe3e2aa8d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDR8fHBpdCUyMHN0b3B8ZW58MHx8fHwxNzgyNzI4NzIzfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1748283373393-efe3e2aa8d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDR8fHBpdCUyMHN0b3B8ZW58MHx8fHwxNzgyNzI4NzIzfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="As Engineering Teams get smaller, is the Technical Program Manager the missing piece?"><p>Do you remember what a &quot;normal&quot; software team looked like?</p><p>A product manager, an engineering manager, a designer, and six or seven engineers. Weekly planning meetings, a backlog to groom, a sprint to run. For most of the last decade, that was just... how teams worked. It was the water we all swam in.</p><p>That was my daily life as an software engineer and then an engineering manager for most of my career. That shape was so familiar it felt like a law of nature rather than a design choice.</p><p>I recently came across an article on the <a href="https://leaddev.com/career-development/the-engineering-manager-role-is-splitting-in-two" rel="noreferrer">LeadDev website</a> that argued that traditional team shape is breaking apart - and fast. AI is reshaping the traditional team structure that engineering managers are used to, and the engineering manager (EM) role itself is splitting into two very different futures. One stays close to the code. The other expands outward across multiple teams, with ratios that would have seemed absurd five years ago.</p><p>It&apos;s a well-argued piece and I agree with most of it. But reading it, I kept thinking: there&apos;s a third role sitting right in the middle of this split that barely gets a mention.</p><p>The <a href="https://deanhume.com/tag/leadership/" rel="noreferrer">Technical Program Manager</a> (aka a TPM).</p><p><strong>I&apos;ve spent time on both sides of the engineering/management divide.</strong> I&apos;ve been an engineering manager, and I&apos;m an IC now. And one thing that experience teaches you is that the messiest, most overlooked part of any org isn&apos;t the people management or the code - it&apos;s the space between teams. The dependencies. The cross-functional delivery risks. The &quot;<em>who owns this?</em>&quot; conversations that stall projects for weeks.</p><p>That&apos;s exactly the space that gets wider as teams shrink.</p><p>I know what you&apos;re thinking - aren&apos;t we just adding extra headcount? Not quite. A single TPM can serve across four or five of those small teams simultaneously, handling the cross-cutting work that would otherwise fragment across every Engineering Manager and tech lead in the building. It&apos;s not a new layer. It&apos;s a better shape.</p><p>That LeadDev article is really describing one thing: small, fast-moving, AI-augmented teams. Tech leads that are head down in the code. Wide-span engineering managers that are spread thin across five or six of those teams. Who&apos;s looking at the <strong>whole </strong>picture? Who&apos;s tracking that Team A&apos;s output is the input for Team B&apos;s Q3 milestone? Who&apos;s in the room when the product roadmap and the engineering capacity don&apos;t line up?</p><p>That work doesn&apos;t disappear just because management layers do. If anything, it gets harder.</p><p>A good TPM doesn&apos;t typically manage teams and doesn&apos;t write production code - but they hold the connective tissue of a programme together. They ask the questions that fall between job descriptions. They translate between engineering constraints and business timelines. They&apos;re technical enough to smell risk early and organised enough to do something about it.</p><p>In a world where your Engineering Manager is managing multiple engineers across multiple teams, the last thing they have time for is running a cross-team dependency log or facilitating a technical risk review. That&apos;s not a criticism - it&apos;s just scope. Something has to give.</p><p>The Technical Program Manager (TPM) fills that gap.</p><p>So why isn&apos;t this already the obvious answer? Partly because the TPM role itself is poorly understood. Ask ten people what a TPM does and you&apos;ll get ten different answers - some companies use the title for someone close to project management, others for a semi-technical lead, others barely use it at all. It&apos;s a role that&apos;s been historically inconsistent, which makes it easy to overlook when org charts get redrawn. But that inconsistency is also an opportunity - as the Engineering Manager role fractures, companies will need to define this connective-tissue work somehow, and the TPM title is sitting right there, half-formed and ready to be shaped into something more central.</p><p><strong>What&apos;s interesting is that the skills which make a great TPM are almost the opposite of what each fork of the Engineering Manager role is optimising for.</strong> The Tech Lead path doubles down on technical depth. The wide-span Engineering Manager path doubles down on people skills and influence. The Technical Program Manager has to hold both - enough technical credibility to challenge engineering estimates, enough interpersonal range to keep a dozen stakeholders aligned.</p><p>If you&apos;re currently an Engineering Manager wondering which way to lean as your org flattens, it might be worth asking yourself: which part of the job do you actually find energising? The 1:1s and the career conversations, or the delivery and the cross-team puzzles? Because that second category - that&apos;s Technical Program Manager territory, and it might be a better fit than either fork.</p><p>And if you&apos;re an IC watching all of this from the sidelines, wondering where the growth paths go from here - don&apos;t sleep on it. The role is less visible than Engineering Manager and less glamorous than staff engineer, but in a flatter org running lots of small teams, it might just be the most useful person in the building. &#x1F642;</p>]]></content:encoded></item><item><title><![CDATA[Meeting Notes - A Free Desktop App for Tracking 1:1s]]></title><description><![CDATA[Meeting Notes is a free offline desktop app for 1:1s. Organise notes by person, tag topics, and transcribe meetings on-device with Whisper.]]></description><link>https://deanhume.com/meeting-notes-desktop-app-windows/</link><guid isPermaLink="false">6a3bdc9a9cdfa500017127f0</guid><category><![CDATA[Technical Leadership]]></category><category><![CDATA[Technical Program Manager]]></category><category><![CDATA[Writing]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Mon, 29 Jun 2026 10:16:48 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/06/meeting_notes_hero.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/06/meeting_notes_hero.png" alt="Meeting Notes - A Free Desktop App for Tracking 1:1s"><p>Have you ever walked out of a 1:1 and realised you can&apos;t quite remember what you agreed on last time? Or spent five minutes before a catch-up scrolling through Slack threads, trying to piece together a conversation from three weeks ago?</p><p>I&apos;ve been there more times than I&apos;d like to admit. That&apos;s exactly why I built <a href="https://deanhume.github.io/meeting-notes/">Meeting Notes</a> - a lightweight, local-first desktop app for keeping organised notes with the people you work with.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/06/image-12.png" class="kg-image" alt="Meeting Notes - A Free Desktop App for Tracking 1:1s" loading="lazy" width="2000" height="941" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/06/image-12.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2026/06/image-12.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1600/2026/06/image-12.png 1600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/06/image-12.png 2368w" sizes="(min-width: 720px) 720px"></figure><h2 id="why-i-built-it">Why I built it</h2><p>Most of us are using the wrong tool for this. Notion is powerful but noisy. Word and Google Docs works, but it&apos;s not really shaped for quick, personal notes. And anything cloud-based means your candid thoughts about a tricky conversation are living on someone else&apos;s server.</p><p>I wanted something simple and fast. Something that felt less like a productivity system and more like a trusted notebook. No accounts to create, no syncing to worry about, no cloud. Just notes, stored on your machine.</p><h2 id="what-it-does">What it does</h2><p>The app is organised around people, not documents. You add the folks you meet with regularly - their name, role, and team - and every note you take lives under their profile. It builds up a natural history of every conversation over time.</p><p>Here&apos;s what&apos;s included:</p><ul><li>&#x1F4DD; Track meeting notes for multiple people</li><li>&#x1F465; Manage contacts with names, roles, and teams</li><li>&#x1F3F7;&#xFE0F; Tag and filter notes by topic (things like <code>hiring</code>, <code>architecture</code>, or <code>follow-up</code>)</li><li>&#x270D;&#xFE0F; Markdown support with live preview and a formatting toolbar</li><li>&#x1F399;&#xFE0F; Voice recording with offline speech-to-text - record your mic and/or system audio and transcribe straight into the note</li><li>&#x1F4BE; Autosave every 20 keystrokes, so you never lose your work</li><li>&#x1F3A8; Light/dark theme</li><li>&#x1F4A1; Discussion prompts suggested when you start a new note</li></ul><p>And everything runs completely offline. Your data never leaves your machine.</p><h2 id="the-voice-transcription-feature">The voice transcription feature</h2><p>This is the one I&apos;m most pleased with. Hit record, have your conversation, and the transcript drops straight into your note - all on-device using the Whisper model. No audio is ever sent anywhere. It even summarises what you discussed. I use it during walking 1:1s and it&apos;s become one of those features I didn&apos;t know I needed until I had it.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/06/image-11.png" class="kg-image" alt="Meeting Notes - A Free Desktop App for Tracking 1:1s" loading="lazy" width="2000" height="965" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/06/image-11.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2026/06/image-11.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1600/2026/06/image-11.png 1600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w2400/2026/06/image-11.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Meeting Notes Desktop App lets you record and summarise using AI</span></figcaption></figure><h2 id="give-it-a-try">Give it a try</h2><p>Meeting Notes is free, open source (MIT), and available now for Windows. Head over to the <a href="https://github.com/deanhume/meeting-notes/releases">releases page</a> to grab the latest version - just run the installer and you&apos;re up and running in under a minute.</p><p>&#x1F449; <a href="https://github.com/deanhume/meeting-notes/releases"><strong>Download Meeting Notes</strong></a> - v1.2.0 &#xB7; Windows x64</p><p>The <a href="https://github.com/deanhume/meeting-notes">source code is on GitHub</a> too, if you want to take a look, contribute, or build it yourself.</p><p>If you&apos;ve got any thoughts or run into anything unexpected, I&apos;d love to hear from you - drop a comment or open an issue. Happy note taking! &#x1F680;</p>]]></content:encoded></item><item><title><![CDATA[Building a Custom MCP Server with Node.js]]></title><description><![CDATA[<p>Ever wished your AI assistant actually knew <em>your</em> stuff? Your files, your schedule, or even a custom data source? That&apos;s exactly what the <a href="https://modelcontextprotocol.io/docs/getting-started/intro">Model Context Protocol</a> (MCP) makes possible.</p><p>MCP is an open standard for connecting AI assistants like Claude to external data sources and tools. Think of</p>]]></description><link>https://deanhume.com/building-a-custom-mcp-server-with-node-js/</link><guid isPermaLink="false">6a214c77d6737c0001317394</guid><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Thu, 04 Jun 2026 10:01:24 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1667372335936-3dc4ff716017?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fG1vZGVsJTIwY29udGV4dCUyMHByb3RvY29sJTIyfGVufDB8fHx8MTc4MDU2NzMyMHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1667372335936-3dc4ff716017?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fG1vZGVsJTIwY29udGV4dCUyMHByb3RvY29sJTIyfGVufDB8fHx8MTc4MDU2NzMyMHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="Building a Custom MCP Server with Node.js"><p>Ever wished your AI assistant actually knew <em>your</em> stuff? Your files, your schedule, or even a custom data source? That&apos;s exactly what the <a href="https://modelcontextprotocol.io/docs/getting-started/intro">Model Context Protocol</a> (MCP) makes possible.</p><p>MCP is an open standard for connecting AI assistants like Claude to external data sources and tools. Think of it like a USB port for AI - a single, standardised connector that works with any compatible device. Build the server once, plug it into any MCP-compatible client.</p><p>In this guide, we&apos;ll build a custom MCP server from scratch using Node.js. The example is deliberately practical: a bin collection reminder system. Simple enough to grasp quickly. Complex enough to cover every core concept you&apos;ll need.</p><h2 id="what-is-the-model-context-protocol">What is the Model Context Protocol?</h2><p>MCP gives AI assistants a common language for talking to the outside world. Instead of every tool needing its own bespoke integration, MCP provides a shared standard.</p><p>Your MCP server exposes &quot;tools&quot; - functions that AI Assistants can call. The server does the work (reading files, querying a database, calling an API), then returns structured data. The AI Assistant handles the rest.</p><p>Right now, Claude Desktop and VS Code (amongst others) support MCP out of the box. The ecosystem is growing fast.</p><h2 id="the-problem-were-solving">The Problem We&apos;re Solving</h2><p>To build a basic example (and experiment with real data in my life) - I wanted to create an MCP server that gave me the details of my Bin Collections. I wanted to be able to run a simple chat query and get a response. Ideally, this would save me time logging onto my local council&apos;s website and entering my information time and time again.</p><p>Here&apos;s the scenario. Garden waste goes out fortnightly on Fridays. Recycling is every Saturday. General refuse is also fortnightly on Fridays - but on different weeks. It&apos;s surprisingly easy to get wrong, especially when you factor in that holidays throw the schedule off.</p><p>So we&apos;ll build an MCP server that Claude can query to answer questions like &quot;when are my bins next due out?&quot; A simple use case that&apos;s genuinely useful and it teaches you the full pattern.</p><h2 id="setting-up-the-project">Setting Up the Project</h2><p>Create a new Node.js project:</p><pre><code class="language-bash">mkdir bin-collection-mcp
cd bin-collection-mcp
npm init -y</code></pre><p>You only need one dependency - the official MCP SDK:</p><pre><code class="language-bash">npm install @modelcontextprotocol/sdk</code></pre><p>Update your <code>package.json</code> to use ES modules. Add <code>&quot;type&quot;: &quot;module&quot;</code>:</p><pre><code class="language-json">{
  &quot;name&quot;: &quot;bin-collection-mcp&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;MCP server for local bin collection day lookups&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;dependencies&quot;: {
    &quot;@modelcontextprotocol/sdk&quot;: &quot;^1.0.0&quot;
  }
}</code></pre><h2 id="the-data-structure">The Data Structure</h2><p>Before writing any server code, think about your data. We&apos;ll use a simple JSON file - <code>bin-days.json</code> - that stores collection dates:</p><pre><code class="language-json">{
  &quot;collections&quot;: [
    {
      &quot;date&quot;: &quot;2026-05-29&quot;,
      &quot;day&quot;: &quot;Friday&quot;,
      &quot;types&quot;: [&quot;garden&quot;],
      &quot;notes&quot;: &quot;&quot;
    },
    {
      &quot;date&quot;: &quot;2026-05-30&quot;,
      &quot;day&quot;: &quot;Saturday&quot;,
      &quot;types&quot;: [&quot;recycling&quot;],
      &quot;notes&quot;: &quot;&quot;
    }
  ],
  &quot;binTypes&quot;: {
    &quot;general&quot;: {
      &quot;colour&quot;: &quot;black&quot;,
      &quot;description&quot;: &quot;General household refuse&quot;
    },
    &quot;garden&quot;: {
      &quot;colour&quot;: &quot;brown&quot;,
      &quot;description&quot;: &quot;Garden waste - grass, leaves, small branches&quot;
    },
    &quot;recycling&quot;: {
      &quot;colour&quot;: &quot;blue&quot;,
      &quot;description&quot;: &quot;Paper, card, plastics, tins, glass&quot;
    }
  }
}</code></pre><p>Each collection has a date, day name, an array of bin types, and optional notes. The <code>binTypes</code> object describes what each bin is for. Keep it readable - you&apos;ll be updating this file manually.</p><h2 id="building-the-mcp-server">Building the MCP Server</h2><p>Create <code>index.js</code>. We&apos;ll build it in steps.</p><h3 id="step-1import-dependencies-and-load-data">Step 1 - Import Dependencies and Load Data</h3><pre><code class="language-javascript">#!/usr/bin/env node

import { Server } from &quot;@modelcontextprotocol/sdk/server/index.js&quot;;
import { StdioServerTransport } from &quot;@modelcontextprotocol/sdk/server/stdio.js&quot;;
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from &quot;@modelcontextprotocol/sdk/types.js&quot;;
import { readFileSync } from &quot;fs&quot;;
import { resolve, dirname } from &quot;path&quot;;
import { fileURLToPath } from &quot;url&quot;;

const __dirname = dirname(fileURLToPath(import.meta.url));
const BIN_DAYS_PATH = resolve(__dirname, &quot;bin-days.json&quot;);

function loadBinDays() {
  const raw = readFileSync(BIN_DAYS_PATH, &quot;utf-8&quot;);
  return JSON.parse(raw);
}</code></pre><p>The shebang at the top makes the file directly executable. We&apos;re loading the JSON synchronously each time it&apos;s needed. Not the most efficient approach - but simple, and it means changes to your data file are picked up immediately.</p><h3 id="step-2add-helper-functions">Step 2 - Add Helper Functions</h3><p>Here&apos;s the business logic before we wire up the server:</p><pre><code class="language-javascript">function getUpcomingCollections(data, daysAhead = 30) {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const cutoff = new Date(today);
  cutoff.setDate(cutoff.getDate() + daysAhead);

  return data.collections.filter((c) =&gt; {
    const date = new Date(c.date);
    return date &gt;= today &amp;&amp; date &lt;= cutoff;
  });
}

function getNextCollection(data, binType) {
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  const upcoming = data.collections
    .filter((c) =&gt; {
      const date = new Date(c.date);
      if (date &lt; today) return false;
      if (binType) return c.types.includes(binType.toLowerCase());
      return true;
    })
    .sort((a, b) =&gt; new Date(a.date) - new Date(b.date));

  return upcoming[0] || null;
}</code></pre><p><code>getUpcomingCollections</code> returns everything within a date window. <code>getNextCollection</code> finds the soonest one, with an optional filter by bin type. Clean, testable functions. Keep your business logic separate from your server wiring.</p><h3 id="step-3initialise-the-server">Step 3 - Initialise the Server</h3><pre><code class="language-javascript">function createServer() {
  const server = new Server(
    { name: &quot;bin-collection&quot;, version: &quot;1.0.0&quot; },
    { capabilities: { tools: {} } }
  );
}</code></pre><p>The <code>capabilities</code> object tells clients what your server can do. Here we&apos;re saying: this server exposes tools. That&apos;s all your AI assistant needs to know upfront.</p><h3 id="step-4define-your-tools">Step 4 - Define Your Tools</h3><p>This is where MCP gets interesting. You&apos;re essentially writing a contract - here&apos;s what I can do, here&apos;s what you can ask me:</p><pre><code class="language-javascript">server.setRequestHandler(ListToolsRequestSchema, async () =&gt; ({
    tools: [
      {
        name: &quot;get_upcoming_collections&quot;,
        description:
          &quot;Returns all bin collections scheduled in the next N days (default 30). Useful for planning ahead or giving a full schedule overview.&quot;,
        inputSchema: {
          type: &quot;object&quot;,
          properties: {
            days_ahead: {
              type: &quot;number&quot;,
              description:
                &quot;How many days ahead to look for collections (default: 30)&quot;,
            },
          },
        },
      },
      {
        name: &quot;get_next_collection&quot;,
        description:
          &quot;Returns the next upcoming bin collection, optionally filtered by bin type (general, recycling, garden).&quot;,
        inputSchema: {
          type: &quot;object&quot;,
          properties: {
            bin_type: {
              type: &quot;string&quot;,
              description:
                &quot;Filter by bin type: &apos;general&apos;, &apos;recycling&apos;, or &apos;garden&apos;. Leave empty for any type.&quot;,
              enum: [&quot;general&quot;, &quot;recycling&quot;, &quot;garden&quot;],
            },
          },
        },
      },
      {
        name: &quot;get_bin_types&quot;,
        description:
          &quot;Returns details about each bin type - colour and what can be put in it.&quot;,
        inputSchema: {
          type: &quot;object&quot;,
          properties: {},
        },
      },
      },
    ],
  }));</code></pre><p>Your <code>inputSchema</code> uses JSON Schema to define parameters. The AI assistant reads this to understand what arguments to pass. Notice the <code>enum</code> on <code>bin_type</code> - that tells the AI assistant exactly which values are valid. Good schemas mean fewer errors.</p><h3 id="step-5handle-tool-calls">Step 5 - Handle Tool Calls</h3><p>When the AI assistant actually calls one of your tools, this handler runs:</p><pre><code class="language-javascript">server.setRequestHandler(CallToolRequestSchema, async (request) =&gt; {
    const { name, arguments: args } = request.params;
    const data = loadBinDays();

    if (name === &quot;get_upcoming_collections&quot;) {
      const daysAhead = args?.days_ahead ?? 30;
      const collections = getUpcomingCollections(data, daysAhead);

      if (collections.length === 0) {
        return {
          content: [
            {
              type: &quot;text&quot;,
              text: `No bin collections found in the next ${daysAhead} days.`,
            },
          ],
        };
      }

      const formatted = collections
        .map((c) =&gt; {
          const types = c.types.join(&quot;, &quot;);
          const note = c.notes ? ` (${c.notes})` : &quot;&quot;;
          return `&#x2022; ${c.date} (${c.day}): ${types}${note}`;
        })
        .join(&quot;\n&quot;);

      return {
        content: [
          {
            type: &quot;text&quot;,
            text: `Upcoming bin collections (next ${daysAhead} days):\n\n${formatted}`,
          },
        ],
      };
    }

    if (name === &quot;get_next_collection&quot;) {
      const binType = args?.bin_type || null;
      const next = getNextCollection(data, binType);

      if (!next) {
        const label = binType ? `${binType} bin` : &quot;any bin&quot;;
        return {
          content: [
            {
              type: &quot;text&quot;,
              text: `No upcoming collections found for ${label}.`,
            },
          ],
        };
      }

      const types = next.types.join(&quot;, &quot;);
      const note = next.notes ? `\nNote: ${next.notes}` : &quot;&quot;;
      const label = binType ? `Next ${binType} collection` : &quot;Next collection&quot;;

      return {
        content: [
          {
            type: &quot;text&quot;,
            text: `${label}: ${next.date} (${next.day})\nBins out: ${types}${note}`,
          },
        ],
      };
    }

    if (name === &quot;get_bin_types&quot;) {
      const lines = Object.entries(data.binTypes)
        .map(
          ([key, val]) =&gt;
            `&#x2022; ${key} (${val.colour} bin): ${val.description}`
        )
        .join(&quot;\n&quot;);

      return {
        content: [
          {
            type: &quot;text&quot;,
            text: `Bin types:\n\n${lines}`,
          },
        ],
      };
    }</code></pre><p>Each tool returns a <code>content</code> array with text. You can return images or embedded resources too - but text handles most cases just fine.</p><h3 id="step-6start-the-server">Step 6 - Start the Server</h3><pre><code class="language-javascript">const transport = new StdioServerTransport();
await server.connect(transport);
console.error(&quot;Bin collection MCP server running&quot;);</code></pre><p>We&apos;re using <code>stdio</code> transport - the server talks via standard input/output. That&apos;s why logging goes to <code>console.error</code>. Stdout is reserved for MCP messages.</p><h2 id="testing-it">Testing It</h2><p>Make the file executable, then run it:</p><pre><code class="language-bash">chmod +x index.js
node index.js</code></pre><p>You should see &quot;Bin collection MCP server running&quot; in the console. The server is now waiting for requests on stdin. You can also write unit tests against your helper functions directly - they&apos;re just plain JavaScript functions.</p><h2 id="connecting-to-visual-studio-code">Connecting to Visual Studio Code</h2><p>VS Code supports MCP servers through its settings. Open your <code>settings.json</code> (Cmd+Shift+P &#x2192; &quot;Open User Settings JSON&quot;) and add the following:</p><pre><code class="language-json">{
  &quot;mcp&quot;: {
    &quot;servers&quot;: {
      &quot;bin-collection&quot;: {
        &quot;type&quot;: &quot;stdio&quot;,
        &quot;command&quot;: &quot;node&quot;,
        &quot;args&quot;: [
          &quot;/absolute/path/to/bin-collection-mcp/index.js&quot;
        ]
      }
    }
  }
}</code></pre><p>Reload VS Code and your server should be picked up automatically. Then try asking Copilot Chat in agent mode:</p><ul><li>&quot;When are my bins next due out?&quot;</li><li>&quot;Show me all collections in the next 7 days&quot;</li><li>&quot;When is the next recycling collection?&quot;</li></ul><p>VS Code calls your tools automatically and gives you a natural language answer. It feels like magic the first time. &#x2728;</p><h2 id="the-pattern-to-remember">The Pattern to Remember</h2><p>MCP servers always follow the same structure. List your tools. Handle tool calls. Return content. That&apos;s it. Once you&apos;ve got this pattern in your head, you can build servers for anything - your local filesystem, an internal API, a Raspberry Pi sensor, whatever you like.</p><p>I took this a step further and deployed this code to an Azure App Service and this made it available from anywhere. The full code for this project is on <a href="https://github.com/deanhume/bin-days-mcp">GitHub</a>. Give it a try - and if you build something interesting with it, I&apos;d love to hear about it.</p>]]></content:encoded></item><item><title><![CDATA[Technical Skills Every Technical Program Manager Should Learn in 2026]]></title><description><![CDATA[The technical skills every TPM needs in 2026 from cloud basics to AI agents and governance. A practical guide to staying close to the tech.]]></description><link>https://deanhume.com/technical-skills-every-tpm-should-learn-in-2026/</link><guid isPermaLink="false">6a214333a5fdd000011796c3</guid><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Tue, 05 May 2026 14:12:37 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/technical-program-manager-skills.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/technical-program-manager-skills.jpg" alt="Technical Skills Every Technical Program Manager Should Learn in 2026"><p>Over the years, I&#x2019;ve learned that being a Technical Program Manager (TPM) is a bit of a strange hybrid role. You&#x2019;re expected to understand the tech well enough to contribute meaningfully, but you&#x2019;re not the one writing and deploying the code anymore. The best TPMs I&#x2019;ve worked with are the ones who stay close to the technology - curious enough to dig in, and technical enough to be useful.</p><p>The challenge? Tech keeps changing. Super Fast. And in 2026, AI has pretty much woven itself into every part of engineering. Systems are more distributed, products ship faster, and teams expect you to keep up.</p><p>A while back, I wrote about <a href="https://deanhume.com/staying-technical-as-a-technical-program-manager/">how to stay technical as a TPM</a> - the habits, the routines, the mindset. Tinkering with side projects, spinning up local environments, sitting in on design reviews.</p><p>Building on from that article, here are the technical skills I&#x2019;ve found most useful as a TPM in today&#x2019;s world. These aren&#x2019;t lofty certifications - just practical things that make your day-to-day easier.</p><h2 id="understand-the-system">Understand the system</h2><p>You don&apos;t need to design the architecture. But when someone pulls up a diagram, you should be able to follow along.</p><p>Start with the basics - APIs, queues, microservices, events, caching. Not so you can lecture anyone, but so the conversation makes sense. Then go one step further: spin up a local dev environment. As I mentioned in the previous post, doing this even once gives you a whole new appreciation for what engineers deal with daily. Dependency hell, environment variables, build steps - the works.</p><p>From there, learn enough cloud to be dangerous. Containers, serverless, CI/CD, what &quot;region&quot; and &quot;quota&quot; and &quot;scaling&quot; actually mean in practice. You don&apos;t need to run Kubernetes clusters. You just need the vocabulary to keep up - and the instinct to know when something sounds harder than it should be.</p><h2 id="follow-the-work">Follow the work</h2><p>This is where a lot of TPMs fall behind - and where the gap between good and great really opens up.</p><p>Learn how engineering actually works. Code reviews, branching strategies, feature flags, testing pipelines, linters, release cycles. Not so you can manage them - so you can plan around them. So when an engineer says &quot;<em>that&apos;ll need a flag</em>&quot; or &quot;<em>we are code complete</em>&quot;, you know exactly what they mean and why it matters.</p><p>Get comfortable reading logs and dashboards too. You don&apos;t need to be on-call. But poking around metrics when something goes wrong - understanding the shape of a problem before the postmortem - makes you genuinely useful in those moments rather than just present.</p><p>And read the docs. Design docs, API Docs, and postmortems. You don&apos;t need to absorb every detail. You just need to skim well enough to spot a risk before it becomes a crisis. It&apos;s a small habit that compounds fast.</p><h2 id="speak-the-language">Speak the language</h2><p>Here&apos;s the one that often gets overlooked: learn to read code.</p><p>Not write it - read it. Follow a pull request. Understand the logic. Tinker with a small script now and then. In the previous post I mentioned keeping a GitHub account and doing a bit of vibe coding just to stay sharp - this is exactly why. It keeps you connected to the craft in a way that&apos;s hard to replicate any other way. And it builds real empathy for engineers doing this every day, under pressure, at pace.</p><p>Pick up some basic data fluency while you&apos;re at it. Running a small query, calling an API, building a quick graph - these are small things that let you answer questions yourself instead of waiting on someone else every time.</p><p>And take AI seriously. Not as a buzzword - as a genuine shift in how engineering teams work. Understanding how your teams actually use it, how it affects velocity, how it changes estimation and debugging - that&apos;s table stakes now. Personally, I&apos;ve found it invaluable when getting up to speed on a new codebase. </p><h2 id="the-2026-layer-ai-is-now-part-of-the-system-youre-managing">The 2026 layer: AI is now part of the system you&apos;re managing</h2><p>This is where I&apos;d add something that wasn&apos;t in my <a href="https://deanhume.com/staying-technical-as-a-technical-program-manager/">original post</a> - because it&apos;s genuinely new territory, and most TPMs haven&apos;t caught up yet.</p><p>AI is no longer just a productivity tool sitting alongside engineering. It&apos;s becoming part of the system itself. That changes what you need to know.</p><h3 id="understand-ai-agent-orchestration">Understand AI agent orchestration</h3><p>Teams aren&apos;t just using AI to write code anymore. They&apos;re running autonomous agents to handle whole chunks of work - prototyping, testing, triaging, even making decisions. Rather than one massive agent that attempts everything, orchestration coordinates multiple specialised agents, with each one handling what it does best. </p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/open-ai-technical-program-manager.jpg" class="kg-image" alt="Technical Skills Every Technical Program Manager Should Learn in 2026" loading="lazy" width="1920" height="1080" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/04/open-ai-technical-program-manager.jpg 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2026/04/open-ai-technical-program-manager.jpg 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1600/2026/04/open-ai-technical-program-manager.jpg 1600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/open-ai-technical-program-manager.jpg 1920w" sizes="(min-width: 720px) 720px"></figure><p>As a TPM, you need to understand how these pipelines are structured, where they can fail, and how to think about estimating work when part of the team is an agent.<a href="https://www.gartner.com/en/newsroom/press-releases/2025-08-26-gartner-predicts-40-percent-of-enterprise-apps-will-feature-task-specific-ai-agents-by-2026-up-from-less-than-5-percent-in-2025"> Gartner forecasts</a> that 40% of enterprises will have embedded AI agents by the end of 2026 - this is not a future problem. It&apos;s a now problem. </p><h3 id="know-what-mcp-is">Know what MCP is</h3><p><a href="https://modelcontextprotocol.io/docs/getting-started/intro">Model Context Protocol</a> has quietly become one of the most important standards in AI engineering right now. It&apos;s an open-source standard for connecting AI applications to external systems - data sources, tools, workflows - enabling them to access key information and perform tasks. Think of it like a USB-C port for AI. You don&apos;t need to build one. But knowing what it is, why teams reach for it, and how it fits into a broader agent architecture puts you ahead of most TPMs right now. <a href="https://cloud.google.com/discover/what-is-model-context-protocol">Google&apos;s explainer</a> is a good place to start. </p><h3 id="rethink-how-you-estimate">Rethink how you estimate</h3><p>AI-assisted teams don&apos;t move at the same pace as traditional engineering teams - and the risk profile is different too. Features that used to take two weeks might take two days. But new failure modes appear: hallucinations, evals regressing, model updates breaking behaviour in subtle ways. Your planning instincts need updating. What does a sprint look like when part of the work is being done by an agent? What does &quot;done&quot; mean when the output is probabilistic? These are questions worth sitting with.</p><h3 id="get-a-basic-handle-on-ai-governance">Get a basic handle on AI governance</h3><p>This one is landing on TPM plates fast - especially if you&apos;re in a regulated industry or shipping to enterprise customers. In the EU, the AI Act is already phasing in requirements, with broader applicability arriving in 2026. In the US, state-level laws are also emerging. </p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/ai-governanace-technical-program-manager.jpg" class="kg-image" alt="Technical Skills Every Technical Program Manager Should Learn in 2026" loading="lazy" width="1920" height="1280" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/04/ai-governanace-technical-program-manager.jpg 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2026/04/ai-governanace-technical-program-manager.jpg 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1600/2026/04/ai-governanace-technical-program-manager.jpg 1600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/ai-governanace-technical-program-manager.jpg 1920w" sizes="(min-width: 720px) 720px"></figure><p>You don&apos;t need to be a compliance expert, but understanding concepts like model versioning, audit trails, bias testing, and what &quot;high-risk AI&quot; means in a regulatory context helps you ask the right questions before they become blockers. <a href="https://www.onetrust.com/blog/responsible-ai-in-2026-a-3-step-guide-for-governance-that-scales/">This overview from OneTrust</a> is a useful starting point. </p><h2 id="one-last-thought">One last thought</h2><p>What&apos;s helped me as a TPM is keeping the habits small. Read the PR. Skim the doc. Run the build. Poke the logs. None of it takes long, but it compounds in a way that&apos;s hard to explain until you feel it - that moment where you&apos;re in a conversation and you actually know what&apos;s going on, rather than pattern-matching your way through it.</p><p>The AI layer makes this a bit more pressing right now, because the ground is shifting quickly. But the underlying idea is the same one from my <a href="https://deanhume.com/staying-technical-as-a-technical-program-manager/">previous post</a> - staying technical isn&apos;t about being the smartest person in the room. It&apos;s just about staying curious enough to be useful.&#x1F44D;</p>]]></content:encoded></item><item><title><![CDATA[Supercharge Your Clipboard with PowerToys Advanced Paste 📋]]></title><description><![CDATA[PowerToys Advanced Paste transforms your clipboard - paste as plain text, Markdown, JSON, extract text from images, and reformat content with AI.]]></description><link>https://deanhume.com/powertoys-advanced-paste-clipboard-supercharged/</link><guid isPermaLink="false">6a214333a5fdd000011796c5</guid><category><![CDATA[Technical Program Manager]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Tue, 28 Apr 2026 16:15:22 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/powertoys-advanced-ai-paste-2.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/powertoys-advanced-ai-paste-2.png" alt="Supercharge Your Clipboard with PowerToys Advanced Paste &#x1F4CB;"><p>A few weeks ago, I wrote about how <a href="https://deanhume.com/how-i-use-powertoys-workspaces-to-switch-contexts-in-two-clicks/">PowerToys Workspaces</a> helped me stop wasting time arranging windows every morning. Since then, a few people asked what else I use from the PowerToys suite. So today I want to talk about another tool that&apos;s quietly become part of my daily workflow - <strong>Advanced Paste</strong>.</p><p>Have you ever copied something from a webpage, pasted it into a doc, and then spent the next two minutes stripping out all the weird formatting? Bold text that shouldn&apos;t be bold, font sizes that don&apos;t match, colours that make no sense. Yeah. It&apos;s one of those small annoyances that adds up over time.</p><p>Advanced Paste fixes that - and a whole lot more.</p><h2 id="what-is-advanced-paste">What is Advanced Paste?</h2><p>Advanced Paste is a clipboard management tool built into PowerToys. The idea is simple: instead of just pasting whatever is on your clipboard as-is, you get to choose <em>how</em> it gets pasted. Plain text, Markdown, JSON, even as a file - all from a single keyboard shortcut.</p><p>Think of it like having a smart clipboard that knows what you actually need, rather than blindly dumping whatever formatting came along for the ride.</p><p>You open it with <code>Win + Shift + V</code> by default, and a clean little window appears with your paste options. That&apos;s it.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/image-2.png" class="kg-image" alt="Supercharge Your Clipboard with PowerToys Advanced Paste &#x1F4CB;" loading="lazy" width="772" height="724" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/04/image-2.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/image-2.png 772w" sizes="(min-width: 720px) 720px"><figcaption>PowerToys Advanced Paste - AI</figcaption></figure><h2 id="paste-as-plain-text">Paste as Plain Text</h2><p>This is the one I use most. Copy something from a browser or a rich text editor, hit the shortcut, and paste it as pure unformatted text. No bold. No headings. No leftover HTML weirdness.</p><p>It sounds almost too simple - but honestly, that&apos;s the point. Removing that friction from something you do dozens of times a day is worth a lot more than it sounds.</p><p>You can also set a direct keyboard shortcut for this so you don&apos;t even need to open the Advanced Paste window. Just copy, hit your shortcut, and you&apos;re done.</p><h2 id="paste-as-markdown">Paste as Markdown</h2><p>Got some HTML on your clipboard and need it in Markdown? Advanced Paste handles that conversion for you automatically. So something like:</p><pre><code>&lt;b&gt;Paste&lt;/b&gt; &lt;i&gt;as&lt;/i&gt; &lt;a href=&quot;...&quot;&gt;Markdown&lt;/a&gt;</code></pre><p>Becomes:</p><pre><code>**Paste** *as* [Markdown](...)</code></pre><p>If you write documentation, blog posts, or work in any tool that uses Markdown - this one will save you a surprising amount of time.</p><h2 id="paste-as-json">Paste as JSON</h2><p>Similar idea, but for structured data. If you&apos;ve got XML or some other structured text on your clipboard and need it in JSON format, Advanced Paste converts it on the fly. No manual reformatting, no copy-pasting into a converter website.</p><p>As someone who spends time working across different tools and formats, this one is genuinely useful.</p><h2 id="paste-as-a-file">Paste as a File</h2><p>This is where it gets a bit more interesting. Advanced Paste can take what&apos;s on your clipboard and paste it directly as a file - a <code>.txt</code> file, an <code>.html</code> file, or even a <code>.png</code> file for images.</p><p>The <code>.html</code> option is particularly handy if you&apos;re copying a section of a webpage (links, formatted text, images and all) and want to save it as a proper file rather than lose all that structure.</p><h2 id="image-to-text-ocr">Image to Text (OCR)</h2><p>Have you ever needed to copy text from a screenshot or an image? Normally that means manually typing it out, which is tedious at best.</p><p>Advanced Paste has a built-in OCR feature that pulls the text straight out of an image on your clipboard. It all runs locally on your machine - no data being sent anywhere. Just copy the image, open Advanced Paste, hit &quot;Image to Text&quot;, and the extracted text is ready to paste.</p><p>I&apos;ve used this more times than I expected - screenshots from Teams calls, error messages in images, text in PDFs that won&apos;t let you select it. It just works.</p><h2 id="paste-with-ai-%F0%9F%A4%96">Paste with AI &#x1F916;</h2><p>This is the most powerful feature in the set, and it&apos;s worth calling out separately.</p><p>If you configure an AI model provider - OpenAI, Azure OpenAI, Mistral, Google, or a local model via Ollama or Foundry Local - you unlock a whole new level of clipboard transformation. You can type a prompt and the AI will reformat or rewrite your clipboard content based on your instructions.</p><p>Some examples of what you can do:</p><ul><li>Summarise a long block of text</li><li>Translate text into another language</li><li>Rewrite something in a more professional tone</li><li>Generate code from a description</li><li>Reformat data into a specific structure</li></ul><p>The local model options (Ollama, Foundry Local) are worth a look if you&apos;d rather keep everything on your machine and not worry about API costs or data leaving your device.</p><p>You can also save your most-used prompts as <strong>custom actions</strong> and assign them keyboard shortcuts. So if you regularly need to &quot;translate to French and clean up formatting&quot;, you can do that in one keypress without even opening the window.</p><h2 id="setting-it-up">Setting It Up</h2><p>If you haven&apos;t already got PowerToys installed, grab it from the <a href="https://aka.ms/getPowertoys">Microsoft Store</a> or via WinGet. Then:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/image.png" class="kg-image" alt="Supercharge Your Clipboard with PowerToys Advanced Paste &#x1F4CB;" loading="lazy" width="2000" height="1317" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/04/image.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2026/04/image.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1600/2026/04/image.png 1600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/image.png 2034w" sizes="(min-width: 720px) 720px"><figcaption>Setting up Powertoys Advanced Paste</figcaption></figure><ol><li>Open <strong>PowerToys Settings</strong> and navigate to <strong>Advanced Paste</strong>.</li><li>Enable <strong>Paste with AI.</strong></li><li>I added Google&apos;s Gemini as a Model Provider. You&apos;ll need to enter an API key, but that is easily done depending on your AI Model Provider.</li></ol><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/image-1.png" class="kg-image" alt="Supercharge Your Clipboard with PowerToys Advanced Paste &#x1F4CB;" loading="lazy" width="1543" height="447" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/04/image-1.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2026/04/image-1.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/04/image-1.png 1543w" sizes="(min-width: 720px) 720px"><figcaption>Add an AI model provider</figcaption></figure><p>Once you&apos;ve completed all those steps, it should be working as expected. The default shortcut (<code>Win + Shift + V</code>) is easy to remember - think of it as an upgraded version of the regular paste you already know.</p><h2 id="a-small-tool-with-a-big-impact">A Small Tool with a Big Impact</h2><p>I&apos;ll be honest - I didn&apos;t expect Advanced Paste to become something I reach for every day. But like Workspaces, it&apos;s one of those tools that solves a real friction point so well that you quickly forget what life was like without it.</p><p>If you&apos;re already using PowerToys, enable it today. If you&apos;re not using PowerToys yet - what are you waiting for? &#x1F604;</p><p>The <a href="https://learn.microsoft.com/en-us/windows/powertoys/advanced-paste">Advanced Paste docs</a> are a great place to explore everything it can do.</p>]]></content:encoded></item><item><title><![CDATA[How I Use PowerToys Workspaces to Switch Contexts in Two Clicks 🫰]]></title><description><![CDATA[Tired of arranging windows every morning? PowerToys Workspaces launches your ideal Windows desktop management setup with two clicks 🫰.]]></description><link>https://deanhume.com/how-i-use-powertoys-workspaces-to-switch-contexts-in-two-clicks/</link><guid isPermaLink="false">6a214333a5fdd000011796c4</guid><category><![CDATA[Technical Program Manager]]></category><category><![CDATA[Writing]]></category><category><![CDATA[Powertoys]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Thu, 12 Mar 2026 14:32:13 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/03/powertoys-workspaces.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/03/powertoys-workspaces.png" alt="How I Use PowerToys Workspaces to Switch Contexts in Two Clicks &#x1FAF0;"><p>Have you ever sat down at your computer, ready to get into a flow state, and then spent the first ten minutes just <em>arranging windows</em>? Dragging your editor here, your browser there, nudging Slack into a corner - only to close it all two hours later, and start from scratch the next day? Yeah. Me too.</p><p>I&apos;ve been a big fan of <a href="https://learn.microsoft.com/en-us/windows/powertoys/">Microsoft PowerToys</a> for a while now. It&apos;s one of those tools that quietly makes your Windows experience feel a lot more intentional. There are a bunch of great utilities in there - but recently I&apos;ve been leaning heavily on one in particular: <strong>PowerToys Workspaces</strong>.</p><h2 id="what-is-powertoys-workspaces">What is PowerToys Workspaces?</h2><p>Workspaces is a Windows desktop management utility that lets you capture your ideal window layout, which apps are open, where they&apos;re positioned, what size they are, and then launch that entire setup with a single click.</p><p>Think of it like a saved game state, but for your desktop.</p><p>You could have a &quot;Coding&quot; workspace that opens VS Code to a specific project, a browser on the side, and Terminal ready to go. Or an &quot;Email &amp; Admin&quot; workspace that brings up Outlook, your calendar, and a notes app. Whatever your workflow looks like - you set it up once, and Workspaces handles the rest every time.</p><h2 id="setting-up-your-first-workspace">Setting Up Your First Workspace</h2><p>If you haven&apos;t already got <a href="https://learn.microsoft.com/en-us/windows/powertoys/">PowerToys </a>installed, you can grab it from the <a href="https://aka.ms/getPowertoys">Microsoft Store</a> or via <a href="https://winget.run/pkg/Microsoft/PowerToys">WinGet</a>. Once it&apos;s running, head into the PowerToys Settings and enable <strong>Workspaces</strong>.</p><p>To create your first workspace:</p><ol><li>Hit <code>Win + Ctrl + &apos; </code> to open the Workspaces editor (or go to Settings and click &quot;Launch editor&quot;).</li><li>Click <strong>&quot;+ Create workspace&quot;.</strong></li><li>You&apos;ll enter Capture mode - arrange your apps exactly how you want them.</li><li>When you&apos;re happy hit <strong>&quot;Capture&quot;.</strong></li><li>Give it a name, tweak anything you need to, and save it.</li></ol><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/03/image.png" class="kg-image" alt="How I Use PowerToys Workspaces to Switch Contexts in Two Clicks &#x1FAF0;" loading="lazy" width="1417" height="567" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/03/image.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2026/03/image.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/03/image.png 1417w" sizes="(min-width: 720px) 720px"></figure><p>That&apos;s it. When it you launch it, it will open those apps exactly as you&apos;d like them. You can create as many different workspaces as you&apos;d like depending on your needs.</p><h2 id="the-part-that-surprised-me">The Part That Surprised Me</h2><p>What I didn&apos;t expect was how useful the <strong>CLI arguments</strong> feature would be. When you&apos;re in the editor, each app has a little dropdown where you can pass command line arguments. So for VS Code, I can point it straight to a specific project folder on launch. For Edge, I can give it a comma-separated list of URLs and it&apos;ll open all those tabs automatically.</p><p>It sounds like a small thing - but removing those few extra clicks every time you start a session adds up surprisingly quickly.</p><h2 id="desktop-shortcut">Desktop Shortcut</h2><p>Within the workspace editor, you can select the option to <strong>Create a desktop shortcut</strong> which creates a handy shortcut on your desktop.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/03/image-1.png" class="kg-image" alt="How I Use PowerToys Workspaces to Switch Contexts in Two Clicks &#x1FAF0;" loading="lazy" width="1662" height="1234" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2026/03/image-1.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2026/03/image-1.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1600/2026/03/image-1.png 1600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/03/image-1.png 1662w" sizes="(min-width: 720px) 720px"><figcaption>A shortcut on my desktop made by PowerToys Workspaces</figcaption></figure><p>I find this super handy for when I start my PC first thing in the morning. Just a double click and your workspace is loaded and ready to go. If you prefer, you could pin it to your taskbar if you really want it front and centre.</p><h2 id="a-few-things-worth-knowing">A Few Things Worth Knowing</h2><p>When a workspace launches, you&apos;ll briefly see windows jumping around the screen as PowerToys repositions them. There&apos;s a status dialog that pops up to show you what&apos;s loading, which helps - but it can look a bit chaotic the first time you see it.</p><p>Also worth noting: if you&apos;ve got apps snapped using Windows&apos; built-in snap feature, Workspaces won&apos;t preserve that snapped state. It uses its own positioning engine under the hood. Not a dealbreaker, but something to be aware of when you&apos;re setting things up.</p><h2 id="how-i-use-it-day-to-day">How I Use It Day to Day</h2><p>If you&apos;ve read my recent post on <a href="https://deanhume.com/staying-technical-as-a-technical-program-manager/">staying technical as a TPM</a>, you&apos;ll know I&apos;m always looking for ways to reduce friction and stay in a coding mindset. Workspaces has become one of those small but surprisingly impactful habits.</p><p>I&apos;ve ended up with two workspaces that cover probably 90% of my week:</p><ul><li><strong>Coding</strong> - VS Code, Terminal, and a browser window with the relevant docs.</li><li><strong>Daily</strong> - Outlook, Teams, Slack and a browser.</li></ul><p>Switching between them takes a double click. It sounds almost too simple - but honestly, that&apos;s the point. The friction of setting up your environment is just gone.</p><hr><p>If you haven&apos;t explored PowerToys yet, it&apos;s one of the best free productivity tools for Windows and the <a href="https://learn.microsoft.com/en-us/windows/powertoys/workspaces">Workspaces docs</a> are a great place to start.</p>]]></content:encoded></item><item><title><![CDATA[Staying Technical as a Technical Program Manager]]></title><description><![CDATA[Learn practical ways Technical Program Managers can stay technical - daily habits, internal engineering practices, and hands-on routines that keep skills sharp while leading programs at scale.]]></description><link>https://deanhume.com/staying-technical-as-a-technical-program-manager/</link><guid isPermaLink="false">6a214333a5fdd000011796bf</guid><category><![CDATA[Game Development]]></category><category><![CDATA[Technical Leadership]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Wed, 18 Feb 2026 11:39:39 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/02/coding.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2026/02/coding.jpg" alt="Staying Technical as a Technical Program Manager"><p>In my career, I&apos;ve been many things - a Support Engineer, a Software Engineer and an Engineering Manager. These days, I&apos;ve settled into a Technical Program Manager role (which I am thoroughly enjoying).</p><p>As a Technical Program Manager (or TPM), you&apos;re often expected to wear multiple hats - strategic planner, cross-functional communicator, and technical problem solver. But how do you stay technically sharp while managing programs at scale?</p><p>The role of a TPM is different from a program manager, you <em>are </em>expected to be <strong>technical</strong>. Here are some of the daily (and weekly) practices that help me keep my technical skills sharp.</p><h3 id="writing-the-occasional-code">Writing the occasional code</h3><p>Early in my career, I was a software engineer. These days, my role doesn&#x2019;t require me to write code, but I still have the urge to explore and tinker. I&#x2019;ve built a bunch of small tools and love updating my <a href="https://github.com/deanhume/">GitHub account</a>. I&#x2019;m also big into AI right now (as is everyone &#x1F602;) and have been doing a lot of &#x201C;vibe coding&#x201D; just to learn something new or scratch an itch. Even writing a few lines here and there keeps your brain tuned to real engineering challenges.</p><h3 id="writing-technical-content">Writing Technical Content</h3><p>Blogging has been a powerful tool for learning. Whether it&#x2019;s writing about <a href="https://deanhume.com/azure-hybrid-and-embedded-text-to-speech/">Azure Text-to-Speech</a>, troubleshooting <a href="https://deanhume.com/tag/ghost-tag/">Ghost CMS</a>, or <a href="https://deanhume.com/tag/game-development/">Game Development</a>, the act of explaining technical concepts forces clarity and deepens my understanding. It also helps others, which is always a bonus.</p><h3 id="following-tech-blogs">Following tech blogs</h3><p>One of the most useful habits I&#x2019;ve developed is staying up to date with what&#x2019;s happening across the tech world. Even if it&#x2019;s not tech I use every day, it&#x2019;s important to see how others are solving problems. You never know when a piece of tech or a pattern will end up being useful later.</p><h3 id="insert-your-hobby-here">&lt;Insert your hobby here&gt;</h3><p>Outside of work, I love brewing my own beer. I even have a <a href="https://humebrew.com/">blog</a> dedicated to it! One of the best ways to learn something new is to combine your hobby with tech. Recently, I built a <a href="https://deanhume.com/using-a-raspberry-pi-to-track-the-progress-of-your-homebrew/">fermentation tracker using a Raspberry Pi</a> and temperature sensors. It keeps me close to hardware and scripting, and it&#x2019;s a fun, low-stakes way to apply engineering principles.</p><p>Balancing technical depth with program management isn&#x2019;t about doing everything - it&#x2019;s about staying curious, being useful, and knowing when to dive in. Whether it&#x2019;s through side projects, internal tools, or collaborative problem&#x2011;solving, staying hands&#x2011;on helps me lead with more confidence and empathy. It might all sound obvious, but the real impact has come from deliberately doing these things, day after day, with intention.</p><h2 id="how-to-stay-close-to-your-companys-tech">How to Stay Close to Your Company&apos;s Tech</h2><p>While it&#x2019;s important to understand what&#x2019;s happening across the wider tech landscape, it&#x2019;s just as important (if not more so) to stay close to the technology <em>inside</em> your own organisation. Knowing how your systems work, how your teams build, and where the real challenges live helps you make better decisions and builds trust with engineering.</p><p>Here are some of the things I try to do in my role to stay close to the tech:</p><p><strong>Read Your Company&#x2019;s Engineering Docs</strong><br>Most teams have design docs, architecture overviews, or RFCs floating around. Even skimming them gives you a deeper understanding of how things fit together and why certain decisions were made.</p><p><strong>Sit In on Architecture or Design Reviews</strong><br>You don&#x2019;t necessarily need to weigh in on every detail, but observing the discussions helps you understand trade&#x2011;offs, constraints, and how engineers think about the system.</p><p><strong>Spin Up a Local Dev Environment</strong><br>Even if you&#x2019;re not coding every day, setting up and running the services your teams build helps you understand the tooling, dependencies, and workflows they use.</p><p><strong>Use the Product Like a Real User</strong><br>If your company makes consumer or developer-facing tools, actually use them end-to-end. It sharpens your instincts for what &#x201C;good&#x201D; feels like and what&#x2019;s actually painful.</p><h3 id="why-staying-technical-matters-for-tpms"><strong>Why Staying Technical Matters for TPMs</strong></h3><ul><li><strong>Credibility with Engineering Teams</strong>: Understanding the tech stack helps you ask the right questions and gain trust. This is especially important in a gaming role! If engineers don&apos;t believe you are credible and have the background knowledge, it becomes much harder to lead effectively.</li><li><strong>Better Decision-Making</strong>: Technical fluency helps you assess trade-offs, spot risks, and understand what &#x201C;good&#x201D; looks like.</li><li><strong>Faster Problem Resolution</strong>: When you understand the underlying systems, you can catch bottlenecks or misalignments earlier.</li></ul><h3 id="time-management">Time Management</h3><p>So how do I fit all this learning time in? </p><p>In a busy work environment, it can be tricky to stay current while balancing program responsibilities. I also have a family, and being a present dad is non-negotiable.</p><p>One thing that has helped tremendously is <em>deliberately</em> blocking time for learning or tinkering - without compromising program delivery. For example, I use <a href="https://feedly.com/">Feedly </a>to read all the RSS feeds I subscribe to. First thing in the morning with a cup of coffee, I catch up on what&apos;s happening in our industry.</p><p>At Microsoft, we also have<a href="https://techcommunity.microsoft.com/blog/mvp-blog/microsoft-global-hackathon-2025-mvps-driving-innovation-across-communities/4442513"> company-wide hackathons</a>, and they&#x2019;re a fantastic opportunity to try something new, collaborate with people you&#x2019;ve never worked with, and solve real technical challenges. I try to participate whenever I can.</p><p>Balancing learning and work isn&#x2019;t easy, but scheduling time for both makes it achievable.</p><h3 id="conclusion">Conclusion</h3><p>I hope you&#x2019;ve found this post useful. These are the things that have personally helped me stay technically sharp - they might not all be right for you, but it&#x2019;s a good place to start. For me, it ultimately comes down to staying curious, being deliberate, and consistently carving out time to keep learning.</p><p>If you&apos;re a Technical Program Manager, how do you stay technical? I&#x2019;d love to hear your approach.</p>]]></content:encoded></item><item><title><![CDATA[Backup Buddy: A Simple Tool to Backup Your Website Content]]></title><description><![CDATA[<p>Have you ever wanted to create a backup of your website or blog? Maybe you&apos;re migrating to a new platform, or perhaps you just want a portable archive of your content that you can read offline. I&apos;ve previously <a href="https://deanhume.com/ghost-blog-fixing-no-space-left-on-device-issue">experienced</a> <a href="https://deanhume.com/amazon-aws-ec2-ghost-cms-setup/">issues </a>with this site going down and</p>]]></description><link>https://deanhume.com/backup-buddy-a-simple-tool-to-backup-your-website-content/</link><guid isPermaLink="false">6a214333a5fdd000011796c2</guid><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Mon, 01 Dec 2025 12:33:06 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/11/logo.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/11/logo.png" alt="Backup Buddy: A Simple Tool to Backup Your Website Content"><p>Have you ever wanted to create a backup of your website or blog? Maybe you&apos;re migrating to a new platform, or perhaps you just want a portable archive of your content that you can read offline. I&apos;ve previously <a href="https://deanhume.com/ghost-blog-fixing-no-space-left-on-device-issue">experienced</a> <a href="https://deanhume.com/amazon-aws-ec2-ghost-cms-setup/">issues </a>with this site going down and I wanted to make sure that I wasn&apos;t stuck without a backup of all my posts. </p><p>That&apos;s exactly why I built <strong>Backup Buddy</strong> - a command-line tool that turns any website into a collection of clean, readable Markdown files with all the images (and videos) preserved.</p><h2 id="what-does-backup-buddy-do">What Does Backup Buddy Do?</h2><p>Backup Buddy takes a website&apos;s sitemap and automatically downloads every page, converting the HTML into clean Markdown format while saving all the images (and videos) locally. Think of it as creating a time capsule of your website that you can read, search, and store anywhere - no internet required.</p><p>The best part? You end up with content in Markdown format, which means you can:</p><ul><li>Read it in any text editor</li><li>Import it into a static site generator like Hugo or Jekyll</li><li>Search through it with standard text tools</li><li>Version control it with Git</li><li>Share it without worrying about broken links or missing images</li></ul><h2 id="how-to-use-it">How to Use It</h2><p>Using Backup Buddy is straightforward. You just need to know your website&apos;s sitemap URL (usually something like <code>https://yoursite.com/sitemap.xml</code>) and run a single command:</p><pre><code class="language-bash">backup-buddy https://yoursite.com/sitemap.xml
</code></pre><p>That&apos;s it! The tool will:</p><ol><li>Download your sitemap</li><li>Find all the URLs</li><li>Download each page</li><li>Convert the HTML to Markdown</li><li>Save all the images</li><li>Organize everything into neat folders</li></ol><h3 id="getting-started">Getting Started</h3><p>Head over to the Git repo and <a href="https://github.com/deanhume/backup-buddy/releases/tag/v1">download the latest release</a>.</p><h2 id="what-you-get">What You Get</h2><p>After running Backup Buddy, you&apos;ll find an <code>output</code> folder with all your content organized like this:</p><pre><code>output/
&#x251C;&#x2500;&#x2500; 1_my-first-post/
&#x2502;   &#x251C;&#x2500;&#x2500; my-first-post.md
&#x2502;   &#x251C;&#x2500;&#x2500; metadata.txt
&#x2502;   &#x251C;&#x2500;&#x2500; images/
&#x2502;   &#x2502;   &#x251C;&#x2500;&#x2500; header-image.jpg
&#x2502;   &#x2502;   &#x2514;&#x2500;&#x2500; diagram.png
&#x2502;   &#x2514;&#x2500;&#x2500; videos/
&#x2502;       &#x2514;&#x2500;&#x2500; demo.mp4
&#x251C;&#x2500;&#x2500; 2_another-post/
&#x2502;   &#x251C;&#x2500;&#x2500; another-post.md
&#x2502;   &#x251C;&#x2500;&#x2500; metadata.txt
&#x2502;   &#x2514;&#x2500;&#x2500; images/
&#x2502;       &#x2514;&#x2500;&#x2500; photo.jpg
&#x2514;&#x2500;&#x2500; ...
</code></pre><p>Each page gets its own folder containing:</p><ul><li>The Markdown version of your content</li><li>A metadata file with the original URL and backup date</li><li>An images folder with all pictures from that page</li><li>A videos folder with any videos from that page</li></ul><h2 id="why-i-built-this">Why I Built This</h2><p>I&apos;ve been blogging for years, and I&apos;ve migrated platforms more than once. Each time, I worried about losing content, breaking image links, or dealing with complicated export tools. I wanted something simple that would give me a clean backup I could actually use.</p><p>The Markdown format is perfect because it&apos;s:</p><ul><li><strong>Human-readable</strong> - You can open it in any text editor</li><li><strong>Future-proof</strong> - It&apos;s just text, so it&apos;ll work forever</li><li><strong>Portable</strong> - Easy to import into almost any blogging platform or static site generator</li><li><strong>Git-friendly</strong> - You can track changes and collaborate on content</li></ul><h2 id="performance-features">Performance Features</h2><p>One thing I made sure to include was speed. Backup Buddy processes multiple pages at once (10 by default), so even large websites with hundreds of pages get backed up quickly. You&apos;ll see real-time progress as it works:</p><pre><code>[1/150] Processing: https://example.com/first-post
[2/150] Processing: https://example.com/second-post
  &#x2713; Saved to: output/1_first-post
  &#x2713; Saved to: output/2_second-post
[3/150] Processing: https://example.com/third-post
...
</code></pre><h2 id="a-few-things-to-know">A Few Things to Know</h2><p>While Backup Buddy handles most websites well, there are a few limitations:</p><ul><li>It only backs up URLs in your sitemap (which is usually everything important)</li><li>Content that&apos;s loaded with JavaScript after the page loads won&apos;t be captured</li><li>Images need to be accessible when you run the backup</li><li>Very large sites might need you to adjust the parallel processing settings</li></ul><h2 id="try-it-out">Try It Out</h2><p>If you&apos;ve been looking for a simple way to backup your website or blog, give Backup Buddy a try. It&apos;s open source (MIT License), so you can use it freely and even modify it for your needs.</p><p>Check it out on GitHub: <a href="https://github.com/deanhume/backup-buddy">github.com/deanhume/backup-buddy</a></p><p>Have questions or suggestions? Feel free to open an issue or submit a pull request. Happy archiving!</p>]]></content:encoded></item><item><title><![CDATA[From Crash to Resolution: A Practical Guide for Xbox & Windows Developers]]></title><description><![CDATA[<p>If you&#x2019;ve ever had an app crash at the worst possible moment, you&#x2019;ll know how frustrating it can be - both for developers and players. Recently, I wrote an article for the Microsoft Developer blog that dives deep into this exact challenge: <strong>how to diagnose and resolve</strong></p>]]></description><link>https://deanhume.com/from-crash-to-resolution-a-practical-guide-for-xbox-windows-developers/</link><guid isPermaLink="false">6a214333a5fdd000011796c1</guid><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Mon, 24 Nov 2025 10:08:19 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/11/XboxWindowsCrash_HERO.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/11/XboxWindowsCrash_HERO.png" alt="From Crash to Resolution: A Practical Guide for Xbox &amp; Windows Developers"><p>If you&#x2019;ve ever had an app crash at the worst possible moment, you&#x2019;ll know how frustrating it can be - both for developers and players. Recently, I wrote an article for the Microsoft Developer blog that dives deep into this exact challenge: <strong>how to diagnose and resolve crashes effectively on Xbox and Windows platforms</strong>.</p><p>In the post, I share practical tips, tools, and workflows that can help you move quickly and efficiently. Whether you&#x2019;re building games or apps, these techniques can improve the overall experience for your users.</p><p>&#x1F449; <a href="https://developer.microsoft.com/en-us/games/articles/2025/11/from-crash-to-resolution-practical-guide-for-xbox-windows-developers/">https://developer.microsoft.com/en-us/games/articles/2025/11/from-crash-to-resolution-practical-guide-for-xbox-windows-developers/</a></p><p>If you&#x2019;ve got any thoughts or tips of your own, I&#x2019;d love to hear them &#x2014; drop a comment or reach out!</p>]]></content:encoded></item><item><title><![CDATA[A Modern Guide to Using OAuth 2.0 with C# and Visual Studio Code]]></title><description><![CDATA[Master OAuth 2.0 in .NET 9.0! This tutorial covers using OAuth 2 to acquire tokens and access Microsoft Graph user data.]]></description><link>https://deanhume.com/a-modern-guide-to-using-oauth-2-0-with-c/</link><guid isPermaLink="false">6a214333a5fdd000011796c0</guid><category><![CDATA[Oauth]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Mon, 03 Nov 2025 10:45:24 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1634979149798-e9a118734e93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDl8fGtleXxlbnwwfHx8fDE3ODEyNTc0MTJ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1634979149798-e9a118734e93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDl8fGtleXxlbnwwfHx8fDE3ODEyNTc0MTJ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="A Modern Guide to Using OAuth 2.0 with C# and Visual Studio Code"><p>Many years ago, I wrote an article on the <a href="https://deanhume.com/a-simple-guide-to-using-oauth-with-c/">basics of OAuth</a> and while it was helpful, it is now quite outdated. A <strong>lot </strong>has changed in the past 14 years! OAuth 2.0 has become the de facto standard for securing APIs and authorizing users in modern applications. </p><p>The older article I wrote used a SoundCloud example and older libraries, this updated guide walks you through implementing OAuth 2.0 in C# using Visual Studio Code, with modern libraries and best practices. </p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/11/Oauth_logo.svg.png" class="kg-image" alt="A Modern Guide to Using OAuth 2.0 with C# and Visual Studio Code" loading="lazy" width="1196" height="1199" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2025/11/Oauth_logo.svg.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2025/11/Oauth_logo.svg.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/11/Oauth_logo.svg.png 1196w" sizes="(min-width: 720px) 720px"></figure><p>Whether you&apos;re building a console app, a web API, or integrating with services like Google or Microsoft Graph, this guide will help you get started with a clean and simple implementation.</p><hr><h3 id="prerequisites">Prerequisites</h3><p>Before we dive in, make sure you have the following:</p><ul><li><a href="https://dotnet.microsoft.com/en-us/download">https://dotnet.microsoft.com/en-us/download</a></li><li><a href="https://code.visualstudio.com/">https://code.visualstudio.com/</a></li><li>Basic understanding of HTTP and REST APIs</li><li>A registered OAuth 2.0 application (e.g., via Google, Microsoft, or a custom provider). </li></ul><p>Let&apos;s get started!</p><hr><h3 id="step-1-create-a-new-console-app">Step 1: Create a New Console App</h3><p>Open your terminal and run:</p><pre><code class="language-shell">dotnet new console -n OAuthDemo
cd OAuthDemo
</code></pre><hr><h3 id="step-2-install-required-packages">Step 2: Install Required Packages</h3><p>We&apos;ll use <code>Microsoft.Identity.Client</code> for handling OAuth flows:</p><pre><code class="language-shell">dotnet add package Microsoft.Identity.Client
dotnet add package Microsoft.Extensions.Configuration</code></pre><hr><h3 id="step-3-configure-your-oauth-settings">Step 3: Configure Your OAuth Settings</h3><p>Create a <code>appsettings.json</code> file to store your credentials:</p><pre><code class="language-json">{
  &quot;OAuth&quot;: {
    &quot;ClientId&quot;: &quot;your-client-id&quot;,
    &quot;TenantId&quot;: &quot;your-tenant-id&quot;,
    &quot;Authority&quot;: &quot;https://login.microsoftonline.com/your-tenant-id&quot;,
    &quot;Scopes&quot;: [ &quot;https://graph.microsoft.com/.default&quot; ],
    &quot;ClientSecret&quot;: &quot;your-client-secret&quot;
  }
}
</code></pre><blockquote>&#x1F50D; What is <code>tenant-id</code>? &gt;This is the unique identifier (GUID) for your Azure Active Directory (AAD) tenant. You can find it in the Azure portal under Azure Active Directory &gt; Overview. Alternatively, you can use your domain name (e.g., <code>contoso.onmicrosoft.com</code>) in place of the GUID. This might be different depending on your OAuth provider.</blockquote><hr><h3 id="step-4-authenticate-and-acquire-token">Step 4: Authenticate and Acquire Token</h3><p>Here&apos;s a simple example using MSAL to get an access token:</p><pre><code class="language-csharp">using Microsoft.Identity.Client;
using Microsoft.Extensions.Configuration;

var config = new ConfigurationBuilder()
    .AddJsonFile(&quot;appsettings.json&quot;)
    .Build();

var clientId = config[&quot;OAuth:ClientId&quot;];
var tenantId = config[&quot;OAuth:TenantId&quot;];
var authority = config[&quot;OAuth:Authority&quot;];
var scopes = config.GetSection(&quot;OAuth:Scopes&quot;).Get&lt;string[]&gt;();

var app = ConfidentialClientApplicationBuilder.Create(clientId)
    .WithAuthority(authority)
    .WithClientSecret(config[&quot;OAuth:ClientSecret&quot;])
    .Build();

var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
Console.WriteLine($&quot;Access Token: {result.AccessToken}&quot;);
</code></pre><hr><h3 id="step-5-make-an-authenticated-api-call">Step 5: Make an Authenticated API Call</h3><p>Use <code>HttpClient</code> to call a protected resource:</p><pre><code class="language-csharp">using System.Net.Http;
using System.Net.Http.Headers;

using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = 
    new AuthenticationHeaderValue(&quot;Bearer&quot;, result.AccessToken);

var response = await httpClient.GetAsync(&quot;https://graph.microsoft.com/v1.0/users&quot;);
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
</code></pre><blockquote>&#x2705; Testing Endpoint: <code>https://graph.microsoft.com/v1.0/users</code> &gt;This endpoint returns a list of users in your Azure AD tenant. To use it, ensure your app registration has Application permissions like <code>User.Read.All</code> and that you&apos;ve granted admin consent.</blockquote><p>If all works as expected, you should see a response similar to the following:</p><pre><code class="language-csharp">{
  &quot;@odata.context&quot;: &quot;https://graph.microsoft.com/v1.0/$metadata#users&quot;,
  &quot;value&quot;: [
    {
      &quot;businessPhones&quot;: [],
      &quot;displayName&quot;: &quot;Dean Hume&quot;,
      &quot;givenName&quot;: &quot;Dean&quot;,
      &quot;jobTitle&quot;: null,
      &quot;mail&quot;: null,
      &quot;mobilePhone&quot;: null,
      &quot;officeLocation&quot;: null,
      &quot;preferredLanguage&quot;: &quot;en&quot;,
      &quot;surname&quot;: &quot;Hume&quot;,
      &quot;userPrincipalName&quot;: &quot;email.com#EXT#@email.onmicrosoft.com&quot;,
      &quot;id&quot;: &quot;id-response-goes-here&quot;
    }
  ]
}</code></pre><hr><h3 id="optional-set-up-an-entra-id-test-oauth-app">*Optional: Set up an Entra ID test OAuth App</h3><p>This is an optional step as you might be wanting to use your own OAuth provider, however, I wanted to be sure that my code worked and I set up an App in <a href="https://www.microsoft.com/en-gb/security/business/identity-access/microsoft-entra-id">Azure&apos;s Entra ID</a> that I could test with. </p><p>If you&apos;d like to do the same, I recommend checking out these resources:</p><ul><li><a href="https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant">Quickstart: Create a new tenant in Microsoft Entra ID</a></li><li><a href="https://www.spletzer.com/2025/01/how-to-get-your-client-id-and-client-secret-from-entra-id/">Client Id &amp; Secret from Entra ID</a></li></ul><h3 id="conclusion">Conclusion</h3><p>OAuth 2.0 doesn&apos;t have to be intimidating. With modern libraries like MSAL and tools like Visual Studio Code, you can securely authenticate and access APIs with just a few lines of code. </p><p>If you&apos;d like to see an example of this code in action, please head over to the <a href="https://github.com/deanhume/oauthdemo">Github repo</a> that I created to accompany this article.</p>]]></content:encoded></item><item><title><![CDATA[HTML Minifier: A 12-Year Journey Building and Maintaining an Open Source Tool]]></title><description><![CDATA[Discover the 12-year journey behind HTML Minifier an open source tool that helped developers optimize web performance and reduce page load times.]]></description><link>https://deanhume.com/html-minifier-a-12-year-journey-building-and-maintaining-an-open-source-tool/</link><guid isPermaLink="false">6a214333a5fdd000011796be</guid><category><![CDATA[Web Performance]]></category><category><![CDATA[HTML]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Thu, 30 Oct 2025 14:49:50 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/10/html-minifier-header.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/10/html-minifier-header.jpg" alt="HTML Minifier: A 12-Year Journey Building and Maintaining an Open Source Tool"><p>Back in 2013, I published a blog post about a simple <a href="https://deanhume.com/a-simple-html-minifier-for-asp-net/">HTML Minifier</a> I&apos;d built for web pages. At the time, web performance was becoming increasingly important, and I needed a straightforward way to reduce the size of HTML files in my projects. What started as a personal solution has evolved into a tool that&apos;s been used by developers worldwide for over a decade. Today, I want to reflect on that journey and share what the tool has become.</p><h2 id="the-problem-then-and-now">The Problem (Then and Now)</h2><p>Web performance has always been critical, but it&apos;s even more important today. Every KB matters when you&apos;re trying to deliver fast experiences to users across the globe on varying network conditions. While we have sophisticated build tools and bundlers now, the core principle remains the same: <strong>smaller files load faster</strong>.</p><p>HTML minification removes unnecessary whitespace, line breaks, and comments from your HTML files - things that make code readable for humans but add unnecessary bytes for browsers. When you&apos;re shipping production code, these extras are just wasted bandwidth.</p><h2 id="what-started-as-a-simple-tool">What Started as a Simple Tool</h2><p>The original HTML Minifier was straightforward: point it at a folder of HTML or ASP.NET Razor views, and it would strip out the unnecessary characters. It was born from a real need I had while working on ASP.NET projects where manually minifying views or integrating complex build processes wasn&apos;t practical.</p><pre><code class="language-bash">HtmlMinifier.exe &quot;C:\MyProject\Views&quot;
</code></pre><p>That was it. Simple, effective, and it got the job done.</p><h2 id="evolution-over-12-years">Evolution Over 12 Years</h2><p>What&apos;s been fascinating is watching the tool evolve based on real-world usage and community feedback. Here are some of the key improvements that have shaped the project:</p><h3 id="framework-support">Framework Support</h3><p>Early on, I learned that aggressive minification could break certain JavaScript frameworks. Knockout.js, for example, uses HTML comments for control flow. Angular had similar patterns. This led to adding configuration flags:</p><pre><code class="language-bash"># Preserve HTML comments for frameworks that need them
HtmlMinifier.exe &quot;C:\Folder&quot; ignorehtmlcomments

# Keep Knockout.js comments specifically
HtmlMinifier.exe &quot;C:\Folder&quot; ignoreknockoutcomments
</code></pre><p>This was a great lesson in understanding your users&apos; workflows. A tool is only useful if it fits into real-world scenarios.</p><h3 id="performance-improvements">Performance Improvements</h3><p>As the tool gained users, performance became more important. People weren&apos;t just minifying a handful of files - they were processing entire web applications with thousands of views. I added:</p><ul><li><strong>Recursive directory scanning</strong> for processing entire project structures</li><li><strong>Multi-file support</strong> for targeted minification</li><li><strong>Line length controls</strong> to prevent overly long lines that could impact certain text editors or source control systems</li><li><strong>Performance metrics</strong> showing bytes saved and processing time</li></ul><pre><code class="language-bash"># Limit lines to 60,000 characters
HtmlMinifier.exe &quot;C:\Folder&quot; &quot;60000&quot;
</code></pre><h3 id="modern-development-practices">Modern Development Practices</h3><p>The .NET ecosystem has changed dramatically since 2013. The project has evolved alongside it:</p><ul><li><strong>Continuous Integration</strong>: GitHub Actions now automatically build and test every commit</li><li><strong>Comprehensive Testing</strong>: Over 60 unit tests covering edge cases, performance benchmarks, and real-world scenarios</li><li><strong>Better Error Handling</strong>: Proper validation, informative error messages, and graceful failure modes</li><li><strong>Cross-framework Support</strong>: HTML, Razor views (.cshtml), and Web Forms (.aspx)</li></ul><h3 id="community-contributions">Community Contributions</h3><p>One of the most rewarding parts of this journey has been the incredible support from the community. Over the years, twelve different contributors have jumped in to help improve the tool - surfacing bugs and edge cases I hadn&#x2019;t considered, submitting fixes for real-world production issues, adding support for specific frameworks and use cases, and even enhancing the documentation and usage examples. </p><p>Every pull request, issue, and suggestion has helped shape the tool into something better.</p><h3 id="performance-gains">Performance Gains</h3><p>The tool typically achieves:</p><ul><li><strong>10-40% size reduction</strong> for typical HTML files</li><li><strong>20-50% reduction</strong> for files with heavy indentation and whitespace</li><li><strong>Processing speeds</strong> that can handle thousands of files in seconds</li></ul><p>For a production website with hundreds of views, this translates to meaningful bandwidth savings and faster page loads.</p><h2 id="lessons-from-maintaining-an-open-source-tool">Lessons from Maintaining an Open Source Tool</h2><h3 id="1-simplicity-is-a-feature">1. <strong>Simplicity is a Feature</strong></h3><p>In an era of complex build toolchains, there&apos;s still value in tools that do one thing well. The HTML Minifier has survived because it&apos;s simple to understand and use. You don&apos;t need to configure Webpack, install npm packages, or learn a new build system - you just run it.</p><p>But I&apos;m also mindful that sometimes the best feature is knowing what <em>not</em> to add. The tool&apos;s simplicity is part of its appeal.</p><h3 id="2-real-users-reveal-real-problems">2. <strong>Real Users Reveal Real Problems</strong></h3><p>Some of the most impactful improvements came directly from folks using the tool in production. They uncovered edge cases I hadn&#x2019;t even considered - like deeply nested HTML structures, Unicode and international character handling, quirky Razor syntax combinations, and performance bottlenecks with massive files. Their feedback helped push the tool to be more robust and production-ready.</p><h3 id="3-open-source-is-a-marathon">3. <strong>Open Source is a Marathon</strong></h3><p>Twelve years is a long time. Technologies come and go, but useful tools persist. The key is:</p><ul><li>Responding to issues thoughtfully</li><li>Being conservative with breaking changes</li><li>Keeping the tool focused on its core purpose</li><li>Appreciating and crediting contributors</li></ul><h2 id="try-it-yourself">Try It Yourself</h2><p>If you&apos;re working on a web project and need a straightforward way to minify HTML, give it a try:</p><p><strong>GitHub</strong>: <a href="https://github.com/deanhume/html-minifier">https://github.com/deanhume/html-minifier</a></p><h3 id="reflections">Reflections</h3><p>Looking back at 12 years of maintaining this tool has been surprisingly rewarding. It&apos;s been used in projects I&apos;ll never know about, solved problems I didn&apos;t anticipate, and connected me with developers around the world.</p><p>The web development landscape has changed dramatically since 2013:</p><ul><li>We&apos;ve gone from jQuery to React, Vue, and beyond</li><li>Build tools have become incredibly sophisticated</li><li>Web performance tooling has advanced tremendously</li><li>The rise of SPAs, PWAs, and modern frameworks</li></ul><p>Yet through all these changes, the need for efficient HTML delivery remains constant. That&apos;s perhaps the most interesting lesson: <strong>fundamental problems persist even as technologies evolve</strong>.</p><h2 id="thank-you">Thank You</h2><p>To everyone who&apos;s used the tool, filed an issue, contributed code, or simply found it helpful - thank you. Open source thrives on community, and this project has been a small but meaningful part of my contribution to that ecosystem.</p><p>Here&apos;s to another 12 years of making the web a little bit faster, one minified file at a time. &#x1F680;</p><hr><p><strong>Resources</strong>:</p><ul><li><a href="https://github.com/deanhume/html-minifier">GitHub Repository</a></li><li><a href="https://deanhume.com/a-simple-html-minifier-for-asp-net/">Original Blog Post (2013)</a></li><li><a href="https://github.com/deanhume/html-minifier/releases">Latest Release</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Best Practices for Handheld Gaming Development]]></title><description><![CDATA[<p>If you&apos;ve ever played a game on a modern handheld device, you&apos;ll know how much fun it can be. I recently wrote an article on the Microsoft Game Dev Blog about some key recommendations to help you craft exceptional gaming experiences for handheld devices.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/06/Handheld-Device-Asus-Rog-Xbox.png" class="kg-image" alt="Handheld Device -Asus Xbox Rog Ally" loading="lazy" width="1920" height="1080" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2025/06/Handheld-Device-Asus-Rog-Xbox.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2025/06/Handheld-Device-Asus-Rog-Xbox.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1600/2025/06/Handheld-Device-Asus-Rog-Xbox.png 1600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/06/Handheld-Device-Asus-Rog-Xbox.png 1920w" sizes="(min-width: 720px) 720px"></figure><p>Please head</p>]]></description><link>https://deanhume.com/best-practices-for-handheld-gaming-development/</link><guid isPermaLink="false">6a214333a5fdd000011796bd</guid><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Mon, 16 Jun 2025 09:47:47 GMT</pubDate><content:encoded><![CDATA[<p>If you&apos;ve ever played a game on a modern handheld device, you&apos;ll know how much fun it can be. I recently wrote an article on the Microsoft Game Dev Blog about some key recommendations to help you craft exceptional gaming experiences for handheld devices.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/06/Handheld-Device-Asus-Rog-Xbox.png" class="kg-image" alt="Handheld Device -Asus Xbox Rog Ally" loading="lazy" width="1920" height="1080" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2025/06/Handheld-Device-Asus-Rog-Xbox.png 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2025/06/Handheld-Device-Asus-Rog-Xbox.png 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1600/2025/06/Handheld-Device-Asus-Rog-Xbox.png 1600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2025/06/Handheld-Device-Asus-Rog-Xbox.png 1920w" sizes="(min-width: 720px) 720px"></figure><p>Please head over to <a href="https://developer.microsoft.com/en-us/games/articles/2025/06/best-practices-for-handheld-gaming-development/">this link</a> to find out more info.</p>]]></content:encoded></item><item><title><![CDATA[Getting started with Azure's Hybrid and Embedded Text-to-Speech]]></title><description><![CDATA[<p>Over the past few months, I&apos;ve been experimenting with Azure&apos;s Text-to-Speech service. It is a super powerful API that enables fluid, natural-sounding text to speech that matches the tone and emotion of human voices.</p><p>Whether you are building an app or a game,</p>]]></description><link>https://deanhume.com/azure-hybrid-and-embedded-text-to-speech/</link><guid isPermaLink="false">6a214333a5fdd000011796bc</guid><category><![CDATA[Text-to-Speech]]></category><category><![CDATA[AI]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Thu, 15 Feb 2024 09:41:58 GMT</pubDate><content:encoded><![CDATA[<p>Over the past few months, I&apos;ve been experimenting with Azure&apos;s Text-to-Speech service. It is a super powerful API that enables fluid, natural-sounding text to speech that matches the tone and emotion of human voices.</p><p>Whether you are building an app or a game, Text-to-Speech can be very useful. For example, think of the different stages of game development - during concept and pre-production, text-to-speech can help build out the feel of the game and enhance your scripts before you record with real voice actors. </p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2024/02/text-to-speech-game-development.jpg" class="kg-image" alt loading="lazy" width="1000" height="516" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2024/02/text-to-speech-game-development.jpg 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2024/02/text-to-speech-game-development.jpg 1000w" sizes="(min-width: 720px) 720px"></figure><p>During release and production , it can be used to provide accessibility options to suit the needs of your users. &#xA0;At the time of writing this article, there are over 456 voices across 147 languages that you can choose from! </p><p>As you are reading through this, you might be thinking to yourself...hang on...This uses a Cloud service, how would this work for an offline game? Or when a user loses connection?</p><p>This is where Hybrid (and Embedded) speech comes into play, and in this article, we are going to explore an example that will work both online and offline scenarios.</p><h2 id="how-hybrid-speech-works">How Hybrid Speech works</h2><p>Hybrid speech uses the cloud speech service by default and embedded speech as a fallback in case cloud connectivity is limited or slow.</p><p>In fact, you could ship your entire app with just the embedded speech and not use the cloud service at all. Its worth mentioning that it is slightly limited in that while the quality is good, the cloud option returns the highest quality speech. To give you and idea of what this looks like, lets compare the two versions. The first is the embedded speech version:</p><div class="kg-card kg-audio-card"><audio src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/media/2024/02/device-22.wav" preload="metadata" controls></audio></div><p>And the the second is the cloud based version:</p><div class="kg-card kg-audio-card"><audio src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/media/2024/02/cloud-22.wav" preload="metadata" controls></audio></div><p>If you listen really closely, you can hear that there is a slight improvement in the tone and cadence of the cloud speech service. There is not much difference, and the embedded version sounds pretty good too!</p><p>In this article, I am going to take you through a basic example of hybrid and embedded speech using Azure&apos;s Text to Speech Service.</p><h2 id="lets-get-started">Let&apos;s get started</h2><p>Before we get started, we need to download the voices that we will use with the embedded version of the code. That is, the voices that will actually &quot;ship&quot; with the code. In order to acquire the voices, you will need to apply for access - follow <a href="https://aka.ms/csgate-embedded-speech">this link to request access</a> to the voices. </p><p>Once you have the voices, we can then start building out our example. First off, let&apos;s start by creating a <a href="https://learn.microsoft.com/en-us/visualstudio/ide/create-new-project?view=vs-2022">new project in Visual Studio Code</a>. Next, add a new class called <em>Keys </em>that will contain the keys and settings that we need.</p><!--kg-card-begin: markdown--><pre><code>public class Keys
{
    public static string EmbeddedSpeechSynthesisVoicePath = @&quot;\voices\en-us&quot;; 
    public static string EmbeddedSpeechSynthesisVoiceKey = &quot;your_key&quot;; 
    public static string EmbeddedSpeechSynthesisVoiceName = &quot;en-US-JennyNeural&quot;; 
    
    public static string CloudSpeechSubscriptionKey = &quot;subscription_key&quot;; 
    public static string CloudSpeechServiceRegion = &quot;eastus&quot;; 
    public static string SpeechRecognitionLocale = &quot;en-US&quot;; 
    public static string SpeechSynthesisLocale = &quot;en-US&quot;; 
}
</code></pre>
<!--kg-card-end: markdown--><p>Let&apos;s break down the code above. Firstly, the variable <strong>EmbeddedSpeechSynthesisVoicePath </strong>points to the file location where the voices are located and <strong>EmbeddedSpeechSynthesisVoiceKey</strong> is the key that you need to access the voices. You&apos;ll be given these when you apply for the access as mentioned above. I&apos;ve also chosen the voice of &quot;Jenny&quot;, but you could choose any from the <a href="https://speech.microsoft.com/portal/voicegallery">Voice Gallery</a>.</p><p>As we are using a hybrid model, we&apos;ll need to provide some cloud details from an Azure speech instance. I created a new speech instance on the Azure portal and on the overview page, I selected the <strong>CloudSpeechSubscriptionKey</strong> and <strong>CloudSpeechServiceRegion </strong>from the portal (highlighted in yellow below).</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2024/02/azure-portal-text-to-speech.jpg" class="kg-image" alt loading="lazy" width="1200" height="1190" srcset="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w600/2024/02/azure-portal-text-to-speech.jpg 600w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/size/w1000/2024/02/azure-portal-text-to-speech.jpg 1000w, https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2024/02/azure-portal-text-to-speech.jpg 1200w" sizes="(min-width: 720px) 720px"></figure><p>In the code above, we are also providing the instance region and the locale that we are going to be using in the variables <strong>SpeechRecognitionLocale </strong>and <strong>SpeechSynthesisLocale</strong>. In this case I&apos;m using US English, but you could use any language and locale of your choice.</p><h2 id="configuring-the-embedded-speech">Configuring the Embedded Speech</h2><p>Now that we have the keys configured, we can create the config for the embedded speech model. I&apos;ve created a new class file called <strong>Settings </strong>and added the following code.</p><!--kg-card-begin: markdown--><pre><code>public class Settings
{

public static EmbeddedSpeechConfig CreateEmbeddedSpeechConfig()
{
    List&lt;string&gt; paths = new List&lt;string&gt;();

    var synthesisVoicePath = Keys.EmbeddedSpeechSynthesisVoicePath;
    if (!string.IsNullOrEmpty(synthesisVoicePath) &amp;&amp; !synthesisVoicePath.Equals(&quot;YourEmbeddedSpeechSynthesisVoicePath&quot;))
    {
        paths.Add(synthesisVoicePath);
    }

    // Make sure that there is a voice path defined above.
    if (paths.Count == 0)
    {
        Console.Error.WriteLine(&quot;## ERROR: No model path(s) specified.&quot;);
        return null;
    }

    var config = EmbeddedSpeechConfig.FromPaths(paths.ToArray());

    if (!string.IsNullOrEmpty(Keys.EmbeddedSpeechSynthesisVoiceName))
    {
        // Mandatory configuration for embedded speech synthesis.
        config.SetSpeechSynthesisVoice(Keys.EmbeddedSpeechSynthesisVoiceName, Keys.EmbeddedSpeechSynthesisVoiceKey);
        if (Keys.EmbeddedSpeechSynthesisVoiceName.Contains(&quot;Neural&quot;))
        {
            // Embedded neural voices only support 24kHz sample rate.
config.SetSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm);
        }
    }

    return config;
}
</code></pre>
<!--kg-card-end: markdown--><p>The code above uses the different file paths and keys that we set in the Keys class file. We&apos;ll be using this <strong>EmbeddedSpeechConfig </strong>object to create speech using the local voices on file.</p><h2 id="configuring-the-hybrid-speech">Configuring the Hybrid Speech</h2><p>In the same way that we set up the <strong>EmbeddedSpeechConfig </strong>object, we&apos;ll need to create a <strong>CreateHybridSpeechConfig </strong>object. </p><!--kg-card-begin: markdown--><pre><code>public static HybridSpeechConfig CreateHybridSpeechConfig()
{
    var cloudSpeechConfig = SpeechConfig.FromSubscription(Keys.CloudSpeechSubscriptionKey, Keys.CloudSpeechServiceRegion);

    cloudSpeechConfig.SpeechRecognitionLanguage = Keys.SpeechRecognitionLocale;
    cloudSpeechConfig.SpeechSynthesisLanguage = Keys.SpeechSynthesisLocale;

    var embeddedSpeechConfig = CreateEmbeddedSpeechConfig();

    var config = HybridSpeechConfig.FromConfigs(cloudSpeechConfig, embeddedSpeechConfig);

    return config;
}
</code></pre>
<!--kg-card-end: markdown--><p>You&apos;ll notice that the code above calls the <em>CreateEmbeddedSpeechConfig()</em> function that we created earlier. This is because we&apos;ll be using this code to use the cloud speech service by default and then fallback to the embedded speech in case cloud connectivity is limited or slow. </p><h2 id="try-it-out">Try it out</h2><p>With all this in place, we are now ready to start calling the API and synthesizing some speech. </p><!--kg-card-begin: markdown--><pre><code>/// &lt;summary&gt;
/// Synthesizes speech using the hybrid speech system and outputs it to the default speaker.
/// &lt;/summary&gt;
private static async Task HybridSynthesisToSpeaker()
{
    var textToSpeak = &quot;Hello, this is a test of the hybrid speech system.&quot;;

    var speechConfig = Settings.CreateHybridSpeechConfig();

    using var audioConfig = AudioConfig.FromDefaultSpeakerOutput();

    using var synthesizer = new SpeechSynthesizer(speechConfig, audioConfig);
    
    using var result = await synthesizer.SpeakTextAsync(textToSpeak);
}
</code></pre>
<!--kg-card-end: markdown--><p>When you call <em>HybridSynthesisToSpeaker().Wait()</em>, you should now hear something coming from your speaker!</p><p>In these code samples, I have created a simple string that we are passing through to the API. Depending on your use case, you could build more complex examples using <a href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup">Speech Synthesis Markup Language (SSML)</a>. Speech Synthesis Markup Language (SSML) is an XML-based markup language that you can use to fine-tune your text to speech output attributes such as pitch, pronunciation, speaking rate, volume, and more. </p><h2 id="using-embedded-speech-only">Using Embedded Speech only</h2><p>If you preferred to use embedded speech only, you can call the <em>CreateEmbeddedSpeechConfig() </em>function that we created earlier.</p><!--kg-card-begin: markdown--><pre><code>/// &lt;summary&gt;
/// Synthesizes speech using the embedded speech system and outputs it to the default speaker.
/// &lt;/summary&gt;
private static async Task EmbeddedSynthesisToSpeaker()
{
    var textToSpeak = &quot;Hello, this is a test of the embedded speech system.&quot;;

    var speechConfig = Settings.CreateEmbeddedSpeechConfig();
    
    using var audioConfig = AudioConfig.FromDefaultSpeakerOutput();

    using var synthesizer = new SpeechSynthesizer(speechConfig, audioConfig);

    using var result = await synthesizer.SpeakTextAsync(textToSpeak);
}
</code></pre>
<!--kg-card-end: markdown--><p>When you call <em>EmbeddedSynthesisToSpeaker().Wait()</em>, you should now hear embedded speech coming from your speaker!</p><h2 id="summary">Summary</h2><p>I&apos;ve barely scratched the surface of the capabilities of text-to-speech; there is so much more to experiment with! If you&apos;d like to learn more about Embedded/Hybrid Speech, I recommend reading the <a href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/embedded-speech?tabs=windows-target%2Cjre&amp;pivots=programming-language-csharp">following article</a> for more information.</p><p>In this article, we covered both embedded and hybrid speech options and its also worth mentioning that you can ship with only embedded speech if you prefer. In the code example that we ran through, we used C# but there are other language options available including C++, and Java SDKs.</p>]]></content:encoded></item><item><title><![CDATA[Azure Function Timer Trigger not firing - NCrontab]]></title><description><![CDATA[<p>If you&apos;ve recently ventured into the world of <a href="https://azure.microsoft.com/en-gb/products/functions/">Azure Functions</a>, you&apos;re likely familiar with the versatility they offer when it comes to scheduling tasks. In this guide, we&apos;ll delve into using <a href="https://learn.microsoft.com/en-gb/azure/azure-functions/functions-create-scheduled-function">Azure Functions&apos; Time Trigger</a> function with Node.js and Visual Studio Code,</p>]]></description><link>https://deanhume.com/azure-http-trigger/</link><guid isPermaLink="false">6a214333a5fdd000011796bb</guid><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Wed, 04 Oct 2023 11:03:57 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2023/10/azure-function-timer-trigger.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2023/10/azure-function-timer-trigger.jpg" alt="Azure Function Timer Trigger not firing - NCrontab"><p>If you&apos;ve recently ventured into the world of <a href="https://azure.microsoft.com/en-gb/products/functions/">Azure Functions</a>, you&apos;re likely familiar with the versatility they offer when it comes to scheduling tasks. In this guide, we&apos;ll delve into using <a href="https://learn.microsoft.com/en-gb/azure/azure-functions/functions-create-scheduled-function">Azure Functions&apos; Time Trigger</a> function with Node.js and Visual Studio Code, uncovering some important nuances along the way.</p><p>This article assumes that you have some familiarity with Node.js and a basic understanding of Azure.</p><h3 id="getting-started">Getting Started</h3><p>To begin, you&apos;ll need Node.js and Visual Studio Code installed, along with the Azure Functions extension. When you create a new function, it comes with some boilerplate code that includes a schedule property defining when the function should run. By default, it&apos;s set to run every 5 minutes.</p><!--kg-card-begin: markdown--><pre><code>const { app } = require(&apos;@azure/functions&apos;);

app.timer(&apos;timerTrigger2&apos;, {
    schedule: &apos;0 */5 * * * *&apos;,
    handler: (myTimer, context) =&gt; {
        context.log(&apos;Timer function processed request.&apos;);
    }
});
</code></pre>
<!--kg-card-end: markdown--><p>The <strong>schedule parameter</strong> in the above code tells the Azure Function how often to fire.</p><h3 id="the-challenge">The Challenge</h3><p>But what if you want your function to run every 11 hours during a day? Crafting the right Cron expression can be tricky, and that&apos;s where I encountered some difficulties. I turned to an online Cron expression generator, hoping it would simplify the process. Here&apos;s what it gave me:</p><!--kg-card-begin: markdown--><pre><code>0 0 */11 * *
</code></pre>
<!--kg-card-end: markdown--><p>Feeling confident, I integrated this expression into my code above and deployed the Azure Function. However, it didn&apos;t work as expected. I scoured the logs for clues but found nothing. To troubleshoot and debug locally, I temporarily changed the Cron expression to run every minute, and it worked flawlessly. So why wasn&apos;t it firing every few hours?</p><h3 id="the-revelation">The Revelation</h3><p>After some online research, I uncovered the reason behind the issue. Azure Functions use an <strong>NCRONTAB expression</strong>, which requires a six-part format instead of the traditional five-part Cron expression. The online generator that I found online had provided a five-part expression, leading to the problem.</p><p>To be fair, the Visual Studio Code extension for Azure Functions provides the correct six-part format by default. My mistake was changing it using a five-part expression from an online tool - d&apos;oh!</p><h3 id="the-solution">The Solution</h3><p>To rectify this, I recommend using the <a href="https://ncrontab.swimburger.net/">NCrontab Expression Tester</a>. This user-friendly tool not only helps you test your expressions but also generates the correct six-part Cron expressions tailored for Azure Functions. </p><p>When I updated my code to use the 6 part format:</p><!--kg-card-begin: markdown--><pre><code>0 0 */11 * * *
</code></pre>
<!--kg-card-end: markdown--><p>It ran perfectly!</p><h3 id="why-ncrontab">Why NCrontab?</h3><p>You might be wondering why Azure Functions use the NCrontab six-part format instead of the traditional five-part format. The answer lies in its flexibility. The six-part format allows you to specify seconds, enabling you to run your functions with higher precision and frequency.</p><!--kg-card-begin: markdown--><pre><code>* * * * * *
- - - - - -
| | | | | |
| | | | | +--- day of week (0 - 6) (Sunday=0)
| | | | +----- month (1 - 12)
| | | +------- day of month (1 - 31)
| | +--------- hour (0 - 23)
| +----------- min (0 - 59)
+------------- sec (0 - 59)
</code></pre>
<!--kg-card-end: markdown--><p>For further details on NCrontab expressions, you can visit the GitHub repository <a href="https://github.com/atifaziz/NCrontab">here</a>.</p><p>Happy coding!</p>]]></content:encoded></item><item><title><![CDATA[Using a Raspberry Pi to track the progress of your homebrew]]></title><description><![CDATA[In this article I go into detail about how I hooked up a Raspberry to continually track and monitor the progress of my beer all whilst graphing it out to another application. It gets a little geeky!]]></description><link>https://deanhume.com/using-a-raspberry-pi-to-track-the-progress-of-your-homebrew/</link><guid isPermaLink="false">6a214333a5fdd000011796b9</guid><category><![CDATA[Charts]]></category><category><![CDATA[Raspberry Pi]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[Dean Hume]]></dc:creator><pubDate>Thu, 18 Aug 2022 10:48:32 GMT</pubDate><media:content url="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2023/10/raspberry-pi-homebrew.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2023/10/raspberry-pi-homebrew.jpg" alt="Using a Raspberry Pi to track the progress of your homebrew"><p>In my spare time, I like to do a bit of <a href="https://humebrew.com">homebrewing</a>. While it isn&apos;t the usual sort of topic that I post on this site, I wanted to cross post to an article I wrote about automating some of the brewing process.</p><p>If you&apos;ve ever goofed around with Raspberry Pi&apos;s before, you&apos;ll know how fun they can be. <a href="https://humebrew.com/tilt-hydrometer-to-a-raspberry-pi-to-brewfather/">In this article</a> I go into detail about how I hooked up a Raspberry Pi to continually track and monitor the progress of my beer all whilst graphing it out to another application. It gets a little geeky!</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/5d/4f/5d4f1357-4cbe-4bf7-8793-2bf95d338ca5/content/images/2022/08/raspberry-pi-tilt-pi.jpg" class="kg-image" alt="Using a Raspberry Pi to track the progress of your homebrew" loading="lazy"></figure><p>Please head over to my homebrew website over at <a href="https://humebrew.com/tilt-hydrometer-to-a-raspberry-pi-to-brewfather/">Humebrew.com</a> to find out more!</p>]]></content:encoded></item></channel></rss>