<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Will Dady</title>
    <description>The latest articles on DEV Community by Will Dady (@willdady).</description>
    <link>https://dev.to/willdady</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F85994%2F45bf4e1c-5668-4c83-92eb-a67dcd02cb80.jpeg</url>
      <title>DEV Community: Will Dady</title>
      <link>https://dev.to/willdady</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/willdady"/>
    <language>en</language>
    <item>
      <title>Getting to the meat and potatoes of serverless recipe parsing with Amazon Bedrock</title>
      <dc:creator>Will Dady</dc:creator>
      <pubDate>Sat, 20 Jul 2024 05:28:25 +0000</pubDate>
      <link>https://dev.to/willdady/getting-to-the-meat-and-potatoes-of-serverless-recipe-parsing-with-amazon-bedrock-4ifc</link>
      <guid>https://dev.to/willdady/getting-to-the-meat-and-potatoes-of-serverless-recipe-parsing-with-amazon-bedrock-4ifc</guid>
      <description>&lt;p&gt;If you're like me you've probably visited a few recipe websites in your time and had the unfortunate experience of having to scroll through the author's life story before getting to the actual recipe. I recently stumbled across an interesting website which is able to take a URL and extract just the recipe without all the surrounding fluff. This got me thinking... could I build something similar on AWS with serverless technologies and generative AI?&lt;/p&gt;

&lt;p&gt;Lately I have been experimenting with various large language models on &lt;a href="https://aws.amazon.com/bedrock/" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;. I have been particularly interested in the latest offering from Anthropic with it's &lt;a href="https://www.anthropic.com/claude" rel="noopener noreferrer"&gt;Claude 3 family of models&lt;/a&gt;. My idea was to see if I could use Claude to extract a recipe from a web page and return it as a structured JSON object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crafting the prompt
&lt;/h2&gt;

&lt;p&gt;I hypothesised that Anthropic's Hiaku model would make light work of finding a recipe in a body of text assuming it is given a well crafted prompt. Luckily, Anthropic's &lt;a href="https://docs.anthropic.com/en/docs/prompt-engineering" rel="noopener noreferrer"&gt;prompt engineering documentation&lt;/a&gt; provides some excellent guidance on how best to craft a prompt to work with their family of LLMs.&lt;/p&gt;

&lt;p&gt;I began with some low-fi manual testing directly in the AWS Bedrock Chat Playground. I figured my eventual solution would scrape HTML from a target website and inject it into the prompt but in the meantime I'd need to do this manually. This is as simple as navigating to a recipe in my browser, viewing the page source and copying the raw HTML. I then paste the raw HTML into my prompt with some surrounding instructional text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.willdady.com%2Fimages%2F2024%2F05%2Fbedrock-screenshot_1917x811.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.willdady.com%2Fimages%2F2024%2F05%2Fbedrock-screenshot_1917x811.png" alt="The Amazon Bedrock Chat Playground showing a well-formed JSON response from Claude"&gt;&lt;/a&gt;&lt;br&gt;The Amazon Bedrock Chat Playground showing a well-formed JSON response from Claude
  &lt;/p&gt;

&lt;p&gt;After some experimentation in the Bedrock console I was able to get Claude to consistently parse a recipe from a body of text and return it as structured JSON. Pretty cool!&lt;/p&gt;

&lt;p&gt;I ended up settling on the following prompt template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read the HTML fragment contained inside the following &amp;lt;document&amp;gt;&amp;lt;/document&amp;gt; XML tags and extract the recipe.

&amp;lt;document&amp;gt;
&amp;lt;%- document %&amp;gt;
&amp;lt;/document&amp;gt;

If you are able to successfully find a recipe in the document output your result as a JSON object with the following fields:

title: A string containing the receipe title
description: A string containing a summary of what the recipe makes
ingredients: An array of strings where each is a single ingredient
steps: An array of strings where each describes a step required to complete the recipe

&amp;lt;example&amp;gt;
{
  "title": "Pasta Aglio E Oilio",
  "description": "Pasta Aglio e Olio is a classic Italian dish known for its simplicity and flavor. Translating to 'pasta with garlic and oil' it's a traditional recipe originating from the region of Naples. The dish features spaghetti cooked al dente and then tossed with minced garlic, olive oil, red pepper flakes, and sometimes parsley. Despite its minimal ingredients, Pasta Aglio e Olio packs a punch of flavor, with the garlic-infused oil coating each strand of pasta for a deliciously satisfying meal. It's a go-to dish for a quick, easy, and tasty dinner option.",
  "ingredients": [
    "Half a Lemon",
    "Fresh Grated Parmesan",
    "2 - 3 Tbsp Butter",
    "1 Cup Chopped Italian Parsley",
    "1 Pinch Chili Flakes or Hot Pepper FlakesFresh",
    "Ground Pepper",
    "2 Tbsp Salt (for pasta water)",
    "3 Cloves Thin Sliced Fresh Garlic",
    "1 Cup Pasta Water",
    "1/4 Cup Olive Oil",
    "450g Spaghetti"
  ],
  "steps": [
    "Slice garlic cloves thin (do not chop) and remove stems from parsley. Chop parsley fine and set all this aside. Have all your ingredients handy and ready since you will need to get to them quickly.",
    "Bring water with 2 TBSP salt to a boil and add pasta. Cook for 10-12 minutes till al dente. Save 2 cups of the hot pasta water (for possibly creating more liquid) before draining.",
    "About 5 minutes before your pasta is ready, heat the olive oil in large pan on medium and add the sliced garlic. Toss gently and after about 30 seconds add the hot pepper flakes and salt and pepper. Cook and toss for about 30 seconds.",
    "Add parsley, cooked pasta, pasta water to your liking, butter and squeeze in the half lemon (watch for seeds). Stir gently and bring to gentle boil. Add more fresh pepper and salt to taste. Toss the mixture well to fully coat and turn off heat.",
    "Let mixture set about 30 seconds so liquid thickens. Serve hot and top with fresh grated parmesan."
  ]
}
&amp;lt;/example&amp;gt;

If you are not able to find a recipe in the document respond with a JSON object with the following fields:

error: A string stating "No recipe found in document"
cause: A string describing the error

&amp;lt;example&amp;gt;
{
  "error": "No recipe found in document",
  "cause": "The document appears to be about Harley Davidson motocycles"
}
&amp;lt;/example&amp;gt;

&amp;lt;example&amp;gt;
{
  "error": "No recipe found in document",
  "cause": "The document appears to be about cats"
}
&amp;lt;/example&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take note of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I am using XML tags to delineate parts of the prompt as recommended in Anthropic's documentation - &lt;a href="https://docs.anthropic.com/en/docs/use-xml-tags" rel="noopener noreferrer"&gt;Use XML tags&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;document&amp;gt;&lt;/code&gt; tags is where you put the HTML you want Claude to read. 
The &lt;code&gt;&amp;lt;%- document %&amp;gt;&lt;/code&gt; contained within is an &lt;a href="https://ejs.co/" rel="noopener noreferrer"&gt;ejs&lt;/a&gt; placeholder. 
More on this shortly.&lt;/li&gt;
&lt;li&gt;I am using &lt;code&gt;&amp;lt;example&amp;gt;&lt;/code&gt; tags to show how I want the JSON to be structured.
I'm also telling Claude what to do if it is unable to find a recipe in the document.&lt;/li&gt;
&lt;li&gt;I am explicitly asking for JSON output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building a solution with the AWS CDK
&lt;/h2&gt;

&lt;p&gt;Now that I have a prompt which I am satisfied works, I set out to automate it.&lt;br&gt;
I began by creating a new &lt;a href="https://aws.amazon.com/cdk/" rel="noopener noreferrer"&gt;AWS CDK&lt;/a&gt; project using Typescript.&lt;/p&gt;

&lt;p&gt;The bulk of the solution will be driven by one of my favourite serverless AWS services, &lt;a href="https://aws.amazon.com/step-functions/" rel="noopener noreferrer"&gt;Step Functions&lt;/a&gt;! Using the CDK I create a Step Functions state machine with it's type set to &lt;em&gt;Express&lt;/em&gt;. We need to use the Express type as we will eventually expose the state machine via an API.&lt;/p&gt;

&lt;p&gt;The state machine workflow performs these main tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scrape (and sanitise) the HTML from a web page&lt;/li&gt;
&lt;li&gt;Generate the prompt by injecting the HTML into our prompt template&lt;/li&gt;
&lt;li&gt;Invoke Amazon Bedrock with our finalised prompt&lt;/li&gt;
&lt;li&gt;Handle the case where Claude was not able to find a recipe in the document&lt;/li&gt;
&lt;/ol&gt;


  &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.willdady.com%2Fimages%2F2024%2F05%2Frecipe-extractor-stepfunctions-graph_544x584.png" alt="Step Functions workflow diagram"&gt;Step Functions workflow diagram
  

&lt;h3&gt;
  
  
  Scraping the web page
&lt;/h3&gt;

&lt;p&gt;The first step of our state machine scrapes the contents of a web page containing our recipe. To do this I opted to create a simple Node.js based Lambda function using Typescript which takes a &lt;code&gt;url&lt;/code&gt; parameter and uses &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API" rel="noopener noreferrer"&gt;fetch&lt;/a&gt; to... ahem... &lt;em&gt;fetch&lt;/em&gt; the target web page.&lt;/p&gt;

&lt;p&gt;At this point I could simply return the raw HTML to advance the state machine but that would be extremely wasteful as there is a lot of redundant markup which serves little value, not to mention the longer the prompt the more it costs. We can do better!&lt;/p&gt;

&lt;p&gt;I opted to use &lt;a href="https://cheerio.js.org/" rel="noopener noreferrer"&gt;cheerio&lt;/a&gt;, a nifty HTML parsing library to clean-up the HTML before returning it from the function.&lt;br&gt;
Using cheerio I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extract the content of &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag&lt;/li&gt;
&lt;li&gt;Delete elements which have little semantic meaning to Claude such as &lt;code&gt;img&lt;/code&gt;, &lt;code&gt;video&lt;/code&gt;, &lt;code&gt;svg&lt;/code&gt; etc.&lt;/li&gt;
&lt;li&gt;Delete all attributes from elements&lt;/li&gt;
&lt;li&gt;Delete all &lt;code&gt;&amp;lt;!-- &amp;gt;&lt;/code&gt; comments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also do some final manipulation of the HTML string to remove excessive whitespace before returning the result.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Astute readers may note this is a far from ideal way of scraping a web page as it fails to consider dynamic content loaded after page load. My assumption is most recipe websites will deliver the complete recipe in the initial HTML sent from the server to aid SEO. That said, a headless Chromium + Puppeteer setup would likely be a better choice in a production environment.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Constructing the prompt
&lt;/h3&gt;

&lt;p&gt;For the second step of our state machine I wrote this short Lambda function. It takes the sanitised HTML output of the previous step and combines it with our prompt template. To do this I use &lt;a href="https://ejs.co/" rel="noopener noreferrer"&gt;ejs&lt;/a&gt; to replace the &lt;code&gt;&amp;lt;%- document %&amp;gt;&lt;/code&gt; placeholder in the prompt template with our sanitised HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ejs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ejs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROMPT_TEMPLATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`...`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Omitted for brevity&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Payload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ejs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PROMPT_TEMPLATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Invoking Amazon Bedrock
&lt;/h3&gt;

&lt;p&gt;Now we have our finalised prompt returned from the previous step we can invoke the model with it. What's nice is AWS Step Functions has a direct integration with Bedrock which means we can invoke it directly without first having to write another Lambda function. Here is what the &lt;code&gt;BedrockInvokeModel&lt;/code&gt; task looks like as defined in the CDK app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FoundationModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromFoundationModelId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FoundationModelIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic.claude-3-haiku-20240307-v1:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aiTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BedrockInvokeModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invoke Model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;anthropic_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock-2023-05-31&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JsonPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.Payload.output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;resultSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JsonPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringToJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JsonPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{}{}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JsonPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.Body.content[0].text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes the prompt I output from the previous step (&lt;code&gt;$.Payload.output&lt;/code&gt;) and invokes the Anthropic Claude 3 Haiku model as it's input.&lt;/p&gt;

&lt;p&gt;You might be wondering what that second 'assistant' message is. That is a message pre-fill which gives Claude a starting point on how to respond to the 'user' input. As instructed in our prompt template we always want Claude to respond with JSON. This combined with the prompt entered in the 'user' message helps guide Claude on how to respond. You can read more about how this works on Anthropic's website - &lt;a href="https://docs.anthropic.com/en/docs/control-output-format#prefilling-claudes-response" rel="noopener noreferrer"&gt;Control output format (JSON mode)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An interesting quirk of pre-filling the opening JSON brace is that it is not included in the message response we receive from Bedrock. You'll notice in the &lt;code&gt;resultSelector&lt;/code&gt; I am using &lt;code&gt;sfn.JsonPath.format&lt;/code&gt;, which is one of Step Function's &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html" rel="noopener noreferrer"&gt;intrinsic functions&lt;/a&gt;, to prepend the missing opening brace. The resulting string is then converted to JSON with the &lt;code&gt;sfn.JsonPath.stringToJson&lt;/code&gt; function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposing via API Gateway
&lt;/h3&gt;

&lt;p&gt;The last piece of the puzzle is to expose our state machine via &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;Amazon API Gateway&lt;/a&gt;. Within the same CDK app I instantiate a new &lt;code&gt;RestApi&lt;/code&gt; which proxies requests directly to my state machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RestApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;StepFunctionsRestApi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addProxy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;defaultIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StepFunctionsIntegration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stateMachine&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once deployed the CDK CLI will output the URL of the newly created Rest API. Copy the URL so we can test it in our web browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it!
&lt;/h2&gt;

&lt;p&gt;At this point all that's left to do is test it out on some web pages. To do this all you need to do is navigate to a website containing a recipe and paste your API URL &lt;em&gt;in-front&lt;/em&gt; of the url in your browser's address bar.&lt;/p&gt;

&lt;p&gt;For example, my Rest API is available at &lt;code&gt;https://c7jdzx7r36.execute-api.ap-southeast-2.amazonaws.com/prod/&lt;/code&gt;. If I want to extract the recipe from &lt;code&gt;https://www.recipetineats.com/caramel-slice/&lt;/code&gt; I simply append the latter to the former e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://c7jdzx7r36.execute-api.ap-southeast-2.amazonaws.com/prod/https://www.recipetineats.com/caramel-slice/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a few seconds, voila!... the recipe extracted from the page as JSON!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Caramel Slice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This is a Caramel Slice that works as promised – the creamy caramel sets perfectly and will never be runny, the chocolate won't crack when cutting it and the caramel won't ooze out. It's an easy recipe with no thermometer required."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ingredients"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"1 cup flour, plain/all purpose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"1/2 cup brown sugar, loosely packed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"1/2 cup desiccated coconut (US: sweetened finely shredded coconut)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"125g / 4.5oz  unsalted butter, melted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"125g / 4.5oz unsalted butter, roughly chopped"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"1/2 cup (80g) brown sugar, loosely packed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"1 tsp vanilla extract (or essence)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"395g / 14oz sweetened condensed milk (1 can, 300ml)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"200g / 7oz dark or milk melting chocolate (US: semi-sweet chocolate chips)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"1 tbsp vegetable oil"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Preheat oven to 180°C/350°F (fan 160°C)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Grease and line a 28x 18cm (lamington pan) / 7&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; x 11&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; rectangle pan with baking/parchment paper (Note 2). Have overhang for ease of removal."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Mix together Base ingredients and press into a pan (I use an egg flip)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bake for 15 minutes until the surface is golden. Cool in fridge if you have time (Note 3)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Lower oven to oven to 160°C/320°F (fan 140°C)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Place butter, sugar and vanilla in a saucepan over medium low heat. When the butter is melted, whisk to combine with sugar, then just leave it until it comes to a simmer."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"When bubbles appear, add condensed milk. Whisk constantly for 5 minutes (Note 4), until you start getting some big slow bubbles on the base."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Once bubbles start appearing, whisk for 1 minute, then pour onto Base. Tilt pan to spread evenly."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bake for 12 minutes. Don't worry if you get splotchy brown bits (this happens with ovens that don't distribute heat evenly)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Cool on counter for 20 minutes then refrigerate 30 minutes - bottom of pan should be warm but surface cool (not cold) to touch. (Note 5)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Place chocolate and oil in a microwave proof bowl. Microwave in 30 second bursts, stirring in between, until chocolate is fully melted (takes me 4 x 30 sec)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Pour over caramel, spread with spatula. Then gently shake pan to make the surface completely flat."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Refrigerate 1 hour or until set. Remove from fridge and leave out for 5 minutes to take chill out of chocolate slightly. Then cut into bars or squares to serve!"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is an example of what is output when you provide a URL to something which doesn't contain a recipe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://c7jdzx7r36.execute-api.ap-southeast-2.amazonaws.com/prod/https://news.ycombinator.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No recipe found in document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cause"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The document appears to be a Hacker News article listing and does not contain any recipes."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;I had a lot of fun building this and hopefully this post highlights how combining serverless technologies with generative AI can be used for novel outcomes. This is very much a toy/experimental project and is not intended for serious production use. To make this safer for use in production a number of additional features would need to be added such as improved error handing, authentication at the API layer and caching to name a few.&lt;/p&gt;

&lt;p&gt;If you'd like to try it out yourself you can find the complete CDK app here &lt;a href="https://github.com/willdady/recipe-extractor-cdk" rel="noopener noreferrer"&gt;https://github.com/willdady/recipe-extractor-cdk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@videmusart?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Syd Wachs&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/onions-and-potato-on-table-epqNIYI6S7E?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>generativeai</category>
      <category>systemdesign</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Photo mosaics with Rust</title>
      <dc:creator>Will Dady</dc:creator>
      <pubDate>Sat, 10 Aug 2019 23:24:36 +0000</pubDate>
      <link>https://dev.to/willdady/photo-mosaics-with-rust-546</link>
      <guid>https://dev.to/willdady/photo-mosaics-with-rust-546</guid>
      <description>&lt;p&gt;When I was a kid I remember seeing a poster of Yoda from Star Wars which was made up from various screenshots from the movie. I remember being facinated how several tiny images could be arranged to create a larger image and that how the colour of each small image contributed to the larger picture overall.&lt;/p&gt;

&lt;p&gt;The idea of arranging small coloured tiles to create images has been around since 300 BC and artwork created in such a way is called a &lt;strong&gt;Mosaic&lt;/strong&gt; which &lt;a href="https://en.wikipedia.org/wiki/Mosaic" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt; describes as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a piece of art or image made from the assembling of small pieces of colored glass, stone, or other materials.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While the technique of arranging coloured tiles to form an image has been around for hundreds of years, photo mosaics are a relatively new take on the art style.&lt;/p&gt;

&lt;h2&gt;
  
  
  Photo mosaics
&lt;/h2&gt;

&lt;p&gt;The first mainstream use of a photo mosaic I recall was on one of the official posters for the 1998 film staring Jim Carrey, The Truman Show.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F08%2Ftruman_poster.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F08%2Ftruman_poster.jpg" alt="A poster for The Truman Show"&gt;&lt;/a&gt;&lt;/p&gt;
A photo mosaic poster for The Truman Show (1998)



&lt;p&gt;You can see in the following close-up of the poster, specifically Jim Carrey's right eye, the image is made up of various scenes from the film. Interestingly each tile is tinted rather than solely relying on the actualy colour in the frame. It's a forgivable workaround considering there probably isn't that many fleshy-beige coloured scenes in the movie to sample from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F08%2Ftruman_poster_eye.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F08%2Ftruman_poster_eye.jpg" alt="A close-up of the poster for The Truman Show (1998)"&gt;&lt;/a&gt;&lt;/p&gt;
A close-up of the poster for The Truman Show (1998)



&lt;h2&gt;
  
  
  Writing a photo-mosaic generator
&lt;/h2&gt;

&lt;p&gt;As I've been learning the &lt;a href="https://www.rust-lang.org/" rel="noopener noreferrer"&gt;Rust programming language&lt;/a&gt; lately I thought creating a photo-mosaic generator could be a fun project. Rust is a relatively new systems programming language akin to C/C++ in terms of performance. What makes Rust unique is the way it manages memory compared to other low-level languages.&lt;/p&gt;

&lt;p&gt;I knew a generator like this would require a large set of tile images to sample from. The original plan was for it to only create mosaics from emoji which is how it got it's name &lt;strong&gt;emosaic&lt;/strong&gt; &lt;em&gt;(emoji + mosaic = emosaic)&lt;/em&gt; but as I progressed it made more sense to keep in generic and let the user provide their own pool of tile images.&lt;/p&gt;

&lt;p&gt;Similar to my previous (and first) Rust project &lt;a href="https://github.com/willdady/swirlr" rel="noopener noreferrer"&gt;swirlr&lt;/a&gt;, I used dependencies from &lt;a href="https://crates.io" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt;, Rust's equivalent to NPM for Node.js. The main libraries I used are &lt;a href="https://crates.io/crates/image" rel="noopener noreferrer"&gt;image&lt;/a&gt;, for loading an manipulating images and sampling pixels, and &lt;a href="https://crates.io/crates/clap" rel="noopener noreferrer"&gt;clap&lt;/a&gt; a command line argument parser.&lt;/p&gt;

&lt;p&gt;Emosaic works by taking a directory of images used as tiles and a source image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;emosaic /path/to/tiles/ source.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The output file will be saved to the current directory as &lt;code&gt;output.png&lt;/code&gt; though a custom output path can be provided via the &lt;code&gt;-o&lt;/code&gt; option. Output is always in PNG format.&lt;/p&gt;

&lt;p&gt;The app works by first reading all images in the tiles directory. Each image's pixels are looped over to find the average colour of each image. I found it necessary to exclude any tiles where more than 50% of it's pixels are completely transparent as these didn't look good in the final output. The image's path and average colour is then added to a &lt;code&gt;Tile&lt;/code&gt; struct. Each &lt;code&gt;Tile&lt;/code&gt; struct was then added to a &lt;code&gt;TileSet&lt;/code&gt; struct which represented all available tiles to choose from in the next step.&lt;/p&gt;

&lt;p&gt;Next the source image is loaded and for each pixel in the source image we call the &lt;code&gt;TileSet&lt;/code&gt;'s &lt;code&gt;closest_tile(&amp;amp;self, rgba: Rgba&amp;lt;u8&amp;gt;) -&amp;gt; Tile&lt;/code&gt; method. This method works by naively looping over each tile and comparing the distance between colours. We say &lt;em&gt;"distance"&lt;/em&gt; as we calculate how close one colour is to another by treating each as a point in 3D space RGB -&amp;gt; XYZ (alpha is ignored). The resulting Tile is then added to a Map keyed by the pixel colour that way subsequent lookups can simply check if the Tile for a given colour has already been found, this is a particularly useful optimization as lossy formats like JPEG will repeat colours often.&lt;/p&gt;

&lt;p&gt;Once the tile is found it's copied to the same row/column coordinates in the output image factoring in the desired tile size. The tile size can be configured as the optional command line option &lt;code&gt;-t&lt;/code&gt; which defaults to 16. For example a 100x100 source image yields an output image of 1600x1600 (100 x 16 = 1600) so it's important the source image is small.&lt;/p&gt;
&lt;h2&gt;
  
  
  Further improvements
&lt;/h2&gt;

&lt;p&gt;Im pretty happy with how this turned out and it's super fast thanks to the low-level nature of Rust. For example, the Marilyn Monroe image above is generated from a directory of 2625 tiles, each with dimensions of 64x64 and a source image with with dimensions of 100x100. The command completes in about &lt;strong&gt;2.9&lt;/strong&gt; seconds on a 2017 Macbook Pro.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F08%2Fwarhol.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F08%2Fwarhol.jpg" alt="100x100 source image"&gt;&lt;/a&gt;&lt;/p&gt;
100x100 source image



&lt;p&gt;Some further optimisations could be made such as caching the TileSet to disk that way the program doesn't need to read potentially thousands of tile images every time the app is run. Also at the time of writing it does &lt;em&gt;not&lt;/em&gt; provided an option to 'tint' the output image like the Truman Show example above.&lt;/p&gt;

&lt;p&gt;Overall I'm really liking Rust. It has a very steep learning curve but it's rewarding once the borrow checker stops complaining. It's nice to feel confident that if the app compiles it will run safely. I find small projects like this are a great way to get familiar with a new programming language.&lt;/p&gt;

&lt;p&gt;Feel free to &lt;a href="https://github.com/willdady/emosaic" rel="noopener noreferrer"&gt;checkout the complete source code for emosaic on github&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/willdady" rel="noopener noreferrer"&gt;
        willdady
      &lt;/a&gt; / &lt;a href="https://github.com/willdady/emosaic" rel="noopener noreferrer"&gt;
        emosaic
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Mosaic image generator written in Rust!
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;emosaic&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Mosaic generator written in &lt;a href="https://www.rust-lang.org/" rel="nofollow noopener noreferrer"&gt;Rust!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/willdady/emosaicexample/warhol.png?raw=true"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fwilldady%2Femosaicexample%2Fwarhol.png%3Fraw%3Dtrue" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Building&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;To build &lt;a href="https://www.rust-lang.org/tools/install" rel="nofollow noopener noreferrer"&gt;make sure you have rust installed&lt;/a&gt;.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;cargo build --release
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once compiled, the binary can be found at &lt;code&gt;target/release/emosaic&lt;/code&gt; in the repository root.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;The command expects a path to a directory containing square 'tile' images and a source image.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;emosaic /path/to/tile/images/ source.png
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Modes&lt;/h3&gt;

&lt;/div&gt;

&lt;p&gt;The strategy used to generate the mosaic is controlled by the &lt;code&gt;-m, --mode&lt;/code&gt; option.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;1to1 (Default)&lt;/h4&gt;

&lt;/div&gt;

&lt;p&gt;For each pixel in the source image a tile with the nearest matching average color will be emitted.&lt;/p&gt;

&lt;p&gt;Assuming a source image with dimensions 100x100 and default tile size of 16 the output image will be 1600x1600.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;4to1&lt;/h4&gt;

&lt;/div&gt;

&lt;p&gt;For every 2x2 pixels one tile will be emitted. Tiles are divided into 2x2 segments and the average colour of each segment is stored. The tile with the nearest average color in &lt;em&gt;each&lt;/em&gt; segment to the target pixels will be chosen. This mode…&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/willdady/emosaic" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>rust</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Creating a remote BitTorrent box with Terraform, Transmission and Docker on AWS</title>
      <dc:creator>Will Dady</dc:creator>
      <pubDate>Sun, 14 Jul 2019 12:47:12 +0000</pubDate>
      <link>https://dev.to/willdady/creating-a-remote-bittorrent-box-with-terraform-transmission-and-docker-on-aws-2m40</link>
      <guid>https://dev.to/willdady/creating-a-remote-bittorrent-box-with-terraform-transmission-and-docker-on-aws-2m40</guid>
      <description>&lt;p&gt;My head has very much been in the DevOps space lately as I've been more and more hands on with AWS. I got thinking about an idea I had some time ago... What would be involved in running an ephemeral BitTorrent client on AWS with downloads automatically saved to S3?&lt;/p&gt;

&lt;p&gt;My ultimate goal was to be able to spin up and tear down a remote BitTorrent client all from the command line. I knew I wanted to run &lt;a href="https://transmissionbt.com/" rel="noopener noreferrer"&gt;Transmission&lt;/a&gt; as I had used it in the past as it has a web interface for adding and removing torrents and monitoring downloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  VPN
&lt;/h2&gt;

&lt;p&gt;As I'm hosting this in the cloud I really wanted the BitTorrent traffic to route via a VPN rather than directly to the host instance. When I first started experimenting with this idea I begain by spinning up a t2.micro instance via the AWS console, connecting via SSH and installing OpenVPN directly on a host. I quickly discovered the hard way than starting OpenVPN while connected to the host over SSH wouldn't work as I guess by default it takes over the host's network traffic, immediately dropping my SSH connection and bricking the server until it's rebooted via the AWS console.&lt;/p&gt;

&lt;p&gt;This lead me to Docker. Having OpenVPN containerised felt like a more sensible solution as it would allow me to SSH into the host instance to tinker with the setup. I figured someone must have already created an OpenVPN docker image and after some googling I found that github user &lt;a href="https://github.com/haugene" rel="noopener noreferrer"&gt;haugene&lt;/a&gt; had created an aptly named image, &lt;a href="https://github.com/haugene/docker-transmission-openvpn" rel="noopener noreferrer"&gt;docker-transmission-openvpn&lt;/a&gt;, which bundles both OpenVPN and Transmission together. Bonus!&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncing completed downloads to S3
&lt;/h2&gt;

&lt;p&gt;Once downloads complete Transmission copies files to the &lt;code&gt;/data/completed&lt;/code&gt; directory. I needed to figure out a way to automatically copy completed downloads to S3. For this I decided to write my own utility in Go which would simply watch the contents of a directory for new files and upload them to an S3 bucket. This utility is called &lt;a href="https://github.com/willdady/go-watch-s3" rel="noopener noreferrer"&gt;go-watch-s3&lt;/a&gt; and I have a dedicated blog post about this &lt;a href="https://willdady.com/uploading-new-files-to-s3-with-golang" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both the Transmission container and the container running go-watch-s3 share a bind-mounted directory so when Transmission wrote files to it's &lt;code&gt;/data/completed&lt;/code&gt; directory go-watch-s3 would see the new files and upload them. Pretty neat!&lt;/p&gt;

&lt;h2&gt;
  
  
  Provisioning the instance with Terraform
&lt;/h2&gt;

&lt;p&gt;The provisioning is handled by a &lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; configuration which sets up our infrastructure. It creates the EC2 instance, a security group so we can access the Transmission UI, and the appropriate IAM role, policy and instance profile so the instance is able to write files to our existing S3 bucket.&lt;/p&gt;

&lt;p&gt;Installing software on the instance is handled as part of a user data script which runs when the instance first launches and is defined as part of the &lt;strong&gt;aws_instance&lt;/strong&gt; resource type in Terraform. This script is actually relatively simple as it just installs Docker, pulls the above mentioned images and runs them. I had considered using &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; as it's usually a nicer experience when orchestrating multiple containers but went ahead with vanilla docker run commands instead.&lt;/p&gt;

&lt;p&gt;Finally, all that's left to do is to apply the configuration with &lt;code&gt;terraform apply&lt;/code&gt; to create our resources on AWS. Once the configuration is applied the url of the Transmission web UI will be output however you will need to wait a few minutes for the startup script to do it's thing. Once the UI is available torrents can be added with completed downloads auto-magically copied to S3!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F07%2Ftransmission-web-ui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F07%2Ftransmission-web-ui.png" alt="Transmission Web UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you're finished, destroying the infrastructure you just created can be torn down with &lt;code&gt;terraform destroy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stating the obvious
&lt;/h2&gt;

&lt;p&gt;In summary I feel like this was a fun 'toy' project though I don't personally feel the need to improve it further. The itch has been scratched if you will. To see the complete Terraform configuration check out &lt;a href="https://github.com/willdady/terraform-transmission-aws" rel="noopener noreferrer"&gt;terraform-transmission-aws&lt;/a&gt; on github.&lt;/p&gt;

&lt;p&gt;While there is nothing illegal about BitTorrent from a pure technology standpoint, pirating content may well fast-track you to a reprimand from AWS or worse your account closed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F07%2Faws-rage.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fs3-ap-southeast-2.amazonaws.com%2Fmedia.willdady.com%2F2019%2F07%2Faws-rage.gif" alt="AWS shutting you down"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>aws</category>
      <category>bittorrent</category>
    </item>
  </channel>
</rss>
