2025.10.14故事生成系统(图片生成功能实现)
第4天:图片生成功能实现
📋 第4天概述
在第3天完成文字生成功能的基础上,第4天将重点实现图片生成功能的完整业务逻辑,包括图像生成服务、图片处理工具、文件管理以及用户界面集成。
🎯 第4天目标
主要任务:构建完整的图片生成系统,实现智能图像生成、图片处理和文件管理
核心需求:开发稳定可靠的图像生成服务,支持多种风格和尺寸定制
🏗️ 架构设计
服务层架构
class ImageGenerationService:
"""图片生成服务类"""
def __init__(self, aliyun_client):
self.client = aliyun_client
self.image_templates = self._load_image_templates()
self.image_storage = ImageStorage()
# 核心生成方法
def generate_image(self, prompt, style, size, **kwargs)
def _build_image_prompt(self, base_prompt, style)
def _process_image_response(self, response)
# 图片管理
def save_image(self, image_data, metadata)
def get_image_history(self, limit=10)
def delete_image(self, image_id)
# 工具方法
def resize_image(self, image_path, target_size)
def convert_format(self, image_path, target_format)
def add_watermark(self, image_path, watermark_text)
🔧 具体实施步骤
步骤1:创建图片生成服务类
创建 src/services/image_service.py:
import os
import base64
import json
import time
from datetime import datetime
from typing import Dict, List, Optional, Any, Tuple
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import io
class ImageGenerationService:
"""
图片生成服务类
负责图像生成、图片处理和文件管理
"""
def __init__(self, aliyun_client, upload_folder: str = "./uploads"):
self.client = aliyun_client
self.upload_folder = Path(upload_folder)
self.image_templates = self._load_image_templates()
self.image_history = []
self.max_history_size = 50
# 确保上传目录存在
self.upload_folder.mkdir(exist_ok=True)
# 创建图片子目录
(self.upload_folder / "images").mkdir(exist_ok=True)
(self.upload_folder / "thumbnails").mkdir(exist_ok=True)
def _load_image_templates(self) -> Dict[str, Dict[str, Any]]:
"""加载图片生成模板"""
templates = {
"卡通风格": {
"prompt_suffix": ",卡通风格,色彩鲜艳,线条简洁",
"styles": ["可爱", "搞笑", "简约", "日系", "美式"],
"size_options": ["512x512", "768x768", "1024x1024", "1024x768", "768x1024"]
},
"写实风格": {
"prompt_suffix": ",写实风格,细节丰富,光影真实",
"styles": ["超写实", "油画", "水彩", "素描", "摄影"],
"size_options": ["512x512", "768x768", "1024x1024", "1024x768", "768x1024"]
},
"奇幻风格": {
"prompt_suffix": ",奇幻风格,充满想象力,神秘氛围",
"styles": ["魔法", "神话", "科幻", "童话", "梦境"],
"size_options": ["512x512", "768x768", "1024x1024", "1024x768", "768x1024"]
},
"教育风格": {
"prompt_suffix": ",教育风格,清晰易懂,适合儿童",
"styles": ["科普", "数学", "语言", "艺术", "历史"],
"size_options": ["512x512", "768x768", "1024x1024", "1024x768", "768x1024"]
}
}
return templates
def get_available_styles(self) -> List[str]:
"""获取可用的图片风格"""
return list(self.image_templates.keys())
def get_style_options(self, style: str) -> List[str]:
"""获取指定风格的子风格选项"""
if style in self.image_templates:
return self.image_templates[style]["styles"]
return []
def get_size_options(self, style: str) -> List[str]:
"""获取指定风格的尺寸选项"""
if style in self.image_templates:
return self.image_templates[style]["size_options"]
return []
def _build_image_prompt(self, base_prompt: str, style: str, sub_style: str = "") -> str:
"""构建图片生成提示词"""
if style in self.image_templates:
prompt_suffix = self.image_templates[style]["prompt_suffix"]
if sub_style:
prompt_suffix = prompt_suffix.replace("风格", f"{sub_style}风格")
final_prompt = base_prompt + prompt_suffix
else:
final_prompt = base_prompt
# 添加质量要求
quality_requirements = """
请确保图片具有以下特点:
1. 构图合理,主体突出
2. 色彩协调,视觉效果良好
3. 细节丰富,避免模糊
4. 符合儿童审美,避免恐怖内容
5. 具有艺术性和创意性
"""
return final_prompt + quality_requirements
def generate_image(self, prompt: str, style: str, size: str = "1024x1024",
sub_style: str = "", model: str = "wan2.5-t2i-preview",
n: int = 1, **kwargs) -> Dict[str, Any]:
"""
生成图片
Args:
prompt: 提示词
style: 图片风格
size: 图片尺寸
sub_style: 子风格
model: 模型名称
n: 生成数量
**kwargs: 其他参数
Returns:
dict: 生成结果
"""
# 构建完整提示词
full_prompt = self._build_image_prompt(prompt, style, sub_style)
print(f"🎨 开始生成图片...")
print(f"提示词: {prompt}")
print(f"风格: {style} - {sub_style}")
print(f"尺寸: {size}")
print(f"完整提示词长度: {len(full_prompt)} 字符")
try:
# 调用API生成图片
start_time = time.time()
response = self.client.generate_image(
prompt=full_prompt,
model=model,
size=size,
style=sub_style or style,
n=n,
**kwargs
)
generation_time = time.time() - start_time
# 处理响应
processed_result = self._process_image_response(response)
# 构建结果
result = {
"success": True,
"prompt": prompt,
"style": style,
"sub_style": sub_style,
"size": size,
"generation_time": round(generation_time, 2),
"timestamp": datetime.now().isoformat(),
"model_used": model,
**processed_result
}
# 保存图片文件
if processed_result.get("image_url"):
saved_path = self._save_image_from_url(
processed_result["image_url"],
result
)
result["local_path"] = str(saved_path)
# 生成缩略图
thumbnail_path = self._create_thumbnail(saved_path)
result["thumbnail_path"] = str(thumbnail_path)
# 保存到历史记录
self._save_to_history(result)
print(f"✅ 图片生成成功!")
print(f"生成时间: {generation_time:.2f}秒")
print(f"图片尺寸: {size}")
return result
except Exception as e:
error_result = {
"success": False,
"error": str(e),
"prompt": prompt,
"style": style,
"size": size,
"timestamp": datetime.now().isoformat()
}
print(f"❌ 图片生成失败: {e}")
return error_result
def _process_image_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
"""处理图片生成响应"""
# 阿里云文生图API返回格式处理
if "output" in response and "task_id" in response["output"]:
# 异步任务模式,返回任务ID
return {
"task_id": response["output"]["task_id"],
"task_status": "pending"
}
elif "data" in response and len(response["data"]) > 0:
# 直接返回图片URL
image_data = response["data"][0]
return {
"image_url": image_data.get("url", ""),
"task_status": "completed"
}
else:
raise ValueError("无法从响应中提取图片信息")
def _save_image_from_url(self, image_url: str, metadata: Dict[str, Any]) -> Path:
"""从URL下载并保存图片"""
import requests
try:
# 下载图片
response = requests.get(image_url, timeout=30)
response.raise_for_status()
# 生成文件名
timestamp = int(time.time())
filename = f"image_{timestamp}_{metadata['style']}.png"
filepath = self.upload_folder / "images" / filename
# 保存图片
with open(filepath, 'wb') as f:
f.write(response.content)
# 验证图片有效性
with Image.open(filepath) as img:
img.verify()
return filepath
except Exception as e:
raise Exception(f"图片保存失败: {e}")
def _create_thumbnail(self, image_path: Path, size: Tuple[int, int] = (200, 200)) -> Path:
"""创建缩略图"""
try:
with Image.open(image_path) as img:
# 保持宽高比生成缩略图
img.thumbnail(size, Image.Resampling.LANCZOS)
# 生成缩略图文件名
thumbnail_name = f"thumb_{image_path.stem}.png"
thumbnail_path = self.upload_folder / "thumbnails" / thumbnail_name
# 保存缩略图
img.save(thumbnail_path, "PNG")
return thumbnail_path
except Exception as e:
raise Exception(f"缩略图创建失败: {e}")
def _save_to_history(self, image_data: Dict[str, Any]):
"""保存图片到历史记录"""
if image_data["success"]:
# 为图片分配ID
image_id = f"image_{len(self.image_history) + 1}_{int(time.time())}"
image_data["id"] = image_id
# 添加到历史记录
self.image_history.append(image_data)
# 限制历史记录大小
if len(self.image_history) > self.max_history_size:
self.image_history = self.image_history[-self.max_history_size:]
def get_image_history(self, limit: int = 10) -> List[Dict[str, Any]]:
"""获取图片历史记录"""
return self.image_history[-limit:]
def get_image_by_id(self, image_id: str) -> Optional[Dict[str, Any]]:
"""根据ID获取图片"""
for image in self.image_history:
if image.get("id") == image_id:
return image
return None
def delete_image(self, image_id: str) -> bool:
"""删除指定图片"""
for i, image in enumerate(self.image_history):
if image.get("id") == image_id:
# 删除本地文件
if "local_path" in image:
try:
Path(image["local_path"]).unlink(missing_ok=True)
except:
pass
if "thumbnail_path" in image:
try:
Path(image["thumbnail_path"]).unlink(missing_ok=True)
except:
pass
# 从历史记录中移除
self.image_history.pop(i)
return True
return False
def resize_image(self, image_path: str, target_size: Tuple[int, int]) -> Path:
"""调整图片尺寸"""
try:
with Image.open(image_path) as img:
# 调整尺寸
resized_img = img.resize(target_size, Image.Resampling.LANCZOS)
# 生成新文件名
original_path = Path(image_path)
new_name = f"{original_path.stem}_{target_size[0]}x{target_size[1]}{original_path.suffix}"
new_path = original_path.parent / new_name
# 保存调整后的图片
resized_img.save(new_path)
return new_path
except Exception as e:
raise Exception(f"图片尺寸调整失败: {e}")
def convert_format(self, image_path: str, target_format: str) -> Path:
"""转换图片格式"""
try:
with Image.open(image_path) as img:
# 生成新文件名
original_path = Path(image_path)
new_name = f"{original_path.stem}.{target_format.lower()}"
new_path = original_path.parent / new_name
# 转换格式并保存
if target_format.upper() == "JPG":
# 转换为RGB模式(JPG不支持透明度)
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
img.save(new_path, target_format.upper())
return new_path
except Exception as e:
raise Exception(f"图片格式转换失败: {e}")
def add_watermark(self, image_path: str, watermark_text: str,
position: str = "bottom-right", opacity: int = 50) -> Path:
"""添加水印"""
try:
with Image.open(image_path) as img:
# 转换为RGBA模式以支持透明度
if img.mode != "RGBA":
img = img.convert("RGBA")
# 创建水印图层
watermark = Image.new("RGBA", img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(watermark)
# 尝试加载字体,失败则使用默认字体
try:
font = ImageFont.truetype("arial.ttf", 24)
except:
font = ImageFont.load_default()
# 计算水印位置
bbox = draw.textbbox((0, 0), watermark_text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
if position == "bottom-right":
x = img.width - text_width - 10
y = img.height - text_height - 10
elif position == "top-left":
x = 10
y = 10
elif position == "center":
x = (img.width - text_width) // 2
y = (img.height - text_height) // 2
else:
x = 10
y = img.height - text_height - 10
# 绘制水印
draw.text((x, y), watermark_text, font=font, fill=(255, 255, 255, int(255 * opacity / 100)))
# 合并图片和水印
watermarked = Image.alpha_composite(img, watermark)
# 生成新文件名
original_path = Path(image_path)
new_name = f"{original_path.stem}_watermarked{original_path.suffix}"
new_path = original_path.parent / new_name
# 保存带水印的图片
watermarked.save(new_path)
return new_path
except Exception as e:
raise Exception(f"水印添加失败: {e}")
步骤2:创建图片生成Web路由
创建 src/web/routes/image_routes.py:
from flask import Blueprint, request, jsonify, send_file
from src.services.image_service import ImageGenerationService
import os
from pathlib import Path
# 创建蓝图
image_bp = Blueprint('image', __name__)
def init_image_routes(app, aliyun_client, upload_folder: str):
"""初始化图片生成路由"""
image_service = ImageGenerationService(aliyun_client, upload_folder)
@image_bp.route('/api/generate_image', methods=['POST'])
def generate_image():
"""生成图片API"""
try:
data = request.get_json()
# 验证必需参数
required_fields = ['prompt', 'style']
for field in required_fields:
if field not in data:
return jsonify({
'success': False,
'error': f'缺少必需参数: {field}'
}), 400
# 调用服务生成图片
result = image_service.generate_image(
prompt=data['prompt'],
style=data['style'],
size=data.get('size', '1024x1024'),
sub_style=data.get('sub_style', ''),
model=data.get('model', 'wan2.5-t2i-preview'),
n=int(data.get('n', 1))
)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'error': f'生成图片失败: {str(e)}'
}), 500
@image_bp.route('/api/image_history', methods=['GET'])
def get_image_history():
"""获取图片历史记录"""
try:
limit = int(request.args.get('limit', 10))
history = image_service.get_image_history(limit)
return jsonify({
'success': True,
'history': history
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@image_bp.route('/api/image_templates', methods=['GET'])
def get_image_templates():
"""获取图片模板信息"""
try:
styles = image_service.get_available_styles()
templates = {}
for style in styles:
templates[style] = {
'sub_styles': image_service.get_style_options(style),
'size_options': image_service.get_size_options(style)
}
return jsonify({
'success': True,
'templates': templates
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@image_bp.route('/api/image/<path:filename>')
def serve_image(filename):
"""提供图片文件服务"""
try:
filepath = Path(upload_folder) / "images" / filename
if not filepath.exists() or not filepath.is_file():
return jsonify({'error': '图片不存在'}), 404
return send_file(filepath)
except Exception as e:
return jsonify({'error': str(e)}), 500
@image_bp.route('/api/thumbnail/<path:filename>')
def serve_thumbnail(filename):
"""提供缩略图服务"""
try:
filepath = Path(upload_folder) / "thumbnails" / filename
if not filepath.exists() or not filepath.is_file():
return jsonify({'error': '缩略图不存在'}), 404
return send_file(filepath)
except Exception as e:
return jsonify({'error': str(e)}), 500
@image_bp.route('/api/process_image', methods=['POST'])
def process_image():
"""图片处理API"""
try:
data = request.get_json()
if 'image_id' not in data or 'action' not in data:
return jsonify({'error': '缺少必需参数'}), 400
image = image_service.get_image_by_id(data['image_id'])
if not image or 'local_path' not in image:
return jsonify({'error': '图片不存在'}), 404
result = {}
if data['action'] == 'resize':
size = tuple(map(int, data['size'].split('x')))
new_path = image_service.resize_image(image['local_path'], size)
result['new_path'] = str(new_path)
elif data['action'] == 'convert':
new_path = image_service.convert_format(image['local_path'], data['format'])
result['new_path'] = str(new_path)
elif data['action'] == 'watermark':
new_path = image_service.add_watermark(
image['local_path'],
data['text'],
data.get('position', 'bottom-right'),
int(data.get('opacity', 50))
)
result['new_path'] = str(new_path)
return jsonify({
'success': True,
'result': result
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
# 注册蓝图
app.register_blueprint(image_bp, url_prefix='/image')
步骤3:创建图片生成前端界面
创建 src/web/templates/image_generation.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI图片生成器</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
.image-generator {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.form-container {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.form-control {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #4CAF50;
}
.btn-generate {
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
color: white;
border: none;
padding: 15px 40px;
border-radius: 25px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.btn-generate:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
.btn-generate:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.image-result {
margin-top: 30px;
text-align: center;
display: none;
}
.generated-image {
max-width: 100%;
max-height: 600px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
margin-bottom: 20px;
}
.image-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.btn-action {
background: #2196F3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
transition: background 0.3s;
}
.btn-action:hover {
background: #1976D2;
}
.loading {
text-align: center;
padding: 40px;
display: none;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 2s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
display: none;
}
.history-section {
margin-top: 50px;
}
.history-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.history-item {
background: white;
border-radius: 10px;
padding: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
cursor: pointer;
transition: transform 0.3s;
}
.history-item:hover {
transform: translateY(-5px);
}
.history-thumbnail {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 5px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="image-generator">
<h1>🎨 AI图片生成器</h1>
<div class="form-container">
<div class="form-group">
<label for="prompt">图片描述:</label>
<textarea id="prompt" class="form-control" rows="3"
placeholder="请输入您想要生成的图片描述,例如:一只可爱的小猫在花园里玩耍..."></textarea>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1;">
<label for="style">主要风格:</label>
<select id="style" class="form-control">
<option value="">请选择风格...</option>
</select>
</div>
<div class="form-group" style="flex: 1;">
<label for="subStyle">子风格:</label>
<select id="subStyle" class="form-control" disabled>
<option value="">请先选择主要风格</option>
</select>
</div>
<div class="form-group" style="flex: 1;">
<label for="size">图片尺寸:</label>
<select id="size" class="form-control" disabled>
<option value="">请先选择风格</option>
</select>
</div>
</div>
<button id="generateBtn" class="btn-generate">✨ 生成图片</button>
</div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>AI正在创作中,这可能需要一些时间...</p>
</div>
<div id="errorMessage" class="error-message"></div>
<div id="imageResult" class="image-result">
<h3>🖼️ 生成的图片</h3>
<img id="generatedImage" class="generated-image" src="" alt="生成的图片">
<div class="image-meta">
<p><strong>生成时间:</strong> <span id="generationTime"></span>秒</p>
<p><strong>图片尺寸:</strong> <span id="imageSize"></span></p>
</div>
<div class="image-actions">
<button id="downloadBtn" class="btn-action">💾 下载图片</button>
<button id="copyPromptBtn" class="btn-action">📋 复制提示词</button>
<button id="regenerateBtn" class="btn-action">🔄 重新生成</button>
</div>
</div>
<div class="history-section">
<h3>📚 生成历史</h3>
<div id="historyGrid" class="history-grid"></div>
</div>
</div>
<script src="{{ url_for('static', filename='js/image_generation.js') }}"></script>
</body>
</html>

浙公网安备 33010602011771号