{
    "componentChunkName": "component---src-pages-blog-mdx-slug-js",
    "path": "/blog/2026-06-26-knitting-assistant-pattern-ingestion/",
    "result": {"data":{"mdx":{"frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In","tags":["AI","Mistral","knitting","machine learning","OCR"],"categories":[],"date":"June 26, 2026","image":null,"imageAlt":null},"id":"85983a41-a504-57c0-8385-5f6327fb821c","body":"var _excluded = [\"components\"];\nfunction _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }\nfunction _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }\nfunction _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }\n/* @jsxRuntime classic */\n/* @jsx mdx */\n\nvar _frontmatter = {\n  \"title\": \"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In\",\n  \"date\": \"2026-06-26T00:00:00.000Z\",\n  \"tags\": [\"AI\", \"Mistral\", \"knitting\", \"machine learning\", \"OCR\"],\n  \"categories\": [],\n  \"draft\": false\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n    props = _objectWithoutProperties(_ref, _excluded);\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"Knitting is one of my favorite hobbies. I've been doing it long enough to accumulate a library of patterns, mostly from \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://knitpicks.com\"\n  }, \"Knitpicks\"), \", my favorite yarn and pattern store, and a corresponding pile of frustrations with how those patterns are structured. A knitting pattern isn't a recipe you read once and put away. You're consulting it mid-project, row by row, trying to remember what \\\"K2tog SSK\\\" means, cross-referencing a chart with an abbreviations table on a different page, searching for the needle size on page one while your hands are covered in fiber. It's a document format designed to be printed and held, not searched or queried.\"), mdx(\"p\", null, \"One of the projects in the Mistral course I've been going through was a homework assistant for children with dyslexia. The idea: take a photo of a homework assignment, send it through the vision model, and have the assistant interpret the content and answer the child's questions about it. Image in, understanding out, chat on top. The mechanics were straightforward, but the concept stuck with me. Could the same approach work with a PDF instead of a photo? Could I build something that ingests a knitting pattern, understands its structure, and then answers specific questions grounded in what's actually in the document? Not generic knitting knowledge, but \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"this\"), \" pattern?\"), mdx(\"p\", null, \"That's what I've been building. I've only tested it against patterns from Knitpicks so far; it's what I have. This post covers the first part: getting the PDF in. I'll start by walking through the code as it stands now, then get into the debugging session that shaped it, because the final version only makes sense once you've seen what it had to work around.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"The App\"), mdx(\"p\", null, \"The backend is a FastAPI server; the frontend is React. The server exposes four REST endpoints: upload a pattern, list patterns, get a pattern, and delete a pattern. There's also a WebSocket stub that will eventually handle real-time voice queries. Uploaded patterns are stored on disk under a \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"library/\"), \" directory, one subdirectory per pattern, each containing the original PDF, the extracted chart images as JPEGs, and a \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"document.json\"), \" with everything the pipeline produced.\"), mdx(\"p\", null, \"The upload endpoint is the only interesting one right now:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"python\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-python\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token decorator annotation punctuation\"\n  }, \"@router\", mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"post\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"/api/upload\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"async\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"def\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"upload_pattern\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"file\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" UploadFile \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" File\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"if\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"not\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"file\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"filename\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"lower\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"endswith\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\".pdf\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"raise\"), \" HTTPException\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"status_code\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token number\"\n  }, \"400\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" detail\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"Only PDF files are supported.\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n    pdf_bytes \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"await\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"file\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"read\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n    doc \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"await\"), \" process_pdf\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"pdf_bytes\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"file\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"filename\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" JSONResponse\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"content\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \"doc\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\")))), mdx(\"p\", null, \"Everything interesting happens in \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"process_pdf\"), \". The full code is on \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://github.com/gjack/knitting-assistant\"\n  }, \"GitHub\"), \" if you want to follow along.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"The Ingestion Pipeline\"), mdx(\"p\", null, mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"process_pdf\"), \" in \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"pattern_processor.py\"), \" runs three things in sequence: OCR, metadata extraction, and chart interpretation.\"), mdx(\"h3\", null, \"Step 1: OCR\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"python\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-python\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, \"pdf_b64 \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" base64\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"b64encode\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"pdf_bytes\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"decode\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\nocr_response \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"await\"), \" asyncio\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"to_thread\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"_ocr_pdf_sync\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" pdf_b64\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\")))), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"python\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-python\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"def\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"_ocr_pdf_sync\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"pdf_b64\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"str\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"-\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \">\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"object\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" client\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"ocr\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"process\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"\\n        model\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"mistral-ocr-latest\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n        document\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \"DocumentURLChunk\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"document_url\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"data:application/pdf;base64,\"), mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), \"pdf_b64\", mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token punctuation\"\n  }, \"}\")), mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"\\\"\")), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n        include_image_base64\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token boolean\"\n  }, \"True\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\")))), mdx(\"p\", null, mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"mistral-ocr-latest\"), \" processes the full PDF and returns each page's content as a markdown string, plus a list of images it extracted from that page (each with a base64-encoded JPEG and bounding box coordinates). The \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"include_image_base64=True\"), \" flag is what gets you the actual image data rather than just references.\"), mdx(\"p\", null, \"After OCR, the page markdowns are cleaned up and joined into a single \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"raw_text\"), \" string. Some things get stripped or replaced during cleaning. These were decisions that came out of the debugging session, not the initial build.\"), mdx(\"h3\", null, \"Step 2: Metadata Extraction\"), mdx(\"p\", null, \"With the raw text in hand, a chat call extracts the structured fields a knitter actually needs:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"python\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-python\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"def\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"_extract_metadata_sync\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"raw_text\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"str\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"-\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \">\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"dict\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n    resp \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" client\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"chat\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"complete\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"\\n        model\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"mistral-small-latest\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n        messages\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"[\"), \"\\n            \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"role\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"system\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"content\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" METADATA_SYSTEM_PROMPT\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n            \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"role\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"user\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"content\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" raw_text\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"]\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n        response_format\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"type\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"json_object\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" json\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"loads\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"resp\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"choices\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"[\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token number\"\n  }, \"0\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"]\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"message\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"content\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\")))), mdx(\"p\", null, \"The system prompt tells the model exactly what to extract:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"python\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-python\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, \"METADATA_SYSTEM_PROMPT \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token triple-quoted-string string\"\n  }, \"\\\"\\\"\\\"You are a knitting pattern parser. Extract the following fields as JSON:\\ntitle (string), sizes (list of strings), gauge (string), needles (list of strings),\\nyarn (string or list), abbreviations (list of {symbol: string, meaning: string}),\\nsections (list of section title strings).\\nIf a field is not present in the pattern, use null or an empty list.\\nReturn only valid JSON.\\\"\\\"\\\"\")))), mdx(\"p\", null, mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"response_format={\\\"type\\\": \\\"json_object\\\"}\"), \" forces the output to be valid JSON, so there's no need to parse freeform text. The result (title, sizes, gauge, needles, yarn, abbreviations, sections) is what the frontend's pattern info panel displays.\"), mdx(\"h3\", null, \"Step 3: Chart Interpretation\"), mdx(\"p\", null, \"Knitting patterns contain two kinds of images worth interpreting: schematics (technical line drawings of garment shapes with labeled measurements) and stitch charts (grids of symbols representing individual stitches). Both need a plain-English description that a user can actually read, especially charts, which are otherwise meaningless without the physical grid in front of you.\"), mdx(\"p\", null, \"Each image extracted by OCR gets sent to the vision model:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"python\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-python\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"def\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"_interpret_chart_sync\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"chart_image\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" title\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"str\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" legend_text\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"str\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"-\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \">\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"dict\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n    prompt \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"This image is from the knitting pattern '\"), mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), \"title\", mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token punctuation\"\n  }, \"}\")), mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"'. \\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"Knitting PDFs contain several types of images. Identify which this is:\\\\n\\\\n\\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"- SCHEMATIC: a technical line drawing with labeled measurements. \\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"List every labeled measurement exactly as shown.\\\\n\\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"- STITCH CHART: a grid of symbols representing individual stitches row by row. \\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"Use the pattern legend (\"), mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), \"legend_text\", mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token punctuation\"\n  }, \"}\")), mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \") to describe the stitch pattern \\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"and give a row-by-row reading in words (e.g. 'K1, YO, K2tog, repeat to end'). \\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"Do not reproduce the grid as ASCII art, a table, or a row of symbols.\\\\n\\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"- OTHER: a photo or decorative image. Describe what you see.\\\\n\\\\n\\\"\")), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-interpolation\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"f\\\"Start with the image type, then give the description.\\\"\")), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n    resp \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" client\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"chat\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"complete\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"\\n        model\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"mistral-small-latest\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n        messages\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"[\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"role\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"user\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"content\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"[\"), \"\\n            \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"type\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"text\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"text\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" prompt\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n            \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"type\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"image_url\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"image_url\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" _to_data_url\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"chart_image\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"image_base64\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"]\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"]\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n        max_tokens\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token number\"\n  }, \"600\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"id\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" chart_image\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"id\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"base64\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"description\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"bounding_box\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\")))), mdx(\"p\", null, \"The legend text (built from \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"metadata.abbreviations\"), \") is passed in so the model knows what the symbols mean when it encounters a stitch chart. All chart interpretations run in parallel with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"asyncio.gather\"), \".\"), mdx(\"h3\", null, \"What Gets Saved\"), mdx(\"p\", null, \"At the end of \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"process_pdf\"), \", everything lands in one JSON document:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"python\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-python\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, \"doc \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"pattern_id\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" pattern_id\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"filename\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" filename\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"upload_date\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" upload_date\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"raw_text\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" raw_text\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"metadata\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" metadata\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"chart_images\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" chart_results\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"pattern_document\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" pattern_document\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"chat_history\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"[\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"]\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\")))), mdx(\"p\", null, mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"pattern_document\"), \" is \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"raw_text\"), \" with the chart descriptions appended as an extra section. This is what will eventually go into the RAG index. \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"chat_history\"), \" starts empty and is where the conversation will accumulate.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"Then the Debugging Started\"), mdx(\"p\", null, \"I uploaded a real Knitpicks pattern to test it: the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.knitpicks.com/fonse-shawlette-free-knitting-pattern/p/55951\"\n  }, \"Fons\\xE9 Shawlette\"), \", the one for the shawl below that I finished last year.\"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"600px\"\n    }\n  }, \"\\n      \", mdx(\"a\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-link\",\n    \"href\": \"/static/2ce5403c330bc9e002a2b2e2fd56a26c/e1596/fonse-shawl.jpg\",\n    \"style\": {\n      \"display\": \"block\"\n    },\n    \"target\": \"_blank\",\n    \"rel\": \"noopener\"\n  }, \"\\n    \", mdx(\"span\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"75.33333333333333%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAwACBf/EABYBAQEBAAAAAAAAAAAAAAAAAAEAAv/aAAwDAQACEAMQAAABAuoFrc0H/8QAGhAAAgMBAQAAAAAAAAAAAAAAAQIDESEAEv/aAAgBAQABBQKT2hWaawZKYWFXc7//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAWEQEBAQAAAAAAAAAAAAAAAAAAETH/2gAIAQIBAT8B1X//xAAbEAACAwADAAAAAAAAAAAAAAAAAQIRMSFhof/aAAgBAQAGPwK4yaKUrOfEdmGH/8QAGBABAAMBAAAAAAAAAAAAAAAAAQARIVH/2gAIAQEAAT8hKuqBmwE92Osi4Y9usuEVKXFXVZk//9oADAMBAAIAAwAAABDrP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABcRAQEBAQAAAAAAAAAAAAAAAAEAEVH/2gAIAQIBAT8QEGtnl//EAB0QAQEAAgEFAAAAAAAAAAAAAAERAEEhUWFxgaH/2gAIAQEAAT8QnDGvw7mKGhhPrWCvTAMEgg8jrzow+QKsJcZlbSmf/9k=')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"img\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"alt\": \"My finished Fonsé shawlette, a triangular lace shawl in deep purple and soft lavender\",\n    \"title\": \"My finished Fonsé shawlette, a triangular lace shawl in deep purple and soft lavender\",\n    \"src\": \"/static/2ce5403c330bc9e002a2b2e2fd56a26c/b4294/fonse-shawl.jpg\",\n    \"srcSet\": [\"/static/2ce5403c330bc9e002a2b2e2fd56a26c/75985/fonse-shawl.jpg 150w\", \"/static/2ce5403c330bc9e002a2b2e2fd56a26c/f93b5/fonse-shawl.jpg 300w\", \"/static/2ce5403c330bc9e002a2b2e2fd56a26c/b4294/fonse-shawl.jpg 600w\", \"/static/2ce5403c330bc9e002a2b2e2fd56a26c/8e1fc/fonse-shawl.jpg 900w\", \"/static/2ce5403c330bc9e002a2b2e2fd56a26c/e5166/fonse-shawl.jpg 1200w\", \"/static/2ce5403c330bc9e002a2b2e2fd56a26c/e1596/fonse-shawl.jpg 2048w\"],\n    \"sizes\": \"(max-width: 600px) 100vw, 600px\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    },\n    \"loading\": \"lazy\",\n    \"decoding\": \"async\"\n  }), \"\\n  \"), \"\\n    \")), mdx(\"p\", null, \"The \\\"Pattern Text\\\" panel looked nothing like a pattern. Three things were wrong.\"), mdx(\"hr\", null), mdx(\"h3\", null, \"Bug 1: The Frontend Was Ignoring Markdown Entirely\"), mdx(\"p\", null, \"Strings like \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"![img-2.jpeg](img-2.jpeg)\"), \" were sitting in the text as literal characters. \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"&amp;\"), \" appeared where \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"&\"), \" should be.\"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"504px\"\n    }\n  }, \"\\n      \", mdx(\"a\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-link\",\n    \"href\": \"/static/b2dc6ceb00739c47b2c509cc45cbeacf/08115/markdown-as-text.png\",\n    \"style\": {\n      \"display\": \"block\"\n    },\n    \"target\": \"_blank\",\n    \"rel\": \"noopener\"\n  }, \"\\n    \", mdx(\"span\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"66%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAABQUlEQVQ4y61Ti26DMAzk//8OCRVEKZRHobzKGwIFXJ23oE7bOrQtkuX4khiffSiiq2jsa7rf73S73SiOY/I8j33btuyLoqA0Tamua/ZlWVKe5+yrquJ38IgVJIT911KGrqRpaEgIQafTiS6XCwVBQOfzmSzLIsdxKIoixnF+vV754bIstK7rJ1Mk5XmeufQsy5hakiT8GNSAN03DiUEf66tkW0LR1xzsXa/uMuVxaGiaJjoejzyQw+FApmmS7/uM2bZNhmEwfZwDl3u0BXfCMOQ2vVf4NhTQHseR+wkvY4nho1ADTGKwYRg4ht8oo8kAQQd7mHz8jMF2TRnZdV3nSaJ00FZVdaMKDLHrujuG8gsdfjeYD7KB0vu+Z+u6juWDP0BikA6YPFf4UthSyHK6mqZtkweGNsD/nFC0XAEq+qsOH0eO7YJ5YyCOAAAAAElFTkSuQmCC')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"img\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"alt\": \"Pattern text panel showing raw markdown image syntax displayed as literal text\",\n    \"title\": \"Pattern text panel showing raw markdown image syntax displayed as literal text\",\n    \"src\": \"/static/b2dc6ceb00739c47b2c509cc45cbeacf/08115/markdown-as-text.png\",\n    \"srcSet\": [\"/static/b2dc6ceb00739c47b2c509cc45cbeacf/8a4e8/markdown-as-text.png 150w\", \"/static/b2dc6ceb00739c47b2c509cc45cbeacf/5a46d/markdown-as-text.png 300w\", \"/static/b2dc6ceb00739c47b2c509cc45cbeacf/08115/markdown-as-text.png 504w\"],\n    \"sizes\": \"(max-width: 504px) 100vw, 504px\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    },\n    \"loading\": \"lazy\",\n    \"decoding\": \"async\"\n  }), \"\\n  \"), \"\\n    \")), mdx(\"p\", null, \"Mistral OCR returns each page as markdown. The frontend was dropping that string into a plain \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"<div>\"), \" text node with no parsing at all. Image references displayed as text, HTML entities were never decoded, and pipe tables rendered as a wall of \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"|\"), \" characters.\"), mdx(\"p\", null, \"The fix: replace the text node with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"react-markdown\"), \", with a custom \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"img\"), \" component that maps each \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"img-N.jpeg\"), \" reference back to the matching entry in \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"chart_images\"), \" by id and renders the actual base64 image inline. GFM table support required adding \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"remark-gfm\"), \" explicitly. Entity decoding came for free.\"), mdx(\"hr\", null), mdx(\"h3\", null, \"Bug 2: The Charts OCR Couldn't Find\"), mdx(\"p\", null, \"Chart 1a of the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.knitpicks.com/fonse-shawlette-free-knitting-pattern/p/55951\"\n  }, \"Fons\\xE9 Shawlette\"), \" pattern showed up as a giant wall of \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"|\"), \", \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"\\u25CB\"), \", and \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"---\"), \" characters. Chart 1b didn't appear at all, just its heading and then the next section. Here's what those two charts actually look like in the pattern PDF:\"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"600px\"\n    }\n  }, \"\\n      \", mdx(\"a\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-link\",\n    \"href\": \"/static/ab09be908348a88356d9fa799fdfa786/aea0a/fonse-charts.png\",\n    \"style\": {\n      \"display\": \"block\"\n    },\n    \"target\": \"_blank\",\n    \"rel\": \"noopener\"\n  }, \"\\n    \", mdx(\"span\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"89.33333333333334%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAACkUlEQVQ4y42Ua0uqURCF358sVJRaaVr2K0IhQ9JudqE0MPB7hCGWlNpVMyvTrIzIOTxzmPd0+3CExd7O3rP2mtvrXF5eSrlcllKpJDc3N9Lv9+X19VXx8vKiK7bf8Pb25uL9/V0+Pj7EOTk5kbu7O3V+fHyUh4cH6XQ6UqlUpF6vq63dbivYX19fy/n5ue4NvV5Pbm9vpVAoiLO4uCirq6vy9PSkB4DXUQwhCu3s+fnZJWTf7XZ1BQsLC5JKpcRJJpMyPj4u29vb6ohS1BwcHMjp6alexg4+E9rj2FdWVmR0dFTW1tbEgXVyclLC4bBsbm7qqygjnxcXF5oCYIScnZ2d6cNGFggEZGJiQtLp9F+FEEYiEQkGg7K+vq45RAVqWq2W/jfCq6sroZAQLi8vu75E+YNwZmZGX8pms6oQQsufraacvHMXH/CD0A6mp6f1/87OjhbH8mcFYL+0tOT6cP8LITkk1NnZWVelhb+7u6v9hTJChJDET01NufcBex7Y2toSJx6Py8jIiMr3+/0yNjYmXq9XfD6fDA0NadJRRy7n5+dleHhYzwD37K7H49G7zv7+vhaCtiHMXC6n2Nvb09XySZEymYxr/w7Ojo6OxCEMHABd/9uPkRoMBvI/P4eK0Vu0xv39vY4Q4dHUtAdjCTgDKDU7/YmNlNRqNbU7yE0kEjpiVkUKwCxDzN6mwuz0IvchYm02mzI3Nyekz8nn85po8miOAEfU2iPYiKJYLOrXychIVSwW07bRjwOEVJc2oSUgMCVGiDPz3Wg0FCgkXNZoNKq+oVDoHyFlt16ClJer1apLiDoKRgrIE6qYZ8IMBoLai/TmF8LPo7exsaEO30O20Ts+PlZlfBRsWiA8PDyUP91mwzUP+bioAAAAAElFTkSuQmCC')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"img\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"alt\": \"Fonsé Charts 1a and 1b from the pattern: symbol grids with yarn over circles, decrease lines, and shaded no-stitch cells\",\n    \"title\": \"Fonsé Charts 1a and 1b from the pattern: symbol grids with yarn over circles, decrease lines, and shaded no-stitch cells\",\n    \"src\": \"/static/ab09be908348a88356d9fa799fdfa786/0a47e/fonse-charts.png\",\n    \"srcSet\": [\"/static/ab09be908348a88356d9fa799fdfa786/8a4e8/fonse-charts.png 150w\", \"/static/ab09be908348a88356d9fa799fdfa786/5a46d/fonse-charts.png 300w\", \"/static/ab09be908348a88356d9fa799fdfa786/0a47e/fonse-charts.png 600w\", \"/static/ab09be908348a88356d9fa799fdfa786/1cfc2/fonse-charts.png 900w\", \"/static/ab09be908348a88356d9fa799fdfa786/c1b63/fonse-charts.png 1200w\", \"/static/ab09be908348a88356d9fa799fdfa786/aea0a/fonse-charts.png 2018w\"],\n    \"sizes\": \"(max-width: 600px) 100vw, 600px\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    },\n    \"loading\": \"lazy\",\n    \"decoding\": \"async\"\n  }), \"\\n  \"), \"\\n    \")), mdx(\"p\", null, \"And here's what the app was showing instead:\"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"600px\"\n    }\n  }, \"\\n      \", mdx(\"a\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-link\",\n    \"href\": \"/static/9c94896d7aede8c9a8a80880d97086dd/0560f/garbled-chart.png\",\n    \"style\": {\n      \"display\": \"block\"\n    },\n    \"target\": \"_blank\",\n    \"rel\": \"noopener\"\n  }, \"\\n    \", mdx(\"span\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"17.333333333333336%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAABYlAAAWJQFJUiTwAAAASklEQVQI152MQQqAUAhEvf9V3ag/rVA0chGfahENvMXwhoEhXIhYzFxE9MrpROTi3lW1MbMCXUYP3L0y8xdzYN/WPoyIh/ya+fwAjX/rDOzLsQ4AAAAASUVORK5CYII=')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"img\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"alt\": \"Chart 1a rendered as an unreadable wall of pipe characters and symbols; Chart 1b shows only a heading with no content\",\n    \"title\": \"Chart 1a rendered as an unreadable wall of pipe characters and symbols; Chart 1b shows only a heading with no content\",\n    \"src\": \"/static/9c94896d7aede8c9a8a80880d97086dd/0a47e/garbled-chart.png\",\n    \"srcSet\": [\"/static/9c94896d7aede8c9a8a80880d97086dd/8a4e8/garbled-chart.png 150w\", \"/static/9c94896d7aede8c9a8a80880d97086dd/5a46d/garbled-chart.png 300w\", \"/static/9c94896d7aede8c9a8a80880d97086dd/0a47e/garbled-chart.png 600w\", \"/static/9c94896d7aede8c9a8a80880d97086dd/1cfc2/garbled-chart.png 900w\", \"/static/9c94896d7aede8c9a8a80880d97086dd/c1b63/garbled-chart.png 1200w\", \"/static/9c94896d7aede8c9a8a80880d97086dd/0560f/garbled-chart.png 3160w\"],\n    \"sizes\": \"(max-width: 600px) 100vw, 600px\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    },\n    \"loading\": \"lazy\",\n    \"decoding\": \"async\"\n  }), \"\\n  \"), \"\\n    \")), mdx(\"p\", null, \"Some of the other charts produced a different kind of failure entirely: the chart interpretation step threw an error because the base64 image data was being double-prefixed, arriving at the API as \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"data:image/jpeg;base64,data:image/jpeg;base64,...\"), \". That was caught early and fixed with a simple guard in \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"_to_data_url\"), \".\"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"600px\"\n    }\n  }, \"\\n      \", mdx(\"a\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-link\",\n    \"href\": \"/static/8b998d29f740ef956a2ae3d5510feed4/28d13/chart-error.png\",\n    \"style\": {\n      \"display\": \"block\"\n    },\n    \"target\": \"_blank\",\n    \"rel\": \"noopener\"\n  }, \"\\n    \", mdx(\"span\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"8%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAPklEQVQI122MSQ4AIAwC/f9vBTyjdTfxMKEpSyqiJTj0AIvo/w3/rCyZTWQnxWAvh3KaL6M8ffHKnjvG0AYrsYuZ1ZE0wxwAAAAASUVORK5CYII=')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"img\", {\n    parentName: \"a\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"alt\": \"Chart section showing a chart interpretation failed error with a malformed base64 URL\",\n    \"title\": \"Chart section showing a chart interpretation failed error with a malformed base64 URL\",\n    \"src\": \"/static/8b998d29f740ef956a2ae3d5510feed4/0a47e/chart-error.png\",\n    \"srcSet\": [\"/static/8b998d29f740ef956a2ae3d5510feed4/8a4e8/chart-error.png 150w\", \"/static/8b998d29f740ef956a2ae3d5510feed4/5a46d/chart-error.png 300w\", \"/static/8b998d29f740ef956a2ae3d5510feed4/0a47e/chart-error.png 600w\", \"/static/8b998d29f740ef956a2ae3d5510feed4/1cfc2/chart-error.png 900w\", \"/static/8b998d29f740ef956a2ae3d5510feed4/c1b63/chart-error.png 1200w\", \"/static/8b998d29f740ef956a2ae3d5510feed4/28d13/chart-error.png 3210w\"],\n    \"sizes\": \"(max-width: 600px) 100vw, 600px\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    },\n    \"loading\": \"lazy\",\n    \"decoding\": \"async\"\n  }), \"\\n  \"), \"\\n    \")), mdx(\"p\", null, \"My first guess for the garbled charts was that the images hadn't been extracted from the PDF. I ran \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"pdfimages -list\"), \" against the source file. Five raw embedded raster images in the whole PDF. The pipeline had produced ten \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"chart_images\"), \" entries.\"), mdx(\"p\", null, \"That mismatch was the key. Mistral OCR isn't pulling image objects out of the PDF's internal structure. It's visually segmenting each rendered page and deciding for each region whether it looks like \\\"an image,\\\" \\\"a table,\\\" or \\\"text.\\\" That judgment is its own inference step, and it's not deterministic. Running the same PDF through the pipeline multiple times gave different results each time.\"), mdx(\"p\", null, \"On the page with charts 1a and 1b, the heuristic had failed in two different ways. For Chart 1a: it decided the chart grid was a table and transcribed it as markdown, one symbol per cell. Visually identical structure to the original grid, completely unreadable as text. For Chart 1b: it produced nothing at all.\"), mdx(\"p\", null, \"The fix is two cheap, deterministic checks in \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"pattern_processor.py\"), \" that run after OCR, before any extra API calls:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"python\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-python\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"def\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"_is_garbled_chart_table\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"lines\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"list\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"[\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"str\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"]\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"-\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \">\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"bool\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n    cells \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"[\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"]\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"for\"), \" line \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"in\"), \" lines\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n        stripped \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" line\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"strip\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"if\"), \" re\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"fullmatch\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"r\\\"\\\\|?\\\\s*-{2,}\\\\s*(\\\\|\\\\s*-{2,}\\\\s*)*\\\\|?\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" stripped\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n            \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"continue\"), \"\\n        cells\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"extend\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"c\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"strip\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"for\"), \" c \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"in\"), \" stripped\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"strip\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"|\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"split\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"|\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"if\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"len\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"cells\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"<\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token number\"\n  }, \"8\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \":\"), \"\\n        \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token boolean\"\n  }, \"False\"), \"\\n    symbolish \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"sum\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token number\"\n  }, \"1\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"for\"), \" c \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"in\"), \" cells \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"if\"), \" c \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"==\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"\\\"\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"or\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"len\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"c\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"<=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token number\"\n  }, \"2\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" symbolish \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"/\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token builtin\"\n  }, \"len\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"cells\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \">\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token number\"\n  }, \"0.85\")))), mdx(\"p\", null, \"A real knitting chart grid is almost entirely single symbols, blanks, and short numbers. If more than 85% of a table's cells are two characters or fewer, it's a misclassified chart grid, not prose. The second check looks for chart headings whose following section contains neither an image reference nor any table at all.\"), mdx(\"p\", null, \"For every page flagged by either check, the pipeline renders that page with PyMuPDF at 3x zoom and sends it through the vision model again, with a prompt that names the specific charts OCR lost and asks for a row-by-row reading.\"), mdx(\"p\", null, \"Getting that fallback prompt right took two rounds. The first version said \\\"describe this chart.\\\" The model's response was a code block of \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"O\"), \" characters, the exact same unreadable failure as the original OCR bug, just moved one layer up. The prompt now explicitly says: describe rows in words like \\\"K1, YO, K2tog, repeat to end,\\\" not as ASCII art, not as a table, not as a row of symbols. A second issue came up when a single page had three named charts (3a, 3b, 3c): the model would write a full description for one and gesture vaguely at the others. The prompt now names exactly how many charts to cover and requires a dedicated section for each, with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"max_tokens\"), \" scaled accordingly.\"), mdx(\"hr\", null), mdx(\"h3\", null, \"Bug 3: The Abbreviations Table, Twice\"), mdx(\"p\", null, \"After fix #1, a different kind of garbled text appeared: the abbreviations section rendered as one long run of pipe characters.\"), mdx(\"p\", null, \"Two separate causes. First, \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"remark-gfm\"), \" (which handles pipe tables) hadn't been added yet, so any pipe table in the OCR'd text silently fell back to being treated as a plain paragraph of \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"|\"), \" and \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"-\"), \" characters. That was fixed as part of Bug 1.\"), mdx(\"p\", null, \"Second: even once the table parsed correctly, this specific one was bad to display. Mistral OCR had transcribed the pattern's printed abbreviations glossary as a wide table with four symbol/meaning pairs per row, an artifact of the PDF's multi-column print layout. That data was already cleanly extracted into \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"metadata.abbreviations\"), \" and already displayed as a proper two-column table in the \\\"Abbreviations & Legend\\\" panel above it. Showing the OCR version again was just noise.\"), mdx(\"p\", null, \"The fix strips any table whose first header cell is literally \\\"Abbreviations\\\" from the pattern text and replaces it with a short pointer to the existing panel. Not a generic reflow algorithm; the content is redundant, so removing it is simpler than reformatting it.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"What This Taught Me About Building on Top of OCR\"), mdx(\"p\", null, \"The lesson across all three bugs: Mistral OCR isn't a reliable pre-processing step you can trust once and move on from. It makes its own inference decisions about page layout (what's an image, what's a table, what's text) and those decisions are probabilistic and inconsistent across runs of the same document. Any pipeline built on top of it needs to treat the output as a draft that requires validation, not a clean extraction.\"), mdx(\"p\", null, \"I've only tested this with Knitpicks patterns. That's a fairly consistent layout and print style, and I don't know yet how much of this would hold up against patterns from other publishers with different conventions.\"), mdx(\"p\", null, \"There are a few other untested shapes worth noting. Every PDF I've used so far contains a single pattern, which is how Knitpicks distributes most of their individual designs. But they also sell whole pattern books as PDFs, and that's a meaningfully different problem: the pipeline currently assumes one pattern per document, so the metadata extraction and chart interpretation would need to be rethought to handle a multi-pattern file correctly. Similarly, the pipeline hasn't been tested on scanned PDFs, patterns photocopied from a book or magazine and saved as a PDF. Those tend to be lower resolution and may have artifacts that affect how OCR segments the page, especially for chart grids where the symbol density is already right at the edge of what the heuristics can handle.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"What's Next\"), mdx(\"p\", null, \"The ingestion pipeline now produces a document I'm reasonably confident is accurate for the patterns I've tested. Next is the part I actually wanted to build: a chat interface that answers questions grounded in the specific pattern. Not \\\"what does YO usually mean in knitting\\\" from general knowledge, but \\\"what does YO mean in this pattern, according to the abbreviations table.\\\" That means building the RAG layer on top of what the ingestion pipeline produces.\"));\n}\n;\nMDXContent.isMDXComponent = true;","timeToRead":7,"slug":"2026-06-26-knitting-assistant-pattern-ingestion/"},"allMdx":{"edges":[{"next":{"slug":"2013-03-17-silicon-valley-devfest/","frontmatter":{"title":"Silicon Valley DevFest"},"id":"edcdad98-0f1c-5f08-b9bf-ef2584a6784d"},"previous":null,"node":{"id":"3378703f-653c-58fd-a11e-7adc675fb79e","frontmatter":{"title":"Makings of a Pythonista"},"slug":"2012-11-21-makings-of-a-pythonista"}},{"next":{"slug":"2015-06-14-in-search-of-a-faster-query","frontmatter":{"title":"In search of a faster query"},"id":"c6497654-5ee0-5b14-b735-e08bbbc6a4bf"},"previous":{"frontmatter":{"title":"Makings of a Pythonista"},"slug":"2012-11-21-makings-of-a-pythonista"},"node":{"id":"edcdad98-0f1c-5f08-b9bf-ef2584a6784d","frontmatter":{"title":"Silicon Valley DevFest"},"slug":"2013-03-17-silicon-valley-devfest/"}},{"next":{"slug":"2015-11-01-diary-of-a-dev-joys-of-building","frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"id":"ca6c84a6-3aea-5278-bf26-267d43acb227"},"previous":{"frontmatter":{"title":"Silicon Valley DevFest"},"slug":"2013-03-17-silicon-valley-devfest/"},"node":{"id":"c6497654-5ee0-5b14-b735-e08bbbc6a4bf","frontmatter":{"title":"In search of a faster query"},"slug":"2015-06-14-in-search-of-a-faster-query"}},{"next":{"slug":"2016-12-24-working-with-d3-based-chart-library/","frontmatter":{"title":"TIL: Working with a D3 based chart library"},"id":"776dcc9f-2ac4-511a-a9c6-982c226e0478"},"previous":{"frontmatter":{"title":"In search of a faster query"},"slug":"2015-06-14-in-search-of-a-faster-query"},"node":{"id":"ca6c84a6-3aea-5278-bf26-267d43acb227","frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"slug":"2015-11-01-diary-of-a-dev-joys-of-building"}},{"next":{"slug":"2017-03-15-react-conf-2017/","frontmatter":{"title":"React Conf 2017"},"id":"97c0196c-f0cd-5d6a-b2b9-fd3c6f1f1baa"},"previous":{"frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"slug":"2015-11-01-diary-of-a-dev-joys-of-building"},"node":{"id":"776dcc9f-2ac4-511a-a9c6-982c226e0478","frontmatter":{"title":"TIL: Working with a D3 based chart library"},"slug":"2016-12-24-working-with-d3-based-chart-library/"}},{"next":{"slug":"2017-04-12-the-initial-state-gotcha","frontmatter":{"title":"The Initial State Gotcha"},"id":"82041731-cb81-571d-958c-00f57d99b927"},"previous":{"frontmatter":{"title":"TIL: Working with a D3 based chart library"},"slug":"2016-12-24-working-with-d3-based-chart-library/"},"node":{"id":"97c0196c-f0cd-5d6a-b2b9-fd3c6f1f1baa","frontmatter":{"title":"React Conf 2017"},"slug":"2017-03-15-react-conf-2017/"}},{"next":{"slug":"2017-05-13-blue-socks-at-orange/","frontmatter":{"title":"Blue Socks Spotted at Orange"},"id":"893cf729-88c0-5706-bcb8-d14f75a30f2b"},"previous":{"frontmatter":{"title":"React Conf 2017"},"slug":"2017-03-15-react-conf-2017/"},"node":{"id":"82041731-cb81-571d-958c-00f57d99b927","frontmatter":{"title":"The Initial State Gotcha"},"slug":"2017-04-12-the-initial-state-gotcha"}},{"next":{"slug":"once-upon-a-blog/","frontmatter":{"title":"Once upon a Blog"},"id":"db5caf55-934b-5de1-aa6b-5a2078e4f869"},"previous":{"frontmatter":{"title":"The Initial State Gotcha"},"slug":"2017-04-12-the-initial-state-gotcha"},"node":{"id":"893cf729-88c0-5706-bcb8-d14f75a30f2b","frontmatter":{"title":"Blue Socks Spotted at Orange"},"slug":"2017-05-13-blue-socks-at-orange/"}},{"next":{"slug":"story-of-a-laptop-upgrade","frontmatter":{"title":"Story of a laptop upgrade"},"id":"91dabe95-0227-5d01-b1d1-f10959c5d43a"},"previous":{"frontmatter":{"title":"Blue Socks Spotted at Orange"},"slug":"2017-05-13-blue-socks-at-orange/"},"node":{"id":"db5caf55-934b-5de1-aa6b-5a2078e4f869","frontmatter":{"title":"Once upon a Blog"},"slug":"once-upon-a-blog/"}},{"next":{"slug":"the-code-we-write-today/","frontmatter":{"title":"The code we write today"},"id":"08584458-be98-53c2-8cf3-9fa51060e04b"},"previous":{"frontmatter":{"title":"Once upon a Blog"},"slug":"once-upon-a-blog/"},"node":{"id":"91dabe95-0227-5d01-b1d1-f10959c5d43a","frontmatter":{"title":"Story of a laptop upgrade"},"slug":"story-of-a-laptop-upgrade"}},{"next":{"slug":"rails-on-docker/","frontmatter":{"title":"Rails on Docker"},"id":"5e92911a-24f7-562d-8ecd-9a7391960489"},"previous":{"frontmatter":{"title":"Story of a laptop upgrade"},"slug":"story-of-a-laptop-upgrade"},"node":{"id":"08584458-be98-53c2-8cf3-9fa51060e04b","frontmatter":{"title":"The code we write today"},"slug":"the-code-we-write-today/"}},{"next":{"slug":"rails-on-docker-part-two/","frontmatter":{"title":"Rails on Docker Part Two"},"id":"534e725d-71be-5e6f-ace7-75db51185701"},"previous":{"frontmatter":{"title":"The code we write today"},"slug":"the-code-we-write-today/"},"node":{"id":"5e92911a-24f7-562d-8ecd-9a7391960489","frontmatter":{"title":"Rails on Docker"},"slug":"rails-on-docker/"}},{"next":{"slug":"super-powers-for-active-record-relation","frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"id":"3d5d8b08-cf43-57be-8c43-aad95e2dfd06"},"previous":{"frontmatter":{"title":"Rails on Docker"},"slug":"rails-on-docker/"},"node":{"id":"534e725d-71be-5e6f-ace7-75db51185701","frontmatter":{"title":"Rails on Docker Part Two"},"slug":"rails-on-docker-part-two/"}},{"next":{"slug":"2026-05-30-outsmarting-github-copilot","frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"id":"8bc48193-2ba7-5463-9edb-0f5e3a910bb1"},"previous":{"frontmatter":{"title":"Rails on Docker Part Two"},"slug":"rails-on-docker-part-two/"},"node":{"id":"3d5d8b08-cf43-57be-8c43-aad95e2dfd06","frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"slug":"super-powers-for-active-record-relation"}},{"next":{"slug":"2026-06-06-first-steps-with-mistral","frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"id":"cb7d26e3-c28a-5c0a-a659-83f90d04bd15"},"previous":{"frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"slug":"super-powers-for-active-record-relation"},"node":{"id":"8bc48193-2ba7-5463-9edb-0f5e3a910bb1","frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"slug":"2026-05-30-outsmarting-github-copilot"}},{"next":{"slug":"2026-06-12-building-the-subtitle-tool/","frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"id":"fb5e6f8e-7da3-506b-b50c-869e8bdde883"},"previous":{"frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"slug":"2026-05-30-outsmarting-github-copilot"},"node":{"id":"cb7d26e3-c28a-5c0a-a659-83f90d04bd15","frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"slug":"2026-06-06-first-steps-with-mistral"}},{"next":{"slug":"2026-06-19-building-a-voice-assistant/","frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"id":"182b1abf-7702-5cb7-8de9-ba26e74b811f"},"previous":{"frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"slug":"2026-06-06-first-steps-with-mistral"},"node":{"id":"fb5e6f8e-7da3-506b-b50c-869e8bdde883","frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"slug":"2026-06-12-building-the-subtitle-tool/"}},{"next":{"slug":"2026-06-26-knitting-assistant-pattern-ingestion/","frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In"},"id":"85983a41-a504-57c0-8385-5f6327fb821c"},"previous":{"frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"slug":"2026-06-12-building-the-subtitle-tool/"},"node":{"id":"182b1abf-7702-5cb7-8de9-ba26e74b811f","frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"slug":"2026-06-19-building-a-voice-assistant/"}},{"next":null,"previous":{"frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"slug":"2026-06-19-building-a-voice-assistant/"},"node":{"id":"85983a41-a504-57c0-8385-5f6327fb821c","frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In"},"slug":"2026-06-26-knitting-assistant-pattern-ingestion/"}}]}},"pageContext":{"id":"85983a41-a504-57c0-8385-5f6327fb821c","slug":"2026-06-26-knitting-assistant-pattern-ingestion/","__params":{"slug":"2026-06-26-knitting-assistant-pattern-ingestion"}}},
    "staticQueryHashes": ["2201243204"]}