<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet href="/feed.xsl" type="text/xsl"?><feed xmlns="http://www.w3.org/2005/Atom"><title>Larry Hudson</title><subtitle>Australian full stack engineer based in Paris, France</subtitle><link href="https://larryhudson.io/full.xml" rel="self"/><link href="https://larryhudson.io/" rel="alternate"/><updated>2024-08-23T00:00:00Z</updated><id>https://larryhudson.io/</id><author><name>Larry Hudson</name><uri>https://larryhudson.io/</uri></author><entry><title>Putting my Strava activities on a single map</title><link href="https://larryhudson.io/astro-strava-map/"/><updated>2024-08-23T00:00:00Z</updated><id>https://larryhudson.io/astro-strava-map/</id><content type="html">&lt;p&gt;I’m excited to share a small project I’ve been working on: a web app that takes your Strava workout activiites and puts them on a single map using Astro, the Strava API and the Mapbox SDK.&lt;/p&gt; &lt;p&gt;After moving to Paris in early May this year, I have been walking a lot, as a way to stay healthy and to explore Paris and learn about its geography. I’ve been walking around the 18th, 9th, 10th and 11th arrondissements in particular.&lt;/p&gt; &lt;p&gt;I thought it would be interesting to make a small web app that takes my Strava activities and renders them on a single map, as a way to visualise the areas that I have been exploring, and encourage myself to check out more.&lt;/p&gt; &lt;p&gt;Here’s a screenshot of my activities on a map:&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://larryhudson.io/images/astro-strava-map-screenshot.jpg&quot; alt=&quot;Screenshot of my Strava Activities Map showing my walks around Paris&quot; /&gt;&lt;/p&gt; &lt;h2 id=&quot;decoding-polyline-strings&quot;&gt;Decoding polyline strings&lt;/h2&gt; &lt;p&gt;When you retrieve your list of activities from the Strava API, it returns the map information as a long encoded string:&lt;/p&gt; &lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;map&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;a1410355832&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;polyline&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ki{eFvqfiVqAWQIGEEKAYJgBVqDJ{BHa@jAkNJw@Pw@V{APs@^aABQAOEQGKoJ_FuJkFqAo@{A}@sH{D...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;resource_state&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;summary_polyline&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ki{eFvqfiVsBmA`Feh@qg@iX`B}JeCcCqGjIq~@kf@cM{KeHeX`@_GdGkSeBiXtB}YuEkPwFyDeAzAe@...&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;These encoded strings follow the &lt;a href=&quot;https://developers.google.com/maps/documentation/utilities/polylinealgorithm&quot;&gt;Encoded Polyline Algorithm Format&lt;/a&gt;. I used the &lt;a href=&quot;https://www.npmjs.com/package/@mapbox/polyline&quot;&gt;@mapbox/polyline&lt;/a&gt; npm library to turn these strings into coordinates that can be rendered on a map.&lt;/p&gt; &lt;h2 id=&quot;aiders-ai-pair-programming&quot;&gt;Aider’s AI pair programming&lt;/h2&gt; &lt;p&gt;I’ve been using &lt;a href=&quot;https://aider.chat/&quot;&gt;Aider&lt;/a&gt; a lot in the last couple of weeks. It’s an open-source AI coding assistant that lives in the terminal. I’ve been using it with Anthropic’s Claude 3.5 Sonnet model and I’ve been really impressed with the results. It’s been a big productivity boost to help me get stuff done.&lt;/p&gt; &lt;h2 id=&quot;check-out-the-github-repo&quot;&gt;Check out the GitHub repo&lt;/h2&gt; &lt;p&gt;If you’re curious about how this project works and want to give it a try, check out the GitHub repository for more details and the source code.&lt;/p&gt; &lt;p&gt;GitHub: &lt;a href=&quot;https://github.com/larryhudson/astro-strava-map&quot;&gt;https://github.com/larryhudson/astro-strava-map&lt;/a&gt;&lt;/p&gt;</content></entry><entry><title>Giving Claude 3.5 Sonnet extra abilities with custom tools</title><link href="https://larryhudson.io/anthropic-claude-chatbot-custom-tools/"/><updated>2024-07-03T00:00:00Z</updated><id>https://larryhudson.io/anthropic-claude-chatbot-custom-tools/</id><content type="html">&lt;p&gt;I’m excited to share a demo chatbot interface built with Next.js that allows you to chat with &lt;a href=&quot;https://www.anthropic.com/news/claude-3-5-sonnet&quot;&gt;Claude 3.5 Sonnet&lt;/a&gt; (a competitor to ChatGPT and the current state of the art large language model) and create custom tools that extend its capabilities. By connecting Claude to a Weaviate vector database, the chatbot has the ability to save notes for future reference, and search for relevant information to be more helpful.&lt;/p&gt; &lt;p&gt;You can &lt;a href=&quot;https://github.com/larryhudson/claude-chat-with-weaviate&quot;&gt;check out the project on GitHub&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;As a side note, in the last couple of weeks I’ve been playing with Claude 3.5 Sonnet a lot, and I’m super impressed with its abilities. I’m having a lot of fun creating little web apps and tiny projects. It feels like you can make anything with it, as long as you can think it through piece by piece.&lt;/p&gt; &lt;h2 id=&quot;project-highlights&quot;&gt;Project highlights&lt;/h2&gt; &lt;p&gt;This demo project showcases several key features and technologies:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Anthropic API Integration&lt;/strong&gt;:&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Custom Tool Integration&lt;/strong&gt;: This allows the AI to perform actions beyond its training data, such as searching through notes, fetching real-time information, or interacting with external systems.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Weaviate Vector Database Integration&lt;/strong&gt;: Weaviate, a vector database enables semantic search capabilities&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Dynamic Note Saving and Retrieval&lt;/strong&gt;: The assistant can save and retrieve notes, creating a growing knowledge base.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Streaming Interface&lt;/strong&gt;: The chatbot provides real-time, streaming responses This is implemented using server-sent events&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Next.js Framework&lt;/strong&gt;: Built on &lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt; using the &lt;a href=&quot;https://nextjs.org/docs/app&quot;&gt;App Router&lt;/a&gt; model.&lt;/li&gt; &lt;/ol&gt; &lt;h2 id=&quot;tool-use-with-claude&quot;&gt;Tool use with Claude&lt;/h2&gt; &lt;p&gt;In this project, I’ve added a few custom tools that Claude can choose to use within the chat interface.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Tools are defined with a name, description, and input schema in the API request.&lt;/li&gt; &lt;li&gt;Claude can decide when to use tools or be instructed to use specific tools.&lt;/li&gt; &lt;li&gt;The process involves Claude requesting tool use, your application executing the tool, and optionally sending results back to Claude.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;For a deep dive into tool use with the Anthropic API, check out these resources:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/build-with-claude/tool-use&quot;&gt;Anthropic’s tool use documentation&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;https://github.com/anthropics/courses/tree/master/ToolUse&quot;&gt;Anthropic’s free tool use course on GitHub&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h3 id=&quot;weaviate-vector-database-integration&quot;&gt;Weaviate vector database integration&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;&lt;strong&gt;Semantic Search&lt;/strong&gt;: Weaviate allows Claude to perform semantic searches rather than simple keyword matching.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Efficient Information Retrieval&lt;/strong&gt;: The vector database enables quick and efficient retrieval of relevant information from a large corpus of data.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Dynamic Knowledge Base&lt;/strong&gt;: new information can be stored in Weaviate, allowing the assistant to expand its knowledge over time.&lt;/li&gt; &lt;/ol&gt; &lt;h2 id=&quot;limitations-and-drawbacks-of-this-approach&quot;&gt;Limitations and drawbacks of this approach&lt;/h2&gt; &lt;p&gt;One limitation of this approach is cost. The more tools that you share with Claude, and the more output that these tools create, use up tokens, which makes your API usage more expensive. While the latest AI models have large context windows, the costs add up, and would be a concern if you were trying to make an app for a large user base.&lt;/p&gt; &lt;p&gt;If you were making a production app, you would need to pass the cost onto the user, or implement severe rate limits which would be annoying for the user.&lt;/p&gt; &lt;p&gt;However, as Ethan Mollick says, you should always treat the current AI model as the worst one you will ever use again, so abilities are going to keep getting better. And costs will go down. So it’s worth experimenting to push these models to get the most out of them.&lt;/p&gt; &lt;h2 id=&quot;running-the-project-locally&quot;&gt;Running the project locally&lt;/h2&gt; &lt;p&gt;To get this project up and running, you can find detailed instructions in the &lt;a href=&quot;https://larryhudson.io/anthropic-claude-chatbot-custom-tools/&quot;&gt;GitHub repository README&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;ideas-for-future-improvements&quot;&gt;Ideas for future improvements&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;&lt;strong&gt;Scheduled workflows&lt;/strong&gt; - I like the idea of being able to create specific tasks that the assistant would perform periodically - eg. monitoring websites and generating summaries, or keeping an eye on to do lists and creating plans for what needs to get done. I think doing automation in this way, using natural language, has a lot of potential.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Text-to-speech integration&lt;/strong&gt; - I think it would be great to be able to talk to the assistant using speech, and to listen to the assistant using a ‘read aloud’ feature. That could reduce the amount of time the user needs to type into a text box. I like the &lt;a href=&quot;https://openai.com/index/introducing-the-chatgpt-app-for-ios/&quot;&gt;ChatGPT app&lt;/a&gt; for this reason.&lt;/li&gt; &lt;li&gt;&lt;strong&gt;Claude’s Artifacts feature&lt;/strong&gt; - I’m a big fan of the Claude web UI’s &lt;a href=&quot;https://support.anthropic.com/en/articles/9487310-what-are-artifacts-and-how-do-i-use-them&quot;&gt;Artifacts&lt;/a&gt; feature, which is how you can collaborate with Claude on a document or a piece code. If it’s a web app, you can preview it without needing to copy and paste it elsewhere. I like the idea of integrating a code editor (like &lt;a href=&quot;https://sandpack.codesandbox.io/&quot;&gt;Sandpack&lt;/a&gt;) into the chatbot interface.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;let-me-know-what-you-think&quot;&gt;Let me know what you think&lt;/h2&gt; &lt;p&gt;If this project is interesting to you, I’d love to hear what you think! Please feel free to reach out.&lt;/p&gt;</content></entry><entry><title>A Neovim shortcut for quickly extracting Twig components</title><link href="https://larryhudson.io/neovim-twig-component-extract/"/><updated>2024-06-13T00:00:00Z</updated><id>https://larryhudson.io/neovim-twig-component-extract/</id><content type="html">&lt;p&gt;In the last week or so I’ve been getting up to speed learning how to make websites with &lt;a href=&quot;https://craftcms.com/&quot;&gt;Craft CMS&lt;/a&gt;. Craft is a PHP-based content management system that uses &lt;a href=&quot;https://twig.symfony.com/&quot;&gt;Twig&lt;/a&gt; templates.&lt;/p&gt; &lt;p&gt;In Twig, you can include smaller templates inside your template by doing something like this:&lt;/p&gt; &lt;pre class=&quot;language-twig&quot;&gt;&lt;code class=&quot;language-twig&quot;&gt;&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; include&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;partials/Header&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;I’m using &lt;a href=&quot;https://neovim.io/&quot;&gt;Neovim&lt;/a&gt; as my code editor - I’ve been using it for the past 6-12 months. Neovim is based on the original &lt;a href=&quot;https://www.vim.org/&quot;&gt;Vim&lt;/a&gt; editor that has been around since the early nineties, but is extremely customisable thanks to its &lt;a href=&quot;https://lua.org/&quot;&gt;Lua&lt;/a&gt; scripting engine. Lua is a flexible and lightweight programming language that is relatively straightforward to learn.&lt;/p&gt; &lt;p&gt;Building a basic homepage template in Twig, I found myself writing a lot of ‘include’ statements and separating components into separate Twig files. I was keen to make this more efficient if possible.&lt;/p&gt; &lt;h2 id=&quot;what-you-can-do-with-lua-scripting-in-neovim&quot;&gt;What you can do with Lua scripting in Neovim&lt;/h2&gt; &lt;p&gt;Within your Neovim configuration file, it’s possible to assign keyboard shortcuts (or ‘keymaps’) to execute certain functions. These may be ‘built in’ functions, or custom functions you write yourself.&lt;/p&gt; &lt;p&gt;Within a custom function, you can automate key presses (so one ‘key map’ can execute a series of key presses), but you can also do more complicated things with files in the current project folder.&lt;/p&gt; &lt;h2 id=&quot;extracting-components-in-twig-files&quot;&gt;Extracting components in Twig files&lt;/h2&gt; &lt;p&gt;I thought it would be interesting to create a custom shortcut where:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;I could select some code, hit the space bar and then hit ‘et’ (standing for ‘[e]xtract [t]emplate’).&lt;/li&gt; &lt;li&gt;The editor would then ask me to type the name of the component that I want to save.&lt;/li&gt; &lt;li&gt;It would save a new Twig file with the selected code in it, at &lt;code&gt;templates/partials/&amp;lt;Component name&amp;gt;.twig&lt;/code&gt;&lt;/li&gt; &lt;li&gt;It would replace the current selected text with an include statement like this:&lt;/li&gt; &lt;/ul&gt; &lt;pre class=&quot;language-twig&quot;&gt;&lt;code class=&quot;language-twig&quot;&gt;&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; include&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;partials/&amp;lt;Component name&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;With a little help from ChatGPT, I was able to get this working fairly quickly!&lt;/p&gt; &lt;p&gt;Here’s the basic Lua code to get this working:&lt;/p&gt; &lt;pre class=&quot;language-lua&quot;&gt;&lt;code class=&quot;language-lua&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ExtractTwigTemplate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Do not run if this is not a Twig file&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;filetype &lt;span class=&quot;token operator&quot;&gt;~=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;twig&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; print &lt;span class=&quot;token string&quot;&gt;&#39;This is not a Twig file&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Ask the user for the template filename&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; filename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input &lt;span class=&quot;token string&quot;&gt;&#39;Enter new template filename: &#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; filename &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; print &lt;span class=&quot;token string&quot;&gt;&#39;Filename cannot be empty!&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Get the selected lines&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; start_line &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;line &lt;span class=&quot;token string&quot;&gt;&quot;&#39;&amp;lt;&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; end_line &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;line &lt;span class=&quot;token string&quot;&gt;&quot;&#39;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; component_lines &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getline&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;start_line&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; end_line&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- If only one line is selected, convert it to a table (like an array in Lua)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;component_lines&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;string&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; component_lines &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; component_lines &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Write the lines to the new template file&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; component_filepath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;templates/partials/%s.twig&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writefile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;component_lines&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; component_filepath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Delete the selected lines&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cmd &lt;span class=&quot;token string&quot;&gt;&quot;&#39;&amp;lt;,&#39;&gt;d&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; include_line &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{{ include(&#39;partials/%s&#39;) }}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Insert the include line&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;start_line &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; include_line&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Set the key map so that the function runs when the user presses &amp;lt;Leader&gt;et in visual mode&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;nvim_set_keymap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;x&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;Leader&gt;et&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;:lua ExtractTwigTemplate()&amp;lt;CR&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; noremap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; silent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;To get this working, I added this code to the end of my &lt;code&gt;init.lua&lt;/code&gt; file in my Neovim configuration folder. You can &lt;a href=&quot;https://neovim.io/doc/user/lua-guide.html&quot;&gt;read more about configuring Neovim using Lua here&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;So once this was added to my config file, I could select some code in a Twig file, hit space then ‘et’, and it would ask me for the component name. When I enter the component name and hit enter, it saves the selected code into a new file and replaces it in the current file with the ‘include’ statement. Handy!&lt;/p&gt; &lt;p&gt;Here’s a GIF showing how the keyboard shortcut works in practice:&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://larryhudson.io/images/neovim-twig/extract-component-without-props.gif&quot; alt=&quot;A screen recording GIF of Larry selecting some code, hitting space then &#39;et&#39;, naming the component &#39;Card&#39;, and the keyboard shortcut running.&quot; /&gt;&lt;/p&gt; &lt;p&gt;(Side note: I used &lt;a href=&quot;https://github.com/keycastr/keycastr&quot;&gt;Keycastr&lt;/a&gt; to show the key strokes in the bottom corner of the screen, macOS’ native Screenshot tool to record the video, and &lt;a href=&quot;https://apps.apple.com/us/app/gifski/id1351639930?mt=12&quot;&gt;Gifski&lt;/a&gt; to turn the video into a GIF.)&lt;/p&gt; &lt;h2 id=&quot;taking-it-further-with-component-props&quot;&gt;Taking it further with component props&lt;/h2&gt; &lt;p&gt;Often when you’re creating components, you want to pass in variables to be included in the component. In Twig, you can do that like this, by passing in an object as the second parameter:&lt;/p&gt; &lt;pre class=&quot;language-twig&quot;&gt;&lt;code class=&quot;language-twig&quot;&gt;&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; include&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;partials/Card&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;Title&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; description&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;I wanted to extend my shortcut so that it would ask for a list of ‘props’ that would then be included in the component.&lt;/p&gt; &lt;p&gt;This meant adding another question that the editor would ask after receiving the component name.&lt;/p&gt; &lt;p&gt;The shortcut would need to, for each prop:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;add a line to the ‘include’ statement setting the prop (eg. &lt;code&gt;title: &#39;&#39;,&lt;/code&gt;)&lt;/li&gt; &lt;li&gt;add a line to the top of the component file itself, to show that the prop is avaiable within the component (eg. &lt;code&gt;{% set title = title|default(&#39;&#39;) %}&lt;/code&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;the ‘object’ lines that are inserted into the ‘include’ statement (eg. , but I also wanted to include the props at the top of the component file itself, to show what props are available to the component:&lt;/p&gt; &lt;p&gt;So if I create a component called ‘Card’ with the props ‘title’ and ‘description’, the output should be:&lt;/p&gt; &lt;pre class=&quot;language-twig&quot;&gt;&lt;code class=&quot;language-twig&quot;&gt;&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;{# where my selected code was #}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; include&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;partials/Card&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; heading&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;{# inside templates/partials/Card.php #}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;token tag-name keyword&quot;&gt;set&lt;/span&gt; title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;default&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;%}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;token tag-name keyword&quot;&gt;set&lt;/span&gt; description &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; description&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;default&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;%}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;{# Original selected code goes here #}&lt;/span&gt;&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;This was a little bit more fiddly, because it required writing new lines of text and manipulating the Lua table of strings before writing the component file. But I’m happy with where this ended up. Here’s the updated code:&lt;/p&gt; &lt;pre class=&quot;language-lua&quot;&gt;&lt;code class=&quot;language-lua&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ExtractTwigTemplateWithProps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;filetype &lt;span class=&quot;token operator&quot;&gt;~=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;twig&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; print &lt;span class=&quot;token string&quot;&gt;&#39;This is not a Twig file&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Ask the user for the template filename&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; filename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input &lt;span class=&quot;token string&quot;&gt;&#39;Enter new template filename: &#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; filename &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; print &lt;span class=&quot;token string&quot;&gt;&#39;Filename cannot be empty!&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Define include_lines as a table so we can add props to it if we need&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; include_lines &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{{ include(&#39;partials/%s&#39; }}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Get the selected lines&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; start_line &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;line &lt;span class=&quot;token string&quot;&gt;&quot;&#39;&amp;lt;&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; end_line &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;line &lt;span class=&quot;token string&quot;&gt;&quot;&#39;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; component_lines &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getline&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;start_line&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; end_line&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- If only one line is selected, convert it to a table (like an array in Lua)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;component_lines&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;string&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; component_lines &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; component_lines &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Ask the user for a list of props&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; props_string &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;input &lt;span class=&quot;token string&quot;&gt;&#39;Enter a list of properties, separated by commas, or leave blank to skip: &#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; props_string &lt;span class=&quot;token operator&quot;&gt;~=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Redefine include_lines, to open the props object&lt;/span&gt; include_lines &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{{ include(&#39;partials/%s&#39;, {&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; props &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;props_string&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;,&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- For each prop, write at the top of the component_lines&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prop &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ipairs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Trim whitespace around prop&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; trimmed_prop &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; table&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;component_lines&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{%% set %s = %s|default(&#39;&#39;) %%}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; trimmed_prop&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; trimmed_prop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; table&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;include_lines&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot; %s: &#39;&#39;,&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; trimmed_prop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Close the props object at the end of the &#39;include&#39; statement&lt;/span&gt; table&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;insert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;include_lines&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;}) }}&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Write the lines to the new template file&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; component_filepath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;templates/partials/%s.twig&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writefile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;component_lines&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; component_filepath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Delete the selected lines&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cmd &lt;span class=&quot;token string&quot;&gt;&quot;&#39;&amp;lt;,&#39;&gt;d&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Insert the include lines&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;start_line &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; include_lines&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;-- Set the key map so that the function runs when the user presses &amp;lt;Leader&gt;et in visual mode&lt;/span&gt; vim&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;nvim_set_keymap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;x&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&amp;lt;Leader&gt;et&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;:lua ExtractTwigTemplateWithProps()&amp;lt;CR&gt;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; noremap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; silent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Here’s another GIF showing how the updated shortcut runs:&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://larryhudson.io/images/neovim-twig/extract-component-with-props.gif&quot; alt=&quot;A screen recording GIF of Larry selecting some code, hitting space then &#39;et&#39;, naming the component &#39;Card&#39;, entering the props &#39;image&#39; and &#39;body&#39;, and the keyboard shortcut running.&quot; /&gt;&lt;/p&gt; &lt;h2 id=&quot;what-else-is-possible&quot;&gt;What else is possible?&lt;/h2&gt; &lt;p&gt;I should point out that I am very new to creating my own shortcuts using Lua in Neovim, so I might not be doing things in the ideal way.&lt;/p&gt; &lt;p&gt;If you’ve made some of these shortcuts yourself, I would love to hear about them. Feel free to reach out.&lt;/p&gt; &lt;p&gt;If you think this sounds interesting but you don’t use Neovim, I would recommend giving it a shot! There is a learning curve when you start using a Vim-based editor, but once you get into the rhythm, you can navigate around code and get things done more efficiently.&lt;/p&gt;</content></entry><entry><title>Focusmate feels like a magic trick for my brain</title><link href="https://larryhudson.io/focusmate/"/><updated>2024-05-18T00:00:00Z</updated><id>https://larryhudson.io/focusmate/</id><content type="html">&lt;p&gt;I have been trying out Focusmate - a web app that matches you with another person for a virtual coworking call - and it is helping me get a lot more done.&lt;/p&gt; &lt;h2 id=&quot;moving-to-paris&quot;&gt;Moving to Paris&lt;/h2&gt; &lt;p&gt;My partner and I are now living in Paris! As of 18 May 2024, we’ve now been here for about two weeks.&lt;/p&gt; &lt;p&gt;I have left my full-time job at &lt;a href=&quot;https://www.informationaccessgroup.com/&quot;&gt;the Information Access Group&lt;/a&gt; in Melbourne, Australia. So I’ve been adjusting to managing my own time, currently split between job search (currently looking for a full stack engineer role!), French language learning and side projects.&lt;/p&gt; &lt;p&gt;It has been a difficult transition in some ways - I’ve felt a slump in motivation. But in the last couple of days I’ve been trying something out which has been helping a lot - the web app &lt;a href=&quot;https://focusmate.com/&quot;&gt;Focusmate&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;what-is-focusmate&quot;&gt;What is Focusmate?&lt;/h2&gt; &lt;p&gt;Focusmate is a web app for virtual coworking - it matches you with another person for a 25 minute video call. At the start of the video call, you say what you are working on this session, then at the end of the session, you say how you went.&lt;/p&gt; &lt;p&gt;During the call, I put my mic on mute, and put the other person’s video in ‘picture-in-picture’ mode in the top right corner of my screen.&lt;/p&gt; &lt;p&gt;It sounds a bit strange to have a video call with a random stranger, but in the ~25 sessions I’ve had so far, everyone has been really nice and encouraging. It feels like a nice little community of likeminded people, who are using the app to help them get stuff done. People are studying for exams, writing press releases, making website wireframes.&lt;/p&gt; &lt;p&gt;I’m surprised how well Focusmate is working for me. Right now, it feels like a magic trick for my brain to click into ‘doing’ mode.&lt;/p&gt; &lt;p&gt;If you struggle with procrastination, or sometimes have a hard time following through on the things that you need or want to do, I would strongly recommend giving it a try.&lt;/p&gt; &lt;h2 id=&quot;how-and-why-does-it-work&quot;&gt;How and why does it work?&lt;/h2&gt; &lt;p&gt;I’m not entirely sure why Focusmate works so well for me, but here are a few ideas off the top of my head. It will be interesting to see if these ideas change over time.&lt;/p&gt; &lt;ul&gt; &lt;li&gt;It feels like Focusmate is helping my brain switch into a more active ‘doing’ mode where I can focus on one thing at a time, whereas before, if I was working on something that I’m not excited about, I would end up scrolling on social media. When I’m in a Focusmate session, I feel like I am staying on track for longer.&lt;/li&gt; &lt;li&gt;For me personally, impressing other people is a big motivator for me. I want to impress the other person at the end of the session when I tell them how I went. I don’t want to let them down.&lt;/li&gt; &lt;li&gt;Because you need to tell the other person what you’re working on at the start of the session, it forces you to clarify your task, at least enough to be able to describe it. It also forces you to pick one thing.&lt;/li&gt; &lt;li&gt;Because the session is only 25 minutes, thinking about what you can get done in 25 minutes, it forces you to break a big task down into smaller, more achievable chunks.&lt;/li&gt; &lt;li&gt;I think working with a total stranger can be a good thing, because all you know about the person is what they are doing for the next 25 mins, and that’s all they know about you. I think this helps the sessions feel light.&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;lets-see-how-long-this-feeling-lasts&quot;&gt;Let’s see how long this feeling lasts&lt;/h2&gt; &lt;p&gt;Right now, I’m feeling really excited about how much I can get done with Focusmate. It feels like more is possible than before.&lt;/p&gt; &lt;p&gt;However, I have been excited about other productivity systems in the past! So I don’t expect this feeling to stay the same forever.&lt;/p&gt; &lt;p&gt;My motivation comes and goes in waves, so it will be interesting to see how Focusmate works for me when the motivation is ebbing.&lt;/p&gt; &lt;p&gt;I’m making a reminder for myself in a months’ time (18 June 2024) to add an update to this post.&lt;/p&gt; &lt;h2 id=&quot;give-it-a-try&quot;&gt;Give it a try&lt;/h2&gt; &lt;p&gt;I don’t think Focusmate is for everyone. Everyone has different things that motivate them. If you don’t struggle with motivation or procrastination, then you probably don’t need to try it.&lt;/p&gt; &lt;p&gt;But from my experience, procrastination is a difficult thing to talk about - it might be something you struggle with but no-one else knows. For example, if you work in a hybrid environment, then you might feel less productive working at home than when you work in the office.&lt;/p&gt; &lt;p&gt;If you ever struggle with these issues, I would strongly recommend giving Focusmate a try. You can do 3 sessions per week for free, or pay US$10 per month for unlimited sessions. It is a bit cheaper if you pay annually.&lt;/p&gt; &lt;p&gt;You can &lt;a href=&quot;https://focusmate.com/?fmreferral=6nPt0Mr3sw&quot;&gt;use my referral link here to get one month of unlimited sessions for free&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;I’m keen to hear what you think. You can &lt;a href=&quot;mailto:larryhudson@hey.com&quot;&gt;email me at larryhudson@hey.com&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;update-after-a-couple-of-months&quot;&gt;Update after a couple of months&lt;/h2&gt; &lt;p&gt;It’s now Wednesday 24 July 2024 - a couple of months after I wrote this post. It took me a while to get around to writing this update.&lt;/p&gt; &lt;p&gt;In the last month or so, I haven’t been using Focusmate as much as I did when it was new to me. The novelty factor has worn off, so it takes more effort for me to book in sessions now.&lt;/p&gt; &lt;p&gt;However, on the days where I use Focusmate, I feel more productive and focused. It’s easier for me to make progress on the things that I need to do, and stay ‘on the rails’, versus when I am just motivating myself.&lt;/p&gt; &lt;p&gt;The challenge for me now is to have the discipline to reach for Focusmate and use it consistently. The difficult thing is, when I need Focusmate the most (when I am feeling unproductive), is when my brain is not reaching for it. So I need to work on that.&lt;/p&gt;</content></entry><entry><title>How text-to-speech can make your content more accessible</title><link href="https://larryhudson.io/iag-text-to-speech/"/><updated>2024-03-13T00:00:00Z</updated><id>https://larryhudson.io/iag-text-to-speech/</id><content type="html">&lt;p&gt;In March 2024, I wrote an article for the &lt;a href=&quot;https://www.informationaccessgroup.com/newsletter.html&quot;&gt;Information Access Group newsletter&lt;/a&gt; about how text-to-speech can make your content more accessible, aimed at content authors and readers.&lt;/p&gt; &lt;p&gt;For this article, we implemented a ‘Listen’ button that allows you to listen to the article while you read. This is the same functionality that the &lt;a href=&quot;https://www.informationaccessgroup.com/our_services/easy_read_html.html&quot;&gt;Easy Read HTML product&lt;/a&gt; uses.&lt;/p&gt; &lt;p&gt;You can &lt;a href=&quot;https://www.informationaccessgroup.com/news/text_to_speech_audio_accessibility.html&quot;&gt;read the article on the Information Access Group website&lt;/a&gt;.&lt;/p&gt;</content></entry><entry><title>A web app for taking screenshots of maps using Protomaps and MapLibre</title><link href="https://larryhudson.io/protomaps-maplibre-screenshot/"/><updated>2024-02-20T00:00:00Z</updated><id>https://larryhudson.io/protomaps-maplibre-screenshot/</id><content type="html">&lt;p&gt;At the start of May, my partner and I will be moving from Australia to live in Paris for a year. We’re really excited! I’ve been trying to learn a bit more French before our move, so I’ve been using the spaced repetition app &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt; to learn vocabulary.&lt;/p&gt; &lt;p&gt;I’m also keen to learn a bit of European geography, to help with navigation. I’ve been using &lt;a href=&quot;https://ankiweb.net/shared/info/1927594591&quot;&gt;this shared Anki deck&lt;/a&gt; to help me memorise the locations of European countries on a map.&lt;/p&gt; &lt;p&gt;For example, where on the map is Croatia?&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://larryhudson.io/images/protomaps-maplibre-screenshot/anki_geography_emptymap.png&quot; alt=&quot;Example of empty Anki flashcard&quot; /&gt;&lt;/p&gt; &lt;details&gt; &lt;summary&gt;Show the answer&lt;/summary&gt; &lt;div&gt; &lt;p&gt;&lt;img src=&quot;https://larryhudson.io/images/protomaps-maplibre-screenshot/anki_geography_croatia.png&quot; alt=&quot;Example of Anki flashcard with Croatia highlighted&quot; /&gt;&lt;/p&gt; &lt;/div&gt; &lt;/details&gt; &lt;p&gt;Last month, I came across the open source mapping libraries &lt;a href=&quot;https://protomaps.com/&quot;&gt;Protomaps&lt;/a&gt; and &lt;a href=&quot;https://maplibre.org/&quot;&gt;MapLibre&lt;/a&gt;. These libraries make it possible to integrate maps into a web app using &lt;a href=&quot;https://www.openstreetmap.org/&quot;&gt;OpenStreetMap’s dataset&lt;/a&gt;. I thought it would be interesting to try making a little web app to generate screenshots of maps with locations highlighted. This could help me learn landmarks in Paris, and make me a bit more confident when travelling.&lt;/p&gt; &lt;p&gt;After a bit of experimentation with the Protomaps and MapLibre libraries, and a lot of help from Bishal Sapkota’s &lt;a href=&quot;https://github.com/bishalspkt/geojson-app&quot;&gt;geojson.app project&lt;/a&gt;, I’ve got a little demo up and running! In the blog post below, I’ll explain how I got it working, and how you can try it out for yourself.&lt;/p&gt; &lt;h2 id=&quot;experimenting-with-mapbox&quot;&gt;Experimenting with Mapbox&lt;/h2&gt; &lt;p&gt;To get a feel for how mapping libraries work in a web app, I started with the &lt;a href=&quot;https://docs.mapbox.com/help/getting-started/web-apps/&quot;&gt;examples on the Mapbox website&lt;/a&gt;. Mapbox is a popular set of APIs and services, and has some good documentation for getting started.&lt;/p&gt; &lt;p&gt;Because my app is a hobby project with only a couple of users, my usage would fit in &lt;a href=&quot;https://www.mapbox.com/pricing&quot;&gt;Mapbox’s free tier&lt;/a&gt;. Mapbox only becomes expensive if your app has more than a few thousand users.&lt;/p&gt; &lt;p&gt;But I wanted to learn more about the open-source mapping libraries &lt;a href=&quot;https://protomaps.com/&quot;&gt;Protomaps&lt;/a&gt; and &lt;a href=&quot;https://maplibre.org/maplibre-gl-js/docs/&quot;&gt;MapLibre&lt;/a&gt;, just for my own knowledge, and to work out what is possible with OpenStreetMap’s dataset.&lt;/p&gt; &lt;h2 id=&quot;poking-around-the-geojsonapp-project&quot;&gt;Poking around the geojson.app project&lt;/h2&gt; &lt;p&gt;&lt;a href=&quot;https://github.com/bishalspkt/geojson-app&quot;&gt;Bishal Sapkota’s geojson.app project&lt;/a&gt; is a great example of a React app using Protomaps and MapLibre:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;It uses MapLibre for the interactive map on the frontend. MapLibre renders the map into a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element and visualises the GeoJSON data using markers, lines and polygons.&lt;/li&gt; &lt;li&gt;It uses Protomaps to self-host the vector map tiles. As you navigate around the map, it loads the tile data from tiles.geojson.app.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;You can learn more about the project in &lt;a href=&quot;https://www.youtube.com/watch?v=btaqCJSIG-E&quot;&gt;Bishal’s recent talk ‘Beyond Mainstream Maps’ at MelbJS&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;I learned a lot by poking around the source code of Bishal’s project. By learning how to get a map working, and how to draw markers and polygons, I was able to get a head start on my own project.&lt;/p&gt; &lt;h2 id=&quot;self-hosting-the-vector-map-tiles&quot;&gt;Self-hosting the vector map tiles&lt;/h2&gt; &lt;p&gt;In order to get my own app up and running, I needed to self-host my own vector map tiles. Here’s what the process involved:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;using the &lt;a href=&quot;https://docs.protomaps.com/pmtiles/cli#extract&quot;&gt;pmtiles CLI&lt;/a&gt; to extract the vector map data for the Paris region, from the global OpenStreetMap dataset. The pmtiles file for the whole world is about 110GB, but the Paris region is only 10MB.&lt;/li&gt; &lt;li&gt;uploading my pmtiles file to a Cloudflare R2 bucket using the rclone CLI.&lt;/li&gt; &lt;li&gt;creating a Cloudflare worker to serve the HTTP requests on a subdomain. When the requests come in to pmtiles.larryhudson.net, the Cloudflare worker renders the data from the pmtiles file in the bucket.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;I followed &lt;a href=&quot;https://docs.protomaps.com/deploy/cloudflare&quot;&gt;these instructions on the Protomaps website&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;I had a little bit of difficulty getting this working. One step that I missed initially, was setting the ‘ALLOWED_ORIGINS’ environment variable, so that requests from my &lt;code&gt;localhost:4321&lt;/code&gt; development server would be allowed. Overall, this process took me around 30 mins to set up, so it wasn’t too bad.&lt;/p&gt; &lt;h2 id=&quot;openstreetmaps-search-api&quot;&gt;OpenStreetMap’s search API&lt;/h2&gt; &lt;p&gt;OpenStreetMap has a &lt;a href=&quot;https://nominatim.openstreetmap.org/&quot;&gt;search API called Nominatim&lt;/a&gt; that allows you to search for locations and get their coordinates. It makes it fairly easy to lookup data for a specific place or region, and get GeoJSON data that can be visualised on a map. For example, &lt;a href=&quot;https://nominatim.openstreetmap.org/search?q=18th%20arrondissement%20paris&amp;amp;format=geojson&amp;amp;polygon_geojson=1&quot;&gt;here’s a search URL to look up the polygon data for the 11th arrondissement of Paris&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;OpenStreetMap allows developers to use the Nominatim API for free, but their usage policy only allows one request per second. While this is ok for a hobby project, if you need to make more requests, they have &lt;a href=&quot;https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_/_Third-party_providers&quot;&gt;alternatives on their wiki&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;OpenStreetMap is a real gift for developers who want to do interesting things with maps - I’m really interested in experimenting more.&lt;/p&gt; &lt;p&gt;It was fairly straightforward integrating a search box into my web app. Because the Nominatim API returns GeoJSON data, I was able to visualise that GeoJSON data using the &lt;a href=&quot;https://github.com/bishalspkt/geojson-app/blob/27f5960212ec66ee1e5669050885f822046cf79c/src/lib/map-utils.tsx#L68C1-L84C2&quot;&gt;functionality within Bishal’s example&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;generating-images-from-canvas-elements&quot;&gt;Generating images from ‘canvas’ elements&lt;/h2&gt; &lt;p&gt;The last part of the puzzle was taking screenshots of maps, so that I can use them in Anki flashcards.&lt;/p&gt; &lt;p&gt;I’ve recently been playing with the &lt;a href=&quot;https://html2canvas.hertzen.com/&quot;&gt;html2canvas library&lt;/a&gt; after finding out about it in &lt;a href=&quot;https://github.com/walpolea/andrewwalpole.com/blob/master/src/pages/preview/%5Bslug%5D.astro&quot;&gt;Andrew Walpole’s great example for generating social media images for blog posts here&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;It turns out, if you can render a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element, it’s fairly easy to turn that into a screenshot using JavaScript. You just need to get the &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element from the DOM and then do &lt;code&gt;canvas.toDataURL(&#39;image/png&#39;)&lt;/code&gt;.&lt;/p&gt; &lt;p&gt;Because the MapLibre library renders the map as a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element, I was almost there. I just needed to enable the &lt;code&gt;preserveDrawingBuffer&lt;/code&gt; option so that the canvas would be able to be rendered to an image. This option is disabled by default for performance reasons.&lt;/p&gt; &lt;p&gt;One ‘gotcha’ that I ran into was with map markers - if I added markers using the MapLibre library, they were not rendered in the canvas, because they are added as separate DOM elements. To get around this, I added a ‘layer’ for each marker to the map. You can &lt;a href=&quot;https://github.com/larryhudson/astro-protomaps-maplibre-screenshot/blob/6627c158774bf0bf4d5c080511136767d1550f50/src/utils/map-utils.ts#L117C13-L129C16&quot;&gt;see that in the source code here&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;an-example-flashcard&quot;&gt;An example flashcard&lt;/h2&gt; &lt;p&gt;Where on the map is the Arc de Triomphe?&lt;/p&gt; &lt;p&gt;&lt;img src=&quot;https://larryhudson.io/images/protomaps-maplibre-screenshot/paris_empty.png&quot; alt=&quot;Map of Paris with no markers&quot; /&gt;&lt;/p&gt; &lt;details&gt; &lt;summary&gt;Show the answer&lt;/summary&gt; &lt;div&gt; &lt;p&gt;&lt;img src=&quot;https://larryhudson.io/images/protomaps-maplibre-screenshot/arc_compressed.png&quot; alt=&quot;Map of Paris with a marker on the Arc de Triomphe&quot; /&gt;&lt;/p&gt; &lt;/div&gt; &lt;/details&gt; &lt;h2 id=&quot;video-walkthrough-and-github-repo&quot;&gt;Video walkthrough and GitHub repo&lt;/h2&gt; &lt;p&gt;You can view a video walkthrough of the web app here:&lt;/p&gt; &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube-nocookie.com/embed/FGsWjBhFo3c?si=lI5A3UE_wWVhbEtv&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt; &lt;p&gt;You can also &lt;a href=&quot;https://github.com/larryhudson/astro-protomaps-maplibre-screenshot/&quot;&gt;explore the source code for the project on GitHub&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;let-me-know-what-you-think&quot;&gt;Let me know what you think&lt;/h2&gt; &lt;p&gt;If this is interesting to you, I’d love to hear what you think. Have you made something similar? Do you have any other ideas for interesting use cases for mapping in a web app?&lt;/p&gt; &lt;p&gt;Feel free to reach out on &lt;a href=&quot;https://www.linkedin.com/in/larryhudson4/&quot;&gt;LinkedIn&lt;/a&gt; or &lt;a href=&quot;https://twitter.com/larryhudsondev&quot;&gt;Twitter&lt;/a&gt;.&lt;/p&gt;</content></entry><entry><title>How can we use AI to do better thinking, not skip thinking?</title><link href="https://larryhudson.io/ai-thinking-and-output/"/><updated>2024-02-15T00:00:00Z</updated><id>https://larryhudson.io/ai-thinking-and-output/</id><content type="html">&lt;p&gt;&lt;a href=&quot;https://www.oneusefulthing.org/p/what-can-be-done-in-59-seconds-an&quot;&gt;Ethan Mollick recently wrote an article demonstrating what you can do with AI in under a minute&lt;/a&gt;. He shows that you can write a product launch, plan a course syllabus, design a kitchen and do market research all in under a minute. It’s a great demonstration of what we can generate with AI tools today.&lt;/p&gt; &lt;p&gt;I thought this was a great insight from Ethan’s article:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;When a middle manager writes a weekly report on the status of a major initiative, the report may not be the point. Instead, it serves as a signal that the middle manager has done their job, speaking to the relevant employees, keeping an eye on the status of the project, and making corrections as needed. And it has always worked well enough - a senior manager could tell at a glance if the report was seemingly substantive (showing effort) and well-written (showing quality). But now every employee with Copilot can produce work that checks all the boxes of a formal report without necessarily representing underlying effort.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;I think this is a big deal - while previously the ‘output’ (the report or summary) was proof of underlying thinking, now these outputs can be generated without much thinking at all. While these AI tools allow us to jump straight to the end result, they work better when we do more thinking and give them more context. In this blog post, I want to focus on how AI tools can help us do better thinking, rather than skip thinking.&lt;/p&gt; &lt;h2 id=&quot;thinking-and-output&quot;&gt;Thinking and output&lt;/h2&gt; &lt;p&gt;When thinking about work, it can be useful to break a task into two components:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;thinking: considering the problem, making a plan, making decisions&lt;/li&gt; &lt;li&gt;output: a report, a presentation, a meeting agenda.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;There are certain tasks that don’t require much thinking. You might have a meeting agenda that you need to prepare. You know the topics that need to be covered, and you know the order that they need to be covered in. If you have a template for the meeting agenda, the work is mostly about filling in the blanks.&lt;/p&gt; &lt;p&gt;There are other tasks that require more thinking. You might have an idea for a new way of working that will save time. You need to think about the potential benefits and challenges of using this new approach, and write a pitch to present to your manager.&lt;/p&gt; &lt;p&gt;With the recent wave of AI tools such as ChatGPT, people can jump straight to the output without doing much thinking. While this means we can get things done a lot faster, we need to be careful to not skip the ‘thinking’ part.&lt;/p&gt; &lt;h2 id=&quot;ai-can-create-better-outputs-if-you-do-more-thinking&quot;&gt;AI can create better outputs if you do more thinking&lt;/h2&gt; &lt;p&gt;For an AI to create a good output, it needs to be given contextual information in its prompt. Without the proper context, it will generate a generic output that might not be relevant to your specific situation. Here’s an example of a generic prompt:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;Write a list of pros and cons of using Jira for task management.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;If you take some time to think about the task, prepare some rough notes, and then use those notes in your prompt, the AI will be able to generate a much better output. Here’s an example of a more contextual prompt:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;I’m considering using Jira for task management. I work at an agency where we make websites for clients. Some of our team members are not technical and some are. We are currently organising tasks using a combination of Trello and Google Sheets. We are having trouble keeping track of tasks and changing deadlines. We need software that can help us manage tasks, deadlines, and deal with external clients. Can you write a list of pros and cons of using Jira for task management in this context?&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;Tools like ChatGPT can really shine when you give them more context. I think this is what sets them apart from traditional search engines.&lt;/p&gt; &lt;h2 id=&quot;ai-tools-can-also-help-with-the-thinking-part&quot;&gt;AI tools can also help with the thinking part&lt;/h2&gt; &lt;p&gt;While AI tools are great for generating outputs, they can also help you with the thinking part.&lt;/p&gt; &lt;p&gt;If you’re trying to decide between two different approaches, you can ask ChatGPT to write a summary arguing for one approach, and another summary arguing for the other approach. You can then read through those outputs and think about them deeply. In this way, ChatGPT is not making the decision for you, but is helping you to think about the problem from different angles.&lt;/p&gt; &lt;p&gt;ChatGPT can also generate a list of ideas for you to consider. For example, if you want to create an app that helps people save money, you could ask ChatGPT to generate a list of 20 different app ideas that help people save money. In this way, ChatGPT can be more like a &lt;a href=&quot;https://components.ai/about&quot;&gt;generative design tool&lt;/a&gt; that prepares many options or combinations of ideas.&lt;/p&gt; &lt;p&gt;I have been using the brainstorming app &lt;a href=&quot;https://brainstory.ai/&quot;&gt;Brainstory&lt;/a&gt; to help me think deeper about my ideas. In the app, an AI coach asks me thought-provoking questions and I answer the questions using my voice. The app transcribes the conversation and generates a concise summary. I think this is a great example of how AI can help users think deeper, rather than skip thinking.&lt;/p&gt; &lt;p&gt;It’s worth noting that it is still early days for AI in the workplace. As time goes on, AI tools are going to do more and more of the thinking for us. Tasks that require ‘thinking’ today, such as finding relevant contextual information and making connections between different pieces of information, will be completed by AI in the future. So our concept of ‘thinking’ will keep changing over time.&lt;/p&gt; &lt;h2 id=&quot;let-me-know-what-you-think&quot;&gt;Let me know what you think&lt;/h2&gt; &lt;p&gt;I think it’s fascinating to think about how AI tools are changing the way we work.&lt;/p&gt; &lt;p&gt;If this is interesting to you, feel free to reach out on &lt;a href=&quot;https://www.linkedin.com/in/larryhudson4/&quot;&gt;LinkedIn&lt;/a&gt; or &lt;a href=&quot;https://twitter.com/larryhudsondev&quot;&gt;Twitter&lt;/a&gt;. I’d love to hear what you think, and if you have any ideas for using AI tools to help you think better.&lt;/p&gt;</content></entry><entry><title>Using Weaviate&#39;s generative search for brainstorming</title><link href="https://larryhudson.io/astro-weaviate-brainstorm/"/><updated>2024-02-10T00:00:00Z</updated><id>https://larryhudson.io/astro-weaviate-brainstorm/</id><content type="html">&lt;p&gt;Lately I’ve been interested in generative search - the idea of searching through a database of text items by semantic similarity and then generating new text based on the search results. I think this is a really powerful idea.&lt;/p&gt; &lt;p&gt;I’ve created &lt;a href=&quot;https://github.com/larryhudson/astro-weaviate-brainstorm&quot;&gt;astro-weaviate-brainstorm&lt;/a&gt; - an example web app that demonstrates this concept using the &lt;a href=&quot;https://astro.build/&quot;&gt;Astro framework&lt;/a&gt; and the &lt;a href=&quot;https://weaviate.io/&quot;&gt;Weaviate vector database&lt;/a&gt;. The web app integrates vector search, which facilitates text search based on semantic meaning, with a large language model to generate new text based on search results.&lt;/p&gt; &lt;p&gt;This project is heavily inspired by the brainstorming app &lt;a href=&quot;https://brainstory.ai/&quot;&gt;Brainstory&lt;/a&gt;, a web app that guides you through a brainstorming session by asking thought-provoking questions. At the end of the brainstorming session, it generates a concise summary for you. This encourages you to think deeper and can help you turn your idea into something tangible and shareable. If you want a real brainstorming app, try out Brainstory!&lt;/p&gt; &lt;p&gt;In this blog post, I’ll introduce the idea of generative search, and then talk about how you can get started trying out the astro-weaviate-brainstorm project.&lt;/p&gt; &lt;h2 id=&quot;what-is-generative-search&quot;&gt;What is generative search?&lt;/h2&gt; &lt;p&gt;Generative search (also called Retrieval Augmented Generation or RAG) is a concept that uses vector search to find semantically similar text in a database, and then uses a large language model to generate new text based on the search results. Let’s break that down a bit:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;vector search is a way to search through a database of text items by semantic similarity, rather than exact keywords. For example, the text strings “I love dogs” and “I love cats” are very similar semantically. Vector search can find these similarities between text items, and can make it easier to find related items to a search query.&lt;/li&gt; &lt;li&gt;a large language model is a machine learning model that can generate new text based on a prompt. For example, if you give it the prompt “Summarise the key ideas from this text” along with a piece of text, it can generate a summary of the text. This is the technology that is behind ChatGPT.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;By combining vector search with a large language model, generative search can be a really powerful tool. In the context of a brainstorming app, it could be used to:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;find similar ideas in different brainstorms&lt;/li&gt; &lt;li&gt;find insights or connections between brainstorms that you might not think are related.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;You can &lt;a href=&quot;https://weaviate.io/developers/weaviate/search/generative&quot;&gt;find out more about generative search in the Weaviate docs&lt;/a&gt;.&lt;/p&gt; &lt;h2 id=&quot;an-example-web-app-using-astro-and-weaviate&quot;&gt;An example web app using Astro and Weaviate&lt;/h2&gt; &lt;p&gt;I’ve created a new side project called &lt;a href=&quot;https://github.com/larryhudson/astro-weaviate-brainstorm&quot;&gt;astro-weaviate-brainstorm&lt;/a&gt; as a way to explore the possibilities of generative search. It’s a fairly simple web app using the &lt;a href=&quot;https://astro.build/&quot;&gt;Astro web app framework&lt;/a&gt; and the &lt;a href=&quot;https://weaviate.io/&quot;&gt;Weaviate vector database&lt;/a&gt;. The web app demonstrates the concept of generative search, and how it can be used in a brainstorming app.&lt;/p&gt; &lt;p&gt;&lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; is my favourite way to create simple web apps using Node.js. The framework’s &lt;a href=&quot;https://docs.astro.build/en/basics/astro-components/&quot;&gt;&lt;code&gt;.astro&lt;/code&gt; component files&lt;/a&gt; make it really easy to write pages and reusable components using HTML and CSS. Astro’s ‘fenced’ section at the top of the file &lt;code&gt;---&lt;/code&gt; component syntax makes it easy to include server-side logic at the top of the file, which makes it easy to work with HTML forms.&lt;/p&gt; &lt;p&gt;&lt;a href=&quot;https://weaviate.io/&quot;&gt;Weaviate&lt;/a&gt; is an open source vector database that can be used to store and search through text items by semantic similarity. While it can be deployed using Docker, for this project I am using Weaviate’s &lt;a href=&quot;https://weaviate.io/developers/weaviate/installation/embedded#embedded-options&quot;&gt;embedded client&lt;/a&gt;, which runs the database within the same process as the Astro web app. This is a great fit for small projects where you don’t want to set up more complicated infrastructure. All you need to bring is an &lt;a href=&quot;https://openai.com/&quot;&gt;OpenAI&lt;/a&gt; API key, which generates the &lt;a href=&quot;https://platform.openai.com/docs/guides/embeddings&quot;&gt;text embeddings&lt;/a&gt; and powers the large language model.&lt;/p&gt; &lt;h2 id=&quot;getting-started-with-astro-weaviate-brainstorm&quot;&gt;Getting started with astro-weaviate-brainstorm&lt;/h2&gt; &lt;p&gt;To get the project running on your computer:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;clone &lt;a href=&quot;https://github.com/larryhudson/astro-weaviate-brainstorm&quot;&gt;this git repository&lt;/a&gt;&lt;/li&gt; &lt;li&gt;change into the project directory and run &lt;code&gt;npm install&lt;/code&gt; to install the Node.js dependencies&lt;/li&gt; &lt;li&gt;duplicate the sample &lt;code&gt;.env.sample&lt;/code&gt; file and rename it to &lt;code&gt;.env&lt;/code&gt;, and then add your OpenAI API key to the &lt;code&gt;.env&lt;/code&gt; file&lt;/li&gt; &lt;li&gt;run &lt;code&gt;npm run dev&lt;/code&gt; to start the local development server&lt;/li&gt; &lt;li&gt;open your web browser and navigate to &lt;a href=&quot;http://localhost:4321/&quot;&gt;http:/localhost:4321&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;h2 id=&quot;video-walkthrough&quot;&gt;Video walkthrough&lt;/h2&gt; &lt;p&gt;I’ve also recorded a video walkthrough where I talk through the idea and demonstrate a few things with the web app. I’m trying out a few different ways to share my side projects, so let me know if you like the video format. You can &lt;a href=&quot;https://www.youtube.com/watch?v=aUSLy2p5RkE&quot;&gt;view the video on YouTube&lt;/a&gt; below:&lt;/p&gt; &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube-nocookie.com/embed/aUSLy2p5RkE?si=hwjzjLF2g-hnEbGl&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt; &lt;h2 id=&quot;let-me-know-if-this-is-interesting-to-you&quot;&gt;Let me know if this is interesting to you&lt;/h2&gt; &lt;p&gt;I’m keen to hear if this concept is interesting to you, and if you have any ideas for how it could be used. I think generative search could be a really powerful tool for brainstorming, and I’m excited to explore it further.&lt;/p&gt; &lt;p&gt;If this idea is interesting to you, feel free to share it on &lt;a href=&quot;https://twitter.com/larryhudsondev/status/1756086534602883464&quot;&gt;Twitter&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:7161871407037997056/&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;</content></entry><entry><title>Active reading with Readwise, Brainstory and Anki</title><link href="https://larryhudson.io/active-reading-readwise-brainstory-anki/"/><updated>2024-01-28T00:00:00Z</updated><id>https://larryhudson.io/active-reading-readwise-brainstory-anki/</id><content type="html">&lt;p&gt;Lately I’ve been interested in active reading techniques and how we can turn the interesting information that we read into useful knowledge.&lt;/p&gt; &lt;p&gt;A lot of these ideas are influenced by &lt;a href=&quot;http://augmentingcognition.com/ltm.html&quot;&gt;Michael Nielsen’s article &lt;em&gt;Augmenting Long-term Memory&lt;/em&gt;&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/watch?v=dmeRQN9z504&quot;&gt;Andy Matuschak’s ideas about self-guided learning&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;In this blog post, I’ll talk about a few apps that I’ve been using to help me read more actively, think through ideas with brainstorming, and resurface those ideas with spaced repetition: &lt;a href=&quot;https://readwise.io/&quot;&gt;Readwise&lt;/a&gt;, &lt;a href=&quot;http://brainstory.ai/&quot;&gt;Brainstory&lt;/a&gt; and &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt;. By using these tools, it’s possible to get more value out of the content that we read.&lt;/p&gt; &lt;h2 id=&quot;readwise-is-great-for-active-reading&quot;&gt;Readwise is great for active reading&lt;/h2&gt; &lt;p&gt;&lt;a href=&quot;https://readwise.io/&quot;&gt;Readwise&lt;/a&gt; is an app that lets you save highlights from the books, articles or documents you’re reading. The app then resurfaces those highlights over time, so you can retain the insights and important information.&lt;/p&gt; &lt;p&gt;Readwise has a separate app called &lt;a href=&quot;https://read.readwise.io/&quot;&gt;Readwise Reader&lt;/a&gt;, which allows you to store articles, documents and other types of content that you want to read. It gives you a nice interface to do that reading across your devices, and makes it easy to highlight important information, which syncs over to the main Readwise app.&lt;/p&gt; &lt;p&gt;As an aside, Readwise Reader’s mobile app also has a great text to speech function, which means you can listen to your articles while you go for a walk or do the dishes. This is huge for me - it’s actually quite similar to my &lt;a href=&quot;https://github.com/larryhudson/astro-sqlite-tts-feed&quot;&gt;astro-sqlite-tts-feed&lt;/a&gt; side project that I have been working on.&lt;/p&gt; &lt;p&gt;Readwise Reader works great with longform YouTube videos too. When you add a YouTube video, the video plays in an embedded player at the top of the screen, and a transcript automatically scrolls below the video, where you can quickly highlight important information. This makes video watching more active, and makes it easier to save the key points. I did this with &lt;a href=&quot;https://www.youtube.com/watch?v=dmeRQN9z504&quot;&gt;Dwarkesh Patel’s interview with Andy Matuschak&lt;/a&gt;. I was able to save quite a few insights from their discussion about self-guided learning.&lt;/p&gt; &lt;h2 id=&quot;thinking-deeper-with-brainstory&quot;&gt;Thinking deeper with Brainstory&lt;/h2&gt; &lt;p&gt;&lt;a href=&quot;http://brainstory.ai/&quot;&gt;Brainstory&lt;/a&gt; is a web app that guides you through a brainstorming session by asking thought-provoking questions. At the end of the brainstorming session, it generates a concise summary for you. This encourages you to think deeper and can help you turn your idea into something tangible and shareable.&lt;/p&gt; &lt;p&gt;&lt;a href=&quot;https://youtu.be/dmeRQN9z504?t=361&quot;&gt;In this Andy Matuschak interview, he mentions that the most important rule of active reading is asking questions about the content and answering them&lt;/a&gt;. When he mentioned that, I immediately thought of Brainstory, which I’ve been playing around with for the last couple of months.&lt;/p&gt; &lt;p&gt;I think Brainstory can be a useful tool in the active reading process. If you highlight interesting information in an article, you can copy and paste those highlights into the start of a brainstorming session, and then ask Brainstory to help you think through each highlight.&lt;/p&gt; &lt;p&gt;The process of thinking through each highlight can help you transform the content from something passive that someone else has written, into something that is more relevant and applicable to your own experience. For example, I did a Brainstory about the highlights in that Andy Matuschak video, and it helped me relate one of the insights to my own experience. You can read that Brainstory here.&lt;/p&gt; &lt;p&gt;I’m hoping that by transforming the original ideas into something new, I can retain the information more. If you’re interested, you can &lt;a href=&quot;https://app.brainstory.ai/feedback?share=98963a5d-c158-40ea-bc3b-2dbcbf4859c4&quot;&gt;read my Brainstory about the Andy Matuschak video here&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;This is just one possible use case for Brainstory. I think the idea of a guided brainstorming session, which helps you think deeper and then generates an ‘output’ summary that you can share and get feedback on, is really powerful. I’m excited to play around more with it and see where else it could be useful.&lt;/p&gt; &lt;h2 id=&quot;spaced-repetition-with-anki-for-resurfacing-ideas&quot;&gt;Spaced repetition with Anki for resurfacing ideas&lt;/h2&gt; &lt;p&gt;One more thing that I’ve been exploring is spaced repetition for memorisation and resurfacing ideas. With an app like &lt;a href=&quot;https://apps.ankiweb.net/&quot;&gt;Anki&lt;/a&gt;, you can create flashcards and test yourself, and the app will retest you over longer and longer periods of time, to help the information stay in your long-term memory. If this is interesting to you, I’d recommend checking out the article &lt;a href=&quot;http://augmentingcognition.com/ltm.html&quot;&gt;&lt;em&gt;Augmenting Long-term Memory&lt;/em&gt;&lt;/a&gt;. Use &lt;a href=&quot;https://read.readwise.io/&quot;&gt;Readwise Reader&lt;/a&gt; if you want to listen to it using text to speech.&lt;/p&gt; &lt;p&gt;I think spaced repetition is another important part of the puzzle when it comes to active reading. After you read some material and highlight the interesting parts, and then ask yourself questions, you need to revisit the material in order for it to stay in long-term memory.&lt;/p&gt; &lt;p&gt;While spaced repetition is most popular for learning foreign language vocab, I’m keen to explore how useful it could be for resurfacing ideas in order to make new connections. For example, a few topics that I’m interested in are asynchronous communication in the workplace, using text-to-speech to listen to text content, and productivity techniques to do focused work. If I have flashcards related to these ideas, and I’m constantly circulating these ideas, then that could help me make new connections and insights.&lt;/p&gt; &lt;h2 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;/h2&gt; &lt;p&gt;I’m pretty excited about the potential of combining active reading with Readwise, deeper thinking with Brainstory and spaced repetition with Anki. Hopefully it will help me get more value out of the information that I’m reading.&lt;/p&gt; &lt;p&gt;I actually planned this blog post in a Brainstory session. Brainstory has a great feedback function where you can share your brainstorm, and then other people can do a Brainstory in response. If the ideas in this blog post are interesting to you, you can &lt;a href=&quot;https://app.brainstory.ai/feedback?share=cb43ee54-9377-4ea6-9da8-6590cc447b79&quot;&gt;do a Brainstory in response here&lt;/a&gt;. It will be a guided brainstorming session where it will ask you thought-provoking questions.&lt;/p&gt;</content></entry><entry><title>Turning a scanned PDF into an audiobook with Azure Document Intelligence and OpenAI APIs</title><link href="https://larryhudson.io/pdf-to-audiobook-document-intelligence-gpt4-openai/"/><updated>2023-12-17T00:00:00Z</updated><id>https://larryhudson.io/pdf-to-audiobook-document-intelligence-gpt4-openai/</id><content type="html">&lt;p&gt;I’ve been experimenting with turning scanned PDFs into audiobooks using:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;the &lt;a href=&quot;https://azure.microsoft.com/en-au/products/ai-services/ai-document-intelligence&quot;&gt;Microsoft Document Intelligence API&lt;/a&gt; to extract text from the PDF&lt;/li&gt; &lt;li&gt;the &lt;a href=&quot;https://openai.com/gpt-4&quot;&gt;OpenAI GPT-4 API&lt;/a&gt; to clean up the text&lt;/li&gt; &lt;li&gt;the &lt;a href=&quot;https://platform.openai.com/docs/guides/text-to-speech&quot;&gt;OpenAI Text-to-Speech API&lt;/a&gt; to turn the cleaned text into audio.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;I’ve found these APIs work really well together, and I’m excited about the potential here. If you like listening to long-form content, then I think this method is worth exploring.&lt;/p&gt; &lt;p&gt;In this post, I’ll walk through how I’m using these APIs to turn a scanned PDF into an audiobook. For each part, I’ll share a Node.js code example.&lt;/p&gt; &lt;h2 id=&quot;extracting-text-from-a-pdf-using-the-document-intelligence-api&quot;&gt;Extracting text from a PDF using the Document Intelligence API&lt;/h2&gt; &lt;p&gt;Microsoft’s Document Intelligence API makes it pretty straightforward to extract text from a PDF. I’ve been really impressed with the quality of the text extraction even when the PDF is a low quality scan.&lt;/p&gt; &lt;p&gt;You can have a play with the API using the &lt;a href=&quot;https://documentintelligence.ai.azure.com/studio&quot;&gt;Document Intelligence Studio&lt;/a&gt; in your web browser.&lt;/p&gt; &lt;p&gt;To get started using the Document Intelligence API, you’ll need to set up an &lt;a href=&quot;https://azure.microsoft.com/en-au/&quot;&gt;Azure&lt;/a&gt; account and create a new ‘resource’ for the Document Intelligence API.&lt;/p&gt; &lt;p&gt;It has a free tier that allows you to extract text from 500 pages per month. You can &lt;a href=&quot;https://azure.microsoft.com/en-au/pricing/details/ai-document-intelligence/&quot;&gt;find out more information about pricing on the Azure website&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;A few things to note here:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;the Document Intelligence API only extracts text from a single page at a time, so in my Node script, I create a new PDF for each page using the &lt;code&gt;pdf-lib&lt;/code&gt; library, then send each ‘page buffer’ to the API.&lt;/li&gt; &lt;li&gt;in my Node script, I write a text file for each page of the PDF, so that each text file can be sent to the GPT-4 API for correction.&lt;/li&gt; &lt;li&gt;rather than extracting text from every page in the PDF, I split up the original PDF into chapters. I didn’t want to use up the free quota too quickly, so I only extracted text from the pages that I needed.&lt;/li&gt; &lt;/ul&gt; &lt;details&gt; &lt;summary&gt;Node.js code example for extracting text from a PDF&lt;/summary&gt; &lt;div&gt; &lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PDFDocument &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pdf-lib&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; DocumentAnalysisClient&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; AzureKeyCredential&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;@azure/ai-form-recognizer&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dotenv/config&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; pMap &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;p-map&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Example usage:&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// node extract-text-from-pdf.js ./my-pdf.pdf&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;argv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; extractedPages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractTextFromFullPdf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdfFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.pdf&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;writeTextFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;extractedPages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pdfFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// this function takes a PDF path, and for each page, creates an individual PDF&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// and extracts the text using the Document Intelligence API&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractTextFromFullPdf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pdfPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fullPdfBytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fullPdfDoc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; PDFDocument&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fullPdfBytes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pageCount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fullPdfDoc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPageCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pageNumbers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Array&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; pageCount &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;_&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; extractedPages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; pageNumbers&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pageNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdfPageBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getPdfForIndividualPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pageNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdfData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractTextFromPagePdf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pdfPageBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pdfData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pageNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;concurrency&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; extractedPages&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// the Document Intelligence API only supports extracting text from a single&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// page at a time, so this function creates a new PDF for a single page&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getPdfForIndividualPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pageNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; existingPdfBytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fullPdfDoc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; PDFDocument&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;existingPdfBytes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; singlePagePdf &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; PDFDocument&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pageIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pageNumber &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;copiedPage&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; singlePagePdf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;copyPages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fullPdfDoc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pageIndex&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; singlePagePdf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;copiedPage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; singlePagePdfBytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; singlePagePdf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; singlePagePdfBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;singlePagePdfBytes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; singlePagePdfBuffer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// this function takes a PDF buffer and sends it to the Document Intelligence&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// API and returns the result&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractTextFromPagePdf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pdfPageBuffer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// environment variables should be in .env file&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; endpoint &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; apiKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;AZURE_DOCUMENT_INTELLIGENCE_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; documentAnalysisClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DocumentAnalysisClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; endpoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AzureKeyCredential&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;apiKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; poller &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; documentAnalysisClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;beginAnalyzeDocument&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;prebuilt-read&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pdfPageBuffer&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdfDataResult &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; poller&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pollUntilDone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; pdfDataResult&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// this function writes a text file for each page of the PDF&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;writeTextFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;extractedPages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pdfFilename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputFolderName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;extracted-pages&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputFolderName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputFolderName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pageNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; extractedPages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; txtFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;pdfFilename&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;pageNumber&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.txt&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; txtFilePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputFolderName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; txtFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;txtFilePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;/div&gt; &lt;/details&gt; &lt;h2 id=&quot;cleaning-up-the-extracted-text-with-gpt-4&quot;&gt;Cleaning up the extracted text with GPT-4&lt;/h2&gt; &lt;p&gt;While the Document Intelligence API does a good job of extracting text from the page, there can be a few issues with the extracted text:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;if sections of the page are blurry, the text can be garbled&lt;/li&gt; &lt;li&gt;if the scanned page contains characters that shouldn’t be on the page (eg. they are on the edge of an adjacent page), then those characters will show up in the extracted text.&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;Here’s a screenshot of a page that includes these issues: &lt;img src=&quot;https://larryhudson.io/images/krautrocksampler_page13.jpg&quot; alt=&quot;A screenshot of a page from the book Krautrocksampler by Julian Cope. The left side of the page is blurry, and in the top left corner there are extra characters from the previous page.&quot; /&gt;&lt;/p&gt; &lt;p&gt;Here’s what the Document Intelligence API extracted from that page. For quite a few lines, the start of the line is garbled, and there are extra characters from the previous page:&lt;/p&gt; &lt;pre&gt;&lt;code&gt;of Underground, Collapsing and the double-album Disaster ot. - all came from it. Some claim that they split from music altoes- gherto continue in a purely political way, but kept up the illu- sion for years with seemingly new LPs. The first Amon Düül records are extraordinary classics and extremely raw, like coned Orcs playing neverending versions of The Mothers&#39; Return of the Son of Monster Magnet&amp;quot; and the Stooges at LA.Blues&amp;quot;. But they are dosed with a higher level of vibe than es any other freakout records - relentless, uplifting and full of the le crodest gimmicks that all work perfectly. Amon Düül did not re stay long, but they laid the beginnings of Krautrock with their ic music, and with one particular song on Psychedelic Undera emund. The name of the song translated as &amp;quot;Mama Düül and it er Sauerkraut band Start Up!&amp;quot; With that title, the lazy British d S ock press at last had something to latch on to. Aha, we&#39;ll call it Krautrock ... e 1 e The First Rumblings of Kosmische Music W. Germany was full of supposed &#39;head&#39; groups by now. But any of them still did not sound remotely German, slavishly trying to be Hard Rach !!! Others, like Embryo, Emergency and Birth Control, mixed obvious Teutonics into unsuccessful fusions with British/American rock and jazz. But in the mean- ame, Amon Düül II, the musical half of the commune, had recorded an amazing free-flowing LP called Phallus Dei, for the British Liberty label. Its overtly mysterious sleeve first con- Booted me when I was 13 and standing in Tamworth Wool- worth&#39;s. I was with my Welsh grandfather, whom I asked about the meaning of Phallus Dei. &amp;quot;Bloody Hell, Don&#39;t tell your mother.&amp;quot; he snorted. &amp;quot;That means God&#39;s cock!&amp;quot; And with the release of that 20 minute title track, both branches of Amon Dual had proved their commitment to the new cosmic political commune scene. This record was very extreme, both the chim- og sound and dizzy two-colour sleeve like something from the 13th Floor Elevators&#39; International Artists label in Texas. And something else again was stirring in Cologne. The Stockhausen/Psychedelia-inspired Can were now a five-piece recording at Schloss Norvenich, the castle home of their patron, 13 ` &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;I experimented with how to clean up the text, including using the GPT-4 Vision API to try to extract text from blurred sections. The Vision API is very powerful but it’s not suited to straightforward text extraction - it is better suited to asking questions about an image.&lt;/p&gt; &lt;p&gt;In the end, I found that asking GPT-4 to clean up the extracted text worked pretty well. The system prompt that I landed on was:&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;The supplied text has been extracted from a blurry page. There may be characters from the edge of adjacent pages that need to be deleted. Correct the text so that it makes sense.&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;Here’s the cleaned up text for the example page above:&lt;/p&gt; &lt;pre style=&quot;white-space: pre-wrap;&quot;&gt; &quot;Underground, Collapsing&quot; and the double-album &quot;Disaster&quot; all came from it. Some claim that they split from music altogether to continue in a purely political way but kept up the illusion for years with seemingly new LPs. The first Amon Düül records are extraordinary classics and extremely raw, like stoned Orcs playing never-ending versions of The Mothers&#39; &quot;Return of the Son of Monster Magnet&quot; and the Stooges&#39; &quot;LA Blues&quot;. But they are dosed with a higher level of vibe than any other freak-out records - relentless, uplifting and full of the oddest gimmicks that all work perfectly. Amon Düül did not stay long, but they laid the beginnings of Krautrock with their music, and with one particular song on &quot;Psychedelic Underground&quot;. The name of the song translated as &quot;Mama Düül and her Sauerkraut band Start Up!&quot; With that title, the lazy British rock press at last had something to latch on to. Aha, we&#39;ll call it Krautrock... The First Rumblings of Kosmische Music West Germany was full of supposed &#39;head&#39; groups by now. But many of them still did not sound remotely German, slavishly trying to be Hard Rock. Others, like Embryo, Emergency and Birth Control, mixed obvious Teutonics into unsuccessful fusions with British/American rock and jazz. But in the meantime, Amon Düül II, the musical half of the commune, had recorded an amazing free-flowing LP called &quot;Phallus Dei&quot;, for the British Liberty label. Its overtly mysterious sleeve first confused me when I was 13 and standing in Tamworth Woolworth&#39;s. I was with my Welsh grandfather, whom I asked about the meaning of Phallus Dei. &quot;Bloody Hell, Don&#39;t tell your mother.&quot; he snorted. &quot;That means God&#39;s cock!&quot; And with the release of that 20-minute title track, both branches of Amon Düül had proved their commitment to the new cosmic political commune scene. This record was very extreme, both the chimeric sound and dizzy two-color sleeve like something from the 13th Floor Elevators&#39; International Artists label in Texas. And something else again was stirring in Cologne. The Stockhausen/Psychedelia-inspired Can were now a five-piece recording at Schloss Norvenich, the castle home of their patron. &lt;/pre&gt; &lt;p&gt;I’m pretty happy with that result! While it’s not perfect (I can see that it says ‘oddest gimmicks’ where it should say ‘crudest gimmicks’), this makes a page that was previously unlistenable into something that will work reasonably well in the audiobook form.&lt;/p&gt; &lt;p&gt;A couple of things to keep in mind here:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;if the Document Intelligence API has missed a word because it is too blurry, GPT-4 will guess what the word would be. In a book I was reading, it was mentioning a person called ‘Ulli Pop’, but GPT-4 guessed the name was ‘Iggy Pop’, which was a surprise.&lt;/li&gt; &lt;li&gt;If the extracted text includes swear words, GPT-4 will filter them out. This may be avoidable by tweaking the system prompt.&lt;/li&gt; &lt;li&gt;As anything with untrusted input, &lt;a href=&quot;https://simonwillison.net/series/prompt-injection/&quot;&gt;prompt injection&lt;/a&gt; is a possibility - if you are extracting text from a document that is telling a robot to do something, that may trip up GPT-4.&lt;/li&gt; &lt;/ul&gt; &lt;details&gt; &lt;summary&gt;Node.js code example for correcting text with GPT-4&lt;/summary&gt; &lt;div&gt; &lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dotenv/config&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Example usage:&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// node correct-text.js ./my-text.txt&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;inputFilePath&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;argv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// read the text file&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputText &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFilePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// correct the text&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; correctedText &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;correctText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputText&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// check output folder exists&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputFolderName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;corrected-pages&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputFolderName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputFolderName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// write the output file&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputFileName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputFilePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.txt&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputFilePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputFolderName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; inputFileName &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.txt&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputFilePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; correctedText&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// this sends the text to the GPT-4 API and returns the corrected text.&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;correctText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; openAiUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://api.openai.com/v1/chat/completions&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; systemPrompt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;The supplied text has been extracted from a blurry page. There may be characters from the edge of adjacent pages that need to be deleted. Correct the text so that it makes sense.&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; requestBody &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;gpt-4&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;system&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; systemPrompt&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;user&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; openAiResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;openAiUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// environment variables should be in .env file&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requestBody&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; responseJson &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; openAiResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; correctedText &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; responseJson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;choices&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; correctedText&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;/div&gt; &lt;/details&gt; &lt;h2 id=&quot;converting-the-text-into-audio-with-openais-text-to-speech-api&quot;&gt;Converting the text into audio with OpenAI’s text to speech API&lt;/h2&gt; &lt;p&gt;In the past year, I’ve been experimenting with a few different text to speech tools. Microsoft Azure’s neural text to speech API has been my go-to. I’ve also been impressed by the quality of the &lt;a href=&quot;https://elevenlabs.io/&quot;&gt;ElevenLabs API&lt;/a&gt;, but its &lt;a href=&quot;https://elevenlabs.io/pricing&quot;&gt;pricing&lt;/a&gt; is not feasible for large documents.&lt;/p&gt; &lt;p&gt;This week I’ve been playing around with OpenAI’s text to speech API. It only has a few voices at this stage, but the quality is very good, and the &lt;a href=&quot;https://openai.com/pricing&quot;&gt;pricing&lt;/a&gt; is comparable with &lt;a href=&quot;https://azure.microsoft.com/en-us/pricing/details/cognitive-services/speech-services/&quot;&gt;Azure&lt;/a&gt;, which makes it a good option for this project.&lt;/p&gt; &lt;p&gt;It seems that this latest generation of text to speech APIs, including ElevenLabs and OpenAI, are more flexible in adapting the voice to suit the content. I’ve been listening to two different books with different styles (&lt;a href=&quot;https://www.goodreads.com/book/show/826231.Krautrocksampler&quot;&gt;Krautrocksampler by Julian Cope&lt;/a&gt; and &lt;a href=&quot;https://www.goodreads.com/en/book/show/98512&quot;&gt;Kraftwerk by Pascal Bussy&lt;/a&gt;), using the same &lt;a href=&quot;https://platform.openai.com/docs/guides/text-to-speech/voice-options&quot;&gt;‘Onyx’ voice from the OpenAI API&lt;/a&gt;, and it’s interesting how the voice changes to suit the content.&lt;/p&gt; &lt;p&gt;Here’s an example from &lt;em&gt;Krautrocksampler&lt;/em&gt;, which has a more informal style:&lt;/p&gt; &lt;audio src=&quot;https://larryhudson.io/mp3/krautrocksampler-page13.mp3&quot; controls=&quot;&quot;&gt; &lt;/audio&gt; &lt;p&gt;And here’s an example from &lt;em&gt;Kraftwerk&lt;/em&gt;, which is more formal:&lt;/p&gt; &lt;audio src=&quot;https://larryhudson.io/mp3/kraftwerk-page20.mp3&quot; controls=&quot;&quot;&gt; &lt;/audio&gt; &lt;p&gt;It’s worth noting the OpenAI API has a max input length of 4096 characters. This means we need to break our text into chunks, get audio for each chunk, and then join the chunks together.&lt;/p&gt; &lt;details&gt; &lt;summary&gt;Node.js code example for converting text to speech&lt;/summary&gt; &lt;div&gt; &lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; pMap &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;p-map&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dotenv/config&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Example usage:&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// node convert-text-to-speech.js ./my-text.txt&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;argv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputText &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; textChunks &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;breakTextIntoChunks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputText&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; audioBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;convertTextChunksToAudio&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;textChunks&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.txt&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputFilePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;./&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;inputFilename&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.mp3&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputFilePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; audioBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// break text into chunks that are no longer than 4000 chars&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;breakTextIntoChunks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sentenceSeparator &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; chunks &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; currentChunk &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// split text into sentences, and filter out empty sentences&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sentences &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sentenceSeparator&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Boolean&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; sentence &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; sentences&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentChunk&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; sentence&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; chunks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentChunk&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; currentChunk &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; currentChunk &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; sentence &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; sentenceSeparator&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentChunk&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; chunks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentChunk&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; chunks&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;convertTextChunksToAudio&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;chunks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; audioBuffers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;chunks&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; convertTextToAudio&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;concurrency&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// join the audio buffers into a single audio buffer&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; audioBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;audioBuffers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; audioBuffer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;convertTextToAudio&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; textIsTooLong &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;textIsTooLong&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Warning: text is too long, only converting first 4000 characters&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; textToConvert &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; text&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ttsApiUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://api.openai.com/v1/audio/speech&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; requestBody &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;tts-1&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; textToConvert&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;voice&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;onyx&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;OPENAI_API_KEY&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; audioResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ttsApiUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requestBody&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; audioArrayBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; audioResponse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arrayBuffer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; audioBuffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;audioArrayBuffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; audioBuffer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;/div&gt; &lt;/details&gt; &lt;h2 id=&quot;an-extra-tip-splitting-a-long-pdf-into-smaller-chapters&quot;&gt;An extra tip: splitting a long PDF into smaller chapters&lt;/h2&gt; &lt;p&gt;If you’re working with a big PDF, your costs for using these APIs will add up quickly.&lt;/p&gt; &lt;p&gt;Rather than converting a full book PDF in one go, I have been splitting up the PDF into chapters. That way, I can create audio for chapters as I get to them.&lt;/p&gt; &lt;p&gt;Below is a Node.js code example for creating a smaller PDF from a PDF, using the ‘start’ and ‘end’ page numbers. a PDF into chapters. It uses the &lt;code&gt;pdf-lib&lt;/code&gt; library to create a new PDF using the start and end page numbers, and allows me to set a ‘page number offset’ in the case the page number in the bottom corner of the page doesn’t line up with the actual page number.&lt;/p&gt; &lt;p&gt;If I wanted to extract pages 20 to 30 from a PDF, and page 20 is page 25 in the PDF, I could use the script this way use the script like this: &lt;code&gt;node extract-pages-from-pdf.js ./my-pdf.pdf 20 30 5&lt;/code&gt;&lt;/p&gt; &lt;p&gt;That would save a new PDF called &lt;code&gt;my-pdf_pages_20_to_30.pdf&lt;/code&gt;, ready for extraction.&lt;/p&gt; &lt;details&gt; &lt;summary&gt;Node.js code example for extracting pages from a big PDF into a smaller PDF&lt;/summary&gt; &lt;div&gt; &lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PDFDocument &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pdf-lib&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Usage: node extract-pages-from-pdf.js [pdf-path] [start-page-num] [end-page-num] [page-number-offset]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Example usage: node extract-pages-from-pdf.js ./my-pdf.pdf 0 5 5&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; startPageNumStr&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; endPageNumStr&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pageNumberOffsetStr&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;argv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// arguments are strings, so we need to convert them to integers&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; startPageNum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;startPageNumStr&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; endPageNum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;endPageNumStr&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// page number offset is added to the page number,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// in case the page number in the bottom corner of the page is different to the actual page number&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pageNumberOffset &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pageNumberOffsetStr&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newPdfBytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractPdfPages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; startPageNum&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; endPageNum&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pageNumberOffset&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdfFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.pdf&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newPdfTitle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;pdfFilename&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;_pages_&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;startPageNum&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;_to_&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;endPageNum&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.pdf&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newPdfTitle&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newPdfBytes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractPdfPages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token parameter&quot;&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; startPageNum&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; endPageNum&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pageNumberOffset&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fullPdfBytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Load a PDFDocument from the existing PDF bytes&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fullPdfDoc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; PDFDocument&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fullPdfBytes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; startPageIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; startPageNum &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; pageNumberOffset&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; endPageIndex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; endPageNum &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; pageNumberOffset&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; startPageIndex&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; endPageIndex &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// get page numbers from start to end&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pageIndices &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Array&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; endPageIndex &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; startPageIndex &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;_&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; startPageIndex&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newPdf &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; PDFDocument&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; copiedPages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; newPdf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;copyPages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fullPdfDoc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pageIndices&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; copiedPage &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; copiedPages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; newPdf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;copiedPage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newPdfBytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; newPdf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; newPdfBytes&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; &lt;/div&gt; &lt;/details&gt; &lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt; &lt;p&gt;I’ll continue experimenting with this method. Any projects that I make will be in public repositories on my &lt;a href=&quot;https://github.com/larryhudson&quot;&gt;GitHub account&lt;/a&gt;.&lt;/p&gt; &lt;p&gt;If this is interesting to you, feel free to reach out. My email address is &lt;a href=&quot;mailto:larryhudson@hey.com&quot;&gt;larryhudson@hey.com&lt;/a&gt;&lt;/p&gt;</content></entry></feed>