Files
menchuang/src/huamenchuang/services/ai_prompt.py
T

192 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""AI 炫图提示词构建器:从 drawing_data 提取窗型结构特征,生成专业英文生图提示词。
设计原则(生图模型工程实践):
- 主体先行: 窗户的几何/材质/分格描述置前,场景氛围居后
- 结构量化: 宽高 mm、分格数、扇数、开向全部显式给出,抑制模型自由发挥
- 负面约束: 明确"保持窗型几何不变"(edits 模式关键)
- 光影专项: 时间(日/黄昏/夜)与室内外视角分别给出摄影级光照词
"""
from huamenchuang.core.drawing import parse_drawing, replay_shape
# 风格 → 型材材质/颜色描述
STYLE_FINISH = {
"modern": ("matte anthracite grey aluminum frames, minimalist slim profiles", "modern minimalist"),
"chinese": ("dark bronze aluminum frames with wood-grain inner finish and subtle Chinese lattice grille", "new-Chinese style"),
"european": ("cream white aluminum frames with classic decorative architrave", "classic European"),
"nordic": ("white aluminum frames with light oak interior foil", "Scandinavian Nordic"),
}
# 时间 → 摄影光照
TIME_LIGHT = {
"day": "bright midday daylight, soft natural shadows, clear sky",
"dusk": "warm golden-hour sunset light, long soft shadows, amber sky",
"night": "night scene, warm interior glow shining through the glass, cool blue ambience",
}
# 场景 → 场景描述(照片式 7 场景对齐源站 renderV2)
SCENE_DESC = {
"render": "the window installed on a clean rendered exterior wall, straight-on elevation view",
"install": "the brand-new window just mounted into the wall opening, installation foam and fixing screws still visible at the edges, home renovation context",
"erase": "renovation scene: the old worn window has been removed from the masonry opening and this new window is being fitted in its place, dusty sill",
"indoor": "interior view, the window as the focal point of a tastefully furnished {style_word} living room with sofa, wooden floor, curtains and green plants",
"villa": "exterior view of a {style_word} villa facade featuring this large window, landscaped garden with lawn below, blue sky",
"fence": "view from inside looking out through the window, clear unobstructed garden and distant hills beyond the glass, no railings or bars outside",
"blank": "unfurnished rough-condition room with bare grey cement walls, the window as the only fitted element, pre-renovation state",
}
# 快速模式场景(无照片)
QUICK_SCENE = {
"indoor": "interior view in a tastefully furnished {style_word} room, the window as the light source",
"outdoor": "exterior facade view on a residential building, mild urban greenery around",
"studio": "studio product shot on a seamless neutral background, softbox lighting, slight floor reflection",
}
# 阳光房场景(对齐目标站 sunnyhouse 平顶/人字顶)
SUNROOM_DESC = {
"sunroom-flat": "a flat-roof glass sunroom attached to the house, slim {frame_word} aluminum posts and glass beams, clear tempered glass roof panels, furnished with rattan chairs and plants, garden view",
"sunroom-gable": "a gable (A-frame pitched) glass sunroom attached to the house, ridge beam with fan-shaped front glass, {frame_word} aluminum structure, clear glass roof, garden view",
}
def _sash_kind(sub: dict) -> str | None:
t = str(sub.get("type", ""))
is_door = bool(sub.get("isDoor"))
if "Screen" in t:
return None # 纱扇不参与文字描述
if "Kfc" in t:
return "tilt-and-turn " + ("door leaf" if is_door else "sash")
if "DoubleSash" in t:
return "pair of French " + ("door leaves" if is_door else "casement sashes opening from the center")
if "Slide" in t:
return "sliding " + ("door panel" if is_door else "sash")
if t in ("Sash",):
return "side-hung casement " + ("door leaf" if is_door else "sash")
return None
def _opening_words(sub: dict) -> str:
toward = str(sub.get("openToward") or "")
od = sub.get("od")
if "up" in str(sub.get("openDirection") or "").lower() or (isinstance(od, (int, float)) and 60 <= od <= 180 and "up" in str(sub.get("openDirection", "")).lower()):
return "top-hung"
return "inward-opening" if toward == "inward" else "outward-opening"
def describe_window(drawing: str | dict) -> dict:
"""从 drawing_data 提取窗型结构特征(供提示词与前端展示)。"""
dd = parse_drawing(drawing)
shapes = [s for s in dd.get("sm", []) if isinstance(s, dict)]
if not shapes:
return {"count": 0, "summary": "", "width_mm": 0, "height_mm": 0}
xs, ys = [], []
parts: list[str] = []
sash_total = 0
has_arc = has_screen = has_deco = False
for shape in shapes:
regions = replay_shape(shape)
pts = [[(seg["ps"]["x"], seg["ps"]["y"]) for seg in r] for r in regions]
if not pts:
continue
flat = [p for r in pts for p in r]
x0, y0 = min(p[0] for p in flat), min(p[1] for p in flat)
x1, y1 = max(p[0] for p in flat), max(p[1] for p in flat)
xs += [x0, x1]; ys += [y0, y1]
w, h = round(x1 - x0), round(y1 - y0)
n_region = len(regions)
# 扇描述
sashes = [sub for sub in (shape.get("sm") or []) if isinstance(sub, dict)]
kind_words: list[str] = []
for sub in sashes:
k = _sash_kind(sub)
if k is None:
if "Screen" in str(sub.get("type", "")):
has_screen = True
continue
sash_total += 1
kind_words.append(f"{_opening_words(sub)} {k}")
if shape.get("rt"):
has_arc = True
if shape.get("decs"):
has_deco = True
part = f"{w}×{h}mm unit with {n_region} glass panes"
if kind_words:
part += ", " + " and ".join(dict.fromkeys(kind_words))
parts.append(part)
if not xs:
return {"count": 0, "summary": "", "width_mm": 0, "height_mm": 0}
width_mm, height_mm = round(max(xs) - min(xs)), round(max(ys) - min(ys))
multi = len(parts) > 1
summary = "; ".join(parts)
if multi:
summary = f"a combination of {len(parts)} window units joined together: " + summary
else:
summary = parts[0]
if has_arc:
summary += ", arched/curved top (heritage style)"
if has_screen:
summary += ", with retractable fly screen"
if has_deco:
summary += ", with decorative gliding bars (grilles) between the glass"
return {"count": len(parts), "summary": summary, "width_mm": width_mm,
"height_mm": height_mm, "sashes": sash_total}
def build_prompt(drawing: str | dict, style: str = "modern", time: str = "day",
scene: str = "render", ratio: str = "landscape",
rect_hint: str = "") -> str:
"""构建优化后的完整英文提示词。
ratio: portrait/landscape/square → 依据窗型宽高比的构图引导。
rect_hint: 照片模式下洞口位置描述(如 "centered slightly left of middle")。
"""
info = describe_window(drawing)
dd = parse_drawing(drawing)
finish, style_word = STYLE_FINISH.get(style, STYLE_FINISH["modern"])
light = TIME_LIGHT.get(time, TIME_LIGHT["day"])
# 阳光房场景: 窗型描述让位于结构描述
frame_word = "anthracite" if style == "modern" else ("bronze" if style == "chinese" else "white")
if scene in SUNROOM_DESC:
scene_text = SUNROOM_DESC[scene].replace("{frame_word}", frame_word)
sr = dd.get("_sunroom") or {}
fw = sr.get("width_mm") or info["width_mm"] or 4000
fd = sr.get("depth_mm") or 3000
fh = sr.get("height_mm") or 2800
bays = max(2, round(fw / (sr.get("bay_mm") or 1000)))
window_desc = (f"Structure: {scene_text}; footprint {fw}×{fd}mm, eave height {fh}mm, "
f"{bays} bays with slim aluminum posts")
else:
scene_tpl = SCENE_DESC.get(scene) or QUICK_SCENE.get(scene) or SCENE_DESC["render"]
scene_text = scene_tpl.replace("{style_word}", style_word)
window_desc = info["summary"] or "a modern aluminum window"
composition = {
"portrait": "vertical composition, window fills most of the frame height",
"landscape": "horizontal composition, generous wall context around the window",
"square": "balanced square composition, window centered",
}.get(ratio, "")
prompt = (
f"Photorealistic architectural photograph of {scene_text}. "
f"The window: {window_desc}; {finish}. "
f"Lighting: {light}. {composition} "
+ (f"{rect_hint} " if rect_hint else "")
+ "Keep the window geometry, pane division and frame profiles exactly as described — do not add or remove sashes. "
"Realistic glass with soft sky reflection, physically correct shadows and contact shadows, "
"true-to-material aluminum texture, high dynamic range, ultra-detailed, 8k, professional architectural photography, no people, no text, no watermark."
)
return prompt
def pick_size(width_mm: int, height_mm: int) -> str:
"""按窗型宽高比选择生成尺寸(gpt-image 支持 1024x1024 / 1536x1024 / 1024x1536)。"""
if not width_mm or not height_mm:
return "1024x1024"
r = width_mm / height_mm
if r > 1.2:
return "1536x1024"
if r < 0.83:
return "1024x1536"
return "1024x1024"