<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ geminiAPI - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ geminiAPI - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 16 May 2026 22:22:53 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/geminiapi/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How I Built a Makaton AI Companion Using Gemini Nano and the Gemini API ]]>
                </title>
                <description>
                    <![CDATA[ When I started my research on AI systems that could translate Makaton (a sign and symbol language designed to support speech and communication), I wanted to bridge a gap in accessibility for learners with speech or language difficulties. Over time, t... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-i-built-a-makaton-ai-companion-using-gemini-nano-and-the-gemini-api/</link>
                <guid isPermaLink="false">690e1f43cb50ea9684f6d9aa</guid>
                
                    <category>
                        <![CDATA[ geminiAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Computer Vision ]]>
                    </category>
                
                    <category>
                        <![CDATA[ nlp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ gemini-nano ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ OMOTAYO OMOYEMI ]]>
                </dc:creator>
                <pubDate>Fri, 07 Nov 2025 16:33:07 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762533154134/e2209ade-6971-464b-aeef-f05abd0a30d7.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When I started my research on AI systems that could translate Makaton (a sign and symbol language designed to support speech and communication), I wanted to bridge a gap in accessibility for learners with speech or language difficulties.</p>
<p>Over time, this academic interest evolved into a working prototype that combines on-device AI and cloud AI to describe images and translate them into English meanings. The idea was simple: I wanted to build a lightweight web app that recognized Makaton gestures or symbols and instantly provided an English interpretation.</p>
<p>In this article, I’ll walk you through how I built my Makaton AI Companion, a single-page web app powered by Gemini Nano (on-device) and the Gemini API (cloud). You’ll see how it works, how I solved common issues like CORS and API model errors, and how this small project became part of my journey toward AI for accessibility.</p>
<p>By the end of this article, you will be able to:</p>
<ul>
<li><p>Understand the core concept behind Makaton and why it’s important in accessibility and inclusive education.</p>
</li>
<li><p>Learn how to combine on-device AI (Gemini Nano) and cloud-based AI (Gemini API) in a single web project.</p>
</li>
<li><p>Build a functional AI-powered web app that can describe images and map them to predefined English meanings.</p>
</li>
<li><p>Discover how to handle common errors such as model endpoint issues, missing API keys, and CORS restrictions when working with generative AI APIs.</p>
</li>
<li><p>Learn how to store API keys locally for user privacy using <code>localStorage</code>.</p>
</li>
<li><p>Use browser speech synthesis to convert the AI-generated English meanings into spoken output.</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-tools-and-tech-stack">Tools and Tech Stack</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-app-step-by-step">Building the App Step by Step</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-fix-the-common-issues">How to Fix the Common Issues</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-demo-the-makaton-ai-companion-in-action">Demo: The Makaton AI Companion in Action</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-broader-reflections">Broader Reflections</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-tools-and-tech-stack">Tools and Tech Stack</h2>
<p>To build the Makaton AI Companion, I wanted something lightweight, fast to prototype, and easy for anyone to run without complicated dependencies. I chose a plain web stack with a focus on accessibility and transparency.</p>
<p>Here’s what I used:</p>
<h3 id="heading-frontend">Frontend</h3>
<ul>
<li><p><strong>HTML + CSS + JavaScript (Vanilla):</strong> No frameworks, just clean and understandable code that any beginner can follow.</p>
</li>
<li><p>A single <code>index.html</code> page handles the upload interface, output display, and AI logic.</p>
</li>
</ul>
<h3 id="heading-ai-components">AI Components</h3>
<ul>
<li><p><strong>Gemini Nano</strong> runs locally in Chrome Canary. This on-device model lets users generate short text without calling the cloud API.</p>
</li>
<li><p><strong>Gemini API (Cloud)</strong> used as a fallback when on-device AI isn’t available or when image analysis is required.</p>
<ul>
<li><p>Model tested: <code>gemini-1.5-flash</code> and <code>gemini-pro-vision</code>.</p>
</li>
<li><p>Fallback logic ensures the app checks multiple model endpoints if one returns a 404 error.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-local-storage">Local Storage</h3>
<ul>
<li>The Gemini API key is stored safely in the browser’s localStorage, so it never leaves the user’s computer.</li>
</ul>
<h3 id="heading-browser-speechsynthesis-api">Browser SpeechSynthesis API</h3>
<ul>
<li>Converts the translated English meaning into spoken audio with one click.</li>
</ul>
<h3 id="heading-mapping-logic">Mapping Logic</h3>
<ul>
<li>A small custom dictionary (<code>mapping.js</code>) links AI-generated descriptions to likely Makaton meanings. For example: <code>{ keywords: ["open hand", "raised hand", "wave"], meaning: "Hello / Stop" }</code></li>
</ul>
<h3 id="heading-local-server">Local Server</h3>
<ul>
<li><p>The app is served locally using Python’s built-in HTTP server to avoid CORS issues:</p>
<p>  <code>python -m http.server 8080</code></p>
</li>
</ul>
<p>Then open <code>http://localhost:8080</code> in Chrome Canary.</p>
<h2 id="heading-building-the-app-step-by-step">Building the App Step by Step</h2>
<p>Now let’s dive into how the Makaton AI Companion works under the hood. This project follows a simple but effective flow: Upload an image → Describe (AI) → Map to Meaning → Speak or Copy the result</p>
<p>We’ll go through each part step by step.</p>
<h3 id="heading-1-setting-up-the-project-folder">1. Setting Up the Project Folder</h3>
<p>You don’t need any complex setup. Just create a new folder and add these files:</p>
<pre><code class="lang-plaintext">makaton-ai-companion/
│
├── index.html
├── styles.css
├── app.js
└── lib/
    ├── mapping.js
    └── ai.js
</code></pre>
<p>If you prefer a ready-to-run version, you can serve everything from one zip (I’ll share a GitHub link at the end).</p>
<h3 id="heading-2-creating-the-basic-html-structure">2. Creating the Basic HTML Structure</h3>
<p>Your <code>index.html</code> file defines the interface where users upload an image, click <em>Describe</em>, and view the results.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>/&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Makaton AI Companion<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"styles.css"</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">header</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"app-header"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>🧩 Makaton AI Companion<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnSettings"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn secondary"</span>&gt;</span>Settings<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">header</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>1) Upload an image (Makaton sign/symbol)<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"file"</span>&gt;</span>
        Choose an image file
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">accept</span>=<span class="hljs-string">"image/*"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Select an image file"</span>/&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"preview"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"preview hidden"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"status"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"status"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"actions"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnDescribe"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn"</span>&gt;</span>Describe (Cloud or Nano)<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnType"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn ghost"</span>&gt;</span>Type a description instead<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"typedBox"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"typed hidden"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">textarea</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"typed"</span> <span class="hljs-attr">rows</span>=<span class="hljs-string">"3"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Describe what you see..."</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">textarea</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnUseTyped"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn"</span>&gt;</span>Use this description<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>2) AI Output<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>Image Description<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"output"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"output"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>English Meaning (Mapped)<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"meaning"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"meaning"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"actions"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnSpeak"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn ghost"</span> <span class="hljs-attr">disabled</span>&gt;</span>🔊 Speak<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnCopy"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn ghost"</span> <span class="hljs-attr">disabled</span>&gt;</span>📋 Copy<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">dialog</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"settings"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"dialog"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"settings-form"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Settings<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>Gemini API key (optional)<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"apiKey"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"AIza..."</span>/&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"settings-actions"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnSaveKey"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn"</span>&gt;</span>Save<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"btnCloseSettings"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn secondary"</span>&gt;</span>Close<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"apiStatus"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"api-status"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">dialog</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"lib/mapping.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"lib/ai.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"app.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>This interface is intentionally minimal: no frameworks, no build tools, just clear HTML.</p>
<h3 id="heading-3-mapping-descriptions-to-makaton-meanings">3. Mapping Descriptions to Makaton Meanings</h3>
<p>The <code>mapping.js</code> file holds a simple keyword-based dictionary. When the AI describes an image (like <em>“a raised open hand”</em>), the app searches for keywords that match known Makaton signs.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/mapping.js</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> MAKATON_GLOSSES = [
  { <span class="hljs-attr">keywords</span>: [<span class="hljs-string">"open hand"</span>, <span class="hljs-string">"raised hand"</span>, <span class="hljs-string">"wave"</span>, <span class="hljs-string">"hand up"</span>], <span class="hljs-attr">meaning</span>: <span class="hljs-string">"Hello / Stop"</span> },
  { <span class="hljs-attr">keywords</span>: [<span class="hljs-string">"eat"</span>, <span class="hljs-string">"food"</span>, <span class="hljs-string">"spoon"</span>, <span class="hljs-string">"hand to mouth"</span>], <span class="hljs-attr">meaning</span>: <span class="hljs-string">"Eat"</span> },
  { <span class="hljs-attr">keywords</span>: [<span class="hljs-string">"drink"</span>, <span class="hljs-string">"cup"</span>, <span class="hljs-string">"glass"</span>, <span class="hljs-string">"bottle"</span>], <span class="hljs-attr">meaning</span>: <span class="hljs-string">"Drink"</span> },
  { <span class="hljs-attr">keywords</span>: [<span class="hljs-string">"home"</span>, <span class="hljs-string">"house"</span>, <span class="hljs-string">"roof"</span>], <span class="hljs-attr">meaning</span>: <span class="hljs-string">"Home"</span> },
  { <span class="hljs-attr">keywords</span>: [<span class="hljs-string">"sleep"</span>, <span class="hljs-string">"bed"</span>, <span class="hljs-string">"eyes closed"</span>], <span class="hljs-attr">meaning</span>: <span class="hljs-string">"Sleep"</span> },
  { <span class="hljs-attr">keywords</span>: [<span class="hljs-string">"book"</span>, <span class="hljs-string">"reading"</span>, <span class="hljs-string">"pages"</span>], <span class="hljs-attr">meaning</span>: <span class="hljs-string">"Book / Read"</span> },
  <span class="hljs-comment">// Added so your current screenshot maps correctly:</span>
  { <span class="hljs-attr">keywords</span>: [<span class="hljs-string">"help"</span>, <span class="hljs-string">"assist"</span>, <span class="hljs-string">"thumb on palm"</span>, <span class="hljs-string">"hand over hand"</span>, <span class="hljs-string">"assisting"</span>], <span class="hljs-attr">meaning</span>: <span class="hljs-string">"Help"</span> },
];

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mapDescriptionToMeaning</span>(<span class="hljs-params">desc</span>) </span>{
  <span class="hljs-keyword">if</span> (!desc) <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>;
  <span class="hljs-keyword">const</span> d = desc.toLowerCase();
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> MAKATON_GLOSSES) {
    <span class="hljs-keyword">if</span> (entry.keywords.some(<span class="hljs-function"><span class="hljs-params">k</span> =&gt;</span> d.includes(k))) <span class="hljs-keyword">return</span> entry.meaning;
  }
  <span class="hljs-keyword">if</span> (d.includes(<span class="hljs-string">"hand"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-string">"Gesture / Hand sign (clarify)"</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-string">"No direct mapping found."</span>;
}
</code></pre>
<p>It’s simple but effective enough to simulate real symbol-to-language translation for demo purposes.</p>
<h3 id="heading-4-adding-gemini-ai-logic">4. Adding Gemini AI Logic</h3>
<p>The <code>ai.js</code> file connects to Gemini Nano (on-device) or the Gemini API (cloud). If Nano isn’t available, the app falls back to the cloud model. And if that fails, it lets users type a description manually.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/ai.js — dynamic model discovery (try-all version)</span>

<span class="hljs-comment">// --- On-device availability (Gemini Nano) ---</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkAvailability</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> res = { <span class="hljs-attr">nanoTextPossible</span>: <span class="hljs-literal">false</span> };
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> canCreate = self.ai?.canCreateTextSession || self.ai?.languageModel?.canCreate;
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> canCreate === <span class="hljs-string">"function"</span>) {
      <span class="hljs-keyword">const</span> ok = <span class="hljs-keyword">await</span> (self.ai.canCreateTextSession?.() || self.ai.languageModel.canCreate?.());
      res.nanoTextPossible = ok === <span class="hljs-string">"readily"</span> || ok === <span class="hljs-string">"after-download"</span> || ok === <span class="hljs-literal">true</span>;
    }
  } <span class="hljs-keyword">catch</span> {}
  <span class="hljs-keyword">return</span> res;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createNanoTextSession</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (self.ai?.createTextSession) <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> self.ai.createTextSession();
  <span class="hljs-keyword">if</span> (self.ai?.languageModel?.create) <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> self.ai.languageModel.create();
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Gemini Nano text session not available"</span>);
}

<span class="hljs-comment">// --- Cloud: dynamically discover models for this key ---</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">listModels</span>(<span class="hljs-params">key</span>) </span>{
  <span class="hljs-keyword">const</span> url = <span class="hljs-string">"https://generativelanguage.googleapis.com/v1/models?key="</span> + <span class="hljs-built_in">encodeURIComponent</span>(key);
  <span class="hljs-keyword">const</span> r = <span class="hljs-keyword">await</span> fetch(url);
  <span class="hljs-keyword">if</span> (!r.ok) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"ListModels failed: "</span> + (<span class="hljs-keyword">await</span> r.text()));
  <span class="hljs-keyword">const</span> j = <span class="hljs-keyword">await</span> r.json();
  <span class="hljs-keyword">return</span> (j.models || []).map(<span class="hljs-function"><span class="hljs-params">m</span> =&gt;</span> m.name).filter(<span class="hljs-built_in">Boolean</span>);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">rankModels</span>(<span class="hljs-params">names</span>) </span>{
  <span class="hljs-comment">// Prefer Gemini 1.5 (multimodal), then flash variants, then anything with vision/pro.</span>
  <span class="hljs-keyword">return</span> names
    .filter(<span class="hljs-function"><span class="hljs-params">n</span> =&gt;</span> n.startsWith(<span class="hljs-string">"models/"</span>))              <span class="hljs-comment">// ignore tunedModels, etc.</span>
    .filter(<span class="hljs-function"><span class="hljs-params">n</span> =&gt;</span> !n.includes(<span class="hljs-string">"experimental"</span>))          <span class="hljs-comment">// skip experimental</span>
    .sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> score(b) - score(a));

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">score</span>(<span class="hljs-params">n</span>) </span>{
    <span class="hljs-keyword">let</span> s = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">if</span> (n.includes(<span class="hljs-string">"1.5"</span>)) s += <span class="hljs-number">10</span>;
    <span class="hljs-keyword">if</span> (n.includes(<span class="hljs-string">"flash"</span>)) s += <span class="hljs-number">8</span>;
    <span class="hljs-keyword">if</span> (n.includes(<span class="hljs-string">"pro-vision"</span>)) s += <span class="hljs-number">7</span>;
    <span class="hljs-keyword">if</span> (n.includes(<span class="hljs-string">"pro"</span>)) s += <span class="hljs-number">6</span>;
    <span class="hljs-keyword">if</span> (n.includes(<span class="hljs-string">"vision"</span>)) s += <span class="hljs-number">5</span>;
    <span class="hljs-keyword">if</span> (n.includes(<span class="hljs-string">"latest"</span>)) s += <span class="hljs-number">2</span>;
    <span class="hljs-keyword">return</span> s;
  }
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">tryGenerateForModels</span>(<span class="hljs-params">imageDataUrl, key, models, mimeType</span>) </span>{
  <span class="hljs-keyword">const</span> base64 = imageDataUrl.split(<span class="hljs-string">","</span>)[<span class="hljs-number">1</span>];
  <span class="hljs-keyword">const</span> body = {
    <span class="hljs-attr">contents</span>: [{
      <span class="hljs-attr">parts</span>: [
        { <span class="hljs-attr">text</span>: <span class="hljs-string">"Describe this image briefly in one sentence focusing on the main gesture or symbol."</span> },
        { <span class="hljs-attr">inline_data</span>: { <span class="hljs-attr">mime_type</span>: mimeType || <span class="hljs-string">"image/png"</span>, <span class="hljs-attr">data</span>: base64 } }
      ]
    }]
  };
  <span class="hljs-keyword">let</span> lastErr = <span class="hljs-string">""</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> model <span class="hljs-keyword">of</span> models) {
    <span class="hljs-keyword">const</span> endpoint = <span class="hljs-string">"https://generativelanguage.googleapis.com/v1/"</span> + model + <span class="hljs-string">":generateContent?key="</span> + <span class="hljs-built_in">encodeURIComponent</span>(key);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> r = <span class="hljs-keyword">await</span> fetch(endpoint, { <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>, <span class="hljs-attr">headers</span>: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> }, <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(body)});
      <span class="hljs-keyword">if</span> (!r.ok) { lastErr = <span class="hljs-keyword">await</span> r.text().catch(<span class="hljs-function">()=&gt;</span><span class="hljs-built_in">String</span>(r.status)); <span class="hljs-keyword">continue</span>; }
      <span class="hljs-keyword">const</span> j = <span class="hljs-keyword">await</span> r.json();
      <span class="hljs-keyword">const</span> text = j?.candidates?.[<span class="hljs-number">0</span>]?.content?.parts?.map(<span class="hljs-function"><span class="hljs-params">p</span>=&gt;</span>p.text).join(<span class="hljs-string">" "</span>).trim();
      <span class="hljs-keyword">if</span> (text) <span class="hljs-keyword">return</span> text;
      lastErr = <span class="hljs-string">"Empty response from "</span> + model;
    } <span class="hljs-keyword">catch</span> (e) {
      lastErr = <span class="hljs-built_in">String</span>(e?.message || e);
    }
  }
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"All discovered models failed. Last error: "</span> + lastErr);
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">describeImageWithGemini</span>(<span class="hljs-params">imageDataUrl, apiKey, mimeType = <span class="hljs-string">"image/png"</span></span>) </span>{
  <span class="hljs-keyword">if</span> (!apiKey) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"No API key provided"</span>);

  <span class="hljs-keyword">const</span> models = <span class="hljs-keyword">await</span> listModels(apiKey);
  <span class="hljs-keyword">if</span> (!models.length) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"No models returned for this key. Ensure Generative Language API is enabled and T&amp;Cs accepted in AI Studio."</span>);

  <span class="hljs-keyword">const</span> ranked = rankModels(models);
  <span class="hljs-keyword">if</span> (!ranked.length) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"No usable model names returned (models/*)."</span>);

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> tryGenerateForModels(imageDataUrl, apiKey, ranked, mimeType);
}

<span class="hljs-comment">// --- Key storage (local only) ---</span>
<span class="hljs-keyword">const</span> KEY = <span class="hljs-string">"makaton_demo_gemini_key"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveApiKey</span>(<span class="hljs-params">k</span>) </span>{ <span class="hljs-built_in">localStorage</span>.setItem(KEY, k || <span class="hljs-string">""</span>); }
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loadApiKey</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-built_in">localStorage</span>.getItem(KEY) || <span class="hljs-string">""</span>; }
</code></pre>
<p>Note: This retry system is essential because many users encounter 404 model errors due to the unavailability of certain Gemini versions in every account.</p>
<h3 id="heading-5-the-main-logic-appjs">5. The Main Logic (app.js)</h3>
<p>This script ties everything together: file upload, AI call, meaning mapping, and output display.</p>
<pre><code class="lang-javascript">
<span class="hljs-keyword">import</span> { mapDescriptionToMeaning } <span class="hljs-keyword">from</span> <span class="hljs-string">'./lib/mapping.js'</span>;
<span class="hljs-keyword">import</span> { checkAvailability, createNanoTextSession, describeImageWithGemini, saveApiKey, loadApiKey } <span class="hljs-keyword">from</span> <span class="hljs-string">'./lib/ai.js'</span>;

<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'DOMContentLoaded'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Makaton] DOM ready'</span>);

  <span class="hljs-keyword">const</span> $ = <span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> <span class="hljs-built_in">document</span>.querySelector(s);

  <span class="hljs-comment">// Elements</span>
  <span class="hljs-keyword">const</span> fileInput   = $(<span class="hljs-string">'#file'</span>);
  <span class="hljs-keyword">const</span> preview     = $(<span class="hljs-string">'#preview'</span>);
  <span class="hljs-keyword">const</span> meaningEl   = $(<span class="hljs-string">'#meaning'</span>);
  <span class="hljs-keyword">const</span> outputEl    = $(<span class="hljs-string">'#output'</span>);
  <span class="hljs-keyword">const</span> btnDescribe = $(<span class="hljs-string">'#btnDescribe'</span>);
  <span class="hljs-keyword">const</span> btnType     = $(<span class="hljs-string">'#btnType'</span>);
  <span class="hljs-keyword">const</span> typedBox    = $(<span class="hljs-string">'#typedBox'</span>);
  <span class="hljs-keyword">const</span> typed       = $(<span class="hljs-string">'#typed'</span>);
  <span class="hljs-keyword">const</span> btnUseTyped = $(<span class="hljs-string">'#btnUseTyped'</span>);
  <span class="hljs-keyword">const</span> btnSpeak    = $(<span class="hljs-string">'#btnSpeak'</span>);
  <span class="hljs-keyword">const</span> btnCopy     = $(<span class="hljs-string">'#btnCopy'</span>);
  <span class="hljs-keyword">const</span> statusEl    = $(<span class="hljs-string">'#status'</span>);

  <span class="hljs-keyword">const</span> settings        = $(<span class="hljs-string">'#settings'</span>);
  <span class="hljs-keyword">const</span> btnSettings     = $(<span class="hljs-string">'#btnSettings'</span>);
  <span class="hljs-keyword">const</span> btnCloseSettings= $(<span class="hljs-string">'#btnCloseSettings'</span>);
  <span class="hljs-keyword">const</span> btnSaveKey      = $(<span class="hljs-string">'#btnSaveKey'</span>);
  <span class="hljs-keyword">const</span> apiKeyInput     = $(<span class="hljs-string">'#apiKey'</span>);
  <span class="hljs-keyword">const</span> apiStatus       = $(<span class="hljs-string">'#apiStatus'</span>);

  <span class="hljs-keyword">let</span> currentImageDataUrl = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">let</span> currentImageMime    = <span class="hljs-string">"image/png"</span>;

  <span class="hljs-comment">// Sanity logs</span>
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Makaton] Elements:'</span>, {
    <span class="hljs-attr">fileInput</span>: !!fileInput, <span class="hljs-attr">preview</span>: !!preview, <span class="hljs-attr">outputEl</span>: !!outputEl,
    <span class="hljs-attr">meaningEl</span>: !!meaningEl, <span class="hljs-attr">btnDescribe</span>: !!btnDescribe, <span class="hljs-attr">statusEl</span>: !!statusEl
  });

  <span class="hljs-comment">// Init API key</span>
  <span class="hljs-keyword">if</span> (apiKeyInput) apiKeyInput.value = loadApiKey() || <span class="hljs-string">""</span>;

  <span class="hljs-comment">// --- Helpers ---</span>
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setStatus</span>(<span class="hljs-params">text</span>) </span>{
    <span class="hljs-keyword">if</span> (statusEl) statusEl.textContent = text || <span class="hljs-string">''</span>;
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Makaton][Status]'</span>, text);
  }
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">clearOutputs</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">if</span> (outputEl) outputEl.textContent = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">if</span> (meaningEl) meaningEl.textContent = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">if</span> (btnSpeak) btnSpeak.disabled = <span class="hljs-literal">true</span>;
    <span class="hljs-keyword">if</span> (btnCopy)  btnCopy.disabled  = <span class="hljs-literal">true</span>;
  }
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setOutput</span>(<span class="hljs-params">desc</span>) </span>{
    <span class="hljs-keyword">if</span> (outputEl) outputEl.textContent = desc || <span class="hljs-string">''</span>;
    <span class="hljs-keyword">const</span> meaning = mapDescriptionToMeaning(desc || <span class="hljs-string">''</span>);
    <span class="hljs-keyword">if</span> (meaningEl) meaningEl.textContent = meaning;
    <span class="hljs-keyword">if</span> (btnSpeak) btnSpeak.disabled = !meaning || meaning.includes(<span class="hljs-string">'No direct mapping'</span>);
    <span class="hljs-keyword">if</span> (btnCopy)  btnCopy.disabled  = !meaning;
    setStatus(<span class="hljs-string">'Done.'</span>);
  }
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fileToDataURL</span>(<span class="hljs-params">file</span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> reader = <span class="hljs-keyword">new</span> FileReader();
      reader.onload  = <span class="hljs-function">() =&gt;</span> resolve(reader.result);
      reader.onerror = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> reject(e);
      reader.readAsDataURL(file);
    });
  }
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleFiles</span>(<span class="hljs-params">files</span>) </span>{
    <span class="hljs-keyword">const</span> file = files?.[<span class="hljs-number">0</span>];
    <span class="hljs-keyword">if</span> (!file) { setStatus(<span class="hljs-string">'No file selected.'</span>); <span class="hljs-keyword">return</span>; }
    currentImageMime = file.type || <span class="hljs-string">"image/png"</span>;
    fileToDataURL(file)
      .then(<span class="hljs-function">(<span class="hljs-params">dataUrl</span>) =&gt;</span> {
        currentImageDataUrl = dataUrl;
        <span class="hljs-keyword">if</span> (preview) {
          preview.innerHTML = <span class="hljs-string">`&lt;img alt="preview" src="<span class="hljs-subst">${dataUrl}</span>" /&gt;`</span>;
          preview.classList.remove(<span class="hljs-string">'hidden'</span>);
        }
        setStatus(<span class="hljs-string">'Image loaded. Click "Describe" to continue.'</span>);
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'[Makaton] fileToDataURL error'</span>, err);
        setStatus(<span class="hljs-string">'Could not read the image.'</span>);
      });
  }

  <span class="hljs-comment">// --- File input change ---</span>
  <span class="hljs-keyword">if</span> (fileInput) {
    fileInput.addEventListener(<span class="hljs-string">'change'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Makaton] file input change'</span>);
      handleFiles(e.target.files);
    });
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'[Makaton] #file input not found in DOM.'</span>);
  }

  <span class="hljs-comment">// --- Drag &amp; drop support on preview area ---</span>
  <span class="hljs-keyword">if</span> (preview) {
    preview.addEventListener(<span class="hljs-string">'dragover'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> { e.preventDefault(); preview.classList.add(<span class="hljs-string">'drag'</span>); });
    preview.addEventListener(<span class="hljs-string">'dragleave'</span>, <span class="hljs-function">() =&gt;</span> preview.classList.remove(<span class="hljs-string">'drag'</span>));
    preview.addEventListener(<span class="hljs-string">'drop'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
      e.preventDefault();
      preview.classList.remove(<span class="hljs-string">'drag'</span>);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Makaton] drop'</span>);
      handleFiles(e.dataTransfer?.files);
    });
  }

  <span class="hljs-comment">// --- Describe click ---</span>
  <span class="hljs-keyword">if</span> (btnDescribe) {
    btnDescribe.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[Makaton] Describe clicked'</span>);
      <span class="hljs-keyword">if</span> (!currentImageDataUrl) { setStatus(<span class="hljs-string">'Please upload an image first.'</span>); <span class="hljs-keyword">return</span>; }
      clearOutputs();
      setStatus(<span class="hljs-string">'Checking on-device AI availability…'</span>);

      <span class="hljs-keyword">const</span> avail = <span class="hljs-keyword">await</span> checkAvailability().catch(<span class="hljs-function">() =&gt;</span> ({ <span class="hljs-attr">nanoTextPossible</span>: <span class="hljs-literal">false</span> }));
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> apiKey = loadApiKey();
        <span class="hljs-keyword">if</span> (apiKey) {
          setStatus(<span class="hljs-string">'Using Gemini cloud for image description…'</span>);
          <span class="hljs-keyword">const</span> desc = <span class="hljs-keyword">await</span> describeImageWithGemini(currentImageDataUrl, apiKey, currentImageMime);
          setOutput(desc);
          <span class="hljs-keyword">return</span>;
        }
        <span class="hljs-keyword">if</span> (avail.nanoTextPossible) {
          setStatus(<span class="hljs-string">'No API key found. Using on-device AI (text) for best guess…'</span>);
          <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> createNanoTextSession();
          <span class="hljs-keyword">const</span> desc = <span class="hljs-keyword">await</span> session.prompt(<span class="hljs-string">'Given an image is uploaded by the user (not directly visible to you), infer a likely one-sentence description of a common Makaton sign or symbol a teacher might upload. Keep it generic and safe.'</span>);
          setOutput(desc);
          <span class="hljs-keyword">return</span>;
        }
        setStatus(<span class="hljs-string">'No AI available. Please type a brief description.'</span>);
        <span class="hljs-keyword">if</span> (typedBox) typedBox.classList.remove(<span class="hljs-string">'hidden'</span>);
      } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'[Makaton] Describe error'</span>, err);
        setStatus(<span class="hljs-string">'Description failed: '</span> + (err?.message || err));
        <span class="hljs-keyword">if</span> (typedBox) typedBox.classList.remove(<span class="hljs-string">'hidden'</span>);
      }
    });
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'[Makaton] Describe button not found.'</span>);
  }

  <span class="hljs-comment">// --- Manual typing flow ---</span>
  <span class="hljs-keyword">if</span> (btnType) {
    btnType.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">if</span> (typedBox) typedBox.classList.remove(<span class="hljs-string">'hidden'</span>);
      <span class="hljs-keyword">if</span> (typed) typed.focus();
    });
  }
  <span class="hljs-keyword">if</span> (btnUseTyped) {
    btnUseTyped.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">const</span> text = (typed?.value || <span class="hljs-string">''</span>).trim();
      <span class="hljs-keyword">if</span> (!text) { setStatus(<span class="hljs-string">'Type a description first.'</span>); <span class="hljs-keyword">return</span>; }
      setOutput(text);
    });
  }

  <span class="hljs-comment">// --- Utilities ---</span>
  <span class="hljs-keyword">if</span> (btnSpeak) {
    btnSpeak.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">const</span> text = meaningEl?.textContent?.trim();
      <span class="hljs-keyword">if</span> (!text) <span class="hljs-keyword">return</span>;
      <span class="hljs-keyword">const</span> u = <span class="hljs-keyword">new</span> SpeechSynthesisUtterance(text);
      speechSynthesis.cancel();
      speechSynthesis.speak(u);
    });
  }
  <span class="hljs-keyword">if</span> (btnCopy) {
    btnCopy.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> text = meaningEl?.textContent?.trim();
      <span class="hljs-keyword">if</span> (!text) <span class="hljs-keyword">return</span>;
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> navigator.clipboard.writeText(text);
        setStatus(<span class="hljs-string">'Copied meaning to clipboard.'</span>);
      } <span class="hljs-keyword">catch</span> {
        setStatus(<span class="hljs-string">'Copy failed.'</span>);
      }
    });
  }

  <span class="hljs-comment">// --- Settings modal ---</span>
  <span class="hljs-keyword">if</span> (btnSettings &amp;&amp; settings) btnSettings.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">() =&gt;</span> settings.showModal());
  <span class="hljs-keyword">if</span> (btnCloseSettings &amp;&amp; settings) btnCloseSettings.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">() =&gt;</span> settings.close());
  <span class="hljs-keyword">if</span> (btnSaveKey) {
    btnSaveKey.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
      e.preventDefault();
      <span class="hljs-keyword">const</span> k = apiKeyInput?.value?.trim() || <span class="hljs-string">""</span>;
      saveApiKey(k);
      <span class="hljs-keyword">if</span> (apiStatus) apiStatus.textContent = k ? <span class="hljs-string">"API key saved locally. Try Describe again."</span> : <span class="hljs-string">"Cleared API key. You can still use on-device or typed mode."</span>;
    });
  }

  <span class="hljs-comment">// First status</span>
  setStatus(<span class="hljs-string">'Ready. Upload an image to begin.'</span>);
});
</code></pre>
<p>Let's break down the main sections of the <code>app.js</code> script for the Makaton AI Companion, as there’s a lot going on here:</p>
<ol>
<li><p><strong>Imports and Initial Setup:</strong></p>
<ul>
<li><p>The script imports functions from <code>mapping.js</code> and <code>ai.js</code> to handle mapping descriptions to meanings and AI interactions.</p>
</li>
<li><p>It sets up event listeners for when the DOM content is fully loaded, ensuring all elements are ready for interaction.</p>
</li>
</ul>
</li>
<li><p><strong>Element Selection:</strong></p>
<ul>
<li>It uses a helper function <code>$</code> to select DOM elements by their CSS selectors. This includes file inputs, buttons, and display areas for image previews and outputs.</li>
</ul>
</li>
<li><p><strong>Sanity Logs:</strong></p>
<ul>
<li>It logs the presence of key elements to the console for debugging purposes, ensuring that all necessary elements are found in the DOM.</li>
</ul>
</li>
<li><p><strong>API Key Initialization:</strong></p>
<ul>
<li>It loads any saved API key from local storage and sets it in the input field for user convenience.</li>
</ul>
</li>
<li><p><strong>Helper Functions:</strong></p>
<ul>
<li><p><code>setStatus</code>: Updates the status message displayed to the user.</p>
</li>
<li><p><code>clearOutputs</code>: Clears the output and meaning display areas and disables buttons for speaking and copying.</p>
</li>
<li><p><code>setOutput</code>: Displays the AI-generated description and maps it to a Makaton meaning, enabling buttons if a valid meaning is found.</p>
</li>
<li><p><code>fileToDataURL</code>: Converts an uploaded file to a data URL for image preview and processing.</p>
</li>
<li><p><code>handleFiles</code>: Handles file selection, updating the preview and setting the current image data URL.</p>
</li>
</ul>
</li>
<li><p><strong>File Input Change Handling:</strong></p>
<ul>
<li>It listens for changes in the file input, processes the selected file, and updates the preview area.</li>
</ul>
</li>
<li><p><strong>Drag &amp; Drop Support:</strong></p>
<ul>
<li>It adds drag-and-drop functionality to the preview area, allowing users to drag files directly onto the app for processing.</li>
</ul>
</li>
<li><p><strong>Describe Button Click:</strong></p>
<ul>
<li><p>It handles the "Describe" button click event, checking for an uploaded image and attempting to describe it using either the Gemini API or on-device AI.</p>
</li>
<li><p>If no AI is available, it prompts the user to type a description manually.</p>
</li>
</ul>
</li>
<li><p><strong>Manual Typing Flow:</strong></p>
<ul>
<li>It allows users to manually type a description if AI processing is unavailable or fails, updating the output with the typed text.</li>
</ul>
</li>
<li><p><strong>Utilities:</strong></p>
<ul>
<li><p><code>btnSpeak</code>: Uses the browser's SpeechSynthesis API to read aloud the mapped meaning.</p>
</li>
<li><p><code>btnCopy</code>: Copies the mapped meaning to the clipboard for easy sharing.</p>
</li>
</ul>
</li>
<li><p><strong>Settings Modal:</strong></p>
<ul>
<li>It manages the settings modal for entering and saving the API key, providing feedback on the key's status.</li>
</ul>
</li>
<li><p><strong>Initial Status:</strong></p>
<ul>
<li>It sets the initial status message to guide the user to upload an image to begin the process.</li>
</ul>
</li>
</ol>
<p>This script effectively ties together the user interface, file handling, AI processing, and output display, providing a seamless experience for translating Makaton signs into English meanings.</p>
<h4 id="heading-how-vision-and-language-work-together-here">How Vision and Language Work Together Here</h4>
<p>While working on this project, I started appreciating how computer vision and language understanding complement each other in multimodal systems like this one.</p>
<ul>
<li><p>The vision model (Gemini or Nano) interprets <em>what it sees</em> like hand shapes, gestures, or layout and turns that visual context into descriptive language.</p>
</li>
<li><p>The language mapping logic then interprets those words, infers intent, and finds the closest semantic match (e.g., “help,” “friend,” “eat”).</p>
</li>
<li><p>It’s a collaboration between two forms of understanding (<em>perceptual</em> and <em>semantic</em>) that together allow the AI to bridge the gap between gesture and meaning.</p>
</li>
</ul>
<p>This realization reshaped how I think about accessibility: the best assistive technologies often emerge not from smarter models alone, but from the interaction between modalities like seeing, describing, and reasoning in context.</p>
<h3 id="heading-6-optional-speak-and-copy">6. Optional — Speak and Copy</h3>
<p>To make the app more accessible, I added speech output and a quick copy button:</p>
<pre><code class="lang-javascript">btnSpeak.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> text = meaningEl.textContent.trim();
  <span class="hljs-keyword">if</span> (text) speechSynthesis.speak(<span class="hljs-keyword">new</span> SpeechSynthesisUtterance(text));
});

btnCopy.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> text = meaningEl.textContent.trim();
  <span class="hljs-keyword">if</span> (text) <span class="hljs-keyword">await</span> navigator.clipboard.writeText(text);
});
</code></pre>
<p>This gives users both visual and auditory feedback, especially helpful for learners or educators.</p>
<h2 id="heading-how-to-fix-the-common-issues">How to Fix the Common Issues</h2>
<p>No AI or web integration project runs smoothly the first time – and that’s okay. Here’s a breakdown of the main issues I faced while building the Makaton AI Companion, how I diagnosed them, and how I fixed each one.</p>
<p>These lessons will help anyone trying to integrate Gemini APIs, on-device AI, or local web apps without a full backend.</p>
<h3 id="heading-1-the-cors-error-when-running-with-file">1. The “CORS” Error When Running With <code>file://</code></h3>
<p>When I first opened my <code>index.html</code> directly from my file explorer, Chrome threw several CORS policy errors:</p>
<pre><code class="lang-python">Access to script at <span class="hljs-string">'file:///lib/ai.js'</span> <span class="hljs-keyword">from</span> origin <span class="hljs-string">'null'</span> has been blocked by CORS policy.
</code></pre>
<p>At first this looked confusing, but the reason is simple: modern browsers block JavaScript modules (<code>import/export</code>) when running from <code>file://</code> paths for security reasons.</p>
<p>✅ <strong>Fix:</strong> I realized I needed to serve the files over <strong>HTTP</strong>, not from the file system. So I ran a quick local web server using Python:</p>
<pre><code class="lang-python">python -m http.server <span class="hljs-number">8080</span>
</code></pre>
<p>Then opened:</p>
<pre><code class="lang-python">http://localhost:<span class="hljs-number">8080</span>/index.html
</code></pre>
<p>That single step fixed all the CORS errors and allowed my modules to load correctly.</p>
<h3 id="heading-2-model-not-found-404-from-the-gemini-api">2. “Model Not Found” (404) From the Gemini API</h3>
<p>The next big challenge came from the Gemini API. Even though I had a valid API key, my console showed this error:</p>
<pre><code class="lang-python"><span class="hljs-string">"models/gemini-1.5-flash"</span> <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> found <span class="hljs-keyword">for</span> API version v1beta, <span class="hljs-keyword">or</span> <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> supported <span class="hljs-keyword">for</span> generateContent.
</code></pre>
<p>It turns out Google’s API endpoints can vary slightly depending on your project setup and key permissions.</p>
<p>✅ <strong>Fix:</strong> I rewrote my <code>lib/ai.js</code> script to automatically <strong>try multiple Gemini model endpoints</strong> until it found one that worked. Something like this:</p>
<pre><code class="lang-python">const GEMINI_IMAGE_ENDPOINTS = [
  <span class="hljs-string">"https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent"</span>,
  <span class="hljs-string">"https://generativelanguage.googleapis.com/v1/models/gemini-1.5-pro:generateContent"</span>,
  <span class="hljs-string">"https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash-latest:generateContent"</span>,
];
</code></pre>
<p>And I wrapped it in a loop that stopped once one endpoint succeeded.</p>
<p>Later, I improved it further by listing available models dynamically using<br><code>https://generativelanguage.googleapis.com/v1/models?key=YOUR_KEY</code> and automatically trying whichever ones supported image generation.</p>
<p>That dynamic discovery approach fixed the 404 errors permanently.</p>
<h3 id="heading-3-packaging-a-local-single-file-version"><strong>3. Packaging a Local Single-File Version</strong></h3>
<p>Once I got everything working, I wanted a version that others could test easily without installing Node.js or running build tools.</p>
<p>✅ <strong>Fix:</strong> I bundled the project into a simple zip file containing:</p>
<pre><code class="lang-python">index.html
app.js
lib/ai.js
lib/mapping.js
styles.css
</code></pre>
<p>That way, anyone can just unzip and run:</p>
<pre><code class="lang-python">python -m http.server <span class="hljs-number">8080</span>
</code></pre>
<p>and open <code>localhost:8080</code>.</p>
<p>Everything runs locally in the browser, no server-side code required. This also makes it perfect for demos, classrooms, and so on.</p>
<h3 id="heading-4-debugging-script-import-errors-in-the-console">4. Debugging Script Import Errors in the Console</h3>
<p>Another subtle issue appeared when I noticed this red message:</p>
<pre><code class="lang-python">The requested module <span class="hljs-string">'./lib/mapping.js'</span> does <span class="hljs-keyword">not</span> provide an export named <span class="hljs-string">'mapDescriptionToMeaning'</span>
</code></pre>
<p>That line told me exactly what was wrong: my import and export function names didn’t match. The fix was straightforward:</p>
<pre><code class="lang-python">// app.js
<span class="hljs-keyword">import</span> { mapDescriptionToMeaning } <span class="hljs-keyword">from</span> <span class="hljs-string">'./lib/mapping.js'</span>;
</code></pre>
<p>And then ensuring the mapping file exported it:</p>
<pre><code class="lang-python">// mapping.js
export function mapDescriptionToMeaning(desc) { ... }
</code></pre>
<p>After that, all the pieces connected smoothly.</p>
<p>Using the browser console <strong>as my debugging dashboard</strong> turned out to be the most powerful tool of all. Every fix started by reading and reasoning about those red error lines.</p>
<h2 id="heading-demo-the-makaton-ai-companion-in-action">Demo: The Makaton AI Companion in Action</h2>
<p>Let’s see the Makaton AI Companion in action and understand what’s happening under the hood.</p>
<h3 id="heading-step-1-run-the-app-locally">Step 1: Run the app locally</h3>
<p>Once you’ve downloaded or cloned the project folder, open your terminal in that directory and start a local development server: <code>python -m http.server 8080</code>. Then open your browser and visit: <code>http://localhost:8080/index.html</code></p>
<p>You should see the Makaton AI Companion interface:</p>
<p><img src="https://github.com/tayo4christ/makaton-ai-companion/blob/9cc834fa75f6dcd39866c538ed42255f9006bb51/assets/app-interface.jpg?raw=true" alt="Main interface of the Makaton AI Companion app" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-2-get-your-gemini-api-key">Step 2: Get Your Gemini API Key</h3>
<p>To enable cloud-based image description, you’ll need a <a target="_blank" href="https://aistudio.google.com/welcome?utm_source=PMAX&amp;utm_medium=display&amp;utm_campaign=FY25-global-DR-pmax-1710442&amp;utm_content=pmax&amp;gclsrc=aw.ds&amp;gad_source=1&amp;gad_campaignid=21521981511&amp;gbraid=0AAAAACn9t66nbeHlpP_VYvpWIrX7IJGEW&amp;gclid=EAIaIQobChMIqf-KiIHbkAMV1ZFQBh0KHA8wEAAYASAAEgKLA_D_BwE"><strong>Gemini API key</strong></a> from Google AI Studio.</p>
<p><strong>Here’s how to generate one:</strong></p>
<ol>
<li><p>Visit: <code>https://aistudio.google.com/welcome</code></p>
</li>
<li><p>Click <strong>“Create API key”</strong> and link it to your Google Cloud project (or create a new one).</p>
</li>
<li><p>Copy the key it will look like this: <code>AIzaSyA...XXXXXXXXXXXX</code></p>
</li>
<li><p>Open the Makaton AI Companion in your browser and click the <strong>Settings</strong> button (top left).</p>
</li>
<li><p>Paste your key in the input box and click <strong>Save</strong>.</p>
</li>
</ol>
<p><img src="https://github.com/tayo4christ/makaton-ai-companion/blob/9cc834fa75f6dcd39866c538ed42255f9006bb51/assets/api-key-setting.jpg?raw=true" alt="Setting up the OpenAI API key in the app interface" width="600" height="400" loading="lazy"></p>
<p>You’ll see a confirmation message like this:</p>
<blockquote>
<p><em>“API key saved locally. Try Describe again.”</em></p>
</blockquote>
<p>This means your key is stored safely in localStorage and is only accessible from your browser.</p>
<h3 id="heading-step-3-enable-gemini-nano-for-on-device-ai">Step 3: Enable Gemini Nano for On-Device AI</h3>
<p>If you’re using <a target="_blank" href="https://www.google.com/intl/en_uk/chrome/canary/"><strong>Chrome Canary</strong>,</a> you can run Gemini Nano locally without internet access. This allows the Makaton AI Companion to generate text even when the API key isn’t set.</p>
<h4 id="heading-download-and-install-chrome-canary">Download and Install Chrome Canary:</h4>
<p>Visit the official Chrome Canary download page and install it on your Windows or macOS system. Chrome Canary is a special version of Chrome designed for developers and early adopters, offering the latest features and updates.</p>
<h4 id="heading-enable-gemini-nano">Enable Gemini Nano:</h4>
<p>Open Chrome Canary and type <code>chrome://flags/#prompt-api-for-gemini-nano</code> in the address bar.</p>
<p>Locate the "Prompt API for Gemini Nano" flag in the list. Set this flag to <strong>Enabled</strong>. This action allows Chrome Canary to support the Gemini Nano model for on-device AI processing.</p>
<p>After enabling the flag, relaunch Chrome Canary to apply the changes.</p>
<h4 id="heading-download-the-gemini-nano-model">Download the Gemini Nano Model:</h4>
<p>Open a new tab in Chrome Canary and enter <code>chrome://components</code> in the address bar.</p>
<p>Scroll down to find the <strong>“Optimization Guide”</strong> component. Click on <strong>Check for update</strong>. This action will initiate the download of the Gemini Nano model, which is necessary for running AI tasks locally without an internet connection.</p>
<h4 id="heading-verify-installation">Verify Installation:</h4>
<p>Once the Gemini Nano model is installed, the Makaton AI Companion app will automatically detect it. You should see a message indicating that the app is using on-device AI: <em>“No API key found. Using on-device AI (text) for best guess…”</em></p>
<p>This confirmation means that the app can now generate text descriptions using the Gemini Nano model without needing an API key or internet access.</p>
<p>By following these detailed steps, you ensure that the Gemini Nano model is correctly set up and ready to use for on-device AI processing in the Makaton AI Companion.</p>
<h3 id="heading-step-4-upload-a-makaton-sign-or-symbol">Step 4: Upload a Makaton sign or symbol</h3>
<p>Click <strong>Choose File</strong> to upload any Makaton image (for example, the “help” sign), then press <strong>Describe (Cloud or Nano)</strong>. You’ll immediately see console logs confirming that the app is running correctly and connecting to the Gemini API:</p>
<p><img src="https://github.com/tayo4christ/makaton-ai-companion/blob/9cc834fa75f6dcd39866c538ed42255f9006bb51/assets/console.jpg?raw=true" alt="Console output showing real-time translation logs" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-5-ai-description-and-mapping">Step 5: AI Description and Mapping</h3>
<p>Here’s what happens next:</p>
<ol>
<li><p>The image is read and encoded as Base64.</p>
</li>
<li><p>The Gemini API (cloud or on-device) generates a short visual description.</p>
</li>
<li><p>The description is passed to the <code>mapDescriptionToMeaning()</code> function.</p>
</li>
<li><p>If keywords match an entry in the <code>MAKATON_GLOSSES</code> dictionary, the app displays the corresponding English meaning.</p>
</li>
<li><p>Finally, users can click <strong>Speak</strong> or <strong>Copy</strong> to hear or reuse the translation.</p>
</li>
</ol>
<p>Example outputs:</p>
<p><strong>When no mapping is found:</strong><br>The AI description is accurate but doesn’t yet match a known Makaton keyword.</p>
<p><img src="https://github.com/tayo4christ/makaton-ai-companion/blob/9cc834fa75f6dcd39866c538ed42255f9006bb51/assets/Incorrect-demonstration.jpg?raw=true" alt="Incorrect demonstration showing the model misinterpreting a sign" width="600" height="400" loading="lazy"></p>
<p><strong>After updating the mapping list:</strong><br>Adding new keywords like <code>"help"</code>, <code>"assist"</code>, or <code>"hand over hand"</code> enables correct translation.</p>
<p><img src="https://github.com/tayo4christ/makaton-ai-companion/blob/9cc834fa75f6dcd39866c538ed42255f9006bb51/assets/correct-demonstration.jpg?raw=true" alt="Correct demonstration where the AI accurately recognizes the Makaton sign" width="600" height="400" loading="lazy"></p>
<h3 id="heading-why-this-matters">Why this matters</h3>
<p>This demonstrates how accessible, AI-assisted tools can support communication for people who rely on Makaton. Even when a gesture isn’t recognized, the system provides a structured output and allows users or educators to expand the mapping list making the tool smarter over time.</p>
<h2 id="heading-broader-reflections">Broader Reflections</h2>
<p>Building this project turned out to be much more than a coding exercise for me.<br>It was a meaningful experiment in combining accessibility, natural language processing, and computer vision. These three fields, when brought together, can create real social impact.</p>
<p>While working on it, I began to understand how computer vision and language understanding complement each other in practice. The vision model perceives the world by identifying shapes, gestures, and spatial patterns, while the language model interprets what those visuals mean in human terms.<br>In this project, the artificial intelligence system first sees the Makaton sign, then describes it, and finally maps it to an English word that carries intent and meaning.</p>
<p>This interaction between perception and semantics is what makes multimodal artificial intelligence so powerful. It is not only about recognizing an image or generating text; it is about building systems that connect understanding across different forms of information to make technology more inclusive and human centered.</p>
<p>This realization changed how I think about accessibility technology. True innovation happens not only through smarter models but through the harmony between seeing and understanding, between what an artificial intelligence system observes and how it communicates that observation to help people.</p>
<h3 id="heading-accessibility-meets-ai">Accessibility Meets AI</h3>
<p>Working on this project reminded me that accessibility isn’t just about compliance or assistive devices. It’s also about inclusion. A simple AI system that can describe a hand gesture or symbol in real time can empower teachers, parents, and students who communicate using Makaton or similar systems.</p>
<p>By mapping AI-generated descriptions to meaningful phrases, the app demonstrates how AI can support inclusive education<strong>,</strong> even at small scales. It bridges the communication gap between verbal and nonverbal learners, which is something that traditional translation systems often overlook.</p>
<h3 id="heading-integrating-nlp-and-computer-vision">Integrating NLP and Computer Vision</h3>
<p>On the technical side, this project showed me how naturally computer vision and language understanding complement each other. The Gemini API’s multimodal models were able to analyze an image and produce coherent natural-language sentences, something that older APIs couldn’t do without chaining multiple tools.</p>
<p>By feeding that output into a lightweight NLP mapping function, I was able to simulate a very early-stage symbol-to-language translator the core of my broader research interest in automatic Makaton-to-English translation.</p>
<h3 id="heading-why-local-ai-gemini-nano-matters">Why Local AI (Gemini Nano) Matters</h3>
<p>While the cloud models are powerful, experimenting with Gemini Nano revealed something exciting:<br>on-device AI can make accessibility tools faster, safer, and more private.</p>
<p>In classrooms or therapy sessions, you often can’t rely on stable internet connections or share sensitive student data. Running inference locally means learners’ gestures or symbol images never leave the device, a crucial step toward privacy-preserving accessibility AI.</p>
<p>And since Nano runs directly inside Chrome Canary, it shows how AI is becoming embedded at the browser level, lowering barriers for teachers and developers to build inclusive solutions without needing large infrastructure.</p>
<h3 id="heading-looking-forward">Looking Forward</h3>
<p>This prototype is just a starting point. Future iterations could integrate gesture recognition directly from camera input, support multiple symbol sets, or even learn from user feedback to expand the dictionary automatically.</p>
<p>Most importantly, it reinforces a central belief in my research and teaching journey:</p>
<p><strong>Accessibility innovation doesn’t require massive systems. It starts with curiosity, empathy, and a few lines of purposeful code.</strong></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building the Makaton AI Companion has been one of the most rewarding projects in my AI journey – not just because it worked, but because it proved how accessible innovation can be.</p>
<p>With just a browser, a few lines of JavaScript, and the right API, I was able to combine computer vision, language understanding, and accessibility design into a working system that translates symbols into meaning. It’s a small step toward a future where anyone, regardless of speech or language ability, can be understood through technology.</p>
<p>The project also reinforced something deeply personal to me as a researcher and educator: that AI for accessibility doesn’t need to be complex, expensive, or centralized. It can be lightweight, open, and built with empathy by anyone who’s willing to learn and experiment.</p>
<h3 id="heading-join-the-conversation">Join the Conversation</h3>
<p>If this project inspires you, I’d love to see your own experiments and improvements. Can you make it support live webcam gestures? Could you adapt it for other symbol systems, like PECS or BSL?</p>
<p>Share your ideas in the comments or tag me if you publish your own version. Together, we can grow a small prototype into a community-driven accessibility tool and continue exploring how AI can give more people a voice.</p>
<p>Full source code on GitHub: <a target="_blank" href="https://github.com/tayo4christ/makaton-ai-companion">Makaton-ai-companion</a></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
