├── .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 | ![](https://github.com/abidlabs/drawbook/blob/main/assets/0.png?raw=true) 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 | ![](https://github.com/abidlabs/drawbook/blob/main/assets/demo.png?raw=true) 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 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 --------------------------------------------------------------------------------