Initial upload of project files

This commit is contained in:
xyz 2026-01-08 20:51:26 +08:00
commit d3b1600936
18 changed files with 4007 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
.env

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.13

106
README.md Normal file
View File

@ -0,0 +1,106 @@
## 团队成员与贡献
| 姓名 | 学号 | 主要贡献 |
|------|------|----------|
| 王斌 | 2411020213 | (组长) Prompt 工程优化 |
| 翟宇轩 | 2411020215 | 后端架构设计 |
| 吴磊 | 2411020211 | 项目的汇报和整理 |
---
## 项目简介
本项目旨在打造一个**零门槛、开箱即用**的 AI 文生图创作平台,让每个人都能轻松创作专业级 AI 画作。
### 核心功能
- 🎨 **智能 Prompt 生成**基于中文描述自动优化为专业英文提示词PydanticAI 驱动结构化生成)
- 🖼️ **多风格支持**:写实、动漫、油画、水彩等 10+ 预设风格
- ⚡ **实时生成**:调用云端 API无需本地显卡秒级出图
- 💾 **历史记录**:自动保存生成历史,支持下载与对比
- 🎛️ **参数调优**:图像尺寸、采样步数、引导系数自定义
---
## 如何运行
### 环境要求
- Python 3.9+
- 互联网连接(用于调用 API
### 快速开始
#### 1. 克隆仓库
```bash
git clone http://hblu.top:3000/Python2025-CourseDesign/Group-TextToImage.git
cd Group-TextToImage
```
#### 2. 安装依赖
使用 `uv`
```bash
uv sync
```
#### 3. 配置 API Key
```bash
# 复制环境变量模板
cp .env.example .env
# 编辑 .env 文件,填入你的 API Key
# 支持OpenAI DALL-E、Stability AI、Replicate 等
```
`.env` 文件示例:
```env
# 文生图 API 配置(三选一)
STABILITY_API_KEY=your_stability_ai_key_here
REPLICATE_API_TOKEN=your_replicate_token_here
OPENAI_API_KEY=your_openai_key_here
# 默认使用的 API 提供商
DEFAULT_PROVIDER=stability
```
#### 4. 启动应用
```bash
python3 backend/app.py
```
打开另一个终端
```bash
uv run streamlit run frontend/main.py
```
浏览器自动打开 `http://localhost:8501`
---
## 项目结构
```
Group-TextToImage/
├── app.py # Streamlit 主应用
├── backend/
│ ├── api_client.py # API 调用封装
│ ├── prompt_engineer.py # Prompt 优化引擎
│ ├── image_processor.py # 图像后处理
│ └── agent.py # PydanticAI 智能体定义
├── frontend/
│ ├── styles.css # 自定义样式
│ └── components.py # UI 组件库
├── .env.example # 环境变量模板
├── .gitignore # Git 忽略配置
├── requirements.txt # 依赖清单
└── README.md # 项目文档
```
---
## 开发心得
1. 单纯急躁的使用AI进行开发没有带来什么成就感反而是不断Debug的挫败感国产AI真没用
2. 协作开发的感觉很好
3. AI对小白很友好
4. 做出来只是第一步,优化任重而道远

8
backend.log Normal file
View File

@ -0,0 +1,8 @@
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5001
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 523-960-332

81
backend/agent.py Normal file
View File

@ -0,0 +1,81 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from pydantic_ai import Agent, RunContext
import os
# --- Data Models ---
class Character(BaseModel):
name: str = Field(description="The name of the character.")
description: str = Field(description="A concise visual description of the character (e.g., 'Blonde hair, blue dress, young').")
class CharacterAnalysisResult(BaseModel):
characters: List[Character] = Field(description="List of main characters identified in the text.")
class MangaSimplePrompt(BaseModel):
prompt: str = Field(description="The generated English manga image prompt.")
# --- Wrapper Functions ---
async def analyze_characters_with_agent(text: str, api_key: str, base_url: Optional[str] = None, model: str = "gpt-4o") -> str:
"""
Uses PydanticAI Agent to analyze characters and returns a formatted string context.
"""
from pydantic_ai.models.openai import OpenAIModel
# Allow model override
model_name = model if model else "gpt-4o"
# Create the model instance with the specific API Key and Base URL
openai_model = OpenAIModel(
model_name,
api_key=api_key,
base_url=base_url
)
# Create a temporary agent for this run
agent = Agent(
openai_model,
result_type=CharacterAnalysisResult,
system_prompt="You are a professional manga editor. Analyze the provided novel text and extract the visual descriptions of the main characters to ensure consistency in manga adaptation."
)
try:
result = await agent.run(text)
# Format the result into a string context
context_str = ""
for char in result.data.characters:
context_str += f"- {char.name}: {char.description}\n"
return context_str
except Exception as e:
print(f"Agent Error (Characters): {e}")
return ""
async def generate_single_prompt_with_agent(paragraph: str, character_context: str, api_key: str, base_url: Optional[str] = None, model: str = "gpt-4o") -> str:
from pydantic_ai.models.openai import OpenAIModel
model_name = model if model else "gpt-4o"
openai_model = OpenAIModel(
model_name,
api_key=api_key,
base_url=base_url
)
agent = Agent(
openai_model,
result_type=MangaSimplePrompt,
deps_type=str,
system_prompt=(
"You are a professional manga artist assistant. Convert the novel text into a detailed manga image prompt. "
"The prompt MUST be in English. Focus on visual details, character appearance, setting, and style (monochrome, manga style, high quality). "
f"Maintain character consistency:\n{character_context}"
)
)
try:
result = await agent.run(paragraph, deps=character_context)
return result.data.prompt
except Exception as e:
print(f"Agent Error (Prompt): {e}")
return f"Error generation prompt: {e}"

70
backend/app.py Normal file
View File

@ -0,0 +1,70 @@
from flask import Flask, jsonify, request
from utils import split_text_into_paragraphs, generate_prompts, generate_image_from_prompt, save_to_history, load_history
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({"status": "healthy", "service": "novel-to-manga-backend"})
@app.route('/process_text', methods=['POST'])
def process_text():
data = request.json
text = data.get('text')
api_key = data.get('api_key') or os.getenv("OPENAI_API_KEY")
base_url = data.get('base_url')
model = data.get('model') or "gpt-4o"
if not text:
return jsonify({"error": "No text provided"}), 400
try:
paragraphs = split_text_into_paragraphs(text)
prompts = generate_prompts(paragraphs, api_key, base_url, model)
return jsonify({"prompts": prompts})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/generate_image', methods=['POST'])
def generate_image():
data = request.json
prompt = data.get('prompt')
# Try Image Gen specific key first, then fallback to general OpenAI key
api_key = data.get('api_key') or os.getenv("IMAGE_GEN_API_KEY") or os.getenv("OPENAI_API_KEY")
base_url = data.get('base_url')
model = data.get('model') or "dall-e-3"
if not prompt:
return jsonify({"error": "No prompt provided"}), 400
try:
image_url = generate_image_from_prompt(prompt, api_key, base_url, model)
if image_url:
# Save to history
save_to_history({
"prompt": prompt,
"image_url": image_url,
"novel_text": data.get('novel_text', '') # Optional
})
return jsonify({"image_url": image_url})
else:
return jsonify({"error": "Failed to generate image"}), 500
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/history', methods=['GET'])
def get_history():
try:
history = load_history()
# Sort by timestamp desc
history.sort(key=lambda x: x.get('timestamp', 0), reverse=True)
return jsonify({"history": history})
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, port=5001)

119
backend/utils.py Normal file
View File

@ -0,0 +1,119 @@
import os
import json
import time
from openai import OpenAI
import asyncio
try:
from backend.agent import analyze_characters_with_agent, generate_single_prompt_with_agent
except ImportError:
from agent import analyze_characters_with_agent, generate_single_prompt_with_agent
def split_text_into_paragraphs(text):
"""
Splits the novel text into paragraphs suitable for manga panels.
Use double newline as separator.
"""
if not text:
return []
return [p.strip() for p in text.split('\n\n') if p.strip()]
def analyze_characters(text, api_key, base_url=None, model="gpt-4o"):
"""
Extracts character descriptions using PydanticAI Agent.
"""
try:
# Run async agent synchronously
return asyncio.run(analyze_characters_with_agent(text, api_key, base_url, model))
except Exception as e:
print(f"Error analyzing characters: {e}")
return ""
async def generate_prompts_async(paragraphs, api_key, base_url, model):
full_text = "\n".join(paragraphs)
character_context = await analyze_characters_with_agent(full_text, api_key, base_url, model)
tasks = []
for p in paragraphs:
tasks.append(generate_single_prompt_with_agent(p, character_context, api_key, base_url, model))
results = await asyncio.gather(*tasks)
prompts = []
for p, res in zip(paragraphs, results):
prompts.append({"paragraph": p, "prompt": res})
return prompts
def generate_prompts(paragraphs, api_key=None, base_url=None, model="gpt-4o"):
"""
Generates manga prompts for each paragraph using PydanticAI Agents.
"""
if not api_key:
print("Warning: No API key provided for prompt generation.")
return [{"paragraph": p, "prompt": f"Manga style panel showing: {p} (Mock generated)"} for p in paragraphs]
try:
return asyncio.run(generate_prompts_async(paragraphs, api_key, base_url, model))
except Exception as e:
print(f"Error generating prompts: {e}")
# Fallback
return [{"paragraph": p, "prompt": f"Error: {str(e)}"} for p in paragraphs]
def generate_image_from_prompt(prompt, api_key=None, base_url=None, model="dall-e-3", size="1024x1024"):
"""
Generates an image from a prompt using OpenAI API.
"""
if not api_key:
print("Warning: No API key for image generation.")
# Return mock URL
return "https://via.placeholder.com/1024x1024.png?text=Mock+Image"
client = OpenAI(api_key=api_key, base_url=base_url)
try:
# Note: If using a non-OpenAI provider that is compatible,
# parameters might need adjustment (e.g. size/quality).
response = client.images.generate(
model=model,
prompt=prompt,
size=size,
quality="standard",
n=1,
)
return response.data[0].url
except Exception as e:
print(f"Error generating image: {e}")
return None
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
HISTORY_FILE = os.path.join(DATA_DIR, 'history.json')
def ensure_data_dir():
if not os.path.exists(DATA_DIR):
os.makedirs(DATA_DIR)
def save_to_history(item):
"""
Saves a generation item to history.json.
Item should be a dict (e.g., {prompt, image_url, timestamp}).
"""
ensure_data_dir()
history = load_history()
item['timestamp'] = item.get('timestamp', time.time())
history.append(item)
with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
json.dump(history, f, ensure_ascii=False, indent=2)
def load_history():
"""
Loads history from history.json.
"""
ensure_data_dir()
if not os.path.exists(HISTORY_FILE):
return []
try:
with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return []

14
data/history.json Normal file
View File

@ -0,0 +1,14 @@
[
{
"prompt": "**Manga Image Prompt:** \nA monochrome manga panel in high-quality ink and screentone style. \n**Scene:** A lush garden at dusk, with dense flower bushes (peonies and chrysanthemums) swaying softly. The atmosphere is tense yet whimsical. \n**Foreground:** Fuxiao Xianchan (拂晓衔蝉) suddenly straightens from a crouching position among the flowers, mimicking a meerkats alert posture. Her long silver hair flows loosely, with a few strands caught on petals. Her crimson eyes widen sharply, pupils contracted in suspicion. She wears a fitted, slightly ruffled blouse and practical trousers, now slightly dirt-stained at the knees. Her body is tense, shoulders raised defensively, hands half-clenched near her chest. \n**Background:** Yu Xunge (虞寻歌) stands a few steps away, illuminated by soft twilight. His pale gold hair glimmers faintly, styled neatly. He wears a simple, elegant robe, standing upright with a calm but earnest expression. His gaze is direct yet gentle, lips parted as if mid-question. \n**Composition:** Dynamic angle from a low perspective, emphasizing Fuxiao Xianchans sudden movement and defensive stance. Speed lines subtly radiate from her figure to convey motion. The background is slightly blurred to focus on the characters interaction. \n**Style:** Classic shōjo manga aesthetic with detailed linework, expressive facial features, and dramatic contrast lighting. No dialogue bubbles or text.",
"image_url": "https://s3.siliconflow.cn/default/outputs/2b4a3c08-71b5-4a12-81d8-fd4011174c95_4ba9bbc3fbb739679d2546eaf1764c21_ComfyUI_8e95d973_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260108%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260108T065136Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6NjVhMzEiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2Nzg0MzIwOSwiZXhwIjoxNzY4MDE2MDA5LCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1ZmliMmM1MG1pczczYWpubzFnIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.pKxO5YLlQ2J_fm5RTSRyj6zYAwUeP1isOQcAtzsjtndudeKY0YpdgDPyyDG9y2MK2HaUKidY7lHpDH5i9_DvRRUxB5IMTyocTGX-TJJI1ZLFJCvjvaPklLByCWYlQD66tbVkGPoTvPUzWVyTIryj8v5qbQWZgTGlUryoqas52DtslFi9AqQ0SPKysuhppC-RY_vVZrYVYw7u4FL0qRddH56jflC7mAqj47dxAK92xpWkZGFh2ha7gUvUaISgbXfZV99R_tI6iJoHFhdm2v5dODapDVz1uvZLR7TOslSJB5Ad7kVOtdaxqBJNjFmuYiuKkrxDIGSuWZynlteIAbR1EXet8T2MOWzYEDoh59fCDAcx9FNZhU-rEwmRIjt4W0NO4fiDHqmHRK8H3x1SmnDbbG8V4Cy4t5PuDSAwnn9b7r14uUI-RKGYMMrLw1mypJHoNEEumI663ENCA-6b9y4cJ15hGeSxqR3qEqbWU7XZ5NnmOpExtyPntePLOPBLk56CG_mL8ON1OITPZrgkMFdmR0dkdQfFCoiBVfK_0GThb9trpn91oybDZ189hFrIfWQ35AGlGmk45J8pzXSJCdrdKpgSMathpnPbg5sicBTU3CbMtcNruc7oDQfk9WU8FjY4mhCn-DpIJOC2FWkK1TulN3JliFjzNxvWLlu62XThWSI&X-Amz-Signature=cf816bd0aa3cd827f4cdbca1cfcda28c119d7f133cb4ca473c2419eb39756c7d",
"novel_text": "正弯腰在花丛中扒拉的拂晓衔蝉像狐獴一样站直身体,全身防御拉满,警惕的望着载酒寻歌:“你又想说什么让人不高兴的话,银发红瞳不美丽吗?”",
"timestamp": 1767855097.4541745
},
{
"prompt": "A manga panel in monochrome, high-quality manga style. The scene is set in a serene garden with blooming flowers. On the left, 拂晓衔蝉 is depicted with silver hair and red eyes, crouching cautiously among the flowers like a meerkat, her posture alert and defensive. On the right, 虞寻歌 stands upright with light golden hair, her expression earnest and non-confrontational as she asks a sincere question. The background features soft, detailed linework of foliage and petals, emphasizing the contrast between their postures and the peaceful setting.",
"image_url": "https://s3.siliconflow.cn/default/outputs/22aedcfb-5b15-43f6-9e4b-1615f33d0b86_8ff8defcdfa7a4c4278e6116bf410c03_ComfyUI_b9cbc20a_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260108%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260108T065148Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6NjVhMzEiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2Nzg0MzIwOSwiZXhwIjoxNzY4MDE2MDA5LCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1ZmliMmM1MG1pczczYWpubzFnIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.pKxO5YLlQ2J_fm5RTSRyj6zYAwUeP1isOQcAtzsjtndudeKY0YpdgDPyyDG9y2MK2HaUKidY7lHpDH5i9_DvRRUxB5IMTyocTGX-TJJI1ZLFJCvjvaPklLByCWYlQD66tbVkGPoTvPUzWVyTIryj8v5qbQWZgTGlUryoqas52DtslFi9AqQ0SPKysuhppC-RY_vVZrYVYw7u4FL0qRddH56jflC7mAqj47dxAK92xpWkZGFh2ha7gUvUaISgbXfZV99R_tI6iJoHFhdm2v5dODapDVz1uvZLR7TOslSJB5Ad7kVOtdaxqBJNjFmuYiuKkrxDIGSuWZynlteIAbR1EXet8T2MOWzYEDoh59fCDAcx9FNZhU-rEwmRIjt4W0NO4fiDHqmHRK8H3x1SmnDbbG8V4Cy4t5PuDSAwnn9b7r14uUI-RKGYMMrLw1mypJHoNEEumI663ENCA-6b9y4cJ15hGeSxqR3qEqbWU7XZ5NnmOpExtyPntePLOPBLk56CG_mL8ON1OITPZrgkMFdmR0dkdQfFCoiBVfK_0GThb9trpn91oybDZ189hFrIfWQ35AGlGmk45J8pzXSJCdrdKpgSMathpnPbg5sicBTU3CbMtcNruc7oDQfk9WU8FjY4mhCn-DpIJOC2FWkK1TulN3JliFjzNxvWLlu62XThWSI&X-Amz-Signature=c3cf0da228e27e055efd520637f72be11aa85e102a3436a6cc500700c7ceb28d",
"novel_text": "虞寻歌尽可能让自己的语气不要带上任何攻击性,因为她是真的好奇而不是为了挑衅,她站直身体,认真的问道:“那你头发变成浅金色后…岂不是很不适应?”",
"timestamp": 1767855109.414846
}
]

415
frontend/main.py Normal file
View File

@ -0,0 +1,415 @@
import streamlit as st
import requests
import os
import datetime
import base64
from pathlib import Path
# Configuration
ST_BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:5001")
st.set_page_config(page_title="Novel to Manga", layout="wide", page_icon="🎨")
# Constants
MODEL_OPTIONS = {
"OpenAI": ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"],
"SiliconFlow": ["deepseek-ai/DeepSeek-V2.5", "Qwen/Qwen2.5-72B-Instruct", "meta-llama/Meta-Llama-3.1-405B-Instruct"],
"AIHubMix": ["gpt-4o", "claude-3-5-sonnet-20240620", "gemini-1.5-pro", "gemini-2.0-flash-exp"],
"DeepSeek": ["deepseek-chat", "deepseek-coder"],
"Custom": []
}
IMAGE_MODEL_OPTIONS = {
"OpenAI": ["dall-e-3", "dall-e-2"],
"AIHubMix": ["gpt-image-1", "g-nano-banana-pro", "imagen-4.0-generate-001", "flux-pro", "midjourney"],
"SiliconFlow": ["black-forest-labs/FLUX.1-schnell", "black-forest-labs/FLUX.1-dev", "stabilityai/stable-diffusion-3-5-large"],
"DeepSeek": ["deepseek-chat"],
"Custom": []
}
# Load decorative images
images_dir = Path(__file__).parent.parent / "images"
# Load character image for top-right corner
char_path = images_dir / "微信图片_20260108172035_101_32.jpg"
char_encoded = ""
if char_path.exists():
with open(char_path, "rb") as f:
char_encoded = base64.b64encode(f.read()).decode()
# Load decorative image for bottom-left corner
bottom_left_path = images_dir / "Pasted image (3).png"
bottom_left_encoded = ""
if bottom_left_path.exists():
with open(bottom_left_path, "rb") as f:
bottom_left_encoded = base64.b64encode(f.read()).decode()
# Load decorative image for sidebar accent
sidebar_accent_path = images_dir / "Pasted image (2).png"
sidebar_accent_encoded = ""
if sidebar_accent_path.exists():
with open(sidebar_accent_path, "rb") as f:
sidebar_accent_encoded = base64.b64encode(f.read()).decode()
# Custom CSS with Light Transparent Background
st.markdown(f"""
<style>
/* Global Background - Semi-transparent White to reveal images */
html, body {{
background: linear-gradient(135deg, rgba(245, 247, 250, 0.9) 0%, rgba(228, 233, 242, 0.9) 100%) !important;
background-attachment: fixed !important;
color: #1a1a2e !important;
}}
.stApp {{
background: transparent !important;
}}
[data-testid="stAppViewContainer"] {{
background: transparent !important;
}}
/* Headers - Dark Blue/Black */
h1, h2, h3, .stHeading {{
color: #0d47a1 !important;
text-shadow: none;
font-family: 'Inter', sans-serif;
}}
/* Standard Text - Dark Gray */
p, div, span, label, li {{
color: #424242 !important;
}}
/* Inputs - White Glassmorphism - Strong Override */
/* Inputs - White Glassmorphism - Strong Override */
.stTextInput input, .stTextArea textarea, .stSelectbox select, div[data-baseweb="select"] > div {{
background-color: rgba(255, 255, 255, 0.7) !important;
color: #1a1a2e !important;
caret-color: #0d47a1;
border: 1px solid rgba(0, 0, 0, 0.15) !important;
backdrop-filter: blur(10px);
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
border-radius: 8px !important;
}}
/* Ensure placeholder text is visible */
::placeholder {{
color: rgba(0, 0, 0, 0.5) !important;
opacity: 1 !important;
}}
.stTextInput input:focus, .stTextArea textarea:focus, .stSelectbox select:focus, div[data-baseweb="select"] > div:focus-within {{
border-color: #00d4ff !important;
background-color: rgba(255, 255, 255, 0.95) !important;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.25);
}}
/* Code blocks */
code {{
color: #d81b60 !important;
background-color: rgba(0, 0, 0, 0.05) !important;
}}
/* Buttons */
.stButton button {{
background: linear-gradient(135deg, #00d4ff 0%, #00a8cc 100%) !important;
color: white !important;
font-weight: 600;
border: none;
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
}}
.stButton button:hover {{
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 212, 255, 0.4);
}}
/* Sidebar - Glassy White */
section[data-testid="stSidebar"] {{
background-color: rgba(255, 255, 255, 0.7) !important;
border-right: 1px solid rgba(0,0,0,0.05);
backdrop-filter: blur(15px);
}}
/* Dropdown menu items */
ul[data-testid="stSelectboxVirtualDropdown"] li {{
background-color: white !important;
color: #333 !important;
}}
ul[data-testid="stSelectboxVirtualDropdown"] li:hover {{
background-color: #e3f2fd !important;
}}
/* Expanders */
.streamlit-expanderHeader {{
background-color: rgba(255,255,255,0.6) !important;
color: #1a1a2e !important;
border-radius: 8px;
}}
/* Tabs */
.stTabs [data-baseweb="tab-list"] {{
background-color: rgba(255,255,255,0.5);
border-radius: 8px;
}}
.stTabs [data-baseweb="tab"] {{
color: #666;
}}
.stTabs [data-baseweb="tab"][aria-selected="true"] {{
background-color: white;
color: #00d4ff;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}}
/* Bottom-left decorative image */
/* Bottom decorative image - Moved to RIGHT */
/* Background decorative image - Full Screen Cover */
.stApp::before {{
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/png;base64,{bottom_left_encoded}');
background-size: cover;
background-repeat: no-repeat;
background-position: center;
opacity: 0.85; /* Much more visible */
z-index: 0;
pointer-events: none;
}}
/* Sidebar decorative accent */
section[data-testid="stSidebar"]::after {{
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 250px;
height: 250px;
background-image: url('data:image/png;base64,{sidebar_accent_encoded}');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0.15;
z-index: -1;
pointer-events: none;
}}
</style>
""", unsafe_allow_html=True)
# Add top-right character image - No whitespace
if char_encoded:
st.markdown(f"""
<div style="position: fixed; top: 60px; right: 20px; z-index: 999;">
<img src='data:image/jpeg;base64,{char_encoded}' style='display: block; width: auto; height: 100px; border-radius: 12px; border: 2px solid rgba(255, 255, 255, 0.9); box-shadow: 0 4px 15px rgba(0,0,0,0.2);'>
</div>
""", unsafe_allow_html=True)
st.title("📚 Novel to Manga Converter")
st.markdown("Transform your stories into visual manga panels with AI.")
# Sidebar for Settings
with st.sidebar:
st.header("⚙️ Settings")
# Provider Selection
provider = st.selectbox(
"API Provider",
["OpenAI", "SiliconFlow", "AIHubMix", "DeepSeek", "Custom"],
help="Select your API provider to auto-fill Base URL"
)
# Defaults based on provider
default_base_url = "https://api.openai.com/v1"
default_model = "gpt-4o"
if provider == "SiliconFlow":
default_base_url = "https://api.siliconflow.cn/v1"
default_model = "deepseek-ai/DeepSeek-V2.5"
elif provider == "AIHubMix":
default_base_url = "https://api.aihubmix.com/v1"
default_model = "gpt-4o"
elif provider == "DeepSeek":
default_base_url = "https://api.deepseek.com"
default_model = "deepseek-chat"
api_key_openai = st.text_input(f"{provider} API Key", type="password", help="Required for generating prompts")
base_url = st.text_input("Base URL", value=default_base_url)
# Model Selection (Text)
if provider == "Custom":
model_name = st.text_input("Model Name", value=default_model)
else:
available_models = MODEL_OPTIONS.get(provider, [default_model])
model_name = st.selectbox("Model Name", available_models, index=0 if default_model in available_models else 0)
st.divider()
st.subheader("Image Generation")
image_provider = st.selectbox(
"Image Provider",
["OpenAI", "AIHubMix", "SiliconFlow", "DeepSeek", "Custom"],
index=0,
help="Select provider for Image Generation"
)
# Image Gen Defaults
default_img_base = "https://api.openai.com/v1"
default_img_model = "dall-e-3"
if image_provider == "AIHubMix":
default_img_base = "https://api.aihubmix.com/v1"
default_img_model = "gpt-image-1"
elif image_provider == "SiliconFlow":
default_img_base = "https://api.siliconflow.cn/v1"
default_img_model = "black-forest-labs/FLUX.1-schnell"
api_key_image = st.text_input("Image Gen API Key (Optional)", type="password", help="Leave blank to use above key")
image_base_url = st.text_input("Image Base URL", value=default_img_base)
# Image Model Selection
if image_provider == "Custom":
image_model_name = st.text_input("Image Model", value=default_img_model, help="e.g. dall-e-3, gpt-image-1")
else:
available_img_models = IMAGE_MODEL_OPTIONS.get(image_provider, [default_img_model])
# Try to find default index, else 0
idx = 0
if default_img_model in available_img_models:
idx = available_img_models.index(default_img_model)
image_model_name = st.selectbox("Image Model", available_img_models, index=idx)
st.divider()
st.info(f"Backend expected at: `{ST_BACKEND_URL}`")
st.markdown("Ensuring backend is running...")
# Main Area
tab1, tab2 = st.tabs(["✨ Create", "📜 History"])
with tab1:
st.subheader("1. Input Novel Text")
novel_text = st.text_area("Paste your novel text here... (Paragraphs separated by double newlines)", height=200, placeholder="The hero stood on the cliff edge...")
if st.button("🚀 Analyze & Generate Prompts", type="primary"):
if not novel_text:
st.error("Please enter some text.")
elif not api_key_openai and not os.getenv("OPENAI_API_KEY"):
st.warning("Please provide an OpenAI API Key.")
else:
with st.spinner("Analyzing text and generating prompts..."):
try:
payload = {
"text": novel_text,
"api_key": api_key_openai,
"base_url": base_url if base_url else None,
"model": model_name
}
response = requests.post(f"{ST_BACKEND_URL}/process_text", json=payload)
if response.status_code == 200:
prompts = response.json().get("prompts", [])
st.session_state['prompts'] = prompts
st.session_state['novel_text'] = novel_text
st.success(f"Generated {len(prompts)} prompts!")
else:
st.error(f"Backend Error: {response.text}")
except requests.exceptions.ConnectionError:
st.error(f"Cannot connect to backend at {ST_BACKEND_URL}. Is the Flask app running?")
except Exception as e:
st.error(f"Error: {e}")
# Display Prompts and Image Gen
if 'prompts' in st.session_state and st.session_state['prompts']:
st.divider()
st.subheader("2. Review Prompts & Generate Images")
# Grid layout for panels
for i, item in enumerate(st.session_state['prompts']):
with st.container():
st.markdown(f"### Panel {i+1}")
col1, col2 = st.columns([1, 1])
with col1:
st.caption("Original Text")
st.info(item['paragraph'])
with col2:
st.caption("Manga Prompt (Editable)")
prompt_key = f"prompt_{i}"
# Initialize default prompt if not edited
if prompt_key not in st.session_state:
st.session_state[prompt_key] = item['prompt']
prompt_text = st.text_area("Prompt", key=prompt_key, label_visibility="collapsed", height=100)
if st.button(f"🎨 Generate Image for Panel {i+1}", key=f"btn_gen_{i}"):
with st.spinner("Drawing..."):
try:
# Use the edited prompt
payload = {
"prompt": prompt_text,
"api_key": api_key_image or api_key_openai, # Fallback
"base_url": image_base_url if image_base_url else None,
"model": image_model_name,
"novel_text": item['paragraph'] # Save context
}
res = requests.post(f"{ST_BACKEND_URL}/generate_image", json=payload)
if res.status_code == 200:
img_url = res.json().get("image_url")
st.session_state[f"image_{i}"] = img_url
# Fetch bytes for download
try:
img_data = requests.get(img_url).content
st.session_state[f"image_data_{i}"] = img_data
except:
pass
else:
st.error(f"Backend Error: {res.text}")
except Exception as e:
st.error(f"Error: {e}")
if f"image_{i}" in st.session_state:
st.image(st.session_state[f"image_{i}"], use_container_width=True)
if f"image_data_{i}" in st.session_state:
st.download_button(
label="⬇️ Download Panel",
data=st.session_state[f"image_data_{i}"],
file_name=f"panel_{i+1}.png",
mime="image/png",
key=f"dl_{i}"
)
with tab2:
st.header("History")
if st.button("🔄 Refresh History"):
try:
res = requests.get(f"{ST_BACKEND_URL}/history")
if res.status_code == 200:
history = res.json().get("history", [])
st.session_state['history'] = history
if not history:
st.info("No history found.")
else:
st.error("Failed to fetch history")
except requests.exceptions.ConnectionError:
st.error(f"Cannot connect to backend.")
if 'history' in st.session_state:
for item in st.session_state['history']:
ts = item.get('timestamp')
date_str = datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') if ts else "Unknown Date"
with st.expander(f"Generations from {date_str} - {item.get('prompt')[:30]}..."):
col_h1, col_h2 = st.columns([1, 2])
with col_h1:
st.image(item.get('image_url'), caption="Generated Image")
with col_h2:
st.markdown("**Prompt:**")
st.code(item.get('prompt'))
if item.get('novel_text'):
st.markdown("**Original Text:**")
st.write(item.get('novel_text'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

BIN
images/Pasted image (2).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
images/Pasted image (3).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
images/Pasted image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
images/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

6
main.py Normal file
View File

@ -0,0 +1,6 @@
def main():
print("Hello from novel-to-manga!")
if __name__ == "__main__":
main()

14
pyproject.toml Normal file
View File

@ -0,0 +1,14 @@
[project]
name = "novel-to-manga"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"flask>=3.1.2",
"openai>=2.14.0",
"pydantic-ai>=1.40.0",
"python-dotenv>=1.2.1",
"requests>=2.32.5",
"streamlit>=1.52.2",
]

3162
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff