<?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:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Kamil Tustanowski on Medium]]></title>
        <description><![CDATA[Stories by Kamil Tustanowski on Medium]]></description>
        <link>https://medium.com/@kamil.tustanowski?source=rss-2c0314596f19------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*FjUyzZJvyo0P5vYya6C4Og.jpeg</url>
            <title>Stories by Kamil Tustanowski on Medium</title>
            <link>https://medium.com/@kamil.tustanowski?source=rss-2c0314596f19------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 15 May 2026 15:52:24 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@kamil.tustanowski/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[AI Agents 101: From Concept to Code (No Frameworks Required)]]></title>
            <link>https://medium.com/@kamil.tustanowski/ai-agents-101-from-concept-to-code-no-frameworks-required-2dfdaf66b6c1?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/2dfdaf66b6c1</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[ai-agent-development]]></category>
            <category><![CDATA[python-programming]]></category>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Sun, 08 Feb 2026 15:05:17 GMT</pubDate>
            <atom:updated>2026-02-09T07:24:26.906Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UMX5ndKApMZYKt3c2Vh3sw.png" /></figure><p>Artificial Intelligence is evolving beyond simple chatbots. Today, we’re witnessing the rise of <strong>AI agents</strong> — autonomous systems that can perceive their environment, make decisions, and take actions to achieve specific goals. Unlike traditional Large Language Models that simply respond to prompts, agents can use tools, learn from interactions, and accomplish complex tasks independently.</p><p>In this post, I’ll explore <strong>who AI agents really are</strong> and show you a super-simple but ready-to-use practical example. But before we go to all of this good stuff:</p><h3>How to Read This Article</h3><p>This is a <strong>hands-on, from-first-principles guide</strong> to AI agents. It’s intentionally detailed, so you don’t need to read it all in one go.</p><p>Choose your path:</p><p><strong>⏱️ Short on time (10–15 minutes)</strong><br>Read:</p><ul><li><em>What is an AI Agent?</em></li><li><em>Why AI Agents Matter</em></li><li><em>Defining the Agent’s Rules (The System Prompt)</em></li><li><em>💡 Pro Insight: The ReAct Pattern</em></li><li><em>The Big Picture</em></li></ul><p>This gives you the <strong>core mental model</strong> without touching the code.</p><p><strong>🧠 Want to really understand agents (35–45 minutes)</strong><br>Read everything above, plus:</p><ul><li><em>Building a Simple Research Agent</em></li><li><em>The Agent Loop</em></li><li><em>Why This Pattern Matters</em></li></ul><p>You’ll understand <strong>how and why</strong> agentic systems work.</p><p><strong>🧑‍💻 Want to build it yourself (60–90 minute)</strong><br>Read the article top-to-bottom and follow the code sections step by step. <br>This path is for readers who want a <strong>working mental model and a runnable agent</strong>.</p><p>You can safely skip or skim setup sections on the first pass and come back later — the article is structured so that each part builds on the previous one without hidden dependencies.</p><p>🤖 The fully functioning agent code is included at the bottom of the article.</p><h3>What is an AI Agent?</h3><p>An AI agent is a software entity that:</p><p>- <strong>Observes</strong> the current state through inputs and data<br>- <strong>Reasons</strong> about available actions and potential outcomes<br>- <strong>Decides</strong> which actions to take based on its goals<br>- <strong>Acts</strong> by executing those decisions, often using external tools<br>- <strong>Learns</strong> and adapts from the results</p><p>Think of an AI agent as a smart assistant that doesn’t just answer questions — it actively solves problems by using available resources.</p><h3>Why AI Agents Matter</h3><p>Traditional AI models are reactive. You ask, they answer. But AI agents are <strong>proactive and adaptive</strong>. They can:</p><p>✅ Complete multi-step tasks autonomously<br>✅ Adapt strategies based on feedback<br>✅ Use specialized tools to extend capabilities<br>✅ Handle complex, real-world scenarios</p><p>This shift represents a fundamental change in how we build intelligent systems.</p><h4>The Example</h4><p>More times than I can remember I was spending hours searching the web while exploring interesting topics. I was looking for reliable sources, websites, blog posts, reddit threads etc. trying to select the best ones. Sometimes I was finding pure gold and sometimes I was just wasting time in rabbit holes. Back then it wasn’t possible to automate this process in a reliable way. For sure not end-to-end.</p><p>This changed in the age of AI. But let’s start from the beginning.</p><p>Brain alone can’t do much without the help of the senses. Let’s run a simple thought experiment and give this brain:</p><ul><li><strong>Legs</strong> — now it can move (note: it can also fall onto the ground)</li><li><strong>Hands</strong> — now it can grab food, rocks, defend itself</li><li><strong>Eyes </strong>— it stops falling into pits and bumping into obstacles, it’s easier to avoid danger, additionally it’s easier to find food and rocks for hands</li><li><strong>Ears</strong> — detecting danger becomes much easier</li><li><strong>Memory</strong> — knowing where to find food, which animals are dangerous and which can be transferred into food, knowing where the home is, no more Groundhog Day resets.</li></ul><p>This is more-or-less (for sure less than more) functioning human. Let’s consider AI agent now:</p><p>LLM is a brain but brain alone can’t do much without the help of the senses. Let’s (again) run a simple thought experiment and give this brain:</p><ul><li><strong>Tools (hands)</strong> — now it can actually <em>do</em> things: call APIs, update databases, write files, send messages, trigger workflows. Without tools, it can only talk about actions.</li><li><strong>Execution environment (legs)</strong> — now it can move work forward: run code, schedule tasks, retry failures, loop, and adapt. It can also fail, get stuck, or crash if not guided properly.</li><li><strong>Observability (eyes)</strong> — logs, metrics, feedback from systems. The agent stops blindly retrying broken actions, notices errors, sees results, and adjusts instead of bumping into the same wall.</li><li><strong>Event signals (ears)</strong> — webhooks, alerts, user input, system events. The agent no longer waits to be prompted; it can react to changes, detect anomalies, and respond to danger early.</li><li><strong>Memory (short + long term)</strong> — context from the current task plus knowledge of past interactions. It remembers what worked, what failed, user preferences, and where “home” is — no more Groundhog Day resets on every prompt.</li></ul><h3>The Analysis</h3><p>We have all the needed bootstrapping and the time has come to make an agent I foreshadowed in the beginning of this article. The agent that can search any requested topic and store any interesting findings on disk. Simple yet useful and powerful if you need versatile investigator at your service.</p><p>First let’s take two steps back and think what we actually want to achieve.</p><p>The Goal — I want to have an assistant who will browse the web in search of topics that I choose. Will do it autonomously and findings will be stored in files.</p><p>Knowing the goal helps with figuring out what we actually need.</p><p>There is no specified framework or service. We will proceed without any help to have better understanding of the process and what this enigmatic Agent really is.</p><p>Having this out of the way, let’s think what we need to achieve our goal. Let’s think what we would need to do to complete this research task. Imagine our boss says to us “Please find interesting news about dogs and save them on disk so I can read them”. How should we tackle this task?</p><ol><li>Think on how to achieve this task.</li><li>Open web browser and find current news about dogs.</li><li>Visit provided webpages.</li><li>Decide which are interesting and fetch details.</li><li>Save the found results onto disk.</li></ol><h3>Building a Simple Research Agent (No Frameworks, Just Python)</h3><p>There’s a lot of noise around “AI agents” right now. Frameworks, diagrams, buzzwords, promises of autonomy.</p><p>⚠️ But if you strip all of that away, an agent is actually a very simple thing:</p><blockquote><strong><em>A loop where a language model reasons, decides what to do next, and uses tools it cannot access directly.</em></strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uTD_QEJn1WxJS2znW7msbA.png" /></figure><p>Now we’ll build from scratch a <strong>small but complete research agent</strong> that can:</p><ul><li>reason about a task</li><li>decide when it needs external information</li><li>search the web</li><li>visit webpages</li><li>save results to disk</li><li>stop when the job is done</li></ul><p>Using Python, LLM, and a clean control loop.</p><h3>Why This Approach?</h3><p>Before jumping into code, it’s important to understand the philosophy here.</p><p>LLMs are extremely good at:</p><ul><li>reasoning</li><li>planning</li><li>summarizing</li><li>deciding <em>what</em> should be done next</li></ul><p>But they can’t:</p><ul><li>browse the web</li><li>write files</li><li>execute code</li><li>interact with the real world</li></ul><p>So instead of pretending the model is autonomous, we treat it like what it really is:</p><blockquote><strong><em>A brain that needs hands, eyes, and memory provided by us.</em></strong></blockquote><p>That’s what tools are.</p><h3>Tooling &amp; Project Setup</h3><p>I’m using uv as the dependency manager and virtual environment tool.</p><p>Why uv?</p><ul><li>extremely fast</li><li>replaces pip, pip-tools, virtualenv</li><li>deterministic installs</li><li>it’s rapidly becoming the new <em>gold standard</em> for Python project and package management</li></ul><h3>Install uv</h3><pre>curl -LsSf https://astral.sh/uv/install.sh | sh</pre><p>Initialize the Project</p><pre>uv init search-agent<br>cd search-agent</pre><p>This creates a new Python project named search-agent with a <strong>fresh virtual environment</strong> and basic setup files.</p><p>Currently the most interesting place in this directory is <strong>pyproject.toml</strong>:</p><pre>[project]<br>name = &quot;search-agent&quot;<br>version = &quot;0.1.0&quot;<br>description = &quot;Add your description here&quot;<br>readme = &quot;README.md&quot;<br>requires-python = &quot;&gt;=3.10&quot;<br>dependencies = []</pre><p>You can update the description. But I will leave it as is and go to the next step.</p><h4>Create a virtual environment</h4><pre>uv venv<br>source .venv/bin/activate</pre><p>A virtual environment keeps each Python project isolated, avoiding version conflicts and making the setup reproducible — so projects stay clean, independent, and predictable.</p><h4>Add Dependencies</h4><p>When we are safely in our own private environment, it’s time to add all the needed dependencies:</p><pre>uv add openai python-dotenv ddgs requests bs4</pre><p>This gives us:</p><ul><li>openai — LLM access</li><li>python-dotenv — environment variables</li><li>ddgs — Dux Distributed Global Search</li><li>requests — HTTP requests</li><li>bs4 — HTML/XML parser</li></ul><p>Don’t worry about this just yet. We are doing this to have it out of the way so we won’t have to revisit this later.</p><p>Your project structure should look like this:</p><pre>search-agent/<br>├── main.py<br>├── pyproject.toml<br>└── .venv/</pre><p>If you open <strong>pyproject.toml</strong> now:</p><pre>[project]<br>name = &quot;search-agent&quot;<br>version = &quot;0.1.0&quot;<br>description = &quot;Add your description here&quot;<br>readme = &quot;README.md&quot;<br>requires-python = &quot;&gt;=3.10&quot;<br>dependencies = [<br>    &quot;bs4&gt;=0.0.2&quot;,<br>    &quot;ddgs&gt;=9.10.0&quot;,<br>    &quot;openai&gt;=1.107.3&quot;,<br>    &quot;python-dotenv&gt;=1.1.1&quot;,<br>    &quot;requests&gt;=2.32.5&quot;,<br>]</pre><p>You will see it’s listing all the dependencies needed for this project.</p><p>Additionally</p><ul><li>.venv – This is your project’s <strong>virtual environment</strong>. It isolates your Python packages from the system Python, preventing version conflicts between projects. Activate it to run project-specific dependencies.</li><li>pyproject.toml – The <strong>project configuration</strong> file. It defines metadata, dependencies, build tools, and scripts, serving as the modern replacement for setup.py and sometimes requirements.txt. uv uses it to manage your project’s packages and setup.</li></ul><h3>Environment Configuration</h3><p>One crucial piece is still missing. Where should we store all the credentials, keys, etc. that we need? The answer is the .env file.</p><p>A .env file is used to <strong>store configuration and secret information</strong> for your project outside of your code. This includes things like API keys, passwords, or other settings that might change depending on the environment (your computer, a server, or a teammate’s machine). By keeping this information in a .env file, your code stays clean, secure, and easy to share.</p><p>For this example we need to provide OPENAI_API_KEY to OpenAI.</p><p>🤝 To make it work you have to create you own .env and add your OPENAI_API_KEY. How to get key → <a href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key">https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key</a>, please let me know if you need any help!</p><pre>OPENAI_API_KEY=sk-...</pre><p>When all of this is done, we just use <strong>load_dotenv()</strong> to load all of the credentials listed in <strong>.env</strong> file.</p><pre>import os<br>from dotenv import load_dotenv</pre><pre>load_dotenv()</pre><pre>if not os.getenv(&quot;OPENAI_API_KEY&quot;):<br>    raise RuntimeError(&quot;OPENAI_API_KEY not found in environment variables&quot;)</pre><p>Fail fast. Silent failures here are painful to debug later.</p><pre>print(os.getenv(&quot;OPENAI_API_KEY&quot;))</pre><p>Make sure that if you add this line you are seeing the key you provided:</p><pre>sk-...</pre><p>If the answer is yes you are set to go 💪!</p><p><strong>⚠️ Why you shouldn’t commit your </strong><strong>.env file</strong><br>The .env file usually contains <strong>sensitive information</strong>, like API keys, passwords, or tokens. If you commit it to a public repository, anyone could see and misuse these secrets. Instead, keep .env local and add it to .gitignore so it stays private, while still allowing your code to read the variables safely.</p><h3>Initializing the OpenAI Client</h3><pre>from openai import OpenAI<br>client = OpenAI()</pre><p>Nothing fancy here — just initializing the official OpenAI client. You don’t have to provide the OPENAI_API_KEY separately because it’s already provided in the <strong>os.getenv(“OPENAI_API_KEY”)</strong>.</p><h3>The LLM Helper Function</h3><p>Now we are getting serious. This function is our single gateway to the model. I’m using gpt-4o-mini because it is enough for our purpose. Also — it’s cheap. If needed you can use any other of the models.</p><pre>def call_llm(messages: list[dict]) -&gt; str:<br>    response = client.chat.completions.create(<br>        model=&quot;gpt-4o-mini&quot;,<br>        messages=messages,<br>        temperature=0<br>    )</pre><pre>return response.choices[0].message.content</pre><h4>Why temperature = 0?</h4><p>Temperature controls how creative or random an AI’s responses are: low makes it predictable and consistent, high makes it more varied and surprising.</p><p>Agents should be:</p><ul><li>predictable</li><li>repeatable</li><li>boring (in a good way)</li></ul><p>Creativity is great for writing stories.<br>Agents need <strong>control</strong>, not surprises.</p><h4>Why messages is a list of dicts?</h4><p>The chat API is designed to support <strong>multi-turn conversations</strong>, not just a single prompt.<br>Each element in the list represents one “message” in the conversation, and must have a <strong>role</strong> and <strong>content</strong>:</p><pre>{<br>    &quot;role&quot;: &quot;system&quot; | &quot;user&quot; | &quot;assistant&quot;,<br>    &quot;content&quot;: &quot;Text of the message&quot;<br>}</pre><p>The role field <strong>describes who is sending the message</strong>. Specifically:</p><ul><li><strong>system</strong> — Instructions for the model (“You are a helpful assistant” etc.)</li><li><strong>user</strong> — Input from the human</li><li><strong>assistant</strong> — Model responses from previous turns</li></ul><p>So the API sees the conversation as a <strong>sequence</strong>, allowing the model to reason across multiple messages.</p><p>Example:</p><pre>messages = [<br>    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a helpful assistant.&quot;},<br>    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;What&#39;s the capital of France?&quot;}<br>]</pre><pre>response = client.chat.completions.create(<br>    model=&quot;gpt-4o-mini&quot;,<br>    messages=messages<br>)</pre><pre>print(response.choices[0].message.content)<br># -&gt; &quot;The capital of France is Paris.&quot;</pre><p>If you wanted a follow-up question:</p><pre>messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;And what about Germany?&quot;})</pre><p>The model <strong>remembers</strong> the previous question and answer because messages is a list of the conversation.</p><h4>Initial State (No conversation yet)</h4><p>The model starts with <strong>just the system prompt</strong>, if there is one. Example:</p><pre>[System]: You are a helpful assistant that explains things clearly.</pre><ul><li><strong>Context content:</strong> Only the system instruction.</li><li><strong>Purpose:</strong> Sets the model’s behavior and tone.</li></ul><h4>After User Message #1</h4><p>You type:</p><pre>[User]: What is the capital of France?</pre><ul><li>The model now <strong>sees both the system prompt and your first message</strong> in its context:</li></ul><pre>[System]: You are a helpful assistant that explains things clearly.<br>[User]: What is the capital of France?</pre><ul><li>The model generates a response using <strong>everything in the context</strong>.</li></ul><pre>[Assistant]: The capital of France is Paris.</pre><ul><li><strong>Context now includes the assistant’s reply</strong>:</li></ul><pre>[System]: You are a helpful assistant that explains things clearly.<br>[User]: What is the capital of France?<br>[Assistant]: The capital of France is Paris.</pre><p>✅ <strong>Observation:</strong> Both your input <em>and</em> the model’s reply are added.</p><h4>After User Message #2</h4><p>You ask a follow-up:</p><pre>[User]: And what about Germany?</pre><ul><li>The model’s <strong>context now includes all previous messages</strong>:</li></ul><pre>[System]: You are a helpful assistant that explains things clearly.<br>[User]: What is the capital of France?<br>[Assistant]: The capital of France is Paris.<br>[User]: And what about Germany?</pre><ul><li>The model generates:</li></ul><pre>[Assistant]: The capital of Germany is Berlin.</pre><ul><li><strong>Context grows</strong> to include this latest assistant reply:</li></ul><pre>[System]: You are a helpful assistant that explains things clearly.<br>[User]: What is the capital of France?<br>[Assistant]: The capital of France is Paris.<br>[User]: And what about Germany?<br>[Assistant]: The capital of Germany is Berlin.</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z724070g5AelMSqscD7lNA.png" /></figure><h4>How Context Changes Over Time</h4><ul><li>Every new user message <strong>adds to the context</strong>.</li><li>Every new model response <strong>also adds to the context</strong>.</li><li>The context is <strong>a running transcript</strong> of the conversation that the model can see to maintain coherence.</li></ul><h4>Summary</h4><ul><li>The <strong>list</strong> represents the <strong>entire conversation history</strong>.</li><li>Each <strong>dict</strong> is a message with role + content.</li><li>This allows <strong>multi-turn reasoning</strong>, which is essential for building agents.</li></ul><h3>Multi-Turn Reasoning</h3><p>Multi-turn reasoning means that an AI doesn’t treat each prompt as an isolated question. Instead, it <strong>maintains context</strong>, <strong>tracks intermediate conclusions</strong>, and <strong>adjusts its next steps</strong> based on previous turns in the conversation.</p><p>Think of it as the difference between:</p><ul><li>Answering one question at a time and forgetting everything else</li><li>Participating in an ongoing discussion, where each response depends on what was already said and learned</li></ul><h3>Adding Tools</h3><p>Tools are add-ons that an agent can use to perform its tasks. In this example, the tools allow the agent to browse and read information on the internet, as well as store it to disk.</p><h4>Search Web Tool</h4><p>The LLM cannot browse the web on its own. We have to do it for it. In order to do this we use <strong>Dux Distributed Global Search</strong> <a href="https://github.com/deedy5/ddgs">https://github.com/deedy5/ddgs</a> . There are other more sophisticated and powerful services but we just need a simple web search functionality, and huge advantage of this one is — it’s simple and doesn’t require account &amp; API key. It’s perfect to use in this publication.</p><p>First we need to download the library, but since we did it already before we can just go ahead and jump to implementation. We import DDGS:</p><pre>from ddgs import DDGS</pre><p>…and create a short function:</p><pre>def search_web(query: str):<br>    results = []</pre><pre>    with DDGS() as ddgs:<br>        for r in ddgs.text(query, max_results=5):<br>            results.append({<br>                &quot;title&quot;: r[&quot;title&quot;],<br>                &quot;url&quot;: r[&quot;href&quot;],<br>                &quot;snippet&quot;: r[&quot;body&quot;],<br>            })<br>    return results</pre><p>This function lets an application query the web in real time to gather the information. It sends a search request to DDGS which collects selected set of relevant results (max <strong>5</strong> in this example). It will be stored in the context and later on passed to the page fetching tool.</p><p>It’s important to note that this tool only accepts a query and performs a web search. It simply produces a string containing the retrieved data — there is no reasoning or LLM involvement at this stage.</p><p>Example:</p><pre>🤖 LLM Reply SEARCH: &quot;most interesting events 2025&quot;<br>🛜 web_search - start<br>=============================<br>🔍 query &quot;most interesting events 2025&quot;<br>🧾 title 2025 - Wikipedia<br>🔗 url <a href="https://en.wikipedia.org/wiki/2025">https://en.wikipedia.org/wiki/2025</a><br>📋 snippet 8 hours ago - January 13 – C/2024 G3 (ATLAS), a non-periodic comet, reaches perihelion, and is dubbed The Great Comet of 2025 .<br>=============================<br>🔍 query &quot;most interesting events 2025&quot;<br>🧾 title 2025 in the United States - Wikipedia<br>🔗 url <a href="https://en.wikipedia.org/wiki/2025_in_the_United_States">https://en.wikipedia.org/wiki/2025_in_the_United_States</a><br>📋 snippet 3 days ago - Idaho Governor Brad Little formally signs a bill into law that permits the death penalty for rape and sexual abuse of children younger than 12 years of age . The bill will become effective on July 1, 2025. This law could ultimately challenge the precedent of Kennedy v.<br>=============================<br>🔍 query &quot;most interesting events 2025&quot;<br>🧾 title Ten Most Significant World Events in 2025 | Council on Foreign Relations<br>🔗 url <a href="https://www.cfr.org/articles/ten-most-significant-world-events-2025">https://www.cfr.org/articles/ten-most-significant-world-events-2025</a><br>📋 snippet As 2025 comes to a close, here are the ten most notable world events of the year.<br>=============================<br>🔍 query &quot;most interesting events 2025&quot;<br>🧾 title 2025: Facts &amp; Events That Happened in This Year - The Fact Site<br>🔗 url <a href="https://www.thefactsite.com/year/2025/">https://www.thefactsite.com/year/2025/</a><br>📋 snippet January 1, 2023 - 2025 was a year marked by major events, including Trump&#39;s return, the first American pope, climate crises, AI advancements, and inspiring global efforts .<br>=============================<br>🔍 query &quot;most interesting events 2025&quot;<br>🧾 title 2025: Year in Review | Pop Culture, Images, Current Events, News, &amp; Timeline | Britannica<br>🔗 url <a href="https://www.britannica.com/topic/2025-Year-in-Review">https://www.britannica.com/topic/2025-Year-in-Review</a><br>📋 snippet December 22, 2025 - A number of firsts also made headlines in 2025. Japan and Namibia inaugurated their first women heads of government —Sanae Takaichi and Netumbo Nandi-Ndaitwah, respectively. On the space front, the first successful soft landing of a commercial ...<br>🛜 web_search - finish - 5 results</pre><p>The LLM says that it want to search for phrase “<em>most interesting events 2025”</em>, we invoke the search and provide the response. Including 5 websites with potential answers.</p><h4>Fetch Webpage Tool</h4><p>When search_web<strong> </strong>tool is done searching for appropriate webpage fetch_webpage<strong> </strong>tool is used to read the contents of the webpage and get actual response.</p><pre>import requests<br>from bs4 import BeautifulSoup</pre><pre>def fetch_webpage(url):<br>    &quot;&quot;&quot;<br>    Fetch provided webpage<br>    &quot;&quot;&quot;<br>    html = requests.get(url).text<br>    soup = BeautifulSoup(html, &quot;html.parser&quot;)    <br>    return soup.get_text()</pre><p>Again, this is a super simple example. This is enough for the sake of this experiment but bear in mind that while working on production application you can and should <strong>do better</strong>.</p><p>⚠️ In a real scenario, this often dumps 5,000 or more words of navigation menus and footer links into the LLM context which is not great. I don’t want to complicate this example but please have this in mind and for production use a bit more sophisticated solution.</p><p>Example:</p><pre>🤖 LLM Reply FETCH: <a href="https://en.wikipedia.org/wiki/2025">https://en.wikipedia.org/wiki/2025</a><br>🌍 FETCH <a href="https://en.wikipedia.org/wiki/2025">https://en.wikipedia.org/wiki/2025</a></pre><p>The LLM says that it would like to use fetch tool to fetch contents of <a href="https://en.wikipedia.org/wiki/2025">https://en.wikipedia.org/wiki/2025</a> and <strong>we</strong> trigger the fetch_webpage function.</p><h4>Save-to-Disk Tool</h4><pre>def save_to_disk(text: str) -&gt; str:<br>    with open(&quot;search_results.txt&quot;, &quot;w&quot;) as f:<br>        f.write(text)</pre><p>This tool is a textbook example of the <strong>“Single Responsibility Principle”</strong> in agentic design. It performs one action: taking a text and persisting it to a local file.</p><p>Agents work best when tools do <em>one thing only</em>.</p><p>Example <strong>search_results.txt</strong>:</p><pre>&quot;In 2025, Cardinal Robert Prevost became Pope Leo XIV, marking the first time <br>a pope was from North America. Prevost, originally from Chicago, was elected <br>after the sudden death of Pope Francis. His election is significant as it <br>reflects a shift in the leadership of the Catholic Church and highlights the <br>increasing influence of North American clergy within the Vatican.&quot;</pre><pre>&quot;The United States and Israel launched a military operation against Iran&#39;s <br>nuclear facilities in June 2025, known as the Twelve-Day War. This conflict <br>was sparked by Israel&#39;s concerns over Iran&#39;s nuclear ambitions and resulted in<br>significant military engagements, including airstrikes and missile attacks. <br>The operation underscored the ongoing tensions in the Middle East and the <br>complexities of international relations involving nuclear proliferation.&quot;</pre><pre>&quot;In 2025, China demonstrated its economic power by weaponizing its dominance <br>over rare-earth minerals, crucial for various industries. Following U.S. <br>tariffs, China halted exports of these minerals to the U.S., showcasing its <br>ability to leverage economic resources in geopolitical conflicts. This event <br>marked a significant moment in the U.S.-China relationship and highlighted the<br>strategic importance of rare-earth materials in global trade.&quot;</pre><pre>The three most interesting and incredible confirmed events from 2025 include <br>the election of Cardinal Robert Prevost as Pope Leo XIV, the military <br>confrontation between the U.S., Israel, and Iran over nuclear facilities, and <br>China&#39;s strategic use of rare-earth minerals in international relations.</pre><h3>Defining the Agent’s Rules (The System Prompt)</h3><p>This is the most important part of the whole system.</p><pre>You are a research assistant.</pre><pre>If you need web data:<br>SEARCH:&lt;query&gt;<br>    <br>If you have a URL:<br>    <br>FETCH:&lt;url&gt;<br>IMPORTANT: When you receive search results with multiple URLs, you MUST try to FETCH all relevant URLs before doing another SEARCH.<br>FETCH only one URL at a time when done go to next URL.<br>Check all the URLs from the search results first. Only search again if none of<br>the URLs contain useful information. If a page can&#39;t be read or is irrelevant, <br>move on to the next URL from your search results. <br>Do not use URL with bad content again.<br>If URLs are provided, base your answer only on their content.    </pre><pre>Save useful info with:<br>SAVE:&lt;text&gt;</pre><pre>Final answer must be based only on verified sources.<br>After saving, provide the final answer based only on verified sources.<br>Do not provide final answer if answer wasn&#39;t saved.</pre><p>Every agent starts with a system prompt. This is not just a <em>“role description”</em> — it’s the agent’s <strong>operating rules</strong>.</p><p>The KEYWORD:value format isn’t arbitrary. LLMs have seen this pattern millions of times in markdown and config files. Compare to alternatives: — JSON like {“action”: “search”} — the model forgets quotes or adds unexpected fields — Function syntax like search(“dogs”) — confuses describing code vs outputting commands The colon creates a clear parsing boundary. Even if the LLM adds text, it works: SEARCH: let me look that up: dogs → extracts everything after the first colon.</p><p>In our case, the system prompt defines four critical behaviors:</p><ul><li><strong>When to search</strong> for external information</li><li><strong>When to gather </strong>concrete information from found webpages</li><li><strong>When to save</strong> gathered knowledge</li><li><strong>When to stop</strong> and return a final answer</li></ul><p>This prompt <strong>forces the model to externalize its decisions</strong> in a structured way.</p><p>Instead of silently “thinking” about whether it needs more data, the model <strong>must</strong> explicitly say:</p><ul><li>SEARCH: ... , FETCH: ...→ I need more information</li><li>SAVE: ... → I’ve gathered something worth persisting</li><li>anything else → I’m done</li></ul><p>This is the core idea behind tool-using agents.</p><p>Notice what we’re doing <strong>and not doing</strong>:</p><p>❌ We do NOT describe Python functions<br>❌ We do NOT mention what we will use to search, fetch<br>❌ We do NOT expose implementation details</p><p>✅ We define <strong>communication protocols</strong></p><p>The LLM learns:</p><ul><li>how to ask for information</li><li>how to store results</li><li>when it is allowed to finish</li></ul><p>This keeps the agent <strong>flexible and model-agnostic</strong>.</p><h3>💡 Pro Insight: The ReAct Pattern</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5wHy0GQDOVzVCceiSZOS8w.png" /></figure><p>What we are implementing here is a simplified version of the <strong>ReAct (Reason + Act)</strong> pattern. This is a foundational concept in AI engineering.</p><p>Instead of just “guessing” an answer, the model follows a cycle:</p><ol><li><strong>Thought:</strong> The LLM analyzes the task (e.g., “I need to find 2025 events”).</li><li><strong>Action:</strong> The LLM selects a tool via our protocol (e.g., SEARCH:2025 events).</li><li><strong>Observation:</strong> The LLM sees the tool’s output and updates its understanding.</li><li><strong>Repeat:</strong> The LLM looks at the results and decides its next move.</li></ol><p>By using <strong>Protocols</strong> (SEARCH:, FETCH:) instead of describing <strong>Python functions</strong>, we decouple the &quot;Brain&quot; from the &quot;Tools&quot;:</p><ul><li><strong>From:</strong> “Call the dogs_search function with a string argument.&quot; — Brittle &amp; confusing</li><li><strong>To:</strong> “If you need data, output SEARCH:&lt;query&gt;.&quot; — Clean &amp; robust</li></ul><p>This abstraction allows the agent to focus on <strong>logic</strong> while your code handles the <strong>heavy lifting</strong>.</p><h4>Why this works:</h4><ul><li><strong>Terminology:</strong> It introduces <em>ReAct</em>, which is a <em>gold star</em> keyword for anyone interviewing for AI roles or reading research papers.</li><li><strong>Visual Aid:</strong> The diagram tag helps the reader visualize the infinite loop you are about to show in the code.</li><li><strong>Clarity:</strong> It reinforces why you chose the specific syntax in your system prompt.</li></ul><h3>The Big Picture</h3><p>This is the mysterious, omnipotent <strong>loop</strong> that makes the agent “<em>tick</em>”. I will be explaining it step by step in a moment but I think it’s easier to unpack it knowing the full picture upfront.</p><pre>def research_agent(task: str) -&gt; str:<br>    &quot;&quot;&quot;<br>    A simple agent loop:</pre><pre>    1. The model reasons about the task<br>    2. Decides whether to use a tool<br>    3. Consumes tool output<br>    4. Stops when the goal is achieved<br>    &quot;&quot;&quot;</pre><pre>  system_prompt = &quot;&quot;&quot;<br>  ☝️ The system prompt shown above goes here...<br>  &quot;&quot;&quot;<br></pre><pre>  messages = [<br>      {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt},<br>      {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: task}<br>  ]</pre><pre>  while True:<br>      reply = call_llm(messages)<br>      messages.append({&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: reply})<br>    <br>     if reply.startswith(&quot;SEARCH:&quot;):<br>          query = reply.replace(&quot;SEARCH:&quot;, &quot;&quot;).strip()<br>          search_result = search_web(query)</pre><pre>          messages.append({<br>              &quot;role&quot;: &quot;user&quot;,<br>              &quot;content&quot;: f&quot;Search result:\n{search_result}&quot;<br>           })</pre><pre>      elif reply.startswith(&quot;FETCH:&quot;):<br>          query = reply.replace(&quot;FETCH:&quot;, &quot;&quot;).strip()<br>          fetch_webpage_result = fetch_webpage(query)</pre><pre>          messages.append({<br>            &quot;role&quot;: &quot;user&quot;,<br>            &quot;content&quot;: f&quot;Fetch result:\n{fetch_webpage_result}&quot;<br>          })</pre><pre>      elif reply.startswith(&quot;SAVE:&quot;):<br>          text = reply.replace(&quot;SAVE:&quot;, &quot;&quot;).strip()<br>          save_to_disk(text)</pre><pre>          messages.append({<br>              &quot;role&quot;: &quot;user&quot;,<br>              &quot;content&quot;: f&quot;Save content:\n{reply}&quot;<br>          })</pre><pre>      else:<br>          return reply</pre><p>Now let’s go step by step and see what is going on here.</p><h3>Messages: The Agent’s Working Memory</h3><p>The messages list acts as the agent’s short-term memory.</p><p>The initial context consists of the system prompt and the task that we want agent to accomplish. It’s created right before the loop is started:</p><pre>messages = [<br>    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt},<br>    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: task}<br>]</pre><p>Later on it’s updated when actions are used:</p><ul><li>when search did finish</li></ul><pre>messages.append({<br>    &quot;role&quot;: &quot;user&quot;,<br>    &quot;content&quot;: f&quot;Search result:\n{search_result}&quot;<br>})</pre><ul><li>when fetch did finish</li></ul><pre>messages.append({<br>    &quot;role&quot;: &quot;user&quot;,<br>    &quot;content&quot;: f&quot;Fetch result:\n{fetch_webpage_result}&quot;<br>})</pre><ul><li>when findings are saved to disk</li></ul><pre>messages.append({<br>    &quot;role&quot;: &quot;user&quot;,<br>    &quot;content&quot;: f&quot;Save content:\n{reply}&quot;<br>})</pre><p>Every step — model replies, tool outputs, saved content — is appended back into this list.</p><p>This means:</p><ul><li>The model sees <strong>its own past decisions</strong></li><li>Tool outputs become <strong>part of the context</strong></li><li>Reasoning unfolds across turns</li></ul><p>This is what enables <strong>multi-step reasoning</strong> without any hidden state.</p><p>I described in detail how context works in “Why messages is a list of dicts?” section. Revisiting this piece will help if feeling lost.</p><h3>The Agent Loop</h3><p>At the heart of the agent is a simple loop:</p><pre>while True:<br>    reply = call_llm(messages)<br>    messages.append({&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: reply})</pre><p>Each iteration follows the same pattern:</p><ol><li><strong>Decide</strong> — The model decides what to do next</li><li><strong>Act</strong> — A tool is invoked if requested</li><li><strong>Observe</strong> — Tool output is fed back to the model</li></ol><p>This mirrors the classic agent architecture:</p><blockquote>Think → Act → Observe → Repeat</blockquote><h4>Tool Invocation Without Magic</h4><p>Notice that there is <strong>no hardcoded logic</strong> deciding when to search, fetch or save. Instead, tool usage is driven entirely by the model’s output:</p><pre>if reply.startswith(&quot;SEARCH:&quot;):<br>    query = ...<br>    search_result = search_web(query)</pre><p>The agent does not “call tools automatically.”<br>It <strong>asks for them explicitly</strong>, using a format defined in the system prompt.</p><p>This makes the system:</p><ul><li>Transparent</li><li>Debuggable</li><li>Model-agnostic</li></ul><p>You can log every decision and see exactly <em>why</em> a tool was used.</p><h4>Feeding Reality Back Into the Model</h4><p>After a search, fetch or save operation, the result is injected back into the conversation:</p><pre>messages.append({<br>    &quot;role&quot;: &quot;user&quot;,<br>    &quot;content&quot;: f&quot;Search result:\n{search_result}&quot;<br>})</pre><p>This step is crucial.</p><p>From the model’s perspective:</p><ul><li>Tool output is just <strong>new context</strong></li><li>There is no distinction between “user input” and “tool output”</li><li>Everything becomes part of the reasoning space</li></ul><p>This is how agents ground themselves in reality.</p><h4>Knowing When to Stop</h4><p>Probably the most underrated part of agent design is <strong>termination</strong>.</p><p>In this implementation, the rule is simple:</p><pre>else:<br>    return reply</pre><p>If the model doesn’t request a tool, the agent assumes:</p><blockquote><em>“The goal has been achieved.”</em></blockquote><p>This works because the system prompt clearly defines:</p><ul><li><em>When tools should be used</em></li><li><em>What a final answer looks like</em></li></ul><p>A well-written system prompt eliminates the need for complex stopping logic.</p><h3>Why This Pattern Matters</h3><p>This small function demonstrates several core principles of agentic AI:</p><ul><li><strong>Explicit decision-making</strong></li><li><strong>Tool use via language, not APIs</strong></li><li><strong>State built entirely from context</strong></li><li><strong>Predictable, inspectable behavior</strong></li></ul><p>Once you understand this loop, you understand <strong>how agents actually work</strong>.</p><h3>Triggering</h3><p>On the top level we just feed question to the agent and print the results afterwards.</p><pre>if __name__ == &quot;__main__&quot;:</pre><pre>    result = research_agent(<br>        &quot;Find me the 3 most interesting and most incredible,&quot;<br>        &quot;but real and confirmed events from 2025.&quot;<br>        &quot;Save each fact with a short explanation&quot;<br>    )</pre><pre>    print(&quot;\nFinal answer:\n&quot;)<br>    print(result)</pre><p>📄 A nice next step would be to let the user pass the prompt via the command line when launching the agent. If you feel like tinkering a bit, try this as a small home assignment — totally optional, just a chance to get your hands dirty.</p><h3>Using the agent</h3><p>In order to use the agent we first have to go to the newly created folder and switch to the virtual environment configured by the <strong>uv</strong> (if not enabled already):</p><pre>source .venv/bin/activate</pre><p>And start the agent:</p><pre>uv run main.py</pre><p>When you are done using the agent remember to deactivate the environment:</p><pre>deactivate</pre><h3>Sample Search</h3><p>Now let’s see why do we even bother. When we ask plain LLM:</p><blockquote>Find me 3 the most interesting and most incredible, but real and confirmed events from 2025.<br>Save each fact with a short explanation</blockquote><p>thegpt-4o-mini responds:</p><blockquote>I’m sorry, but I can’t provide real events from 2025 as my training only includes information up until October 2023, and I don’t have access to future events or developments. However, I can help you brainstorm potential future scenarios or discuss trends that might lead to interesting events in 2025 based on current knowledge. Would you like to explore that instead?</blockquote><p>Like I said before, LLM is just a brain.</p><p>Now let’s see what our Search Agent can do when asked the same question:</p><pre>[<br>   {<br>      &quot;role&quot;:&quot;system&quot;,<br>      &quot;content&quot;:&quot;\n    You are a research assistant.\n    If you need web data:\n    SEARCH:&lt;query&gt;\n    \n    If you have a URL:\n    \n    FETCH:&lt;url&gt;\n    IMPORTANT: When you receive search results with multiple URLs, you MUST try to FETCH all relevant URLs before doing another SEARCH.\n    FETCH only one URL at a time when done go to next URL.\n    Check all the URLs from the search results first. Only search again if none of the URLs contain useful information.\n    If a page can&#39;t be read or is irrelevant, move on to the next URL from your search results. \n    Do not use URL with bad content again.\n\n    If URLs are provided, base your answer only on their content.    \n    Final answer must be based only on verified sources.\n    Save useful info with:\n    SAVE:&lt;text&gt;\n\n    After saving, provide the final answer based only on verified sources.\n    Do not provide final answer if answer wasn&#39;t saved.\n    &quot;<br>   },<br>   {<br>      &quot;role&quot;:&quot;user&quot;,<br>      &quot;content&quot;:&quot;Find me the 3 most interesting and most incredible,but real and confirmed events from 2025.Save each fact with a short explanation&quot;<br>   },<br>   {<br>      &quot;role&quot;:&quot;assistant&quot;,<br>      &quot;content&quot;:&quot;SEARCH: \&quot;most interesting events 2025\&quot;&quot;<br>   },<br>   {<br>      &quot;role&quot;:&quot;user&quot;,<br>      &quot;content&quot;:&quot;Search result:\n[{&#39;title&#39;: &#39;2025 - Wikipedia&#39;, &#39;url&#39;: &#39;https://en.wikipedia.org/wiki/2025&#39;, &#39;snippet&#39;: &#39;The year saw an escalation of major ongoing armed conflicts, including the Gaza war, marked by famine and a humanitarian crisis; the Russian invasion of Ukraine ... Events · January · March · April&#39;}, {&#39;title&#39;: &#39;Ten Most Significant World Events in 2025 - Council on Foreign Relations&#39;, &#39;url&#39;: &#39;https://www.cfr.org/articles/ten-most-significant-world-events-2025&#39;, &#39;snippet&#39;: &#39;Dec 23, 2025 · Ten Most Significant World Events in 2025 · 10.Cambodia and Thailand Clash · 9. Cardinal Robert Prevost Becomes Pope Leo XIV · 8.India and ...&#39;}, {&#39;title&#39;: &#39;2025: Year in Review | Pop Culture, Images, Current Events ... - Britannica&#39;, &#39;url&#39;: &#39;https://www.britannica.com/topic/2025-Year-in-Review&#39;, &#39;snippet&#39;: &#39;Dec 22, 2025 · In pop culture news, Taylor Swift became engaged to Travis Kelce, Anora was the big winner at the Oscars, and Beyoncé finally won the Grammy ...&#39;}, {&#39;title&#39;: &#39;Historical Events in 2025 - On This Day&#39;, &#39;url&#39;: &#39;https://www.onthisday.com/events/date/2025&#39;, &#39;snippet&#39;: &#39;Historical Events in 2025 · Youngest Ever Darts Champion · Presidential Medal of Freedom · Trudeau Promises to Resign · No Fact-Checking on Facebook and Instagram.&#39;}, {&#39;title&#39;: &#39;15 Events to Watch in 2025 | Opus Agency&#39;, &#39;url&#39;: &#39;https://www.opusagency.com/article/15-events-to-watch-in-2025&#39;, &#39;snippet&#39;: &#39;World Experience Summit (WXO)— London, April 29-May 1, 2025 · C2 Montréal — May 20-22, 2025 · Future of Everything Festival — NYC, May 28-29, 2025 · Web Summit ( ...&#39;}]&quot;<br>   },<br>   {<br>      &quot;role&quot;:&quot;assistant&quot;,<br>      &quot;content&quot;:&quot;FETCH: https://www.cfr.org/articles/ten-most-significant-world-events-2025&quot;<br>   },<br>   {<br>      &quot;role&quot;:&quot;user&quot;,<br>      &quot;content&quot;:&quot;Fetch result:\nTen Most Significant World Events in 2025 | Council on Foreign RelationsSkip to contentSupport CFRMember LoginRegionsTopicsResearchExpertsEventsArticlesThe Water&#39;s EdgeTen Most Significant World Events in 2025ShareDiplomacy and International InstitutionsTen Most Significant World Events in 2025As 2025 comes to a close, here are the ten most notable world events of the year.\\xa0\\xa0\nDisplaced Sudanese flee Zamzam displacement camp in North Darfur, Sudan, after attacks by the Rapid Support Forces, April 15, 2025.\nREUTERS/Stringer/File PhotoBy experts and staffPublishedDecember 23, 2025  9:48\u202fa.m.ExpertsBy James M. LindsayMary and David Boies Distinguished Senior Fellow in U.S. Foreign PolicyShareSign up to receive CFR President Mike Froman’s analysis on the most important foreign policy story of the week, delivered to your inbox every Friday. Subscribe to The World This Week.Anyone\\xa0hoping that 2025 would provide a break from what was\\xa0an exhausting 2024 on the world stage came away disappointed. The past twelve months have been a trying time for international cooperation, as the forces of conflict and contention grew stronger and the end of the American led world order more clearly came into view. Unlike 2024, when the pageantry of the Summer Olympics and beauty of the host city Paris reminded everyone of what cooperation and collaboration can accomplish, 2025 provided few instances of inspiration. One can only hope that 2026 will surprise us in a good way. But before we jump to the new year, here are my top ten most significant world events in 2025. You may want to read what follows closely. Many of these stories will continue to make news in 2026.\u202f\\xa010.Cambodia and Thailand ClashCountries can come to blows even when one is substantially larger and better equipped than the other. A case in point is the conflict that erupted in July between Cambodia and Thailand. With a population of just over 17 million, Cambodia is far smaller and militarily weaker than its neighbor Thailand, which has a population four times larger. The July fighting, the worst in decades, ostensibly was about a century-old border dispute. In particular, Cambodia and Thailand both claim ownership of Prasat Ta Muen Thom, an ancient temple perched in the forested Dangrek Mountains that divide the two countries. But complicated domestic politics on both sides of the border pushed those latent tensions into open conflict. The initial fighting killed several dozen people and displaced tens of thousands. President Donald Trump intervened and threatened to halt trade negotiations with Cambodia and Thailand unless the fighting stopped. Both sides then grudgingly agreed to a ceasefire. A formal deal was signed in late October in Kuala Lumpur on the margins of the annual ASEAN Summit. The agreement, however, did not stick. A landmine killed four Thai soldiers in November, prompting Bangkok to suspend parts of the agreement. The fighting escalated earlier this month when Thai fighter jets bombed targets in Cambodia. Thailand’s new prime minister, whose predecessor was\\xa0ousted for being too conciliatory\\xa0to\\xa0Phnom Penh,\\xa0has vowed that “if the fighting is to stop, Cambodia must follow the course of action set by Thailand.” That sounds like a recipe for continued fighting.\\xa0\\xa09. Cardinal Robert Prevost Becomes Pope Leo XIVThe Roman Catholic Church has had 267 popes. Until 2025, not one of them had been from North America, let alone the United States. That changed in May. On April 21, the ailing Pope Francis died suddenly after suffering a stroke. He had been pope for a dozen years. The Roman Catholic Church then entered a period it calls the\\xa0interregnum, which lasts until a new pope is selected. Following time-honored tradition, the 133 cardinals eligible to participate assembled in Rome for\\xa0a conclave, the formal process for selecting the next pope. The first three ballots saw black smoke emerge from the Sistine Chapel’s chimney, signifying that no one had achieved the\\xa0 necessary two-thirds support. Then on the fourth ballot, the smoke turned white. The conclave had selected Cardinal Robert Prevost. Born and raised on the South Side of Chicago and a graduate of Villanova University, he spent more than two decades in Peru, first doing missionary work and then as Bishop of Chiclayo. In 2023, he moved to Rome to become prefect of the\u202fDicastery for Bishops, which oversees the selection of most bishops,\u202fand was subsequently made a cardinal. Prevost became the first member of the Augustinian religious order, which was founded in 1244, to become pope. Upon being installed, he took the name Leo XIV to recall the example of Pope Leo XIII, the author of the 1891 encyclical\u202fRerum Novarum (“Revolutionary Change”) that called for improving the lot of the working class.\\xa0\\xa08.India and Pakistan ClashIn 2000, President Bill Clinton\\xa0called Kashmir “the most dangerous place in the world.” The simmering tensions between India and Pakistan over the region erupted into open conflict in May, two weeks after five terrorists killed twenty-six people near Pahalgam in Indian-administered Jammu and Kashmir. Lashkar-e-Taiba, the Pakistani-based terrorist organization that carried out the 2008\\xa0Mumbai\\xa0terrorist attack that killed 175 people, likely sponsored the Pahalgam attack. In 2008, India declined to retaliate. This time, however, it struck what\\xa0it\\xa0called “terrorist infrastructure” inside Pakistan. Pakistan responded with drone and missile strikes against Indian military bases and shot down two of India’s most advanced fighter jets. India in turn destroyed air defense systems around the Pakistani city of Lahore. Neither side gained a meaningful military advantage from their most intense fighting in half a century, and they agreed to a ceasefire after three days. President Donald Trump said he mediated the ceasefire, a claim that Pakistan’s Army Chief of Staff and de facto\\xa0leader Asim Munir supported\\xa0and Indian Prime Minister Narendra Modi\\xa0rejected. The immediate consequence of the fighting was a rift in U.S.-Indian relations, as Trump slapped steep tariffs on Indian exports partly in retaliation for Modi’s refusal to acknowledge his mediation efforts. But the underlying animus between India and Pakistan remains. One open issue is whether India’s decision to suspend its participation in the\u202fIndus Waters Treaty of 1960 means it will curtail the flow of water in the Indus River system that supplies 80 percent of Pakistani farms.\\xa0\\xa07. The AI Race IntensifiesRussian President Vladimir Putin\\xa0said of artificial intelligence\\xa0(AI)\\xa0back in 2017 that “whoever becomes the leader in this sphere will become the ruler of the world.” History may prove him wrong, but for now, countries are acting as if he might be right. The United States and China are the world’s two leading AI powers, and no one else is close. The Chinese firm DeepSeek captured global attention in January when it released an AI model that matched the best U.S. AI models without using advanced Nvidia chips that had been seen as essential to cutting-edge AI operations. Experts\\xa0challenged DeepSeek’s claims, but U.S. firms took them as reason to double down on their own AI investments. The importance of chips in AI development fueled the Trump administration’s initial decision to continue and then expand the restrictions the Biden administration placed on the export of advanced semiconductors made in the United States or with U.S. technology. Two weeks ago, however, Trump reversed course and authorized Nvidia to sell its powerful H200 chip to China. The decision triggered\\xa0biting\\xa0criticism that he was endangering U.S. national security. Lost in this debate is the fact that the AI race is not just about which country builds the best models but also who does a better job integrating AI into everyday operations. Here China may have the advantage. Meanwhile, critics ask whether AI might turn into an epic bust that shocks the economy\\xa0or an epic success that\\xa0erases jobs.\\xa0\\xa06. Sudan’s Grueling Civil War Continues“Hell on Earth” may be the best description of Sudan’s nearly three-year long civil war. The fighting\u202fpits\u202fthe Sudanese Armed Forces (SAF), led by Gen. Abdel Fattah al-Burhan, against the Rapid Support Forces (RSF), led by Mohamed Hamdan “Hemedti” Dagalo. The two men seized power in a coup in October 2021 but eventually had a falling out.\u202fNeither side has won a decisive breakthrough, and fighting continues\\xa0across multiple fronts. The SAF governs from Port Sudan on the Red Sea, controls the major cities in the east and north, and is recognized as Sudan’s legitimate government. Meanwhile, the RSF controls most of Darfur and other areas in central and western Sudan. Each side has foreign backers, with Egypt, Russia, and Turkey among the countries supporting the SAF and Chad, Ethiopia, and the United Arab Emirates among those supporting the RSF. The war’s human cost has been staggering. As many as 400,000 people\\xa0have died and more than twelve million have been displaced. Much of the country faces famine, and the demand for humanitarian aid far outpaces available resources. The fighting was particularly gruesome in the city of El Fasher, the last SAF stronghold in Darfur, which the RSF took in October after an eighteen-month siege. The blood and bodies from the resulting slaughter of the city’s inhabitants could be seen from space. The prospects for a mediated end to the civil war remain dim, and the country’s de facto partition is a possibility.\\xa0\\xa05. The United States Brokers a Gaza Peace PlanAfter\\xa0two years of brutal fighting,\\xa0Israel and Hamas\\xa0agreed in October to a ceasefire. It was the second of the year. The Biden administration negotiated a ceasefire in January that increased humanitarian aid to Gaza, led Hamas to release thirty-three\\xa0hostages,\\xa0and\\xa0prompted\\xa0Israel to free\\xa0nearly 2,000 Palestinian prisoners. The respite from the fighting ended in mid March after the two sides disagreed on the terms of an extension. President Donald Trump played a central role in pushing for the\\xa0October ceasefire, with Qatar, Turkey, and Egypt also helping mediate. The agreement laid out a three phase peace plan for Gaza: 1) an immediate ceasefire with Israeli security forces withdrawing to preset lines and an exchange of hostages and prisoners; 2) the demilitarization of Hamas and the dispatch of an international stabilization force to Gaza; and 3) the reconstitution of Palestinian governance and the reconstruction of Gaza. The United States considers phase one completed, though Israel insists that it will not move to phase two until the last Israeli hostage is released. The UN Security Council endorsed\\xa0the\\xa0Gaza peace plan\u202fin November, authorizing the deployment of the International Stabilization Force, and calling for the establishment of a\u202fPalestinian Committee\u202fto manage Gaza’s day-to-day governance. A lasting peace, however, remains elusive. Hamas shows no signs of disarming, Israel has resumed strikes\\xa0in Gaza,\\xa0and\\xa0no country has yet formally committed\\xa0to\\xa0contribute troops\\xa0to\\xa0the International Stabilization Force.\\xa0\\xa04. The War in Ukraine Grinds onThe war in Ukraine, now in its fourth year, ground on in 2025. Russia intensified its missile and drone campaigns, repeatedly striking Ukrainian cities, causing heavy civilian casualties and damaging major infrastructure. In March, Russia reclaimed its province of Kursk, which Ukraine seized in a surprise invasion in August 2024. Russian advances in Ukraine itself, though, were meager; Russia increased its control of Ukrainian territory by less than 1 percent in 2025. Those gains came at a frightening cost, with\\xa0Russia losing\\xa0roughly\\xa0one thousand soldiers every day. Ukraine’s losses are far lower, but its population is just a third of Russia’s. Ukraine stunned the world in June with Operation Spiderweb, a covert drone strike deep into Russia that hit five air bases. The attack, however, did not change the war’s basic dynamics. Experts debate how long Russia and Ukraine can continue fighting, though Ukraine’s position looks more precarious. President Donald Trump insists that Kyiv does not “have the cards” needed to win, and he remains opposed to increasing U.S. support. Europe is providing Kyiv with substantial financial and military support, and the European Union ended 2025 by agreeing to provide Kyiv with a $105 billion loan. That should cover two-thirds of\\xa0Ukraine’s financial needs\\xa0over the next two years. Trump is pushing Ukrainian President Volodymyr Zelenskyy to accept a ceasefire that many experts see as heavily tilted in Russia’s favor. Even so, Russian President Vladimir Putin looks likely to\\xa0hold out for more.\\xa0\\xa03. Israel and the United States Attack Iran’s Nuclear FacilitiesSome\\xa0wars are measured in years, others in days. The June confrontation between Israel and Iran falls in the latter category; it is now known as the Twelve-Day War. The clash was years in the making. Israel has long seen Iran’s nuclear ambitions and its support for anti-Israeli militias across the Middle East as an existential threat. After the October 7, 2023 Hamas attacks, Israel targeted Iranian proxies in Lebanon, Iraq, Syria, and Yemen. These operations crippled Tehran’s ability to use the so-called Axis of Resistance to deter an Israeli attack on Iran. In October 2024, Israeli airstrikes devastated\\xa0Iran’s missile\\xa0facilities and decimated its air defenses. In June 2025, as Iran\\xa0neared the point where it could quickly build a small number of crude nuclear weapons, Israel launched\\xa0Operation Rising Lion. It involved airstrikes on Iranian nuclear and ballistic-missile facilities, as well as on military bases and command nodes, and the assassination of politicians, military leaders, and nuclear scientists. Iran retaliated by firing\\xa0missiles\\xa0and\\xa0drones at Israel. Assisted by the U.S. military, Israel shot down most of the barrages. Then, on June 22, U.S. B2 bombers and Tomahawk missiles hit Iran’s nuclear facilities as part of Operation Midnight Hammer. President Donald Trump\\xa0claimed the\\xa0attack “totally obliterated” Iran’s nuclear program. However, Iran likely dispersed its stockpile of enriched uranium before the bombs fell. Israel and Iran agreed to a ceasefire on June 24. That halted the fighting but left the underlying political disputes unresolved.\\xa02. China Weaponizes Rare-Earth MineralsFuture historians may flag 2025 as the year China established itself as\\xa0 superpower equal to the United States. For decades, Washington used its economic dominance, particularly of international finance, to advance its foreign policy objectives. In April 2025 and again in October, Beijing showed that it could also weaponize its economic advantages. In its case, the leverage comes from rare-earth minerals essential to a wide range of industrial and military applications. China has worked for years to dominate\\xa0the rare-earth supply chain; it now controls roughly 60 percent of the world’s rare-earth mining and 90 percent of it refining capacity. China’s willingness to weaponize its dominance was no secret. Beijing curtailed rare-earth exports to Japan in 2010 during a territorial dispute over the Senkaku (or Diaoyu) islands. Despite the warning, the United States did little to protect itself. When Trump raised tariffs on China in April, Beijing halted\\xa0the export\\xa0of\\xa0magnets and\\xa0seven rare earth minerals to the United States. Within a month, Trump\\xa0scaled back his tariffs. In October, after Trump imposed additional restrictions on the export of advanced semiconductor chips and technology to China, Beijing imposed new regulations on the export of products using Chinese rare earth minerals. Faced with a potentially substantial disruption to the U.S. economy, Trump delayed implementation of his\\xa0export controls and abandoned several other measures aimed at China. Beijing in turn delayed implementation of its October regulations for one year, suggesting that the rare-earth threat continues to hang over the United States.\\xa0\\xa01.Donald Trump Disrupts U.S. Foreign PolicyIs making good on your word always a good thing? Donald Trump vowed in 2024 to overhaul U.S. foreign policy. He can now say, “Promises Made. Promises Kept.” His disruptions began even before he took office when he declared his desire to acquire Greenland, make Canada the fifty-first state, and reassert control over the Panama Canal. On Inauguration Day, he withdrew the United States from the Paris Agreement\\xa0and the\\xa0World Health Organization, restricted\\xa0refugee readmissions, and designated drug cartels as foreign terrorist organizations. In his first month in office, he began shuttering the U.S. Agency for International Development, ending independent oversight of major federal agencies, and slashing government payrolls. On April 2, he launched “Liberation Day,” imposing 10 percent tariffs on most imports along with additional country specific tariffs of up to 50 percent. Trump claimed credit for convincing NATO members to spend 5 percent of their GDP\\xa0on defense and for\\xa0ending eight foreign wars. He failed, however, to end the war in Ukraine, and to the surprise of his non-interventionist supporters, he threatened Venezuela with regime change. Earlier this month, he released a National Security Strategy that provided a strategic logic for his actions. Critics found much to fault in what Trump did: his tariffs\\xa0hurt U.S. consumers and producers, his Ukraine peace plan rewarded Russia for its aggression, many of the conflicts he claims\\xa0to have ended\\xa0continue, and his downsizing of the national security bureaucracy left the U.S.\\xa0government greatly weakened. One thing, however, has become clear: Trump has\\xa0ended\\xa0the\\xa0era of\\xa0Pax Americana. That reality brings to mind Joni Mitchell’s haunting lyrics: “You don’t know what you’ve got. Till it’s gone.”\\xa0\\xa0\\xa0Oscar Berry and Sofia Park Jadotte\\xa0assisted\\xa0in the preparation of this post.\u202f\\xa0Other posts in this series:\u202f\\xa0Ten Most Significant World Events in 2024\\xa0Ten Most Significant World Events in 2023\u202f\\xa0Ten Most Significant World Events in 2022\u202f\u202f\\xa0Ten Most Significant World Events in 2021\u202f\\xa0Ten Most Significant World Events in 2020\u202f\\xa0Ten Most Significant World Events in 2019\u202f\\xa0Ten Most Significant World Events in 2018\u202f\\xa0Ten Most Significant World Events in 2017\u202f\\xa0Ten Most Significant World Events in 2016\u202f\\xa0Ten Most Significant World Events in 2015\u202f\\xa0Ten Most Significant World Events in 2014\u202f\\xa0InformationFrom The Water&#39;s Edge and U.S. Foreign Policy ProgramCreative Commons: Some rights reserved.This work is licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) License.\nDiplomacy and International InstitutionsSudanTime Hasn&#39;t Clarified Trump&#39;s Venezuela StrategyBy Roxanna VigilFebruary 4, 2026The AI Bubble Is Getting Closer to PoppingBy Shannon K. O&#39;NeilJanuary 29, 2026Everything but Territory: Europe’s Response to Trump’s Greenland ThreatsBy Liana FixJanuary 16, 2026Council on Foreign RelationsAbout CFRMembershipSupport CFRNewslettersCareersCFR EducationFor MediaContact UsPrivacy PolicyGet CFR’s Daily News BriefSubscribe©2026 Council on Foreign Relations&quot;<br>   },<br>   {<br>      &quot;role&quot;:&quot;assistant&quot;,<br>      &quot;content&quot;:&quot;SAVE: \&quot;In 2025, Cardinal Robert Prevost became Pope Leo XIV, marking the first time a pope was from North America. His election followed the sudden death of Pope Francis and was significant for the Catholic Church, as Prevost aimed to address social issues and improve the church\\&#39;s engagement with contemporary challenges.\&quot;\n\nSAVE: \&quot;The Twelve-Day War between Israel and Iran occurred in June 2025, where Israel launched a series of airstrikes on Iranian nuclear facilities, leading to a significant military confrontation. This conflict underscored the ongoing tensions in the Middle East and the complexities surrounding nuclear proliferation.\&quot;\n\nSAVE: \&quot;In 2025, the United States and China intensified their competition in artificial intelligence, with both nations making significant advancements. The Chinese firm DeepSeek released a competitive AI model, prompting the U.S. to increase its investments in AI technology, highlighting the global race for technological supremacy.\&quot;\n\nFinal answer based on verified sources:\n\n1. **Cardinal Robert Prevost Becomes Pope Leo XIV**: In 2025, Cardinal Robert Prevost was elected as Pope Leo XIV, making history as the first pope from North America. His election followed the death of Pope Francis and was significant for the Catholic Church, as Prevost aimed to address social issues and improve the church\\&#39;s engagement with contemporary challenges.\n\n2. **The Twelve-Day War Between Israel and Iran**: In June 2025, Israel launched a series of airstrikes on Iranian nuclear facilities, leading to a military confrontation known as the Twelve-Day War. This conflict highlighted the ongoing tensions in the Middle East and the complexities surrounding nuclear proliferation.\n\n3. **Intensified AI Competition Between the U.S. and China**: The year 2025 saw a significant escalation in the competition between the United States and China in the field of artificial intelligence. The Chinese firm DeepSeek released a competitive AI model, prompting the U.S. to increase its investments in AI technology, underscoring the global race for technological supremacy.&quot;<br>   },<br>   {<br>      &quot;role&quot;:&quot;user&quot;,<br>      &quot;content&quot;:&quot;Save content:\nSAVE: \&quot;In 2025, Cardinal Robert Prevost became Pope Leo XIV, marking the first time a pope was from North America. His election followed the sudden death of Pope Francis and was significant for the Catholic Church, as Prevost aimed to address social issues and improve the church\\&#39;s engagement with contemporary challenges.\&quot;\n\nSAVE: \&quot;The Twelve-Day War between Israel and Iran occurred in June 2025, where Israel launched a series of airstrikes on Iranian nuclear facilities, leading to a significant military confrontation. This conflict underscored the ongoing tensions in the Middle East and the complexities surrounding nuclear proliferation.\&quot;\n\nSAVE: \&quot;In 2025, the United States and China intensified their competition in artificial intelligence, with both nations making significant advancements. The Chinese firm DeepSeek released a competitive AI model, prompting the U.S. to increase its investments in AI technology, highlighting the global race for technological supremacy.\&quot;\n\nFinal answer based on verified sources:\n\n1. **Cardinal Robert Prevost Becomes Pope Leo XIV**: In 2025, Cardinal Robert Prevost was elected as Pope Leo XIV, making history as the first pope from North America. His election followed the death of Pope Francis and was significant for the Catholic Church, as Prevost aimed to address social issues and improve the church\\&#39;s engagement with contemporary challenges.\n\n2. **The Twelve-Day War Between Israel and Iran**: In June 2025, Israel launched a series of airstrikes on Iranian nuclear facilities, leading to a military confrontation known as the Twelve-Day War. This conflict highlighted the ongoing tensions in the Middle East and the complexities surrounding nuclear proliferation.\n\n3. **Intensified AI Competition Between the U.S. and China**: The year 2025 saw a significant escalation in the competition between the United States and China in the field of artificial intelligence. The Chinese firm DeepSeek released a competitive AI model, prompting the U.S. to increase its investments in AI technology, underscoring the global race for technological supremacy.&quot;<br>   },<br>   {<br>      &quot;role&quot;:&quot;assistant&quot;,<br>      &quot;content&quot;:&quot;The content has been saved successfully. Here is the final answer based on verified sources:\n\n1. **Cardinal Robert Prevost Becomes Pope Leo XIV**: In 2025, Cardinal Robert Prevost was elected as Pope Leo XIV, making history as the first pope from North America. His election followed the death of Pope Francis and was significant for the Catholic Church, as Prevost aimed to address social issues and improve the church&#39;s engagement with contemporary challenges.\n\n2. **The Twelve-Day War Between Israel and Iran**: In June 2025, Israel launched a series of airstrikes on Iranian nuclear facilities, leading to a military confrontation known as the Twelve-Day War. This conflict highlighted the ongoing tensions in the Middle East and the complexities surrounding nuclear proliferation.\n\n3. **Intensified AI Competition Between the U.S. and China**: The year 2025 saw a significant escalation in the competition between the United States and China in the field of artificial intelligence. The Chinese firm DeepSeek released a competitive AI model, prompting the U.S. to increase its investments in AI technology, underscoring the global race for technological supremacy.&quot;<br>   }<br>]</pre><p>This is a log of the conversation.</p><p>When done it saves the final response in <strong>search_results.txt</strong> file:</p><blockquote>Final answer based on verified sources:</blockquote><blockquote>1. **Cardinal Robert Prevost Becomes Pope Leo XIV**: In 2025, Cardinal Robert Prevost was elected as Pope Leo XIV, making history as the first pope from North America. His election followed the death of Pope Francis and was significant for the Catholic Church, as Prevost aimed to address social issues and improve the church’s engagement with contemporary challenges.</blockquote><blockquote>2. **The Twelve-Day War Between Israel and Iran**: In June 2025, Israel launched a series of airstrikes on Iranian nuclear facilities, leading to a military confrontation known as the Twelve-Day War. This conflict highlighted the ongoing tensions in the Middle East and the complexities surrounding nuclear proliferation.</blockquote><blockquote>3. **Intensified AI Competition Between the U.S. and China**: The year 2025 saw a significant escalation in the competition between the United States and China in the field of artificial intelligence. The Chinese firm DeepSeek released a competitive AI model, prompting the U.S. to increase its investments in AI technology, underscoring the global race for technological supremacy.</blockquote><h3>The Conclusion</h3><p>LLM alone wasn’t able to provide the information we required but our agent was able to browse the web and save its findings on disk 🤯</p><p>Stay tuned: this is just the beginning 💪</p><p>Please find the full code below ⬇️</p><p>⚠️ <em>The purpose of this post is to keep things as simple as possible, providing an easy-to-understand overview of how AI Agents work, how they are built, and what their main components are. That said, unexpected issues and nuances may arise in practice, which are not covered here — this is a deliberate trade-off to prioritize clarity and accessibility.</em></p><p>☕️ A small coffee goes a long way! If you feel like supporting me , that’s awesome (go to <a href="http://buymeacoffee.com/ktustanowski">buymeacoffee.com/ktustanowski</a> or scan the QR code below) — but no worries at all if not. I hope you got something valuable here!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*arU38s0o3dvJ3gZ1k-0BIQ.png" /></figure><pre># ----------------------------<br># Imports<br># ----------------------------</pre><pre>import os<br>from dotenv import load_dotenv</pre><pre>from openai import OpenAI<br>from ddgs import DDGS<br>import requests<br>from bs4 import BeautifulSoup<br></pre><pre># ----------------------------<br># Environment setup<br># ----------------------------</pre><pre># Load environment variables from .env file<br>load_dotenv()</pre><pre># Expect OPENAI_API_KEY to be defined in .env<br>if not os.getenv(&quot;OPENAI_API_KEY&quot;):<br>    raise RuntimeError(&quot;OPENAI_API_KEY not found in environment variables&quot;)</pre><pre># Initialize OpenAI client (2026 API style)<br>client = OpenAI()<br></pre><pre># ----------------------------<br># LLM helper<br># ----------------------------</pre><pre>def call_llm(messages: list[dict]) -&gt; str:<br>    &quot;&quot;&quot;<br>    Send a conversation to the language model<br>    and return the assistant&#39;s response text.<br>    &quot;&quot;&quot;</pre><pre>    response = client.chat.completions.create(<br>        model=&quot;gpt-4o-mini&quot;,<br>        messages=messages,<br>        temperature=0<br>    )</pre><pre>    return response.choices[0].message.content<br></pre><pre># ----------------------------<br># Tools<br># ----------------------------</pre><pre>def search_web(query: str):<br>    print(&quot;🛜 web_search - start&quot;)<br>    results = []</pre><pre>    with DDGS() as ddgs:<br>        for r in ddgs.text(query, max_results=1):<br>            results.append({<br>                &quot;title&quot;: r[&quot;title&quot;],<br>                &quot;url&quot;: r[&quot;href&quot;],<br>                &quot;snippet&quot;: r[&quot;body&quot;],<br>            })</pre><pre>            print(&quot;=============================&quot;)<br>            print(f&quot;🔍 query {query}&quot;)<br>            print(f&quot;🧾 title {r[&#39;title&#39;]}&quot;)<br>            print(f&quot;🔗 url {r[&#39;href&#39;]}&quot;)<br>            print(f&quot;📋 snippet {r[&#39;body&#39;]}&quot;)</pre><pre>    print(f&quot;🛜 web_search - finish - {len(results)} results&quot;)<br>    return results</pre><pre>def fetch_webpage(url):<br>    &quot;&quot;&quot;<br>    Fetch provided webpage<br>    &quot;&quot;&quot;<br>    print(f&quot;🌍 FETCH {url}&quot;)<br>    html = requests.get(url).text<br>    soup = BeautifulSoup(html, &quot;html.parser&quot;)<br>    return soup.get_text()</pre><pre>def save_to_disk(text: str) -&gt; str:<br>    print(&quot;💾 Save to search_results.txt&quot;)<br>    with open(&quot;search_results.txt&quot;, &quot;w&quot;) as f:<br>        f.write(text)</pre><pre># ----------------------------<br># Agent logic<br># ----------------------------</pre><pre>def research_agent(task: str) -&gt; str:<br>    &quot;&quot;&quot;<br>    A simple agent loop:</pre><pre>    1. The model reasons about the task<br>    2. Decides whether to use a tool<br>    3. Consumes tool output<br>    4. Stops when the goal is achieved<br>    &quot;&quot;&quot;</pre><pre>    system_prompt = &quot;&quot;&quot;<br>    You are a research assistant.<br>    If you need web data:<br>    SEARCH:&lt;query&gt;<br>    If you have a URL:<br>    FETCH:&lt;url&gt;<br>    If a page can’t be read or is irrelevant, discard it and search again. <br>    Do not use URL with bad content again.<br>    If URLs are provided, base your answer only on their content.<br>    Save useful info with:<br>    SAVE:&lt;text&gt;<br>    After saving, provide the final answer based only on verified sources.    &quot;&quot;&quot;<br>    messages = [<br>        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt},<br>        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: task}<br>    ]</pre><pre>    while True:<br>        reply = call_llm(messages)<br>        print(f&quot;🤖 LLM Reply {reply}&quot;)<br>        messages.append({&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: reply})<br>        if reply.startswith(&quot;SEARCH:&quot;):<br>            query = reply.replace(&quot;SEARCH:&quot;, &quot;&quot;).strip()<br>            search_result = search_web(query)</pre><pre>            messages.append({<br>                &quot;role&quot;: &quot;user&quot;,<br>                &quot;content&quot;: f&quot;Search result:\n{search_result}&quot;<br>            })</pre><pre>        elif reply.startswith(&quot;FETCH:&quot;):<br>            query = reply.replace(&quot;FETCH:&quot;, &quot;&quot;).strip()<br>            fetch_page_result = fetch_webpage(query)</pre><pre>            messages.append({<br>                &quot;role&quot;: &quot;user&quot;,<br>                &quot;content&quot;: f&quot;Feetch result:\n{fetch_page_result}&quot;<br>            })</pre><pre>        elif reply.startswith(&quot;SAVE:&quot;):<br>            text = reply.replace(&quot;SAVE:&quot;, &quot;&quot;).strip()<br>            save_to_disk(text)</pre><pre>            messages.append({<br>                &quot;role&quot;: &quot;user&quot;,<br>                &quot;content&quot;: f&quot;Save content:\n{reply}&quot;<br>            })</pre><pre>        else:<br>            return reply<br></pre><pre># ----------------------------<br># Entry point<br># ----------------------------</pre><pre>if __name__ == &quot;__main__&quot;:</pre><pre>    result = research_agent(<br>        &quot;Browse the web to find one strange but true historical facts that sound fake.&quot;<br>        &quot;Save each fact with a short explanation&quot;<br>    )</pre><pre>    print(&quot;\n✅ Final answer:\n&quot;)<br>    print(result)</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2dfdaf66b6c1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI-Powered Mobile App Creation]]></title>
            <link>https://medium.com/@kamil.tustanowski/ai-powered-mobile-app-creation-2aaffe8e9ec4?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/2aaffe8e9ec4</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Sun, 10 Sep 2023 06:47:02 GMT</pubDate>
            <atom:updated>2023-09-21T11:48:26.210Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VUX68v7o-_wG9A1d2a36Kg.png" /></figure><p>Creating an application is a multifaceted endeavor, often requiring a diverse skill set. A proficient developer may excel in coding but fall short in areas such as design, user interface/user experience (UI/UX), or business strategy. Conversely, a talented artist might lack the technical know-how to implement their ideas in code, and a successful entrepreneur might not possess the design acumen needed for crafting an appealing app. Even seemingly simple applications can pose challenges, as individuals may have expertise in some aspects (X and Y), but find themselves struggling when confronted with others (Z). Excelling in all these areas simultaneously can be a formidable task.</p><p>My interest in artificial intelligence (AI) led me to embark on an experiment aimed at transforming the way we approach application development. The focus of this experiment was to create a basic yet fully functional Minimum Viable Product (MVP) while minimizing the amount of code writing required.</p><p>To accomplish this, I leveraged several AI tools and models:<br>- ChatGPT 3.5<br>- Midjourney (Version 5.0 and above)<br>- Stable Diffusion XL<br>- Bard (Experiment), as of August 18, 2023</p><p>I was particularly mindful of cost efficiency throughout this project, allocating expenses primarily to Midjourney while utilizing other resources that were either freely available or offered free prompts.</p><p>In summary, the experiment I conducted sought to challenge the traditional approach to application development by harnessing the power of AI, thereby demonstrating the potential for a more accessible and efficient method of creating software solutions.</p><h3>The Idea</h3><p>In pursuit of a straightforward yet beneficial endeavor, I opted to develop a water intake logging application with the aim of enhancing personal hydration management. With a clear goal in mind, I proceeded to the next phase of the project.</p><h3>The Plan</h3><p>Typically, this was the stage where I would commence coding, engaging in a combination of experimentation and deliberation regarding the app’s UI design. Unfortunately, this approach often led to frequent code refactoring, as my development process revealed opportunities for improvement. Even when the application appeared theoretically complete, I found myself continuously adding features, as it became evident that without them, the app lacked a sense of fulfillment.</p><p>However, for this particular project, I decided to adopt a novel approach. I formulated a prompt and sought assistance from <strong>ChatGPT</strong> and <strong>Bard</strong>. Here’s the prompt I used:</p><blockquote>Act like iOS app designer and propose simple water driking app.</blockquote><h4>Bard</h4><blockquote>Sure, here is a simple water drinking app design for iOS:</blockquote><blockquote>Home Screen: The home screen should have a large water bottle icon in the center, with a progress bar underneath showing how much water the user has drunk today. There should also be a button to set a reminder to drink water, and a button to view the water calculator.</blockquote><blockquote>Water Reminder: The water reminder should be set to go off at regular intervals throughout the day. The user can choose how often they want to be reminded to drink water.</blockquote><blockquote>Water Calculator: The water calculator should calculate the amount of water the user needs to drink based on their weight, activity level, and climate. The user can also override the calculator’s recommendation if they want.</blockquote><blockquote>Medical Data: The app should be able to access the user’s medical data, such as their weight, if they have provided it. This data can be used to improve the accuracy of the water calculator.</blockquote><blockquote>Here are some additional features that could be added to the app:</blockquote><blockquote>Habit Tracking: The app could track the user’s water drinking habits over time. This could help the user to see how much progress they are making and stay motivated.</blockquote><blockquote>Social Sharing: The user could share their water drinking goals and progress with friends and family. This could help to create a sense of community and accountability.</blockquote><blockquote>Gameification: The app could gamify the water drinking experience by awarding points or badges for drinking water. This could make the experience more fun and engaging.</blockquote><blockquote>I hope this gives you a starting point for your water drinking app design.</blockquote><h4>ChatGPT</h4><blockquote>Title: HydrateMe — Your Personal Water Drinking Assistant</blockquote><blockquote>Description: HydrateMe is a simple yet powerful water drinking app designed to help you stay hydrated throughout the day. With its intuitive interface and advanced features, it ensures you never forget to drink water and guides you on how much water you need to consume based on your personal information. The app can even fetch your weight data from your medical records (with your permission) to calculate the appropriate daily water intake.</blockquote><blockquote>Main Features:</blockquote><blockquote>Personalized Water Intake Calculator:</blockquote><blockquote>The app will ask you to enter some basic information, such as your weight, age, gender, and activity level.</blockquote><blockquote>Utilizing this data, HydrateMe will calculate your recommended daily water intake based on widely accepted hydration guidelines.</blockquote><blockquote>Customizable Water Goals:</blockquote><blockquote>You can set your preferred daily water drinking goal. HydrateMe will keep track of your progress and provide timely reminders to ensure you achieve it.</blockquote><blockquote>Intelligent Water Intake Reminders:</blockquote><blockquote>HydrateMe will send you push notifications at regular intervals, reminding you to drink water.</blockquote><blockquote>The app will consider your schedule and preferences to set reminders at the most suitable times for you.</blockquote><blockquote>Quick Water Entry:</blockquote><blockquote>Logging your water intake is hassle-free. Simply tap on the app to record the amount of water you’ve consumed.</blockquote><blockquote>Visual Progress Tracking:</blockquote><blockquote>A visual representation of your daily water intake progress will motivate you to stay on track and reach your hydration goals.</blockquote><blockquote>Integration with Medical Data (Optional):</blockquote><blockquote>If you grant permission, the app can access your medical records to retrieve your weight data.</blockquote><blockquote>By using your weight data, HydrateMe ensures a more accurate calculation of your personalized water intake needs.</blockquote><blockquote>Achievements and Rewards:</blockquote><blockquote>Stay motivated with achievements and rewards for consistently achieving your daily water intake goals.</blockquote><blockquote>Hydration Tips and Information:</blockquote><blockquote>The app will provide useful hydration tips and information on the importance of staying hydrated.</blockquote><blockquote>Dark Mode Support:</blockquote><blockquote>HydrateMe offers a dark mode option for a comfortable and energy-efficient experience during nighttime usage.</blockquote><blockquote>Data Sync and Backup:</blockquote><blockquote>Your water intake data will be securely stored in the cloud, ensuring you can access it across multiple devices and never lose your progress.</blockquote><blockquote>HydrateMe is your reliable companion to help you develop and maintain healthy hydration habits. Stay refreshed, energized, and focused with this user-friendly water drinking app!</blockquote><p>I found <strong>ChatGPT’s</strong> response more appealing, as it was comprehensive and even suggested a potential app name. However, it was slightly too extensive for my needs. Therefore, I requested <strong>ChatGPT</strong> to:</p><blockquote>Simplify this just to mvp for now</blockquote><p>Response:</p><blockquote>Title: Hydration Buddy</blockquote><blockquote>Description: Hydration Buddy is a simple water drinking app designed to remind you to drink water and track your daily intake. With just a few essential features, it helps you stay on top of your hydration goals effortlessly.</blockquote><blockquote>Main Features:</blockquote><blockquote>Water Intake Reminder:<br>Set personalized reminders to drink water at regular intervals throughout the day.</blockquote><blockquote>Customizable Daily Goal:<br>Choose your preferred daily water intake goal.</blockquote><blockquote>Intuitive Water Entry:<br>Easily log your water consumption with a quick and straightforward interface.</blockquote><blockquote>Automatic Weight-Based Calculation:<br>Optionally, provide your weight, and the app will calculate your recommended daily water intake based on it.</blockquote><blockquote>Hydration Buddy simplifies the process of staying hydrated, keeping you refreshed and healthy every day.</blockquote><p>This streamlined approach proved efficient, and the longer version remained available for reference if needed, all accomplished within a matter of minutes.</p><h3>The Code</h3><p>Now that I have a clear vision of what I intend to construct and a well-defined plan in mind, it’s time to commence the coding phase. Nevertheless, it’s important to remember that AI can also play a pivotal role in this process. To harness its potential, I employed another prompt:</p><blockquote>Generate the code for iOS written in swift for these application</blockquote><p>I could do that because <strong>ChatGPT</strong> utilizes the context of the conversation. This implies that:</p><blockquote>In the realm of natural language processing (NLP), the term “context window” refers to the amount of preceding text that a language model can consider when generating a response. For ChatGPT, this context window is measured in tokens, which can be as short as one character or as long as one word.</blockquote><blockquote>The context window plays a pivotal role in shaping the conversation with a chatbot. It’s like a sliding window that moves with each new message, always keeping track of the most recent tokens up to its maximum size.</blockquote><p>Check <a href="https://ecoagi.ai/topics/ChatGPT/chatgpt-context-window">this article</a> for more info.</p><p>Note: This project is an experiment in which my goal was to utilize as much of the generated code as possible. While I did perform some refactoring, my curiosity led me to explore the outcomes of using the code in its original form.</p><p>🚨 It’s important to note that <strong>AI</strong> may not always provide the correct solution, as it strives to offer responses regardless of potential errors. These errors may be glaringly evident, causing code to fail to compile. In other cases, issues may not be immediately apparent. Therefore, it’s crucial to exercise caution and thoroughly comprehend the code you integrate into your application. 🚨</p><p>It’s time to proceed with the response:</p><pre>import UIKit<br><br>class HomeViewController: UIViewController {<br>    // Add outlets to display water intake status and progress on the UI<br><br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        // Load user&#39;s water intake status and progress from data storage<br>        updateWaterIntakeStatus()<br>    }<br><br>    // Function to update the UI with water intake status<br>    func updateWaterIntakeStatus() {<br>        // Retrieve and display the current water intake status and progress on the UI<br>    }<br><br>    @IBAction func logWaterButtonTapped(_ sender: UIButton) {<br>        // Navigate to the Water Intake Log Screen<br>        let waterIntakeLogVC = WaterIntakeLogViewController()<br>        waterIntakeLogVC.delegate = self<br>        navigationController?.pushViewController(waterIntakeLogVC, animated: true)<br>    }<br>}<br><br>// Implement an extension to handle the delegate callback after logging water intake<br>extension HomeViewController: WaterIntakeLogDelegate {<br>    func didLogWaterIntake(amount: Int) {<br>        // Handle the logged water intake amount<br>        // Update the water intake status and progress on the UI<br>        updateWaterIntakeStatus()<br>    }<br>}<br><br>import UIKit<br><br>protocol WaterIntakeLogDelegate: AnyObject {<br>    func didLogWaterIntake(amount: Int)<br>}<br><br>class WaterIntakeLogViewController: UIViewController {<br>    weak var delegate: WaterIntakeLogDelegate?<br><br>    @IBAction func waterAmountButtonTapped(_ sender: UIButton) {<br>        // Get the water intake amount from the button title or tag<br>        let selectedAmount = Int(sender.title(for: .normal) ?? &quot;0&quot;) ?? 0<br><br>        // Call the delegate method to pass the logged water intake amount back to the HomeViewController<br>        delegate?.didLogWaterIntake(amount: selectedAmount)<br><br>        // Navigate back to the Home Screen<br>        navigationController?.popViewController(animated: true)<br>    }<br>}<br>Note: This code assumes that you have set up the UI elements (buttons, labels, etc.) in your storyboard or programmatically. Additionally, it&#39;s a simplified example, and you might need to adapt it to fit the specific structure and architecture of your iOS app.</pre><p>My apologies, I intended to request the code in SwiftUI.</p><blockquote>Generate the code for iOS written in swiftUI for these application</blockquote><pre><br>import SwiftUI<br><br>struct HomeView: View {<br>    @State private var waterIntakeStatus = 0 // Initialize with user&#39;s current water intake status<br>    @State private var dailyGoal = 2000 // Set your preferred daily water intake goal<br><br>    var body: some View {<br>        VStack {<br>            Text(&quot;Water Intake: \(waterIntakeStatus) ml&quot;)<br>                .font(.headline)<br>            <br>            Text(&quot;Daily Goal: \(dailyGoal) ml&quot;)<br>                .font(.subheadline)<br>            <br>            Button(&quot;Log Water Intake&quot;, action: showWaterIntakeLogScreen)<br>                .padding()<br>        }<br>    }<br><br>    func showWaterIntakeLogScreen() {<br>        let waterIntakeLogView = WaterIntakeLogView(delegate: self)<br>        // You can present this view in a navigation stack or as a sheet, depending on your app&#39;s navigation structure.<br>        // For example, using NavigationView:<br>        NavigationView {<br>            waterIntakeLogView<br>        }<br>    }<br>}<br><br>extension HomeView: WaterIntakeLogDelegate {<br>    func didLogWaterIntake(amount: Int) {<br>        // Update the water intake status and progress<br>        waterIntakeStatus += amount<br>        // Here, you can also update your data storage to save the new water intake status.<br>    }<br>}<br><br>import SwiftUI<br><br>protocol WaterIntakeLogDelegate: AnyObject {<br>    func didLogWaterIntake(amount: Int)<br>}<br><br>struct WaterIntakeLogView: View {<br>    weak var delegate: WaterIntakeLogDelegate?<br><br>    let waterAmounts = [100, 250, 500, 750, 1000] // Predefined water quantity options<br><br>    var body: some View {<br>        VStack {<br>            Text(&quot;Select Water Intake Amount&quot;)<br>                .font(.headline)<br>            <br>            ForEach(waterAmounts, id: \.self) { amount in<br>                Button(&quot;\(amount) ml&quot;, action: { logWaterIntake(amount: amount) })<br>                    .padding()<br>            }<br>        }<br>    }<br><br>    func logWaterIntake(amount: Int) {<br>        delegate?.didLogWaterIntake(amount: amount)<br>    }<br>}</pre><p>That’s better.</p><p>I passed the code to Xcode and:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*38XbbzXtfFtbDCtP12TRng.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-ZUA1oTKdSRk-0oBXe7Ozg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-FDFb4B0XszzMqskHha2lA.png" /></figure><p>A promising beginning on this lengthy journey! However, I couldn’t help but notice the absence of ViewModels in the current implementation. Rather than attempting to modify the code independently, though:</p><blockquote>Separate the logic for these code in the ViewModels</blockquote><pre>import SwiftUI<br><br>class HomeViewModel: ObservableObject {<br>    @Published var waterIntakeStatus = 0 // Initialize with user&#39;s current water intake status<br>    @Published var dailyGoal = 2000 // Set your preferred daily water intake goal<br><br>    func logWaterIntake(amount: Int) {<br>        waterIntakeStatus += amount<br>        // Here, you can also update your data storage to save the new water intake status.<br>    }<br>}<br><br>import SwiftUI<br><br>class WaterIntakeLogViewModel: ObservableObject {<br>    weak var delegate: WaterIntakeLogDelegate?<br><br>    let waterAmounts = [100, 250, 500, 750, 1000] // Predefined water quantity options<br><br>    func logWaterIntake(amount: Int) {<br>        delegate?.didLogWaterIntake(amount: amount)<br>    }<br>}<br><br>import SwiftUI<br><br>struct HomeView: View {<br>    @StateObject private var viewModel = HomeViewModel()<br><br>    var body: some View {<br>        VStack {<br>            Text(&quot;Water Intake: \(viewModel.waterIntakeStatus) ml&quot;)<br>                .font(.headline)<br>            <br>            Text(&quot;Daily Goal: \(viewModel.dailyGoal) ml&quot;)<br>                .font(.subheadline)<br>            <br>            Button(&quot;Log Water Intake&quot;, action: { showWaterIntakeLogScreen() })<br>                .padding()<br>        }<br>        .sheet(isPresented: $showWaterIntakeLogView) {<br>            WaterIntakeLogView(viewModel: WaterIntakeLogViewModel(delegate: viewModel))<br>        }<br>    }<br><br>    @State private var showWaterIntakeLogView = false<br><br>    func showWaterIntakeLogScreen() {<br>        showWaterIntakeLogView = true<br>    }<br>}<br><br>struct WaterIntakeLogView: View {<br>    @ObservedObject var viewModel: WaterIntakeLogViewModel<br><br>    var body: some View {<br>        VStack {<br>            Text(&quot;Select Water Intake Amount&quot;)<br>                .font(.headline)<br><br>            ForEach(viewModel.waterAmounts, id: \.self) { amount in<br>                Button(&quot;\(amount) ml&quot;, action: { viewModel.logWaterIntake(amount: amount) })<br>                    .padding()<br>            }<br>        }<br>    }<br>}</pre><p>When I pasted the code into Xcode, I encountered a minor issue. The ‘WaterIntakeLogDelegate’ was missing, even though it was present in the previous version of the code. Instead of manually copying and pasting it, I opted to communicate the issue to <strong>ChatGPT</strong>:</p><blockquote>WaterIntakeLogDelegate is missing</blockquote><p>And among other things I got:</p><pre>// WaterIntakeLogDelegate Protocol<br>protocol WaterIntakeLogDelegate: AnyObject {<br>    func didLogWaterIntake(amount: Int)<br>}</pre><p>The missing element was the ‘WaterIntakeLogDelegate.’ While this code snippet had been included in previous samples, it was notably absent in this instance. Fortunately, this discrepancy was swiftly addressed.</p><h3>The Design</h3><p>I have the first iteration of my app, just a very basic — yet functional — piece of code. A good start. But before going further, I would like to switch roles again and prepare a prototype so I know what I want to create as a developer later on.</p><p>I was quickly exploring various prompts, and when I found inspiration and clarity regarding my objectives, I formulated the following prompt:</p><p><strong>Note</strong>: Using the same prompt alone won’t result in the same image because:</p><blockquote>The Midjourney bot uses a seed number to create a field of visual noise, like television static, as a starting point to generate the initial image grids. Seed numbers are generated randomly for each image but can be specified with the --seed parameter. If you use the same seed number and prompt, you will get similar final images.</blockquote><p>Seed documentation in <a href="https://docs.midjourney.com/docs/seeds">midjourney</a> and <a href="https://getimg.ai/guides/guide-to-seed-parameter-in-stable-diffusion">stable diffusion xl</a></p><p>The prompt I used:</p><blockquote>plan the app design for iOS mobile app, iPhone, for water drinking app with landscape in the background which fades out when user is drinking more and more water</blockquote><p>Let’s see what generated <strong>Stable Diffusion XL</strong> first:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*abyuE8bD6I6NKkH4k1N6lQ.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z64bJpEvpAuGT45hm5UbjQ.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8W8KLsmL-4qX0DnobvqK1A.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qu8et-R1iEApXvoaFuHDqw.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TQPIqH37g5rf2dRnbE5iGw.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E4QwAFmfhnnXqsEPyIO4fA.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F1GdyLUprNLPL-AeZnHXvw.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qBZuFhGMRuZjRtH1H1La7A.jpeg" /></figure><p>And now <strong>Midjourney</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e8dtglyn_z8dR7b5mb8ZsA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FBMdMLQ26FKnaf9vohuyTQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rfZxH-HzNA5IvdEITKbXUQ.png" /></figure><p>I found <strong>Midjourney’s</strong> suggestions to be slightly more captivating.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CYRyrJSK0C__1JcFY6sOvg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ugvc2EmuNVH6k5uaFxgr0w.png" /></figure><p>I plan to strive for the combination of both of these aspects.</p><p>Now, let’s enjoy the creative process and design the application icon:</p><blockquote>drink water everyday app ios icon, colorfull, show exotic places</blockquote><p>Again I will start from <strong>Stable Diffusion XL</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LEg1D0VxBNdxXM3m_vcSPQ.png" /></figure><p>And now <strong>Midjourney</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EX8pAmUHgu8XdBZstZWUyA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M_iGk0O33W69POEHCF90uA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BBMHS_3yam-Vq4_8FrHimg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EkWWr7lLdhK8BQ-jDEHtmQ.png" /></figure><p><strong>Midjourney</strong> wins again:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s0lL-xvoFEMJiyaYg3C0vw.png" /></figure><p>I have a clear vision for the UI direction I want to pursue, and I’ve also created the icon. Based on my past experiences, having these elements in place early on in the process is advantageous. And it looks nice.</p><h3>The Data</h3><p>Currently we do not store anything. When the app is closed it just vanishes and start over next time. This seems to be something both AI forgot. Maybe they don’t have this problem because all of this is stored by default. Since storing data is quite important for our app I asked <strong>ChatGPT</strong>:</p><blockquote>Add code to store the values of how much user drank each day in userdefaults</blockquote><p>Response:</p><pre>import Foundation<br><br>class DataManager: ObservableObject {<br>    static let shared = DataManager()<br><br>    private let userDefaults: UserDefaults<br><br>    private init() {<br>        #if DEBUG<br>        userDefaults = UserDefaults(suiteName: &quot;com.example.app.debug&quot;)!<br>        #else<br>        userDefaults = UserDefaults.standard<br>        #endif<br>    }<br><br>    func saveWaterIntake(_ intakeAmount: Int, forDate date: Date) {<br>        var data = loadWaterIntakeData()<br>        data[date] = intakeAmount<br>        saveWaterIntakeData(data)<br>    }<br><br>    func loadWaterIntakeData() -&gt; [Date: Int] {<br>        guard let data = userDefaults.dictionary(forKey: &quot;WaterIntakeData&quot;) as? [String: Int] else {<br>            return [:]<br>        }<br><br>        var result = [Date: Int]()<br>        for (key, value) in data {<br>            if let date = DateFormatterManager.shared.dateFormatter.date(from: key) {<br>                result[date] = value<br>            }<br>        }<br>        return result<br>    }<br><br>    private func saveWaterIntakeData(_ data: [Date: Int]) {<br>        var saveData = [String: Int]()<br>        for (key, value) in data {<br>            let dateString = DateFormatterManager.shared.dateFormatter.string(from: key)<br>            saveData[dateString] = value<br>        }<br>        userDefaults.set(saveData, forKey: &quot;WaterIntakeData&quot;)<br>    }<br>}<br><br>class DateFormatterManager {<br>    static let shared = DateFormatterManager()<br><br>    private init() {}<br><br>    lazy var dateFormatter: DateFormatter = {<br>        let formatter = DateFormatter()<br>        formatter.dateFormat = &quot;yyyy-MM-dd&quot;<br>        return formatter<br>    }()<br>}<br><br>import SwiftUI<br><br>class HomeViewModel: ObservableObject {<br>    @Published var waterIntakeStatus = 0 // Initialize with user&#39;s current water intake status<br>    @Published var dailyGoal = 2000 // Set your preferred daily water intake goal<br><br>// ... (previously defined code)<br><br>    func logWaterIntake(amount: Int) {<br>        waterIntakeStatus += amount<br>        DataManager.shared.saveWaterIntake(amount, forDate: Date())<br>    }<br>}<br><br>import SwiftUI<br><br>struct HomeView: View {<br>    @EnvironmentObject private var viewModel: HomeViewModel<br>  <br>// ... (previously defined code)<br><br>}</pre><p><strong>Note</strong>: I’m only providing the code section and not the entire response, as typically <strong>ChatGPT</strong> also supplies context, instructions on how to use the code, and additional information.</p><p>Overall, it’s quite impressive. To be completely honest, there are a few areas I might consider for improvement, but it’s satisfactory, and the speed is exceptional. I just noticed a small snippet that I believe I’ll require, so I requested it:</p><blockquote>Generate water intake getter function but for particular dat not all of them</blockquote><p>Response:</p><pre>import Foundation<br><br>class DataManager: ObservableObject {<br>    // ... (previously defined code)<br><br>    func getWaterIntake(forDate date: Date) -&gt; Int? {<br>        let waterIntakeData = loadWaterIntakeData()<br>        return waterIntakeData[date]<br>    }<br>}<br><br>import SwiftUI<br><br>class HomeViewModel: ObservableObject {<br>    // ... (previously defined code)<br><br>    func logWaterIntake(amount: Int) {<br>        waterIntakeStatus += amount<br>        DataManager.shared.saveWaterIntake(amount, forDate: Date())<br>    }<br><br>    func waterIntake(forDate date: Date) -&gt; Int? {<br>        return DataManager.shared.getWaterIntake(forDate: date)<br>    }<br>}<br><br>import SwiftUI<br><br>struct HomeView: View {<br>    @EnvironmentObject private var viewModel: HomeViewModel<br>    @State private var selectedDate = Date()<br><br>    var body: some View {<br>        VStack {<br>            DatePicker(&quot;Select Date&quot;, selection: $selectedDate, displayedComponents: .date)<br>                .padding()<br><br>            if let waterIntake = viewModel.waterIntake(forDate: selectedDate) {<br>                Text(&quot;Water Intake on \(selectedDate, formatter: DateFormatterManager.shared.dateFormatter): \(waterIntake) ml&quot;)<br>            } else {<br>                Text(&quot;No water intake recorded for \(selectedDate, formatter: DateFormatterManager.shared.dateFormatter)&quot;)<br>            }<br><br>            // ... (other UI elements)<br>        }<br>    }<br>}</pre><p>From now on I will refrain from sharing all the specific details of each interaction with <strong>ChatGPT</strong>. Working on this application led to numerous discussions with <strong>ChatGPT</strong>, and at times, it was more challenging for me to articulate my requirements effectively than to write the code myself.</p><p>This experiment reinforced a principle I was already familiar with: the quality of responses is heavily influenced by the context and prompts provided. It serves as a reminder that well-crafted context and prompts significantly enhance the quality of the generated responses.</p><h3>Add The Log Water Intake Screen</h3><blockquote>Intuitive Water Entry:<br>Easily log your water consumption with a quick and straightforward interface.</blockquote><p>By <strong>ChatGPT</strong> acting as an app designer</p><p>I initially had a basic water intake screen connected to the home screen. However, I wanted to display the water intake using two rows of glasses. Despite my efforts, I struggled to effectively convey this concept to <strong>ChatGPT</strong>. Consequently, I took it upon myself to update the user interface.</p><p>Once I had the code in place, my next requirement was images for the glasses. In this endeavor, <strong>Midjourney</strong> emerged as the preferred choice and provided the needed assets.</p><p>The process wasn’t without its challenges. In fact, the provided samples represent less than half of the images I generated in my quest to find the matching ones.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aOprwI8IyXXrFl0S54lTcw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TU5yn-min10IbM38OzrMSw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F8jihLNvnCU5ReX3JwXmbA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9jwf7VYAW0SRQRByoLHZeA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4N6eglszHc9pKmxAn5bJ0A.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UPPPPC4rJCbWnduxkeEaRg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LnN8YOwzmRKSeay2keXPvg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wYqLB74lMLETb2nPoKIWpA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nM93uLNa4Ua8om7gjJwSiQ.png" /></figure><p>Now, let’s explore the final screen before and after:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-ZUA1oTKdSRk-0oBXe7Ozg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oCtmLdSzutSQGEJrVrI3_Q.png" /></figure><h3>Add The Settings Screen</h3><blockquote>Customizable Daily Goal:<br>Choose your preferred daily water intake goal.<br>Automatic Weight-Based Calculation:<br>Optionally, provide your weight, and the app will calculate your recommended daily water intake based on it.</blockquote><p>By <strong>ChatGPT</strong> acting as app designer</p><p>The “Settings” component is a pivotal element of this puzzle. I opted not to connect the app to health data at this stage, choosing instead to allow users to manually input the information required for water intake calculation. In this approach, users provide the necessary data, and the app takes care of calculating and storing the suggested water intake.</p><blockquote>Create a code for settings screen, on this screen user will calculate water intake needed for his body</blockquote><pre>import SwiftUI<br><br>struct SettingsView: View {<br>    @State private var weight: String = &quot;&quot;<br>    @State private var waterIntake: Double = 0.0<br><br>    var body: some View {<br>        NavigationView {<br>            Form {<br>                Section(header: Text(&quot;Calculate Needed Water Intake&quot;)) {<br>                    TextField(&quot;Enter your weight (kg)&quot;, text: $weight)<br>                        .keyboardType(.decimalPad)<br>                    <br>                    Button(&quot;Calculate&quot;) {<br>                        calculateWaterIntake()<br>                    }<br>                    <br>                    Text(&quot;Recommended Water Intake: \(waterIntake, specifier: &quot;%.1f&quot;) ml&quot;)<br>                        .foregroundColor(.accentColor)<br>                }<br>            }<br>            .navigationBarTitle(&quot;Settings&quot;)<br>        }<br>    }<br>    <br>    func calculateWaterIntake() {<br>        if let weightValue = Double(weight) {<br>            // Calculate water intake based on weight (example formula)<br>            waterIntake = weightValue * 30.0 // Adjust the formula as needed<br>        } else {<br>            waterIntake = 0.0<br>        }<br>    }<br>}</pre><p>This was the first from the many prompts I used to produce the “Settings” screen. Piece by piece I was completing the puzzle.</p><p>I knew that there is a way to calculate suggested water intake. So I asked <strong>ChatGPT</strong> for it:</p><blockquote>Make me an equation how much water should a person drink</blockquote><p>Response:</p><blockquote>Sure, here’s a simple equation that you can use as a starting point to estimate daily water intake for adults. Keep in mind that individual hydration needs can vary based on factors such as age, weight, activity level, climate, and health conditions.</blockquote><blockquote>Water Intake (in milliliters) = Body Weight (in kilograms) * Hydration Factor</blockquote><blockquote>Where:</blockquote><blockquote>Body Weight is the person’s weight in kilograms.</blockquote><blockquote>Hydration Factor is a value that you can adjust based on individual factors. A common range is between 30–40 mL of water per kilogram of body weight.</blockquote><blockquote>For example, if someone weighs 70 kilograms and you use a hydration factor of 35 mL/kg:</blockquote><blockquote>Water Intake = 70 kg * 35 mL/kg = 2450 mL (approximately 2.45 liters)</blockquote><blockquote>Remember that this is a general guideline, and it’s important to consider other factors like activity level, climate, and personal preferences. Always consult with a healthcare professional or registered dietitian for personalized recommendations on water intake, especially if you have specific health conditions or dietary needs.</blockquote><p>After conducting tests on various water drinking apps and observing that many of them incorporated gender as a parameter in their intake calculation I did the same:</p><blockquote>Add gender parameter to the equation</blockquote><p>Response:</p><blockquote>Certainly, adding a gender parameter to the equation can help adjust the estimated water intake based on differences in body composition and physiological needs between genders. Here’s the equation with a gender parameter:</blockquote><blockquote>Water Intake (in milliliters) = Body Weight (in kilograms) * Gender Factor * Hydration Factor</blockquote><blockquote>Where:</blockquote><blockquote>Body Weight is the person’s weight in kilograms.</blockquote><blockquote>Gender Factor is a coefficient that considers the difference between genders. This factor can be adjusted based on the individual’s gender.</blockquote><blockquote>Hydration Factor is the amount of water per kilogram of body weight.</blockquote><blockquote>For example, if someone weighs 70 kilograms, identifies as male, and you use a hydration factor of 35 mL/kg:</blockquote><blockquote>If Gender Factor for males = 1: Water Intake = 70 kg * 1 * 35 mL/kg = 2450 mL (approximately 2.45 liters)</blockquote><blockquote>If Gender Factor for females = 0.8: Water Intake = 70 kg * 0.8 * 35 mL/kg = 1960 mL (approximately 1.96 liters)</blockquote><blockquote>Remember that the gender factor can vary based on individual differences and may not be a fixed number. Factors such as muscle mass, metabolism, and overall health can influence water needs. It’s a good idea to consult with a healthcare professional or registered dietitian for personalized recommendations, especially if you have specific health conditions or dietary needs.</blockquote><p>I had the basic requirements crystalised now and could start building the UI:</p><blockquote>Include gender in SettingsView for calculating suggested water intake</blockquote><p>Response:</p><pre>struct SettingsView: View {<br>    @State private var weight: Double = 70<br>    @State private var selectedGender: Gender = .male<br><br>    enum Gender {<br>        case male<br>        case female<br>    }<br><br>    var suggestedWaterIntake: Double {<br>        let hydrationFactor: Double = 35 // You can adjust this value<br>        let genderFactor: Double = selectedGender == .male ? 1.0 : 0.8<br><br>        return weight * genderFactor * hydrationFactor<br>    }<br><br>    var body: some View {<br>        VStack {<br>            Text(&quot;Calculate Suggested Water Intake&quot;)<br>                .font(.title)<br><br>            Picker(&quot;Select Gender&quot;, selection: $selectedGender) {<br>                Text(&quot;Male&quot;).tag(Gender.male)<br>                Text(&quot;Female&quot;).tag(Gender.female)<br>            }<br>            .pickerStyle(SegmentedPickerStyle())<br>            .padding()<br><br>            Slider(value: $weight, in: 1...150, step: 1) {<br>                Text(&quot;Weight: \(Int(weight)) kg&quot;)<br>            }<br>            .padding()<br><br>            Text(&quot;Suggested Water Intake: \(Int(suggestedWaterIntake)) mL&quot;)<br>                .font(.headline)<br>                .foregroundColor(.accentColor)<br>                .padding()<br>        }<br>    }<br>}</pre><p>Now, I needed to update the weight support functionality:</p><blockquote>include body weight</blockquote><p>Response:</p><pre>struct SettingsView: View {<br>    @State private var weightKg: Double = 70<br>    @State private var weightLbs: Double = 154 // Default value for 70 kg<br><br>    @State private var selectedGender: Gender = .male<br><br>    enum Gender {<br>        case male<br>        case female<br>    }<br><br>    var suggestedWaterIntake: Double {<br>        let hydrationFactor: Double = 35 // You can adjust this value<br>        let genderFactor: Double = selectedGender == .male ? 1.0 : 0.8<br>        let weightInKg = selectedUnit == .kg ? weightKg : weightLbs * 0.453592 // Convert lbs to kg<br><br>        return weightInKg * genderFactor * hydrationFactor<br>    }<br><br>    var body: some View {<br>        VStack {<br>            Text(&quot;Calculate Suggested Water Intake&quot;)<br>                .font(.title)<br><br>            Picker(&quot;Select Gender&quot;, selection: $selectedGender) {<br>                Text(&quot;Male&quot;).tag(Gender.male)<br>                Text(&quot;Female&quot;).tag(Gender.female)<br>            }<br>            .pickerStyle(SegmentedPickerStyle())<br>            .padding()<br><br>            Picker(&quot;Select Unit&quot;, selection: $selectedUnit) {<br>                Text(&quot;kg&quot;).tag(Unit.kg)<br>                Text(&quot;lbs&quot;).tag(Unit.lbs)<br>            }<br>            .pickerStyle(SegmentedPickerStyle())<br>            .padding()<br><br>            if selectedUnit == .kg {<br>                Slider(value: $weightKg, in: 1...150, step: 1) {<br>                    Text(&quot;Weight: \(Int(weightKg)) kg&quot;)<br>                }<br>            } else {<br>                Slider(value: $weightLbs, in: 1...330, step: 1) {<br>                    Text(&quot;Weight: \(Int(weightLbs)) lbs&quot;)<br>                }<br>            }<br>            .padding()<br><br>            Text(&quot;Suggested Water Intake: \(Int(suggestedWaterIntake)) mL&quot;)<br>                .font(.headline)<br>                .foregroundColor(.accentColor)<br>                .padding()<br>        }<br>    }<br><br>    enum Unit {<br>        case kg<br>        case lbs<br>    }<br><br>    @State private var selectedUnit: Unit = .kg<br>}</pre><p>After a few more iterations, I arrived at the following result:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S3cBJY2eblgQvfS30jTqdg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*k7LwyfPJrtPUHFx0i6dsyQ.png" /></figure><h3>The Home Screen</h3><p>I made numerous requests to <strong>ChatGPT</strong> to shape the UI. While some responses were helpful and most were functional, they often didn’t align precisely with my intended design. I recognized that this particular screen involved a significant amount of graphics, which may have contributed to the challenges I encountered. <strong>ChatGPT</strong> tends to excel in generating code, but describing visual aspects precisely can be a more complex task.</p><p>Ultimately, I ended up writing most of the UI code myself, with <strong>ChatGPT</strong> assisting in specific cases, such as creating custom shapes or animations. The AI played a valuable role in providing code-building-blocks for me to incorporate.</p><p>Now, here’s how the home screen appears. The image becomes less blurred as the user consumes water:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KJS9i1YV0lEkxoBd1plgPg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pscBLGetzkiqjS3bq5LIKw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3DISTYuYvCw7Ax9ByUur3g.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fLHEEv__1bjEAgp7IJqG7w.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YBYU_KieGaJXHW7-2g9nAA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PhfUz0OiGzGVFtVXbul0OQ.png" /></figure><h3>The Colors</h3><p>In my quest to choose a color palette for the app, I sought assistance from my AI designer, opting for a more personalized approach rather than relying on online sources as I typically would:</p><blockquote>What main color would fit water drinking app with random nature images in the background?</blockquote><p>And I got:</p><blockquote>Aqua Blue or Turquoise: Shades of aqua blue or turquoise are often associated with water and ocean themes. They convey a sense of freshness, purity, and tranquility.</blockquote><blockquote>Leaf Green: Green is commonly associated with nature and health. A leafy green shade can convey the idea of natural hydration and wellness.</blockquote><blockquote>Sky Blue: Light blue shades reminiscent of the sky can evoke a feeling of refreshment and openness. They pair well with nature imagery.</blockquote><blockquote>Soft Teal: A soft teal color combines elements of blue and green, creating a soothing and calming hue that aligns with nature.</blockquote><blockquote>Cool Gray: A cool gray color can provide a neutral backdrop that complements the random nature images while giving a modern and clean look.</blockquote><blockquote>Mint Green: Mint green is a cool and refreshing color that can tie into both the water theme and the natural imagery.</blockquote><p>I made a mistake that fortunately could be easily corrected:</p><blockquote>Give me rgb of these colors</blockquote><p>Which finally produced something I could use:</p><blockquote>Aqua Blue / Turquoise: RGB: (0, 175, 230) or #00AFE6</blockquote><blockquote>Leaf Green: RGB: (46, 139, 87) or #2E8B57</blockquote><blockquote>Sky Blue: RGB: (135, 206, 235) or #87CEEB</blockquote><blockquote>Soft Teal: RGB: (90, 190, 175) or #5ABEAF</blockquote><blockquote>Cool Gray: RGB: (169, 169, 169) or #A9A9A9</blockquote><blockquote>Mint Green: RGB: (152, 255, 152) or #98FF98</blockquote><p>In the context of this simple app, I only required the main color for the Minimum Viable Product (MVP). However, it’s worth noting that <strong>ChatGPT</strong> possesses the capability to handle much more complex tasks and provide comprehensive solutions across a wide range of software development needs. Its potential extends far beyond basic color selection.</p><h3>The Notifications</h3><p>To implement reminders for the user to drink water, I decided to utilize local notifications. This time, I turned to my AI developer and requested:</p><blockquote>Generate the code to schedule 10 local notifications for drinking water</blockquote><p>Response:</p><pre>func scheduleNotifications() {<br>        let center = UNUserNotificationCenter.current()<br>        <br>        // Request permission for notifications<br>        center.requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in<br>            if granted {<br>                for i in 1...10 {<br>                    let content = UNMutableNotificationContent()<br>                    content.title = &quot;Drink Water!&quot;<br>                    content.body = &quot;It&#39;s time to drink water.&quot;<br>                    content.sound = .default<br>                    <br>                    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(i * 3600), repeats: false)<br>                    <br>                    let request = UNNotificationRequest(identifier: &quot;water\(i)&quot;, content: content, trigger: trigger)<br>                    center.add(request) { error in<br>                        if let error = error {<br>                            print(&quot;Error scheduling notification: \(error)&quot;)<br>                        }<br>                    }<br>                }<br>            } else if let error = error {<br>                print(&quot;Error requesting authorization: \(error)&quot;)<br>            }<br>        }<br>    }</pre><p>I realized that I hadn’t formulated the request correctly.</p><blockquote>Schedule drinking water each hour from 10 to 20, for the whole wheek</blockquote><p>Response:</p><pre>func scheduleNotifications() {<br>        let center = UNUserNotificationCenter.current()<br>        <br>        // Request permission for notifications<br>        center.requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in<br>            if granted {<br>                let daysOfWeek: [Int] = Array(1...7) // 1 is Sunday, 2 is Monday, and so on<br>                <br>                for day in daysOfWeek {<br>                    for hour in 10...20 {<br>                        let content = UNMutableNotificationContent()<br>                        content.title = &quot;Drink Water!&quot;<br>                        content.body = &quot;It&#39;s time to drink water.&quot;<br>                        content.sound = .default<br>                        <br>                        var dateComponents = DateComponents()<br>                        dateComponents.weekday = day<br>                        dateComponents.hour = hour<br>                        dateComponents.minute = 0 // Set the minute to 0<br>                        <br>                        let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)<br>                        <br>                        let request = UNNotificationRequest(identifier: &quot;water\(day)\(hour)&quot;, content: content, trigger: trigger)<br>                        center.add(request) { error in<br>                            if let error = error {<br>                                print(&quot;Error scheduling notification: \(error)&quot;)<br>                            }<br>                        }<br>                    }<br>                }<br>            } else if let error = error {<br>                print(&quot;Error requesting authorization: \(error)&quot;)<br>            }<br>        }<br>    }</pre><p>Finally, I wanted to have more appealing text for the notifications:</p><blockquote>Generate title and subtitle for notification for drinking water app</blockquote><p>Response:</p><pre>func generateNotificationTitle() -&gt; String {<br>    let titles = [&quot;Stay Hydrated!&quot;, &quot;Time to Drink!&quot;, &quot;Stay Healthy!&quot;, &quot;Hydration Reminder&quot;]<br>    return titles.randomElement() ?? &quot;Drink Water!&quot;<br>}<br><br>func generateNotificationSubtitle() -&gt; String {<br>    let subtitles = [&quot;Your body needs water.&quot;, &quot;Stay refreshed and hydrated.&quot;, &quot;Keep up the good work!&quot;, &quot;Don&#39;t forget to drink.&quot;]<br>    return subtitles.randomElement() ?? &quot;It&#39;s time to drink water.&quot;<br>}</pre><h3>The Tests</h3><p>Writing the code is just the beginning of a software project. To ensure reliability and maintainability, thorough testing is crucial. This includes unit tests, integration tests, and more. Additionally, testing helps in safely adding new code, making refactors, and collaborating with other team members on the same codebase.</p><p><strong>ChatGPT</strong> can assist in creating test cases, testing strategies, and even explaining how to conduct various types of testing. It’s a valuable resource in the software development process beyond just code generation.</p><blockquote>Write unit tests for *I pasted the code here*</blockquote><p>Result:</p><pre>class SettingsViewModelTests: XCTestCase {<br><br>    // Create a mock DataManager for testing<br>    class MockDataManager: DataManager {<br>        // Implement mock functions used in your viewModel<br>        // You can override save and load methods to store and retrieve values in memory for testing<br>        // For simplicity, we&#39;ll just store values in memory using variables<br>        var settingsInfo: SettingsInfo?<br>        var requiredWaterIntake: Int?<br><br>        override func save(requiredWaterIntake: Int) {<br>            self.requiredWaterIntake = requiredWaterIntake<br>        }<br><br>        override func loadSettingsInfo() -&gt; SettingsInfo? {<br>            return settingsInfo<br>        }<br>    }<br><br>    // Create an instance of SettingsViewModel with the mock DataManager<br>    var viewModel: SettingsViewModel!<br>    var mockDataManager: MockDataManager!<br><br>    override func setUp() {<br>        super.setUp()<br>        mockDataManager = MockDataManager()<br>        viewModel = SettingsViewModel(dataManager: mockDataManager)<br>    }<br><br>    override func tearDown() {<br>        viewModel = nil<br>        mockDataManager = nil<br>        super.tearDown()<br>    }<br><br>    func testSuggestedWaterIntakeCalculation() {<br>        // Set up your viewModel with test data<br>        viewModel.selectedGender = .male<br>        viewModel.selectedUnit = .kg<br>        viewModel.weightKg = 70<br><br>        // Calculate the suggested water intake<br>        let suggestedWaterIntake = viewModel.suggestedWaterIntake<br><br>        // Assert that the calculation is correct based on your formula<br>        XCTAssertEqual(suggestedWaterIntake, 70 * Int(1.0) * 35) // Adjust the expected value based on your formula<br>    }<br><br>    func testSuggestedWaterIntakeSaved() {<br>        // Set up your viewModel with test data<br>        viewModel.selectedGender = .female<br>        viewModel.selectedUnit = .lbs<br>        viewModel.weightLbs = 154<br><br>        // Calculate the suggested water intake, which should also save it through the mock DataManager<br>        _ = viewModel.suggestedWaterIntake<br><br>        // Assert that the required water intake was saved<br>        XCTAssertEqual(mockDataManager.requiredWaterIntake, Int(154 * 0.453592 * 0.8 * 35))<br>    }<br>    <br>    // Add more test cases as needed<br>}</pre><p>While it may not be ideal, it’s undeniable that <strong>ChatGPT</strong> provides a valuable starting point, generating code swiftly within seconds. This can significantly expedite the development process and serve as a foundation for further refinement and optimization.</p><h3>The Conclusions</h3><p>AI indeed offers substantial benefits in the realm of business analysis, idea validation, feature planning, text generation, script preparation, and various text-related tasks.</p><p>For app development, AI proves highly valuable in expediting the initial phases by swiftly generating foundational code upon which to build.</p><p>Furthermore, AI excels in providing specific code segments, bug identification, and test case generation, with results improving when prompts are detailed and specific.</p><p>In terms of design, AI is exceptional at crafting prototype designs, app icons, and assets, although maintaining asset consistency and precise alignment with requirements can pose challenges. Nevertheless, the iterative process is notably rapid.</p><p>It’s important to emphasize that AI thrives on clear and detailed prompts to function efficiently. Even with this, a fundamental understanding is essential to effectively integrate all components into the development process.</p><p>I hold the belief that the synergy between a developer and AI surpasses the capabilities of an individual developer alone. Similarly, this holds true for designers and other roles. However, it’s important to underscore that AI remains a valuable <strong>tool</strong> at our disposal, augmenting our abilities and enabling us to achieve more efficient and effective results in various domains.</p><p><strong>Note</strong>: I primarily utilize <strong>ChatGPT</strong> for code-related tasks due to my familiarity with it. However, I also acknowledge the capabilities of <strong>Bard</strong>, which, despite a slower start today, is a valuable tool by default connected to the internet (ChatGPT3.5 is not…) . I encourage you to explore both options to determine which best suits your needs.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2aaffe8e9ec4" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Comparing images using the Vision framework]]></title>
            <link>https://medium.com/@kamil.tustanowski/comparing-images-using-the-vision-framework-ff13291901ff?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/ff13291901ff</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Sun, 23 Jul 2023 08:04:34 GMT</pubDate>
            <atom:updated>2023-07-23T08:04:34.875Z</atom:updated>
            <content:encoded><![CDATA[<p>Photography is one of my hobbies. There is something magical in taking pictures. It’s like stopping time at a particular moment. The only problem is that I’m no expert and not all of my photographs are great, if any, therefore often I take more than one photo. Just to be safe. “Being safe” in my case means I have a bunch of redundant pictures that serve no purpose. I should remove them but I always have a good reason not to. I have more interesting and important things to do or places to be.</p><p>Last time I was complaining that I’m terrible at selfies but managed to find a solution to my little problem thanks to technology. Check my <a href="https://medium.com/@kamil.tustanowski/detecting-face-capture-quality-using-the-vision-framework-e94af66f31e3">Detecting face capture quality using the Vision framework</a> for more details. This time is no different.</p><p>The Vision request I will introduce today is unique. VNGenerateImageFeaturePrintRequest behaves slightly different from the other requests. But we will get to that. For now, knowing that this inconspicuous request is able to compare images for us is enough. Let&#39;s begin with:</p><pre>import Vision</pre><p>Then create a request:</p><pre>let request = VNGenerateImageFeaturePrintRequest()</pre><p>And pass it to the request handler:</p><pre>guard let cgImage = image.cgImage else { return nil }<br>let request = VNGenerateImageFeaturePrintRequest()<br><br>let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                           orientation: .init(image.imageOrientation),<br>                                           options: [:])<br>do {<br>    try requestHandler.perform([request])<br>} catch {<br>    print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>}</pre><p>This is explained in detail in my <a href="https://medium.com/@kamil.tustanowski/detecting-body-pose-using-vision-framework-caba5435796a">Detecting body pose using Vision framework</a> article.</p><p>As the result, we get a VNFeaturePrintObservation:</p><pre>guard let result = request.results?.first else { return nil }<br>return result</pre><p>The complete function:</p><pre>func process(_ image: UIImage) -&gt; VNFeaturePrintObservation? {<br>    guard let cgImage = image.cgImage else { return nil }<br>    let request = VNGenerateImageFeaturePrintRequest()<br>    <br>    let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                               orientation: .init(image.imageOrientation),<br>                                               options: [:])<br>    do {<br>        try requestHandler.perform([request])<br>    } catch {<br>        print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>    }<br>    <br>    guard let result = request.results?.first else { return nil }<br>    return result<br>}</pre><p>Note: I’m working with this code in a playground and it makes things easier when the function is synchronous but the Vision requests will block the main thread of your application. Make sure to execute them i.e. on:</p><pre>private let visionQueue = DispatchQueue.global(qos: .userInitiated)</pre><p>But remember to return to the main queue to display the results.</p><p>And that’s it. The work is done.</p><p>This is the place where you should ask</p><blockquote>How it’s done? What about comparing the images?</blockquote><p>This is why this request is unique. VNFeaturePrintObservation holds the calculated feature print data. We can&#39;t parse it and use it to compare images in an easy way but the observation can do it for us. The way we can use it to compare images is to use observation to compute the distance between the feature prints.</p><p>To compare two images similarity we need to:</p><ul><li>Use the request on both of them to get the VNFeaturePrintObservation containing feature print data for each of them.</li><li>We use this function provided by the observations to compare the distance between the feature prints they hold:</li></ul><pre>open func computeDistance(_ outDistance: UnsafeMutablePointer&lt;Float&gt;, to featurePrint: VNFeaturePrintObservation) throws</pre><p>Notice the UnsafeMutablePointer&lt;Float&gt; which is our outDistance. Returning values is not the only way function can provide its result:</p><blockquote>You use instances of the UnsafeMutablePointer type to access data of a specific type in memory. The type of data that a pointer can access is the pointer’s Pointee type. UnsafeMutablePointer provides no automated memory management or alignment guarantees. You are responsible for handling the life cycle of any memory you work with through unsafe pointers to avoid leaks or undefined behavior.</blockquote><p><a href="https://developer.apple.com/documentation/swift/unsafemutablepointer">UnsafeMutablePointer Documentation</a></p><p>This is a pointer to a Float located somewhere in memory that our function will use to pass the result.</p><p>Pointers are not something we use in Swift on a daily basis. I remember pointers mainly from the C / C++ and... Objective-C days. You can find more information <a href="https://www.cplusplus.com/doc/tutorial/pointers/">here</a>.</p><p>We know now that with pointers we can access specific locations in the memory but this doesn’t explain what we need to do to get our distance.</p><p>First, we need to have something to compare. This code will produce observations for two images:</p><pre>let balloon1 = UIImage(named: &quot;balloon_1.jpg&quot;)!<br>let balloon1FeaturePrint = process(balloon1)!<br><br>let balloon2 = UIImage(named: &quot;balloon_2.jpg&quot;)!<br>let balloon2FeaturePrint = process(balloon2)!</pre><p>I use ! for simplicity. I don’t recommend this approach on production code.</p><p>Then we need to make a variable for our distance:</p><pre>var balloon1ToBallon2Distance: Float = .infinity</pre><p>It will hold the distance between feature prints of balloon_1 and balloon_2 images. The lesser the distance is the more the images are similar. Identical images will have a 0.0 distance between their features. That&#39;s why we use .infinity as the initial value.</p><p>Now it’s the time to face the UnsafeMutablePointer&lt;Float&gt;. We are used to providing parameters for functions but this is different. This time we need to provide a pointer which the function can later use to provide us the result. To do it we will use &amp; which in <a href="https://www.cplusplus.com/doc/tutorial/pointers/">C++ is called address-of operator</a>. Instead of providing balloon1ToBallon2Distance we will pass the &amp;balloon1ToBallon2Distance which is a pointer containing an address to a place in memory where this var is located:</p><pre>do {<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToBallon2Distance, to: balloon2FeaturePrint)<br>} catch {<br>    print(&quot;Couldn&#39;t compute the distance&quot;)<br>}</pre><p>And after the function is complete we will have the result in this variable. Pointers are powerful but also terrifying and dangerous. Behold their power:</p><pre>print(balloon1ToBallon2Distance)</pre><pre>11.10728</pre><p>This variable was .infinity a while ago and now it contains the distance between feature points of balloon_1 and balloon_2 images. They are pretty close which means they are similar.</p><p>See it for yourself. I made a few photographs of this year’s Christmas tree:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*OQ59ZpO_iv27N6CdTxJiDA.png" /></figure><p>As you can see I made four photos of a balloon, one of a heart. The plane photo is for demonstration purposes. The plane is a photo by <a href="https://unsplash.com/@nbb_photos">Lacie Slezak</a>.</p><p>Let’s imagine we are working on an application that will help us with removing the redundant photographs. The user makes a few photographs and we want to know whether they are similar or not. If we find similar photographs we can try to help the user to select the best one and automatically get rid of the rest.</p><p>First, we need to have these feature prints for each image:</p><pre>let balloon1 = UIImage(named: &quot;balloon_1.jpg&quot;)!<br>let balloon1FeaturePrint = process(balloon1)!<br><br>let balloon2 = UIImage(named: &quot;balloon_2.jpg&quot;)!<br>let balloon2FeaturePrint = process(balloon2)!<br><br>let balloon3 = UIImage(named: &quot;balloon_3.jpg&quot;)!<br>let balloon3FeaturePrint = process(balloon3)!<br><br>let balloon4 = UIImage(named: &quot;balloon_4.jpg&quot;)!<br>let balloon4FeaturePrint = process(balloon4)!<br><br>let heart = UIImage(named: &quot;heart.jpg&quot;)!<br>let heartFeaturePrint = process(heart)!<br><br>let plane = UIImage(named: &quot;plane.jpg&quot;)! // Original photo by https://unsplash.com/@nbb_photos<br>let planeFeaturePrint = process(plane)!</pre><p>Then we need to prepare variables for the distance:</p><pre>var balloon1ToBallon2Distance: Float = .infinity<br>var balloon1ToBallon3Distance: Float = .infinity<br>var balloon1ToBallon4Distance: Float = .infinity<br>var balloon1ToHeartDistance: Float = .infinity<br>var balloon1ToPlaneDistance: Float = .infinity</pre><p>The last piece is to calculate distances using observations:</p><pre>do {<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToBallon2Distance, to: balloon2FeaturePrint)<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToBallon3Distance, to: balloon3FeaturePrint)<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToBallon4Distance, to: balloon4FeaturePrint)<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToHeartDistance, to: heartFeaturePrint)<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToPlaneDistance, to: planeFeaturePrint)<br>} catch {<br>    print(&quot;Couldn&#39;t compute the distance&quot;)<br>}</pre><p>The idea is simple. We take the first image and compare it to the next images. When the distance is small we can assume the images are similar therefore redundant.</p><p>Let’s see the results:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*UDeZBEAjmf16P-VHfXumVQ.png" /></figure><pre>11.10728</pre><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*5ugkexB1r1s2gQS3S4AjUw.png" /></figure><pre>11.60783</pre><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*v1V1OPhIucO7rUk1SAdf-g.png" /></figure><pre>10.46046</pre><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*rW0HdM7kicgQDFhLH6MTRg.png" /></figure><pre>21.83002</pre><p>The Christmas tree is the same and the heart is located near the balloon but the distance has doubled. This is good because this photograph is different than the previous four. The background is similar but the salient object has changed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*_na1peB5ow3glm2KbsAa1Q.png" /></figure><pre>28.88646</pre><p>The plane distance from the balloon_1 is almost three times larger than from the other balloons.</p><p>Thanks to this request you can group similar images together. When they are grouped together you can analyze them one by one trying to find the best looking, least blurry image of them all. You can apply other vision requests like saliency detection to find which one has objects positioned in a better way, the face capture quality request can find the one with better-captured faces.</p><p>You can find the code here:</p><pre>import UIKit<br>import Vision<br><br>func process(_ image: UIImage) -&gt; VNFeaturePrintObservation? {<br>    guard let cgImage = image.cgImage else { return nil }<br>    let request = VNGenerateImageFeaturePrintRequest()<br>    <br>    let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                               orientation: .init(image.imageOrientation),<br>                                               options: [:])<br>    do {<br>        try requestHandler.perform([request])<br>    } catch {<br>        print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>    }<br>    <br>    guard let result = request.results?.first else { return nil }<br>    return result<br>}<br><br>let balloon1 = UIImage(named: &quot;balloon_1.jpg&quot;)!<br>let balloon1FeaturePrint = process(balloon1)!<br><br>let balloon2 = UIImage(named: &quot;balloon_2.jpg&quot;)!<br>let balloon2FeaturePrint = process(balloon2)!<br><br>let balloon3 = UIImage(named: &quot;balloon_3.jpg&quot;)!<br>let balloon3FeaturePrint = process(balloon3)!<br><br>let balloon4 = UIImage(named: &quot;balloon_4.jpg&quot;)!<br>let balloon4FeaturePrint = process(balloon4)!<br><br>let heart = UIImage(named: &quot;heart.jpg&quot;)!<br>let heartFeaturePrint = process(heart)!<br><br>let plane = UIImage(named: &quot;plane.jpg&quot;)! // Original photo by https://unsplash.com/@nbb_photos<br>let planeFeaturePrint = process(plane)!<br><br>var balloon1ToBallon2Distance: Float = .infinity<br>var balloon1ToBallon3Distance: Float = .infinity<br>var balloon1ToBallon4Distance: Float = .infinity<br>var balloon1ToHeartDistance: Float = .infinity<br>var balloon1ToPlaneDistance: Float = .infinity<br><br>do {<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToBallon2Distance, to: balloon2FeaturePrint)<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToBallon3Distance, to: balloon3FeaturePrint)<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToBallon4Distance, to: balloon4FeaturePrint)<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToHeartDistance, to: heartFeaturePrint)<br>    try balloon1FeaturePrint.computeDistance(&amp;balloon1ToPlaneDistance, to: planeFeaturePrint)<br>} catch {<br>    print(&quot;Couldn&#39;t compute the distance&quot;)<br>}</pre><p>If you want to play with Vision and see it for yourself you can check the latest version of my vision demo application <a href="https://github.com/ktustanowski/visiondemo">here</a>. You can find the code used in this article <a href="https://github.com/ktustanowski/visiondemo/blob/main/VisionDemoApp/TheVision.playground/Pages/VNGenerateImageFeaturePrintRequest.xcplaygroundpage/Contents.swift">here</a>.</p><p>If you have any feedback, or just want to say hi, you are more than welcome to write me an [e-mail] (<a href="mailto:kamil.tustanowski@gmail.com">mailto:kamil.tustanowski@gmail.com</a>) or <a href="https://twitter.com/intent/tweet?screen_name=tustanowskik&amp;ref_src=twsrc%5Etfw">tweet to @tustanowskik</a></p><p>If you want to be up to date and always be the first to know what I’m working on tap <a href="https://twitter.com/tustanowskik?ref_src=twsrc%5Etfw">follow @tustanowskik on Twitter</a></p><p>Thank you for reading!</p><p>If you want to help me stay on my feet during the night when I’m working on my blog — now you can:</p><p><a href="https://www.buymeacoffee.com/ktustanowski">Kamil Tustanowski is AI enthusiast, iOS Dev, content cretor</a></p><p>[This was first published on my blog]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ff13291901ff" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Detecting face capture quality using the Vision framework]]></title>
            <link>https://medium.com/@kamil.tustanowski/detecting-face-capture-quality-using-the-vision-framework-e94af66f31e3?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/e94af66f31e3</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Sun, 23 Jul 2023 07:45:20 GMT</pubDate>
            <atom:updated>2023-07-23T07:53:44.512Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bTJqvTN-0PTA1jfsHdwiZw.png" /></figure><p>Selfies. I’m terrible at selfies. Even my daughter laughs at me when I try. I guess:</p><blockquote>I’m too old for this 💩!</blockquote><p><a href="https://lethalweapon.fandom.com/wiki/Roger_Murtaugh">Roger Murtaugh — Lethal Weapon</a></p><p>Potentially.</p><p>What I’m certain of is that technology can help me with my little problem. I may even solve more than one problem at a time.</p><p>I like photography. I like to take pictures. The problem is that I don’t like to select the best pictures and I end up with redundant photos. What if I could assess the selfie quality? I could make fewer pictures if I knew the quality upfront or I could check multiple selfies quality at once and leave the best one.</p><p>This is what we will do today. Despite its mundane character, I think this is one of the features that are easy to apply and can bring a huge benefit to the user.</p><p>To make it happen we need VNDetectFaceCaptureQualityRequest this as a result produces a float number telling us how good the face quality is and a frame to indicate where the face is.</p><p>As usual, we start with:</p><pre>import Vision</pre><p>Then we create the request:</p><pre>let faceQualityRequest = VNDetectFaceCaptureQualityRequest()</pre><p>And we pass it to the request handler:</p><pre>guard let cgImage = image.cgImage else { return }<br>let faceQualityRequest = VNDetectFaceCaptureQualityRequest()<br>let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                           orientation: .init(image.imageOrientation),<br>                                           options: [:])<br>do {<br>    try requestHandler.perform([faceQualityRequest])<br>} catch {<br>    print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>}</pre><p>This is explained in detail in my <a href="https://medium.com/@kamil.tustanowski/detecting-body-pose-using-vision-framework-caba5435796a">Detecting body pose using Vision framework</a> article.</p><p>When a request is performed we get the results:</p><pre>guard let results = faceQualityRequest.results else { return }</pre><p>The result type is VNFaceObservation. From iOS 15 we don&#39;t have to do typecasting anymore.</p><p>We know this type because we were working with it before. This time in VNFaceObservation we are interested in the faceCaptureQuality which will give us a float number:</p><blockquote>The value ranges from 0 to 1. Faces with quality closer to 1 are better lit, sharper, and more centrally positioned than faces with quality closer to 0.</blockquote><p><a href="https://developer.apple.com/documentation/vision/vndetectfacecapturequalityrequest">VNDetectFaceCaptureQualityRequest documentation</a></p><p>Additionally, there is a frame to indicate which info is for which face. More than one face can be verified at a time.</p><p>It’s time to prepare the results for the presentation. This is done in a few phases because of all the translations we need to do:</p><pre>let boxesAndNames = results<br>    .map { (box: $0.boundingBox.rectangle(in: image),<br>            name: &quot;\($0.faceCaptureQuality ?? 0.0)&quot;) }</pre><p>First, we associate the face capture quality with the bounding box and we project the bounding box CGRect onto the image to get non-normalized values.</p><pre>let rectangles = boxesAndNames.map { $0.box }<br>    .map { CGRect(origin: $0.origin.translateFromCoreImageToUIKitCoordinateSpace(using: image.size.height - $0.size.height),<br>                  size: $0.size) }</pre><p>Then we translate the non-normalized CGRect to UIKit coordinate space and populate DisplayableText we used in previous articles:</p><pre>let displayableTexts = zip(rectangles,<br>                           boxesAndNames.map { $0.name })<br>    .map { DisplayableText(frame: $0.0,<br>                           text: $0.1) }</pre><p>This is described in more detail in <a href="https://medium.com/@kamil.tustanowski/barcode-detection-using-vision-framework-b5ce55b82c4e">Barcode detection using Vision framework</a> and <a href="https://medium.com/@kamil.tustanowski/detecting-body-pose-using-vision-framework-caba5435796a">Detecting body pose using Vision framework</a>.</p><p>The last part is to prepare an updated image and set it in our user-facing image view:</p><pre>self?.imageView.image = image.draw(rectangles: rectangles,<br>                                   displayableTexts: displayableTexts)But to do that we need the draw function we remember from previous articles. First, we draw the rectangles indicating faces:</pre><pre>extension UIImage {<br>    func draw(rectangles: [CGRect],<br>              displayableTexts: [DisplayableText],<br>              strokeColor: UIColor = .primary,<br>              lineWidth: CGFloat = 2) -&gt; UIImage? {<br>        let renderer = UIGraphicsImageRenderer(size: size)<br>        return renderer.image { context in<br>            draw(in: CGRect(origin: .zero, size: size))<br><br>            context.cgContext.setStrokeColor(strokeColor.cgColor)<br>            context.cgContext.setLineWidth(lineWidth)<br>            rectangles.forEach { context.cgContext.addRect($0) }<br>            context.cgContext.drawPath(using: .stroke)</pre><p>And then we display texts for each face:</p><pre>let textAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .bold),<br>                                  NSAttributedString.Key.foregroundColor: strokeColor,<br>                                  NSAttributedString.Key.backgroundColor: UIColor.black]<br>            <br>            displayableTexts.forEach { displayableText in<br>                displayableText.text.draw(with: displayableText.frame!,<br>                                          options: [],<br>                                          attributes: textAttributes,<br>                                          context: nil)<br>            }<br>        }<br>    }<br>}</pre><p>And this is it.</p><p>Friendly reminder: It’s good to make a separate queue because vision requests will block the main thread.</p><p>Finally, the exciting part:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*Rxi4BRw4wf-jSPXWk_xymw.jpeg" /></figure><p>Original photy by <a href="https://unsplash.com/@aaronandrewang">Aaron Andrew Ang</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*vPWsZflQi5PseSF-qj_WXA.jpeg" /></figure><p>Original photy by <a href="https://unsplash.com/@benwhitephotography">Ben White</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*eFSVZ4wMyq2nmmsRyRZWjA.jpeg" /></figure><p>Original photy by <a href="https://unsplash.com/@speckfechta">x )</a></p><p>You can see a pattern here. A clear, sharp picture of a face watching towards the camera got a much better score.</p><p>This request can detect the quality of multiple faces:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*ysGixMtxlG3X0J1z9nzd4A.jpeg" /></figure><p>Original photy by <a href="https://unsplash.com/@jasongoodman_youxventures">Jason Goodman</a></p><p>I would like to show you one last photo:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*Jj_ZyJxmJIR_8UOCNvGLqw.jpeg" /></figure><p>Original photy by <a href="https://unsplash.com/@parkstreet">Audrey Fretz</a></p><p>It has the lowest score. But… I like it. My bet is that the rating went down because the face is obstructed, a bit blurry, eyes are closed, it’s too close. But… I like it.</p><p>This request doesn’t tell you whether the selfie is good or bad. It tells whether the face is properly lit, positioned, sharp, and so on. You will know whether, technically speaking, one photo did a better job at exposing the face, than the other. Not which photo is more beautiful.</p><p>⚠️ A word of advice. If you use this request to allow users to purge all the bad quality selfies make sure you ask the user for confirmation before you delete them.</p><p>Below you can find a complete code. You need to provide the image you want to use for the request and have an imageView you want to use to display updated image:</p><pre>let visionQueue = DispatchQueue.global(qos: .userInitiated)<br><br>func process(_ image: UIImage) {<br>    guard let cgImage = image.cgImage else { return }<br>    let faceQualityRequest = VNDetectFaceCaptureQualityRequest()<br>    <br>    let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                               orientation: .init(image.imageOrientation),<br>                                               options: [:])<br><br>    saveImageButton.isHidden = false<br>    visionQueue.async { [weak self] in<br>        do {<br>            try requestHandler.perform([faceQualityRequest])<br>        } catch {<br>            print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>        }<br><br>        guard let results = faceQualityRequest.results else { return }<br>        <br>        let boxesAndNames = results<br>            .map { (box: $0.boundingBox.rectangle(in: image),<br>                    name: &quot;\($0.faceCaptureQuality ?? 0.0)&quot;) }<br>            <br>        let rectangles = boxesAndNames.map { $0.box }<br>            .map { CGRect(origin: $0.origin.translateFromCoreImageToUIKitCoordinateSpace(using: image.size.height - $0.size.height),<br>                          size: $0.size) }<br><br>        let displayableTexts = zip(rectangles,<br>                                   boxesAndNames.map { $0.name })<br>            .map { DisplayableText(frame: $0.0,<br>                                   text: $0.1) }<br>        <br>        DispatchQueue.main.async {<br>            self?.imageView.image = image.draw(rectangles: rectangles,<br>                                               displayableTexts: displayableTexts)<br>        }<br>    }<br>}<br><br>extension UIImage {<br>    func draw(rectangles: [CGRect],<br>              displayableTexts: [DisplayableText],<br>              strokeColor: UIColor = .primary,<br>              lineWidth: CGFloat = 2) -&gt; UIImage? {<br>        let renderer = UIGraphicsImageRenderer(size: size)<br>        return renderer.image { context in<br>            draw(in: CGRect(origin: .zero, size: size))<br><br>            context.cgContext.setStrokeColor(strokeColor.cgColor)<br>            context.cgContext.setLineWidth(lineWidth)<br>            rectangles.forEach { context.cgContext.addRect($0) }<br>            context.cgContext.drawPath(using: .stroke)<br><br>            let textAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .bold),<br>                                  NSAttributedString.Key.foregroundColor: strokeColor,<br>                                  NSAttributedString.Key.backgroundColor: UIColor.black]<br>            <br>            displayableTexts.forEach { displayableText in<br>                displayableText.text.draw(with: displayableText.frame!,<br>                                          options: [],<br>                                          attributes: textAttributes,<br>                                          context: nil)<br>            }<br>        }<br>    }<br>}</pre><p>⚠️ I made a playground version of this code first but noticed that it was producing incorrect output. The same code used in the application on the device is providing correct output but if you run the code in the playgrounds or in the simulator — it’s wrong.</p><p>If you want to play with Vision and see it for yourself you can check the latest version of my vision demo application <a href="https://github.com/ktustanowski/visiondemo">here</a>. You can find the code used in this article <a href="https://github.com/ktustanowski/visiondemo/blob/main/VisionDemoApp/VisionDemo/ImageProcessingViewController%2BFaceQuality.swift">here</a>.</p><p>If you have any feedback, or just want to say hi, you are more than welcome to write me an [e-mail] (<a href="mailto:kamil.tustanowski@gmail.com">mailto:kamil.tustanowski@gmail.com</a>) or <a href="https://twitter.com/intent/tweet?screen_name=tustanowskik&amp;ref_src=twsrc%5Etfw">tweet to @tustanowskik</a></p><p>If you want to be up to date and always be the first to know what I’m working on tap <a href="https://twitter.com/tustanowskik?ref_src=twsrc%5Etfw">follow @tustanowskik on Twitter</a></p><p>Thank you for reading!</p><p>If you want to help me stay on my feet during the night when I’m working on my blog — now you can:</p><p><a href="https://www.buymeacoffee.com/ktustanowski">Kamil Tustanowski is AI enthusiast, iOS Dev, content cretor</a></p><p>[This was first published on my blog]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e94af66f31e3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Speech synthesis using AVSpeechSynthesizer]]></title>
            <link>https://medium.com/@kamil.tustanowski/speech-synthesis-using-avspeechsynthesizer-97b6c5620bb?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/97b6c5620bb</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Fri, 21 Jul 2023 20:03:36 GMT</pubDate>
            <atom:updated>2023-07-21T20:03:36.557Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RpE31OkBCDqZ-8DpjXvFeQ.png" /></figure><p>Do you know what is a dialogue?</p><blockquote>Dialogue is a written or spoken conversational exchange between two or more people, and a literary and theatrical form that depicts such an exchange.</blockquote><p><a href="https://en.wikipedia.org/wiki/Dialogue">Wikipedia</a></p><p>To be honest I have a problem with this definition.</p><blockquote>between two or more people</blockquote><p>Why people? Why can’t we converse with other beings? What is the difference?</p><p>This made me think about the Turing test:</p><blockquote>The Turing test, originally called the imitation game by Alan Turing in 1950, is a test of a machine’s ability to exhibit intelligent behaviour equivalent to, or indistinguishable from, that of a human. Turing proposed that a human evaluator would judge natural language conversations between a human and a machine designed to generate human-like responses. The evaluator would be aware that one of the two partners in conversation is a machine, and all participants would be separated from one another. The conversation would be limited to a text-only channel such as a computer keyboard and screen so the result would not depend on the machine’s ability to render words as speech. If the evaluator cannot reliably tell the machine from the human, the machine is said to have passed the test. The test results do not depend on the machine’s ability to give correct answers to questions, only how closely its answers resemble those a human would give.</blockquote><p><a href="https://en.wikipedia.org/wiki/Turing_test">Wikipedia</a></p><p>It’s a simpler, and actually real, predecessor of <a href="https://bladerunner.fandom.com/wiki/Voight-Kampff_test">the Voight-Kampff test</a>.</p><p>When the evaluator is conversing with human it’s dialogue. When the evaluator is conversing with a machine it’s not a dialogue any more. At least according to Wikipedia.</p><p>But what happens before the evaluator decides who is a person and who is not? Can we call this part of the test a dialogue? A dialogue with a machine?</p><p>I will leave you with this question open. Sorry but I’m not a philosopher.</p><p>My previous article was about <a href="https://medium.com/@kamil.tustanowski/speech-recognition-using-the-speech-framework-72d31f4f344a">Speech recognition using the Speech framework</a>. I presented a way we can talk to our applications and make them recognize our speech. It’s time to allow the applications to speak back. It’s time to give them a voice.</p><p>Did you notice the AV prefix in AVSpeechSynthesizer? No new &amp; fancy frameworks this time just good ol&#39; AVFoundation:</p><pre>import AVFoundation</pre><p>Let’s imagine we are working on a cooking application. We want to allow the user to use the application without touching or even watching the screen. Consider the scenario where we want the application to inform the user that the chicken should be placed in the oven:</p><blockquote>Bake the chicken in the oven for fifteen minutes</blockquote><p>First, we decide what the application will say by using AVSpeechUtterance:</p><pre>let englishUtterance = AVSpeechUtterance(string: &quot;Bake the chicken in the oven for fifteen minutes&quot;)</pre><p>I encourage you to immediately add:</p><pre>englishUtterance.prefersAssistiveTechnologySettings = true</pre><p>⚠️ There are a few ways we can tweak the way the application will speak our message. But what about the users with disabilities who are using VoiceOver? There is a high chance that the voice won’t be identical to the one from the VoiceOver which is confusing and uncomfortable to the user. This line makes sure that when VoiceOver is on our application will use an identical voice.</p><p>Next, we create AVSpeechSynthesizer which we will use in a moment to speak our AVSpeechUtterance:</p><pre>let synthesizer = AVSpeechSynthesizer()</pre><p>If you prefer a simple approach you can add:</p><pre>synthesizer.usesApplicationAudioSession = false</pre><p>But note that:</p><blockquote>If the value of this property is false, the capture session makes use of a private AVAudioSession instance for audio recording, which may cause interruption if your app uses its own audio session for playback.</blockquote><p><a href="https://developer.apple.com/documentation/avfoundation/avcapturesession/1620490-usesapplicationaudiosession">usesApplicationAudioSession documentation</a></p><p>The last part is passing utterance to speech synthesizer:</p><pre>synthesizer.speak(englishUtterance)</pre><p>As soon as you do this you will hear the application talking to you.</p><p>The code:</p><pre>let englishUtterance = AVSpeechUtterance(string: &quot;Bake the chicken in the oven for fifteen minutes&quot;)<br>englishUtterance.prefersAssistiveTechnologySettings = true<br>let synthesizer = AVSpeechSynthesizer()<br>synthesizer.usesApplicationAudioSession = false<br>synthesizer.speak(englishUtterance)</pre><p>Yes. It’s that easy.</p><p>But that’s not all. You can specify concrete language and speech synthesis can speak many different languages. Including the Polish language which I use every day.</p><pre>let polishUtterance = AVSpeechUtterance(string: &quot;Piecz kurczaka w piekarniku przez piętnaście minut&quot;)<br>polishUtterance.prefersAssistiveTechnologySettings = true<br>let polishVoice = AVSpeechSynthesisVoice(language: &quot;pl-PL&quot;)<br>polishUtterance.voice = polishVoice<br>let synthesizer = AVSpeechSynthesizer()<br>synthesizer.usesApplicationAudioSession = false<br>synthesizer.speak(polishUtterance)</pre><p>As you can see we can create a voice matching the language of the text. When you have a voice you need to pass it to the utterance:</p><pre>let polishVoice = AVSpeechSynthesisVoice(language: &quot;pl-PL&quot;)<br>polishUtterance.voice = polishVoice</pre><p>You can paste the code samples into a playground to hear how they sound.</p><p>AVSpeechUtterance has a few configuration options:</p><ul><li>rate - Lower values correspond to slower speech, and higher values correspond to faster speech.</li><li>pitchMultiplier - The baseline pitch the speech synthesizer uses when speaking the utterance.</li><li>postUtteranceDelay and preUtteranceDelay - When multiple utterances are enqueued these values mark the delays between them. One from the start, the other after the end.</li><li>volume - The volume of the speech.</li><li>voice - The voice to be used to read the text. You can use a voice that doesn&#39;t match the country of the text but this won&#39;t end well.</li></ul><p>You can use:</p><pre>print(AVSpeechSynthesisVoice.speechVoices())</pre><p>To see available voices:</p><pre>Language: ar-SA, Name: Maged, Quality: Default [com.apple.ttsbundle.Maged-compact]<br>Language: cs-CZ, Name: Zuzana, Quality: Default [com.apple.ttsbundle.Zuzana-compact]<br>Language: da-DK, Name: Sara, Quality: Default [com.apple.ttsbundle.Sara-compact]<br>Language: de-DE, Name: Anna, Quality: Default [com.apple.ttsbundle.Anna-compact]<br>Language: el-GR, Name: Melina, Quality: Default [com.apple.ttsbundle.Melina-compact]<br>Language: en-AU, Name: Karen, Quality: Default [com.apple.ttsbundle.Karen-compact]<br>Language: en-GB, Name: Daniel, Quality: Default [com.apple.ttsbundle.Daniel-compact]<br>Language: en-IE, Name: Moira, Quality: Default [com.apple.ttsbundle.Moira-compact]<br>Language: en-IN, Name: Rishi, Quality: Default [com.apple.ttsbundle.Rishi-compact]<br>Language: en-US, Name: Samantha, Quality: Default [com.apple.ttsbundle.Samantha-compact]<br>Language: en-ZA, Name: Tessa, Quality: Default [com.apple.ttsbundle.Tessa-compact]<br>Language: es-ES, Name: Mónica, Quality: Default [com.apple.ttsbundle.Monica-compact]<br>Language: es-MX, Name: Paulina, Quality: Default [com.apple.ttsbundle.Paulina-compact]<br>Language: fi-FI, Name: Satu, Quality: Default [com.apple.ttsbundle.Satu-compact]<br>Language: fr-CA, Name: Amélie, Quality: Default [com.apple.ttsbundle.Amelie-compact]<br>Language: fr-FR, Name: Thomas, Quality: Default [com.apple.ttsbundle.Thomas-compact]<br>Language: he-IL, Name: Carmit, Quality: Default [com.apple.ttsbundle.Carmit-compact]<br>Language: hi-IN, Name: Lekha, Quality: Default [com.apple.ttsbundle.Lekha-compact]<br>Language: hu-HU, Name: Mariska, Quality: Default [com.apple.ttsbundle.Mariska-compact]<br>Language: id-ID, Name: Damayanti, Quality: Default [com.apple.ttsbundle.Damayanti-compact]<br>Language: it-IT, Name: Alice, Quality: Default [com.apple.ttsbundle.Alice-compact]<br>Language: ja-JP, Name: Kyoko, Quality: Default [com.apple.ttsbundle.Kyoko-compact]<br>Language: ko-KR, Name: Yuna, Quality: Default [com.apple.ttsbundle.Yuna-compact]<br>Language: nl-BE, Name: Ellen, Quality: Default [com.apple.ttsbundle.Ellen-compact]<br>Language: nl-NL, Name: Xander, Quality: Default [com.apple.ttsbundle.Xander-compact]<br>Language: no-NO, Name: Nora, Quality: Default [com.apple.ttsbundle.Nora-compact]<br>Language: pl-PL, Name: Zosia, Quality: Default [com.apple.ttsbundle.Zosia-compact]<br>Language: pt-BR, Name: Luciana, Quality: Default [com.apple.ttsbundle.Luciana-compact]<br>Language: pt-PT, Name: Joana, Quality: Default [com.apple.ttsbundle.Joana-compact]<br>Language: ro-RO, Name: Ioana, Quality: Default [com.apple.ttsbundle.Ioana-compact]<br>Language: ru-RU, Name: Milena, Quality: Default [com.apple.ttsbundle.Milena-compact]<br>Language: sk-SK, Name: Laura, Quality: Default [com.apple.ttsbundle.Laura-compact]<br>Language: sv-SE, Name: Alva, Quality: Default [com.apple.ttsbundle.Alva-compact]<br>Language: th-TH, Name: Kanya, Quality: Default [com.apple.ttsbundle.Kanya-compact]<br>Language: tr-TR, Name: Yelda, Quality: Default [com.apple.ttsbundle.Yelda-compact]<br>Language: zh-CN, Name: Ting-Ting, Quality: Default [com.apple.ttsbundle.Ting-Ting-compact]<br>Language: zh-HK, Name: Sin-Ji, Quality: Default [com.apple.ttsbundle.Sin-Ji-compact]<br>Language: zh-TW, Name: Mei-Jia, Quality: Default [com.apple.ttsbundle.Mei-Jia-compact]]</pre><p>⚠️ You need to set these properties before enqueuing the utterance because setting it afterward has no effect.</p><p>This will get you going but it will take a lot more to make your application pass the Turing test.</p><p>If you have any feedback, or just want to say hi, you are more than welcome to write me an [e-mail] (<a href="mailto:kamil.tustanowski@gmail.com">mailto:kamil.tustanowski@gmail.com</a>) or <a href="https://twitter.com/intent/tweet?screen_name=tustanowskik&amp;ref_src=twsrc%5Etfw">tweet to @tustanowskik</a></p><p>If you want to be up to date and always be the first to know what I’m working on tap <a href="https://twitter.com/tustanowskik?ref_src=twsrc%5Etfw">follow @tustanowskik on Twitter</a></p><p>Thank you for reading!</p><p>[This was first published on my blog]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=97b6c5620bb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Speech recognition using the Speech framework]]></title>
            <link>https://medium.com/@kamil.tustanowski/speech-recognition-using-the-speech-framework-72d31f4f344a?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/72d31f4f344a</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Fri, 21 Jul 2023 18:44:21 GMT</pubDate>
            <atom:updated>2023-07-21T18:44:21.956Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dloHL32NLqJpZ1ceJv5RqQ.png" /></figure><p>Interfaces. Interfaces everywhere. We use door handles when we want to go out outside, we use keys if we want to secure something, we use the steering wheel if we need to drive somewhere, we buy a ticket if we travel by bus or train.</p><p>Oh and I “almost forgot”, we use applications on our smartphones. The screen is an interface itself that hosts sub-interfaces for each application. Each application’s look and feel is different. Even if they share similarities thanks to conforming to design guidelines they are different and you need to learn how to use them.</p><blockquote>Cognitive load refers to the amount of effort that is exerted or required while reasoning and thinking. Any mental process, from memory to perception to language, creates a cognitive load because it requires energy and effort. When cognitive load is high, thought processes are potentially interfered with. To the UX designer, a common goal when designing interfaces would be to keep users’ cognitive load to a minimum.</blockquote><p><a href="https://www.interaction-design.org/literature/topics/cognitive-load">Check here if you want to know more</a></p><p>Did you hear about “Don’t make me think” or “The Best Interface is No Interface”? These are great books about design. The best about them is that you don’t even need to read them to start learning! Read the titles and think about their meaning for a second.</p><p><strong>Mindblowing.</strong></p><p>Now imagine an application presenting financial data and a user working on a task i.e. comparing reports and making assumptions based on the data. See the poor man touching the screen here and there, copying, binding, adding data to comparison, preparing intermediate results, and so on.</p><p>Imagine someone who is cooking a meal and has the recipe opened on an iPad. See this person working on steps of the recipe who needs to constantly wash and dry their hands to scroll the recipe on the screen.</p><p>Imagine yourself every time you are confused and annoyed by the application’s interface.</p><p>Wouldn’t it be great if you could tell the application what do you want it to do?</p><p>It’s not easy and the road is long and bumpy but this doesn’t mean we can’t start making the first steps.</p><p>Please, allow me to introduce the Speech framework. This will be the cornerstone of our no-interface approach:</p><pre>import Speech</pre><p>In this article, I will focus on the code needed to make speech recognition. I won’t clutter it with the application code. The full code will be linked at the end of the article for you to try on.</p><p>I want the code to be easy to understand and use therefore the output of this article will be a functioning SpeechAnalyzer class:</p><pre>final class SpeechAnalyzer: ObservableObject {<br>}</pre><p>It’s ObservableObject because I&#39;m using SwiftUI in the demo application.</p><p>Since we are interested in “talking” to our applications we will analyze the live audio. We need AVAudioEngine to do that:</p><pre>private let audioEngine = AVAudioEngine()</pre><p>Our analyzer will have a simple, easy to use, API:</p><pre>final class SpeechAnalyzer: ObservableObject {<br>    @Published var recognizedText: String?<br>    @Published var isProcessing: Bool = false<br>    <br>    func start() {}<br>    func stop() {}<br>}</pre><p>First, we will tackle the start function. We are working on live audio therefore we want to configure a few things in the audio session:</p><pre>private var inputNode: AVAudioInputNode?<br>private var audioSession: AVAudioSession?<br><br>func start() {<br>        audioSession = AVAudioSession.sharedInstance()<br>        do {<br>            try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)<br>            try audioSession.setActive(true, options: .notifyOthersOnDeactivation)<br>        } catch {<br>            print(&quot;Couldn&#39;t configure the audio session properly&quot;)<br>        }<br>        <br>        inputNode = audioEngine.inputNode<br>}</pre><p>I don’t want to get into too much detail on this code and will just say that .record will make sure the other audio is silenced and .measurement tells the session we want to:</p><blockquote>minimize the amount of system-supplied signal processing to input and output signals</blockquote><p>We will place taps on AVAudioInputNode in a few seconds.</p><p>Now we are finally getting to a place where it starts to be interesting. We make a few vars we will need to handle speech recognition:</p><pre>private var speechRecognizer: SFSpeechRecognizer?<br>private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?<br>private var recognitionTask: SFSpeechRecognitionTask?</pre><p>We start by instantiating SFSpeechRecognizer:</p><pre>self.speechRecognizer = SFSpeechRecognizer()</pre><p>Which should use the current locale or we can specify a concrete locale:</p><pre>self.speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: &quot;pl_PL&quot;))</pre><p>The initializers for SFSpeechRecognizer will fail and return nil if the locale is not supported for speech recognition:</p><pre>public convenience init?() // Returns speech recognizer with user&#39;s current locale, or nil if is not supported<br><br>public init?(locale: Locale) // returns nil if the locale is not supported</pre><p>SFSpeechRecognizer is a central object that facilitates the recognition but there are a few more needed. Next is:</p><pre>recognitionRequest = SFSpeechAudioBufferRecognitionRequest()</pre><p>Which allows us to customize the way we want to process the audio. We may choose whether we want to use the full power of Apple servers or process the speech on the device:</p><pre>recognitionRequest?.requiresOnDeviceRecognition = true</pre><blockquote>Set this property to true to prevent an SFSpeechRecognitionRequest from sending audio over the network. However, on-device requests won’t be as accurate.</blockquote><p><a href="https://developer.apple.com/documentation/speech/sfspeechrecognitionrequest/3152603-requiresondevicerecognition">requiresOnDeviceRecognition documentation</a></p><p>Note: This will take effect if:</p><pre>speechRecognizer.supportsOnDeviceRecognition</pre><p>Returns true. In other words, this might be possible but is not guaranteed.</p><blockquote>On-device speech recognition is available for some languages, but the framework also relies on Apple’s servers for speech recognition. Always assume that performing speech recognition requires a network connection.</blockquote><p><a href="https://developer.apple.com/documentation/speech">Speech framework documentation</a></p><p>The other option is to allow the request to return partially recognized texts. This makes the process smoother because the results are coming right from the start and updating in real-time. If you are not interested you can wait for the final recognition:</p><pre>recognitionRequest?.shouldReportPartialResults = false</pre><blockquote>If you want only final results (and you don’t care about intermediate results), set this property to false to prevent the system from doing extra work.</blockquote><p><a href="https://developer.apple.com/documentation/speech/sfspeechrecognitionrequest/1649392-shouldreportpartialresults/">shouldReportPartialResults documentation</a></p><p>The next step is that we need to make sure we have everything we need and whether speech recognition is available:</p><pre>guard let speechRecognizer = speechRecognizer,<br>      speechRecognizer.isAvailable,<br>      let recognitionRequest = recognitionRequest,<br>      let inputNode = inputNode<br>else {<br>    assertionFailure(&quot;Unable to start the speech recognition!&quot;)<br>    return<br>}</pre><p>Note: always make sure whether the speech recognizer can recognize speech for the locale:</p><pre>speechRecognizer.isAvailable</pre><p>While testing this on various devices I noticed it wasn’t available on iPhone 12 Mini but was working perfectly fine for iPhone XR, iPhone 12 Pro max, or iPad Air.</p><p>The time has come to provide audio to our SFSpeechAudioBufferRecognitionRequest:</p><pre>let recordingFormat = inputNode.outputFormat(forBus: 0)<br>inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in<br>    recognitionRequest.append(buffer)<br>}</pre><p>This code will allow our application to tap into the live audio and pass the audio buffer to the request for speech recognition.</p><p>Now we need to create a concrete recognition task:</p><pre>recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in<br>    self?.recognizedText = result?.bestTranscription.formattedString<br>    <br>    guard error != nil || result?.isFinal == true else { return }<br>    self?.stop()<br>}</pre><p>Which is provided by the speechRecognizer and is using the recognitionRequest. It&#39;s the final piece of the puzzle that ties everything together. This is the place where we get our results. The result is of type SFSpeechRecognitionResult and we are interested in:</p><ul><li>bestTranscription - Which returns the SFTranscription that is considered the most accurate. The transcription provides formattedString that returns a string we can use. You can see the other results in transcriptions if you like.</li><li>isFinal - This indicates whether transcription is final and finished.</li></ul><p>We set the result string to our:</p><pre>@Published var recognizedText: String?Which in turn provides this value to our application. We will provide the implementation for self?.stop() in a moment. For now, make a mental note that when there is an error or recognition is final it&#39;s a good time to stop the recognition process.</pre><p>Everything is prepared now and wired together. But there is silence. It’s time to bring the sound:</p><pre>audioEngine.prepare()<br><br>do {<br>    try audioEngine.start()<br>    isProcessing = true<br>} catch {<br>    print(&quot;Coudn&#39;t start audio engine!&quot;)<br>    stop()<br>}</pre><p>First, we tell the AVAudioEngine to prepare, and later we start it and indicate that processing is in progress. If it couldn&#39;t start we call stop() to clear the resources.</p><p>We used the stop function a few times now. It&#39;s a good time to create it:</p><pre>func stop() {<br>          recognitionTask?.cancel()<br>    <br>    audioEngine.stop()<br>    <br>    inputNode?.removeTap(onBus: 0)<br>    try? audioSession?.setActive(false)<br>    audioSession = nil<br>    inputNode = nil<br>    <br>    isProcessing = false<br>    <br>    recognitionRequest = nil<br>    recognitionTask = nil<br>    speechRecognizer = nil<br>}</pre><p>The purpose of this function is to clear everything that is not needed anymore. It stops the currently running tasks, stops the audio engine, removes the tap on inputNode, informs that analyzer is not processing, and clears the memory.</p><p>The availability of speech recognition can change and we need to monitor this state and respond accordingly. Luckily there is a delegate for that: SFSpeechRecognizerDelegate. We need to change our SpeechAnalyzer declaration to implement this protocol:</p><pre>final class SpeechAnalyzer: NSObject, ObservableObject, SFSpeechRecognizerDelegate</pre><p>We need to additionally add NSObject because this delegate requires NSObjects to operate.</p><blockquote>Hello Objective-C my old friend.</blockquote><p>Now we let our SFSpeechRecognizer know we want to be its delegate:</p><pre>speechRecognizer.delegate = self</pre><p>Thanks to this when we add:</p><pre>public func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {<br>    if available {<br>        print(&quot;✅ Available&quot;)<br>    } else {<br>        print(&quot;🔴 Unavailable&quot;)<br>        recognizedText = &quot;Text recognition unavailable. Sorry!&quot;<br>        stop()<br>    }<br>}</pre><p>We will immediately start receiving availability information.</p><p>This was a lot to take in. I know. But it’s time to see it in action. I made a simple application with a record button and label for the speech recognition result. This application is using our SpeechAnalyzer.</p><p>It’s time to test. We run the application and the first tap on the button results in a crash:</p><pre>This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app&#39;s Info.plist must contain an NSMicrophoneUsageDescription key with a string value explaining to the user how the app uses this data.</pre><p>We need to provide NSMicrophoneUsageDescription key with a description of why we need the microphone access in the Info.plist.</p><p>Second run, and second tap on the button. This time the alert where the user can allow, or not, microphone access is presented. We tap allow and… the application crashes again:</p><pre>Error Domain=kAFAssistantErrorDomain Code=1700 &quot;User denied access to speech recognition&quot; UserInfo={NSLocalizedDescription=User denied access to speech recognition}</pre><p>The user has to deliberately allow the application to not only use the microphone but also do the speech recognition. We must add NSSpeechRecognitionUsageDescription with description to the Info.plist.</p><p>Note: If you can’t find the Info.plist file in project navigator tap on the top project and look for the Info tab.</p><p>Now when we run the application and tap the record button the alert for speech recognition is shown. Allow the recognition and start talking in English or any other language you created the speech recognizer for. The text will appear above the button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*-8iBu7UPIao3Nx0xuWhkjg.png" /></figure><p>Note: I did this the easy way to not complicate the example code but SFSpeechRecognizer offers methods to implement the authorization properly:</p><pre>open class func authorizationStatus() -&gt; SFSpeechRecognizerAuthorizationStatus<br><br>open class func requestAuthorization(_ handler: @escaping (SFSpeechRecognizerAuthorizationStatus) -&gt; Void)</pre><p>I encourage you to make use of these methods in your application.</p><p>This is the full code:</p><pre>final class SpeechAnalyzer: NSObject, ObservableObject, SFSpeechRecognizerDelegate {<br>    private let audioEngine = AVAudioEngine()<br>    private var inputNode: AVAudioInputNode?<br>    private var speechRecognizer: SFSpeechRecognizer?<br>    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?<br>    private var recognitionTask: SFSpeechRecognitionTask?<br>    private var audioSession: AVAudioSession?<br>    <br>    @Published var recognizedText: String?<br>    @Published var isProcessing: Bool = false<br><br>    func start() {<br>        audioSession = AVAudioSession.sharedInstance()<br>        do {<br>            try audioSession?.setCategory(.record, mode: .measurement, options: .duckOthers)<br>            try audioSession?.setActive(true, options: .notifyOthersOnDeactivation)<br>        } catch {<br>            print(&quot;Couldn&#39;t configure the audio session properly&quot;)<br>        }<br>        <br>        inputNode = audioEngine.inputNode<br>        <br>        speechRecognizer = SFSpeechRecognizer()<br>        print(&quot;Supports on device recognition: \(speechRecognizer?.supportsOnDeviceRecognition == true ? &quot;✅&quot; : &quot;🔴&quot;)&quot;)<br><br>        // Force specified locale<br>        // self.speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: &quot;pl_PL&quot;))<br>        recognitionRequest = SFSpeechAudioBufferRecognitionRequest()<br>        <br>        // Disable partial results<br>        // recognitionRequest?.shouldReportPartialResults = false<br>        <br>        // Enable on-device recognition<br>        // recognitionRequest?.requiresOnDeviceRecognition = true<br><br>        guard let speechRecognizer = speechRecognizer,<br>              speechRecognizer.isAvailable,<br>              let recognitionRequest = recognitionRequest,<br>              let inputNode = inputNode<br>        else {<br>            assertionFailure(&quot;Unable to start the speech recognition!&quot;)<br>            return<br>        }<br>        <br>        speechRecognizer.delegate = self<br>        <br>        let recordingFormat = inputNode.outputFormat(forBus: 0)<br>        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in<br>            recognitionRequest.append(buffer)<br>        }<br><br>        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in<br>            self?.recognizedText = result?.bestTranscription.formattedString<br>            <br>            guard error != nil || result?.isFinal == true else { return }<br>            self?.stop()<br>        }<br><br>        audioEngine.prepare()<br>        <br>        do {<br>            try audioEngine.start()<br>            isProcessing = true<br>        } catch {<br>            print(&quot;Coudn&#39;t start audio engine!&quot;)<br>            stop()<br>        }<br>    }<br>    <br>    func stop() {<br>        recognitionTask?.cancel()<br>        <br>        audioEngine.stop()<br>        <br>        inputNode?.removeTap(onBus: 0)<br>        try? audioSession?.setActive(false)<br>        audioSession = nil<br>        inputNode = nil<br>        <br>        isProcessing = false<br>        <br>        recognitionRequest = nil<br>        recognitionTask = nil<br>        speechRecognizer = nil<br>    }<br>    <br>    public func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {<br>        if available {<br>            print(&quot;✅ Available&quot;)<br>        } else {<br>            print(&quot;🔴 Unavailable&quot;)<br>            recognizedText = &quot;Text recognition unavailable. Sorry!&quot;<br>            stop()<br>        }<br>    }<br>}</pre><p>Additionally, this is the code for the application:</p><pre>struct SpeechRecognitionView: View {<br>    private enum Constans {<br>        static let recognizeButtonSide: CGFloat = 100<br>    }<br>    <br>    @ObservedObject private var speechAnalyzer = SpeechAnalyzer()<br>    var body: some View {<br>        VStack {<br>            Spacer()<br>            Text(speechAnalyzer.recognizedText ?? &quot;Tap to begin&quot;)<br>                .padding()<br>            <br>            Button {<br>                toggleSpeechRecognition()<br>            } label: {<br>                Image(systemName: speechAnalyzer.isProcessing ? &quot;waveform.circle.fill&quot; : &quot;waveform.circle&quot;)<br>                    .resizable()<br>                    .frame(width: Constans.recognizeButtonSide,<br>                           height: Constans.recognizeButtonSide,<br>                           alignment: .center)<br>                    .foregroundColor(speechAnalyzer.isProcessing ? .red : .gray)<br>                    .aspectRatio(contentMode: .fit)<br>            }<br>            .padding()<br>        }<br>    }<br>}<br><br>private extension SpeechRecognitionView {<br>    func toggleSpeechRecognition() {<br>        if speechAnalyzer.isProcessing {<br>            speechAnalyzer.stop()<br>        } else {<br>            speechAnalyzer.start()<br>        }<br>    }<br>}</pre><p>This is all you need to start communicating verbally with your application.</p><p>The quality of this service is at least good enough. I was testing English in both on-device and regular and it was working fine. When I switched to polish, my native language, I was surprised by how accurate the recognition was.</p><p>⚠️ Important Note: The on-device recognition is less accurate but it’s not limited. The speech recognition over the network is limited:</p><blockquote>The current rate limit for the number of SFSpeechRecognitionRequest calls a device can make is 1000 requests per hour. Please note this limit is on the number of requests that a device can make and is not tied to the application making it. This is regardless of the length of audio associated with the request. For a given SFSpeechRecognitionRequest, you are allowed up to one minute of audio per request.</blockquote><p><a href="https://developer.apple.com/library/archive/qa/qa1951/_index.html">The source</a></p><p>In short, each device can make up to 1k requests per hour. Each request can take up to 1 minute in total. This sounds reasonable but… you should be aware of these limitations.</p><p>You can download the demo application <a href="https://github.com/ktustanowski/sounddemo">here</a>.</p><p>Enjoy!</p><p>If you have any feedback, or just want to say hi, you are more than welcome to write me an [e-mail] (<a href="mailto:kamil.tustanowski@gmail.com">mailto:kamil.tustanowski@gmail.com</a>) or <a href="https://twitter.com/intent/tweet?screen_name=tustanowskik&amp;ref_src=twsrc%5Etfw">tweet to @tustanowskik</a></p><p>If you want to be up to date and always be the first to know what I’m working on tap <a href="https://twitter.com/tustanowskik?ref_src=twsrc%5Etfw">follow @tustanowskik on Twitter</a></p><p>Thank you for reading!</p><p>[This was first published on my blog]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=72d31f4f344a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Sound classification using the Sound Analysis framework]]></title>
            <link>https://medium.com/@kamil.tustanowski/sound-classification-using-the-sound-analysis-framework-b1679177e606?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/b1679177e606</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Fri, 21 Jul 2023 18:26:57 GMT</pubDate>
            <atom:updated>2023-07-21T18:26:57.717Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wv8MDmGKW1UbXOgynKHYlg.png" /></figure><p>Sound means to our applications as little as the images. It’s data without any meaning. Users can decode it, enjoy or hate it, understand it. Applications can analyze the sound patterns, reduce pauses, do other transformations and improvements but not understand what the sound file represents.</p><p>It’s time to change it.</p><p>I have written a few articles about the Vision framework in applications and I’m far from over but I love to experiment and look for new ways of user-device communication and interaction. This brings me to today’s topic which is not vision-related but it’s like vision but for sounds.</p><p>Please meet the Sound Analysis framework.</p><pre>import SoundAnalysis</pre><p>Today’s code samples will be provided in a playground. I will add a link to this playground at the end of the article.</p><p>Sound classification requests are more eager to throwing errors than the vision requests therefore we will start with do-catch:</p><pre>do {<br>    /* Our classification code will go here */<br>} catch {<br>    print(&quot;Something went terribly wrong!&quot;)<br>}</pre><p>And we create a SNClassifySoundRequest request:</p><pre>let soundClassifyRequest = try SNClassifySoundRequest(classifierIdentifier: .version1)</pre><p>We initiate SNClassifySoundRequest with the only currently available version - which is 1. But this request is much more flexible:</p><blockquote>Alternatively, you identify a custom set of sounds by providing the sound request with a custom Core ML model</blockquote><p><a href="https://developer.apple.com/documentation/soundanalysis">The Sound Analysis framework documentation</a></p><p>This means we can train our own machine learning models with custom sounds we want to classify. Let me know whether you would like to know more because this is material for the whole article.</p><p>Now let’s get back to our built-in, revision1, request and make sure it can classify the sounds we are interested in. This code will print all the supported identifiers:</p><pre>soundClassifyRequest.knownClassifications<br>    .enumerated()<br>    .forEach { index, identifier in print(&quot;\(index). \(identifier)&quot;) }</pre><pre>0. speech<br>1. shout<br>2. yell<br>3. battle_cry<br>4. children_shouting<br>5. screaming<br>6. whispering<br>7. laughter<br>8. baby_laughter<br>9. giggling<br>10. snicker<br>11. belly_laugh<br>12. chuckle_chortle<br>13. crying_sobbing<br>14. baby_crying<br>15. sigh<br>16. singing<br>17. choir_singing<br>18. yodeling<br>19. rapping<br>20. humming<br>21. whistling<br>22. breathing<br>23. snoring<br>24. gasp<br>25. cough<br>26. sneeze<br>27. nose_blowing<br>28. person_running<br>29. person_shuffling<br>30. person_walking<br>[...]</pre><p>The full list contains more than 300 identifiers. You can check it <a href="https://gist.github.com/ktustanowski/ccb66f95967b7f0541d4a7cf948db6bf">here</a>.</p><p>Next, let’s add a sound we will analyze to the playground Resources. Please check <a href="https://medium.com/@kamil.tustanowski/working-with-the-vision-framework-in-the-playgrounds-b036189e1dae">Working with the Vision framework in the playgrounds</a> for the instruction on how to add resources to the playground.</p><p>I used <a href="https://www.zapsplat.com/music/approaching-thunderstorm-with-light-rain/">Approaching thunderstorm with light rain</a> from <a href="https://www.zapsplat.com">zapsplat</a>.</p><p>With the file in place, we need to construct the URL which we can then pass to the SNAudioFileAnalyzer:</p><pre>let fileUrl = Bundle.main.url(forResource: &quot;storm&quot;, withExtension: &quot;mp3&quot;)</pre><p>The URL is optional and will be nil if the file is not located. We need this file for processing and can&#39;t do anything without it. We are in the do-catch block therefore we should make use of it. First, we create an error:</p><pre>enum FileError: Error {<br>    case notFound<br>}</pre><p>And then we throw it if the file is not there:</p><pre>guard let filePath = Bundle.main.path(forResource: &quot;storm&quot;, ofType: &quot;mp3&quot;) else { throw FileError.notFound }</pre><p>Let’s move this above the line where we create a request. The is no need for a request if we don’t have a file.</p><p>The next step is to create the SNAudioFileAnalyzer. This is where we use the file URL:</p><pre>let audioFileAnalyzer = try SNAudioFileAnalyzer(url: fileUrl)</pre><p>Analyzer needs a file to analyze. To start the analysis we need to add a request we want the analyzer to use to process the file:</p><pre>open func add(_ request: SNRequest, withObserver observer: SNResultsObserving) throws</pre><p>The problem is that it requires an observer which must conform to SNResultsObserving protocol. This protocol consists of three methods:</p><pre>public protocol SNResultsObserving : NSObjectProtocol {<br>    func request(_ request: SNRequest, didProduce result: SNResult)<br>    optional func request(_ request: SNRequest, didFailWithError error: Error)<br>    optional func requestDidComplete(_ request: SNRequest)<br>}</pre><p>You can find more info in <a href="https://developer.apple.com/documentation/soundanalysis/snresultsobserving">the documentation</a>.</p><p>This is how the analyzer can communicate the status of the analysis. We need to make a small class conforming to this protocol and NSObject due to the NSObjectProtocol requirement. Which can trigger a wave of memories of Objective-C days in those who remember those times.</p><p>Let’s conform to the protocol. DidComplete and DidFail are self-explanatory:</p><pre>final class AudioAnalysisObserver: NSObject, SNResultsObserving {<br>    func requestDidComplete(_ request: SNRequest) {<br>        print(&quot;Processing completed!&quot;)<br>    }<br>    <br>    func request(_ request: SNRequest, didFailWithError error: Error) {<br>        print(&quot;Failed with \(error)&quot;)<br>    }<br>}</pre><p>But there is more to do in the function that provides the SNResults:</p><pre>func request(_ request: SNRequest, didProduce result: SNResult)</pre><p>The first problem we need to solve is casting the result to the concrete result type. SNResult is an empty protocol. The documentation is <a href="https://developer.apple.com/documentation/soundanalysis/snresult">here</a>.</p><p>The SNClassificationResult is what we need:</p><pre>guard let result = result as? SNClassificationResult else  { return }</pre><p>SNClassificationResult contains:</p><ul><li>classifications - An array of SNClassification which contains information about the identifiers and the confidence (as usual within [0, 1.0] range).</li><li>timeRange - CMTimeRange informing about the time range of the analysis.</li></ul><p>Classifications are sorted therefore if we get the first one we will have the best match:</p><pre>guard let result = result as? SNClassificationResult,<br>      let bestClassification = result.classifications.first else  { return }</pre><p>Next, we get time information from timeRange:</p><pre>let timeStart = result.timeRange.start.seconds</pre><p>The last piece is putting this together into a meaningful message and providing it to the user:</p><pre>print(&quot;Found \(bestClassification.identifier) at \(Int((bestClassification.confidence) * 100))% at \(timeStart)s&quot;)</pre><p>This is our observer:</p><pre>final class AudioAnalysisObserver: NSObject, SNResultsObserving {<br>    func requestDidComplete(_ request: SNRequest) {<br>        print(&quot;Processing completed!&quot;)<br>    }<br>    <br>    func request(_ request: SNRequest, didProduce result: SNResult) {<br>        guard let result = result as? SNClassificationResult,<br>              let bestClassification = result.classifications.first else  { return }<br>        let timeStart = result.timeRange.start.seconds<br>        <br>        print(&quot;Found \(bestClassification.identifier) at \(Int((bestClassification.confidence) * 100))% at \(timeStart)s&quot;)<br>    }<br>    <br>    func request(_ request: SNRequest, didFailWithError error: Error) {<br>        print(&quot;Failed with \(error)&quot;)<br>    }<br>}</pre><p>Finally, we can add our request to the analyzer:</p><pre>let resultsObserver = AudioAnalysisObserver()<br>try audioFileAnalyzer.add(soundClassifyRequest, withObserver: resultsObserver)</pre><p>The last part is to start the analysis:</p><pre>audioFileAnalyzer.analyze()</pre><p>Please remember that analyze() is executing synchronously therefore you shouldn&#39;t call it from the main thread or it will block the application. More info in <a href="https://developer.apple.com/documentation/soundanalysis/snaudiofileanalyzer/3182399-analyze">the documentation</a></p><p>I’m working in the playground so synchronous is fine. But in the application, it is a no-go.</p><p>Additionally:</p><blockquote>You can run the same sound analysis request on multiple file analyzers, and each analyzer can process multiple requests.</blockquote><p>More in <a href="https://developer.apple.com/documentation/soundanalysis/snaudiofileanalyzer">the documentation</a>.</p><p>But I’m talking about threads and other things and we are excited about the results. You can find the audio file <a href="https://www.zapsplat.com/music/approaching-thunderstorm-with-light-rain/">here</a> and here are the results we received:</p><pre>Found water at 61% at 0.0s<br>Found water at 55% at 1.5s<br>Found water at 59% at 3.0s<br>Found water at 50% at 4.5s<br>Found rain at 55% at 6.0s<br>Found water at 59% at 7.5s<br>Found water at 55% at 9.0s<br>Found water at 50% at 10.5s<br>Found water at 56% at 12.0s<br>Found water at 53% at 13.5s<br>Found water at 53% at 15.0s<br>Found water at 61% at 16.5s<br>Found water at 53% at 18.0s<br>Found water at 48% at 19.5s<br>Found rain at 71% at 21.0s<br>Found thunderstorm at 71% at 22.5s<br>Found water at 59% at 24.0s<br>Found rain at 61% at 25.5s<br>Found rain at 63% at 27.0s<br>Found water at 62% at 28.5s<br>Found water at 65% at 30.0s<br>Found water at 57% at 31.5s<br>Found water at 58% at 33.0s<br>Found water at 49% at 34.5s<br>Found water at 51% at 36.0s<br>Found water at 57% at 37.5s<br>Found rain at 54% at 39.0s<br>Found thunder at 83% at 40.5s<br>Found thunderstorm at 67% at 42.0s<br>Found thunder at 71% at 43.5s<br>Found thunder at 80% at 45.0s<br>Found thunder at 78% at 46.5s<br>Found thunder at 93% at 48.0s<br>Found thunderstorm at 81% at 49.5s<br>Found rain at 65% at 51.0s<br>Found rain at 61% at 52.5s<br>Found water at 57% at 54.0s<br>Found water at 49% at 55.5s<br>Found water at 58% at 57.0s<br>Found water at 62% at 58.5s<br>Found water at 56% at 60.0s<br>Found water at 59% at 61.5s<br>Found water at 58% at 63.0s<br>Found water at 53% at 64.5s<br>Found water at 59% at 66.0s<br>Found water at 54% at 67.5s<br>Processing completed!</pre><p>Moments ago we didn’t know anything about the file and now we can make an educated guess that this audio file contains the recording of a storm. This is a huge leap forward which opens a lot of new possibilities. Generating the tags for the files allowing users to filter them is just the beginning.</p><p>You can find the code below. Remember about adding a sound file for the analysis:</p><pre>import UIKit<br>import SoundAnalysis<br><br>/*<br> <br> Uncomment if you want to see list of identifiers<br> <br>(try? SNClassifySoundRequest(classifierIdentifier: SNClassifierIdentifier.version1))?.knownClassifications<br>    .enumerated()<br>    .forEach { index, identifier in print(&quot;\(index). \(identifier)&quot;) }<br>*/<br><br>final class AudioAnalysisObserver: NSObject, SNResultsObserving {<br>    func requestDidComplete(_ request: SNRequest) {<br>        print(&quot;Processing completed!&quot;)<br>    }<br>    <br>    func request(_ request: SNRequest, didProduce result: SNResult) {<br>        guard let result = result as? SNClassificationResult,<br>              let bestClassification = result.classifications.first else  { return }<br>        let timeStart = result.timeRange.start.seconds<br>        <br>        print(&quot;Found \(bestClassification.identifier) at \(Int((bestClassification.confidence) * 100))% at \(timeStart)s&quot;)<br>    }<br>    <br>    func request(_ request: SNRequest, didFailWithError error: Error) {<br>        print(&quot;Failed with \(error)&quot;)<br>    }<br>}<br><br>enum FileError: Error {<br>    case notFound<br>}<br><br>do {<br>    guard let fileUrl = Bundle.main.url(forResource: &quot;storm&quot;, withExtension: &quot;mp3&quot;) else { throw FileError.notFound }<br>    let soundClassifyRequest = try SNClassifySoundRequest(classifierIdentifier: SNClassifierIdentifier.version1)<br><br>    let audioFileAnalyzer = try SNAudioFileAnalyzer(url: fileUrl)<br>    let resultsObserver = AudioAnalysisObserver()<br>    try audioFileAnalyzer.add(soundClassifyRequest, withObserver: resultsObserver)<br>    <br>    audioFileAnalyzer.analyze()<br>} catch {<br>    print(&quot;Something went terribly wrong!&quot;)<br>}</pre><p>You can find the code <a href="https://github.com/ktustanowski/visiondemo/blob/main/VisionDemoApp/TheSoundAnalysis.playground/Pages/SoundAnalysis.xcplaygroundpage/Contents.swift">here</a>. I added it as a part of my <a href="https://github.com/ktustanowski/visiondemo">vision demo application</a> since it has the potential to make the analysis of real-time video and sound even better.</p><p>⚠️ If you are using <strong>M1</strong> and run <strong>Xcode</strong> using <strong>Rosetta</strong> playgrounds won’t work: <a href="https://twitter.com/kwcodes/status/1451275902772461571">https://twitter.com/kwcodes/status/1451275902772461571</a></p><p>⚠️ If you are using M1 and run Xcode using Rosetta playgrounds won’t work. Check a <a href="https://twitter.com/kwcodes/status/1451275902772461571?s=20">Krzysztof’s tweet</a> to know more. Have fun!</p><p>If you have any feedback, or just want to say hi, you are more than welcome to write me an [e-mail] (<a href="mailto:cornerbit@protonmail.com">mailto:kamil.tustanowski@gmail.com</a>) or <a href="https://twitter.com/intent/tweet?screen_name=tustanowskik&amp;ref_src=twsrc%5Etfw">tweet to @tustanowskik</a></p><p>If you want to be up to date and always be the first to know what I’m working on tap <a href="https://twitter.com/tustanowskik?ref_src=twsrc%5Etfw">follow @tustanowskik on Twitter</a></p><p>Thank you for reading!</p><p>[This was first published on my blog]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b1679177e606" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Working with the API calls in the playgrounds]]></title>
            <link>https://medium.com/@kamil.tustanowski/working-with-the-api-calls-in-the-playgrounds-25323f5ea7c9?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/25323f5ea7c9</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Fri, 21 Jul 2023 18:02:16 GMT</pubDate>
            <atom:updated>2023-07-21T18:02:16.961Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GMkgZAQkEz8aXPLX1E0ymw.png" /></figure><p>Making the network requests is the true core functionality of many applications. Logging in, getting the data, making changes, and many, many more.</p><p>Implementing these calls would be a pain if not the contracts we have that determine how the network calls works, what they require and what they provide. It’s great that they always work and they don’t have any bugs. The only thing we need to do is to implement the handling on the application side and we are done.</p><p>In an ideal world that is.</p><p>In the real world, it’s located somewhere between this ideal case and:</p><blockquote>Oh man, we have no documentation and nothing is working</blockquote><p>Handling API calls tend to be time-consuming and annoying. In addition, the only way to test the code is to run the application. One of my personal workarounds was to write unit tests without mocks during the development. When I was running them they were contacting the API and I was able to see what is going on without running the application. In the end, I was either mocking them or discarding/disabling. It wasn’t ideal but much better than the regular approach.</p><p>Workarounds are not needed any more thanks to the playgrounds.</p><p>Let’s imagine a situation where we need to implement an API call that will return a list of items. We will use <a href="https://api.publicapis.org/entries">https://api.publicapis.org/entries</a> as an example. To make it harder and realistic at the same time we have the URL and no documentation.</p><p>First, we need to create a playground. In Xcode click on File &gt; New &gt; Playground or use shift + option + command + N. Then select Blank playground from the list.</p><p>Now we need to add a small tweak to make it asynchronous-execution friendly:</p><pre>import PlaygroundSupport<br>PlaygroundPage.current.needsIndefiniteExecution = true</pre><blockquote>By default, all top-level code is executed, and then execution is terminated. When working with asynchronous code, enable indefinite execution to allow execution to continue after the end of the playground’s top-level code is reached. This, in turn, gives threads and callbacks time to execute.</blockquote><p><a href="https://developer.apple.com/documentation/playgroundsupport/playgroundpage/1964501-needsindefiniteexecution">The Documentation</a></p><p>Then we need to make the call to see what is there. We could use the browser but where is fun in that?</p><pre>let apiListRequestUrl = URL(string: &quot;https://api.publicapis.org/entries&quot;)!<br>let apiListTask = URLSession.shared.dataTask(with: apiListRequestUrl) { data, response, error in<br>    guard let data = data else { return }<br>    let jsonString = String(data: data, encoding: .utf8)!<br>    print(jsonString)<br>}</pre><p>The code above makes the request to a specified URL and provides data or error in return.</p><p>When we run the playground we see:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*irKy64Vn6bqx4bAQnHvGrg.png" /></figure><p>What is great is that all the features we used in <a href="https://medium.com/@kamil.tustanowski/working-with-the-vision-framework-in-the-playgrounds-b036189e1dae">Working with the Vision framework in the playgrounds</a> are there.</p><p>This means we can tap on the eye button:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*laTy7u19n6ufbhJFCgoP2A.png" /></figure><p>And we can do the same with the rectangle / embed button:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*mAYBvZIYrYXvzB6JO-opDA.png" /></figure><p>Regular prints are working as well:</p><pre>{&quot;count&quot;:1185,&quot;entries&quot;:[{&quot;API&quot;:&quot;Axolotl&quot;,&quot;Description&quot;:&quot;Collection of axolotl pictures and facts&quot;,&quot;Auth&quot;:&quot;&quot;,&quot;HTTPS&quot;:true,&quot;Cors&quot;:&quot;yes&quot;,&quot;Link&quot;:&quot;https://theaxolotlapi.netlify.app/&quot;,&quot;Category&quot;:&quot;Animals&quot;},{&quot;API&quot;:&quot;Cat Facts&quot;,&quot;Description&quot;:&quot;Daily cat facts&quot;,&quot;Auth&quot;:&quot;&quot;,&quot;HTTPS&quot;:true,&quot;Cors&quot;:&quot;no&quot;,&quot;Link&quot;:&quot;https://alexwohlbruck.github.io/cat-facts/&quot;,&quot;Category&quot;:&quot;Animals&quot;},{&quot;API&quot;:&quot;Cataas&quot;,&quot;Description&quot;:&quot;Cat as a service (cats pictures and gifs)&quot;,&quot;Auth&quot;:&quot;&quot;,&quot;HTTPS&quot;:true,&quot;Cors&quot;:&quot;no&quot;,&quot;Link&quot;:&quot;https://cataas.com/&quot;,&quot;Category&quot;:&quot;Animals&quot;},{&quot;API&quot;:&quot;catAPI&quot;,&quot;Description&quot;:&quot;Random pictures of cats&quot;,&quot;Auth&quot;:&quot;&quot;,&quot;HTTPS&quot;:true,&quot;Cors&quot;:&quot;yes&quot;,&quot;Link&quot;:&quot;https://thatcopy.pw/catapi&quot;,&quot;Category&quot;:&quot;Animals&quot;},{&quot;API&quot;:&quot;Cats&quot;,&quot;Description&quot;:&quot;Pictures of cats from Tumblr&quot;,&quot;Auth&quot;:&quot;apiKey&quot;,&quot;HTTPS&quot;:true,&quot;Cors&quot;:&quot;no&quot;,&quot;Link&quot;:&quot;https://docs.thecatapi.com/&quot;,&quot;Category&quot;:&quot;Animals&quot;},{&quot;API&quot;:&quot;Dog Facts&quot;,&quot;Description&quot;:&quot;Random dog facts&quot;,&quot;Auth&quot;:&quot;&quot;,&quot;HTTPS&quot;:true,&quot;Cors&quot;:&quot;yes&quot;,&quot;Link&quot;:&quot;https://dukengn.github.io/Dog-facts-API/&quot;,&quot;Category&quot;:&quot;Animals&quot;},\<br>[...]</pre><p>Based on the gathered info we can make a few assumptions. The results are in a container that has two main variable count and entries:</p><pre>{&quot;count&quot;:1185,<br> &quot;entries&quot;:[\* Entry items *\]</pre><p>We can start implementing the simple structure:</p><pre>struct ApiListEnvelope: Codable {<br>    let count: Int<br>}</pre><p>And add a code for decoding instead of print:</p><pre>let decoded = try! JSONDecoder().decode(ApiListEnvelope.self, from: data)</pre><p>What happens is:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*NvKTjl4dS6Fm3v_36BQB_Q.png" /></figure><p>This confirms that the count is properly decoded but this is not the end. We need an Entry:</p><pre>{&quot;API&quot;:&quot;Axolotl&quot;,&quot;Description&quot;:&quot;Collection of axolotl pictures and facts&quot;,&quot;Auth&quot;:&quot;&quot;,&quot;HTTPS&quot;:true,&quot;Cors&quot;:&quot;yes&quot;,&quot;Link&quot;:&quot;https://theaxolotlapi.netlify.app/&quot;,&quot;Category&quot;:&quot;Animals&quot;,</pre><pre>struct Entry: Codable {<br>    let api: String<br>    let description: String<br>    let link: String<br>    let category: String<br>}</pre><p>To make the envelope complete:</p><pre>struct ApiListEnvelope: Codable {<br>    let count: Int<br>    let entries: [Entry]<br>}</pre><p>But when we run the playground we see there is a problem:</p><pre>__lldb_expr_34/TheAPIPlayground.playground:30: Fatal error: &#39;try!&#39; expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: &quot;api&quot;, intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: &quot;entries&quot;, intValue: nil), _JSONKey(stringValue: &quot;Index 0&quot;, intValue: 0)], debugDescription: &quot;No value associated with key CodingKeys(stringValue: \&quot;api\&quot;, intValue: nil) (\&quot;api\&quot;).&quot;, underlyingError: nil))</pre><p>The names in JSON doesn&#39;t match which means we need to provide CodingKeys for entry:</p><pre>enum CodingKeys: String, CodingKey {<br>    case api = &quot;API&quot;<br>    case description = &quot;Description&quot;<br>    case link = &quot;Link&quot;<br>    case category = &quot;Category&quot;<br>}</pre><p>Now our response is decoded properly. Note: we decode only the information we intend to use:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*TllBQalKt2aBKNv7ZBm-GQ.png" /></figure><p>This is it. We implemented the loading of the list of items we want to display in the application.</p><p>We investigated the structure of the call and implemented decoding step-by-step fixing issues as soon as they arise. Lightning-fast feedback included.</p><p>We didn’t need to run the application to achieve this. We didn’t have to tackle all the issues at once.</p><p>This is the full code:</p><pre>import UIKit<br>import PlaygroundSupport<br><br>struct Entry: Codable {<br>    let api: String<br>    let description: String<br>    let link: String<br>    let category: String<br>    <br>    enum CodingKeys: String, CodingKey {<br>        case api = &quot;API&quot;<br>        case description = &quot;Description&quot;<br>        case link = &quot;Link&quot;<br>        case category = &quot;Category&quot;<br>    }<br>}<br><br>struct ApiListEnvelope: Codable {<br>    let count: Int<br>    let entries: [Entry]<br>}<br><br>let apiListRequestUrl = URL(string: &quot;https://api.publicapis.org/entries&quot;)!<br>let apiListTask = URLSession.shared.dataTask(with: apiListRequestUrl) { data, response, error in<br>    guard let data = data else { return }<br>    let decoded = try! JSONDecoder().decode(ApiListEnvelope.self, from: data)<br>}<br><br>apiListTask.resume()<br><br>PlaygroundPage.current.needsIndefiniteExecution = true</pre><p>⚠️ If you are using M1 and run Xcode using Rosetta playgrounds won’t work. Check a <a href="https://twitter.com/kwcodes/status/1451275902772461571?s=20">Krzysztof’s tweet</a> to know more.</p><p>If you have any feedback, or just want to say hi, you are more than welcome to write me an [e-mail] (<a href="mailto:cornerbit@protonmail.com">mailto:k</a>amil.tustanowski@gmil.com) or <a href="https://twitter.com/intent/tweet?screen_name=tustanowskik&amp;ref_src=twsrc%5Etfw">tweet to @tustanowskik</a></p><p>If you want to be up to date and always be first to know what I’m working on tap <a href="https://twitter.com/tustanowskik?ref_src=twsrc%5Etfw">follow @tustanowskik on Twitter</a></p><p>Thank you for reading!</p><p>[This was first published on my blog]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=25323f5ea7c9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Working with the Vision framework in the playgrounds]]></title>
            <link>https://medium.com/@kamil.tustanowski/working-with-the-vision-framework-in-the-playgrounds-b036189e1dae?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/b036189e1dae</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Fri, 21 Jul 2023 14:09:29 GMT</pubDate>
            <atom:updated>2023-07-21T14:09:29.649Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7fFN9nPHGnge5jKGp56kPw.png" /></figure><p>When playgrounds were first introduced I didn’t grasp the idea at first. Time has passed and I finally started to treat playgrounds like… playgrounds. Place where I could quickly test my ideas, make prototypes, and more.</p><p>What is great about playgrounds is the constant feedback they provide. In a few seconds, you know whether the changes you made are working properly or not. This is a game-changer for anyone used to compiling and running the application to check how the new code behaves.</p><p>This time I won’t introduce any new requests. Instead, I will show you how you can work with the Vision framework in playgrounds. Where you can verify whether the framework suits your needs or experiment with the requests.</p><p>First, we need to create a playground. In Xcode click on File &gt; New &gt; Playground or use shift + option + command + N. Then select Blank playground from the list.</p><p>With playground in place we need to add imports first:</p><pre>import UIKit<br>import Vision</pre><p>Then it’s time for the request. I will reuse the code from <a href="https://medium.com/@kamil.tustanowski/image-classification-using-the-vision-framework-3cac0ab6f399">Image classification using the Vision framework</a> from last week:</p><pre>func process(_ image: UIImage) {<br>    guard let cgImage = image.cgImage else { return }<br>    let request = VNClassifyImageRequest()<br>    <br>    let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                               orientation: .init(image.imageOrientation),<br>                                               options: [:])<br>    <br>    DispatchQueue.global(qos: .userInitiated).async {<br>        do {<br>            try requestHandler.perform([request])<br>        } catch {<br>            print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>        }<br>        <br>        guard let results = request.results as? [VNClassificationObservation] else { return }<br>        <br>        results<br>            .filter { $0.confidence &gt; 0.7 }<br>            .forEach { print(&quot;\($0.identifier) - \((Int($0.confidence * 100)))%&quot;) }<br>    }<br>}</pre><p>And an extension:</p><pre>extension CGImagePropertyOrientation {<br>    init(_ uiOrientation: UIImage.Orientation) {<br>        switch uiOrientation {<br>        case .up: self = .up<br>        case .upMirrored: self = .upMirrored<br>        case .down: self = .down<br>        case .downMirrored: self = .downMirrored<br>        case .left: self = .left<br>        case .leftMirrored: self = .leftMirrored<br>        case .right: self = .right<br>        case .rightMirrored: self = .rightMirrored<br>        @unknown default:<br>            self = .up<br>        }<br>    }<br>}</pre><p>Which is described in <a href="https://medium.com/@kamil.tustanowski/detecting-body-pose-using-vision-framework-caba5435796a">Detecting body pose using Vision framework</a>. If you want to know more about the code please refer to the mentioned articles.</p><p>We have the processing function ready. Now we need images. The playground has a special place called Resources for providing images and any other assets:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*KVpxgp3VfeJMyWKNKnlXVg.png" /></figure><p>We drag and drop the images:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*tom1IH3dvxfbEmTvgPkjFA.png" /></figure><p>Instantiate cupcake image in the playground:</p><pre>let cupcake = UIImage(named: &quot;cupcake.jpg&quot;)!</pre><p>Add code for processing and start the playground by tapping the play icon:</p><pre>process(cupcake)</pre><p>The result is printed to the console:</p><pre>food - 86%<br>baked_goods - 78%<br>dessert - 71%<br>cake - 71%<br>cupcake - 71%</pre><p>And that’s it! In a couple of minutes, we created functional proof-of-concept code. No application is needed. We could focus on the main task alone.</p><p>We could end here but there is a few cool features playground has to offer and it would be a shame to not use them.</p><p>What we can improve is how we present inputs and outputs. If we look closely at the line where we created the image:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*XGDXdFUXBaOJJAZ_GHNbVw.png" /></figure><p>We will see two icons on the right. If we tap the eye we get the large preview:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*RQpxd8tXJ3q9xwFix3tcxg.png" /></figure><p>The other icon displays the current value inside the playground:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*EsghXxRbZJoYGZG6xCmVig.png" /></figure><p>Now let’s present the processing results better. First, we need to make a small change to our process function. We drop the dispatch queue code and make it work on the main thread and additionally return the result as an array of strings:</p><pre>func process(_ image: UIImage) -&gt; [String] {<br>    guard let cgImage = image.cgImage else { return [] }<br>    let request = VNClassifyImageRequest()<br>    <br>    let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                               orientation: .init(image.imageOrientation),<br>                                               options: [:])<br>    <br>    do {<br>        try requestHandler.perform([request])<br>    } catch {<br>        print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>    }<br>    <br>    guard let results = request.results as? [VNClassificationObservation] else { return [] }<br>    <br>    return results<br>        .filter { $0.confidence &gt; 0.7 }<br>        .map { &quot;\($0.identifier) - \((Int($0.confidence * 100)))%&quot; }<br>}</pre><p>It’s the easiest way to get the results right where we want them:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*OFgHBIP4qowRT2TxUyYChg.png" /></figure><p><strong>Note:</strong> <em>As you can see the </em><em>rectangle button can show more than the images.</em></p><p>With this approach, we have a clear overview of our inputs and outputs to check and we don’t have to constrain ourselves:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*qYVum26TxS9t4VJ9oEJ7Vw.png" /></figure><p>We can do the same for the saliency requests described in <a href="https://medium.com/@kamil.tustanowski/saliency-detection-using-the-vision-framework-d53a38e4ccaa">Saliency detection using the Vision framework</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*epDh_sQEUbay2qsfR8qHJw.png" /></figure><p>Animals requests from <a href="https://medium.com/@kamil.tustanowski/animals-detection-using-the-vision-framework-1a755fbc639f">Animals detection using the Vision framework</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*AHlwspsXJADTzNVTC8nmQg.png" /></figure><p>Or any other.</p><p>Playgrounds are a great tool to have.</p><p>This is the whole code needed:</p><pre>import UIKit<br>import Vision<br><br>func process(_ image: UIImage) -&gt; [String] {<br>    guard let cgImage = image.cgImage else { return [] }<br>    let request = VNClassifyImageRequest()<br>    <br>    let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                               orientation: .init(image.imageOrientation),<br>                                               options: [:])<br>    <br>    do {<br>        try requestHandler.perform([request])<br>    } catch {<br>        print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>    }<br>    <br>    guard let results = request.results as? [VNClassificationObservation] else { return [] }<br>    <br>    return results<br>        .filter { $0.confidence &gt; 0.7 }<br>        .map { &quot;\($0.identifier) - \((Int($0.confidence * 100)))%&quot; }<br>}<br><br>extension CGImagePropertyOrientation {<br>    init(_ uiOrientation: UIImage.Orientation) {<br>        switch uiOrientation {<br>        case .up: self = .up<br>        case .upMirrored: self = .upMirrored<br>        case .down: self = .down<br>        case .downMirrored: self = .downMirrored<br>        case .left: self = .left<br>        case .leftMirrored: self = .leftMirrored<br>        case .right: self = .right<br>        case .rightMirrored: self = .rightMirrored<br>        @unknown default:<br>            self = .up<br>        }<br>    }<br>}<br><br>let cupcake = UIImage(named: &quot;cupcake.jpg&quot;)!<br>process(cupcake)<br>let plane = UIImage(named: &quot;plane.jpg&quot;)!<br>process(plane)<br>let lake = UIImage(named: &quot;lake.jpg&quot;)!<br>process(lake)</pre><p>⚠️ Remember to add your assets to Resources and provide proper images names in UIImage(named: &quot;cupcake.jpg&quot;)!.</p><p>If you want to play with Vision and see it for yourself you can check the latest version of my vision demo application <a href="https://github.com/ktustanowski/visiondemo">here</a> where you will find TheVision.playground with three pages for you to experiment with:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*Z9cHD4EsL03RTzjIjX6FXw.png" /></figure><p>⚠️ Playgrounds are dependent on the project so you need to build the project first before using them.</p><p>⚠️ I was working with these playgrounds on Xcode 13.</p><p>⚠️ If you are using M1 and run Xcode using Rosetta playgrounds won’t work. Check a <a href="https://twitter.com/kwcodes/status/1451275902772461571?s=20">Krzysztof’s tweet</a> to know more. Have fun! ⚠️ There are requests that require device to operate and won’t work properly in the playgrounds and simulators.</p><p>If you have any feedback, or just want to say hi, you are more than welcome to write me an [e-mail] (<a href="mailto:cornerbit@protonmail.com">mailto:kamil.tustanowski@gmail.com</a>) or <a href="https://twitter.com/intent/tweet?screen_name=tustanowskik&amp;ref_src=twsrc%5Etfw">tweet to @tustanowskik</a></p><p>If you want to be up to date and always be first to know what I’m working on tap <a href="https://twitter.com/tustanowskik?ref_src=twsrc%5Etfw">follow @tustanowskik on Twitter</a></p><p>Thank you for reading!</p><p>P.S. Another way for getting quick feedback is using unit tests and snapshot tests. But this is a topic for another article. Let me know if you like to know more!</p><p>Photos I used in this article are made by: <a href="https://unsplash.com/@ibrahimboran">Ibrahim Boran</a>, <a href="https://unsplash.com/@nbb_photos">Lacie Slezak</a>, <a href="https://unsplash.com/@u2b_photos">Ursa Bavcar</a>, and <a href="https://unsplash.com/@theluckyneko">The Lucky Neko</a>.</p><p>[This was first published on my blog]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b036189e1dae" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Image classification using the Vision framework]]></title>
            <link>https://medium.com/@kamil.tustanowski/image-classification-using-the-vision-framework-3cac0ab6f399?source=rss-2c0314596f19------2</link>
            <guid isPermaLink="false">https://medium.com/p/3cac0ab6f399</guid>
            <dc:creator><![CDATA[Kamil Tustanowski]]></dc:creator>
            <pubDate>Thu, 20 Jul 2023 19:48:05 GMT</pubDate>
            <atom:updated>2023-07-20T19:48:05.757Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6HSExf3EkFQ1toULkGx7Jw.png" /></figure><p>For the last couple of weeks, I was presenting awesome features of the Vision framework and simple ways of presenting the results. They were, I hope, visually appealing and interesting.</p><p>In today’s article, I won’t be presenting anything in the image. No overlays, rectangles, lines, texts. I know this might be disappointing but please don’t give up on reading.</p><p>The request I want to talk about today might seem modest compared to others previously presented. It returns an array of strings and nothing more.</p><p>But the strings in the list are describing the contents of an image.</p><p>It’s fast, it’s simple and it can change the way you handle images in your application, how you classify them, what you can allow users to do and how.</p><p>This is the same image I used in the <a href="https://medium.com/@kamil.tustanowski/saliency-detection-using-the-vision-framework-d53a38e4ccaa">saliency detection article</a>. Last week we were able to detect where users will focus their attention. This week I will show you how to try to understand what <strong>is</strong> on the image.</p><p>Keep reading if you find getting from here:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*kWAyNbaRIxYmjvlKvpyybg.jpeg" /></figure><p>To here interesting:</p><pre>outdoor - 99%<br>land - 98%<br>liquid - 98%<br>water - 98%<br>water_body - 98%<br>waterways - 98%<br>waterfall - 98%<br>sky - 82%<br>cloudy - 82%</pre><p>VNClassifyImageRequest is the request used to classify images. Unlike other requests, I presented this one doesn&#39;t provide any points or rectangles. It generates an array of identifiers with associated levels of confidence.</p><p>Let’s create the request:</p><pre>let request = VNClassifyImageRequest()</pre><p>And check what can be identified:</p><pre>// iOS 15 and up<br>let supportedIdentifiers = try? request.supportedIdentifiers()</pre><p>Note: This is for iOS 15 and above.</p><pre>// Below iOS 15<br>let supportedIdentifiers = try? VNClassifyImageRequest.knownClassifications(forRevision: VNClassifyImageRequestRevision1)</pre><p>You can find a complete list of <strong>1303</strong> (VNClassifyImageRequestRevision1) supported identifiers <a href="https://gist.github.com/ktustanowski/56c0d7541813868fed4aceb60ab5d149">here</a>.</p><p>The next step is running the request on a selected image:</p><pre>let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                           orientation: .init(image.imageOrientation),<br>                                           options: [:])<br><br>DispatchQueue.global(qos: .userInitiated).async {<br>    do {<br>        try requestHandler.perform([request])<br>    } catch {<br>        print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>    }<br>}</pre><p>Please check my <a href="https://medium.com/@kamil.tustanowski/detecting-body-pose-using-vision-framework-caba5435796a">Detecting body pose using Vision framework</a> article if you need more information on running the requests.</p><p>And getting the results:</p><pre>guard let results = request.results as? [VNClassificationObservation] else { return }</pre><p>Note: In iOS 15 and above we don’t need to map the results anymore. They come with the correct type and not arrays of Any.</p><p>In VNClassificationObservation we are interested in:</p><ul><li>The identifier which holds information about the contents of the image.</li><li>The confidence which contains values from 0.0 to 1.0 describing whether Vision is certain of this observation or not.</li></ul><p>Let’s see what the Vision has to say about our image using a print function:</p><pre>results<br>    .forEach { print(&quot;\($0.identifier) - \((Int($0.confidence * 100)))%&quot;) }</pre><p>The result:</p><pre>outdoor - 99%<br>land - 98%<br>liquid - 98%<br>water - 98%<br>water_body - 98%<br>waterways - 98%<br>waterfall - 98%<br>sky - 82%<br>cloudy - 82%<br>structure - 28%<br>rocks - 28%<br>hill - 16%<br>rainbow - 13%<br>mountain - 12%<br>river - 7%<br>cliff - 7%<br>grass - 7%<br>blue_sky - 4%<br>canyon - 4%<br>creek - 4%<br>sunset_sunrise - 2%<br>plant - 2%<br>moss - 2%<br>shrub - 1%<br>sun - 1%<br>foliage - 0%<br>painting - 0%<br>bridge - 0%<br>forest - 0%<br>/* A lot of other identifiers */<br>xylophone - 0%<br>yacht - 0%<br>yarn - 0%<br>yoga - 0%<br>yogurt - 0%<br>yolk - 0%<br>zebra - 0%<br>zoo - 0%<br>zucchini - 0%</pre><p>The total count of the results is <strong>1303</strong>. The same as the number of identifiers supported by the machine learning model used for this request.</p><p>It’s the time when confidence shines. We filter the results to accept identifiers with confidence larger than <strong>70%</strong>:</p><pre>results<br>    .filter { $0.confidence &gt; 0.7 }<br>    .forEach { print(&quot;\($0.identifier) - \((Int($0.confidence * 100)))%&quot;) }</pre><p>Which gives us the list I presented in the beginning of the article:</p><pre>outdoor - 99%<br>land - 98%<br>liquid - 98%<br>water - 98%<br>water_body - 98%<br>waterways - 98%<br>waterfall - 98%<br>sky - 82%<br>cloudy - 82%</pre><p>This is the whole code needed to get these results:</p><pre>func process(_ image: UIImage) {<br>    guard let cgImage = image.cgImage else { return }<br>    let request = VNClassifyImageRequest()<br>    <br>    let requestHandler = VNImageRequestHandler(cgImage: cgImage,<br>                                               orientation: .init(image.imageOrientation),<br>                                               options: [:])<br>    <br>    DispatchQueue.global(qos: .userInitiated).async {<br>        do {<br>            try requestHandler.perform([request])<br>        } catch {<br>            print(&quot;Can&#39;t make the request due to \(error)&quot;)<br>        }<br>        <br>        guard let results = request.results as? [VNClassificationObservation] else { return }<br>        <br>        results<br>            .filter { $0.confidence &gt; 0.7 }<br>            .forEach { print(&quot;\($0.identifier) - \((Int($0.confidence * 100)))%&quot;) }<br>    }<br>}</pre><p>Imagine what happens when you classify all images you have in the application and allow users to filter them by content? If someone wants to find the most beautiful sky — it’s done.</p><p>And this is just the beginning.</p><p>If you want to play with Vision and see it for yourself you can check the latest version of my vision demo application <a href="https://github.com/ktustanowski/visiondemo">here</a>. The example code is located in this <a href="https://github.com/ktustanowski/visiondemo/blob/main/VisionDemoApp/VisionDemo/ImageProcessingViewController%2BImageClassification.swift">file</a>.</p><p>If you have any feedback, or just want to say hi, you are more than welcome to write me an <a href="mailto:cornerbit@protonmail.com">e-mail</a> or <a href="https://twitter.com/intent/tweet?screen_name=tustanowskik&amp;ref_src=twsrc%5Etfw">tweet to @tustanowskik</a></p><p>If you want to be up to date and always be first to know what I’m working on tap <a href="https://twitter.com/tustanowskik?ref_src=twsrc%5Etfw">follow @tustanowskik on Twitter</a></p><p>Thank you for reading!</p><p>[This was first published on my blog]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3cac0ab6f399" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>