Initial Commit
Browse files- .gitignore +15 -0
- .gradio/certificate.pem +31 -0
- README.md +139 -12
- app.py +541 -0
- data/stories/example_story.txt +11 -0
- data/templates/template.png +0 -0
- manga.py +616 -0
- pyproject.toml +13 -0
- requirements.txt +5 -0
- utils.py +238 -0
- uv.lock +0 -0
.gitignore
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__/
|
2 |
+
**/.DS_Store
|
3 |
+
.venv
|
4 |
+
|
5 |
+
# Python-generated files
|
6 |
+
*.py[oc]
|
7 |
+
build/
|
8 |
+
dist/
|
9 |
+
wheels/
|
10 |
+
*.egg-info
|
11 |
+
|
12 |
+
# Virtual environments
|
13 |
+
.venv
|
14 |
+
|
15 |
+
data/examples/*/scene*.png
|
.gradio/certificate.pem
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
-----BEGIN CERTIFICATE-----
|
2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
31 |
+
-----END CERTIFICATE-----
|
README.md
CHANGED
@@ -1,12 +1,139 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# MangakAI 📚✨
|
2 |
+
|
3 |
+
Transform your stories into stunning manga panels with AI! MangakAI is an intelligent manga generation tool that converts written narratives into visual manga-style panels using Google's Gemini AI models.
|
4 |
+
|
5 |
+
## 🌟 Features
|
6 |
+
|
7 |
+
- **🎨 AI-Powered Generation**: Convert stories into professional manga panels with scene intelligence
|
8 |
+
- **🎭 Style Customization**: Multiple art styles, moods, color palettes, and composition options
|
9 |
+
- **🔄 Panel Management**: Regenerate specific panels with custom modifications and reference images
|
10 |
+
- **📋 Custom Templates**: Upload your own panel layouts for personalized manga creation
|
11 |
+
- **📁 Export Options**: Generate PDFs and organize panels with version control
|
12 |
+
- **🖥️ Web Interface**: User-friendly Gradio interface with text/file input and example stories
|
13 |
+
|
14 |
+
## 🚀 Quick Start
|
15 |
+
|
16 |
+
### Prerequisites
|
17 |
+
- Python 3.11 or higher
|
18 |
+
- Google Gemini API key
|
19 |
+
- UV package manager (recommended) or pip
|
20 |
+
|
21 |
+
### Installation
|
22 |
+
|
23 |
+
1. **Clone the repository**
|
24 |
+
```bash
|
25 |
+
git clone https://github.com/Shiva4113/MangakAI.git
|
26 |
+
cd MangakAI
|
27 |
+
```
|
28 |
+
|
29 |
+
2. **Install dependencies**
|
30 |
+
```bash
|
31 |
+
# Using UV (recommended)
|
32 |
+
uv sync
|
33 |
+
```
|
34 |
+
|
35 |
+
3. **Set up environment variables**
|
36 |
+
```bash
|
37 |
+
# Create .env file
|
38 |
+
echo "GEMINI_API_KEY=your_gemini_api_key_here" > .env
|
39 |
+
```
|
40 |
+
|
41 |
+
4. **Run the application**
|
42 |
+
```bash
|
43 |
+
uv run app.py
|
44 |
+
```
|
45 |
+
|
46 |
+
5. **Access the interface**
|
47 |
+
Open your browser and navigate to `http://localhost:7860`
|
48 |
+
|
49 |
+
## 📖 Usage Guide
|
50 |
+
|
51 |
+
### 🖋️ **Generate Manga from Text**
|
52 |
+
|
53 |
+
1. Enter your story in the text area
|
54 |
+
2. Select number of scenes (1-10 panels)
|
55 |
+
3. Choose style preferences (optional)
|
56 |
+
4. Upload custom template (optional)
|
57 |
+
5. Click "Generate Manga"
|
58 |
+
|
59 |
+
### 📄 **Generate from File**
|
60 |
+
|
61 |
+
1. Upload a .txt story file
|
62 |
+
2. Configure settings and generate
|
63 |
+
|
64 |
+
### 🔄 **Regenerate Panels**
|
65 |
+
|
66 |
+
1. Select panel number to modify
|
67 |
+
2. Enter modification instructions
|
68 |
+
3. Upload reference image (optional)
|
69 |
+
4. Choose to replace original or keep both
|
70 |
+
|
71 |
+
### 📥 **Export as PDF**
|
72 |
+
|
73 |
+
1. Generate your manga first
|
74 |
+
2. Go to "Download PDF" tab and click "Create PDF"
|
75 |
+
|
76 |
+
## 🏗️ Project Structure
|
77 |
+
|
78 |
+
```
|
79 |
+
MangakAI/
|
80 |
+
├── app.py # Main Gradio interface
|
81 |
+
├── manga.py # Core manga generation logic
|
82 |
+
├── utils.py # Utility functions and prompts
|
83 |
+
├── main.py # Entry point
|
84 |
+
├── pyproject.toml # Project configuration
|
85 |
+
├── .env # Environment variables (create this)
|
86 |
+
├── data/
|
87 |
+
│ ├── examples/ # Example manga panels
|
88 |
+
│ │ ├── LittleLantern/
|
89 |
+
│ │ ├── PaperKite/
|
90 |
+
│ │ └── StrayPuppy/
|
91 |
+
│ ├── output/ # Generated manga panels
|
92 |
+
│ ├── stories/ # Story text files
|
93 |
+
│ └── templates/ # Panel templates
|
94 |
+
└── README.md
|
95 |
+
```
|
96 |
+
|
97 |
+
## 🛠️ Configuration
|
98 |
+
|
99 |
+
### Environment Variables
|
100 |
+
|
101 |
+
Create a `.env` file with the following variables:
|
102 |
+
|
103 |
+
```env
|
104 |
+
GEMINI_API_KEY=your_gemini_api_key_here
|
105 |
+
TEMPLATE_PATH=data/templates/template1.png
|
106 |
+
OUTPUT_DIR=data/output
|
107 |
+
STORIES_DIR=data/stories
|
108 |
+
IMAGE_MODEL_NAME=gemini-2.5-flash-image-preview
|
109 |
+
SCENE_MODEL_NAME=gemini-2.0-flash
|
110 |
+
```
|
111 |
+
|
112 |
+
### API Setup
|
113 |
+
|
114 |
+
1. **Get Gemini API Key**:
|
115 |
+
- Visit [Google AI Studio](https://makersuite.google.com/app/apikey)
|
116 |
+
- Create a new API key
|
117 |
+
- Add it to your `.env` file
|
118 |
+
|
119 |
+
## 🎨 Style Options
|
120 |
+
|
121 |
+
Choose from various art styles (Traditional Manga, Shonen, Shoujo, Seinen, Chibi, Cyberpunk, Fantasy, Horror), moods (Epic, Dark, Light, Dramatic, Action-packed), color palettes, character styles, and composition options to create your unique manga aesthetic.
|
122 |
+
|
123 |
+
## 🔧 Advanced Features
|
124 |
+
|
125 |
+
- **Smart Prompts**: Analyzes story structure and maintains character consistency
|
126 |
+
- **Custom Templates**: Upload your own panel layouts with automatic AI adaptation
|
127 |
+
- **Reference Images**: Guide style, composition, and character appearance
|
128 |
+
|
129 |
+
## 📋 Examples
|
130 |
+
|
131 |
+
The project includes three example stories with generated panels:
|
132 |
+
|
133 |
+
1. **The Little Lantern**: A heartwarming tale of courage and kindness
|
134 |
+
2. **The Paper Kite**: A story about letting go and finding wonder
|
135 |
+
3. **The Stray Puppy**: A touching story of compassion and friendship
|
136 |
+
|
137 |
+
|
138 |
+
|
139 |
+
*Transform your imagination into visual stories with the power of AI!*
|
app.py
ADDED
@@ -0,0 +1,541 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from manga import (
|
3 |
+
generate_manga_interface,
|
4 |
+
generate_manga_from_file_interface,
|
5 |
+
regenerate_and_replace_interface,
|
6 |
+
create_pdf_interface,
|
7 |
+
get_current_panels,
|
8 |
+
create_user_session
|
9 |
+
)
|
10 |
+
from utils import ART_STYLES, MOOD_OPTIONS, COLOR_PALETTES, CHARACTER_STYLES, LINE_STYLES, COMPOSITION_STYLES
|
11 |
+
import os
|
12 |
+
|
13 |
+
# Define your example data
|
14 |
+
EXAMPLES = {
|
15 |
+
"The Little Lantern": {
|
16 |
+
"title": "The Little Lantern",
|
17 |
+
"story": """In a small coastal village, where the sea whispered secrets to the shore, there lived a curious boy named Arun. Every evening, as the sun dipped behind the hills and the sky turned shades of orange and violet, Arun would carry his little lantern and sit on the rocks, watching the restless waves dance under the fading light.
|
18 |
+
|
19 |
+
His lantern was old, with a glass chimney slightly cracked, but Arun cared for it as if it were a treasure. "One day, this light will help someone," he would whisper to himself, as the sea breeze tousled his hair.
|
20 |
+
|
21 |
+
One night, the sky darkened earlier than usual. Thick clouds rolled in, and the wind began to howl like a wild beast. The sea churned, waves slamming against the shore with fierce force. Arun, worried, held his lantern tighter and scanned the horizon.
|
22 |
+
|
23 |
+
Suddenly, through the curtain of rain and spray, he spotted a small fishing boat tossed about by the waves, its lantern barely visible. The boat's crew struggled to control it, fear written on their faces.
|
24 |
+
|
25 |
+
Without a second thought, Arun jumped onto the rocks, his lantern held high above his head. "Here! This way!" he shouted through the roar of the wind and water.
|
26 |
+
|
27 |
+
The captain of the boat saw the glow and turned the vessel toward the shore. The villagers, alerted by Arun's brave signal, ran to the beach with ropes and nets. Together, they pulled the boat safely onto land.
|
28 |
+
|
29 |
+
The captain, drenched but smiling, knelt down and embraced Arun. "You saved us," he said. "Your little lantern became our lighthouse."
|
30 |
+
|
31 |
+
From that day forward, Arun's lantern became more than just an object—it became a symbol of courage and kindness. Every evening, Arun continued to sit by the sea, his light shining steadily. The villagers often gathered around him, sharing stories, laughter, and warmth. And whenever a storm brewed on the horizon, Arun's lantern stood as a reminder that even the smallest light, held with love, can brighten the darkest night.""",
|
32 |
+
"panels": [
|
33 |
+
"data/examples/LittleLantern/scene1.png",
|
34 |
+
"data/examples/LittleLantern/scene2.png",
|
35 |
+
"data/examples/LittleLantern/scene3.png",
|
36 |
+
"data/examples/LittleLantern/scene4.png",
|
37 |
+
"data/examples/LittleLantern/scene5.png"
|
38 |
+
]
|
39 |
+
},
|
40 |
+
"The Paper Kite": {
|
41 |
+
"title": "The Paper Kite",
|
42 |
+
"story": """Mira loved making kites. She would spend hours folding colorful paper, gluing tails, and tying strings until each kite looked perfect. Her favorite one was a bright red kite with golden edges that danced beautifully in the wind.
|
43 |
+
|
44 |
+
One afternoon, while flying it near the meadow, a sudden gust of wind tore the string and the kite soared away. Mira's heart sank as she watched it drift higher and higher, beyond the trees and far into the sky.
|
45 |
+
|
46 |
+
Feeling sad, she sat on a rock, staring at the empty sky. Just then, an old man passing by smiled gently. "Sometimes, letting go leads to something wonderful," he said.
|
47 |
+
|
48 |
+
The next morning, while walking through the meadow, Mira found her kite caught on a branch. But this time, instead of grabbing it, she smiled and left it there. She realized the wind had given her kite a chance to explore new heights.
|
49 |
+
|
50 |
+
From that day on, Mira made many more kites—not all stayed with her, but each one carried her dreams farther than she ever imagined.""",
|
51 |
+
"panels": [
|
52 |
+
"data/examples/PaperKite/scene1.png",
|
53 |
+
"data/examples/PaperKite/scene2.png",
|
54 |
+
"data/examples/PaperKite/scene3.png",
|
55 |
+
"data/examples/PaperKite/scene4.png",
|
56 |
+
"data/examples/PaperKite/scene5.png"
|
57 |
+
]
|
58 |
+
},
|
59 |
+
"The Stray Puppy": {
|
60 |
+
"title": "The Stray Puppy",
|
61 |
+
"story": """One chilly evening, as the sun was setting, Leo was walking home from school when he heard soft whimpering near the park bench. Curious, he peeked behind it and found a tiny, shivering puppy with big, sad eyes.
|
62 |
+
|
63 |
+
Leo gently picked it up and wrapped it in his jacket. The puppy licked his cheek as if thanking him. He took it home, gave it warm milk, and made a little bed from an old blanket.
|
64 |
+
|
65 |
+
The next day, Leo asked his neighbors if the puppy belonged to anyone, but no one came forward. So he decided to keep it and named it "Buddy".""",
|
66 |
+
"panels": [
|
67 |
+
"data/examples/StrayPuppy/scene1.png",
|
68 |
+
"data/examples/StrayPuppy/scene2.png",
|
69 |
+
"data/examples/StrayPuppy/scene3.png",
|
70 |
+
"data/examples/StrayPuppy/scene4.png"
|
71 |
+
]
|
72 |
+
}
|
73 |
+
}
|
74 |
+
|
75 |
+
def load_example(example_name):
|
76 |
+
"""Load example data when user selects an example."""
|
77 |
+
if example_name in EXAMPLES:
|
78 |
+
example = EXAMPLES[example_name]
|
79 |
+
existing_panels = [panel for panel in example["panels"] if os.path.exists(panel)]
|
80 |
+
return example["title"], example["story"], existing_panels
|
81 |
+
return "", "", []
|
82 |
+
|
83 |
+
def initialize_session(api_key: str):
|
84 |
+
"""Initialize a new user session with API key."""
|
85 |
+
try:
|
86 |
+
if not api_key or not api_key.strip():
|
87 |
+
return None, "❌ Please enter your API key", gr.Tabs(visible=False)
|
88 |
+
|
89 |
+
session_id = create_user_session(api_key.strip())
|
90 |
+
status_message = f"✅ Session initialized successfully! Session ID: {session_id[:8]}..."
|
91 |
+
|
92 |
+
return session_id, status_message, gr.Tabs(visible=True)
|
93 |
+
except Exception as e:
|
94 |
+
return None, f"❌ Error initializing session: {str(e)}", gr.Tabs(visible=False)
|
95 |
+
|
96 |
+
def create_gradio_interface():
|
97 |
+
with gr.Blocks(title="MangakAI", theme=gr.themes.Soft()) as demo:
|
98 |
+
gr.Markdown("# 📚 MangakAI")
|
99 |
+
gr.Markdown("Transform your stories into manga panels with AI and custom style preferences!")
|
100 |
+
|
101 |
+
# Session state
|
102 |
+
session_id_state = gr.State(value=None)
|
103 |
+
|
104 |
+
# API Key Section at the top
|
105 |
+
gr.Markdown("## 🔑 API Configuration")
|
106 |
+
gr.Markdown("**Enter your Gemini API Key to get started.** You can get one from [Google AI Studio](https://makersuite.google.com/app/apikey)")
|
107 |
+
|
108 |
+
with gr.Row():
|
109 |
+
api_key_input = gr.Textbox(
|
110 |
+
label="Gemini API Key",
|
111 |
+
placeholder="Enter your Gemini API key here...",
|
112 |
+
type="password",
|
113 |
+
scale=4
|
114 |
+
)
|
115 |
+
initialize_btn = gr.Button("🚀 Initialize Session", variant="primary", scale=1)
|
116 |
+
|
117 |
+
session_status = gr.Textbox(
|
118 |
+
label="Session Status",
|
119 |
+
value="⚠️ Please enter your API key and initialize session",
|
120 |
+
interactive=False
|
121 |
+
)
|
122 |
+
|
123 |
+
gr.Markdown("---")
|
124 |
+
|
125 |
+
# Main tabs (initially hidden until session is initialized)
|
126 |
+
main_tabs = gr.Tabs(visible=False)
|
127 |
+
|
128 |
+
with main_tabs:
|
129 |
+
with gr.Tab("📝 Generate Manga"):
|
130 |
+
with gr.Tab("Text Input"):
|
131 |
+
with gr.Row():
|
132 |
+
with gr.Column(scale=2):
|
133 |
+
story_input = gr.Textbox(
|
134 |
+
label="Enter your story",
|
135 |
+
placeholder="Once upon a time...",
|
136 |
+
lines=10
|
137 |
+
)
|
138 |
+
with gr.Column(scale=1):
|
139 |
+
num_scenes_text = gr.Slider(
|
140 |
+
minimum=1,
|
141 |
+
maximum=10,
|
142 |
+
value=5,
|
143 |
+
step=1,
|
144 |
+
label="Number of Scenes"
|
145 |
+
)
|
146 |
+
|
147 |
+
# Style Preferences Section
|
148 |
+
gr.Markdown("### 🎨 Style Preferences")
|
149 |
+
with gr.Row():
|
150 |
+
with gr.Column():
|
151 |
+
art_style_text = gr.Dropdown(
|
152 |
+
choices=["None"] + ART_STYLES,
|
153 |
+
value="None",
|
154 |
+
label="Art Style"
|
155 |
+
)
|
156 |
+
mood_text = gr.Dropdown(
|
157 |
+
choices=["None"] + MOOD_OPTIONS,
|
158 |
+
value="None",
|
159 |
+
label="Overall Mood"
|
160 |
+
)
|
161 |
+
color_palette_text = gr.Dropdown(
|
162 |
+
choices=["None"] + COLOR_PALETTES,
|
163 |
+
value="None",
|
164 |
+
label="Color Palette"
|
165 |
+
)
|
166 |
+
with gr.Column():
|
167 |
+
character_style_text = gr.Dropdown(
|
168 |
+
choices=["None"] + CHARACTER_STYLES,
|
169 |
+
value="None",
|
170 |
+
label="Character Style"
|
171 |
+
)
|
172 |
+
line_style_text = gr.Dropdown(
|
173 |
+
choices=["None"] + LINE_STYLES,
|
174 |
+
value="None",
|
175 |
+
label="Line Art Style"
|
176 |
+
)
|
177 |
+
composition_text = gr.Dropdown(
|
178 |
+
choices=["None"] + COMPOSITION_STYLES,
|
179 |
+
value="None",
|
180 |
+
label="Composition Style"
|
181 |
+
)
|
182 |
+
|
183 |
+
additional_notes_text = gr.Textbox(
|
184 |
+
label="Additional Style Notes",
|
185 |
+
placeholder="Any specific style preferences, character descriptions, or artistic directions...",
|
186 |
+
lines=3
|
187 |
+
)
|
188 |
+
|
189 |
+
# Custom Template Upload Section
|
190 |
+
gr.Markdown("### 📋 Custom Template (Optional)")
|
191 |
+
gr.Markdown("*Upload your own manga panel template. If not provided, default template will be used.*")
|
192 |
+
with gr.Row():
|
193 |
+
user_template_text = gr.File(
|
194 |
+
label="Upload Custom Template",
|
195 |
+
file_types=[".png", ".jpg", ".jpeg", ".webp"]
|
196 |
+
)
|
197 |
+
|
198 |
+
gr.Markdown("**Template Guidelines:**")
|
199 |
+
gr.Markdown("""
|
200 |
+
- Use PNG format for best results
|
201 |
+
- Template should have clear panel borders
|
202 |
+
- Recommended size: 1024x1024 or higher
|
203 |
+
- The AI will use your template as a guide for panel layout
|
204 |
+
""")
|
205 |
+
|
206 |
+
generate_btn = gr.Button("🎨 Generate Manga", variant="primary", size="lg")
|
207 |
+
|
208 |
+
with gr.Row():
|
209 |
+
output_gallery = gr.Gallery(
|
210 |
+
label="Generated Manga Panels",
|
211 |
+
show_label=True,
|
212 |
+
elem_id="gallery",
|
213 |
+
columns=2,
|
214 |
+
rows=3,
|
215 |
+
height="auto"
|
216 |
+
)
|
217 |
+
|
218 |
+
scene_output = gr.Textbox(
|
219 |
+
label="Scene Descriptions / Status",
|
220 |
+
lines=5,
|
221 |
+
max_lines=10
|
222 |
+
)
|
223 |
+
|
224 |
+
with gr.Tab("File Upload"):
|
225 |
+
with gr.Row():
|
226 |
+
with gr.Column(scale=2):
|
227 |
+
file_input = gr.File(
|
228 |
+
label="Upload Story File (.txt)",
|
229 |
+
file_types=[".txt"]
|
230 |
+
)
|
231 |
+
with gr.Column(scale=1):
|
232 |
+
num_scenes_file = gr.Slider(
|
233 |
+
minimum=1,
|
234 |
+
maximum=10,
|
235 |
+
value=5,
|
236 |
+
step=1,
|
237 |
+
label="Number of Scenes"
|
238 |
+
)
|
239 |
+
|
240 |
+
# Style Preferences Section for File Upload
|
241 |
+
gr.Markdown("### 🎨 Style Preferences")
|
242 |
+
with gr.Row():
|
243 |
+
with gr.Column():
|
244 |
+
art_style_file = gr.Dropdown(
|
245 |
+
choices=["None"] + ART_STYLES,
|
246 |
+
value="None",
|
247 |
+
label="Art Style"
|
248 |
+
)
|
249 |
+
mood_file = gr.Dropdown(
|
250 |
+
choices=["None"] + MOOD_OPTIONS,
|
251 |
+
value="None",
|
252 |
+
label="Overall Mood"
|
253 |
+
)
|
254 |
+
color_palette_file = gr.Dropdown(
|
255 |
+
choices=["None"] + COLOR_PALETTES,
|
256 |
+
value="None",
|
257 |
+
label="Color Palette"
|
258 |
+
)
|
259 |
+
with gr.Column():
|
260 |
+
character_style_file = gr.Dropdown(
|
261 |
+
choices=["None"] + CHARACTER_STYLES,
|
262 |
+
value="None",
|
263 |
+
label="Character Style"
|
264 |
+
)
|
265 |
+
line_style_file = gr.Dropdown(
|
266 |
+
choices=["None"] + LINE_STYLES,
|
267 |
+
value="None",
|
268 |
+
label="Line Art Style"
|
269 |
+
)
|
270 |
+
composition_file = gr.Dropdown(
|
271 |
+
choices=["None"] + COMPOSITION_STYLES,
|
272 |
+
value="None",
|
273 |
+
label="Composition Style"
|
274 |
+
)
|
275 |
+
|
276 |
+
additional_notes_file = gr.Textbox(
|
277 |
+
label="Additional Style Notes",
|
278 |
+
placeholder="Any specific style preferences, character descriptions, or artistic directions...",
|
279 |
+
lines=3
|
280 |
+
)
|
281 |
+
|
282 |
+
# Custom Template Upload Section for File Upload
|
283 |
+
gr.Markdown("### 📋 Custom Template (Optional)")
|
284 |
+
gr.Markdown("*Upload your own manga panel template. If not provided, default template will be used.*")
|
285 |
+
with gr.Row():
|
286 |
+
user_template_file = gr.File(
|
287 |
+
label="Upload Custom Template",
|
288 |
+
file_types=[".png", ".jpg", ".jpeg", ".webp"]
|
289 |
+
)
|
290 |
+
|
291 |
+
generate_file_btn = gr.Button("🎨 Generate Manga from File", variant="primary", size="lg")
|
292 |
+
|
293 |
+
with gr.Row():
|
294 |
+
output_gallery_file = gr.Gallery(
|
295 |
+
label="Generated Manga Panels",
|
296 |
+
show_label=True,
|
297 |
+
columns=2,
|
298 |
+
rows=3,
|
299 |
+
height="auto"
|
300 |
+
)
|
301 |
+
|
302 |
+
scene_output_file = gr.Textbox(
|
303 |
+
label="Scene Descriptions / Status",
|
304 |
+
lines=5,
|
305 |
+
max_lines=10
|
306 |
+
)
|
307 |
+
|
308 |
+
with gr.Tab("🔄 Regenerate Panels"):
|
309 |
+
gr.Markdown("### Select a panel to regenerate with modifications")
|
310 |
+
gr.Markdown("**Note:** You must generate manga first before you can regenerate panels.")
|
311 |
+
|
312 |
+
with gr.Row():
|
313 |
+
with gr.Column(scale=1):
|
314 |
+
panel_selector = gr.Number(
|
315 |
+
label="Panel Number to Regenerate",
|
316 |
+
value=1,
|
317 |
+
minimum=1,
|
318 |
+
maximum=10,
|
319 |
+
precision=0
|
320 |
+
)
|
321 |
+
|
322 |
+
modification_input = gr.Textbox(
|
323 |
+
label="Modification Instructions",
|
324 |
+
placeholder="e.g., 'Make the lighting more dramatic', 'Change character expression to angry', 'Add more action lines'",
|
325 |
+
lines=3
|
326 |
+
)
|
327 |
+
|
328 |
+
# Reference Image Upload Section
|
329 |
+
gr.Markdown("### 🖼️ Reference Image (Optional)")
|
330 |
+
gr.Markdown("*Upload an image to guide the regeneration style, composition, or specific elements*")
|
331 |
+
reference_image_upload = gr.File(
|
332 |
+
label="Upload Reference Image",
|
333 |
+
file_types=[".png", ".jpg", ".jpeg", ".webp"]
|
334 |
+
)
|
335 |
+
|
336 |
+
replace_checkbox = gr.Checkbox(
|
337 |
+
label="Replace original panel",
|
338 |
+
value=False
|
339 |
+
)
|
340 |
+
gr.Markdown("*Check this to replace the original panel in the main gallery*")
|
341 |
+
|
342 |
+
regenerate_btn = gr.Button("🔄 Regenerate Panel", variant="secondary", size="lg")
|
343 |
+
|
344 |
+
with gr.Column(scale=2):
|
345 |
+
regenerated_image = gr.Image(
|
346 |
+
label="Regenerated Panel",
|
347 |
+
show_label=True
|
348 |
+
)
|
349 |
+
|
350 |
+
regeneration_status = gr.Textbox(
|
351 |
+
label="Status",
|
352 |
+
interactive=False
|
353 |
+
)
|
354 |
+
|
355 |
+
# Updated main gallery display (shows when panels are replaced)
|
356 |
+
with gr.Row():
|
357 |
+
updated_main_gallery = gr.Gallery(
|
358 |
+
label="Updated Main Gallery (when replacing panels)",
|
359 |
+
show_label=True,
|
360 |
+
columns=2,
|
361 |
+
rows=3,
|
362 |
+
height="auto",
|
363 |
+
visible=False
|
364 |
+
)
|
365 |
+
|
366 |
+
gr.Markdown("### Reference Image Guidelines:")
|
367 |
+
gr.Markdown("""
|
368 |
+
- **Style Reference**: Upload an image with the art style you want to emulate
|
369 |
+
- **Composition Reference**: Show the camera angle, pose, or layout you prefer
|
370 |
+
- **Color Reference**: Provide color palette or lighting inspiration
|
371 |
+
- **Character Reference**: Show specific character appearances or expressions
|
372 |
+
- **Environment Reference**: Demonstrate background or setting elements
|
373 |
+
""")
|
374 |
+
|
375 |
+
gr.Markdown("### Common Modification Examples:")
|
376 |
+
gr.Markdown("""
|
377 |
+
- **Lighting**: "Make it darker with more shadows", "Add bright sunlight", "Create moody twilight atmosphere"
|
378 |
+
- **Character**: "Make character look more angry", "Change pose to defensive stance", "Add more detailed facial expression"
|
379 |
+
- **Composition**: "Change to close-up shot", "Make it a wide establishing shot", "Add more dynamic camera angle"
|
380 |
+
- **Style**: "Add more action lines", "Make it more dramatic", "Simplify the background"
|
381 |
+
- **Details**: "Add more environmental details", "Remove background clutter", "Focus more on the character"
|
382 |
+
""")
|
383 |
+
|
384 |
+
with gr.Tab("📥 Download PDF"):
|
385 |
+
gr.Markdown("### Export your manga as a PDF")
|
386 |
+
gr.Markdown("**Note:** You must generate manga first before you can create a PDF.")
|
387 |
+
|
388 |
+
with gr.Row():
|
389 |
+
with gr.Column(scale=1):
|
390 |
+
create_pdf_btn = gr.Button("📥 Create PDF", variant="primary", size="lg")
|
391 |
+
|
392 |
+
with gr.Column(scale=2):
|
393 |
+
pdf_status = gr.Textbox(
|
394 |
+
label="Status",
|
395 |
+
interactive=False
|
396 |
+
)
|
397 |
+
|
398 |
+
download_pdf = gr.File(
|
399 |
+
label="Download PDF",
|
400 |
+
visible=False
|
401 |
+
)
|
402 |
+
|
403 |
+
gr.Markdown("### PDF Features:")
|
404 |
+
gr.Markdown("""
|
405 |
+
- **A4 format** with proper margins and professional layout
|
406 |
+
- **One panel per page** with panel numbering
|
407 |
+
- **Title page** with manga information
|
408 |
+
- **Custom template notation** if user template was used
|
409 |
+
- **Automatic image scaling** to fit pages while maintaining aspect ratio
|
410 |
+
""")
|
411 |
+
|
412 |
+
with gr.Tab("🎯 Examples"):
|
413 |
+
gr.Markdown("### Explore Example Stories and Manga")
|
414 |
+
gr.Markdown("Select from our curated examples to see how stories transform into manga panels!")
|
415 |
+
|
416 |
+
with gr.Row():
|
417 |
+
with gr.Column(scale=1):
|
418 |
+
example_selector = gr.Dropdown(
|
419 |
+
choices=list(EXAMPLES.keys()),
|
420 |
+
label="Select Example",
|
421 |
+
value=list(EXAMPLES.keys())[0] if EXAMPLES else None
|
422 |
+
)
|
423 |
+
|
424 |
+
example_title = gr.Textbox(
|
425 |
+
label="Story Title",
|
426 |
+
interactive=False,
|
427 |
+
lines=1
|
428 |
+
)
|
429 |
+
|
430 |
+
example_story = gr.Textbox(
|
431 |
+
label="Story Text",
|
432 |
+
interactive=False,
|
433 |
+
lines=8,
|
434 |
+
max_lines=12
|
435 |
+
)
|
436 |
+
|
437 |
+
with gr.Column(scale=2):
|
438 |
+
example_gallery = gr.Gallery(
|
439 |
+
label="Manga Panels",
|
440 |
+
show_label=True,
|
441 |
+
columns=2,
|
442 |
+
rows=3,
|
443 |
+
height="auto"
|
444 |
+
)
|
445 |
+
|
446 |
+
gr.Markdown("### How It Works:")
|
447 |
+
gr.Markdown("""
|
448 |
+
1. **Select an Example**: Choose from the dropdown above
|
449 |
+
2. **View the Story**: Read the original story text
|
450 |
+
3. **See the Manga**: Observe how AI transforms text into visual panels
|
451 |
+
4. **Try Your Own**: Use the "Generate Manga" tab to create your own!
|
452 |
+
""")
|
453 |
+
|
454 |
+
# Security notice
|
455 |
+
gr.Markdown("---")
|
456 |
+
gr.Markdown("### 🔒 Privacy & Security")
|
457 |
+
gr.Markdown("""
|
458 |
+
- **Your API key is never stored permanently** - it's only kept in memory during your session
|
459 |
+
- **All generated content is isolated** - each user gets their own private workspace
|
460 |
+
- **Sessions automatically expire** after 24 hours of inactivity
|
461 |
+
- **Your files are cleaned up** when your session ends
|
462 |
+
""")
|
463 |
+
|
464 |
+
# Helper functions
|
465 |
+
def update_gallery_visibility(gallery_images):
|
466 |
+
if gallery_images:
|
467 |
+
return gr.Gallery(visible=True, value=gallery_images)
|
468 |
+
else:
|
469 |
+
return gr.Gallery(visible=False)
|
470 |
+
|
471 |
+
def create_and_provide_pdf(session_id):
|
472 |
+
if session_id is None:
|
473 |
+
return "Session not initialized. Please refresh and enter your API key.", gr.File(visible=False)
|
474 |
+
|
475 |
+
status, pdf_path = create_pdf_interface(session_id)
|
476 |
+
if pdf_path:
|
477 |
+
return status, gr.File(value=pdf_path, visible=True)
|
478 |
+
else:
|
479 |
+
return status, gr.File(visible=False)
|
480 |
+
|
481 |
+
def load_first_example():
|
482 |
+
if EXAMPLES:
|
483 |
+
first_key = list(EXAMPLES.keys())[0]
|
484 |
+
return load_example(first_key)
|
485 |
+
return "", "", []
|
486 |
+
|
487 |
+
# Event handlers
|
488 |
+
initialize_btn.click(
|
489 |
+
fn=initialize_session,
|
490 |
+
inputs=[api_key_input],
|
491 |
+
outputs=[session_id_state, session_status, main_tabs]
|
492 |
+
)
|
493 |
+
|
494 |
+
generate_btn.click(
|
495 |
+
fn=generate_manga_interface,
|
496 |
+
inputs=[session_id_state, story_input, num_scenes_text, art_style_text, mood_text, color_palette_text,
|
497 |
+
character_style_text, line_style_text, composition_text, additional_notes_text, user_template_text],
|
498 |
+
outputs=[output_gallery, scene_output]
|
499 |
+
)
|
500 |
+
|
501 |
+
generate_file_btn.click(
|
502 |
+
fn=generate_manga_from_file_interface,
|
503 |
+
inputs=[session_id_state, file_input, num_scenes_file, art_style_file, mood_file, color_palette_file,
|
504 |
+
character_style_file, line_style_file, composition_file, additional_notes_file, user_template_file],
|
505 |
+
outputs=[output_gallery_file, scene_output_file]
|
506 |
+
)
|
507 |
+
|
508 |
+
regenerate_btn.click(
|
509 |
+
fn=regenerate_and_replace_interface,
|
510 |
+
inputs=[session_id_state, panel_selector, modification_input, replace_checkbox, reference_image_upload],
|
511 |
+
outputs=[regeneration_status, regenerated_image, updated_main_gallery]
|
512 |
+
).then(
|
513 |
+
fn=update_gallery_visibility,
|
514 |
+
inputs=[updated_main_gallery],
|
515 |
+
outputs=[updated_main_gallery]
|
516 |
+
)
|
517 |
+
|
518 |
+
create_pdf_btn.click(
|
519 |
+
fn=create_and_provide_pdf,
|
520 |
+
inputs=[session_id_state],
|
521 |
+
outputs=[pdf_status, download_pdf]
|
522 |
+
)
|
523 |
+
|
524 |
+
# Example selection
|
525 |
+
example_selector.change(
|
526 |
+
fn=load_example,
|
527 |
+
inputs=[example_selector],
|
528 |
+
outputs=[example_title, example_story, example_gallery]
|
529 |
+
)
|
530 |
+
|
531 |
+
# Load first example on startup
|
532 |
+
demo.load(
|
533 |
+
fn=load_first_example,
|
534 |
+
outputs=[example_title, example_story, example_gallery]
|
535 |
+
)
|
536 |
+
|
537 |
+
return demo
|
538 |
+
|
539 |
+
if __name__ == "__main__":
|
540 |
+
demo = create_gradio_interface()
|
541 |
+
demo.launch(share=True)
|
data/stories/example_story.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
In a quiet village surrounded by hills, there stood an ancient oak tree at the edge of the forest. The villagers said it was magical—that if you whispered your wish to its trunk, the tree would carry it to the stars.
|
2 |
+
|
3 |
+
Nine-year-old Nora didn’t believe in magic, but she loved stories. One evening, as the sky turned purple and the fireflies came out, she tiptoed to the tree with a secret wish. “I want to find a friend who understands me,” she whispered, pressing her cheek against the rough bark.
|
4 |
+
|
5 |
+
The next day, while walking through the woods, she heard soft humming. Following the sound, she found a small fox with bright amber eyes, tangled in some vines. Without thinking, she freed it gently. The fox didn’t run away but sat beside her, tail wagging.
|
6 |
+
|
7 |
+
From that day on, Nora and the fox became inseparable. They explored streams, climbed hills, and lay beneath the stars sharing dreams. No one else saw the fox, but Nora didn’t mind.
|
8 |
+
|
9 |
+
On clear nights, she would sit by the oak tree and thank it. The tree’s leaves rustled in response, as if smiling.
|
10 |
+
|
11 |
+
The villagers still whispered about the magic tree, but Nora knew the truth—it wasn’t about spells or charms. Sometimes, all it takes is a wish, a little courage, and a heart open enough to hear the world’s softest whispers.
|
data/templates/template.png
ADDED
![]() |
manga.py
ADDED
@@ -0,0 +1,616 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import PIL
|
3 |
+
from io import BytesIO
|
4 |
+
from google import genai
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
import pathlib
|
7 |
+
from utils import (
|
8 |
+
get_scene_splitting_prompt,
|
9 |
+
get_panel_prompt,
|
10 |
+
SCENE_BREAK_DELIMITER,
|
11 |
+
get_regeneration_prompt
|
12 |
+
)
|
13 |
+
import time
|
14 |
+
from PIL import Image
|
15 |
+
from reportlab.pdfgen import canvas
|
16 |
+
from reportlab.lib.pagesizes import A4
|
17 |
+
import shutil
|
18 |
+
import uuid
|
19 |
+
import hashlib
|
20 |
+
from typing import Dict, Optional
|
21 |
+
|
22 |
+
# Load environment variables
|
23 |
+
load_dotenv()
|
24 |
+
|
25 |
+
class UserSession:
|
26 |
+
"""Represents a user session with isolated data and API key."""
|
27 |
+
|
28 |
+
def __init__(self, session_id: str, api_key: str):
|
29 |
+
self.session_id = session_id
|
30 |
+
self.api_key_hash = self._hash_api_key(api_key) # Store only hash for security
|
31 |
+
self._api_key = api_key # Keep encrypted or in memory only
|
32 |
+
self.created_at = time.time()
|
33 |
+
self.last_activity = time.time()
|
34 |
+
|
35 |
+
# Session-specific directories
|
36 |
+
self.base_dir = os.path.join("data", "sessions", session_id)
|
37 |
+
self.output_dir = os.path.join(self.base_dir, "output")
|
38 |
+
self.user_templates_dir = os.path.join(self.base_dir, "templates")
|
39 |
+
self.user_references_dir = os.path.join(self.base_dir, "references")
|
40 |
+
|
41 |
+
# Create session directories
|
42 |
+
os.makedirs(self.output_dir, exist_ok=True)
|
43 |
+
os.makedirs(self.user_templates_dir, exist_ok=True)
|
44 |
+
os.makedirs(self.user_references_dir, exist_ok=True)
|
45 |
+
|
46 |
+
# Session generation data
|
47 |
+
self.current_generation = {
|
48 |
+
'scenes': [],
|
49 |
+
'generated_images': [],
|
50 |
+
'chat': None,
|
51 |
+
'user_preferences': {},
|
52 |
+
'current_template_path': os.getenv("TEMPLATE_PATH", "data/templates/template.png")
|
53 |
+
}
|
54 |
+
|
55 |
+
def _hash_api_key(self, api_key: str) -> str:
|
56 |
+
"""Create a hash of the API key for identification (not storage)."""
|
57 |
+
return hashlib.sha256(api_key.encode()).hexdigest()[:16]
|
58 |
+
|
59 |
+
def get_api_key(self) -> str:
|
60 |
+
"""Get the API key (should be implemented with proper encryption in production)."""
|
61 |
+
return self._api_key
|
62 |
+
|
63 |
+
def update_activity(self):
|
64 |
+
"""Update last activity timestamp."""
|
65 |
+
self.last_activity = time.time()
|
66 |
+
|
67 |
+
def is_expired(self, timeout_hours: int = 24) -> bool:
|
68 |
+
"""Check if session has expired."""
|
69 |
+
return (time.time() - self.last_activity) > (timeout_hours * 3600)
|
70 |
+
|
71 |
+
def cleanup(self):
|
72 |
+
"""Clean up session files and data."""
|
73 |
+
try:
|
74 |
+
if os.path.exists(self.base_dir):
|
75 |
+
shutil.rmtree(self.base_dir)
|
76 |
+
print(f"Cleaned up session: {self.session_id}")
|
77 |
+
except Exception as e:
|
78 |
+
print(f"Error cleaning up session {self.session_id}: {e}")
|
79 |
+
|
80 |
+
|
81 |
+
class SessionManager:
|
82 |
+
"""Manages user sessions and provides isolation between users."""
|
83 |
+
|
84 |
+
def __init__(self):
|
85 |
+
self.sessions: Dict[str, UserSession] = {}
|
86 |
+
self.session_timeout_hours = 24
|
87 |
+
|
88 |
+
def create_session(self, api_key: str) -> str:
|
89 |
+
"""Create a new session for a user with their API key."""
|
90 |
+
session_id = str(uuid.uuid4())
|
91 |
+
self.sessions[session_id] = UserSession(session_id, api_key)
|
92 |
+
self._cleanup_expired_sessions()
|
93 |
+
return session_id
|
94 |
+
|
95 |
+
def get_session(self, session_id: str) -> Optional[UserSession]:
|
96 |
+
"""Get a session by ID, return None if not found or expired."""
|
97 |
+
if session_id not in self.sessions:
|
98 |
+
return None
|
99 |
+
|
100 |
+
session = self.sessions[session_id]
|
101 |
+
if session.is_expired(self.session_timeout_hours):
|
102 |
+
self._remove_session(session_id)
|
103 |
+
return None
|
104 |
+
|
105 |
+
session.update_activity()
|
106 |
+
return session
|
107 |
+
|
108 |
+
def _remove_session(self, session_id: str):
|
109 |
+
"""Remove and cleanup a session."""
|
110 |
+
if session_id in self.sessions:
|
111 |
+
self.sessions[session_id].cleanup()
|
112 |
+
del self.sessions[session_id]
|
113 |
+
|
114 |
+
def _cleanup_expired_sessions(self):
|
115 |
+
"""Clean up all expired sessions."""
|
116 |
+
expired_sessions = [
|
117 |
+
sid for sid, session in self.sessions.items()
|
118 |
+
if session.is_expired(self.session_timeout_hours)
|
119 |
+
]
|
120 |
+
for session_id in expired_sessions:
|
121 |
+
self._remove_session(session_id)
|
122 |
+
|
123 |
+
|
124 |
+
class MangaGenerator:
|
125 |
+
def __init__(self, session: UserSession):
|
126 |
+
"""Initialize the manga generator for a specific user session."""
|
127 |
+
self.session = session
|
128 |
+
self.api_key = session.get_api_key()
|
129 |
+
|
130 |
+
# Use session-specific directories
|
131 |
+
self.template_path = os.getenv("TEMPLATE_PATH", "data/templates/template.png")
|
132 |
+
self.output_dir = session.output_dir
|
133 |
+
self.user_templates_dir = session.user_templates_dir
|
134 |
+
self.user_references_dir = session.user_references_dir
|
135 |
+
|
136 |
+
self.image_gen_model_name = os.getenv("IMAGE_MODEL_NAME", "gemini-2.5-flash-image-preview")
|
137 |
+
self.scene_gen_model_name = os.getenv("SCENE_MODEL_NAME", "gemini-2.0-flash")
|
138 |
+
|
139 |
+
# Initialize clients with session's API key
|
140 |
+
try:
|
141 |
+
self.image_gen_client = genai.Client(api_key=self.api_key)
|
142 |
+
self.scene_gen_client = genai.Client(api_key=self.api_key)
|
143 |
+
except Exception as e:
|
144 |
+
raise ValueError(f"Invalid API key or client initialization failed: {e}")
|
145 |
+
|
146 |
+
@property
|
147 |
+
def current_generation(self):
|
148 |
+
"""Access current generation data from session."""
|
149 |
+
return self.session.current_generation
|
150 |
+
|
151 |
+
def save_user_template(self, uploaded_file):
|
152 |
+
"""Save user uploaded template and return the path."""
|
153 |
+
if uploaded_file is None:
|
154 |
+
return None
|
155 |
+
|
156 |
+
try:
|
157 |
+
# Generate unique filename with timestamp
|
158 |
+
timestamp = int(time.time())
|
159 |
+
filename = f"user_template_{timestamp}.png"
|
160 |
+
template_path = os.path.join(self.user_templates_dir, filename)
|
161 |
+
|
162 |
+
# Handle different file input types
|
163 |
+
if hasattr(uploaded_file, 'name'): # Gradio file object
|
164 |
+
shutil.copy2(uploaded_file.name, template_path)
|
165 |
+
else:
|
166 |
+
if isinstance(uploaded_file, str):
|
167 |
+
shutil.copy2(uploaded_file, template_path)
|
168 |
+
elif hasattr(uploaded_file, 'save'): # PIL Image
|
169 |
+
uploaded_file.save(template_path)
|
170 |
+
|
171 |
+
# Verify the template was saved and is a valid image
|
172 |
+
test_image = PIL.Image.open(template_path)
|
173 |
+
test_image.verify()
|
174 |
+
|
175 |
+
print(f"User template saved to: {template_path}")
|
176 |
+
return template_path
|
177 |
+
|
178 |
+
except Exception as e:
|
179 |
+
print(f"Error saving user template: {e}")
|
180 |
+
return None
|
181 |
+
|
182 |
+
def save_user_reference_image(self, uploaded_file):
|
183 |
+
"""Save user uploaded reference image and return the path."""
|
184 |
+
if uploaded_file is None:
|
185 |
+
return None
|
186 |
+
|
187 |
+
try:
|
188 |
+
timestamp = int(time.time())
|
189 |
+
filename = f"user_reference_{timestamp}.png"
|
190 |
+
reference_path = os.path.join(self.user_references_dir, filename)
|
191 |
+
|
192 |
+
if hasattr(uploaded_file, 'name'): # Gradio file object
|
193 |
+
shutil.copy2(uploaded_file.name, reference_path)
|
194 |
+
else:
|
195 |
+
if isinstance(uploaded_file, str):
|
196 |
+
shutil.copy2(uploaded_file, reference_path)
|
197 |
+
elif hasattr(uploaded_file, 'save'): # PIL Image
|
198 |
+
uploaded_file.save(reference_path)
|
199 |
+
|
200 |
+
test_image = PIL.Image.open(reference_path)
|
201 |
+
test_image.verify()
|
202 |
+
|
203 |
+
print(f"User reference image saved to: {reference_path}")
|
204 |
+
return reference_path
|
205 |
+
|
206 |
+
except Exception as e:
|
207 |
+
print(f"Error saving user reference image: {e}")
|
208 |
+
return None
|
209 |
+
|
210 |
+
def set_template_for_generation(self, template_path):
|
211 |
+
"""Set the template to use for the current generation session."""
|
212 |
+
if template_path and os.path.exists(template_path):
|
213 |
+
self.current_generation['current_template_path'] = template_path
|
214 |
+
return True
|
215 |
+
return False
|
216 |
+
|
217 |
+
def get_current_template_path(self):
|
218 |
+
"""Get the current template path being used."""
|
219 |
+
return self.current_generation.get('current_template_path', self.template_path)
|
220 |
+
|
221 |
+
def read_story(self, file_path):
|
222 |
+
"""Read story text from file."""
|
223 |
+
with open(file_path, "r") as f:
|
224 |
+
return f.read()
|
225 |
+
|
226 |
+
def split_into_scenes(self, story_text: str, n_scenes: int):
|
227 |
+
"""Split story into visual scenes with descriptions."""
|
228 |
+
prompt = get_scene_splitting_prompt(story_text, n_scenes)
|
229 |
+
|
230 |
+
response = self.scene_gen_client.models.generate_content(
|
231 |
+
model=self.scene_gen_model_name,
|
232 |
+
contents=[prompt]
|
233 |
+
)
|
234 |
+
|
235 |
+
full_response_text = ""
|
236 |
+
for part in response.candidates[0].content.parts:
|
237 |
+
if part.text:
|
238 |
+
full_response_text += part.text
|
239 |
+
|
240 |
+
scenes = [scene.strip() for scene in full_response_text.split(SCENE_BREAK_DELIMITER)]
|
241 |
+
scenes = [scene for scene in scenes if scene]
|
242 |
+
|
243 |
+
return scenes[:n_scenes]
|
244 |
+
|
245 |
+
def save_image(self, response, path):
|
246 |
+
"""Save the generated image from response."""
|
247 |
+
time.sleep(3)
|
248 |
+
for part in response.parts:
|
249 |
+
if image := part.as_image():
|
250 |
+
image.save(path)
|
251 |
+
return image
|
252 |
+
return None
|
253 |
+
|
254 |
+
def generate_image_for_scene(self, scene_description: str, output_path: str):
|
255 |
+
"""Generate image for a single scene."""
|
256 |
+
current_template = self.get_current_template_path()
|
257 |
+
response = self.image_gen_client.models.generate_content(
|
258 |
+
model=self.image_gen_model_name,
|
259 |
+
contents=[
|
260 |
+
scene_description,
|
261 |
+
PIL.Image.open(current_template)
|
262 |
+
]
|
263 |
+
)
|
264 |
+
|
265 |
+
saved_image = self.save_image(response, output_path)
|
266 |
+
return response, saved_image
|
267 |
+
|
268 |
+
def generate_image_with_chat(self, scene_description: str, output_path: str, chat):
|
269 |
+
"""Generate image using chat context for consistency."""
|
270 |
+
current_template = self.get_current_template_path()
|
271 |
+
response = chat.send_message([
|
272 |
+
scene_description,
|
273 |
+
PIL.Image.open(current_template)
|
274 |
+
])
|
275 |
+
|
276 |
+
saved_image = self.save_image(response, output_path)
|
277 |
+
return response, saved_image
|
278 |
+
|
279 |
+
def generate_image_with_chat_and_reference(self, scene_description: str, output_path: str, chat, reference_image_path=None):
|
280 |
+
"""Generate image using chat context with optional reference image."""
|
281 |
+
current_template = self.get_current_template_path()
|
282 |
+
|
283 |
+
content = [scene_description, PIL.Image.open(current_template)]
|
284 |
+
|
285 |
+
if reference_image_path and os.path.exists(reference_image_path):
|
286 |
+
content.append(PIL.Image.open(reference_image_path))
|
287 |
+
print(f"Using reference image: {reference_image_path}")
|
288 |
+
|
289 |
+
response = chat.send_message(content)
|
290 |
+
saved_image = self.save_image(response, output_path)
|
291 |
+
return response, saved_image
|
292 |
+
|
293 |
+
def regenerate_specific_panel(self, panel_index: int, modification_request: str, reference_image=None):
|
294 |
+
"""Regenerate a specific panel with modifications and optional reference image."""
|
295 |
+
if not self.current_generation['scenes'] or not self.current_generation['chat']:
|
296 |
+
raise ValueError("No active generation session. Please generate manga first.")
|
297 |
+
|
298 |
+
if panel_index >= len(self.current_generation['scenes']):
|
299 |
+
raise ValueError(f"Panel index {panel_index} is out of range.")
|
300 |
+
|
301 |
+
original_scene = self.current_generation['scenes'][panel_index]
|
302 |
+
|
303 |
+
reference_image_path = None
|
304 |
+
if reference_image is not None:
|
305 |
+
reference_image_path = self.save_user_reference_image(reference_image)
|
306 |
+
if reference_image_path:
|
307 |
+
modification_request += "\n\nIMPORTANT: Use the provided reference image as visual guidance for style, composition, or specific elements while maintaining the story's integrity."
|
308 |
+
|
309 |
+
user_preferences = self.current_generation.get('user_preferences', {})
|
310 |
+
modified_prompt = get_regeneration_prompt(
|
311 |
+
original_scene,
|
312 |
+
modification_request,
|
313 |
+
is_first_panel=(panel_index == 0),
|
314 |
+
user_preferences=user_preferences
|
315 |
+
)
|
316 |
+
|
317 |
+
current_version = self.current_generation['generated_images'][panel_index].get('version', 1)
|
318 |
+
output_path = os.path.join(self.output_dir, f"scene{panel_index+1}_v{current_version + 1}.png")
|
319 |
+
|
320 |
+
response, saved_image = self.generate_image_with_chat_and_reference(
|
321 |
+
modified_prompt,
|
322 |
+
output_path,
|
323 |
+
self.current_generation['chat'],
|
324 |
+
reference_image_path
|
325 |
+
)
|
326 |
+
|
327 |
+
return output_path, saved_image
|
328 |
+
|
329 |
+
def replace_panel(self, panel_index: int, new_image_path: str, new_image: PIL.Image):
|
330 |
+
"""Replace a panel in the current generation."""
|
331 |
+
if not self.current_generation['generated_images']:
|
332 |
+
raise ValueError("No active generation session.")
|
333 |
+
|
334 |
+
if panel_index >= len(self.current_generation['generated_images']):
|
335 |
+
raise ValueError(f"Panel index {panel_index} is out of range.")
|
336 |
+
|
337 |
+
current_version = self.current_generation['generated_images'][panel_index].get('version', 1)
|
338 |
+
self.current_generation['generated_images'][panel_index].update({
|
339 |
+
'image_path': new_image_path,
|
340 |
+
'image': new_image,
|
341 |
+
'version': current_version + 1
|
342 |
+
})
|
343 |
+
|
344 |
+
def get_current_gallery_paths(self):
|
345 |
+
"""Get current image paths for gallery display."""
|
346 |
+
if not self.current_generation['generated_images']:
|
347 |
+
return []
|
348 |
+
return [img['image_path'] for img in self.current_generation['generated_images']]
|
349 |
+
|
350 |
+
def generate_manga_from_story(self, story_text: str, n_scenes: int = 5, user_preferences: dict = None, user_template=None):
|
351 |
+
"""Generate complete manga from story text with user preferences and optional custom template."""
|
352 |
+
if user_template is not None:
|
353 |
+
template_path = self.save_user_template(user_template)
|
354 |
+
if template_path:
|
355 |
+
self.set_template_for_generation(template_path)
|
356 |
+
print(f"Using user template: {template_path}")
|
357 |
+
else:
|
358 |
+
print("Failed to save user template, using default template")
|
359 |
+
|
360 |
+
if user_preferences is None:
|
361 |
+
user_preferences = {}
|
362 |
+
|
363 |
+
scenes = self.split_into_scenes(story_text, n_scenes)
|
364 |
+
|
365 |
+
chat = self.image_gen_client.chats.create(model=self.image_gen_model_name)
|
366 |
+
|
367 |
+
generated_images = []
|
368 |
+
responses = []
|
369 |
+
|
370 |
+
for i, scene in enumerate(scenes):
|
371 |
+
scene_description = get_panel_prompt(scene, is_first_panel=(i == 0), user_preferences=user_preferences)
|
372 |
+
|
373 |
+
output_path = os.path.join(self.output_dir, f"scene{i+1}.png")
|
374 |
+
response, saved_image = self.generate_image_with_chat(scene_description, output_path, chat)
|
375 |
+
|
376 |
+
responses.append(response)
|
377 |
+
generated_images.append({
|
378 |
+
'scene_number': i + 1,
|
379 |
+
'scene_text': scene,
|
380 |
+
'image_path': output_path,
|
381 |
+
'image': saved_image,
|
382 |
+
'version': 1
|
383 |
+
})
|
384 |
+
|
385 |
+
print(f"Generated scene {i+1}")
|
386 |
+
|
387 |
+
self.current_generation.update({
|
388 |
+
'scenes': scenes,
|
389 |
+
'generated_images': generated_images,
|
390 |
+
'chat': chat,
|
391 |
+
'user_preferences': user_preferences
|
392 |
+
})
|
393 |
+
|
394 |
+
return generated_images, scenes
|
395 |
+
|
396 |
+
def generate_manga_from_file(self, story_file_path: str, n_scenes: int = 5, user_preferences: dict = None, user_template=None):
|
397 |
+
"""Generate manga from story file with user preferences and optional custom template."""
|
398 |
+
story_text = self.read_story(story_file_path)
|
399 |
+
return self.generate_manga_from_story(story_text, n_scenes, user_preferences, user_template)
|
400 |
+
|
401 |
+
def get_current_panels(self):
|
402 |
+
"""Get current panel information for the interface."""
|
403 |
+
if not self.current_generation['generated_images']:
|
404 |
+
return []
|
405 |
+
|
406 |
+
return [(i+1, img['image_path']) for i, img in enumerate(self.current_generation['generated_images'])]
|
407 |
+
|
408 |
+
def create_manga_pdf(self, output_filename=None):
|
409 |
+
"""Create a PDF file from all current manga panels."""
|
410 |
+
if not self.current_generation['generated_images']:
|
411 |
+
raise ValueError("No manga panels to export. Please generate manga first.")
|
412 |
+
|
413 |
+
if output_filename is None:
|
414 |
+
output_filename = os.path.join(self.output_dir, "manga_complete.pdf")
|
415 |
+
|
416 |
+
c = canvas.Canvas(output_filename, pagesize=A4)
|
417 |
+
page_width, page_height = A4
|
418 |
+
|
419 |
+
c.setFont("Helvetica-Bold", 24)
|
420 |
+
title_text = "Generated Manga"
|
421 |
+
title_width = c.stringWidth(title_text, "Helvetica-Bold", 24)
|
422 |
+
c.drawString((page_width - title_width) / 2, page_height - 100, title_text)
|
423 |
+
|
424 |
+
c.setFont("Helvetica", 12)
|
425 |
+
subtitle_text = f"Total Panels: {len(self.current_generation['generated_images'])}"
|
426 |
+
subtitle_width = c.stringWidth(subtitle_text, "Helvetica", 12)
|
427 |
+
c.drawString((page_width - subtitle_width) / 2, page_height - 130, subtitle_text)
|
428 |
+
|
429 |
+
current_template = self.get_current_template_path()
|
430 |
+
if current_template != self.template_path:
|
431 |
+
template_info = "Custom Template Used"
|
432 |
+
template_width = c.stringWidth(template_info, "Helvetica", 12)
|
433 |
+
c.drawString((page_width - template_width) / 2, page_height - 150, template_info)
|
434 |
+
|
435 |
+
c.showPage()
|
436 |
+
|
437 |
+
for i, panel_data in enumerate(self.current_generation['generated_images']):
|
438 |
+
image_path = panel_data['image_path']
|
439 |
+
|
440 |
+
if os.path.exists(image_path):
|
441 |
+
img = Image.open(image_path)
|
442 |
+
|
443 |
+
img_width, img_height = img.size
|
444 |
+
aspect_ratio = img_width / img_height
|
445 |
+
|
446 |
+
max_width = page_width - 100
|
447 |
+
max_height = page_height - 150
|
448 |
+
|
449 |
+
if aspect_ratio > 1:
|
450 |
+
new_width = min(max_width, img_width)
|
451 |
+
new_height = new_width / aspect_ratio
|
452 |
+
else:
|
453 |
+
new_height = min(max_height, img_height)
|
454 |
+
new_width = new_height * aspect_ratio
|
455 |
+
|
456 |
+
x = (page_width - new_width) / 2
|
457 |
+
y = (page_height - new_height) / 2
|
458 |
+
|
459 |
+
c.drawImage(image_path, x, y, width=new_width, height=new_height)
|
460 |
+
|
461 |
+
c.setFont("Helvetica", 10)
|
462 |
+
c.drawString(50, page_height - 30, f"Panel {i + 1}")
|
463 |
+
|
464 |
+
c.showPage()
|
465 |
+
|
466 |
+
c.save()
|
467 |
+
print(f"PDF saved to: {output_filename}")
|
468 |
+
return output_filename
|
469 |
+
|
470 |
+
|
471 |
+
# Global session manager
|
472 |
+
_session_manager = SessionManager()
|
473 |
+
|
474 |
+
def get_session_manager() -> SessionManager:
|
475 |
+
"""Get the global session manager."""
|
476 |
+
return _session_manager
|
477 |
+
|
478 |
+
def get_generator_for_session(session_id: str) -> Optional[MangaGenerator]:
|
479 |
+
"""Get a manga generator for a specific session."""
|
480 |
+
session = _session_manager.get_session(session_id)
|
481 |
+
if session is None:
|
482 |
+
return None
|
483 |
+
return MangaGenerator(session)
|
484 |
+
|
485 |
+
def create_user_session(api_key: str) -> str:
|
486 |
+
"""Create a new user session with API key."""
|
487 |
+
if not api_key or not api_key.strip():
|
488 |
+
raise ValueError("API key is required")
|
489 |
+
return _session_manager.create_session(api_key.strip())
|
490 |
+
|
491 |
+
# Interface functions with session support
|
492 |
+
def generate_manga_interface(session_id: str, story_text: str, num_scenes: int = 5, art_style: str = None, mood: str = None,
|
493 |
+
color_palette: str = None, character_style: str = None, line_style: str = None,
|
494 |
+
composition: str = None, additional_notes: str = "", user_template=None):
|
495 |
+
"""Interface function for Gradio - generates manga from text input with session support."""
|
496 |
+
try:
|
497 |
+
generator = get_generator_for_session(session_id)
|
498 |
+
if generator is None:
|
499 |
+
return [], "Session expired or invalid. Please refresh and enter your API key again."
|
500 |
+
|
501 |
+
user_preferences = {}
|
502 |
+
if art_style and art_style != "None":
|
503 |
+
user_preferences['art_style'] = art_style
|
504 |
+
if mood and mood != "None":
|
505 |
+
user_preferences['mood'] = mood
|
506 |
+
if color_palette and color_palette != "None":
|
507 |
+
user_preferences['color_palette'] = color_palette
|
508 |
+
if character_style and character_style != "None":
|
509 |
+
user_preferences['character_style'] = character_style
|
510 |
+
if line_style and line_style != "None":
|
511 |
+
user_preferences['line_style'] = line_style
|
512 |
+
if composition and composition != "None":
|
513 |
+
user_preferences['composition'] = composition
|
514 |
+
if additional_notes.strip():
|
515 |
+
user_preferences['additional_notes'] = additional_notes.strip()
|
516 |
+
|
517 |
+
generated_images, scenes = generator.generate_manga_from_story(story_text, num_scenes, user_preferences, user_template)
|
518 |
+
|
519 |
+
image_paths = [img['image_path'] for img in generated_images]
|
520 |
+
scene_descriptions = [img['scene_text'] for img in generated_images]
|
521 |
+
|
522 |
+
return image_paths, "\n\n".join([f"Scene {i+1}: {scene}" for i, scene in enumerate(scene_descriptions)])
|
523 |
+
except Exception as e:
|
524 |
+
print(f"Error generating manga: {e}")
|
525 |
+
return [], f"Error: {str(e)}"
|
526 |
+
|
527 |
+
def generate_manga_from_file_interface(session_id: str, story_file, num_scenes: int = 5, art_style: str = None, mood: str = None,
|
528 |
+
color_palette: str = None, character_style: str = None, line_style: str = None,
|
529 |
+
composition: str = None, additional_notes: str = "", user_template=None):
|
530 |
+
"""Interface function for Gradio - generates manga from uploaded file with session support."""
|
531 |
+
try:
|
532 |
+
generator = get_generator_for_session(session_id)
|
533 |
+
if generator is None:
|
534 |
+
return [], "Session expired or invalid. Please refresh and enter your API key again."
|
535 |
+
|
536 |
+
if hasattr(story_file, 'name'): # Gradio file object
|
537 |
+
with open(story_file.name, 'r') as f:
|
538 |
+
story_text = f.read()
|
539 |
+
else:
|
540 |
+
story_text = str(story_file)
|
541 |
+
|
542 |
+
user_preferences = {}
|
543 |
+
if art_style and art_style != "None":
|
544 |
+
user_preferences['art_style'] = art_style
|
545 |
+
if mood and mood != "None":
|
546 |
+
user_preferences['mood'] = mood
|
547 |
+
if color_palette and color_palette != "None":
|
548 |
+
user_preferences['color_palette'] = color_palette
|
549 |
+
if character_style and character_style != "None":
|
550 |
+
user_preferences['character_style'] = character_style
|
551 |
+
if line_style and line_style != "None":
|
552 |
+
user_preferences['line_style'] = line_style
|
553 |
+
if composition and composition != "None":
|
554 |
+
user_preferences['composition'] = composition
|
555 |
+
if additional_notes.strip():
|
556 |
+
user_preferences['additional_notes'] = additional_notes.strip()
|
557 |
+
|
558 |
+
generated_images, scenes = generator.generate_manga_from_story(story_text, num_scenes, user_preferences, user_template)
|
559 |
+
|
560 |
+
image_paths = [img['image_path'] for img in generated_images]
|
561 |
+
scene_descriptions = [img['scene_text'] for img in generated_images]
|
562 |
+
|
563 |
+
return image_paths, "\n\n".join([f"Scene {i+1}: {scene}" for i, scene in enumerate(scene_descriptions)])
|
564 |
+
except Exception as e:
|
565 |
+
print(f"Error generating manga: {e}")
|
566 |
+
return [], f"Error: {str(e)}"
|
567 |
+
|
568 |
+
def regenerate_and_replace_interface(session_id: str, panel_number: int, modification_request: str, replace_original: bool, reference_image=None):
|
569 |
+
"""Interface function for Gradio - regenerate panel with session support."""
|
570 |
+
try:
|
571 |
+
generator = get_generator_for_session(session_id)
|
572 |
+
if generator is None:
|
573 |
+
return "Session expired or invalid. Please refresh and enter your API key again.", None, []
|
574 |
+
|
575 |
+
if not modification_request.strip():
|
576 |
+
return "Please provide modification instructions.", None, []
|
577 |
+
|
578 |
+
panel_index = panel_number - 1
|
579 |
+
new_image_path, saved_image = generator.regenerate_specific_panel(panel_index, modification_request, reference_image)
|
580 |
+
|
581 |
+
updated_gallery = []
|
582 |
+
if replace_original and generator.current_generation['generated_images']:
|
583 |
+
generator.replace_panel(panel_index, new_image_path, saved_image)
|
584 |
+
updated_gallery = generator.get_current_gallery_paths()
|
585 |
+
status_message = f"Panel {panel_number} regenerated and replaced successfully!"
|
586 |
+
else:
|
587 |
+
status_message = f"Panel {panel_number} regenerated successfully! (Original preserved)"
|
588 |
+
|
589 |
+
return status_message, new_image_path, updated_gallery
|
590 |
+
|
591 |
+
except Exception as e:
|
592 |
+
return f"Error regenerating panel: {e}", None, []
|
593 |
+
|
594 |
+
def create_pdf_interface(session_id: str):
|
595 |
+
"""Interface function for Gradio - create PDF with session support."""
|
596 |
+
try:
|
597 |
+
generator = get_generator_for_session(session_id)
|
598 |
+
if generator is None:
|
599 |
+
return "Session expired or invalid. Please refresh and enter your API key again.", None
|
600 |
+
|
601 |
+
if not generator.current_generation['generated_images']:
|
602 |
+
return "No manga panels to export. Please generate manga first.", None
|
603 |
+
|
604 |
+
pdf_path = generator.create_manga_pdf()
|
605 |
+
message = f"PDF created successfully! ({len(generator.current_generation['generated_images'])} panels)"
|
606 |
+
|
607 |
+
return message, pdf_path
|
608 |
+
except Exception as e:
|
609 |
+
return f"Error creating PDF: {e}", None
|
610 |
+
|
611 |
+
def get_current_panels(session_id: str):
|
612 |
+
"""Get current panel information for the interface with session support."""
|
613 |
+
generator = get_generator_for_session(session_id)
|
614 |
+
if generator is None:
|
615 |
+
return []
|
616 |
+
return generator.get_current_panels()
|
pyproject.toml
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "MangakAI"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Transform your imagination into visual stories with the power of AI!"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.11"
|
7 |
+
dependencies = [
|
8 |
+
"google-genai>=1.33.0",
|
9 |
+
"gradio>=5.44.1",
|
10 |
+
"pillow>=11.3.0",
|
11 |
+
"python-dotenv>=1.1.1",
|
12 |
+
"reportlab>=4.4.3",
|
13 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
google-genai>=1.33.0
|
2 |
+
gradio>=5.44.1
|
3 |
+
pillow>=11.3.0
|
4 |
+
python-dotenv>=1.1.1
|
5 |
+
reportlab>=4.4.3
|
utils.py
ADDED
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Utility functions and prompt templates for manga generation.
|
3 |
+
"""
|
4 |
+
|
5 |
+
def get_scene_splitting_prompt(story_text: str, n_scenes: int) -> str:
|
6 |
+
"""Generate the prompt for splitting a story into scenes."""
|
7 |
+
return f"""
|
8 |
+
Act as a master film director and veteran comic book artist, with an exceptional eye for cinematic composition and dramatic effect. Your task is to read the following story and transmute it into {n_scenes} distinct, visually-dense scene descriptions, including key dialogues and a reference to the original text.
|
9 |
+
|
10 |
+
The goal is to create a powerful visual description for each scene that can be used directly as a prompt for an AI image generator, while also capturing the most important lines spoken and preserving the source text.
|
11 |
+
|
12 |
+
**STORY:**
|
13 |
+
{story_text}
|
14 |
+
|
15 |
+
---
|
16 |
+
|
17 |
+
**CRITICAL INSTRUCTIONS FOR EACH SCENE'S DESCRIPTION:**
|
18 |
+
- **Reference the Source:** In the 'Original Scene Text' field, you must include the exact, unaltered passage from the story that you are adapting for this scene.
|
19 |
+
- **Be the Camera:** Describe the scene as if looking through a camera lens. Explicitly mention the shot type (e.g., wide shot, extreme close-up, over-the-shoulder), camera angle, and the composition of elements.
|
20 |
+
- **Paint the Atmosphere:** Weave together the setting, time of day, weather, and mood. Use lighting (e.g., "harsh neon glare," "soft morning light," "dramatic chiaroscuro shadows") to establish the tone.
|
21 |
+
- **Focus on the Subject:** Detail the characters' specific poses, actions, and intense facial expressions. Describe their clothing and how it interacts with their actions and the environment (e.g., "a rain-soaked cloak clinging to their frame," "wind-whipped hair").
|
22 |
+
- **Emphasize the Core Moment:** Ensure the description builds towards the single most important visual element, action, or emotional beat in the scene.
|
23 |
+
- **Isolate Key Dialogue:** If the scene contains crucial, plot-advancing dialogue, extract the most impactful line(s) verbatim.
|
24 |
+
|
25 |
+
**OUTPUT FORMAT:**
|
26 |
+
You must use the following format. Separate each scene block with '---SCENE_BREAK---'.
|
27 |
+
|
28 |
+
**Scene:** [Scene Number]
|
29 |
+
**Original Scene Text:** [The exact, unaltered passage from the story for this scene.]
|
30 |
+
**Visual Description:** [A single, comprehensive paragraph combining all the visual instructions. This must be a self-contained prompt for an image model. **DO NOT include dialogue here**.]
|
31 |
+
**Key Dialogue:** [The most important line(s) of dialogue from the scene. If there is no dialogue, write 'None'.]
|
32 |
+
---SCENE_BREAK---
|
33 |
+
"""
|
34 |
+
|
35 |
+
def get_first_panel_prompt(scene: str, user_preferences: dict = None) -> str:
|
36 |
+
"""Generate the prompt for the first manga panel."""
|
37 |
+
style_instructions = get_style_instructions(user_preferences) if user_preferences else ""
|
38 |
+
|
39 |
+
return f"""
|
40 |
+
**Act as a master mangaka creating the first panel of a new manga.**
|
41 |
+
|
42 |
+
**YOUR TASK:** Create a visually stunning manga page based on the scene description below. You have two creative options:
|
43 |
+
|
44 |
+
1. **USE A TEMPLATE:** Choose ONE of the three provided panel templates. Your illustration must perfectly integrate into the chosen panel's borders, using its layout as the foundation for a dynamic composition.
|
45 |
+
|
46 |
+
2. **CREATE A SPLASH PAGE:** If the scene is a powerful establishing shot or a highly dramatic moment, you may ignore the templates and create a full-page, borderless splash image that captures the full impact of the scene.
|
47 |
+
|
48 |
+
**CRITICAL:** You must adhere strictly to the visual details in the scene description.
|
49 |
+
- **Style:** The art style, line weight, and shading must remain consistent.
|
50 |
+
- **Original Story:** Do not deviate from the original story text. Every scene must accurately reflect the source material as well.
|
51 |
+
- **Ensure that there is no repetition of scenes. Each panel must advance the story.**
|
52 |
+
- **If the scene contains dialogue, include the exact lines in speech bubbles.**
|
53 |
+
- **Create multiple panels per page to maintain pacing.**
|
54 |
+
- **Include good story narration boxes so the reader can follow the story.**
|
55 |
+
|
56 |
+
{style_instructions}
|
57 |
+
|
58 |
+
---
|
59 |
+
**SCENE DESCRIPTION:**
|
60 |
+
{scene}
|
61 |
+
"""
|
62 |
+
|
63 |
+
def get_subsequent_panel_prompt(scene: str, user_preferences: dict = None) -> str:
|
64 |
+
"""Generate the prompt for subsequent manga panels."""
|
65 |
+
style_instructions = get_style_instructions(user_preferences) if user_preferences else ""
|
66 |
+
|
67 |
+
return f"""
|
68 |
+
**Act as a master mangaka continuing a manga sequence.**
|
69 |
+
|
70 |
+
**YOUR TASK:** Create the next manga page, ensuring it flows perfectly from the previous panel. You have two creative options:
|
71 |
+
|
72 |
+
1. **USE A TEMPLATE:** Choose ONE of the three provided panel templates to best fit the action. Your illustration must integrate perfectly into the chosen panel's borders.
|
73 |
+
|
74 |
+
2. **CREATE A SPLASH PAGE:** If this scene is a major climax or a dramatic shift, you may ignore the templates and create a full-page, borderless splash image.
|
75 |
+
|
76 |
+
**CRITICAL - MAINTAIN VISUAL CONTINUITY:**
|
77 |
+
- **Characters:** Must have the exact same appearance, clothing, and features as the previous panel.
|
78 |
+
- **Style:** The art style, line weight, and shading must remain consistent.
|
79 |
+
- **Environment:** The setting and lighting must logically follow the previous panel.
|
80 |
+
- **Original Story:** Do not deviate from the original story text. Every scene must accurately reflect the source material as well.
|
81 |
+
- **Ensure that there is no repetition of scenes. Each panel must advance the story.**
|
82 |
+
- **If the scene contains dialogue, include the exact lines in speech bubbles.**
|
83 |
+
- **Create multiple panels per page to maintain pacing.**
|
84 |
+
- **Include good story narration boxes so the reader can follow the story.**
|
85 |
+
|
86 |
+
{style_instructions}
|
87 |
+
|
88 |
+
---
|
89 |
+
**SCENE DESCRIPTION:**
|
90 |
+
{scene}
|
91 |
+
"""
|
92 |
+
|
93 |
+
def get_panel_prompt(scene: str, is_first_panel: bool = False, user_preferences: dict = None) -> str:
|
94 |
+
"""Get the appropriate panel prompt based on whether it's the first panel or not."""
|
95 |
+
if is_first_panel:
|
96 |
+
return get_first_panel_prompt(scene, user_preferences)
|
97 |
+
else:
|
98 |
+
return get_subsequent_panel_prompt(scene, user_preferences)
|
99 |
+
|
100 |
+
def get_regeneration_prompt(original_scene: str, modification_request: str, is_first_panel: bool = False, user_preferences: dict = None) -> str:
|
101 |
+
"""Generate prompt for regenerating a panel with modifications."""
|
102 |
+
base_instruction = get_first_panel_prompt(original_scene, user_preferences) if is_first_panel else get_subsequent_panel_prompt(original_scene, user_preferences)
|
103 |
+
|
104 |
+
return f"""
|
105 |
+
{base_instruction}
|
106 |
+
|
107 |
+
---
|
108 |
+
**MODIFICATION REQUEST:**
|
109 |
+
The user has requested the following changes to this panel:
|
110 |
+
{modification_request}
|
111 |
+
|
112 |
+
**CRITICAL:** Incorporate these modifications while maintaining all other aspects of the original scene description and ensuring visual continuity with the manga sequence.
|
113 |
+
"""
|
114 |
+
|
115 |
+
def get_style_instructions(user_preferences: dict) -> str:
|
116 |
+
"""Generate style instructions based on user preferences."""
|
117 |
+
if not user_preferences:
|
118 |
+
return ""
|
119 |
+
|
120 |
+
instructions = "\n**USER STYLE PREFERENCES:**"
|
121 |
+
|
122 |
+
if user_preferences.get('art_style'):
|
123 |
+
instructions += f"\n- **Art Style:** {user_preferences['art_style']}"
|
124 |
+
|
125 |
+
if user_preferences.get('mood'):
|
126 |
+
instructions += f"\n- **Overall Mood:** {user_preferences['mood']}"
|
127 |
+
|
128 |
+
if user_preferences.get('color_palette'):
|
129 |
+
instructions += f"\n- **Color Palette:** {user_preferences['color_palette']}"
|
130 |
+
|
131 |
+
if user_preferences.get('character_style'):
|
132 |
+
instructions += f"\n- **Character Design:** {user_preferences['character_style']}"
|
133 |
+
|
134 |
+
if user_preferences.get('line_style'):
|
135 |
+
instructions += f"\n- **Line Art Style:** {user_preferences['line_style']}"
|
136 |
+
|
137 |
+
if user_preferences.get('composition'):
|
138 |
+
instructions += f"\n- **Composition Preference:** {user_preferences['composition']}"
|
139 |
+
|
140 |
+
if user_preferences.get('additional_notes'):
|
141 |
+
instructions += f"\n- **Additional Notes:** {user_preferences['additional_notes']}"
|
142 |
+
|
143 |
+
instructions += "\n\n**CRITICAL:** Incorporate ALL these style preferences while maintaining the story integrity, visual continuity, and all the requirements above."
|
144 |
+
|
145 |
+
return instructions
|
146 |
+
|
147 |
+
# Constants
|
148 |
+
SCENE_BREAK_DELIMITER = "---SCENE_BREAK---"
|
149 |
+
|
150 |
+
# Pre-defined style options for the interface
|
151 |
+
ART_STYLES = [
|
152 |
+
"Traditional Manga/Anime",
|
153 |
+
"Shonen (Bold, Dynamic)",
|
154 |
+
"Shoujo (Soft, Romantic)",
|
155 |
+
"Seinen (Mature, Detailed)",
|
156 |
+
"Chibi (Cute, Simplified)",
|
157 |
+
"Realistic",
|
158 |
+
"Semi-Realistic",
|
159 |
+
"Minimalist",
|
160 |
+
"Dark/Gothic",
|
161 |
+
"Cyberpunk",
|
162 |
+
"Fantasy",
|
163 |
+
"Horror",
|
164 |
+
"Comedy/Cartoon"
|
165 |
+
]
|
166 |
+
|
167 |
+
MOOD_OPTIONS = [
|
168 |
+
"Epic/Heroic",
|
169 |
+
"Dark/Mysterious",
|
170 |
+
"Light/Cheerful",
|
171 |
+
"Dramatic/Intense",
|
172 |
+
"Romantic",
|
173 |
+
"Action-Packed",
|
174 |
+
"Peaceful/Serene",
|
175 |
+
"Suspenseful",
|
176 |
+
"Melancholic",
|
177 |
+
"Whimsical"
|
178 |
+
]
|
179 |
+
|
180 |
+
COLOR_PALETTES = [
|
181 |
+
"Full Color",
|
182 |
+
"Black and White",
|
183 |
+
"Sepia/Vintage",
|
184 |
+
"Monochromatic Blue",
|
185 |
+
"Monochromatic Red",
|
186 |
+
"Warm Tones",
|
187 |
+
"Cool Tones",
|
188 |
+
"High Contrast",
|
189 |
+
"Pastel Colors",
|
190 |
+
"Neon/Vibrant"
|
191 |
+
]
|
192 |
+
|
193 |
+
CHARACTER_STYLES = [
|
194 |
+
"Detailed/Realistic",
|
195 |
+
"Stylized/Expressive",
|
196 |
+
"Simple/Clean",
|
197 |
+
"Muscular/Athletic",
|
198 |
+
"Elegant/Graceful",
|
199 |
+
"Cute/Moe",
|
200 |
+
"Mature/Adult",
|
201 |
+
"Young/Teen",
|
202 |
+
"Fantasy/Otherworldly"
|
203 |
+
]
|
204 |
+
|
205 |
+
LINE_STYLES = [
|
206 |
+
"Clean/Precise",
|
207 |
+
"Rough/Sketchy",
|
208 |
+
"Bold/Thick",
|
209 |
+
"Fine/Delicate",
|
210 |
+
"Variable Weight",
|
211 |
+
"Minimalist",
|
212 |
+
"Detailed/Complex"
|
213 |
+
]
|
214 |
+
|
215 |
+
COMPOSITION_STYLES = [
|
216 |
+
"Dynamic/Action",
|
217 |
+
"Balanced/Stable",
|
218 |
+
"Asymmetrical",
|
219 |
+
"Close-up Focus",
|
220 |
+
"Wide/Environmental",
|
221 |
+
"Dramatic Angles",
|
222 |
+
"Traditional/Conservative"
|
223 |
+
]
|
224 |
+
|
225 |
+
# Optional: Additional utility functions for prompt customization
|
226 |
+
def customize_scene_prompt(base_prompt: str, **kwargs) -> str:
|
227 |
+
"""Allow for dynamic prompt customization if needed."""
|
228 |
+
return base_prompt.format(**kwargs)
|
229 |
+
|
230 |
+
def get_style_modifiers() -> dict:
|
231 |
+
"""Return common style modifiers that can be added to prompts."""
|
232 |
+
return {
|
233 |
+
"manga_style": "in traditional manga/anime art style with clean line art and dynamic compositions",
|
234 |
+
"dark_theme": "with dark, moody atmosphere and dramatic shadows",
|
235 |
+
"action_focus": "emphasizing dynamic action and movement",
|
236 |
+
"character_focus": "with detailed character expressions and emotions",
|
237 |
+
"cinematic": "with cinematic camera angles and professional composition"
|
238 |
+
}
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|