从零构建AI纹身设计工具:提示词工程与图像生成实战
2026/9/16 13:20:52 网站建设 项目流程

我们直接进入正题。最近在 Hacker News 上看到一个很有意思的项目:TattooIdeas,定位是AI 纹身设计与规划工具。它不只是简单的“输入关键词出图”,而是把“纹身设计”这个强个性化、高审美门槛的需求,拆解成了一套完整的 AI 工作流:风格选择、图案生成、部位适配、尺寸规划。

这个项目正好踩中了当前 AI 应用落地的一个典型方向:用大模型 + 图像生成模型,去解决一个垂直领域的创作问题。这篇文章我会结合 TattooIdeas 的产品思路,完整拆解如何从零构建一个 AI 纹身设计工具,包括需求分析、架构设计、提示词工程、后端服务、前端页面、常见踩坑和工程建议。代码部分会尽量完整,你可以直接照着跑。


1. 背景与核心概念

1.1 什么是 AI 纹身设计工具

纹身设计是一个高度依赖“设计师审美 + 客户个人故事 + 皮肤部位”的复合型需求。传统流程是:客户找纹身师沟通想法,纹身师手绘初稿,反复修改,最后才上针。这个流程周期长、成本高,而且很多客户其实自己也不清楚想要什么风格。

AI 纹身设计工具要解决的核心问题就是:把“模糊的想法”快速变成“可感知的视觉方案”。用户只需要输入几个关键词,比如“海浪、日式、小臂”,AI 就能生成若干张风格明确的纹身图案,帮助用户快速确认方向,再带着参考图去找纹身师细化和定稿。

从技术视角看,这就是一个典型的AI 图像生成应用,但比普通文生图多了几层业务逻辑:

  • 纹身风格有很多细分,需要基于纹身领域的知识做风格分类和提示词封装;
  • 生成结果要区分“打样概念图”和“真正能上身的设计稿”;
  • 不同身体部位对图案的横竖比例、线条粗细要求不同,需要做尺寸和构图规划;
  • 纹身图案很容易出现文字错误、畸形手指等生成瑕疵,需要有质量过滤机制。

1.2 它解决了什么问题

可以总结成三个层面:

用户痛点AI 工具价值
说不清楚自己想要什么风格通过风格分类和示例图,帮用户快速聚焦
找纹身师沟通成本高先生成参考图,带着方案去沟通,效率更高
担心纹身图案不合适支持部位、尺寸模拟,提前在“虚拟皮肤”上预览效果

对开发者来说,这个项目类型非常适合作为AI 应用开发的练手项目。它不像聊天机器人那样只处理文本,而是融合了提示词工程、图像生成、异步任务、存储、渲染等多个模块,能让你完整走一遍 AI 应用的工程链路。

1.3 和普通文生图应用的区别

很多人会问:直接用 Midjourney 或者 Stable Diffusion 不就行了吗?为什么还要自己开发?

区别在于:

  • Midjourney 是通用工具,它不了解纹身行业的知识结构,不会主动问你“想要黑灰还是彩色”“要传统还是写实”;
  • 纹身风格的提示词有一套专门的表达体系,普通用户不会写;
  • 纹身设计还需要后处理,比如透明背景输出、图案去重、清晰度增强等,通用工具做不到;
  • 一个面向普通用户的工具,交互界面必须足够简单,不能让用户去学提示词语法。

所以,TattooIdeas 这类产品的工程核心就是:把纹身领域知识沉淀成提示词模板和规则引擎,在通用图像模型之上做一层垂直封装


2. 系统架构与核心流程设计

2.1 整体架构

构建一个 AI 纹身设计工具,比较合理的架构如下:

用户输入 ↓ 前端页面(风格选择 + 关键词输入) ↓ 后端服务(FastAPI) ├── 风格识别与提示词组装 ├── 图像生成代理层(对接多个生成服务) ├── 异步任务队列 / 同步请求 ├── 生成结果过滤与后处理 └── 结果存储(本地文件 / 对象存储 / 数据库) ↓ 返回多张候选图 + 部位预览信息

如果你的目标是快速验证,可以先不做任务队列,采用同步请求的方式。如果未来要支持高并发,再引入 Celery 或者 Redis 队列。

2.2 核心模块拆解

整个系统我建议拆成以下模块:

模块职责
输入解析识别用户输入中的风格、主体、色彩、部位等要素
提示词引擎根据解析结果,拼装出适合图像模型的完整提示词
图像生成客户端对接 OpenAI、Stable Diffusion API 或其他兼容接口
结果处理器过滤低质量图片、校验尺寸、生成缩略图
数据存储保存用户设计记录,方便历史回溯
Web 界面提供风格选择、生成展示、下载功能

2.3 技术选型

这套项目我在本地复刻时用的技术栈如下,供参考:

  • 后端:Python 3.10+、FastAPI
  • 图像生成:OpenAI 兼容的图像生成接口(也可以换 Stable Diffusion WebUI API 或国内云厂商的生成服务)
  • 前端:原生 HTML + JavaScript + Tailwind CDN(轻量,不引入复杂工程)
  • 存储:SQLite 记录元数据,图片落盘到本地output目录
  • 环境管理:pip + virtualenv

版本这块需要说明一下:AI 图像生成的第三方 SDK 更新非常快,OpenAI Python SDK 为例,1.x 和 0.x 的调用写法差异很大。本文的代码以 OpenAI Python SDK 1.x 的写法为例,如果你安装的版本不同,请根据官方文档微调。核心逻辑不变。


3. 核心原理拆解:纹身提示词工程

3.1 为什么提示词是核心

图像生成模型的能力边界,很大程度上由提示词决定。同样的模型,用“tattoo design”和用一段结构化的纹身提示词,生成质量天差地别。

在 TattooIdeas 这类工具中,提示词引擎不能是简单的字符串拼接,而应该是一套有结构的模板系统。我把纹身提示词拆成了八个维度:

  1. 主体对象(Subject):海浪、老鹰、玫瑰花、狼、经文、几何图形
  2. 风格(Style):传统美式、日式、黑灰写实、点刺、水彩、几何线条
  3. 线条风格(Line Style):粗线条、细线条、单针细线、纯黑轮廓
  4. 色彩模式(Color):黑白、黑灰变调、单色、彩色
  5. 构图(Composition):居中对称、流线型、包臂满背、小面积点缀
  6. 尺寸与密度(Scale & Density):大面积满铺、中等面积、小图精细
  7. 背景与画布(Canvas):纯白背景、透明背景、模拟皮肤贴图
  8. 负面提示(Negative Prompt):重影、变形、文字乱码、过曝等

3.2 风格类型与提示词关键词

下面是纹身领域常见风格的中英文关键词对照表,这部分是我做提示词模板时的重要积累:

中文风格英文提示词关键词
传统美式traditional american tattoo, bold black outline, limited color palette
日式传统japanese traditional tattoo, irezumi style, flowing waves, peony
黑灰写实black and grey realism tattoo, soft shading, high contrast
点刺dotwork tattoo, stippling, geometric dots
几何线条geometric line art tattoo, fine lines, sacred geometry
水彩watercolor tattoo style, splash of color, no hard outline
极简线条minimalist tattoo, single line art, continuous line
新传统neo traditional tattoo, illustrative, rich textures

3.3 模板设计示例

我设计的提示词模板大致如下:

def build_tattoo_prompt(subject: str, style: str, color: str, placement: str, extra: str = "") -> str: style_keywords = TATTOO_STYLES.get(style, TATTOO_STYLES["black_grey_realism"]) prompt = ( f"{style_keywords} of {subject}, " f"tattoo design, {color} palette, " f"designed for {placement}, " f"clean composition, high detail, " f"isolated on white background, " f"professional tattoo flash art, " f"{extra}" ) return prompt

这里的设计要点是:

  • 风格关键词放在最前面,因为模型对开头的 token 注意力更高;
  • 明确说明tattoo design,锁定输出类型;
  • 指定placement字段,让模型根据身体部位调整构图;
  • 加上isolated on white background,方便后续抠图或者直接用于预览。

3.4 负面提示词的重要性

纹身图片最容易出现的问题是:

  • 文字拼写错误(纹身内容一旦出现拼写错误,基本就废了);
  • 手指、眼睛、牙齿畸形;
  • 边缘晕染、线条断裂;
  • 背景杂乱,无法作为设计稿使用。

所以负面提示词必须包含:

text, typography, watermark, signature, blurry, distorted, deformed fingers, bad anatomy, bad hands, extra limbs, oversaturated, cluttered background, frame, border

如果你是调用 OpenAI 的 images API,部分模型不支持负面提示词,那就需要第二次审核过滤。这一点后面会讲。


4. 环境准备与项目结构

4.1 环境准备

在本地开发这套 AI 纹身设计工具,你需要准备以下环境:

  • 操作系统:Windows 10/11、macOS、Ubuntu 均可;
  • Python:建议 3.10 或 3.11,避免过老版本导致依赖解析问题;
  • 图像生成 API Key:准备一个图像生成服务的 API Key;
  • 网络:能正常访问你选择的图像生成 API 服务即可。

安装依赖的命令如下:

# 创建虚拟环境 python -m venv venv # 激活虚拟环境 # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate # 安装依赖 pip install fastapi uvicorn openai python-multipart jinja2 pillow

其中python-multipart是为了支持表单提交,jinja2用于服务端渲染页面,pillow用于图片后处理。如果你选择前后端分离,jinja2可以去掉。

4.2 项目目录结构

我建议按照下面的目录来组织项目:

tattoo-ideas/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── config.py # 配置项 │ ├── prompt_engine.py # 提示词引擎 │ ├── generator.py # 图像生成客户端 │ ├── storage.py # 存储模块 │ └── templates/ │ └── index.html # 页面模板 ├── output/ # 生成的图片保存目录 ├── tattoos.db # SQLite 数据库(自动生成) ├── requirements.txt └── README.md

这样分层的好处是:提示词逻辑、生成逻辑、存储逻辑彼此独立,后续替换图像生成服务或者修改提示词规则,都只用动对应模块。


5. 完整实战:搭建 TattooIdeas 核心服务

下面我们来实现一个最小可用版本。它支持的功能是:

  • 用户选择纹身风格;
  • 输入主体描述;
  • 选择身体部位;
  • 后端自动组装提示词;
  • 调用图像生成 API 输出 2~4 张候选图;
  • 页面展示结果并支持下载。

5.1 配置管理

app/config.py文件内容如下:

import os class Config: # 图像生成 API 配置 OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "") OPENAI_MODEL = os.getenv("OPENAI_IMAGE_MODEL", "gpt-image-1") # 服务配置 HOST = os.getenv("TATTOO_HOST", "0.0.0.0") PORT = int(os.getenv("TATTOO_PORT", "8080")) # 输出配置 OUTPUT_DIR = os.getenv("TATTOO_OUTPUT_DIR", "output") DB_PATH = os.getenv("TATTOO_DB_PATH", "tattoos.db")

这样配置的好处是:所有敏感信息都通过环境变量注入。你可以在命令行执行时临时设置,也可以写到.env文件里。注意不要硬编码 Key 到代码中,否则一旦开源就会泄露。

如果你的 API Key 直接存在 shell 环境变量里,启动命令可以这样写:

export OPENAI_API_KEY="你的密钥" uavicorn app.main:app --host 0.0.0.0 --port 8080

5.2 风格映射与提示词引擎

这是整个项目的灵魂部分。我把常见纹身风格整理成字典,并实现了提示词组装函数。

app/prompt_engine.py文件内容如下:

# 纹身风格关键词映射 TATTOO_STYLES = { "traditional": ( "traditional american tattoo style, " "bold black outlines, limited classic color palette" ), "japanese": ( "japanese irezumi tattoo style, " "flowing wind bars and waves, rich gradients, " "peony and koi motifs" ), "black_grey_realism": ( "black and grey realism tattoo style, " "soft smooth shading, high contrast, " "photorealistic elements" ), "dotwork": ( "dotwork tattoo style, stippling shading, " "fine dots, precise geometric patterns" ), "geometric": ( "geometric tattoo style, sacred geometry, " "clean lines, symmetrical composition" ), "watercolor": ( "watercolor tattoo style, " "vibrant ink splashes, soft edges, " "no harsh outline" ), "minimalist": ( "minimalist tattoo style, " "single continuous line, elegant simplicity, " "lots of negative space" ), "neo_traditional": ( "neo traditional tattoo style, " "illustrative, rich colors, " "varying line weights, ornamental details" ), } # 身体部位构图约束 PLACEMENT_HINTS = { "forearm": "vertical elongated composition following the arm shape", "upper_arm": "wrapped composition, following the deltoid curve", "back": "large centered composition, symmetrical or dynamic flow", "chest": "broad horizontal composition, centered on the pectoral area", "calf": "vertical composition, adapted to the calf muscle shape", "shoulder": "curved composition following the shoulder line", "wrist": "small compact composition, delicate and fine lines", "ankle": "small delicate composition, fitted to the ankle bone area", } # 负面提示词 NEGATIVE_PROMPT = ( "text, typography, words, letters, watermark, signature, " "blurry, low resolution, distorted, deformed fingers, " "bad anatomy, bad hands, extra limbs, extra fingers, " "oversaturated, cluttered background, photo frame, border, " "3d render, cartoon sticker style" ) def build_style_keywords(style: str) -> str: """返回风格对应的英文提示词关键词""" return TATTOO_STYLES.get(style, TATTOO_STYLES["black_grey_realism"]) def build_placement_hint(placement: str) -> str: """返回部位对应的构图约束""" return PLACEMENT_HINTS.get(placement, "well balanced composition") def build_tattoo_prompt( subject: str, style: str = "black_grey_realism", placement: str = "forearm", color_mode: str = "black and grey", extra_style: str = "", ) -> dict: """ 组装完整的提示词。 返回词典对象,包含正向提示词和负面提示词。 """ style_kw = build_style_keywords(style) placement_hint = build_placement_hint(placement) color_keywords = { "black_and_grey": "black and grey shading", "black_white": "pure black and white, bold contrast", "color": "vibrant classic tattoo colors", "mono_red": "single red ink tone", }.get(color_mode, "black and grey shading") prompt = ( f"{style_kw}, {subject}, " f"tattoo design, {color_keywords}, " f"{placement_hint}, " f"clean edges, high detail, professional tattoo flash art, " f"isolated on pure white background" ) if extra_style: prompt = prompt + ", " + extra_style return { "prompt": prompt, "negative_prompt": NEGATIVE_PROMPT, "style": style, "placement": placement, }

说明几个关键点:

  • 风格字典的 value 是按英文书写习惯拼成的描述短语,不是简单堆叠名词。模型对“带连接词的自然描述”理解更好;
  • isolated on pure white background这一句很重要,它决定了生成图是否为设计稿风格,而不是人物照片;
  • 负面提示词中把texttypography放在最前面,因为纹身图里出现文字是最不可接受的瑕疵。

5.3 图像生成客户端

app/generator.py是这个项目的适配层。它统一封装对图像生成 API 的调用,后期如果换服务商,只需要改这里。

import base64 import time import openai from app.config import Config client = None def get_client(): """懒加载 OpenAI 客户端""" global client if client is None: client = openai.OpenAI( api_key=Config.OPENAI_API_KEY, base_url=Config.OPENAI_BASE_URL or None, ) return client def generate_tattoo_images(prompt_data: dict, n: int = 3, size: str = "1024x1024"): """ 调用图像生成接口,返回图片字节列表。 推荐使用 gpt-image-1 等支持图片输出的模型。 """ prompt = prompt_data["prompt"] try: client = get_client() response = client.images.generate( model=Config.OPENAI_MODEL, prompt=prompt, n=min(n, 4), size=size, quality="high", response_format="b64_json", ) images = [] for item in response.data: if getattr(item, "b64_json", None): images.append(base64.b64decode(item.b64_json)) elif getattr(item, "url", None): # 部分服务返回 URL,需要自行下载 import requests r = requests.get(item.url, timeout=30) r.raise_for_status() images.append(r.content) return images except Exception as e: raise RuntimeError(f"图像生成失败: {e}") from e

这里要注意,不同模型对n参数的支持不同。有些模型一次只能生成一张,那就需要循环调用。如果你的服务商不支持quality参数,可以去掉。

另外一个细节:response_format="b64_json"比直接获取 URL 更稳定,减少了一次 HTTP 请求,也不容易因为外链过期导致图片失效。如果你的 API 服务不支持 b64 返回,也可以用item.url方式下载。

5.4 存储模块

存储模块负责把生成记录写入 SQLite,并把图片保存到本地目录。用 SQLite 作为起步方案足够了,后续迁移到 MySQL 或 PostgreSQL 也容易。

app/storage.py文件内容如下:

import os import sqlite3 import uuid from datetime import datetime from app.config import Config def get_conn(): """获取数据库连接""" conn = sqlite3.connect(Config.DB_PATH) conn.row_factory = sqlite3.Row return conn def init_db(): """初始化数据库表结构""" conn = get_conn() conn.execute(""" CREATE TABLE IF NOT EXISTS tattoos ( id TEXT PRIMARY KEY, prompt TEXT NOT NULL, style TEXT NOT NULL, placement TEXT NOT NULL, image_path TEXT NOT NULL, created_at TEXT NOT NULL ) """) conn.commit() conn.close() def save_image_to_disk(image_bytes: bytes, prefix: str = "tattoo") -> str: """将图片字节保存到磁盘,返回相对路径""" os.makedirs(Config.OUTPUT_DIR, exist_ok=True) file_name = f"{prefix}_{uuid.uuid4().hex[:8]}.png" file_path = os.path.join(Config.OUTPUT_DIR, file_name) with open(file_path, "wb") as f: f.write(image_bytes) return file_path def save_tattoo_record(schema: dict) -> None: """保存一条生成记录到数据库""" conn = get_conn() conn.execute( """ INSERT INTO tattoos (id, prompt, style, placement, image_path, created_at) VALUES (?, ?, ?, ?, ?, ?) """, ( schema["id"], schema["prompt"], schema["style"], schema["placement"], schema["image_path"], schema["created_at"], ), ) conn.commit() conn.close()

5.5 FastAPI 主服务

接下来是 FastAPI 主文件。它负责接收用户请求、调用提示词引擎、执行图像生成、保存结果、返回页面渲染数据。

app/main.py文件内容如下:

import base64 import uuid from datetime import datetime from fastapi import FastAPI, Form, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pathlib import Path from app.config import Config from app.prompt_engine import build_tattoo_prompt from app.generator import generate_tattoo_images from app.storage import init_db, save_image_to_disk, save_tattoo_record BASE_DIR = Path(__file__).resolve().parent app = FastAPI(title="TattooIdeas AI 纹身设计工具") app.mount("/output", StaticFiles(directory=Config.OUTPUT_DIR), name="output") templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) # 页面可选风格和部位 STYLE_OPTIONS = [ ("traditional", "传统美式"), ("japanese", "日式传统"), ("black_grey_realism", "黑灰写实"), ("dotwork", "点刺"), ("geometric", "几何线条"), ("watercolor", "水彩"), ("minimalist", "极简线条"), ("neo_traditional", "新传统"), ] PLACEMENT_OPTIONS = [ ("forearm", "小臂"), ("upper_arm", "上臂"), ("back", "背部"), ("chest", "胸部"), ("calf", "小腿"), ("shoulder", "肩部"), ("wrist", "手腕"), ("ankle", "脚踝"), ] @app.on_event("startup") def on_startup(): init_db() @app.get("/", response_class=HTMLResponse) def index(request: Request): """渲染首页表单""" return templates.TemplateResponse( request, "index.html", { "styles": STYLE_OPTIONS, "placements": PLACEMENT_OPTIONS, }, ) @app.post("/api/generate") async def api_generate( request: Request, subject: str = Form(...), style: str = Form("black_grey_realism"), placement: str = Form("forearm"), color_mode: str = Form("black_and_grey"), n: int = Form(3), ): """ 生成纹身设计图。 返回包含图片列表和提示词的信息。 """ subject = subject.strip() if not subject: return JSONResponse({"error": "请输入纹身主体描述"}, status_code=400) # 1. 组装提示词 prompt_data = build_tattoo_prompt( subject=subject, style=style, placement=placement, color_mode=color_mode, ) # 2. 调用图像生成 try: image_list = generate_tattoo_images(prompt_data, n=n) except RuntimeError as e: return JSONResponse({"error": str(e)}, status_code=500) if not image_list: return JSONResponse({"error": "生成结果为空,请重试"}, status_code=502) # 3. 保存结果 results = [] for image_bytes in image_list: image_path = save_image_to_disk(image_bytes) record_id = uuid.uuid4().hex save_tattoo_record( { "id": record_id, "prompt": prompt_data["prompt"], "style": style, "placement": placement, "image_path": image_path, "created_at": datetime.now().isoformat(), } ) results.append( { "id": record_id, "url": f"/output/{Path(image_path).name}", "prompt": prompt_data["prompt"], } ) return {"results": results, "prompt": prompt_data["prompt"]}

要注意的是@app.on_event("startup")在较新的 FastAPI 版本中标记为弃用,但短期内仍然可用。如果你正在使用 FastAPI 0.111 以上的版本,也可以改用 lifespan 方式。这里为了示例简单,保留传统写法。

5.6 前端页面

前端页面使用原生 HTML + Tailwind CDN,保持轻量。模板文件放在app/templates/index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>TattooIdeas - AI 纹身设计工具</title> <script src="https://cdn.tailwindcss.com"></script> </head> <body class="bg-gray-50 min-h-screen"> <div class="max-w-6xl mx-auto p-6"> <h1 class="text-3xl font-bold text-gray-800 mb-2">AI 纹身设计工具</h1> <p class="text-gray-500 mb-8">输入你的想法,选择风格与部位,AI 帮你生成纹身设计参考图。</p> <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <!-- 左侧表单 --> <div class="bg-white p-6 rounded-lg shadow-sm"> <form id="generateForm"> <div class="mb-4"> <label class="block text-gray-700 text-sm font-semibold mb-2">纹身主体描述</label> <input type="text" id="subject" name="subject" class="w-full border border-gray-300 rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-400" placeholder="例如:海浪与日月,狼头,玫瑰与匕首"> </div> <div class="mb-4"> <label class="block text-gray-700 text-sm font-semibold mb-2">纹身风格</label> <select id="style" name="style" class="w-full border border-gray-300 rounded-md px-4 py-2"> {% for value, label in styles %} <option value="{{ value }}">{{ label }}</option> {% endfor %} </select> </div> <div class="mb-4"> <label class="block text-gray-700 text-sm font-semibold mb-2">身体部位</label> <select id="placement" name="placement" class="w-full border border-gray-300 rounded-md px-4 py-2"> {% for value, label in placements %} <option value="{{ value }}">{{ label }}</option> {% endfor %} </select> </div> <div class="mb-4"> <label class="block text-gray-700 text-sm font-semibold mb-2">色彩模式</label> <select id="color_mode" name="color_mode" class="w-full border border-gray-300 rounded-md px-4 py-2"> <option value="black_and_grey">黑灰</option> <option value="black_white">纯黑白</option> <option value="color">彩色</option> <option value="mono_red">单色红</option> </select> </div> <div class="mb-6"> <label class="block text-gray-700 text-sm font-semibold mb-2">生成数量</label> <input type="number" id="n" name="n" value="3" min="1" max="4" class="w-full border border-gray-300 rounded-md px-4 py-2"> </div> <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-md transition"> 生成设计图 </button> </form> <div id="errorBox" class="hidden mt-4 bg-red-50 border border-red-200 text-red-700 text-sm rounded-md p-3"></div> </div> <!-- 右侧结果区 --> <div class="lg:col-span-2"> <div id="loading" class="hidden text-center py-20"> <p class="text-gray-500 text-lg">AI 正在绘制中,请稍候…</p> </div> <div id="results" class="grid grid-cols-1 sm:grid-cols-2 gap-6"></div> <div id="promptInfo" class="hidden mt-6 bg-gray-50 border border-gray-200 rounded-md p-4"> <p class="text-sm text-gray-600 font-semibold mb-1">本次使用的提示词:</p> <p id="promptText" class="text-sm text-gray-500 break-all"></p> </div> </div> </div> </div> <script> document.getElementById('generateForm').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const loading = document.getElementById('loading'); const results = document.getElementById('results'); const errorBox = document.getElementById('errorBox'); const promptInfo = document.getElementById('promptInfo'); // 清空上次结果 results.innerHTML = ''; promptInfo.classList.add('hidden'); errorBox.classList.add('hidden'); // 显示加载中 loading.classList.remove('hidden'); try { const resp = await fetch('/api/generate', { method: 'POST', body: formData }); if (!resp.ok) { const data = await resp.json(); errorBox.textContent = data.error || '请求失败'; errorBox.classList.remove('hidden'); return; } const data = await resp.json(); // 渲染结果图片 data.results.forEach(item => { const card = document.createElement('div'); card.className = 'bg-white rounded-lg overflow-hidden shadow-md'; card.innerHTML = ` <img src="${item.url}" alt="Tattoo design" class="w-full h-80 object-cover" loading="lazy"> <a href="${item.url}" download class="block text-center text-sm text-indigo-600 hover:text-indigo-800 py-2 font-medium"> ↓ 下载图片 </a> `; results.appendChild(card); }); // 显示提示词 document.getElementById('promptText').textContent = data.prompt; promptInfo.classList.remove('hidden'); } catch (err) { errorBox.textContent = '网络或服务异常: ' + err.message; errorBox.classList.remove('hidden'); } finally { loading.classList.add('hidden'); } }); </script> </body> </html>

前端页面不需要很复杂,核心就是把用户的表单数据 POST 到后端接口,拿到图片路径后渲染出来。如果你后续要做真正的产品,可以引入 Vue 或 React,但起步阶段原生 JS 足够。

5.7 启动与验证

在项目根目录创建requirements.txt

fastapi uvicorn openai python-multipart jinja2 pillow requests

然后执行安装与启动:

pip install -r requirements.txt # 设置 API Key(按需设置) export OPENAI_API_KEY="你的密钥" # 启动服务 uvicorn app.main:app --host 0.0.0.0 --port 8080

打开浏览器访问http://localhost:8080,输入“海浪与日月”,选择“日式传统”,部位选择“小臂”,点击生成。

预期结果:

  • 页面先显示加载状态;
  • 几秒到几十秒后(取决于生成服务速度),右侧出现 3 张纹身设计图;
  • 下方展示本次组装后的完整提示词;
  • output目录中会保存原始图片;
  • tattoos.db中会写入对应的生成记录。

6. 常见问题与排查思路

AI 图像生成服务的 debug 方式和传统后端不太一样,很多问题要靠“拆层级”来定位。我把常见问题整理成一张表:

问题现象常见原因解决思路
接口返回 401API Key 错误或未设置检查环境变量OPENAI_API_KEY是否正确
接口返回 429触发速率限制或配额不足降低并发,增加指数退避重试;检查账号额度
长时间无响应网络问题或模型排队先取消,再测试一行代码能否连通 API
生成的图有文字乱码提示词没加负面词或模型能力限制确保负面提示词含text, words;考虑后过滤
生成的图不像纹身,像插画提示词缺少tattoo design约束检查提示词模板,确保风格关键词在最前
每次生成风格差异很大随机性高,未固定 seed部分模型支持seed参数,可固定后挑图
输出图片大量重复提示词缺少多样性约束在提示词末尾追加随机风格词或调整 temperature
b64 解析失败服务商返回格式不同打印 response 结构,检查是否有data.urlb64_json

6.1 典型问题一:图像生成服务返回 400

400 错误通常是请求参数不合法。我遇到过的情况包括:

  • 某个模型不支持size=1024x1024之外的尺寸;
  • 某个模型不允许n>1
  • quality参数在这个模型上不支持。

排查步骤:

  1. 打印完整的请求参数;
  2. 对照你所用模型的官方文档逐个核对;
  3. n改成 1,把quality删掉,再用最小请求测试。

6.2 典型问题二:生成的纹身带文字

纹身图带文字是最影响观感的问题。原因是模型在生成时倾向于在图案中“嵌入”装饰性文字,尤其是传统美式风格,经常出现 banner 或 ribbon 元素。

解决方案:

  1. 负面提示词中强化text, banner, ribbon, letters
  2. 在正向提示词中明确no text, no lettering
  3. 如果还不行,可以在后处理阶段用 OCR 工具检测文字区域并标记不合格图片;
  4. 也可以在生成任务中多出几张图,让用户手动挑选。

6.3 典型问题三:提示词报错但业务代码没有捕获

图像生成接口的报错信息往往很笼统。建议在开发阶段把prompt打印出来,人工确认提示词是否符合预期。可以写一个简单的单元测试:

def test_build_tattoo_prompt(): result = build_tattoo_prompt( subject="wolf head", style="black_grey_realism", placement="upper_arm", color_mode="black_and_grey", ) print(result["prompt"]) assert "tattoo design" in result["prompt"] assert "upper_arm" in result["prompt"] or "deltoid" in result["prompt"]

这类测试的价值在于:当你改模板时,能快速发现拼写错误和变量丢失。


7. 最佳实践与工程建议

7.1 提示词模板化,而不是硬编码

不要把提示词散落在业务代码里。建议把所有风格关键词、部位约束、色彩关键词放到独立的配置文件或数据库中。这样产品运营可以在不改代码的情况下调整风格库。

7.2 生成结果必须经过质量过滤

图像生成模型不是每次都能产出合格结果。我建议在后端加一个简单的图像质量检查:

  • 文件大小不能小于某个阈值(通常低于 200KB 的 PNG 可能内容过于简单);
  • 图片尺寸必须符合预期;
  • 可以接入 OCR 检测文字缺陷;
  • 可以接入 NSFW 检测服务做合规检查。

生产环境一定要做合规审查,这是内容安全底线。

7.3 控制成本和并发

图像生成 API 是按张计费的,成本远高于文本接口。工程上的建议是:

  • 设置用户级速率限制,比如每个用户每分钟最多生成 6 张;
  • 对同一主题的重复请求做缓存,如果用户在一段时间内重复提交相同参数,直接返回历史结果;
  • 提供“预览模式”(更低分辨率、更少张数),让用户确认方向后再生成高清大图。

7.4 设计规划的附加价值

TattooIdeas 的亮点在于“Planning”而不只是“Design”。除了生成图片,你还可以扩展以下功能:

  • 部位匹配推荐:通过知识库告诉用户哪个图案适合哪个身体区域;
  • 图案放大/缩小预览:在用户上传的皮肤照片上叠加生成图,模拟纹身后的效果;
  • 风格对比:同一主题一次生成多种风格,让用户横向对比;
  • 纹身师对接:把 AI 生成图作为沟通素材,连接线下纹身师。

这些能力不需要你训练模型,只需要做好领域知识沉淀和产品交互设计。

7.5 数据隐私与合规

纹身设计涉及用户的身体照片和个人偏好,属于敏感数据。工程上必须注意:

  • 用户上传的皮肤照片只做临时处理,处理完成后立即删除;
  • 数据库中的 Prompt 记录脱敏,不存储可选的脸部信息;
  • 生成图片的存储权限要严格控制,配置防盗链;
  • 如果面向儿童用户,还要考虑内容分级问题。

7.6 异步化改造

当前示例是同步请求。如果生成服务响应时间超过 30 秒,浏览器很容易超时。生产级方案建议:

  • 后端接收请求后立即返回task_id
  • 后台使用 Celery 或 RQ 执行生成任务;
  • 前端轮询任务状态接口,等任务完成后展示图片。

这种异步架构虽然复杂度提高,但用户体验和系统稳定性都会好很多。


8. 进一步优化方向

到这里,一个可运行的 AI 纹身设计工具已经完成。我觉得下一步可以按照下面的优先级继续迭代:

第一优先级:提升生成质量

  • 增加质量过滤模块,自动剔除明显有瑕疵的生成图;
  • 引入“高清重绘”流程,对选中的图片做 upscale 和锐化;
  • 针对不同风格做提示词的 A/B 测试,建立风格效果评分表。

第二优先级:增强规划能力

  • 做一个“纹身风格测试”,用户回答几个问题后推荐合适的风格;
  • 加入纹身尺寸计算器,根据部位图片估算图案真实尺寸;
  • 建立纹身知识库,支持“手腕适合什么风格”“彩色图案褪色风险”等百科式查询。

第三优先级:打造社区闭环

  • 用户生成的图案可以公开分享和点赞;
  • 纹身师可以入驻,查看用户的 AI 设计稿并报价;
  • 付费模式可以按“生成点数”售卖,用户充值时获得更多生成次数。

最后想多说一句:AI 纹身设计这类垂直应用,本质上不是在“取代纹身师”,而是在降低用户从“想法”到“草图”之间的摩擦。真正有价值的产品,不是让你一键得到一个最终纹身,而是帮你把一个模糊的念头,慢慢打磨成一张能让纹身师理解、能让你自己确认的设计图。这个思路,也适用于所有 AI 生成类工具的设计——模型解决的是“生成”,而产品要解决的是“决策”

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询