最近OpenAI的DALL-E 3 API价格降了不少,我就琢磨着能不能做个批量生图的工具,既能满足自己的需求,又能顺便赚点服务器钱。折腾了几天,还真做出来了,今天来分享一下整个开发过程。
为什么要做批量生图?
做自媒体的都知道,配图是个大问题。写一篇文章需要好几张配图,一张一张生成太慢了。市面上的AI绘图工具要么太贵,要么功能太复杂,我就想做个简单直接的批量生图工具。
核心需求就三个:
- 批量生成:一次生成1-10张图
- 可控成本:不同质量不同价格,用户自己选
- 支持参考图:能上传参考图控制风格
技术选型
后端用的FastAPI,前端就简单的Vue 3,AI模型用的是OpenAI的GPT-Image-1.5(其实就是DALL-E 3的升级版)。
为什么选GPT-Image-1.5?
- 生成速度快,平均5-8秒一张
- 质量稳定,不像SD那样需要反复调参
- API简单,不用自己搭模型

核心代码实现
1. 后端API设计
先搞个基础的FastAPI框架:
python
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from typing import List, Optional
import openai
import asyncio
from pydantic import BaseModel
app = FastAPI(title="批量生图API")
# 跨域配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class ImageGenerationRequest(BaseModel):
prompt: str
size: str = "1024x1024" # 1024x1024, 1536x1024, 1024x1536
quality: str = "standard" # standard, hd
count: int = 1 # 1-10
reference_images: Optional[List[str]] = None
@app.post("/api/generate-images")
async def generate_images(request: ImageGenerationRequest):
"""批量生成图片"""
return await batch_generate(request)
2. 批量生成逻辑
这是核心部分,要处理并发生成和错误重试:
python
import aiohttp
import base64
from pathlib import Path
async def batch_generate(request: ImageGenerationRequest):
"""异步批量生成图片"""
# 构建完整的prompt
full_prompt = request.prompt
# 如果有参考图,先分析参考图
if request.reference_images:
style_description = await analyze_reference_images(
request.reference_images
)
full_prompt = f"{request.prompt}, style: {style_description}"
# 计算费用
cost = calculate_cost(request.size, request.quality, request.count)
# 并发生成多张图片
tasks = []
for i in range(request.count):
task = generate_single_image(
prompt=full_prompt,
size=request.size,
quality=request.quality,
index=i
)
tasks.append(task)
# 等待所有任务完成
results = await asyncio.gather(*tasks, return_exceptions=True)
# 过滤掉失败的任务
success_images = [r for r in results if not isinstance(r, Exception)]
failed_count = len(results) - len(success_images)
return {
"success": True,
"total": request.count,
"generated": len(success_images),
"failed": failed_count,
"cost": cost,
"images": success_images
}
async def generate_single_image(
prompt: str,
size: str,
quality: str,
index: int,
max_retries: int = 3
) -> dict:
"""生成单张图片,支持重试"""
for attempt in range(max_retries):
try:
response = await openai.Image.acreate(
model="dall-e-3",
prompt=prompt,
size=size,
quality=quality,
n=1
)
image_url = response.data[0].url
# 下载图片到本地
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as resp:
image_data = await resp.read()
# 保存图片
filename = f"generated_{index}_{int(time.time())}.png"
file_path = Path(f"static/images/{filename}")
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "wb") as f:
f.write(image_data)
return {
"index": index,
"url": f"/images/{filename}",
"revised_prompt": response.data[0].revised_prompt
}
except Exception as e:
if attempt == max_retries - 1:
raise e
await asyncio.sleep(2 ** attempt) # 指数退避
raise Exception(f"Failed to generate image {index}")
3. 参考图分析
这个功能是亮点,用GPT-4 Vision分析参考图的风格:
python
async def analyze_reference_images(image_paths: List[str]) -> str:
"""分析参考图片,提取风格特征"""
# 读取图片并转base64
image_contents = []
for path in image_paths[:3]: # 最多分析3张
with open(path, "rb") as f:
base64_image = base64.b64encode(f.read()).decode()
image_contents.append({
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
})
# 调用GPT-4 Vision
response = await openai.ChatCompletion.acreate(
model="gpt-4-vision-preview",
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": "分析这些图片的视觉风格,包括色调、构图、光影、艺术风格等,用简洁的英文描述,用于AI绘图的风格参考。"
},
*image_contents
]
}
],
max_tokens=200
)
return response.choices[0].message.content
4. 价格计算
根据不同尺寸和质量计算费用:
python
def calculate_cost(size: str, quality: str, count: int) -> float:
"""计算生成费用"""
# 基础价格(单位:元)
base_prices = {
"1024x1024": {"standard": 0.04, "hd": 0.08},
"1536x1024": {"standard": 0.08, "hd": 0.12},
"1024x1536": {"standard": 0.08, "hd": 0.12}
}
unit_price = base_prices.get(size, {}).get(quality, 0.04)
# 加上平台服务费(150%加价)
final_price = unit_price * 2.5
# 质量映射
quality_map = {"standard": 0.1, "hd": 0.5}
if quality == "hd":
final_price = 1.5
return round(count * final_price, 2)
5. 前端交互
前端用Vue 3写个简单的界面:
vue
<template>
<div class="image-generator">
<h1>批量生图</h1>
<p class="subtitle">GPT-Image-1.5 批量图片生成,快速高效</p>
<!-- 提示词输入 -->
<div class="prompt-section">
<label>提示词</label>
<textarea
v-model="prompt"
placeholder="例如:一只可爱的猫咪在樱花树下玩耍,动漫风格"
rows="5"
/>
<button @click="clearPrompt">清空提示词</button>
</div>
<!-- 参考图上传 -->
<div class="reference-section">
<label>参考图片</label>
<div class="upload-area" @click="selectFiles">
<i class="icon-upload"></i>
<p>添加图片</p>
</div>
<input
ref="fileInput"
type="file"
multiple
accept="image/*"
@change="handleFileUpload"
style="display: none"
/>
</div>
<!-- 尺寸选择 -->
<div class="size-section">
<label>图像尺寸</label>
<div class="options">
<button
v-for="size in sizes"
:key="size"
:class="{ active: selectedSize === size }"
@click="selectedSize = size"
>
{{ size }}
</button>
</div>
</div>
<!-- 质量选择 -->
<div class="quality-section">
<label>质量</label>
<div class="options">
<button
:class="{ active: quality === 'standard' }"
@click="quality = 'standard'"
>
低 (0.1元)
</button>
<button
:class="{ active: quality === 'hd' }"
@click="quality = 'hd'"
>
高 (1.5元)
</button>
</div>
</div>
<!-- 数量滑块 -->
<div class="count-section">
<label>生成数量</label>
<input
type="range"
v-model="count"
min="1"
max="10"
/>
<span>{{ count }}</span>
</div>
<!-- 生成按钮 -->
<button
class="generate-btn"
@click="generateImages"
:disabled="loading"
>
{{ loading ? '生成中...' : `生成图片 (${totalCost}元)` }}
</button>
<!-- 结果展示 -->
<div class="results" v-if="generatedImages.length">
<h3>生成结果</h3>
<div class="image-grid">
<img
v-for="(img, index) in generatedImages"
:key="index"
:src="img.url"
:alt="`Generated ${index + 1}`"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import axios from 'axios'
const prompt = ref('')
const selectedSize = ref('1024x1024')
const quality = ref('standard')
const count = ref(1)
const loading = ref(false)
const generatedImages = ref([])
const referenceImages = ref([])
const sizes = ['1024x1024', '1536x1024', '1024x1536']
const totalCost = computed(() => {
const priceMap = {
'standard': 0.1,
'hd': 1.5
}
return (count.value * priceMap[quality.value]).toFixed(2)
})
const generateImages = async () => {
if (!prompt.value.trim()) {
alert('请输入提示词')
return
}
loading.value = true
try {
const response = await axios.post('/api/generate-images', {
prompt: prompt.value,
size: selectedSize.value,
quality: quality.value,
count: count.value,
reference_images: referenceImages.value
})
generatedImages.value = response.data.images
alert(`成功生成 ${response.data.generated} 张图片!`)
} catch (error) {
alert('生成失败:' + error.message)
} finally {
loading.value = false
}
}
const handleFileUpload = (event) => {
const files = Array.from(event.target.files)
// 处理文件上传逻辑
}