├── .DS_Store
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── README.md
├── Test Notebook.ipynb
├── assets
├── 0.png
├── 1.png
├── 2.png
├── 3.png
└── demo.png
├── drawbook
├── .DS_Store
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-312.pyc
│ └── core.cpython-312.pyc
└── core.py
├── project_structure
├── pyproject.toml
├── requirements.txt
├── test.pptx
├── tests
└── test_core.py
└── version.txt
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/.DS_Store
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to PyPI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - 'version.txt'
9 |
10 | jobs:
11 | publish:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Set up Python
17 | uses: actions/setup-python@v4
18 | with:
19 | python-version: '3.x'
20 |
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install build twine
25 |
26 | - name: Build package
27 | run: python -m build
28 |
29 | - name: Publish to PyPI
30 | env:
31 | TWINE_USERNAME: __token__
32 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
33 | run: |
34 | python -m twine upload dist/*
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Jupyter Notebook checkpoints
2 | .ipynb_checkpoints
3 | */.ipynb_checkpoints/*
4 |
5 | # Python cache files
6 | __pycache__/
7 | *.py[cod]
8 |
9 | # Dist files
10 | dist/
11 | drawbook.egg-info/
12 | # Mac system files
13 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Drawbook
2 |
3 | `drawbook` is a Python library that helps you create illustrated children's books using AI. It leverages image generation AI models to generate watercolor-style illustrations corresponding to text that you have written and then exports them to a PowerPoint / Slides file that you can further edit.
4 |
5 | 
6 |
7 | ## Features
8 | - **AI-Generated Illustrations**: Automatically create watercolor illustrations based on the text you provide.
9 | - **Create Illustrations Programmatically or with a User-Firendly UI**: Create illustrations with a single line of Python -- `book.illustrate()` --, or a open up a Gradio UI in your browser -- `book.preview()` -- to have more fine-grained control over the illustrations.
10 | - **Start Quickly, Refine Later**: Export your illustrations a presentation (PowerPoint/Google Slides) that serves as a starting point - you can then change the layouts, images, and text to perfect your final design.
11 | - **Free**: This project uses the free Hugging Face Inference API to generate illustrations. We strongly recommend having a Hugging Face Pro account so that you do not get rate-limited.
12 |
13 | ## Prerequisites
14 | * Python 3.10+
15 | * Hugging Face Pro membership (strongly recommended, as this project uses the free Hugging Face Inference API)
16 |
17 | ## Installation
18 | To install Drawbook, use `pip`:
19 |
20 | ```bash
21 | pip install drawbook
22 | ```
23 |
24 | ## Usage
25 | Here’s how you can create an illustrated book using Drawbook in a few lines of Python:
26 |
27 | ```python
28 | from drawbook import Book
29 |
30 | book = Book(title="Mustafa's Trip to Mars", pages=[
31 | "One day, Mustafa climbs into the attic and finds a white spacesuit.",
32 | "He puts on the spacesuit and the spacesuit starts to glow!",
33 | "Mustafa starts to float up into the air in his spacesuit. He waves bye-bye to his house as it gets tiny down below.",
34 | "The stars look like tiny lights all around him. His spacesuit flies fast past the moon and the sun.",
35 | "Mustafa lands on Mars. The landing makes a big crater in the Martian surface.",
36 | "Mustafa explores Mars in his spacesuit. He meets a Martian and waves hello to him.",
37 | "Mustafa and the Martian play with toys on Mars. They have a great time together!",
38 | ], author="Abubakar Abid")
39 |
40 | book.illustrate() # Generates illustrations for every page
41 |
42 | book.export("Mustafas_Trip_To_Mars.pptx")
43 | ```
44 |
45 | When you run the code above, Drawbook will generate a PowerPoint file (`Mustafas_Trip_To_Mars.pptx`) that contains:
46 | - Text content formatted across multiple slides.
47 | - AI-generated watercolor illustrations that match the content of each page.
48 |
49 | ## Preview & Refine
50 |
51 | If you'd like to regenerate the illustrations on any specific page, simply run:
52 |
53 | ```
54 | book.preview()
55 | ```
56 |
57 | This will launch a [Gradio demo](https://gradio.dev/) that lets you see the prompt used to create each illustration. You can edit the prompt and keep re-generating images until you have a great series of illustrations for your book. Once you're down, just click "Export" and then "Download" to get your exported slides.
58 |
59 | 
60 |
61 | ## Contributing
62 | Contributions to `drawbook` are welcome! If you have ideas for new features or improvements, feel free to submit an issue or pull request on the [GitHub repository](#).
63 |
64 | ## License
65 | Drawbook is open-source software licensed under the [MIT License](LICENSE).
66 |
67 | ## Version
68 | The current version can be found in `version.txt` in the root directory, or accessed programmatically via:
69 |
--------------------------------------------------------------------------------
/Test Notebook.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "id": "6eb14615-5dc3-47a8-9a9b-40b28c56e1b3",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "from drawbook import Book\n",
11 | "\n",
12 | "book = Book(title=\"Mustafa's Trip to Mars\", pages=[\n",
13 | " \"One day, Mustafa climbs into the attic and finds a white spacesuit.\",\n",
14 | " \"He puts on the spacesuit and the spacesuit starts to glow!\",\n",
15 | " \"Mustafa starts to float up into the air in his spacesuit. He waves bye-bye to his house as it gets tiny down below.\",\n",
16 | " \"The stars look like tiny lights all around him. His spacesuit flies fast past the moon and the sun.\",\n",
17 | " \"Mustafa lands on Mars. The landing makes a big crater in the Martian surface.\",\n",
18 | " \"Mustafa explores Mars in his spacesuit. He meets a Martian and waves hello to him.\",\n",
19 | " \"Mustafa and the Martian play with toys on Mars. They have a great time together!\",\n",
20 | "], author=\"Abubakar Abid\")"
21 | ]
22 | },
23 | {
24 | "cell_type": "code",
25 | "execution_count": 2,
26 | "id": "2c882b0f",
27 | "metadata": {},
28 | "outputs": [
29 | {
30 | "name": "stdout",
31 | "output_type": "stream",
32 | "text": [
33 | "Generating illustrations... This could take a few minutes.\n"
34 | ]
35 | },
36 | {
37 | "name": "stderr",
38 | "output_type": "stream",
39 | "text": [
40 | "Generating illustrations: 0%| | 0/8 [00:00, ?it/s]"
41 | ]
42 | },
43 | {
44 | "name": "stdout",
45 | "output_type": "stream",
46 | "text": [
47 | "\n",
48 | "=== Processing title ===\n",
49 | "Original text: Mustafa's Trip to Mars\n",
50 | "Title illustration prompt: A boy traveling to Mars\n",
51 | "Final title image prompt: A AQUACOLTOK watercolor painting with a white background of: A boy traveling to Mars\n"
52 | ]
53 | },
54 | {
55 | "name": "stderr",
56 | "output_type": "stream",
57 | "text": [
58 | "Generating illustrations: 12%|█▎ | 1/8 [01:40<11:40, 100.10s/it]"
59 | ]
60 | },
61 | {
62 | "name": "stdout",
63 | "output_type": "stream",
64 | "text": [
65 | "Image saved to: /var/folders/kf/ll6fqkys7_s05x4tvb546mdc0000gn/T/tmp5n1etyw9/title.png\n",
66 | "\n",
67 | "=== Processing page_1 ===\n",
68 | "Original text: One day, Mustafa climbs into the attic and finds a white spacesuit.\n",
69 | "Illustration prompt: A child climbing into an attic and finding a white spacesuit.\n",
70 | "Final image prompt: A AQUACOLTOK watercolor painting with a white background of: A child climbing into an attic and finding a white spacesuit.\n"
71 | ]
72 | },
73 | {
74 | "name": "stderr",
75 | "output_type": "stream",
76 | "text": [
77 | "Generating illustrations: 25%|██▌ | 2/8 [02:44<07:54, 79.11s/it] "
78 | ]
79 | },
80 | {
81 | "name": "stdout",
82 | "output_type": "stream",
83 | "text": [
84 | "Image saved to: /var/folders/kf/ll6fqkys7_s05x4tvb546mdc0000gn/T/tmp5n1etyw9/page_1.png\n",
85 | "\n",
86 | "=== Processing page_2 ===\n",
87 | "Original text: He puts on the spacesuit and the spacesuit starts to glow!\n",
88 | "Illustration prompt: A glowing spacesuit\n",
89 | "Final image prompt: A AQUACOLTOK watercolor painting with a white background of: A glowing spacesuit\n"
90 | ]
91 | },
92 | {
93 | "name": "stderr",
94 | "output_type": "stream",
95 | "text": [
96 | "Generating illustrations: 38%|███▊ | 3/8 [03:38<05:37, 67.52s/it]"
97 | ]
98 | },
99 | {
100 | "name": "stdout",
101 | "output_type": "stream",
102 | "text": [
103 | "Image saved to: /var/folders/kf/ll6fqkys7_s05x4tvb546mdc0000gn/T/tmp5n1etyw9/page_2.png\n",
104 | "\n",
105 | "=== Processing page_3 ===\n",
106 | "Original text: Mustafa starts to float up into the air in his spacesuit. He waves bye-bye to his house as it gets tiny down below.\n",
107 | "Illustration prompt: A boy in a spacesuit floating up into the air, waving bye-bye to a tiny house below.\n",
108 | "Final image prompt: A AQUACOLTOK watercolor painting with a white background of: A boy in a spacesuit floating up into the air, waving bye-bye to a tiny house below.\n"
109 | ]
110 | },
111 | {
112 | "name": "stderr",
113 | "output_type": "stream",
114 | "text": [
115 | "Generating illustrations: 50%|█████ | 4/8 [04:02<03:20, 50.24s/it]"
116 | ]
117 | },
118 | {
119 | "name": "stdout",
120 | "output_type": "stream",
121 | "text": [
122 | "Image saved to: /var/folders/kf/ll6fqkys7_s05x4tvb546mdc0000gn/T/tmp5n1etyw9/page_3.png\n",
123 | "\n",
124 | "=== Processing page_4 ===\n",
125 | "Original text: The stars look like tiny lights all around him. His spacesuit flies fast past the moon and the sun.\n",
126 | "Illustration prompt: A spacesuit flying past the moon and the sun\n",
127 | "Final image prompt: A AQUACOLTOK watercolor painting with a white background of: A spacesuit flying past the moon and the sun\n"
128 | ]
129 | },
130 | {
131 | "name": "stderr",
132 | "output_type": "stream",
133 | "text": [
134 | "Generating illustrations: 50%|█████ | 4/8 [04:19<04:19, 64.77s/it]\n"
135 | ]
136 | },
137 | {
138 | "ename": "KeyboardInterrupt",
139 | "evalue": "",
140 | "output_type": "error",
141 | "traceback": [
142 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
143 | "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
144 | "Cell \u001b[0;32mIn[2], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mbook\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43millustrate\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n",
145 | "File \u001b[0;32m~/dev/drawbook/drawbook/core.py:415\u001b[0m, in \u001b[0;36mBook.illustrate\u001b[0;34m(self, save_dir, page_num)\u001b[0m\n\u001b[1;32m 412\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m page_num \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 413\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFinal image prompt: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mprompt\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 415\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[43mrequests\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpost\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 416\u001b[0m \u001b[43m \u001b[49m\u001b[43mAPI_URL\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mjson\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43minputs\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mprompt\u001b[49m\u001b[43m}\u001b[49m\n\u001b[1;32m 417\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 419\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m response\u001b[38;5;241m.\u001b[39mstatus_code \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m200\u001b[39m:\n\u001b[1;32m 420\u001b[0m msg \u001b[38;5;241m=\u001b[39m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFailed to generate illustration for \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtask_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mresponse\u001b[38;5;241m.\u001b[39mtext\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n",
146 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/site-packages/requests/api.py:115\u001b[0m, in \u001b[0;36mpost\u001b[0;34m(url, data, json, **kwargs)\u001b[0m\n\u001b[1;32m 103\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mpost\u001b[39m(url, data\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, json\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 104\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124;03m\"\"\"Sends a POST request.\u001b[39;00m\n\u001b[1;32m 105\u001b[0m \n\u001b[1;32m 106\u001b[0m \u001b[38;5;124;03m :param url: URL for the new :class:`Request` object.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 112\u001b[0m \u001b[38;5;124;03m :rtype: requests.Response\u001b[39;00m\n\u001b[1;32m 113\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 115\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mpost\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mjson\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mjson\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
147 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/site-packages/requests/api.py:59\u001b[0m, in \u001b[0;36mrequest\u001b[0;34m(method, url, **kwargs)\u001b[0m\n\u001b[1;32m 55\u001b[0m \u001b[38;5;66;03m# By using the 'with' statement we are sure the session is closed, thus we\u001b[39;00m\n\u001b[1;32m 56\u001b[0m \u001b[38;5;66;03m# avoid leaving sockets open which can trigger a ResourceWarning in some\u001b[39;00m\n\u001b[1;32m 57\u001b[0m \u001b[38;5;66;03m# cases, and look like a memory leak in others.\u001b[39;00m\n\u001b[1;32m 58\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m sessions\u001b[38;5;241m.\u001b[39mSession() \u001b[38;5;28;01mas\u001b[39;00m session:\n\u001b[0;32m---> 59\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43msession\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
148 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/site-packages/requests/sessions.py:589\u001b[0m, in \u001b[0;36mSession.request\u001b[0;34m(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)\u001b[0m\n\u001b[1;32m 584\u001b[0m send_kwargs \u001b[38;5;241m=\u001b[39m {\n\u001b[1;32m 585\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtimeout\u001b[39m\u001b[38;5;124m\"\u001b[39m: timeout,\n\u001b[1;32m 586\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mallow_redirects\u001b[39m\u001b[38;5;124m\"\u001b[39m: allow_redirects,\n\u001b[1;32m 587\u001b[0m }\n\u001b[1;32m 588\u001b[0m send_kwargs\u001b[38;5;241m.\u001b[39mupdate(settings)\n\u001b[0;32m--> 589\u001b[0m resp \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msend\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprep\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43msend_kwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 591\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m resp\n",
149 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/site-packages/requests/sessions.py:703\u001b[0m, in \u001b[0;36mSession.send\u001b[0;34m(self, request, **kwargs)\u001b[0m\n\u001b[1;32m 700\u001b[0m start \u001b[38;5;241m=\u001b[39m preferred_clock()\n\u001b[1;32m 702\u001b[0m \u001b[38;5;66;03m# Send the request\u001b[39;00m\n\u001b[0;32m--> 703\u001b[0m r \u001b[38;5;241m=\u001b[39m \u001b[43madapter\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msend\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 705\u001b[0m \u001b[38;5;66;03m# Total elapsed time of the request (approximately)\u001b[39;00m\n\u001b[1;32m 706\u001b[0m elapsed \u001b[38;5;241m=\u001b[39m preferred_clock() \u001b[38;5;241m-\u001b[39m start\n",
150 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/site-packages/requests/adapters.py:667\u001b[0m, in \u001b[0;36mHTTPAdapter.send\u001b[0;34m(self, request, stream, timeout, verify, cert, proxies)\u001b[0m\n\u001b[1;32m 664\u001b[0m timeout \u001b[38;5;241m=\u001b[39m TimeoutSauce(connect\u001b[38;5;241m=\u001b[39mtimeout, read\u001b[38;5;241m=\u001b[39mtimeout)\n\u001b[1;32m 666\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 667\u001b[0m resp \u001b[38;5;241m=\u001b[39m \u001b[43mconn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43murlopen\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 668\u001b[0m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 669\u001b[0m \u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 670\u001b[0m \u001b[43m \u001b[49m\u001b[43mbody\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbody\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 671\u001b[0m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mrequest\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 672\u001b[0m \u001b[43m \u001b[49m\u001b[43mredirect\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 673\u001b[0m \u001b[43m \u001b[49m\u001b[43massert_same_host\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 674\u001b[0m \u001b[43m \u001b[49m\u001b[43mpreload_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 675\u001b[0m \u001b[43m \u001b[49m\u001b[43mdecode_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 676\u001b[0m \u001b[43m \u001b[49m\u001b[43mretries\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmax_retries\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 677\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 678\u001b[0m \u001b[43m \u001b[49m\u001b[43mchunked\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mchunked\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 679\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 681\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (ProtocolError, \u001b[38;5;167;01mOSError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m err:\n\u001b[1;32m 682\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mConnectionError\u001b[39;00m(err, request\u001b[38;5;241m=\u001b[39mrequest)\n",
151 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/site-packages/urllib3/connectionpool.py:789\u001b[0m, in \u001b[0;36mHTTPConnectionPool.urlopen\u001b[0;34m(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)\u001b[0m\n\u001b[1;32m 786\u001b[0m response_conn \u001b[38;5;241m=\u001b[39m conn \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m release_conn \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 788\u001b[0m \u001b[38;5;66;03m# Make the request on the HTTPConnection object\u001b[39;00m\n\u001b[0;32m--> 789\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_make_request\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 790\u001b[0m \u001b[43m \u001b[49m\u001b[43mconn\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 791\u001b[0m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 792\u001b[0m \u001b[43m \u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 793\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout_obj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 794\u001b[0m \u001b[43m \u001b[49m\u001b[43mbody\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbody\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 795\u001b[0m \u001b[43m \u001b[49m\u001b[43mheaders\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mheaders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 796\u001b[0m \u001b[43m \u001b[49m\u001b[43mchunked\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mchunked\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 797\u001b[0m \u001b[43m \u001b[49m\u001b[43mretries\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mretries\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 798\u001b[0m \u001b[43m \u001b[49m\u001b[43mresponse_conn\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mresponse_conn\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 799\u001b[0m \u001b[43m \u001b[49m\u001b[43mpreload_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpreload_content\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 800\u001b[0m \u001b[43m \u001b[49m\u001b[43mdecode_content\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdecode_content\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 801\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mresponse_kw\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 802\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 804\u001b[0m \u001b[38;5;66;03m# Everything went great!\u001b[39;00m\n\u001b[1;32m 805\u001b[0m clean_exit \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n",
152 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/site-packages/urllib3/connectionpool.py:536\u001b[0m, in \u001b[0;36mHTTPConnectionPool._make_request\u001b[0;34m(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)\u001b[0m\n\u001b[1;32m 534\u001b[0m \u001b[38;5;66;03m# Receive the response from the server\u001b[39;00m\n\u001b[1;32m 535\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 536\u001b[0m response \u001b[38;5;241m=\u001b[39m \u001b[43mconn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgetresponse\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 537\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (BaseSSLError, \u001b[38;5;167;01mOSError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m 538\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_raise_timeout(err\u001b[38;5;241m=\u001b[39me, url\u001b[38;5;241m=\u001b[39murl, timeout_value\u001b[38;5;241m=\u001b[39mread_timeout)\n",
153 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/site-packages/urllib3/connection.py:507\u001b[0m, in \u001b[0;36mHTTPConnection.getresponse\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 504\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mresponse\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m HTTPResponse\n\u001b[1;32m 506\u001b[0m \u001b[38;5;66;03m# Get the response from http.client.HTTPConnection\u001b[39;00m\n\u001b[0;32m--> 507\u001b[0m httplib_response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgetresponse\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 509\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 510\u001b[0m assert_header_parsing(httplib_response\u001b[38;5;241m.\u001b[39mmsg)\n",
154 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/http/client.py:1423\u001b[0m, in \u001b[0;36mHTTPConnection.getresponse\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1421\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1422\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1423\u001b[0m \u001b[43mresponse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbegin\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1424\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mConnectionError\u001b[39;00m:\n\u001b[1;32m 1425\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mclose()\n",
155 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/http/client.py:331\u001b[0m, in \u001b[0;36mHTTPResponse.begin\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 329\u001b[0m \u001b[38;5;66;03m# read until we get a non-100 response\u001b[39;00m\n\u001b[1;32m 330\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m--> 331\u001b[0m version, status, reason \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_read_status\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 332\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m status \u001b[38;5;241m!=\u001b[39m CONTINUE:\n\u001b[1;32m 333\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n",
156 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/http/client.py:292\u001b[0m, in \u001b[0;36mHTTPResponse._read_status\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 291\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_read_status\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[0;32m--> 292\u001b[0m line \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m(\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreadline\u001b[49m\u001b[43m(\u001b[49m\u001b[43m_MAXLINE\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124miso-8859-1\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 293\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(line) \u001b[38;5;241m>\u001b[39m _MAXLINE:\n\u001b[1;32m 294\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m LineTooLong(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstatus line\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
157 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/socket.py:707\u001b[0m, in \u001b[0;36mSocketIO.readinto\u001b[0;34m(self, b)\u001b[0m\n\u001b[1;32m 705\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[1;32m 706\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 707\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_sock\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrecv_into\u001b[49m\u001b[43m(\u001b[49m\u001b[43mb\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 708\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m timeout:\n\u001b[1;32m 709\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_timeout_occurred \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n",
158 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/ssl.py:1252\u001b[0m, in \u001b[0;36mSSLSocket.recv_into\u001b[0;34m(self, buffer, nbytes, flags)\u001b[0m\n\u001b[1;32m 1248\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m flags \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 1249\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 1250\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnon-zero flags not allowed in calls to recv_into() on \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m%\u001b[39m\n\u001b[1;32m 1251\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__class__\u001b[39m)\n\u001b[0;32m-> 1252\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnbytes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbuffer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1253\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 1254\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mrecv_into(buffer, nbytes, flags)\n",
159 | "File \u001b[0;32m~/.pyenv/versions/3.12.2/lib/python3.12/ssl.py:1104\u001b[0m, in \u001b[0;36mSSLSocket.read\u001b[0;34m(self, len, buffer)\u001b[0m\n\u001b[1;32m 1102\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1103\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m buffer \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m-> 1104\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_sslobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbuffer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1105\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 1106\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_sslobj\u001b[38;5;241m.\u001b[39mread(\u001b[38;5;28mlen\u001b[39m)\n",
160 | "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
161 | ]
162 | }
163 | ],
164 | "source": [
165 | "book.illustrate()"
166 | ]
167 | },
168 | {
169 | "cell_type": "code",
170 | "execution_count": 3,
171 | "id": "65584ca8",
172 | "metadata": {},
173 | "outputs": [
174 | {
175 | "name": "stdout",
176 | "output_type": "stream",
177 | "text": [
178 | "Creating preview...\n",
179 | "* Running on local URL: http://127.0.0.1:7860\n",
180 | "\n",
181 | "To create a public link, set `share=True` in `launch()`.\n"
182 | ]
183 | },
184 | {
185 | "data": {
186 | "text/html": [
187 | "
"
188 | ],
189 | "text/plain": [
190 | ""
191 | ]
192 | },
193 | "metadata": {},
194 | "output_type": "display_data"
195 | }
196 | ],
197 | "source": [
198 | "book.preview()"
199 | ]
200 | },
201 | {
202 | "cell_type": "code",
203 | "execution_count": 3,
204 | "id": "5451dab6-9bee-484b-af77-4cdff546a2ed",
205 | "metadata": {},
206 | "outputs": [
207 | {
208 | "name": "stdout",
209 | "output_type": "stream",
210 | "text": [
211 | "Book exported to: /Users/abidlabs/dev/drawbook/test.pptx\n"
212 | ]
213 | },
214 | {
215 | "data": {
216 | "text/plain": [
217 | "True"
218 | ]
219 | },
220 | "execution_count": 3,
221 | "metadata": {},
222 | "output_type": "execute_result"
223 | }
224 | ],
225 | "source": [
226 | "book.export(\"test.pptx\")\n",
227 | "import webbrowser\n",
228 | "webbrowser.open('file:///Users/abidlabs/dev/drawbook/test.pptx')"
229 | ]
230 | }
231 | ],
232 | "metadata": {
233 | "kernelspec": {
234 | "display_name": "Python 3.12",
235 | "language": "python",
236 | "name": "python3"
237 | },
238 | "language_info": {
239 | "codemirror_mode": {
240 | "name": "ipython",
241 | "version": 3
242 | },
243 | "file_extension": ".py",
244 | "mimetype": "text/x-python",
245 | "name": "python",
246 | "nbconvert_exporter": "python",
247 | "pygments_lexer": "ipython3",
248 | "version": "3.12.2"
249 | }
250 | },
251 | "nbformat": 4,
252 | "nbformat_minor": 5
253 | }
254 |
--------------------------------------------------------------------------------
/assets/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/assets/0.png
--------------------------------------------------------------------------------
/assets/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/assets/1.png
--------------------------------------------------------------------------------
/assets/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/assets/2.png
--------------------------------------------------------------------------------
/assets/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/assets/3.png
--------------------------------------------------------------------------------
/assets/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/assets/demo.png
--------------------------------------------------------------------------------
/drawbook/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/drawbook/.DS_Store
--------------------------------------------------------------------------------
/drawbook/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | DrawBook - A Python library for drawing
3 | """
4 |
5 | from .core import *
6 | from pathlib import Path
7 |
8 | def get_version():
9 | version_file = Path(__file__).parent.parent / 'version.txt'
10 | with open(version_file, 'r') as f:
11 | return f.read().strip()
12 |
13 | __version__ = get_version()
--------------------------------------------------------------------------------
/drawbook/__pycache__/__init__.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/drawbook/__pycache__/__init__.cpython-312.pyc
--------------------------------------------------------------------------------
/drawbook/__pycache__/core.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/drawbook/__pycache__/core.cpython-312.pyc
--------------------------------------------------------------------------------
/drawbook/core.py:
--------------------------------------------------------------------------------
1 | """
2 | Core functionality for the drawbook library.
3 | """
4 |
5 | from pathlib import Path
6 | from typing import List, Literal
7 | import tempfile
8 | import io
9 | import requests
10 | import warnings
11 | from tqdm import tqdm
12 | from PIL import Image
13 | import huggingface_hub
14 | from pptx import Presentation
15 | from pptx.util import Inches
16 | from pptx.enum.text import PP_ALIGN
17 | from pptx.enum.shapes import MSO_SHAPE
18 | from pptx.dml.color import RGBColor
19 | from huggingface_hub import InferenceClient
20 | from PIL import ImageDraw, ImageFont
21 | import gradio as gr
22 | import textwrap
23 | import json
24 |
25 |
26 | class Book:
27 | """A class representing a children's book that can be exported to PowerPoint."""
28 |
29 | def __init__(
30 | self,
31 | title: str = "Untitled Book",
32 | pages: List[str] = None,
33 | title_illustration: str | Literal[False] | None = None,
34 | illustrations: List[str | None | Literal[False]] = None,
35 | lora: str = "SebastianBodza/Flux_Aquarell_Watercolor_v2",
36 | author: str | None = None,
37 | illustration_prompts: List[str | None] = None,
38 | title_illustration_prompt: str | None = None,
39 | ):
40 | """
41 | Initialize a new Book.
42 |
43 | Args:
44 | title: The book's title
45 | pages: List of strings containing text for each page
46 | illustrations: List of illustration paths or placeholders
47 | (str for path, None for pending, False for no illustration)
48 | lora: The LoRA model on Hugging Face to use for illustrations
49 | author: The book's author name
50 | illustration_prompts: Optional list of custom prompts for page illustrations
51 | title_illustration_prompt: Optional custom prompt for title illustration
52 | """
53 | self.title = title
54 | self.pages = pages or []
55 | self.illustrations = illustrations or []
56 | self.lora = lora
57 | self.title_illustration = title_illustration
58 | self.author = author
59 | self.illustration_prompts = illustration_prompts or []
60 | self.title_illustration_prompt = title_illustration_prompt
61 | self.client = InferenceClient()
62 | self.page_previews = []
63 |
64 | # Ensure illustrations list matches pages length
65 | while len(self.illustrations) < len(self.pages):
66 | self.illustrations.append(None)
67 |
68 | # Ensure illustration_prompts list matches pages length
69 | while len(self.illustration_prompts) < len(self.pages):
70 | self.illustration_prompts.append(None)
71 |
72 | def _get_illustration_prompt(self, text: str) -> str:
73 | """Get an illustration prompt from the text using Qwen."""
74 | system_prompt = """You are a helpful assistant that converts children's book text into illustration prompts.
75 | Extract a key object along with its description that could be used to illustrate the page.
76 | Replace any proper names with more generic versions.
77 |
78 | For example:
79 | If the text is: "Mustafa loves his silver cybertruck. One day, his cybertruck starts to glow, grow, and zoom up into the sky"
80 | You should return: "A silver cybertruck zooming into the sky"
81 |
82 | If the text is: "Up, up, up goes Mustafa in his special cybertruck. He waves bye-bye to his house as it gets tiny down below"
83 | You should return: "A boy in the sky waving bye"
84 | """
85 |
86 | user_prompt = f"""This is the text of a page in a children's book. From this text, extract a key object along with its description that could be used to illustrate this page. Replace any proper names with more generic versions.
87 |
88 | Text: {text}
89 |
90 | Return ONLY the illustration description, nothing else."""
91 |
92 | messages = [
93 | {"role": "system", "content": system_prompt},
94 | {"role": "user", "content": user_prompt},
95 | ]
96 |
97 | try:
98 | stream = self.client.chat.completions.create(
99 | model="Qwen/Qwen2.5-72B-Instruct",
100 | messages=messages,
101 | max_tokens=500,
102 | stream=True,
103 | )
104 |
105 | response = ""
106 | for chunk in stream:
107 | if chunk.choices[0].delta.content is not None:
108 | response += chunk.choices[0].delta.content
109 |
110 | return response.strip()
111 | except Exception:
112 | gr.warning("Could not access Hugging Face Inference API, make sure that you are logged in locally to your Hugging Face account")
113 | return text
114 |
115 | def _get_prompt(self, illustration_prompt: str) -> str:
116 | if self.lora == "SebastianBodza/Flux_Aquarell_Watercolor_v2":
117 | return f"A AQUACOLTOK watercolor painting with a white background of: {illustration_prompt}"
118 | else:
119 | warnings.warn(
120 | f"The LoRA model '{self.lora}' is not officially supported. "
121 | "Results may not be as expected.",
122 | UserWarning,
123 | )
124 | return f"An illustration of: {illustration_prompt}"
125 |
126 | def export(self, filename: str | Path | None = None) -> None:
127 | """
128 | Export the book to a PowerPoint file.
129 |
130 | Args:
131 | filename: Optional path where to save the file. If None, creates in temp directory.
132 | """
133 | if filename is None:
134 | # Create temp file with .pptx extension
135 | temp_file = tempfile.NamedTemporaryFile(suffix=".pptx", delete=False)
136 | output_path = Path(temp_file.name)
137 | temp_file.close()
138 | else:
139 | # Convert to Path object and resolve to absolute path
140 | output_path = Path(filename).resolve()
141 | # Ensure parent directories exist
142 | output_path.parent.mkdir(parents=True, exist_ok=True)
143 |
144 | prs = Presentation()
145 |
146 | # Add title slide
147 | title_slide_layout = prs.slide_layouts[0]
148 | slide = prs.slides.add_slide(title_slide_layout)
149 |
150 | # Add diagonal striped border at the top
151 | border = slide.shapes.add_shape(
152 | MSO_SHAPE.RECTANGLE, Inches(0), Inches(0), Inches(0.2), Inches(7.5)
153 | )
154 | border.fill.solid()
155 | border.fill.fore_color.rgb = RGBColor(128, 0, 0) # Maroon
156 |
157 | # Add title illustration first if available
158 | if isinstance(self.title_illustration, str):
159 | try:
160 | slide.shapes.add_picture(
161 | self.title_illustration,
162 | Inches(2.5),
163 | Inches(1.5),
164 | Inches(5),
165 | Inches(5),
166 | )
167 | except Exception as e:
168 | print(f"Warning: Could not add title illustration: {e}")
169 |
170 | # Add title with adjusted positioning and z-order
171 | title = slide.shapes.title
172 | title.top = Inches(0)
173 | title.height = Inches(2.0)
174 | title.width = Inches(10)
175 |
176 | # Add title text
177 | p1 = title.text_frame.paragraphs[0]
178 | # Clear any existing text
179 | p1.clear()
180 | p1.font.name = "Trebuchet MS"
181 | p1.alignment = PP_ALIGN.CENTER
182 |
183 | # Define common stop words
184 | stop_words = {
185 | "a",
186 | "an",
187 | "and",
188 | "are",
189 | "as",
190 | "at",
191 | "be",
192 | "by",
193 | "for",
194 | "from",
195 | "has",
196 | "he",
197 | "in",
198 | "is",
199 | "it",
200 | "its",
201 | "of",
202 | "on",
203 | "that",
204 | "the",
205 | "to",
206 | "was",
207 | "were",
208 | "will",
209 | "with",
210 | }
211 |
212 | # Split title and add each word with appropriate size
213 | words = self.title.split()
214 | for i, word in enumerate(words):
215 | run = p1.add_run()
216 | run.text = word + (" " if i < len(words) - 1 else "")
217 | run.font.name = "Trebuchet MS"
218 | if word.lower() in stop_words:
219 | run.font.size = Inches(0.42) # Smaller size for stop words
220 | else:
221 | run.font.size = Inches(0.5) # Regular size for other words
222 |
223 | # Add author with adjusted positioning
224 | if self.author is not None:
225 | author_box = slide.shapes.add_textbox(
226 | Inches(0),
227 | Inches(6.5), # Moved up from bottom
228 | Inches(10),
229 | Inches(0.5),
230 | )
231 | author_frame = author_box.text_frame
232 | author_frame.text = f"Written by {self.author}"
233 | author_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
234 | author_frame.paragraphs[0].font.name = "Trebuchet MS"
235 | author_frame.paragraphs[0].font.size = Inches(0.25)
236 |
237 | # Add content slides
238 | content_slide_layout = prs.slide_layouts[5] # Blank layout
239 |
240 | for page_num, (text, illustration) in enumerate(
241 | zip(self.pages, self.illustrations)
242 | ):
243 | slide = prs.slides.add_slide(content_slide_layout)
244 |
245 | if isinstance(illustration, str):
246 | try:
247 | slide.shapes.add_picture(
248 | illustration, Inches(2.5), Inches(1.4), Inches(5), Inches(5)
249 | )
250 | except Exception as e:
251 | print(
252 | f"Warning: Could not add illustration on page {page_num + 1}: {e}"
253 | )
254 |
255 | # Split text into sentences and join with newlines
256 | sentences = text.replace(". ", ".\n").split("\n")
257 |
258 | # Special formatting for first page
259 | if page_num == 0 and text:
260 | # Split first character from first sentence
261 | first_char = sentences[0][0]
262 | first_sentence_rest = sentences[0][1:]
263 |
264 | p = slide.shapes.title.text_frame.paragraphs[0]
265 | p.line_spacing = 1.5 # Add line spacing
266 | run = p.add_run()
267 | run.text = first_char
268 | run.font.size = Inches(0.3)
269 | run.font.name = "Trebuchet MS"
270 |
271 | run = p.add_run()
272 | run.text = first_sentence_rest
273 | run.font.size = Inches(0.25)
274 | run.font.name = "Trebuchet MS"
275 |
276 | # Add remaining sentences as new paragraphs
277 | for sentence in sentences[1:]:
278 | p = slide.shapes.title.text_frame.add_paragraph()
279 | p.line_spacing = 1.5 # Add line spacing
280 | p.text = sentence
281 | p.font.name = "Trebuchet MS"
282 | p.font.size = Inches(0.25)
283 | p.alignment = PP_ALIGN.CENTER
284 | else:
285 | # Add each sentence as a separate paragraph
286 | first_paragraph = True
287 | for sentence in sentences:
288 | if first_paragraph:
289 | p = slide.shapes.title.text_frame.paragraphs[0]
290 | first_paragraph = False
291 | else:
292 | p = slide.shapes.title.text_frame.add_paragraph()
293 | p.line_spacing = 1.5 # Add line spacing
294 | p.text = sentence
295 | p.font.name = "Trebuchet MS"
296 | p.font.size = Inches(0.25)
297 | p.alignment = PP_ALIGN.CENTER
298 |
299 | # Add page number at bottom center
300 | page_number = page_num + 1 # Add 1 since page_num is 0-based
301 | page_num_box = slide.shapes.add_textbox(
302 | Inches(0),
303 | Inches(6.5), # Y position near bottom of slide
304 | Inches(10),
305 | Inches(0.5), # Full width of slide for centering
306 | )
307 | page_num_frame = page_num_box.text_frame
308 | page_num_frame.text = str(page_number)
309 | page_num_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
310 | page_num_frame.paragraphs[0].font.name = "Trebuchet MS"
311 | page_num_frame.paragraphs[0].font.size = Inches(
312 | 0.15
313 | ) # Slightly smaller than main text
314 |
315 | # Save the presentation
316 | prs.save(str(output_path))
317 | print(f"Book exported to: {output_path.absolute()}")
318 | return output_path.absolute()
319 |
320 | def __len__(self) -> int:
321 | """Return the number of pages in the book."""
322 | return len(self.pages)
323 |
324 | def illustrate(
325 | self, save_dir: str | Path | None = None, page_num: int | None = None
326 | ) -> str | None:
327 | """
328 | Generate illustrations using the Hugging Face Inference API.
329 |
330 | Args:
331 | save_dir: Optional directory to save the generated images.
332 | If None, creates a temporary directory.
333 | page_num: Optional specific page to illustrate (0 for title page, 1+ for content pages).
334 | If None, illustrates all pages.
335 |
336 | Returns:
337 | Status message if page_num is specified, None otherwise.
338 | """
339 | token = huggingface_hub.get_token()
340 | if not token:
341 | msg = "No Hugging Face token found. Please login using `huggingface-cli login`"
342 | if page_num is not None:
343 | return f"Error: {msg}"
344 | warnings.warn(msg)
345 |
346 | API_URL = f"https://api-inference.huggingface.co/models/{self.lora}"
347 | headers = {"Authorization": f"Bearer {token}"}
348 |
349 | # Create save directory if provided
350 | if save_dir:
351 | save_dir = Path(save_dir)
352 | save_dir.mkdir(parents=True, exist_ok=True)
353 | else:
354 | save_dir = Path(tempfile.mkdtemp())
355 |
356 | # Create list of tasks
357 | if page_num is not None:
358 | if page_num == 0:
359 | tasks = [("title", self.title, self.title_illustration)]
360 | else:
361 | page_idx = page_num - 1
362 | tasks = [
363 | (
364 | f"page_{page_num}",
365 | self.pages[page_idx],
366 | self.illustrations[page_idx],
367 | )
368 | ]
369 | else:
370 | print("Generating illustrations... This could take a few minutes.")
371 | tasks = []
372 | if self.title_illustration is None:
373 | tasks.append(("title", self.title, None))
374 | tasks.extend(
375 | (f"page_{i+1}", text, current_illust)
376 | for i, (text, current_illust) in enumerate(
377 | zip(self.pages, self.illustrations)
378 | )
379 | )
380 |
381 | for task_name, text, current_illust in tqdm(
382 | tasks, desc="Generating illustrations", disable=page_num is not None
383 | ):
384 | # Skip if illustration already exists or is explicitly disabled
385 | if isinstance(current_illust, str) or current_illust is False:
386 | continue
387 |
388 | try:
389 | if page_num is None:
390 | print(f"\n=== Processing {task_name} ===")
391 | print(f"Original text: {text}")
392 |
393 | if task_name == "title":
394 | if not self.title_illustration_prompt:
395 | self.title_illustration_prompt = self._get_illustration_prompt(
396 | text
397 | )
398 | if page_num is None:
399 | print(
400 | f"Title illustration prompt: {self.title_illustration_prompt}"
401 | )
402 | prompt = self._get_prompt(self.title_illustration_prompt)
403 | if page_num is None:
404 | print(f"Final title image prompt: {prompt}")
405 | else:
406 | page_idx = int(task_name.split("_")[1]) - 1
407 | if not self.illustration_prompts[page_idx]:
408 | self.illustration_prompts[page_idx] = (
409 | self._get_illustration_prompt(text)
410 | )
411 | if page_num is None:
412 | print(
413 | f"Illustration prompt: {self.illustration_prompts[page_idx]}"
414 | )
415 | prompt = self._get_prompt(self.illustration_prompts[page_idx])
416 | if page_num is None:
417 | print(f"Final image prompt: {prompt}")
418 |
419 | response = requests.post(
420 | API_URL, headers=headers, json={"inputs": prompt}
421 | )
422 |
423 | if response.status_code != 200:
424 | msg = f"Failed to generate illustration for {task_name}: {response.text}"
425 | if page_num is not None:
426 | return f"Error: {msg}"
427 | print(f"Warning: {msg}")
428 | continue
429 |
430 | # Save the image
431 | image = Image.open(io.BytesIO(response.content))
432 | image_path = save_dir / f"{task_name}.png"
433 | image.save(image_path)
434 | if page_num is None:
435 | print(f"Image saved to: {image_path}")
436 |
437 | # Update the appropriate illustration reference
438 | if task_name == "title":
439 | self.title_illustration = str(image_path)
440 | else:
441 | page_idx = int(task_name.split("_")[1]) - 1
442 | self.illustrations[page_idx] = str(image_path)
443 |
444 | except Exception as e:
445 | msg = f"Error generating illustration for {task_name}: {e}"
446 | if page_num is not None:
447 | return f"Error: {msg}"
448 | print(f"Warning: {msg}")
449 | continue
450 |
451 | if page_num is None:
452 | print(f"\nAll illustrations saved to: {save_dir}")
453 | else:
454 | return "Illustration generated successfully!"
455 |
456 | def create_preview(self, page_num: int | None = None):
457 | """
458 | Create visual previews of book pages.
459 |
460 | Args:
461 | page_num: Optional specific page to preview (0 for title page, 1+ for content pages).
462 | If None, creates previews for all pages.
463 | """
464 | # Constants for page layout (matching PowerPoint dimensions and positioning)
465 | PAGE_WIDTH = 1920
466 | PAGE_HEIGHT = 1080
467 | ILLUSTRATION_WIDTH = 840
468 | ILLUSTRATION_HEIGHT = 840
469 | ILLUSTRATION_X = (PAGE_WIDTH - ILLUSTRATION_WIDTH) // 2
470 | ILLUSTRATION_Y = 120
471 |
472 | # Try to load Trebuchet MS font, fall back to Arial if not available
473 | try:
474 | title_font = ImageFont.truetype("Trebuchet MS", 96)
475 | body_font = ImageFont.truetype("Trebuchet MS", 48)
476 | page_num_font = ImageFont.truetype("Trebuchet MS", 29)
477 | author_font = ImageFont.truetype("Trebuchet MS", 48)
478 | except OSError:
479 | title_font = ImageFont.truetype("Arial", 96)
480 | body_font = ImageFont.truetype("Arial", 48)
481 | page_num_font = ImageFont.truetype("Arial", 29)
482 | author_font = ImageFont.truetype("Arial", 48)
483 |
484 | # Determine which pages to process
485 | if page_num is not None:
486 | if page_num == 0:
487 | pages_to_process = [(0, "title", None, None)]
488 | else:
489 | page_idx = page_num - 1
490 | pages_to_process = [
491 | (
492 | page_num,
493 | "content",
494 | self.pages[page_idx],
495 | self.illustrations[page_idx],
496 | )
497 | ]
498 | else:
499 | # Process all pages
500 | pages_to_process = [(0, "title", None, None)] # Title page
501 | pages_to_process.extend(
502 | (i + 1, "content", text, illust)
503 | for i, (text, illust) in enumerate(zip(self.pages, self.illustrations))
504 | )
505 |
506 | # Process each page
507 | for page_num, page_type, text, illustration in pages_to_process:
508 | if page_type == "title":
509 | # Create title page
510 | page = Image.new("RGB", (PAGE_WIDTH, PAGE_HEIGHT), "white")
511 | draw = ImageDraw.Draw(page)
512 |
513 | # Add maroon border on left
514 | draw.rectangle([(0, 0), (38, PAGE_HEIGHT)], fill=(128, 0, 0))
515 |
516 | # Add title illustration if available
517 | if isinstance(self.title_illustration, str):
518 | try:
519 | illust = Image.open(self.title_illustration)
520 | illust = illust.resize(
521 | (ILLUSTRATION_WIDTH, ILLUSTRATION_HEIGHT)
522 | )
523 | page.paste(illust, (ILLUSTRATION_X, ILLUSTRATION_Y))
524 | except Exception as e:
525 | print(f"Warning: Could not add title illustration: {e}")
526 |
527 | # Add title text
528 | title_y = 40
529 | bbox = draw.textbbox((0, 0), self.title, font=title_font)
530 | title_width = bbox[2] - bbox[0]
531 | draw.text(
532 | ((PAGE_WIDTH - title_width) // 2, title_y),
533 | self.title,
534 | font=title_font,
535 | fill="black",
536 | )
537 |
538 | # Add author if available
539 | if self.author:
540 | author_text = f"Written by {self.author}"
541 | bbox = draw.textbbox((0, 0), author_text, font=author_font)
542 | author_width = bbox[2] - bbox[0]
543 | draw.text(
544 | ((PAGE_WIDTH - author_width) // 2, PAGE_HEIGHT - 100),
545 | author_text,
546 | font=author_font,
547 | fill="black",
548 | )
549 |
550 | else:
551 | # Create content page
552 | page = Image.new("RGB", (PAGE_WIDTH, PAGE_HEIGHT), "white")
553 | draw = ImageDraw.Draw(page)
554 |
555 | # Add illustration if available
556 | if isinstance(illustration, str):
557 | try:
558 | illust = Image.open(illustration)
559 | illust = illust.resize(
560 | (ILLUSTRATION_WIDTH, ILLUSTRATION_HEIGHT)
561 | )
562 | page.paste(illust, (ILLUSTRATION_X, ILLUSTRATION_Y))
563 | except Exception as e:
564 | print(
565 | f"Warning: Could not add illustration on page {page_num}: {e}"
566 | )
567 |
568 | # Add text
569 | text_y = 50
570 | wrapped_text = textwrap.fill(text, width=50)
571 | bbox = draw.textbbox((0, 0), wrapped_text, font=body_font)
572 | text_width = bbox[2] - bbox[0]
573 | draw.text(
574 | ((PAGE_WIDTH - text_width) // 2, text_y),
575 | wrapped_text,
576 | font=body_font,
577 | fill="black",
578 | align="center",
579 | )
580 |
581 | # Add page number
582 | page_num_text = str(page_num)
583 | bbox = draw.textbbox((0, 0), page_num_text, font=page_num_font)
584 | page_num_width = bbox[2] - bbox[0]
585 | draw.text(
586 | ((PAGE_WIDTH - page_num_width) // 2, PAGE_HEIGHT - 100),
587 | page_num_text,
588 | font=page_num_font,
589 | fill="black",
590 | )
591 |
592 | # Update the preview pages
593 | if page_num is None:
594 | self.page_previews.append(page)
595 | else:
596 | # Ensure list is long enough
597 | while len(self.page_previews) <= page_num:
598 | self.page_previews.append(None)
599 | self.page_previews[page_num] = page
600 |
601 | return self.page_previews if page_num is None else self.page_previews[page_num]
602 |
603 | def preview(self) -> None:
604 | """
605 | Create a visual preview of the book pages and display them in a Gradio interface.
606 | """
607 | print("Creating preview...")
608 | self.create_preview()
609 |
610 | # Launch Gradio interface
611 | with gr.Blocks(theme="citrus") as preview_interface:
612 | selected_page = gr.State(0)
613 |
614 | def select_page(selected: gr.SelectData):
615 | index = selected.index
616 | if index == 0:
617 | return self.title, self.title_illustration_prompt, index
618 | else:
619 | return (
620 | self.pages[index - 1],
621 | self.illustration_prompts[index - 1],
622 | index,
623 | )
624 |
625 | def export_book():
626 | output_path = self.export()
627 | return gr.DownloadButton(value=output_path, interactive=True)
628 |
629 | gr.Markdown(f"{self.title}
")
630 | with gr.Row():
631 | with gr.Column():
632 | page = gr.Textbox(self.title, label="Page text", lines=3, interactive=False)
633 | prompt = gr.Textbox(
634 | self.title_illustration_prompt,
635 | label="Illustration prompt",
636 | lines=3,
637 | )
638 | with gr.Row():
639 | prompt_button = gr.Button(
640 | "Generate Prompt", variant="secondary"
641 | )
642 | image_button = gr.Button("Generate Image", variant="primary")
643 | with gr.Column():
644 | gallery = gr.Gallery(
645 | value=self.page_previews,
646 | columns=2,
647 | rows=2,
648 | height=600,
649 | show_label=False,
650 | preview=True,
651 | )
652 | with gr.Row():
653 | export_button = gr.Button("Export", variant="secondary")
654 | download_button = gr.DownloadButton(
655 | label="Download", variant="primary", interactive=False
656 | )
657 |
658 | def generate_prompt_page(selected_page: int, page_text: str):
659 | yield {prompt_button: gr.Button("Generating...", interactive=False)}
660 | illustration_prompt = self._get_illustration_prompt(page_text)
661 | if selected_page == 0:
662 | self.title = page_text
663 | self.title_illustration_prompt = illustration_prompt
664 | else:
665 | self.pages[selected_page - 1] = page_text
666 | self.illustration_prompts[selected_page - 1] = illustration_prompt
667 | yield {prompt_button: gr.Button("Generate Prompt", interactive=True), prompt: illustration_prompt}
668 |
669 | def generate_illustration_page(selected_page: int, page_text: str, illustration_prompt: str):
670 | yield {image_button: gr.Button("Generating...", interactive=False)}
671 | if not illustration_prompt:
672 | illustration_prompt = self._get_illustration_prompt(page_text)
673 | yield {prompt: illustration_prompt}
674 | self.illustrate(page_num=selected_page)
675 | self.create_preview(page_num=selected_page)
676 | print("self.page_previews", self.page_previews)
677 | yield {gallery: self.page_previews, image_button: gr.Button("Generate Image", interactive=True)}
678 |
679 | gallery.select(
680 | select_page,
681 | outputs=[page, prompt, selected_page],
682 | show_progress="hidden",
683 | )
684 | prompt_button.click(
685 | fn=generate_prompt_page,
686 | inputs=[selected_page, page],
687 | outputs=[prompt_button, prompt],
688 | show_progress="minimal",
689 | )
690 | image_button.click(
691 | fn=generate_illustration_page,
692 | inputs=[selected_page, page, prompt],
693 | outputs=[image_button, gallery, prompt],
694 | show_progress="minimal",
695 | )
696 | export_button.click(
697 | fn=export_book,
698 | inputs=[],
699 | outputs=[download_button],
700 | show_progress="minimal",
701 | )
702 |
703 | preview_interface.launch()
704 |
705 | def save(self, filepath: str | Path = "book.json") -> None:
706 | """
707 | Save the book data to a JSON file.
708 |
709 | Args:
710 | filepath: Path where to save the JSON file. Defaults to "book.json"
711 | """
712 | filepath = Path(filepath)
713 |
714 | # Create dictionary of book data
715 | book_data = {
716 | "title": self.title,
717 | "pages": self.pages,
718 | "title_illustration": self.title_illustration,
719 | "illustrations": self.illustrations,
720 | "lora": self.lora,
721 | "author": self.author,
722 | "illustration_prompts": self.illustration_prompts,
723 | "title_illustration_prompt": self.title_illustration_prompt,
724 | }
725 |
726 | # Save to JSON file
727 | with open(filepath, "w", encoding="utf-8") as f:
728 | json.dump(book_data, f, indent=2, ensure_ascii=False)
729 |
730 | print(f"Book saved to: {filepath.absolute()}")
731 |
732 | @classmethod
733 | def load(cls, filepath: str | Path = "book.json") -> "Book":
734 | """
735 | Load a book from a JSON file.
736 |
737 | Args:
738 | filepath: Path to the JSON file to load. Defaults to "book.json"
739 |
740 | Returns:
741 | A new Book instance with the loaded data
742 | """
743 | filepath = Path(filepath)
744 |
745 | if not filepath.exists():
746 | raise FileNotFoundError(f"No book file found at: {filepath}")
747 |
748 | with open(filepath, "r", encoding="utf-8") as f:
749 | book_data = json.load(f)
750 |
751 | # Create new book instance with loaded data
752 | book = cls(
753 | title=book_data["title"],
754 | pages=book_data["pages"],
755 | title_illustration=book_data["title_illustration"],
756 | illustrations=book_data["illustrations"],
757 | lora=book_data["lora"],
758 | author=book_data["author"],
759 | illustration_prompts=book_data["illustration_prompts"],
760 | title_illustration_prompt=book_data["title_illustration_prompt"],
761 | )
762 |
763 | return book
764 |
--------------------------------------------------------------------------------
/project_structure:
--------------------------------------------------------------------------------
1 | drawbook/
2 | ├── src/
3 | │ └── drawbook/
4 | │ ├── __init__.py
5 | │ └── core.py
6 | ├── tests/
7 | │ ├── __init__.py
8 | │ └── test_core.py
9 | ├── pyproject.toml
10 | ├── requirements.txt
11 | └── README.md
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "hatchling",
4 | "hatch-requirements-txt",
5 | ]
6 | build-backend = "hatchling.build"
7 |
8 | [project]
9 | name = "drawbook"
10 | authors = [
11 | { name = "Your Name", email = "your.email@example.com" },
12 | ]
13 | description = "A Python library for illustrating children's books."
14 | readme = "README.md"
15 | requires-python = ">=3.10"
16 | classifiers = [
17 | "Programming Language :: Python :: 3",
18 | "License :: OSI Approved :: MIT License",
19 | "Operating System :: OS Independent",
20 | ]
21 | dynamic = ["version", "dependencies"]
22 |
23 | [tool.hatch.build]
24 | only-packages = true
25 | artifacts = ["*.pyd", "*.so"]
26 |
27 | [project.urls]
28 | "Homepage" = "https://github.com/yourusername/drawbook"
29 | "Bug Tracker" = "https://github.com/yourusername/drawbook/issues"
30 |
31 | [tool.hatch.build.targets.sdist]
32 | include = [
33 | "/requirements.txt"
34 | ]
35 |
36 | [tool.hatch.version]
37 | path = "version.txt"
38 | pattern = "^(?P[0-9]+\\.[0-9]+\\.[0-9]+)$"
39 |
40 | [tool.hatch.build.targets.wheel]
41 | packages = ["drawbook"]
42 |
43 | [tool.hatch.metadata.hooks.requirements_txt]
44 | filename = "requirements.txt"
45 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Dependencies will be added here as needed
2 | pytest>=7.0.0
3 | python-pptx>=0.6.21
4 | huggingface-hub>=0.20.1
5 | requests>=2.31.0
6 | tqdm>=4.66.1
7 | Pillow>=10.0.0
8 | gradio>=5.5.0
--------------------------------------------------------------------------------
/test.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abidlabs/drawbook/df3b8292b6696410b98766592b5ef60a04897780/test.pptx
--------------------------------------------------------------------------------
/tests/test_core.py:
--------------------------------------------------------------------------------
1 | from drawbook.core import Book
2 | from pathlib import Path
3 |
4 | def test_book_creation():
5 | book = Book()
6 | assert book.title == "Untitled Book"
7 | assert book.pages == []
8 | assert book.illustrations == []
9 |
10 | def test_book_with_parameters():
11 | book = Book(
12 | title="My Book",
13 | pages=["Page 1", "Page 2"],
14 | illustrations=[None, False]
15 | )
16 | assert book.title == "My Book"
17 | assert len(book) == 2
18 | assert book.illustrations == [None, False]
19 |
20 | def test_export():
21 | book = Book(
22 | title="Test Book",
23 | pages=["Page 1", "Page 2"],
24 | illustrations=[None, False]
25 | )
26 | book.export()
27 | # Note: We can't easily test the exact file location since it's temporary,
28 | # but we can verify the method runs without errors
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | 0.3.2
--------------------------------------------------------------------------------