dsh-plugin-image-workbench 1.1.5 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +5 -3
- package/README.md +7 -3
- package/client.js +241 -241
- package/cordis.patch.yml +14 -14
- package/host.js +51 -0
- package/install.mjs +333 -387
- package/package.json +3 -2
- package/scripts/fetch-style-images.mjs +104 -0
- package/scripts/vendor-snapshot.py +154 -154
- package/server/access.py +26 -26
- package/server/app.py +1329 -1329
- package/server/catalog.py +147 -147
- package/server/composer.py +391 -391
- package/server/drafts.py +158 -158
- package/server/history.py +575 -575
- package/server/imaging.py +172 -172
- package/server/pool.py +1335 -1335
- package/server/pool_admin.py +921 -921
- package/server/pool_service.py +1073 -1073
- package/server/requirements.txt +6 -6
- package/server/smoke_check.py +402 -402
- package/server/static/access.html +202 -202
- package/vendor/chatgpt2api/source/.env.example +82 -82
- package/vendor/chatgpt2api/source/.python-version +1 -1
- package/vendor/chatgpt2api/source/LICENSE +21 -21
- package/vendor/chatgpt2api/source/README.md +391 -391
- package/vendor/chatgpt2api/source/VERSION +1 -1
- package/vendor/chatgpt2api/source/api/__init__.py +2 -2
- package/vendor/chatgpt2api/source/api/accounts.py +537 -537
- package/vendor/chatgpt2api/source/api/ai.py +217 -217
- package/vendor/chatgpt2api/source/api/app.py +62 -62
- package/vendor/chatgpt2api/source/api/errors.py +46 -46
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
- package/vendor/chatgpt2api/source/api/support.py +134 -134
- package/vendor/chatgpt2api/source/api/system.py +365 -365
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
- package/vendor/chatgpt2api/source/config.default.json +108 -108
- package/vendor/chatgpt2api/source/main.py +9 -9
- package/vendor/chatgpt2api/source/pyproject.toml +27 -27
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
- package/vendor/chatgpt2api/source/services/config.py +667 -667
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
- package/vendor/chatgpt2api/source/services/image_service.py +376 -376
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
- package/vendor/chatgpt2api/source/services/log_service.py +337 -337
- package/vendor/chatgpt2api/source/services/model_service.py +182 -182
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
- package/vendor/chatgpt2api/source/utils/helper.py +469 -469
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
- package/vendor/chatgpt2api/source/utils/log.py +110 -110
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
- package/vendor/chatgpt2api/source/utils/pow.py +207 -207
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
- package/vendor/chatgpt2api/source/uv.lock +904 -904
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
- package/vendor/chatgpt2api/source/web/components.json +24 -24
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
- package/vendor/chatgpt2api/source/web/package.json +58 -58
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
package/server/composer.py
CHANGED
|
@@ -1,391 +1,391 @@
|
|
|
1
|
-
"""把 handraw-style 的「风格 + 主题色 + 图型 + 融合 + 文字槽位 + 情绪 + 比例」
|
|
2
|
-
拼成两层产物:
|
|
3
|
-
|
|
4
|
-
1. 出图指令(instruction):一行短指令,人能看懂、也能直接丢给 AI 再加工
|
|
5
|
-
2. 完整提示词(prompt):真正提交给号池生图的那段文本
|
|
6
|
-
|
|
7
|
-
规则来源:handdraw-painter/SKILL.md
|
|
8
|
-
- 提示词内不出现任何编号(FA-001 / SC-002 / C-16 一律换成自然语言名),
|
|
9
|
-
否则生图模型会把编号当文字画进画面
|
|
10
|
-
- 风格特征只保留正向描述,含「避免/不要/不准/禁止」的分句一律剔除
|
|
11
|
-
- 图型 prompt 原文照用,它是排版权威描述
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import re
|
|
17
|
-
|
|
18
|
-
NEG_MARKERS = ("避免", "不要", "不准", "禁止")
|
|
19
|
-
|
|
20
|
-
# 缩微摄影跨媒介融合:角色小、场景真、同光源同空间
|
|
21
|
-
MICRO_FUSION = (
|
|
22
|
-
"缩微摄影跨媒介融合:角色尺寸异常小、场景正常尺寸。"
|
|
23
|
-
"场景视觉语言=暖调写实生活摄影(真实材质、可信光影)。"
|
|
24
|
-
"角色与场景同空间同光源,有可信接触、投影、遮挡,角色真正活在场景里而不是贴纸。"
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
WHITESPACE = {
|
|
28
|
-
"moderate": "【大量留白】",
|
|
29
|
-
"high": "【大量留白,场景只显示必要部分,不要显示全】",
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
# 2.35:1 的公众号首图是「3:2 出图 + 上下各裁一截」得来的,不提前约束构图,
|
|
33
|
-
# 裁完就会砍掉标题或者削掉人物头顶。这里把安全区写死进提示词里。
|
|
34
|
-
WIDE_CROP_RATIO = "2.35:1"
|
|
35
|
-
WIDE_CROP_HINT = (
|
|
36
|
-
"横幅构图:主体、标题和所有关键元素都压在同一块居中的横向安全区里,"
|
|
37
|
-
"画面上下各留一段可以整块裁掉的次要背景(纯色、天空、地面或虚化),"
|
|
38
|
-
"文字和人物头顶都不要贴到画面上沿或下沿"
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
# 摄影模式:文案全部照搬 handraw-style 上游,不另写一套
|
|
42
|
-
# - 「摄影写真模式」见 handraw-style/gallery/tutorials.html 的 photo 模式前缀
|
|
43
|
-
# - 「高级摄影师风格,创意构图,强烈的戏剧张力」见
|
|
44
|
-
# handraw-style/skills/couple-photo-orchestrator/SKILL.md「通用摄影关键词」
|
|
45
|
-
# 上游那句「请先出3个方案供我挑选」是让 AI 先给方案的指令,进生图会被当成画面要求,
|
|
46
|
-
# 所以这里只取模式标识本身。
|
|
47
|
-
PHOTO_LEAD = "摄影写真模式。"
|
|
48
|
-
PHOTO_KEYWORDS = "通用摄影关键词:高级摄影师风格,创意构图,强烈的戏剧张力。"
|
|
49
|
-
PHOTO_MODES = ("graphic-text", "photo")
|
|
50
|
-
|
|
51
|
-
TAIL_INSTRUCTION = ""
|
|
52
|
-
|
|
53
|
-
# 带了参考图时插在最前面:先定画风基准,再谈版式和文字,避免模型把参考图
|
|
54
|
-
# 原样复刻(用户要的是「融合」,不是「重画同一张」)
|
|
55
|
-
REF_FUSION = (
|
|
56
|
-
"参考我上传的这张图:沿用它的画风、笔触、配色和整体质感,把它当作视觉基准。"
|
|
57
|
-
"画面内容按下文重新绘制,下面的版式与文字要求优先,不要照抄参考图里的文字。"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
# 中层小辅助图没填时的兜底描述,保证三层版式不会空一层
|
|
61
|
-
SCENE_FALLBACK = "一个涂鸦小人和一件具体实物道具,动作直接对应上层标题的意思,造型简单、线条少"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def clean_traits(features: str, limit: int = 110) -> str:
|
|
65
|
-
"""只留正向特征,砍掉含否定词的分句,过长截断。"""
|
|
66
|
-
segs = re.split(r"[;;。]", features or "")
|
|
67
|
-
keep = []
|
|
68
|
-
for seg in segs:
|
|
69
|
-
seg = seg.strip()
|
|
70
|
-
if not seg or any(m in seg for m in NEG_MARKERS):
|
|
71
|
-
continue
|
|
72
|
-
keep.append(seg)
|
|
73
|
-
# 按分句累加,超长就整句舍弃,不留半句、不加省略号
|
|
74
|
-
out: list[str] = []
|
|
75
|
-
total = 0
|
|
76
|
-
for seg in keep:
|
|
77
|
-
add = len(seg) + (1 if out else 0)
|
|
78
|
-
if out and total + add > limit:
|
|
79
|
-
break
|
|
80
|
-
if not out and len(seg) > limit:
|
|
81
|
-
seg = seg[:limit].rstrip("、,;")
|
|
82
|
-
out.append(seg)
|
|
83
|
-
total += add
|
|
84
|
-
return ";".join(out)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def style_line(style: dict | None) -> str:
|
|
88
|
-
"""Loose Ink-and-Wash Storybook(参考 Quentin Blake:抖动墨线、松散水彩…)。"""
|
|
89
|
-
if not style:
|
|
90
|
-
return ""
|
|
91
|
-
name = (style.get("name_en") or "").strip()
|
|
92
|
-
ref = (style.get("ref") or "").strip()
|
|
93
|
-
traits = clean_traits(style.get("features", ""))
|
|
94
|
-
inner = ""
|
|
95
|
-
if ref and traits:
|
|
96
|
-
inner = f"参考 {ref}:{traits}"
|
|
97
|
-
elif ref:
|
|
98
|
-
inner = f"参考 {ref}"
|
|
99
|
-
elif traits:
|
|
100
|
-
inner = traits
|
|
101
|
-
if not name and not inner:
|
|
102
|
-
return ""
|
|
103
|
-
if name and inner:
|
|
104
|
-
return f"{name}({inner})。"
|
|
105
|
-
return f"{name or inner}。"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def color_line(color: dict | None) -> str:
|
|
109
|
-
if not color:
|
|
110
|
-
return ""
|
|
111
|
-
return (color.get("prompt") or "").strip()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def layout_line(layout: dict | None) -> str:
|
|
115
|
-
if not layout:
|
|
116
|
-
return ""
|
|
117
|
-
text = (layout.get("prompt") or "").strip()
|
|
118
|
-
if text and not text.endswith(("。", "!", "》", "」")):
|
|
119
|
-
text += "。"
|
|
120
|
-
return text
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def clean_title(raw: str) -> str:
|
|
124
|
-
"""01|先存后花 / **01|先存后花** / 【01|先存后花】 → 01 先存后花"""
|
|
125
|
-
t = (raw or "").strip()
|
|
126
|
-
t = t.strip("**【】[] ")
|
|
127
|
-
t = re.sub(r"[||]", " ", t)
|
|
128
|
-
return re.sub(r"\s{2,}", " ", t).strip()
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def short_body(body: str, limit: int = 62) -> str:
|
|
132
|
-
"""副标题槽位只用正文第一句;太长就在句内标点处收掉,别让图里塞一整段。"""
|
|
133
|
-
t = re.sub(r"\s+", "", body or "")
|
|
134
|
-
if not t:
|
|
135
|
-
return ""
|
|
136
|
-
parts = re.split(r"(?<=[。!?!?])", t)
|
|
137
|
-
first = (parts[0] if parts and parts[0] else t).strip()
|
|
138
|
-
if len(first) <= limit:
|
|
139
|
-
return first
|
|
140
|
-
cut = first[:limit]
|
|
141
|
-
for mark in (",", "、", ";", ":"):
|
|
142
|
-
i = cut.rfind(mark)
|
|
143
|
-
if i > limit * 0.45:
|
|
144
|
-
return cut[:i] + "。"
|
|
145
|
-
return cut + "。"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def build_slots(title: str, body: str, scene: str, layout: dict | None) -> str:
|
|
149
|
-
"""文字槽位。三层版式按「上层主标题 / 中层小辅助图 / 下层副标题」写,
|
|
150
|
-
其他图型退回「主标题 / 正文」,免得槽位和图型打架。"""
|
|
151
|
-
three = bool(layout) and "三层" in (layout.get("prompt") or "")
|
|
152
|
-
sub = short_body(body)
|
|
153
|
-
bits = []
|
|
154
|
-
if three:
|
|
155
|
-
if title:
|
|
156
|
-
bits.append(f"上层主标题文字:「{title}」")
|
|
157
|
-
bits.append("中层小辅助图:" + (scene.strip() or SCENE_FALLBACK))
|
|
158
|
-
if sub:
|
|
159
|
-
bits.append(f"下层副标题文字:「{sub}」")
|
|
160
|
-
else:
|
|
161
|
-
if title:
|
|
162
|
-
bits.append(f"主标题文字:「{title}」")
|
|
163
|
-
if sub:
|
|
164
|
-
bits.append(f"正文文字:「{sub}」")
|
|
165
|
-
if not bits:
|
|
166
|
-
return ""
|
|
167
|
-
out = "。".join(bits)
|
|
168
|
-
if not out.endswith(("」", "。")):
|
|
169
|
-
out += "。"
|
|
170
|
-
return out
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def parse_moods(raw) -> list[str]:
|
|
174
|
-
if not raw:
|
|
175
|
-
return []
|
|
176
|
-
if isinstance(raw, (list, tuple)):
|
|
177
|
-
tokens = []
|
|
178
|
-
for part in raw:
|
|
179
|
-
tokens += re.split(r"[、,,/\s]+", str(part))
|
|
180
|
-
else:
|
|
181
|
-
tokens = re.split(r"[、,,/\s]+", str(raw))
|
|
182
|
-
seen, out = set(), []
|
|
183
|
-
for t in tokens:
|
|
184
|
-
t = t.strip()
|
|
185
|
-
if t and t not in seen:
|
|
186
|
-
seen.add(t)
|
|
187
|
-
out.append(t)
|
|
188
|
-
return out
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def build_instruction(sel: dict, ratio: str, moods: list[str], fusion: bool, whitespace: str,
|
|
192
|
-
ref: bool = False, image_mode: str = "graphic-text", shoot: str = "") -> str:
|
|
193
|
-
"""一行短指令,形如:图文模式,图型:SC-002,风格:FB-001,主题色:C-26,画幅比例:3:4,情绪:治愈、幽默"""
|
|
194
|
-
bits = ["摄影写真模式" if image_mode == "photo" else "图文模式"]
|
|
195
|
-
if ref:
|
|
196
|
-
bits.append("参考图:已上传")
|
|
197
|
-
layout = sel.get("layout")
|
|
198
|
-
style = sel.get("style")
|
|
199
|
-
color = sel.get("color")
|
|
200
|
-
# 摄影模式不套图型版式:图型模板写的是「主标题/小辅助图/副标题」的位置,和写真不排字冲突
|
|
201
|
-
if layout and image_mode != "photo":
|
|
202
|
-
bits.append(f"图型:{layout['id']}")
|
|
203
|
-
if style:
|
|
204
|
-
bits.append(f"风格:{style['id']}")
|
|
205
|
-
if color:
|
|
206
|
-
bits.append(f"主题色:{color['id']}")
|
|
207
|
-
# 三样都可以不选:说明这一轮靠什么出图,避免指令读起来像漏了参数
|
|
208
|
-
if not layout and not style and not color:
|
|
209
|
-
bits.append("图型 / 风格 / 主题色:跟参考图" if ref else "图型 / 风格 / 主题色:不限")
|
|
210
|
-
if ratio:
|
|
211
|
-
bits.append(f"画幅比例:{ratio}")
|
|
212
|
-
if moods:
|
|
213
|
-
bits.append("情绪:" + "、".join(moods))
|
|
214
|
-
if image_mode == "photo" and shoot:
|
|
215
|
-
bits.append(f"拍摄类型:{shoot}")
|
|
216
|
-
if fusion:
|
|
217
|
-
bits.append("缩微摄影融合")
|
|
218
|
-
if whitespace == "moderate":
|
|
219
|
-
bits.append("留白:大量")
|
|
220
|
-
elif whitespace == "high":
|
|
221
|
-
bits.append("留白:极简")
|
|
222
|
-
return ",".join(bits)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _ids_to_names(instr: str, sel: dict) -> str:
|
|
226
|
-
"""手改指令里的编号换成自然语言名,遵守「提示词不出现编号」的规矩。"""
|
|
227
|
-
layout = sel.get("layout")
|
|
228
|
-
style = sel.get("style")
|
|
229
|
-
color = sel.get("color")
|
|
230
|
-
if layout:
|
|
231
|
-
instr = instr.replace(f"图型:{layout['id']}", f"图型:{(layout.get('title') or '').strip()}")
|
|
232
|
-
if style:
|
|
233
|
-
instr = instr.replace(f"风格:{style['id']}",
|
|
234
|
-
f"风格:{(style.get('name_en') or style.get('name') or '').strip()}")
|
|
235
|
-
if color:
|
|
236
|
-
instr = instr.replace(f"主题色:{color['id']}", f"主题色:{(color.get('name') or '').strip()}")
|
|
237
|
-
return instr
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def build_prompt(item: dict, sel: dict, ratio: str, moods: list[str],
|
|
241
|
-
fusion: bool, whitespace: str, note: str = "", ref: bool = False,
|
|
242
|
-
image_mode: str = "graphic-text", shoot: str = "", instr: str = "") -> str:
|
|
243
|
-
"""单条完整提示词。item = {title, body}"""
|
|
244
|
-
style = sel.get("style")
|
|
245
|
-
layout = sel.get("layout")
|
|
246
|
-
color = sel.get("color")
|
|
247
|
-
title = clean_title(item.get("title") or "")
|
|
248
|
-
body = (item.get("body") or "").strip()
|
|
249
|
-
scene = (item.get("scene") or "").strip()
|
|
250
|
-
|
|
251
|
-
blocks: list[str] = []
|
|
252
|
-
|
|
253
|
-
if ref:
|
|
254
|
-
blocks.append(REF_FUSION)
|
|
255
|
-
|
|
256
|
-
# 手改过的出图指令原样进提示词(编号已换成自然语言名),用户改了什么就生效什么
|
|
257
|
-
if instr:
|
|
258
|
-
blocks.append(_ids_to_names(instr, sel))
|
|
259
|
-
|
|
260
|
-
head = style_line(style) + color_line(color)
|
|
261
|
-
if head:
|
|
262
|
-
blocks.append(head)
|
|
263
|
-
|
|
264
|
-
layout_text = "" if image_mode == "photo" else layout_line(layout)
|
|
265
|
-
if layout_text:
|
|
266
|
-
blocks.append(layout_text)
|
|
267
|
-
|
|
268
|
-
# 缩微摄影融合在图文 / 摄影两种模式下都生效(用户 2026-11-05 定)
|
|
269
|
-
if fusion:
|
|
270
|
-
blocks.append(MICRO_FUSION)
|
|
271
|
-
|
|
272
|
-
if note.strip():
|
|
273
|
-
blocks.append(f"补充要求:{note.strip()}。")
|
|
274
|
-
|
|
275
|
-
if image_mode == "photo":
|
|
276
|
-
# 摄影模式:上游提示词里没有文字槽位,分镜的标题/正文只当画面内容
|
|
277
|
-
blocks.append(PHOTO_LEAD)
|
|
278
|
-
if shoot:
|
|
279
|
-
blocks.append(f"拍摄类型:{shoot}。")
|
|
280
|
-
blocks.append(PHOTO_KEYWORDS)
|
|
281
|
-
subject = re.sub(r"^0*\d{1,3}[\s..、,,::||\-—]+", "", title).strip() or title
|
|
282
|
-
# 分镜标题/正文/小辅助图都当画面内容;各自收掉句末标点,避免「。。」
|
|
283
|
-
scene_bits = [re.sub(r"[。!?\.!?,,;;]+$", "", x).strip() for x in (subject, body, scene)]
|
|
284
|
-
scene_bits = [x for x in scene_bits if x]
|
|
285
|
-
if scene_bits:
|
|
286
|
-
blocks.append("画面:" + "。".join(scene_bits) + "。")
|
|
287
|
-
elif ref:
|
|
288
|
-
# 只传了参考图、没写任何文案:不给主体的话模型只能照抄原图
|
|
289
|
-
blocks.append("画面:沿用参考图里的主体与场景,重新组织构图拍一张。")
|
|
290
|
-
else:
|
|
291
|
-
slots = build_slots(title, body, scene, layout)
|
|
292
|
-
if slots:
|
|
293
|
-
blocks.append(slots)
|
|
294
|
-
blocks.append("文字清晰可读,文字参与构图,不要出现多余文字。")
|
|
295
|
-
elif ref:
|
|
296
|
-
# 只上传参考图、不选图型风格主题色(用户 2026-11-08 要求):没有文案可排,
|
|
297
|
-
# 就让参考图当唯一依据,重新组织构图出一张完整画面,别把原图原样复刻
|
|
298
|
-
blocks.append(
|
|
299
|
-
"没有文案:画面主体与画风都取自参考图,重新组织构图、环境与动作,"
|
|
300
|
-
"画一张完整的插画,画面里不排文字。"
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
tail = []
|
|
304
|
-
if moods:
|
|
305
|
-
tail.append("情绪:" + "、".join(moods))
|
|
306
|
-
if color:
|
|
307
|
-
tail.append(f"{color.get('name') or ''}作统一色调点缀")
|
|
308
|
-
if ratio:
|
|
309
|
-
tail.append(f"画幅比例 {ratio}")
|
|
310
|
-
tail = [t for t in tail if t and t.strip(":")]
|
|
311
|
-
if tail:
|
|
312
|
-
blocks.append("。".join(tail) + "。")
|
|
313
|
-
|
|
314
|
-
if str(ratio or "").strip() == WIDE_CROP_RATIO:
|
|
315
|
-
blocks.append(WIDE_CROP_HINT)
|
|
316
|
-
|
|
317
|
-
if whitespace in WHITESPACE:
|
|
318
|
-
blocks.append(WHITESPACE[whitespace])
|
|
319
|
-
|
|
320
|
-
blocks.append(TAIL_INSTRUCTION)
|
|
321
|
-
return "\n".join(b for b in blocks if b)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def compose(payload: dict, styles: list[dict], layouts: list[dict], colors: list[dict]) -> dict:
|
|
325
|
-
"""组装全部条目。payload 里 items 为空时用 topic 兜底生成一条。"""
|
|
326
|
-
|
|
327
|
-
def pick(items, item_id):
|
|
328
|
-
if not item_id:
|
|
329
|
-
return None
|
|
330
|
-
return next((x for x in items if x["id"] == item_id), None)
|
|
331
|
-
|
|
332
|
-
sel = {
|
|
333
|
-
"style": pick(styles, payload.get("style_id")),
|
|
334
|
-
"layout": pick(layouts, payload.get("layout_id")),
|
|
335
|
-
"color": pick(colors, payload.get("color_id")),
|
|
336
|
-
}
|
|
337
|
-
ratio = (payload.get("ratio") or "").strip()
|
|
338
|
-
moods = parse_moods(payload.get("mood"))
|
|
339
|
-
fusion = bool(payload.get("fusion"))
|
|
340
|
-
whitespace = (payload.get("whitespace") or "normal").strip()
|
|
341
|
-
note = (payload.get("note") or "").strip()
|
|
342
|
-
ref = bool(payload.get("ref"))
|
|
343
|
-
# 出图模式:默认图文,和以前完全一样;只有显式传 photo 才走摄影分支
|
|
344
|
-
image_mode = (payload.get("image_mode") or "graphic-text").strip()
|
|
345
|
-
if image_mode not in PHOTO_MODES:
|
|
346
|
-
image_mode = "graphic-text"
|
|
347
|
-
shoot = (payload.get("shoot_type") or "").strip()[:40] if image_mode == "photo" else ""
|
|
348
|
-
# allow_blank:左侧没写标题/正文时也允许出提示词(纯指令出图);instruction:手改指令
|
|
349
|
-
allow_blank = bool(payload.get("allow_blank"))
|
|
350
|
-
custom_instr = (payload.get("instruction") or "").strip()
|
|
351
|
-
|
|
352
|
-
raw_items = payload.get("items") or []
|
|
353
|
-
items = []
|
|
354
|
-
for raw in raw_items:
|
|
355
|
-
if not isinstance(raw, dict):
|
|
356
|
-
continue
|
|
357
|
-
title = (raw.get("title") or "").strip()
|
|
358
|
-
body = (raw.get("body") or "").strip()
|
|
359
|
-
scene = (raw.get("scene") or "").strip()
|
|
360
|
-
if not title and not body and not allow_blank:
|
|
361
|
-
continue
|
|
362
|
-
items.append({"title": title, "body": body, "scene": scene})
|
|
363
|
-
|
|
364
|
-
if not items:
|
|
365
|
-
topic = (payload.get("topic") or "").strip()
|
|
366
|
-
if topic:
|
|
367
|
-
lines = topic.split("\n", 1)
|
|
368
|
-
items.append({"title": lines[0].strip(), "body": lines[1].strip() if len(lines) > 1 else "", "scene": ""})
|
|
369
|
-
|
|
370
|
-
instruction = custom_instr or build_instruction(
|
|
371
|
-
sel, ratio, moods, fusion, whitespace, ref, image_mode, shoot)
|
|
372
|
-
out_items = []
|
|
373
|
-
for idx, item in enumerate(items, 1):
|
|
374
|
-
out_items.append({
|
|
375
|
-
"index": idx,
|
|
376
|
-
"title": item["title"],
|
|
377
|
-
"body": item["body"],
|
|
378
|
-
"scene": item.get("scene", ""),
|
|
379
|
-
"instruction": instruction,
|
|
380
|
-
"prompt": build_prompt(item, sel, ratio, moods, fusion, whitespace, note, ref,
|
|
381
|
-
image_mode, shoot, instr=custom_instr),
|
|
382
|
-
})
|
|
383
|
-
return {
|
|
384
|
-
"instruction": instruction,
|
|
385
|
-
"items": out_items,
|
|
386
|
-
"selection": {
|
|
387
|
-
"style": sel["style"]["id"] if sel["style"] else None,
|
|
388
|
-
"layout": sel["layout"]["id"] if sel["layout"] else None,
|
|
389
|
-
"color": sel["color"]["id"] if sel["color"] else None,
|
|
390
|
-
},
|
|
391
|
-
}
|
|
1
|
+
"""把 handraw-style 的「风格 + 主题色 + 图型 + 融合 + 文字槽位 + 情绪 + 比例」
|
|
2
|
+
拼成两层产物:
|
|
3
|
+
|
|
4
|
+
1. 出图指令(instruction):一行短指令,人能看懂、也能直接丢给 AI 再加工
|
|
5
|
+
2. 完整提示词(prompt):真正提交给号池生图的那段文本
|
|
6
|
+
|
|
7
|
+
规则来源:handdraw-painter/SKILL.md
|
|
8
|
+
- 提示词内不出现任何编号(FA-001 / SC-002 / C-16 一律换成自然语言名),
|
|
9
|
+
否则生图模型会把编号当文字画进画面
|
|
10
|
+
- 风格特征只保留正向描述,含「避免/不要/不准/禁止」的分句一律剔除
|
|
11
|
+
- 图型 prompt 原文照用,它是排版权威描述
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
NEG_MARKERS = ("避免", "不要", "不准", "禁止")
|
|
19
|
+
|
|
20
|
+
# 缩微摄影跨媒介融合:角色小、场景真、同光源同空间
|
|
21
|
+
MICRO_FUSION = (
|
|
22
|
+
"缩微摄影跨媒介融合:角色尺寸异常小、场景正常尺寸。"
|
|
23
|
+
"场景视觉语言=暖调写实生活摄影(真实材质、可信光影)。"
|
|
24
|
+
"角色与场景同空间同光源,有可信接触、投影、遮挡,角色真正活在场景里而不是贴纸。"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
WHITESPACE = {
|
|
28
|
+
"moderate": "【大量留白】",
|
|
29
|
+
"high": "【大量留白,场景只显示必要部分,不要显示全】",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# 2.35:1 的公众号首图是「3:2 出图 + 上下各裁一截」得来的,不提前约束构图,
|
|
33
|
+
# 裁完就会砍掉标题或者削掉人物头顶。这里把安全区写死进提示词里。
|
|
34
|
+
WIDE_CROP_RATIO = "2.35:1"
|
|
35
|
+
WIDE_CROP_HINT = (
|
|
36
|
+
"横幅构图:主体、标题和所有关键元素都压在同一块居中的横向安全区里,"
|
|
37
|
+
"画面上下各留一段可以整块裁掉的次要背景(纯色、天空、地面或虚化),"
|
|
38
|
+
"文字和人物头顶都不要贴到画面上沿或下沿"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# 摄影模式:文案全部照搬 handraw-style 上游,不另写一套
|
|
42
|
+
# - 「摄影写真模式」见 handraw-style/gallery/tutorials.html 的 photo 模式前缀
|
|
43
|
+
# - 「高级摄影师风格,创意构图,强烈的戏剧张力」见
|
|
44
|
+
# handraw-style/skills/couple-photo-orchestrator/SKILL.md「通用摄影关键词」
|
|
45
|
+
# 上游那句「请先出3个方案供我挑选」是让 AI 先给方案的指令,进生图会被当成画面要求,
|
|
46
|
+
# 所以这里只取模式标识本身。
|
|
47
|
+
PHOTO_LEAD = "摄影写真模式。"
|
|
48
|
+
PHOTO_KEYWORDS = "通用摄影关键词:高级摄影师风格,创意构图,强烈的戏剧张力。"
|
|
49
|
+
PHOTO_MODES = ("graphic-text", "photo")
|
|
50
|
+
|
|
51
|
+
TAIL_INSTRUCTION = ""
|
|
52
|
+
|
|
53
|
+
# 带了参考图时插在最前面:先定画风基准,再谈版式和文字,避免模型把参考图
|
|
54
|
+
# 原样复刻(用户要的是「融合」,不是「重画同一张」)
|
|
55
|
+
REF_FUSION = (
|
|
56
|
+
"参考我上传的这张图:沿用它的画风、笔触、配色和整体质感,把它当作视觉基准。"
|
|
57
|
+
"画面内容按下文重新绘制,下面的版式与文字要求优先,不要照抄参考图里的文字。"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# 中层小辅助图没填时的兜底描述,保证三层版式不会空一层
|
|
61
|
+
SCENE_FALLBACK = "一个涂鸦小人和一件具体实物道具,动作直接对应上层标题的意思,造型简单、线条少"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def clean_traits(features: str, limit: int = 110) -> str:
|
|
65
|
+
"""只留正向特征,砍掉含否定词的分句,过长截断。"""
|
|
66
|
+
segs = re.split(r"[;;。]", features or "")
|
|
67
|
+
keep = []
|
|
68
|
+
for seg in segs:
|
|
69
|
+
seg = seg.strip()
|
|
70
|
+
if not seg or any(m in seg for m in NEG_MARKERS):
|
|
71
|
+
continue
|
|
72
|
+
keep.append(seg)
|
|
73
|
+
# 按分句累加,超长就整句舍弃,不留半句、不加省略号
|
|
74
|
+
out: list[str] = []
|
|
75
|
+
total = 0
|
|
76
|
+
for seg in keep:
|
|
77
|
+
add = len(seg) + (1 if out else 0)
|
|
78
|
+
if out and total + add > limit:
|
|
79
|
+
break
|
|
80
|
+
if not out and len(seg) > limit:
|
|
81
|
+
seg = seg[:limit].rstrip("、,;")
|
|
82
|
+
out.append(seg)
|
|
83
|
+
total += add
|
|
84
|
+
return ";".join(out)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def style_line(style: dict | None) -> str:
|
|
88
|
+
"""Loose Ink-and-Wash Storybook(参考 Quentin Blake:抖动墨线、松散水彩…)。"""
|
|
89
|
+
if not style:
|
|
90
|
+
return ""
|
|
91
|
+
name = (style.get("name_en") or "").strip()
|
|
92
|
+
ref = (style.get("ref") or "").strip()
|
|
93
|
+
traits = clean_traits(style.get("features", ""))
|
|
94
|
+
inner = ""
|
|
95
|
+
if ref and traits:
|
|
96
|
+
inner = f"参考 {ref}:{traits}"
|
|
97
|
+
elif ref:
|
|
98
|
+
inner = f"参考 {ref}"
|
|
99
|
+
elif traits:
|
|
100
|
+
inner = traits
|
|
101
|
+
if not name and not inner:
|
|
102
|
+
return ""
|
|
103
|
+
if name and inner:
|
|
104
|
+
return f"{name}({inner})。"
|
|
105
|
+
return f"{name or inner}。"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def color_line(color: dict | None) -> str:
|
|
109
|
+
if not color:
|
|
110
|
+
return ""
|
|
111
|
+
return (color.get("prompt") or "").strip()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def layout_line(layout: dict | None) -> str:
|
|
115
|
+
if not layout:
|
|
116
|
+
return ""
|
|
117
|
+
text = (layout.get("prompt") or "").strip()
|
|
118
|
+
if text and not text.endswith(("。", "!", "》", "」")):
|
|
119
|
+
text += "。"
|
|
120
|
+
return text
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def clean_title(raw: str) -> str:
|
|
124
|
+
"""01|先存后花 / **01|先存后花** / 【01|先存后花】 → 01 先存后花"""
|
|
125
|
+
t = (raw or "").strip()
|
|
126
|
+
t = t.strip("**【】[] ")
|
|
127
|
+
t = re.sub(r"[||]", " ", t)
|
|
128
|
+
return re.sub(r"\s{2,}", " ", t).strip()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def short_body(body: str, limit: int = 62) -> str:
|
|
132
|
+
"""副标题槽位只用正文第一句;太长就在句内标点处收掉,别让图里塞一整段。"""
|
|
133
|
+
t = re.sub(r"\s+", "", body or "")
|
|
134
|
+
if not t:
|
|
135
|
+
return ""
|
|
136
|
+
parts = re.split(r"(?<=[。!?!?])", t)
|
|
137
|
+
first = (parts[0] if parts and parts[0] else t).strip()
|
|
138
|
+
if len(first) <= limit:
|
|
139
|
+
return first
|
|
140
|
+
cut = first[:limit]
|
|
141
|
+
for mark in (",", "、", ";", ":"):
|
|
142
|
+
i = cut.rfind(mark)
|
|
143
|
+
if i > limit * 0.45:
|
|
144
|
+
return cut[:i] + "。"
|
|
145
|
+
return cut + "。"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_slots(title: str, body: str, scene: str, layout: dict | None) -> str:
|
|
149
|
+
"""文字槽位。三层版式按「上层主标题 / 中层小辅助图 / 下层副标题」写,
|
|
150
|
+
其他图型退回「主标题 / 正文」,免得槽位和图型打架。"""
|
|
151
|
+
three = bool(layout) and "三层" in (layout.get("prompt") or "")
|
|
152
|
+
sub = short_body(body)
|
|
153
|
+
bits = []
|
|
154
|
+
if three:
|
|
155
|
+
if title:
|
|
156
|
+
bits.append(f"上层主标题文字:「{title}」")
|
|
157
|
+
bits.append("中层小辅助图:" + (scene.strip() or SCENE_FALLBACK))
|
|
158
|
+
if sub:
|
|
159
|
+
bits.append(f"下层副标题文字:「{sub}」")
|
|
160
|
+
else:
|
|
161
|
+
if title:
|
|
162
|
+
bits.append(f"主标题文字:「{title}」")
|
|
163
|
+
if sub:
|
|
164
|
+
bits.append(f"正文文字:「{sub}」")
|
|
165
|
+
if not bits:
|
|
166
|
+
return ""
|
|
167
|
+
out = "。".join(bits)
|
|
168
|
+
if not out.endswith(("」", "。")):
|
|
169
|
+
out += "。"
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def parse_moods(raw) -> list[str]:
|
|
174
|
+
if not raw:
|
|
175
|
+
return []
|
|
176
|
+
if isinstance(raw, (list, tuple)):
|
|
177
|
+
tokens = []
|
|
178
|
+
for part in raw:
|
|
179
|
+
tokens += re.split(r"[、,,/\s]+", str(part))
|
|
180
|
+
else:
|
|
181
|
+
tokens = re.split(r"[、,,/\s]+", str(raw))
|
|
182
|
+
seen, out = set(), []
|
|
183
|
+
for t in tokens:
|
|
184
|
+
t = t.strip()
|
|
185
|
+
if t and t not in seen:
|
|
186
|
+
seen.add(t)
|
|
187
|
+
out.append(t)
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_instruction(sel: dict, ratio: str, moods: list[str], fusion: bool, whitespace: str,
|
|
192
|
+
ref: bool = False, image_mode: str = "graphic-text", shoot: str = "") -> str:
|
|
193
|
+
"""一行短指令,形如:图文模式,图型:SC-002,风格:FB-001,主题色:C-26,画幅比例:3:4,情绪:治愈、幽默"""
|
|
194
|
+
bits = ["摄影写真模式" if image_mode == "photo" else "图文模式"]
|
|
195
|
+
if ref:
|
|
196
|
+
bits.append("参考图:已上传")
|
|
197
|
+
layout = sel.get("layout")
|
|
198
|
+
style = sel.get("style")
|
|
199
|
+
color = sel.get("color")
|
|
200
|
+
# 摄影模式不套图型版式:图型模板写的是「主标题/小辅助图/副标题」的位置,和写真不排字冲突
|
|
201
|
+
if layout and image_mode != "photo":
|
|
202
|
+
bits.append(f"图型:{layout['id']}")
|
|
203
|
+
if style:
|
|
204
|
+
bits.append(f"风格:{style['id']}")
|
|
205
|
+
if color:
|
|
206
|
+
bits.append(f"主题色:{color['id']}")
|
|
207
|
+
# 三样都可以不选:说明这一轮靠什么出图,避免指令读起来像漏了参数
|
|
208
|
+
if not layout and not style and not color:
|
|
209
|
+
bits.append("图型 / 风格 / 主题色:跟参考图" if ref else "图型 / 风格 / 主题色:不限")
|
|
210
|
+
if ratio:
|
|
211
|
+
bits.append(f"画幅比例:{ratio}")
|
|
212
|
+
if moods:
|
|
213
|
+
bits.append("情绪:" + "、".join(moods))
|
|
214
|
+
if image_mode == "photo" and shoot:
|
|
215
|
+
bits.append(f"拍摄类型:{shoot}")
|
|
216
|
+
if fusion:
|
|
217
|
+
bits.append("缩微摄影融合")
|
|
218
|
+
if whitespace == "moderate":
|
|
219
|
+
bits.append("留白:大量")
|
|
220
|
+
elif whitespace == "high":
|
|
221
|
+
bits.append("留白:极简")
|
|
222
|
+
return ",".join(bits)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _ids_to_names(instr: str, sel: dict) -> str:
|
|
226
|
+
"""手改指令里的编号换成自然语言名,遵守「提示词不出现编号」的规矩。"""
|
|
227
|
+
layout = sel.get("layout")
|
|
228
|
+
style = sel.get("style")
|
|
229
|
+
color = sel.get("color")
|
|
230
|
+
if layout:
|
|
231
|
+
instr = instr.replace(f"图型:{layout['id']}", f"图型:{(layout.get('title') or '').strip()}")
|
|
232
|
+
if style:
|
|
233
|
+
instr = instr.replace(f"风格:{style['id']}",
|
|
234
|
+
f"风格:{(style.get('name_en') or style.get('name') or '').strip()}")
|
|
235
|
+
if color:
|
|
236
|
+
instr = instr.replace(f"主题色:{color['id']}", f"主题色:{(color.get('name') or '').strip()}")
|
|
237
|
+
return instr
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def build_prompt(item: dict, sel: dict, ratio: str, moods: list[str],
|
|
241
|
+
fusion: bool, whitespace: str, note: str = "", ref: bool = False,
|
|
242
|
+
image_mode: str = "graphic-text", shoot: str = "", instr: str = "") -> str:
|
|
243
|
+
"""单条完整提示词。item = {title, body}"""
|
|
244
|
+
style = sel.get("style")
|
|
245
|
+
layout = sel.get("layout")
|
|
246
|
+
color = sel.get("color")
|
|
247
|
+
title = clean_title(item.get("title") or "")
|
|
248
|
+
body = (item.get("body") or "").strip()
|
|
249
|
+
scene = (item.get("scene") or "").strip()
|
|
250
|
+
|
|
251
|
+
blocks: list[str] = []
|
|
252
|
+
|
|
253
|
+
if ref:
|
|
254
|
+
blocks.append(REF_FUSION)
|
|
255
|
+
|
|
256
|
+
# 手改过的出图指令原样进提示词(编号已换成自然语言名),用户改了什么就生效什么
|
|
257
|
+
if instr:
|
|
258
|
+
blocks.append(_ids_to_names(instr, sel))
|
|
259
|
+
|
|
260
|
+
head = style_line(style) + color_line(color)
|
|
261
|
+
if head:
|
|
262
|
+
blocks.append(head)
|
|
263
|
+
|
|
264
|
+
layout_text = "" if image_mode == "photo" else layout_line(layout)
|
|
265
|
+
if layout_text:
|
|
266
|
+
blocks.append(layout_text)
|
|
267
|
+
|
|
268
|
+
# 缩微摄影融合在图文 / 摄影两种模式下都生效(用户 2026-11-05 定)
|
|
269
|
+
if fusion:
|
|
270
|
+
blocks.append(MICRO_FUSION)
|
|
271
|
+
|
|
272
|
+
if note.strip():
|
|
273
|
+
blocks.append(f"补充要求:{note.strip()}。")
|
|
274
|
+
|
|
275
|
+
if image_mode == "photo":
|
|
276
|
+
# 摄影模式:上游提示词里没有文字槽位,分镜的标题/正文只当画面内容
|
|
277
|
+
blocks.append(PHOTO_LEAD)
|
|
278
|
+
if shoot:
|
|
279
|
+
blocks.append(f"拍摄类型:{shoot}。")
|
|
280
|
+
blocks.append(PHOTO_KEYWORDS)
|
|
281
|
+
subject = re.sub(r"^0*\d{1,3}[\s..、,,::||\-—]+", "", title).strip() or title
|
|
282
|
+
# 分镜标题/正文/小辅助图都当画面内容;各自收掉句末标点,避免「。。」
|
|
283
|
+
scene_bits = [re.sub(r"[。!?\.!?,,;;]+$", "", x).strip() for x in (subject, body, scene)]
|
|
284
|
+
scene_bits = [x for x in scene_bits if x]
|
|
285
|
+
if scene_bits:
|
|
286
|
+
blocks.append("画面:" + "。".join(scene_bits) + "。")
|
|
287
|
+
elif ref:
|
|
288
|
+
# 只传了参考图、没写任何文案:不给主体的话模型只能照抄原图
|
|
289
|
+
blocks.append("画面:沿用参考图里的主体与场景,重新组织构图拍一张。")
|
|
290
|
+
else:
|
|
291
|
+
slots = build_slots(title, body, scene, layout)
|
|
292
|
+
if slots:
|
|
293
|
+
blocks.append(slots)
|
|
294
|
+
blocks.append("文字清晰可读,文字参与构图,不要出现多余文字。")
|
|
295
|
+
elif ref:
|
|
296
|
+
# 只上传参考图、不选图型风格主题色(用户 2026-11-08 要求):没有文案可排,
|
|
297
|
+
# 就让参考图当唯一依据,重新组织构图出一张完整画面,别把原图原样复刻
|
|
298
|
+
blocks.append(
|
|
299
|
+
"没有文案:画面主体与画风都取自参考图,重新组织构图、环境与动作,"
|
|
300
|
+
"画一张完整的插画,画面里不排文字。"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
tail = []
|
|
304
|
+
if moods:
|
|
305
|
+
tail.append("情绪:" + "、".join(moods))
|
|
306
|
+
if color:
|
|
307
|
+
tail.append(f"{color.get('name') or ''}作统一色调点缀")
|
|
308
|
+
if ratio:
|
|
309
|
+
tail.append(f"画幅比例 {ratio}")
|
|
310
|
+
tail = [t for t in tail if t and t.strip(":")]
|
|
311
|
+
if tail:
|
|
312
|
+
blocks.append("。".join(tail) + "。")
|
|
313
|
+
|
|
314
|
+
if str(ratio or "").strip() == WIDE_CROP_RATIO:
|
|
315
|
+
blocks.append(WIDE_CROP_HINT)
|
|
316
|
+
|
|
317
|
+
if whitespace in WHITESPACE:
|
|
318
|
+
blocks.append(WHITESPACE[whitespace])
|
|
319
|
+
|
|
320
|
+
blocks.append(TAIL_INSTRUCTION)
|
|
321
|
+
return "\n".join(b for b in blocks if b)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def compose(payload: dict, styles: list[dict], layouts: list[dict], colors: list[dict]) -> dict:
|
|
325
|
+
"""组装全部条目。payload 里 items 为空时用 topic 兜底生成一条。"""
|
|
326
|
+
|
|
327
|
+
def pick(items, item_id):
|
|
328
|
+
if not item_id:
|
|
329
|
+
return None
|
|
330
|
+
return next((x for x in items if x["id"] == item_id), None)
|
|
331
|
+
|
|
332
|
+
sel = {
|
|
333
|
+
"style": pick(styles, payload.get("style_id")),
|
|
334
|
+
"layout": pick(layouts, payload.get("layout_id")),
|
|
335
|
+
"color": pick(colors, payload.get("color_id")),
|
|
336
|
+
}
|
|
337
|
+
ratio = (payload.get("ratio") or "").strip()
|
|
338
|
+
moods = parse_moods(payload.get("mood"))
|
|
339
|
+
fusion = bool(payload.get("fusion"))
|
|
340
|
+
whitespace = (payload.get("whitespace") or "normal").strip()
|
|
341
|
+
note = (payload.get("note") or "").strip()
|
|
342
|
+
ref = bool(payload.get("ref"))
|
|
343
|
+
# 出图模式:默认图文,和以前完全一样;只有显式传 photo 才走摄影分支
|
|
344
|
+
image_mode = (payload.get("image_mode") or "graphic-text").strip()
|
|
345
|
+
if image_mode not in PHOTO_MODES:
|
|
346
|
+
image_mode = "graphic-text"
|
|
347
|
+
shoot = (payload.get("shoot_type") or "").strip()[:40] if image_mode == "photo" else ""
|
|
348
|
+
# allow_blank:左侧没写标题/正文时也允许出提示词(纯指令出图);instruction:手改指令
|
|
349
|
+
allow_blank = bool(payload.get("allow_blank"))
|
|
350
|
+
custom_instr = (payload.get("instruction") or "").strip()
|
|
351
|
+
|
|
352
|
+
raw_items = payload.get("items") or []
|
|
353
|
+
items = []
|
|
354
|
+
for raw in raw_items:
|
|
355
|
+
if not isinstance(raw, dict):
|
|
356
|
+
continue
|
|
357
|
+
title = (raw.get("title") or "").strip()
|
|
358
|
+
body = (raw.get("body") or "").strip()
|
|
359
|
+
scene = (raw.get("scene") or "").strip()
|
|
360
|
+
if not title and not body and not allow_blank:
|
|
361
|
+
continue
|
|
362
|
+
items.append({"title": title, "body": body, "scene": scene})
|
|
363
|
+
|
|
364
|
+
if not items:
|
|
365
|
+
topic = (payload.get("topic") or "").strip()
|
|
366
|
+
if topic:
|
|
367
|
+
lines = topic.split("\n", 1)
|
|
368
|
+
items.append({"title": lines[0].strip(), "body": lines[1].strip() if len(lines) > 1 else "", "scene": ""})
|
|
369
|
+
|
|
370
|
+
instruction = custom_instr or build_instruction(
|
|
371
|
+
sel, ratio, moods, fusion, whitespace, ref, image_mode, shoot)
|
|
372
|
+
out_items = []
|
|
373
|
+
for idx, item in enumerate(items, 1):
|
|
374
|
+
out_items.append({
|
|
375
|
+
"index": idx,
|
|
376
|
+
"title": item["title"],
|
|
377
|
+
"body": item["body"],
|
|
378
|
+
"scene": item.get("scene", ""),
|
|
379
|
+
"instruction": instruction,
|
|
380
|
+
"prompt": build_prompt(item, sel, ratio, moods, fusion, whitespace, note, ref,
|
|
381
|
+
image_mode, shoot, instr=custom_instr),
|
|
382
|
+
})
|
|
383
|
+
return {
|
|
384
|
+
"instruction": instruction,
|
|
385
|
+
"items": out_items,
|
|
386
|
+
"selection": {
|
|
387
|
+
"style": sel["style"]["id"] if sel["style"] else None,
|
|
388
|
+
"layout": sel["layout"]["id"] if sel["layout"] else None,
|
|
389
|
+
"color": sel["color"]["id"] if sel["color"] else None,
|
|
390
|
+
},
|
|
391
|
+
}
|