# Project export: BrailleBot: Make Every Document Accessible!

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: World's first braille printer under $15
- Devpost: https://devpost.com/software/readable-ai-braille-printer
- GitHub: https://github.com/cathyzbn/braille-printer
- Video: https://www.youtube.com/embed/atzED5lXhCE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Best Hardware Hack (DJI Mini 3 Drone with Remote Control per team member))
- Team: 3 GitHub contributor(s) — Scott Hickmann (54 commits), Jason Lin (49 commits), Cathy Zhou (46 commits)

## Devpost submission (written by the team)

### Overview

BrailleBot transforms any document into printed standard braille, well-suited for printing documents, textbooks, websites, or even Zoom lecture summaries. It’s the first printer that can take any document and convert it to an accessible text format including textual image descriptions using AI visual recognition. It then prints the braille using a novel spring-loaded embossing mechanism mounted on a low-cost 3D printer. Today, the cheapest braille printers are around $1500. Our embossing mechanism costs less than $15, and is then mounted on any low-cost 3D printer. At this price point, our solution is almost 100x cheaper than existing options while maintaining high quality, empowering teachers and students by making learning materials accessible at an unprecedented low price point. Why build this? During the start of Treehacks, we chatted with a fellow builder who described corresponding with her visually-impaired pen pal: She types her message on her computer, and then manually punches the braille, letter by letter, on a sheet of paper to send over. Why does she do this? Braille printers are outrageously expensive due to the high degree of precision required and the niche market. Due to the high cost, many who seek to print braille documents instead manually punch each letter with a specialized hole puncher. This struggle extends far beyond personal correspondence—it highlights a critical gap in education for the visually impaired. A low-cost Braille printer could empower teachers and students, making learning materials accessible to those in low-income communities, individuals who are both visually and audibly impaired, and those who rely on long-lasting, tangible print to acquire knowledge and information. That's when we had our breakthrough. Modern 3D printers offer incredible precision at a fraction of the cost of specialized equipment. By retrofitting a basic 3D printer with our custom embossing mechanism, we created a braille printer that maintains professional-grade accuracy while cutting costs by nearly 99%. We didn't stop at making braille printing affordable. Today, only 3% of the internet is accessible to visually impaired users, largely because most content creators don't include text descriptions for images and visuals. We realized that we can use AI to describe images and convert any document to an accessible format. We believe that document format or visual content should never be a barrier to access, and with BrailleBot, sharing ideas can be as simple as pressing 'print' for everyone.

### How we built it

Hardware: The “punch” mechanism is comprised of a series of 3D printed parts and powered by the printer’s extruder motor, making the modification incredibly fast and inexpensive. A snap action mechanism converts a quarter rotation of the extruder motor into a load and then rapid punch motion. A screw at the top allows the user to adjust the spring’s compression, thereby changing the depth of the punch. The stock extruder can create up to 10 punches a second. Software On the backend, we leverage the Groq inference engine and Zoom API, using Llama3-8B and Llama3-11B Vision to transcribe documents, generate text descriptions of images, and summarize Zoom lecture transcripts with low latency. Then, we use our Python Flask backend to convert the ASCII PDF representations into braille letters and GCode instructions for the Marlin firmware (used by most 3D printers). Commands are sent to the printer through USB serial. We use a JS canvas to display a live realtime preview of the print. The frontend is built with Next.js, Typescript, Chakra, and React.

### Challenges we ran into

Finding the right combination of punch geometry, travel distance, and impact material was a challenge on both the software and hardware side because of the limited resources on hand and criteria that the product be cheap and easy to retrofit. We tried a variety of foams, cloths, and papers, eventually landing on a cork sheet which can be purchased at hobby stores for a few dollars. This had the right amount of compliance to make a clear mark without punching through the paper. We also created a mechanism to iteratively test spring compressions and travel distances by having an adjustable spring and head offset, allowing us to efficiently explore a large design space. We 3D-printed multiple punch heads to ensure the best tactile experience. Reflection It was so fun to build the braille printer together, combining each of our unique focus areas. Scott is an EE, Lawton is a MechE, Cathy specializes in AI, and Jason in front-end, resulting in a full-stack team. Building parts of the project and handing them off to each other felt like handing off the baton in a relay race. We’re proud that the resultant product works with a high degree of accuracy, and we were surprised at how low we were able to make the total cost of materials. On the software front, we're proud to be able to make all document formats accessible, from PDFs and word documents to scanned images using OCR. Above all, we're most proud of creating something that could make a real difference in people's lives. Building an affordable, reliable braille printer that can handle multiple document types means more visually impaired people can have access to printed materials in their preferred format.

### What's next

We plan to open-source our work, so anyone seeking to build a braille printer can do so at low cost. We will include detailed documentation, files, and backend code logic, 3D printed prototypes, such that anyone can replicate our build.

## README (from the GitHub repository)

# BrailleBot: Turn a 3D Printer to a Braille Printer for $15

> Winner of Treehacks 2025 Best Hardware Hack
>
> Video intro: https://youtu.be/atzED5lXhCE

**BrailleBot** transforms any document into printed standard braille, well-suited for printing documents, textbooks, websites, or even Zoom lecture summaries. It’s the first printer that can take any document and convert it to an accessible text format including textual image descriptions using AI visual recognition. It then prints the braille using a novel spring-loaded embossing mechanism mounted on a low-cost 3D printer.

Today, the cheapest braille printers are around $1500. Our embossing mechanism costs less than $15, and is then mounted on any low-cost 3D printer. At this price point, our solution is almost 100x cheaper than existing options while maintaining high quality, empowering teachers and students by making learning materials accessible at an unprecedented low price point.


## How we built it

Hardware:
The “punch” mechanism is comprised of a series of 3D printed parts and powered by the printer’s extruder motor, making the modification incredibly fast and inexpensive. A snap action mechanism converts a quarter rotation of the extruder motor into a load and then rapid punch motion. A screw at the top allows the user to adjust the spring’s compression, thereby changing the depth of the punch. The stock extruder can create up to 10 punches a second. 

Software
On the backend, we leverage the Groq inference engine and Zoom API, using Llama3-8B and Llama3-11B Vision to transcribe documents, generate text descriptions of images, and summarize Zoom lecture transcripts with low latency. Then, we use our Python Flask backend to convert the ASCII PDF representations into braille letters and GCode instructions for the Marlin firmware (used by most 3D printers).
Commands are sent to the printer through USB serial.
We use a JS canvas to display a live realtime preview of the print.
The frontend is built with Next.js, Typescript, Chakra, and React.



## Detected evidence (automated analysis)

Indexed codebase: 38 recognized source files, 87 KB.
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (55 of 55)

```
.DS_Store
.gitignore
braille-printer-backend/.gitignore
braille-printer-backend/app.py
braille-printer-backend/app.spec
braille-printer-backend/example_pipeline.py
braille-printer-backend/flask_server_ai.py
braille-printer-backend/flask_server_no_ai.py
braille-printer-backend/Pipfile
braille-printer-backend/Pipfile.lock
braille-printer-backend/README.md
braille-printer-backend/utils/__init__.py
braille-printer-backend/utils/braille_to_gcode.py
braille-printer-backend/utils/pdf_extraction.py
braille-printer-backend/utils/printer.py
braille-printer-backend/utils/text_to_braille.py
braille-printer-frontend/.gitignore
braille-printer-frontend/assets/mac/icon.icns
braille-printer-frontend/main/renderer.d.ts
braille-printer-frontend/main/src/main.ts
braille-printer-frontend/main/src/preload.ts
braille-printer-frontend/main/tsconfig.json
braille-printer-frontend/package.json
braille-printer-frontend/README.md
braille-printer-frontend/renderer/.gitignore
braille-printer-frontend/renderer/app/layout.tsx
braille-printer-frontend/renderer/app/page.tsx
braille-printer-frontend/renderer/components/connector.tsx
braille-printer-frontend/renderer/components/ui/avatar.tsx
braille-printer-frontend/renderer/components/ui/checkbox.tsx
braille-printer-frontend/renderer/components/ui/close-button.tsx
braille-printer-frontend/renderer/components/ui/color-mode.tsx
braille-printer-frontend/renderer/components/ui/dialog.tsx
braille-printer-frontend/renderer/components/ui/drawer.tsx
braille-printer-frontend/renderer/components/ui/field.tsx
braille-printer-frontend/renderer/components/ui/file-upload.tsx
braille-printer-frontend/renderer/components/ui/input-group.tsx
braille-printer-frontend/renderer/components/ui/pdf-live.tsx
braille-printer-frontend/renderer/components/ui/pdf-preview.tsx
braille-printer-frontend/renderer/components/ui/popover.tsx
braille-printer-frontend/renderer/components/ui/provider.tsx
braille-printer-frontend/renderer/components/ui/radio.tsx
braille-printer-frontend/renderer/components/ui/slider.tsx
braille-printer-frontend/renderer/components/ui/toaster.tsx
braille-printer-frontend/renderer/components/ui/tooltip.tsx
braille-printer-frontend/renderer/eslint.config.mjs
braille-printer-frontend/renderer/next.config.ts
braille-printer-frontend/renderer/public/printer.lottie
braille-printer-frontend/renderer/renderer.d.ts
braille-printer-frontend/renderer/tsconfig.json
braille-printer-frontend/renderer/utils/api.ts
braille-printer-frontend/tsconfig.json
Pipfile
Pipfile.lock
README.md
```

### Dependencies

- braille-printer-frontend/package.json: @chakra-ui/react@^3.8.0, @emotion/react@^11.14.0, @eslint/eslintrc@^3, @fontsource-variable/literata@^5.1.1, @lottiefiles/dotlottie-react@^0.13.0, @types/node@^20, @types/react@^19, @types/react-dom@^19, concurrently@^9.1.2, cross-env@^7.0.3, dotenv@^16.4.7, electron@^34.2.0, electron-builder@^25.1.8, electron-default-menu@^1.0.2, electron-serve@^1.3.0, eslint@^9, eslint-config-next@15.1.7, next@^15.1.7, next-electron-rsc@^0.2.3, next-themes@^0.4.4, react@^19.0.0, react-dom@^19.0.0, react-icons@^5.4.0, tsc-alias@^1.8.10, tsc-watch@^6.2.1, typescript@^5, wait-on@^8.0.2

### Recent commits (newest first)

- Update README.md
- Update README.md
- Update README.md
- Create README.md
- zoom
- Rename to BrailleBot
- Smaller font
- Proper margins and offsets
- Unflip
- Merge pull request #12 from cathyzbn/live
- Minor color changes
- Merge branch 'main' into live
- Live view of pdf status
- add textarea
- fix cosmetic
- oops sorry scott
- fix merge issue
- Merge branch 'main' of https://github.com/cathyzbn/braille-printer
- button aesthetics
- Hide toolbar & navpanes

## Key source files (fetched from GitHub, selected and truncated for size)

### braille-printer-frontend/package.json

```
{
  "name": "braille-printer-frontend",
  "version": "0.1.0",
  "private": true,
  "main": "main/out/main.js",
  "scripts": {
    "dev": "cross-env NODE_ENV=development concurrently -k \"cross-env BROWSER=none npm run renderer:dev\" \"npm run electron:dev\"",
    "build": "npm run python:build && npm run build:main && npm run renderer:build",
    "start": "cross-env npm run electron",
    "build:main": "tsc -p main && tsc-alias -p main/tsconfig.json",
    "renderer:dev": "cd renderer && next dev --turbopack",
    "renderer:build": "cd renderer && next build",
    "renderer:start": "cd renderer && next start",
    "renderer:serve": "serve out",
    "renderer:lint": "cd renderer && next lint",
    "electron:dev": "cross-env NODE_ENV=development npm run build:main && wait-on tcp:3000 && electron .",
    "electron": "electron .",
    "python:build": "cd ../braille-printer-backend && pipenv run pyinstaller --clean --onedir app.py",
    "pack": "npm run build && electron-builder --dir",
    "dist": "npm run build && electron-builder",
    "builder": "electron-builder --dir"
  },
  "build": {
    "appId": "Braille-Bot",
    "productName": "BrailleBot",
    "extraResources": [
      {
        "from": "../braille-printer-backend",
        "to": "braille-printer-backend"
      }
    ],
    "asar": false,
    "win": {
      "target": [
        "nsis"
      ],
      "icon": "assets/win/icon.ico"
    },
    "nsis": {
      "oneClick": false,
      "perMachine": true,
      "allowToChangeInstallationDirectory": true,
      "uninstallDisplayName": "BrailleBot"
    },
    "mac": {
      "category": "public.app-category.utilities",
      "target": [
        "dmg"
      ],
      "icon": "assets/mac/icon.icns"
    },
    "dmg": {
      "title": "BrailleBot Installer"
    }
  },
  "dependencies": {
    "@chakra-ui/react": "^3.8.0",
    "@emotion/react": "^11.14.0",
    "@fontsource-variable/literata": "^5.1.1",
    "@lottiefiles/dotlottie-react": "^0.13.0",
    "dotenv": "^16.4.7",
    "electron-default-menu": "^1.0.2",
    "electron-serve": "^1.3.0",
    "next": "^15.1.7",
    "next-electron-rsc": "^0.2.3",
    "next-themes": "^0.4.4",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-icons": "^5.4.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "concurrently": "^9.1.2",
    "cross-env": "^7.0.3",
    "electron": "^34.2.0",
    "electron-builder": "^25.1.8",
    "eslint": "^9",
    "eslint-config-next": "15.1.7",
    "tsc-alias": "^1.8.10",
    "tsc-watch": "^6.2.1",
    "typescript": "^5",
    "wait-on": "^8.0.2"
  }
}

```

### braille-printer-backend/app.py

```python
from flask_server_ai import app

if __name__ == "__main__":
    app.run(port=6969, debug=False)
```

### braille-printer-frontend/renderer/app/layout.tsx

```typescript
import { Provider } from "../components/ui/provider";
import { Literata, Mulish } from "next/font/google";

const literata = Literata({
  variable: "--font-literata",
  subsets: ["latin"],
});

const mulish = Mulish({
  variable: "--font-mulish",
  subsets: ["latin"],
  weight: "400",
});

export default function RootLayout(props: { children: React.ReactNode }) {
  const { children } = props;
  return (
    <html
      className={`${literata.variable} ${mulish.variable}`}
      suppressHydrationWarning
    >
      <body>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}

```

### braille-printer-frontend/main/src/main.ts

```typescript
// Main File for Electron

import { app, BrowserWindow, ipcMain } from "electron";
import path from "path";
import serve from "electron-serve";
import child_process from "child_process";

import dotenv from "dotenv";

dotenv.config({
  path: app.isPackaged
    ? path.join(process.resourcesPath, ".env")
    : path.resolve(process.cwd(), ".env"),
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function handleSetTitle(event: any, title: string) {
  const webContents = event.sender;
  const win = BrowserWindow.fromWebContents(webContents);
  if (win !== null) {
    win.setTitle(title);
  }
}

// run renderer
const isProd = process.env.NODE_ENV !== "development";
if (isProd) {
  serve({ directory: "renderer/out" });
} else {
  app.setPath("userData", `${app.getPath("userData")} (development)`);
}

const iconPath = app.isPackaged ? path.join(
  process.resourcesPath,
  "assets/png/1024x1024.png"
) : path.join(__dirname, "../../assets/png/1024x1024.png");

const createWindow = () => {
  const win = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      devTools: !isProd,
    },
    show: false,
    icon: iconPath
  });

  // Expose URL
  if (isProd) {
    win.loadURL("app://./home.html");
  } else {
    // const port = process.argv[2];
    win.loadURL("http://localhost:3000/");
  }

  win.webContents.on("did-finish-load", () => {
    win.maximize();
    win.show();
  });
};

const backendPath = path.join(process.resourcesPath, "braille-printer-backend/dist/app/app");

const backendDir = app.isPackaged
  ? path.dirname(backendPath)
  : path.join(__dirname, "../../../braille-printer-backend");

const env = {
  ...process.env,
  ANTHROPIC_API_KEY: "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",
  GROQ_API_KEY: "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"
}

app.whenReady().then(() => {
  const subpy = app.isPackaged
    ? child_process.spawn(backendPath, { env })
    : child_process.spawn('pipenv', ['run', 'python', 'flask_server_ai.py'], { cwd: backendDir, env });

  // handle stdout and stderr
  subpy.stdout.on("data", (data) => {
    console.log("Python: ", data.toString());
  });
  subpy.stderr.on("data", (data) => {
    console.error("Python: ", data.toString());
  });

  subpy.on("close", (code) => {
    console.log(`Python process exited with code ${code}`);
  });

  subpy.on("error", (error) => {
    console.error("Python error: ", error);
  });

  ipcMain.on("set-title", handleSetTitle);
  createWindow();

  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });

  app.on("before-quit", () => {
    subpy.kill();
  });
});

app.on("window-all-closed", () => {
  app.quit();
});
```

### braille-printer-frontend/renderer/app/page.tsx

```typescript
"use client";
import { useEffect, useState } from "react";
import {
  Heading,
  HStack,
  Text,
  VStack,
  Button,
  Textarea,
  Spinner,
  Icon,
} from "@chakra-ui/react";

import { toaster, Toaster } from "../components/ui/toaster";
import { DotPositions } from "../components/ui/pdf-preview";
import { PDFLive } from "../components/ui/pdf-live";
import dynamic from "next/dynamic";
import { Connector } from "../components/connector";
import { fetchApi } from "../utils/api";
import {
  FileUploadList,
  FileUploadRoot,
  FileUploadTrigger,
} from "../components/ui/file-upload";

const FaStop = dynamic(
  () => import("react-icons/fa").then((mod) => mod.FaStop),
  { ssr: false }
);
const FaPlay = dynamic(
  () => import("react-icons/fa").then((mod) => mod.FaPlay),
  { ssr: false }
);
const FaPause = dynamic(
  () => import("react-icons/fa").then((mod) => mod.FaPause),
  { ssr: false }
);

const DotLottieReact = dynamic(
  () =>
    import("@lottiefiles/dotlottie-react").then((mod) => mod.DotLottieReact),
  { ssr: false }
);

export default function Home() {
  const [pdfFile, setPdfFile] = useState<File | null>(null);
  const [isProcessingPdf, setIsProcessingPdf] = useState(false);
  const [text, setText] = useState("");
  const [dotPositions, setDotPositions] = useState<DotPositions>([]);
  const [currPage, setCurrPage] = useState(0);
  const [isPrinting, setIsPrinting] = useState(false);
  const [isConnected, setIsConnected] = useState(false);

  const submitText = async () => {
    if (!text.trim()) return;
    const formData = new FormData();
    formData.append("type", "text");
    formData.append("text", text);
    setIsProcessingPdf(true);
    const response = await fetchApi("/", {
      method: "POST",
      body: formData,
    });
    if (response) {
      toaster.success({
        title: "Success",
        description: "Text transcribed successfully!",
      });
      const braillePositions: DotPositions = await response.json();
      setDotPositions(braillePositions);
      setCurrPage(0);
    }
    setIsProcessingPdf(false);
  };

  const submitPdf = async () => {
    if (!pdfFile) return;
    setIsProcessingPdf(true);
    const formData = new FormData();
    formData.append("file", pdfFile);
    formData.append("type", "pdf");
    const response = await fetchApi("/", {
      method: "POST",
      body: formData,
    });
    if (response) {
      toaster.success({
        title: "Success",
        description: "PDF file transcribed successfully!",
      });
      const braillePositions: DotPositions = await response.json();
      setDotPositions(braillePositions);
      setCurrPage(0);
    }
    setIsProcessingPdf(false);
  };

  async function printCurrentPage() {
    if (!dotPositions[currPage]) return;
    setIsPrinting(true);
    const response = await fetchApi("/print_dots", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ dotPositions: dotPositions[currPage] }),
    });
    if (response) {
      toaster.success({
        title: "Printing",
        description: `Page ${currPage + 1} is printing...`,
      });
    }
    setIsPrinting(false);
  }

  useEffect(() => {
    window.electronAPI?.setTitle("BrailleBot");
  }, []);

  return (
    <VStack alignContent="center" p={3}>
      <Toaster />
      <VStack
        borderBottom="1px solid"
        borderColor="gray.300"
        w="100%"
        p={5}
        gapY={5}
      >
        <HStack>
          <Heading fontSize="6xl">BrailleBot</Heading>
        </HStack>
        <HStack>
          <Text fontSize="2xl">Any PDF to printed braille</Text>
        </HStack>
      </VStack>

      <Connector isConnected={isConnected} setIsConnected={setIsConnected} />

      {dotPositions.length === 0 && (
        <DotLottieReact
          src="./printer.lottie"
          loop
          autoplay
          style={{ width: "400px", height: "200px" }}
        />
      )}

      {isConnected && dotPositions.length === 0 && (
        <VStack w="80%" p={3}>
          <Text fontSize="xl">Enter text or upload a PDF</Text>

          {/* TEXT INPUT (disabled if a PDF is already chosen) */}
          <Textarea
            placeholder="Enter text here"
            value={text}
            onChange={(e) => {
              setText(e.target.value);
              if (e.target.value) {
                // if user starts typing, remove any selected PDF
                setPdfFile(null);
              }
            }}
            disabled={!!pdfFile}
          />

          {/* SUBMIT BUTTONS */}
          <HStack w="100%" alignItems="start" justifyContent="flex-end">
            <FileUploadRoot
              accept=".pdf"
              onChange={(event) => {
                const files = (event.target as HTMLInputElement).files;
                if (files && files.length > 0) {
                  setPdfFile(files[0]);
                  setText(""); // if user chooses a PDF, clear any typed text
                }
              }}
            >
              <FileUploadTrigger asChild>
                <Button
                  variant="outline"
                  disabled={isProcessingPdf || !!text}
                  // leftIcon={<HiUpload />}
                >
                  Attach File
                </Button>
              </FileUploadTrigger>
              <FileUploadList />
            </FileUploadRoot>
            <Button
              onClick={() => {
                if (text.trim()) {
                  submitText();
                } else {
                  submitPdf();
                }
              }}
            >
              Submit
            </Button>
          </HStack>

          {isProcessingPdf && (
            <HStack mt={2}>
              <Spinner size="sm" />
              <Text>Processing...</Text>
            </HStack>
          )}
        </VStack>
      )}

      {isConnected && dotPositions.length !== 0 && (
        <VStack w="50%" p={3}>
          <HStack justifyConten
[truncated — 2034 more characters]
```

### braille-printer-backend/example_pipeline.py

```python
from utils.braille_to_gcode import get_dots_pos_and_page, dot_pos_to_gcode, dot_pos_to_pdf
from utils.pdf_extraction import extract_text_from_pdf
from utils.printer import print_gcode

import sys
sys.path.append('utils')


if __name__ == "__main__":
    # Convert text to braille
    # text = text_to_braille("Hello my name is Cathy")
    # print("Braille string:", text)

    text = extract_text_from_pdf("test.pdf", input_type="path")
    
    # Get dot positions for each page
    dot_positions = get_dots_pos_and_page(text)
    
    # Generate PDF preview
    pdf = dot_pos_to_pdf([dot for page in dot_positions for dot in page])
    pdf.output("test_output.pdf")
    print("Generated PDF preview as test_output.pdf")
    
    # Generate and print GCODE
    for page_num, page in enumerate(dot_positions):
        print(f"\nGenerating GCODE for page {page_num + 1}:")
        actions = dot_pos_to_gcode(page)
        print_gcode(actions)
        for action in actions:
            print(str(action))

```

### braille-printer-backend/flask_server_no_ai.py

```python
from flask import Flask, request, jsonify
from PyPDF2 import PdfReader
from flask_cors import CORS

from utils.text_to_braille import text_to_braille

app = Flask(__name__)
CORS(app)  # Allow requests from your Next.js app

@app.route('/', methods=['POST'])
def handle_input():
    # If the request is multipart/form-data, treat it as a PDF upload.
    if request.content_type.startswith("multipart/form-data"):
        if 'file' in request.files:
            pdf_file = request.files['file']
            # Optionally, check for form field "type" if desired:
            # if request.form.get("type") != "pdf": return jsonify({"error": "Invalid type"}), 400

            pdf_reader = PdfReader(pdf_file)
            extracted_text = ""
            for page in pdf_reader.pages:
                extracted_text += page.extract_text() or ""
            braille_output = text_to_braille(extracted_text)
            print("Braille (PDF):", braille_output)
            return jsonify({"braille": braille_output}), 200
        return jsonify({"error": "No file provided"}), 400

    # If the request is JSON, treat it as a text submission.
    elif request.is_json:
        data = request.get_json()
        if data.get('type') == 'text':
            user_text = data.get('payload', "")
            braille_output = text_to_braille(user_text)
            print("Braille (Text):", braille_output)
            return jsonify({"braille": braille_output}), 200
        return jsonify({"error": "Invalid JSON payload"}), 400

    return jsonify({"error": "Unsupported Content-Type"}), 400


if __name__ == '__main__':
    app.run(port=6969, debug=True)

```

### braille-printer-backend/flask_server_ai.py

```python
from io import BytesIO
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from dotenv import load_dotenv
import anthropic
import atexit

from utils.pdf_extraction import extract_text_from_pdf, extract_text_from_zoom
from utils.text_to_braille import text_to_braille
from utils.braille_to_gcode import DotPosition, dot_pos_to_pdf, get_dots_pos_and_page, dot_pos_to_gcode, printed_dots
from utils.printer import PrinterConnection, PrintStatus, pause_print, print_gcode, resume_print, stop_print

DEBUG = False

load_dotenv()
client = anthropic.Anthropic()

app = Flask(__name__)
CORS(app)

printer = None

# pdf to dot positions
# right now it's pdf to ascii


@app.route('/', methods=['POST'])
def handle_input():
    if 'file' in request.files:
        pdf_file = request.files['file']
        pdf_bytes = pdf_file.read()
        transcript = extract_text_from_pdf(pdf_bytes)
    elif 'text' in request.form:
        text = request.form['text']
        if text.startswith('https://') and 'zoom.us' in text:
            transcript = extract_text_from_zoom(text)
        else:
            transcript = text
            
    if DEBUG:
        print("DEBUG: transcript", transcript)
    braille = text_to_braille(transcript)
    dots_pos = get_dots_pos_and_page(braille)
    return jsonify(dots_pos), 200


@app.route('/connect', methods=['POST'])
def handle_connect():
    global printer
    data = request.get_json()
    try:
        printer = PrinterConnection(data["port"], data["baudRate"])
        printer.connect()
    except Exception as e:
        printer.status = PrintStatus.ERROR
        return jsonify({"error": str(e)}), 500
    return jsonify({"success": True}), 200


@app.route('/disconnect', methods=['POST'])
def handle_disconnect():
    global printer
    if printer is not None:
        printer.close()
        printer = None
    return jsonify({"success": True}), 200


@app.route('/dot_pos_to_pdf', methods=['POST'])
def handle_dot_pos_to_pdf():
    data = request.get_json()
    # data["dotPositions"] can be either a list of DotPositions or list of list-of-DotPositions
    # If you only sent the single page, it's a list of DotPositions
    dot_positions = data["dotPositions"]
    dot_positions = [DotPosition(**dot_dict) for dot_dict in dot_positions]

    # If your 'dot_positions' is a nested list, you might need to flatten it:
    # flatten_dot_positions = [dot for page in dot_positions for dot in page]
    #
    # If it's already a single list, skip flattening.

    pdf = dot_pos_to_pdf(dot_positions)
    pdf_bytes = pdf.output(dest='S').encode('latin1')
    return send_file(
        BytesIO(pdf_bytes),
        mimetype="application/pdf",
        as_attachment=True,
        download_name="braille.pdf"
    )


@app.route('/printed_dots', methods=['POST'])
def handle_printed_dots():
    return jsonify(printed_dots.dots), 200


@app.route('/print_dots', methods=['POST'])
def handle_print_dots():
    data = request.get_json()

    dot_positions = data["dotPositions"]
    dot_positions = [DotPosition(**dot_dict) for dot_dict in dot_positions]

    # convert from
    actions = dot_pos_to_gcode(dot_positions)
    print_gcode(actions, printer)
    return jsonify({"success": True}), 200


@app.route('/stop_print', methods=['POST'])
def handle_stop_print():
    stop_print(printer)
    printed_dots.clear()
    return jsonify({"success": True}), 200


@app.route('/pause_print', methods=['POST'])
def handle_pause_print():
    pause_print(printer)
    return jsonify({"success": True}), 200


@app.route('/resume_print', methods=['POST'])
def handle_resume_print():
    resume_print(printer)
    return jsonify({"success": True}), 200


def cleanup():
    global printer
    if printer is not None:
        try:
            printer.close()
            printer = None
        except Exception as e:
            print(f"Error disconnecting printer: {e}")


atexit.register(cleanup)

if __name__ == '__main__':
    app.run(port=6969, debug=True)

```

### braille-printer-frontend/main/renderer.d.ts

```typescript
import { electronAPI } from "./src/preload";

declare global {
  interface Window {
    electronAPI: typeof electronAPI;
  }
}

export {};
```

### braille-printer-frontend/renderer/renderer.d.ts

```typescript
import { electronAPI } from "../main/preload";

declare global {
  interface Window {
    electronAPI: (typeof electronAPI) | undefined;
  }
}

```

[25 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]