feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。
**目前能用的(真环境实测,非推断)**:
profile/other : 200, 7075 字节 —— 博主主页指标(粉丝/获赞/作品数/昵称)
aweme/detail : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)
**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
挑中的恰好是「批量拉作品列表」这个最敏感的动作。
2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
代价,过几小时要重测)。
所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。
**排查中控住变量后得到的两条事实**(都写进注释了):
· `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
(我先前猜的 sec-ch-ua 不是关键。)
· 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
从 CDP 取齐 cookie + UA + hints,而不是各自写死。
「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。
测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
scene_generator/generator.py — 场景图生成(单张 / 多视角套装)
|
||||
|
||||
复用 `character_sheet._generate_kling`(可灵图片生成,已跑通),
|
||||
通过 `raw_prompt` 直接传成品提示词(不走角色模板)。
|
||||
|
||||
套装流程与角色套装同构:
|
||||
① 广角全景(主图)→ 文生图 T2I
|
||||
② 其余 4 视角 → **以主图为参考图** 图生图 I2I(保证是同一个空间)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(_SCRIPT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_ROOT))
|
||||
|
||||
from lib.logger import get_logger # noqa: E402
|
||||
from lib.config import load_config # noqa: E402
|
||||
from scene_generator.prompt_assembler import ( # noqa: E402
|
||||
SCENE_VIEWS, build_scene_prompt, build_scene_view_prompt, build_scene_set)
|
||||
|
||||
log = get_logger("scene_generator.generator")
|
||||
|
||||
# 场景图默认输出目录(与角色缓存同级)
|
||||
try:
|
||||
from character_sheet import CACHE_DIR as _CHAR_CACHE
|
||||
SCENE_CACHE_DIR = _CHAR_CACHE.parent / "scene_sheet"
|
||||
except Exception:
|
||||
SCENE_CACHE_DIR = Path(os.environ.get("AGENT_LOCAL", ".")) / "tools/ave/cache/scene_sheet"
|
||||
|
||||
SCENE_ASPECT = "9:16" # 竖屏(与短剧成片一致)
|
||||
|
||||
|
||||
def generate_scene_image(prompt: str, dest: str, ref_image: str = "",
|
||||
seed=None) -> dict:
|
||||
"""生成**单张**场景图(有参考图则走图生图)
|
||||
|
||||
返回 {ok, path, error?}
|
||||
"""
|
||||
from character_sheet import _generate_kling
|
||||
try:
|
||||
r = _generate_kling(
|
||||
desc="", output_path=dest, lang="zh", force=True,
|
||||
mode="scene", ref_image=ref_image, seed=seed,
|
||||
raw_prompt=prompt, # ★ 直接用成品提示词
|
||||
)
|
||||
path = r.get("grid") or dest
|
||||
return {"ok": bool(path and Path(path).exists()), "path": path, "raw": r}
|
||||
except Exception as e:
|
||||
log.error("场景图生成失败: %s", str(e)[:200])
|
||||
return {"ok": False, "path": "", "error": str(e)[:300]}
|
||||
|
||||
|
||||
def build_scene_set_images(layers: dict, scene_name: str = "", out_dir: str = "",
|
||||
anchor_image: str = "", only: tuple = (),
|
||||
force: bool = False, extra: str = "",
|
||||
prompts: dict = None,
|
||||
progress_cb=None) -> dict:
|
||||
"""生成场景**多视角套装**(5 张)
|
||||
|
||||
★ 与角色套装同构:
|
||||
① 广角全景(主图)→ T2I
|
||||
② 其余视角 → **以主图为参考图** I2I(确保是同一个空间)
|
||||
|
||||
· 断点续跑:已存在文件默认跳过(force=True 强制重生成)
|
||||
· 每张生成后自动质检(背景/构图)
|
||||
· 成本约 ¥0.2/张 × 5 = ¥1/场景
|
||||
|
||||
返回 {anchor, views, done, skipped, cost_estimate, note, saved_to?}
|
||||
"""
|
||||
_cb = progress_cb or (lambda p, n: None)
|
||||
base = Path(out_dir) if out_dir else (SCENE_CACHE_DIR / (scene_name or "scene_set"))
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
targets = [v for v in sorted(SCENE_VIEWS, key=lambda k: SCENE_VIEWS[k]["order"])
|
||||
if (not only or v in only)]
|
||||
out: dict = {"anchor": anchor_image or "", "views": {}, "done": 0, "skipped": 0,
|
||||
"cost_estimate": round(len(targets) * 0.2, 2)}
|
||||
anchor_path = anchor_image
|
||||
|
||||
for idx, vid in enumerate(targets, 1):
|
||||
v = SCENE_VIEWS[vid]
|
||||
dst = base / f"{vid}.png"
|
||||
_cb(int((idx - 1) / max(1, len(targets)) * 100), f"{v['label']}")
|
||||
|
||||
if dst.exists() and not force and dst.stat().st_size > 10_000:
|
||||
log.info(" ♻️ 跳过已存在:%s", dst.name)
|
||||
out["views"][vid] = {"path": str(dst), "ok": True, "skipped": True,
|
||||
"is_anchor": v["is_anchor"]}
|
||||
out["skipped"] += 1
|
||||
if v["is_anchor"] and not anchor_path:
|
||||
anchor_path = str(dst)
|
||||
continue
|
||||
|
||||
# 主图:文生图(或复用指定图)
|
||||
if v["is_anchor"]:
|
||||
if anchor_path and Path(str(anchor_path)).exists():
|
||||
import shutil
|
||||
shutil.copy2(str(anchor_path), dst)
|
||||
out["views"][vid] = {"path": str(dst), "ok": True, "reused": True,
|
||||
"is_anchor": True}
|
||||
out["anchor"] = str(dst)
|
||||
log.info(" ♻️ 主图复用:%s", Path(str(anchor_path)).name)
|
||||
continue
|
||||
ref = ""
|
||||
else:
|
||||
# 其余视角:以主图为参考图(核心:保证同一空间)
|
||||
ref = anchor_path or (str(base / "wide.png") if (base / "wide.png").exists() else "")
|
||||
if not ref:
|
||||
log.warning(" ⚠️ 无主图参考,%s 将走文生图(空间可能不一致)", v["label"])
|
||||
|
||||
# ★ 优先用调用方给的成品提示词(用户自带提示词 / 预设模式)
|
||||
if prompts and str(prompts.get(vid) or "").strip():
|
||||
prompt = str(prompts[vid]).strip()
|
||||
else:
|
||||
prompt = build_scene_view_prompt(vid, layers, extra=extra)
|
||||
log.info(" 🏠 [%d/%d] %s(%s)", idx, len(targets), v["label"],
|
||||
"图生图·锚定主图" if ref else "文生图")
|
||||
r = generate_scene_image(prompt, str(dst), ref_image=ref)
|
||||
ok = bool(r.get("ok"))
|
||||
q = None
|
||||
if ok:
|
||||
try:
|
||||
from character_generator.quality_check import check_reference_quality
|
||||
q = check_reference_quality(r["path"]).get("edge_std")
|
||||
except Exception:
|
||||
pass
|
||||
out["views"][vid] = {"path": r.get("path", ""), "ok": ok, "quality": q,
|
||||
"is_anchor": v["is_anchor"], "used_ref": bool(ref),
|
||||
"error": r.get("error", "")}
|
||||
if ok:
|
||||
out["done"] += 1
|
||||
if v["is_anchor"]:
|
||||
anchor_path = r["path"]
|
||||
out["anchor"] = r["path"]
|
||||
time.sleep(0.5)
|
||||
|
||||
_cb(100, "完成")
|
||||
okn = sum(1 for x in out["views"].values() if x.get("ok"))
|
||||
out["note"] = f"场景套装完成 {okn}/{len(targets)} 张(约 ¥{out['cost_estimate']})"
|
||||
log.info(" 🏠 %s", out["note"])
|
||||
|
||||
# 自动入库(场景资产)
|
||||
if scene_name:
|
||||
try:
|
||||
out["saved_to"] = _save_scene_to_assets(scene_name, out, base)
|
||||
except Exception as e:
|
||||
log.warning(" ⚠️ 场景入库失败(文件仍在 cache): %s", str(e)[:150])
|
||||
out["save_error"] = str(e)[:200]
|
||||
return out
|
||||
|
||||
|
||||
def _save_scene_to_assets(scene_name: str, out: dict, base: Path) -> dict:
|
||||
"""把场景套装写入「场景」资产(按名查找/创建 + 复制 5 张 + 更新 reference_images)"""
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from assets_manager import AssetStore
|
||||
|
||||
st = AssetStore("locations")
|
||||
it = st.get(scene_name)
|
||||
if not it:
|
||||
for x in st.list():
|
||||
if (x.get("name") or "").strip() == scene_name.strip():
|
||||
it = x
|
||||
break
|
||||
aid = (it or {}).get("id") or scene_name
|
||||
if not it:
|
||||
st.create({"id": aid, "name": scene_name,
|
||||
"description": "由「🏠 场景生成器」创建", "tags": ["场景", "套装"]})
|
||||
log.info(" 🆕 已创建场景: %s", scene_name)
|
||||
|
||||
target = Path(st.kind_dir(aid))
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
refs = dict((it or {}).get("reference_images") or {})
|
||||
added = []
|
||||
for vid, v in (out.get("views") or {}).items():
|
||||
src = Path(v.get("path") or "")
|
||||
if not src.exists() or not v.get("ok"):
|
||||
continue
|
||||
dst = target / f"ref_{vid}.png"
|
||||
shutil.copy2(src, dst)
|
||||
refs[vid] = dst.name
|
||||
added.append(f"{vid}={dst.name}")
|
||||
if added:
|
||||
st.update(aid, {"reference_images": refs,
|
||||
"generation_history": list((it or {}).get("generation_history") or []) + [{
|
||||
"at": datetime.now().isoformat(timespec="seconds"),
|
||||
"kind": "scene_set", "files": added}]})
|
||||
log.info(" 💾 已入库场景「%s」:%d 张", scene_name, len(added))
|
||||
return {"aid": aid, "added": added}
|
||||
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
scene_generator/prompt_assembler.py — 场景(拍摄地)结构化提示词系统
|
||||
|
||||
设计参照 `character_generator/prompt_assembler.py`(角色 10 层 47 字段),
|
||||
为**场景**建一套同样粒度的分层字段:**10 层 51 字段**。
|
||||
|
||||
## 为什么需要
|
||||
原实现只有"传一张图",无法描述"这是个什么样的空间" ——
|
||||
分镜时模型不知道窗在哪、门朝哪、床怎么摆 → 换机位空间就乱。
|
||||
结构化字段让场景可复用、可检索、可交给 AI 扩写。
|
||||
|
||||
## 10 层
|
||||
L0 场景身份 空间类型 / 室内外 / 尺度 / 地域
|
||||
L1 空间格局 面积 / 层高 / 格局 / 进深 / 特殊结构
|
||||
L2 建筑风格 年代 / 风格 / 新旧 / 材质倾向 / 地域特征
|
||||
L3 表面材质 墙面 / 地面 / 顶面 / 固定设施 / 磨损
|
||||
L4 家具陈设 主家具 / 次家具 / 摆放 / 数量 / 年代感 / 功能区
|
||||
L5 细节道具 桌面 / 墙面装饰 / 地面 / 生活痕迹 / 电子设备
|
||||
L6 光源 主光 / 色温 / 方向 / 强度 / 辅助光 / 实用光源
|
||||
L7 色彩氛围 调性 / 主色 / 辅色 / 饱和度 / 氛围
|
||||
L8 时间天气 时间 / 天气 / 季节 / 窗外可见 / 空气感
|
||||
L9 镜头视角 常用机位 / 空间感 / 尺度参照 / 禁忌
|
||||
|
||||
## 多视角(5 张 · 与角色套装同一套机制)
|
||||
wide(广角全景·主图)· front(正面)· side(侧面)· top(俯视/顶图)· detail(细节)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 分层字段定义
|
||||
# ════════════════════════════════════════════════════════════
|
||||
SCENE_LAYERS: dict = {
|
||||
"S0_identity": {
|
||||
"name": "场景身份",
|
||||
"icon": "🏠",
|
||||
"fields": [
|
||||
{"key": "name", "label": "场景名", "type": "text", "placeholder": "如:市一院住院部 302 病房"},
|
||||
{"key": "space_type", "label": "空间类型", "type": "select", "options": [
|
||||
"病房", "诊室", "手术室外", "医院走廊", "客厅", "卧室", "厨房", "卫生间",
|
||||
"办公室", "会议室", "工位区", "教室", "图书馆", "餐厅", "咖啡馆", "酒吧",
|
||||
"便利店", "商场", "地铁车厢", "地铁站", "公交车", "出租车内", "私家车内",
|
||||
"街道", "小巷", "天桥", "地下通道", "公园", "河边", "天台", "楼梯间",
|
||||
"电梯", "车库", "仓库", "工厂车间", "工地", "老式居民楼", "出租屋",
|
||||
]},
|
||||
{"key": "indoor_outdoor", "label": "室内 / 室外", "type": "select",
|
||||
"options": ["室内", "室外", "半开放(阳台/走廊/门洞)", "车内"]},
|
||||
{"key": "scale", "label": "空间尺度", "type": "select",
|
||||
"options": ["狭小(<10㎡)", "小(10-30㎡)", "中等(30-100㎡)", "大(100㎡+)", "开阔无边界"]},
|
||||
{"key": "region", "label": "地域风格", "type": "select",
|
||||
"options": ["中国", "东亚", "东南亚", "南亚", "中东", "欧美", "拉美", "非洲", "架空/无地域"]},
|
||||
],
|
||||
},
|
||||
"S1_layout": {
|
||||
"name": "空间格局",
|
||||
"icon": "📐",
|
||||
"fields": [
|
||||
{"key": "area", "label": "面积感", "type": "select",
|
||||
"options": ["极紧凑", "紧凑", "适中", "宽敞", "空旷"]},
|
||||
{"key": "ceiling", "label": "层高", "type": "select",
|
||||
"options": ["低矮压抑(<2.4m)", "标准(2.6-2.8m)", "挑高(3.5m+)", "loft/通高"]},
|
||||
{"key": "layout", "label": "格局", "type": "select",
|
||||
"options": ["单间开敞", "一室一厅", "长条形走廊", "L 形", "回字形", "开放大厅", "隔断分区"]},
|
||||
{"key": "depth", "label": "进深 / 纵深感", "type": "select",
|
||||
"options": ["浅(一望到底)", "中等", "深(有纵深)", "极深(望不到头)"]},
|
||||
{"key": "structure", "label": "特殊结构", "type": "select",
|
||||
"options": ["无", "承重柱", "楼梯", "拱门", "横梁", "玻璃隔断", "推拉门", "天窗"]},
|
||||
],
|
||||
},
|
||||
"S2_architecture": {
|
||||
"name": "建筑风格",
|
||||
"icon": "🏛️",
|
||||
"fields": [
|
||||
{"key": "era", "label": "年代", "type": "select",
|
||||
"options": ["当代(2020s)", "10年前", "2000 年代", "90 年代", "80 年代", "民国", "古代", "未来"]},
|
||||
{"key": "style", "label": "装修风格", "type": "select",
|
||||
"options": ["现代简约", "医疗专业", "工业风", "中式", "日式", "欧式", "北欧",
|
||||
"复古怀旧", "毛坯/未装修", "混搭"]},
|
||||
{"key": "condition", "label": "新旧程度", "type": "select",
|
||||
"options": ["崭新", "整洁", "正常使用", "老旧", "破败斑驳"]},
|
||||
{"key": "material_tone", "label": "材质基调", "type": "select",
|
||||
"options": ["木质为主", "水泥裸露", "瓷砖/石材", "金属工业", "玻璃通透", "混合材质"]},
|
||||
{"key": "region_style", "label": "地域特征", "type": "select",
|
||||
"options": ["无明显地域", "北方(暖气/厚墙)", "南方(潮湿/开放阳台)", "沿海", "内陆", "海外"]},
|
||||
],
|
||||
},
|
||||
"S3_surfaces": {
|
||||
"name": "表面材质",
|
||||
"icon": "🧱",
|
||||
"fields": [
|
||||
{"key": "wall", "label": "墙面", "type": "select",
|
||||
"options": ["乳白涂料墙", "浅灰墙", "瓷砖墙", "水泥墙", "木饰面", "墙纸",
|
||||
"半墙瓷砖+上白墙", "玻璃幕墙"]},
|
||||
{"key": "floor", "label": "地面", "type": "select",
|
||||
"options": ["抛光瓷砖", "哑光瓷砖", "木地板", "复合地板", "水泥地", "地毯",
|
||||
"PVC 地胶(医院)", "环氧地坪", "沥青路面"]},
|
||||
{"key": "ceiling_surface", "label": "顶面", "type": "select",
|
||||
"options": ["平顶白墙", "吊顶+灯槽", "裸露管线", "木梁", "格栅吊顶", "无顶(室外)"]},
|
||||
{"key": "fixtures", "label": "固定设施", "type": "select",
|
||||
"options": ["无", "洗手台", "管道外露", "空调出风口", "消防栓", "配电箱", "插座面板"]},
|
||||
{"key": "wear", "label": "磨损程度", "type": "select",
|
||||
"options": ["全新", "正常使用痕迹", "明显老旧", "污渍划痕明显", "破损"]},
|
||||
],
|
||||
},
|
||||
"S4_furniture": {
|
||||
"name": "家具陈设",
|
||||
"icon": "🛏️",
|
||||
"fields": [
|
||||
{"key": "main_furniture", "label": "主家具", "type": "text",
|
||||
"placeholder": "如:可升降病床 / 办公桌 / 三人沙发"},
|
||||
{"key": "secondary", "label": "次要家具", "type": "text",
|
||||
"placeholder": "如:床头柜、陪护椅 / 文件柜、转椅"},
|
||||
{"key": "arrangement", "label": "摆放方式", "type": "select",
|
||||
"options": ["规整对齐", "靠墙排列", "居中摆放", "围合成区", "随意散置"]},
|
||||
{"key": "quantity", "label": "数量感", "type": "select",
|
||||
"options": ["极少(空)", "适中", "较多", "密集拥挤"]},
|
||||
{"key": "era_match", "label": "家具年代感", "type": "select",
|
||||
"options": ["现代", "老式(10-20年前)", "复古(30年前+)"]},
|
||||
{"key": "function_zone", "label": "功能分区", "type": "select",
|
||||
"options": ["单一功能", "多功能混合", "无明显分区", "有明显的区域划分"]},
|
||||
],
|
||||
},
|
||||
"S5_props": {
|
||||
"name": "细节道具",
|
||||
"icon": "🧴",
|
||||
"fields": [
|
||||
{"key": "tabletop", "label": "桌面/台面物件", "type": "text",
|
||||
"placeholder": "如:水杯、病历夹、监护仪、纸巾"},
|
||||
{"key": "wall_decor", "label": "墙面装饰", "type": "select",
|
||||
"options": ["无", "挂钟", "照片/画", "白板/公告", "海报", "日历", "锦旗"]},
|
||||
{"key": "floor_items", "label": "地面物件", "type": "select",
|
||||
"options": ["无", "垃圾桶", "纸箱/杂物", "电线/输液管", "水渍", "地毯边角"]},
|
||||
{"key": "life_traces", "label": "生活痕迹", "type": "select",
|
||||
"options": ["干净无菌", "整洁有序", "有人生活的痕迹", "凌乱", "荒废感"]},
|
||||
{"key": "devices", "label": "电子设备", "type": "select",
|
||||
"options": ["无", "监护仪/医疗设备", "电脑/显示器", "打印机", "电视", "老旧风扇"]},
|
||||
],
|
||||
},
|
||||
"S6_lighting": {
|
||||
"name": "光源",
|
||||
"icon": "💡",
|
||||
"fields": [
|
||||
{"key": "main_light", "label": "主光源", "type": "select",
|
||||
"options": ["窗光(自然光)", "顶部日光灯", "暖色吸顶灯", "台灯/落地灯",
|
||||
"霓虹/招牌光", "无明确主光", "混合多光源"]},
|
||||
{"key": "color_temp", "label": "色温", "type": "select",
|
||||
"options": ["暖黄(2700K)", "中性(4000K)", "冷白(6000K)", "冷暖混合"]},
|
||||
{"key": "light_dir", "label": "来光方向", "type": "select",
|
||||
"options": ["顶光", "侧光(左)", "侧光(右)", "逆光", "正面平光", "漫射无方向"]},
|
||||
{"key": "intensity", "label": "亮度", "type": "select",
|
||||
"options": ["明亮通透", "适中", "偏暗", "昏暗", "极暗(仅局部亮)"]},
|
||||
{"key": "fill_light", "label": "辅助光", "type": "select",
|
||||
"options": ["有环境反射补光", "无明显补光(暗部很深)", "有反光面"]},
|
||||
{"key": "practical", "label": "实用光源状态", "type": "select",
|
||||
"options": ["灯全开", "部分开", "灯关着(仅自然光)", "灯管闪烁/故障"]},
|
||||
],
|
||||
},
|
||||
"S7_color_mood": {
|
||||
"name": "色彩氛围",
|
||||
"icon": "🎨",
|
||||
"fields": [
|
||||
{"key": "tone", "label": "调性", "type": "select",
|
||||
"options": ["冷调", "暖调", "中性", "高对比", "低对比/灰调"]},
|
||||
{"key": "primary_color", "label": "主色", "type": "select",
|
||||
"options": ["白色系", "灰色系", "米黄系", "蓝色系", "绿色系", "棕色系",
|
||||
"黑色系", "混合"]},
|
||||
{"key": "accent_color", "label": "点缀色", "type": "select",
|
||||
"options": ["无", "蓝色", "红色", "黄色", "绿色", "橙色", "粉色", "金属色"]},
|
||||
{"key": "saturation", "label": "饱和度", "type": "select",
|
||||
"options": ["低饱和(灰淡)", "中等", "饱和度高", "近乎单色"]},
|
||||
{"key": "mood", "label": "氛围", "type": "select",
|
||||
"options": ["专业冷静", "温馨日常", "压抑沉重", "冷清空旷", "热闹嘈杂",
|
||||
"紧张不安", "怀旧感", "崭新疏离"]},
|
||||
],
|
||||
},
|
||||
"S8_time_weather": {
|
||||
"name": "时间天气",
|
||||
"icon": "🕐",
|
||||
"fields": [
|
||||
{"key": "time_of_day", "label": "时间", "type": "select",
|
||||
"options": ["清晨", "上午", "正午", "下午", "黄昏", "夜晚", "深夜"]},
|
||||
{"key": "weather", "label": "天气", "type": "select",
|
||||
"options": ["晴", "多云", "阴", "小雨", "大雨", "雪", "雾", "不适用(室内)"]},
|
||||
{"key": "season", "label": "季节", "type": "select",
|
||||
"options": ["春", "夏", "秋", "冬", "不明确"]},
|
||||
{"key": "outdoor_visible", "label": "窗外可见", "type": "select",
|
||||
"options": ["无窗", "对面建筑", "天空", "树/绿化", "街道", "夜灯/霓虹", "雨雪"]},
|
||||
{"key": "atmosphere", "label": "空气感", "type": "select",
|
||||
"options": ["清澈", "有灰尘/浮尘", "蒸汽/水汽", "烟雾", "雨后湿润", "干燥"]},
|
||||
],
|
||||
},
|
||||
"S9_camera": {
|
||||
"name": "镜头视角",
|
||||
"icon": "🎥",
|
||||
"fields": [
|
||||
{"key": "common_angles", "label": "常用机位", "type": "select",
|
||||
"options": ["平视为主", "俯视/顶拍", "低角度", "过肩", "广角全景", "混合多机位"]},
|
||||
{"key": "spatial_feel", "label": "空间感", "type": "select",
|
||||
"options": ["开阔通透", "适中", "略显局促", "幽闭压迫"]},
|
||||
{"key": "scale_ref", "label": "尺度参照物", "type": "select",
|
||||
"options": ["有(门/桌椅/人体)", "无(纯空镜)", "有标识物(牌/字)"]},
|
||||
{"key": "avoid", "label": "需避免", "type": "text",
|
||||
"placeholder": "如:不要出现人、不要镜子反光、不要现代标识"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# ── 多视角定义(5 张 · 与角色套装同一套机制)──
|
||||
SCENE_VIEWS: dict = {
|
||||
"back": {
|
||||
"order": 6,
|
||||
"label": "门口 / 反向视角",
|
||||
"is_anchor": False,
|
||||
"shot": "Reverse angle from the opposite end: the camera looks back toward "
|
||||
"the entrance/door, revealing the doorway and what is behind it. "
|
||||
"Same room, same furniture, same lighting — only the viewpoint reversed.",
|
||||
},
|
||||
"wide": {
|
||||
"order": 1, "label": "广角全景(主图)", "is_anchor": True,
|
||||
"shot": "Wide establishing shot of the entire space: the camera stands at one end "
|
||||
"showing the whole room at once, eye-level, 24mm wide lens, deep depth of "
|
||||
"field so front and back are both readable.",
|
||||
"consistency": "The same space, same furniture layout, same lighting, same color palette.",
|
||||
},
|
||||
"front": {
|
||||
"order": 2, "label": "正面视线", "is_anchor": False,
|
||||
"shot": "Facing the main wall of the same space straight on (where the room's focal "
|
||||
"point is), eye-level, 35mm lens, the layout unchanged.",
|
||||
"consistency": "The same space, same furniture layout, same lighting, same color palette.",
|
||||
},
|
||||
"side": {
|
||||
"order": 3, "label": "侧面视角", "is_anchor": False,
|
||||
"shot": "From the side of the same space at about 45 degrees: reveals the depth and "
|
||||
"how the furniture sits against the walls, eye-level, 35mm lens.",
|
||||
"consistency": "The same space, same furniture layout, same lighting, same color palette.",
|
||||
},
|
||||
"top": {
|
||||
"order": 4, "label": "俯视 / 顶图", "is_anchor": False,
|
||||
"shot": "Looking straight down at the same space from above (bird's-eye plan view): "
|
||||
"shows the spatial relationship between furniture and walls, ceiling not visible.",
|
||||
"consistency": "The same space, same furniture layout, same colors.",
|
||||
},
|
||||
"detail": {
|
||||
"order": 5, "label": "细节特写", "is_anchor": False,
|
||||
"shot": "Close-up of the most telling detail of the same space (the focal object / "
|
||||
"surface texture), shallow depth of field.",
|
||||
"consistency": "The same space, same materials, same lighting, same color palette.",
|
||||
},
|
||||
}
|
||||
|
||||
# 通用负面词(场景不该出现的东西)
|
||||
SCENE_NEGATIVE = (
|
||||
"people, person, human figure, character, hand, "
|
||||
"text, watermark, logo, caption, subtitle, signature, "
|
||||
"distorted geometry, warped perspective, fisheye distortion, "
|
||||
"blurry, low resolution, jpeg artifacts, "
|
||||
"场景中的人物,人影,文字,水印,logo,字幕,"
|
||||
"透视扭曲,鱼眼畸变,模糊,低清,噪点,"
|
||||
"现代物品(年代不符时),穿帮,重复结构"
|
||||
)
|
||||
|
||||
# 写实质感层(研究结论:不写 4K/超清,写相机与物理细节)
|
||||
LOOK_REALISM = (
|
||||
"A real photograph of a real place, not a render. "
|
||||
"Full-frame camera, 24-35mm lens at f/4-5.6, true-to-life white balance, "
|
||||
"no HDR halo, no oversaturation, slight film grain. "
|
||||
"Photographed in available light."
|
||||
)
|
||||
|
||||
|
||||
def _join(vals) -> str:
|
||||
"""把字段值拼成一句(去空、去重、中英用不同分隔)"""
|
||||
out, seen = [], set()
|
||||
for v in vals:
|
||||
s = str(v or "").strip().strip("。,, ;;")
|
||||
if s and s not in seen:
|
||||
seen.add(s)
|
||||
out.append(s)
|
||||
return ",".join(out)
|
||||
|
||||
|
||||
def build_scene_prompt(layers: dict, extra: str = "") -> str:
|
||||
"""把分层字段组装成**场景提示词**(文生图用)
|
||||
|
||||
layers 形如 {"S0_identity": {"space_type": "病房", ...}, ...}
|
||||
"""
|
||||
g = lambda lid, key: ((layers.get(lid) or {}).get(key) or "").strip()
|
||||
|
||||
# ① 主体(这是什么空间)—— 最重要,放最前
|
||||
main = _join([
|
||||
g("S0_identity", "region"),
|
||||
g("S2_architecture", "era"),
|
||||
g("S2_architecture", "condition"),
|
||||
g("S0_identity", "space_type"),
|
||||
g("S2_architecture", "style"),
|
||||
])
|
||||
# ② 空间(多大、什么形状)
|
||||
space = _join([
|
||||
g("S0_identity", "scale"), g("S1_layout", "area"), g("S1_layout", "ceiling"),
|
||||
g("S1_layout", "layout"), g("S1_layout", "depth"), g("S1_layout", "structure"),
|
||||
])
|
||||
# ③ 材质表面
|
||||
surf = _join([
|
||||
g("S3_surfaces", "wall"), g("S3_surfaces", "floor"),
|
||||
g("S3_surfaces", "ceiling_surface"), g("S3_surfaces", "fixtures"),
|
||||
g("S3_surfaces", "wear"),
|
||||
])
|
||||
# ④ 家具与道具
|
||||
furn = _join([
|
||||
g("S4_furniture", "main_furniture"), g("S4_furniture", "secondary"),
|
||||
g("S4_furniture", "arrangement"), g("S4_furniture", "quantity"),
|
||||
g("S4_furniture", "era_match"), g("S4_furniture", "function_zone"),
|
||||
g("S5_props", "tabletop"), g("S5_props", "wall_decor"),
|
||||
g("S5_props", "floor_items"), g("S5_props", "life_traces"),
|
||||
g("S5_props", "devices"),
|
||||
])
|
||||
# ⑤ 光与色
|
||||
light = _join([
|
||||
g("S6_lighting", "main_light"), g("S6_lighting", "color_temp"),
|
||||
g("S6_lighting", "light_dir"), g("S6_lighting", "intensity"),
|
||||
g("S6_lighting", "fill_light"), g("S6_lighting", "practical"),
|
||||
])
|
||||
color = _join([
|
||||
g("S7_color_mood", "tone"), g("S7_color_mood", "primary_color"),
|
||||
g("S7_color_mood", "accent_color"), g("S7_color_mood", "saturation"),
|
||||
g("S7_color_mood", "mood"),
|
||||
])
|
||||
# ⑥ 时空
|
||||
timew = _join([
|
||||
g("S8_time_weather", "time_of_day"), g("S8_time_weather", "weather"),
|
||||
g("S8_time_weather", "season"), g("S8_time_weather", "outdoor_visible"),
|
||||
g("S8_time_weather", "atmosphere"),
|
||||
])
|
||||
|
||||
parts = [
|
||||
f"【空间】{main}" if main else "",
|
||||
f"【格局】{space}" if space else "",
|
||||
f"【材质】{surf}" if surf else "",
|
||||
f"【陈设】{furn}" if furn else "",
|
||||
f"【光线】{light}" if light else "",
|
||||
f"【色彩】{color}" if color else "",
|
||||
f"【时空】{timew}" if timew else "",
|
||||
]
|
||||
body = "。".join([p for p in parts if p]) + "。" if any(parts) else ""
|
||||
tail = f"{LOOK_REALISM}"
|
||||
if extra:
|
||||
tail += " " + extra.strip()
|
||||
return (body + tail).strip()
|
||||
|
||||
|
||||
def build_scene_view_prompt(view: str, layers: dict, extra: str = "") -> str:
|
||||
"""组装「场景多视角」中某一张的提示词(构图指令置首 + 空间描述)"""
|
||||
v = SCENE_VIEWS.get(view) or SCENE_VIEWS["wide"]
|
||||
base = build_scene_prompt(layers, extra="")
|
||||
return " ".join([
|
||||
v["shot"], # ★ 构图指令必须在最前
|
||||
base,
|
||||
v["consistency"],
|
||||
LOOK_REALISM,
|
||||
] + ([extra.strip()] if extra else [])).strip()
|
||||
|
||||
|
||||
def build_scene_set(layers: dict, prompts: dict = None, extra: str = "") -> dict:
|
||||
"""组装全部视角(5 张)→ {view: {order,label,is_anchor,prompt}}
|
||||
|
||||
prompts: 可选,{view: 成品提示词} —— 给定时优先使用(预设 / 用户自带提示词)
|
||||
"""
|
||||
prompts = prompts or {}
|
||||
out = {}
|
||||
for vid, v in sorted(SCENE_VIEWS.items(), key=lambda kv: kv[1]["order"]):
|
||||
p = str(prompts.get(vid) or "").strip()
|
||||
out[vid] = {
|
||||
"order": v["order"], "label": v["label"], "is_anchor": v["is_anchor"],
|
||||
"prompt": p or build_scene_view_prompt(vid, layers, extra=extra),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def scene_layer_summary() -> dict:
|
||||
"""层/字段统计(供前端展示与自检)"""
|
||||
layers = len(SCENE_LAYERS)
|
||||
fields = sum(len(L.get("fields") or []) for L in SCENE_LAYERS.values())
|
||||
return {"layers": layers, "fields": fields,
|
||||
"views": len(SCENE_VIEWS),
|
||||
"layer_names": [L["name"] for L in SCENE_LAYERS.values()]}
|
||||
|
||||
|
||||
def parse_prompt_schema() -> dict:
|
||||
"""给 LLM 的结构化输出说明(AI 自动生成字段时用)"""
|
||||
import json
|
||||
spec = {}
|
||||
for lid, L in SCENE_LAYERS.items():
|
||||
spec[lid] = {f["key"]: (f.get("options") or "text") for f in L.get("fields") or []}
|
||||
return {"target": "scene", "layers": spec,
|
||||
"instruction": "根据用户描述,为每个字段选择最合适的值(只能从给定选项中选);"
|
||||
"没提到的字段留空。输出 JSON:" + json.dumps({"layers": spec}, ensure_ascii=False)[:200]}
|
||||
Reference in New Issue
Block a user