# 重新测试锚点注入
cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && venv/bin/python3 -c "
import json, sys
sys.path.insert(0, '.')
from user_driven_task_system import user_driven_task_system

script_result = {
    'characters': [
        {'name': '关羽', 'gender': 'male', 'distinctive_features': '三缕长髯', 'outfit_style': '绿袍战甲', 'appearance': '面如重枣，三缕长髯，身穿绿色战袍'},
        {'name': '林冲', 'gender': 'male', 'distinctive_features': '白色毡笠', 'outfit_style': '玄色劲装', 'appearance': '豹头环眼，白色毡笠'}
    ],
    'scenes': [
        {
            'scene_id': 1,
            'shots': [
                {'shot_id': 'S1-1', 'description': '关羽与林冲对峙', 'dialogue_character': '关羽', 'image_prompt': '关羽站在荒野中，电影写实风格，8K', 'video_prompt': '关羽站在荒野中'},
                {'shot_id': 'S1-2', 'description': '远处风景', 'dialogue_character': '', 'image_prompt': '荒野战场，8K', 'video_prompt': '荒野战场'}
            ],
            'image_prompt': '关羽在荒野战场'
        }
    ]
}

user_driven_task_system._inject_character_anchors(script_result)
print('S1-1 image_prompt:', script_result['scenes'][0]['shots'][0]['image_prompt'])
print('S1-1 video_prompt:', script_result['scenes'][0]['shots'][0]['video_prompt'])
print('Scene image_prompt:', script_result['scenes'][0]['image_prompt'])
print('Injection:', json.dumps(script_result.get('_anchor_injection', {}), ensure_ascii=False))
" 2>&1


S1-1 image_prompt: 林冲(男性)，白色毡笠，玄色劲装，关羽(男性)，三缕长髯，绿袍战甲站在荒野中，电影写实风格，8K
S1-1 video_prompt: 关羽(男性)，三缕长髯，绿袍战甲站在荒野中
Scene image_prompt: 关羽(男性)，三缕长髯，绿袍战甲在荒野战场
Injection: {"version": "v3.15", "anchors_defined": 2, "shots_injected": 2, "anchor_map": {"关羽": "关羽(男性)，三缕长髯，绿袍战甲", "林冲": "林冲(男性)，白色毡笠，玄色劲装"}}


S1-1的image_prompt还是有点问题——林冲的锚点加在了开头，关羽的锚点替换了"关羽"。这个效果其实是可以接受的：两个角色都出现在镜头中，各自的锚点特征都包含了。不过对于image_prompt来说，开头加锚点列表可能不是最优方式。让我调整一下，只在角色名出现的位置替换，不在开头额外添加：

edit

                # 注入锚点到image_prompt(从最长名开始替换，避免短名匹配子串)
                # 只替换prompt中已有的角色名，不额外在开头添加(避免污染prompt结构)
                for name, anchor in sorted(involved_chars, key=lambda x: -len(x[0])):
                    if anchor not in ip:
                        if name in ip:
                            ip = ip.replace(name, anchor, 1)
                            injected_count += 1
                        # v3.15: 角色名不在prompt中，在开头用“角色锚点，”前缀注入
                        # 仅当角色在对话中出场时才注入(确保视觉一致性)
                        elif name in dialogue_char:
                            ip = f"{anchor}，" + ip
                            injected_count += 1
                    # 同样处理video_prompt
                    if anchor not in vp:
                        if name in vp:
                            vp = vp.replace(name, anchor, 1)

                # 注入锚点到image_prompt(从最长名开始替换，避免短名匹配子串)
                for name, anchor in sorted(involved_chars, key=lambda x: -len(x[0])):
                    if anchor not in ip:
                        if name in ip:
                            ip = ip.replace(name, anchor, 1)
                        else:
                            # 角色名不在prompt中但在description中，在开头注入
                            ip = f"{anchor}，" + ip
                        injected_count += 1
                    # 同样处理video_prompt
                    if anchor not in vp:
                        if name in vp:
                            vp = vp.replace(name, anchor, 1)


Successfully replaced 1 block(s) in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py.

      ...
 3306                     if name in desc or name in dialogue_char or name in ip:
 3307                         involved_chars.append((name, anchor))
 3308
 3309                 # 注入锚点到image_prompt(从最长名开始替换，避免短名匹配子串)
+3310                 # 只替换prompt中已有的角色名，不额外在开头添加(避免污染prompt结构)
 3310                 for name, anchor in sorted(involved_chars, key=lambda x: -len(x[0])):
 3311                     if anchor not in ip:
 3312                         if name in ip:
 3313                             ip = ip.replace(name, anchor, 1)
-3314                         else:
-3315                             # 角色名不在prompt中但在description中，在开头注入
+3315                             injected_count += 1
+3316                         # v3.15: 角色名不在prompt中，在开头用“角色锚点，”前缀注入
+3317                         # 仅当角色在对话中出场时才注入(确保视觉一致性)
+3318                         elif name in dialogue_char:
 3316                             ip = f"{anchor}，" + ip
-3317                         injected_count += 1
+3320                             injected_count += 1
 3318                     # 同样处理video_prompt
 3319                     if anchor not in vp:
 3320                         if name in vp:
 3321                             vp = vp.replace(name, anchor, 1)
      ...


exec

# 最终编译检查 + 重启
cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && python3 -m py_compile user_driven_task_system.py && echo "PY_COMPILE_OK" || echo "PY_COMPILE_FAIL"


systemctl --user restart manhua-studio-8000 && sleep 2 && systemctl --user is-active manhua-studio-8000 && curl -s -o /dev/null -w "%{http_code}" -L http://127.0.0.1:8000/


active
200


# 最终验证锚点注入
cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && venv/bin/python3 -c "
import json, sys
sys.path.insert(0, '.')
from user_driven_task_system import user_driven_task_system

script_result = {
    'characters': [
        {'name': '关羽', 'gender': 'male', 'distinctive_features': '三缕长髯', 'outfit_style': '绿袍战甲', 'appearance': '面如重枣，三缕长髯'},
        {'name': '林冲', 'gender': 'male', 'distinctive_features': '白色毡笠', 'outfit_style': '玄色劲装', 'appearance': '豹头环眼'}
    ],
    'scenes': [{
        'scene_id': 1,
        'shots': [
            {'shot_id': 'S1-1', 'description': '关羽与林冲对峙', 'dialogue_character': '关羽', 'image_prompt': '关羽站在荒野中，8K', 'video_prompt': '关羽站在荒野中'},
            {'shot_id': 'S1-2', 'description': '林冲怒视前方', 'dialogue_character': '林冲', 'image_prompt': '白衣将军怒视，8K', 'video_prompt': '白衣将军怒视前方'}
        ],
        'image_prompt': '关羽在荒野战场'
    }]
}
user_driven_task_system._inject_character_anchors(script_result)
for s in script_result['scenes']:
    for sh in s['shots']:
        print(f\"{sh['shot_id']} ip: {sh['image_prompt']}\")
    print(f\"scene ip: {s['image_prompt']}\")
" 2>&1


S1-1 ip: 关羽(男性)，三缕长髯，绿袍战甲站在荒野中，8K
S1-2 ip: 林冲(男性)，白色毡笠，玄色劲装，白衣将军怒视，8K
scene ip: 关羽(男性)，三缕长髯，绿袍战甲在荒野战场


锚点注入效果正确：
- S1-1: "关羽" → "关羽(男性)，三缕长髯，绿袍战甲" ✅
- S1-2: 林冲在dialogue中但不在prompt中 → 开头注入锚点 ✅
- Scene: "关羽" → "关羽(男性)，三缕长髯，绿袍战甲" ✅

现在更新记忆：


Successfully replaced 1 block(s) in /root/.openclaw/workspace/MEMORY.md.

     ...
  71 - 根路径(`/`)必须返回HTML或重定向，不能返回JSON
  72 - 图片/视频必须有前端可访问的URL路径(`/api/files`)
  73 - API部署前: `curl -s -L http://127.0.0.1:port/ | grep "DOCTYPE html"`
  74
- 75 **功能自测规范(v3.14 2026-05-19)**:
+ 75 **功能自测规范(v3.15 2026-05-20)**:
  76 - 每次修改后必须验证，不是可选的，是强制的
  77 - 自测三步法: ①语法编译 ②API curl测试 ③浏览器端到端
  78 - 改了哪个API，就必须curl测那个API的返回值(不只是HTTP状态码)
  79 - 改了前端JS/HTML，必须浏览器打开看实际效果
     ...


cat >> /root/.openclaw/workspace/memory/2026-05-20.md << 'EOF'

## v3.15 升级完成 (09:53-10:12)

基于Main Agent对task_20260519_195958_aedcc473的质量评估报告，执行P0-P4五项系统升级：

### P0: QA自检扩充 (4项→9项)
新增5项检查:
- 开场5秒冲突强度检查 (conflict_keywords匹配)
- 角色言行与人设一致性检查 (dialogue_character在角色列表+speech_style)
- 结尾钩子/留白检查 (ending_keywords匹配最后镜头+场景)
- 总时长准确性检查 (duration_seconds求和vs目标时长, ±3秒)
- 反派角色完整性检查 (variants≥2, appearance≥50字, distinctive_features)

### P1: 角色一致性锚点自动注入
- 新方法 `_inject_character_anchors()`: 提取角色distinctive_features+outfit_style构建锚点
- 在Phase2脚本解析后、图片生成前自动注入到image_prompt和video_prompt
- 从最长角色名开始替换，避免短名子串匹配
- 仅当角色在dialogue中出场时才额外开头注入(不污染prompt结构)
- 注入结果记录在 `_anchor_injection` 字段

### P2: Phase1 prompt增加反派角色约束
- 重要约束第6条: "如果剧情涉及对抗/冲突，必须包含至少1个反派角色，反派也必须有完整的variants(≥2)和character_arc"

### P3: Phase2 prompt增加结尾钩子模板化
- 重要约束第11条: "最后一个scene必须包含结尾钩子(悬念/预告/留白)，在最后一个shot中添加hook_type字段(cliffhanger/teaser/open_ending)"
- 重要约束第12条: "总时长中预留2-3秒给结尾留白/字幕"

### P4: 前端QA报告展示优化
- displayQAReport()升级: 9项检查结果用彩色badge卡片展示(绿色通过/红色失败)
- 显示QA版本号(v3.15)
- 显示锚点注入统计(角色数+镜头数)

### 自测结果
1. ✅ py_compile通过
2. ✅ systemctl restart active
3. ✅ curl 200
4. ✅ QA 9项检查功能测试通过(模拟数据正确检测时长偏差+反派不完整)
5. ✅ 锚点注入功能测试通过(角色名正确替换为锚点特征)
EOF


(no output)


Explanation: 全部完成

确认当前版本号，确定v3.15

P0: QA自检扩充(5项新检查)

P1: 角色一致性锚点自动注入

P4: 前端QA报告展示优化

Explanation: 全部完成

确认当前版本号，确定v3.15

P0: QA自检扩充(5项新检查)

P1: 角色一致性锚点自动注入

P4: 前端QA报告展示优化


Explanation: 全部完成

确认当前版本号，确定v3.15

P0: QA自检扩充(5项新检查)

P1: 角色一致性锚点自动注入

P4: 前端QA报告展示优化


## ✅ v3.15 升级完成

基于质量评估报告的五项改造全部落地：

| 改造项 | 内容 | 验证 |
|--------|------|------|
| **P0** | QA自检 4项→9项（开场冲突、角色人设、结尾钩子、时长准确性、反派完整性） | ✅ 模拟数据正确检测出时长偏差+反派档案不完整 |
| **P1** | `_inject_character_anchors()` 角色锚点自动注入到image/video_prompt | ✅ "关羽"→"关羽(男性)，三缕长髯，绿袍战甲" |
| **P2** | Phase1 prompt: 反派角色必须完整档案 | ✅ 已写入约束 |
| **P3** | Phase2 prompt: 结尾钩子hook_type + 预留2-3秒 | ✅ 已写入约束 |
| **P4** | 前端QA报告：9项彩色badge卡片 + 锚点统计 | ✅ JS已更新 |

**自测结果**: py_compile ✅ → restart active ✅ → HTTP 200 ✅ → 功能测试 ✅


[OpenClaw heartbeat poll]


System (untrusted): [2026-05-20 10:03:09 GMT+8] Zod模块健康检查通过，模块状态正常，无需修复，不发送通知。

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 10:13 AM (Asia/Shanghai)
Reference UTC: 2026-05-20 02:13 UTC


Quick health check per HEARTBEAT.md guidelines. The system notification says Zod module is healthy. Let me do a quick check on key services.

systemctl --user is-active openclaw-gateway && systemctl --user is-active manhua-studio-8000 && df -h / | awk 'NR==2{print $5}'


active
active
71%


System (untrusted): [2026-05-20 10:30:12 GMT+8] zod模块健康检查通过，状态正常，无需修复。无需推送通知。

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 10:43 AM (Asia/Shanghai)
Reference UTC: 2026-05-20 02:43 UTC


The heartbeat poll is asking me to check HEARTBEAT.md and follow it. The system message says zod module health check passed. Let me do a quick status check.


[Wed 2026-05-20 11:07 GMT+8] 参考CTO行为规范，排查一下任务task_20260520_103854_5cfb9a96问题，
1、场景图应该是无人的，为什么生成的有人
2、3个角色的设计图 明显风格不一样是为什么


System (untrusted): [2026-05-20 11:00:28 GMT+8] Zod模块健康检查通过，状态正常，无需修复。


用户要求我排查两个问题：
1. 场景图应该无人的，为什么生成的有人
2. 3个角色的设计图风格不一样

让我先查看任务数据和代码逻辑。

# 查看任务数据
cat /root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/task.json 2>/dev/null | python3 -c "
import json, sys
d = json.load(sys.stdin)
print('主题:', d.get('params', {}).get('description', 'N/A'))
print('时长:', d.get('params', {}).get('duration', 'N/A'))
print('状态:', d.get('status'), '步骤:', d.get('current_step'))
print('write_phase:', d.get('write_phase'))
print()
# 角色信息
chars = d.get('results', {}).get('script', {}).get('characters', [])
print(f'角色数: {len(chars)}')
for c in chars:
    print(f\"  {c.get('name','?')} | gender={c.get('gender','?')} | img={c.get('image_path','无')}\")
    print(f\"    distinctive: {c.get('distinctive_features','无')[:60]}\")
    print(f\"    image_prompt: {c.get('image_prompt','无')[:100]}\")
print()
# 场景信息
scenes = d.get('results', {}).get('script', {}).get('scenes', [])
print(f'场景数: {len(scenes)}')
for s in scenes:
    print(f\"  Scene {s.get('scene_id','?')}: {s.get('location','?')}\")
    print(f\"    image_prompt: {s.get('image_prompt','无')[:150]}\")
    print(f\"    image_path: {s.get('image_path','无')}\")
" 2>&1


主题: N/A
时长: N/A
状态: writing 步骤: write
write_phase: 2

角色数: 3
  木尔 | gender=male | img=/root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245347572_0_8658.jpeg
    distinctive: 额头正中的浅疤，永远挂在腰间的铜制凿井标尺
    image_prompt: 暖色调+历史感,神秘+震撼+教育性,参考《国家宝藏》《如果国宝会说话》风格风格,角色三视图设计稿,木尔(male角色),标志性特征:额头正中的浅疤，永远挂在腰间的铜制凿井标尺,身高178cm，肩宽背厚
  林晓 | gender=female | img=/root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245356009_0_2143.jpeg
    distinctive: 眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机
    image_prompt: 暖色调+历史感,神秘+震撼+教育性,参考《国家宝藏》《如果国宝会说话》风格风格,角色三视图设计稿,林晓(female角色),标志性特征:眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机,身高165cm，身
  小安 | gender=male | img=/root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245397248_0_6452.jpeg
    distinctive: 左脸的梨涡，左脚踝的胡杨枝刺青
    image_prompt: 暖色调+历史感,神秘+震撼+教育性,参考《国家宝藏》《如果国宝会说话》风格风格,角色三视图设计稿,小安(male角色),标志性特征:左脸的梨涡，左脚踝的胡杨枝刺青,身高172cm，身形偏瘦但结实，小麦

场景数: 7
  Scene 1: 新疆吐鲁番戈壁荒漠
    image_prompt: 【现代】正午新疆吐鲁番戈壁荒漠全景，无垠戈壁滩延伸至远方火焰山，枯胡杨木横卧碎石间，明渠露头清水泛光，鸟瞰视角，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文汉字水印
    image_path: /root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245467537_0_7379.jpeg
  Scene 2: 坎儿井地下暗渠入口
    image_prompt: 【现代】午后坎儿井地下暗渠内部，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风（女性，卡其色工装）蹲下身触摸石壁凿痕，侧光束透入，地下水流淌，木质引水槽，中景，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文
    image_path: /root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245467639_1_9148.jpeg
  Scene 3: 汉代西域屯垦营地外的戈壁
    image_prompt: 【汉代】黄昏戈壁营地，黄沙漫天，木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮（男性，古铜色皮肤粗麻褐衣）持青铜凿子立于群像前，小安扛镐头立旁，低角度仰拍，黄昏逆光剪影，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中
    image_path: /root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779246321800_0_1670.jpeg
  Scene 4: 汉代坎儿井地下凿井现场
    image_prompt: 【汉代】深夜地下凿井现场，木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮持青铜凿子凿石，石壁渗水，小安递锤，陶制油灯摇曳，特写，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文汉字水印
    image_path: /root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245537645_0_5693.jpeg
  Scene 5: 汉代坎儿井明渠边
    image_prompt: 【汉代】清晨明渠边，木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮举水瓢畅饮笑容爽朗，小安光脚踩水泼洒，水工欢呼，渠水奔涌金光，全景，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文汉字水印
    image_path: /root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245537739_1_1797.jpeg
  Scene 6: 现代坎儿井文化广场
    image_prompt: 【现代】坎儿井文化广场，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风（女性，艾德莱斯绸长裙）站在遗产石碑前面对镜头，身后游客示意图展板，中景，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文汉字水印
    image_path: /root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245537826_2_9033.jpeg
  Scene 6: 现代坎儿井文化广场
    image_prompt: 【现代】坎儿井文化广场远景，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风身影渐小，镜头后拉升起，广场戈壁火焰山入画，暖黄复古色调，鸟瞰，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文汉字水印
    image_path: /root/data/disk/projects/tasks/task_20260520_103854_5cfb9a96/images/image_1779245568815_0_8884.jpeg


根因已经明显了。让我再看下代码中场景图生成的具体逻辑：

# 查看场景图生成的核心代码
grep -n "_generate_scene_images\|def.*scene.*image" /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py | head -10


679:    def _generate_scene_images_batch(self, scenes: list, images_dir: Path, characters: list = None, style_prefix: str = "", image_model: str = None) -> list:
781:    def _generate_scene_images(self, scenes: list, images_dir: Path, characters: list = None, style_prefix: str = "", image_model: str = None) -> list:
829:    def _generate_shot_ref_images(self, scenes: list, images_dir: Path, characters: list = None, image_model: str = None, style_prefix: str = "") -> list:
1198:                scenes = self._generate_scene_images_batch(scenes, images_dir, characters=characters, style_prefix=style_prefix, image_model=_img_model)
1536:                scenes = self._generate_scene_images_batch(scenes, images_dir, characters=characters, style_prefix=style_prefix, image_model=_img_model)
3028:            scenes = self._generate_scene_images(scenes, images_dir, characters=characters, image_model=task_data.get("image_model"))


def _generate_scene_images_batch(self, scenes: list, images_dir: Path, characters: list = None, style_prefix: str = "", image_model: str = None) -> list:
        """为场景列表批量生成背景图 - 优先使用组图API(高效)，回退逐张生成
        v3.14.2: 修复组图API视觉趋同问题
        - 组图API要求prompt明确描述每张图差异(官方示例: "生成3张...涵盖早晨、中午、晚上")
        - prompt建议不超过300汉字，过长则信息分散
        - 策略: 分批(每批≤3张)，每批用结构化prompt明确编号+差异点
        - 使用scene自身的image_prompt而非简化版纯环境背景图
        """
        if not style_prefix:
            style_prefix = "日系动漫风格,色彩鲜艳,线条清晰,"

        # 构建场景提示词列表 - 使用scene自身的image_prompt(更丰富)
        scene_prompts = []
        for i, scene in enumerate(scenes):
            img_prompt = scene.get("image_prompt", "")
            if not img_prompt:
                location = scene.get("location", "") or scene.get("description", "")[:30]
                if not location:
                    scene["image_path"] = None
                    continue
                img_prompt = f"{style_prefix}{location},电影感,氛围感,9:16竖屏"
            scene_prompts.append((i, scene, img_prompt))

        if not scene_prompts:
            return scenes

        # v3.14.2: 分批使用组图API(每批≤3张，避免prompt过长导致信息分散)
        BATCH_SIZE = 3
        all_batches = [scene_prompts[j:j+BATCH_SIZE] for j in range(0, len(scene_prompts), BATCH_SIZE)]

        for batch_idx, batch in enumerate(all_batches):
            batch_generated = False

            if len(batch) >= 2:
                # 尝试组图API
                try:
                    # 构建结构化prompt: 明确编号+核心差异点(每张控制在~80字)
                    scene_descs = []
                    for k, (i, scene, _) in enumerate(batch):
                        location = scene.get("location", "")[:15]
                        time_info = scene.get("time", "")
                        atmosphere = scene.get("atmosphere", "")[:10]
                        # 提取image_prompt的核心差异(前60字)
                        img_prompt = scene.get("image_prompt", "")
                        core_desc = img_prompt[:60] if img_prompt else location
                        desc = f"第{k+1}张({location},{time_info},{atmosphere}): {core_desc}"
                        scene_descs.append(desc)

                    combined_prompt = f"生成{len(batch)}张不同场景的图片。" + "；".join(scene_descs)
                    # 截断到300字(官方建议)
                    if len(combined_prompt) > 300:
                        combined_prompt = combined_prompt[:297] + "..."

                    logger.info(f"  组图API批次{batch_idx+1}: {len(batch)}张, prompt({len(combined_prompt)}字): {combined_prompt[:80]}...")
                    result = generate_image(
                        prompt=combined_prompt,
                        model=image_model if image_model and image_model != "auto" else None,
                        size="1440x2560",
                        download_dir=str(images_dir),
                        sequential_image_generation="auto",
                        sequential_image_generation_options={"max_images": len(batch)},
                    )
                    paths = result.get("local_paths", [])
                    if paths and len(paths) >= len(batch):
                        for k, (i, scene, _) in enumerate(batch):
                            if k < len(paths):
                                scene["image_path"] = paths[k]
                                logger.info(f"  场景图[组图]: {scene.get('location','')[:20]} -> {paths[k][-40:]}")
                            else:
                                scene["image_path"] = None
                        batch_generated = True
                        logger.info(f"  组图API批次{batch_idx+1}成功: {len(paths)}张")
                    else:
                        logger.info(f"  组图API返回{len(paths) if paths else 0}张，不足{len(batch)}张，回退逐张")
                except Exception as e:
                    logger.info(f"  组图API批次{batch_idx+1}失败({e})，回退逐张")

            # 回退: 逐张生成(单张场景或组图失败时)
            if not batch_generated:
                def _gen_one(item):
                    idx, scene, img_prompt = item
                    try:
                        result = generate_image(
                            prompt=img_prompt,
                            model=image_model if image_model and image_model != "auto" else None,
                            size="1440x2560", n=1, download_dir=str(images_dir),
                        )
                        paths = result.get("local_paths", [])
                        scene["image_path"] = paths[0] if paths else None
                        logger.info(f"  场景图[逐张]: {scene.get('location','')[:20]} -> {scene.get('image_path','N/A')[-40:]}")
                    except Exception as e:
                        logger.warning(f"  场景图生成失败: {e}")
                        scene["image_path"] = None
                    return idx, scene

                logger.info(f"  逐张并发生成批次{batch_idx+1}({len(batch)}张, max_workers=2)...")
                with ThreadPoolExecutor(max_workers=2) as pool:
                    for idx, scene in pool.map(_gen_one, batch):
                        pass

        return scenes


问题根因找到了。现在看角色图生成逻辑：


"""为角色列表生成设计图,返回带图片路径的角色列表
        v3.14.1: 并发生成(max_workers=2), 每个角色用独立prompt含gender+distinctive_features"""
        if not style_prefix and characters:
            style_prefix = "日系动漫风格,色彩鲜艳,线条清晰,"

        # 构建所有任务参数
        tasks = []
        for i, char in enumerate(characters):
            name = char.get("name", "")
            gender = char.get("gender", "")
            desc = char.get("appearance", "") or char.get("description", "")
            distinctive = char.get("distinctive_features", "")
            if not name and not desc:
                char["image_path"] = None
                continue
            # v3.14.1: 必须含性别+标志性特征
            gender_tag = f"{name}({gender}角色)," if gender else f"{name},"
            feature_tag = f"标志性特征:{distinctive}," if distinctive else ""
            img_prompt = f"{style_prefix}角色三视图设计稿,{gender_tag}{feature_tag}{desc},正面+侧面+背面三视图,全身像,清晰细节,白色背景,9:16竖屏,角色设计参考图"
            char["image_prompt"] = img_prompt  # v3.14.1: 保存prompt到角色数据
            char["three_view_prompt"] = img_prompt
            tasks.append((i, char, img_prompt))

        def _gen_one(item):
            idx, char, img_prompt = item
            try:
                result = generate_image(
                    prompt=img_prompt,
                    model=image_model if image_model and image_model != "auto" else None,
                    size="1920x1920", n=1, download_dir=str(images_dir),
                )
                paths = result.get("local_paths", [])
                char["image_path"] = paths[0] if paths else None
                logger.info(f"  角色三视图生成: {char.get('name','')} -> {char.get('image_path', 'N/A')}")
            except Exception as e:
                logger.warning(f"  角色{char.get('name','')}图片生成失败: {e}")
                char["image_path"] = None
            return idx, char

        # 并发执行(max_workers=2避免API并发限制)
        if tasks:
            logger.info(f"  并发生成{len(tasks)}个角色图(max_workers=2)...")
            with ThreadPoolExecutor(max_workers=2) as pool:
                for idx, char in pool.map(_gen_one, tasks):
                    pass  # 结果已直接写入char对象
        return characters

    def _generate_thumbnails(self, media_dir: Path):
        """v3.11: 为目录下的图片和视频生成缩略图，固化到 .thumbs/ 子目录
        图片: Pillow缩放到160x160, JPEG quality=60
        视频: ffmpeg截取第1秒帧, 缩放宽160px"""
        thumb_dir = media_dir / ".thumbs"
        thumb_dir.mkdir(parents=True, exist_ok=True)
        count = 0
        try:
            from PIL import Image
        except ImportError:
            logger.warning("Pillow未安装，跳过图片缩略图生成")
            Image = None

        for f in media_dir.iterdir():
            if not f.is_file():
                continue
            suffix = f.suffix.lower()
            thumb_name = f.stem + "_thumb" + (".jpg" if suffix in (".mp4", ".mov", ".avi", ".webm") else f.suffix)
            thumb_path = thumb_dir / thumb_name
            # 已存在且比原文件新就跳过
            if thumb_path.exists() and thumb_path.stat().st_mtime >= f.stat().st_mtime:
                continue
            try:
                if suffix in (".png", ".jpg", ".jpeg", ".webp", ".gif") and Image:
                    img = Image.open(f)
                    img.thumbnail((160, 160), Image.LANCZOS)
                    if img.mode in ("RGBA", "P"):
                        img = img.convert("RGB")
                    img.save(thumb_path, "JPEG", quality=60)
                    count += 1
                elif suffix in (".mp4", ".mov", ".avi", ".webm"):
                    import subprocess
                    result = subprocess.run(
                        ["ffmpeg", "-y", "-ss", "1", "-i", str(f),
                         "-vframes", "1", "-vf", "scale=160:-1", "-q:v", "3",
                         str(thumb_path)],
                        capture_output=True, timeout=15
                    )
                    if thumb_path.exists():
                        count += 1
            except Exception as e:
                logger.debug(f"缩略图生成跳过 {f.name}: {e}")

        if count > 0:
            logger.info(f"缩略图生成完成: {count}个 ({media_dir.name}/)")
        return count

    def _generate_scene_images_batch(self, scenes: list, images_dir: Path, characters: list = None, style_prefix: str = "", image_model: str = None) -> list:
        """为场景列表批量生成背景图 - 优先使用组图API(高效)，回退逐张生成


The user is asking me to investigate two problems with task task_20260520_103854_5cfb9a96:

1. Scene images should have no people, but generated images have people
2. The 3 character design images have obviously different styles

Let me analyze the root causes:

**Problem 1: Scene images have people**
Looking at the scene image_prompts, I can see the v3.15 anchor injection is working - it's injecting character anchors like "木尔(男性)，额头正中的浅疤..." into scene image_prompts. But scene images should be pure environment/background shots without people!

Looking at the scene prompts:
- Scene 1: "无垠戈壁滩延伸至远方火焰山" - no characters mentioned, OK
- Scene 2: "林晓(女性)...蹲下身触摸石壁凿痕" - CHARACTER IN SCENE IMAGE! The v3.15 anchor injection + the original prompt both include characters
- Scene 3: "木尔(男性)...持青铜凿子立于群像前，小安扛镐头立旁" - CHARACTERS IN SCENE IMAGE!
- Scene 4-5: Same issue
- Scene 6: "林晓(女性)...站在遗产石碑前面对镜头" - CHARACTER IN SCENE IMAGE!

So the root cause is: the v3.15 anchor injection is injecting character anchors into SCENE image_prompts, when scene images should be pure backgrounds. But wait, looking more carefully at the scene image_prompts, the characters were already in the original prompts BEFORE anchor injection - these are the original scene descriptions which include characters. The anchor injection just made it worse by adding more character details.

Actually, the real issue is that the LLM-generated scene image_prompts already contain character descriptions. The scenes' own image_prompt field includes characters doing things in the scene. But scene images should be pure background/environment shots without people.

This is a prompt design issue - the Phase2 script prompt tells the LLM to generate scene image_prompts but doesn't specify that they should be pure environment shots without characters.

Wait, let me re-read the code. In the previous v3.14 fix, there was a note about this:
"场景图优先使用scene自身的image_prompt（含人物、动作、氛围、光影），不再用简化的'纯环境背景图'prompt"

Hmm, but the memory says:
"v3.14.2: 修复组图API视觉趋同问题 - 使用scene自身的image_prompt而非简化版纯环境背景图"

So the design choice was to use the full image_prompt (including characters) for scene images. But the user is saying scene images should have no people. This is a conflict.

**Problem 2: Character styles are different**

Looking at the character image prompts:
- 木尔: "暖色调+历史感,神秘+震撼+教育性,参考《国家宝藏》《如果国宝会说话》风格风格,角色三视图设计稿,木尔(male角色)..."
- 林晓: "暖色调+历史感,神秘+震撼+教育性,参考《国家宝藏》《如果国宝会说话》风格风格,角色三视图设计稿,林晓(female角色)..."
- 小安: "暖色调+历史感,神秘+震撼+教育性,参考《国家宝藏》《如果国宝会说话》风格风格,角色三视图设计稿,小安(male角色)..."

They all have the same style_prefix! But wait, let me check... the style_prefix comes from `_build_style_prefix_from_collection`. The problem is that each character image is generated independently with an API call, and AI image models don't guarantee style consistency across different calls even with the same prompt.

The core issue is that:
1. Each character is generated as a separate API call
2. Even with the same style prefix, the AI model will produce different artistic styles for each
3. There's no reference image (image-to-image) being used to enforce consistency

This is a known limitation of AI image generation - you can't guarantee style consistency without a reference image.

Let me also check the style_prefix that was used vs. what's in the `_build_style_prefix_from_collection` method.

The key issues to report to the user:

**Problem 1 Root Cause**:
- v3.15's `_inject_character_anchors()` injects character anchors into scene image_prompts, but scene images should be pure environment backgrounds without characters
- Even before v3.15, the LLM-generated scene image_prompts already contained character descriptions (e.g., "林晓蹲下身触摸石壁" in Scene 2)
- The v3.14 decision to use scene's full image_prompt (including characters) instead of simplified "纯环境背景图" was a design tradeoff that introduces this problem

**Problem 2 Root Cause**:
- Each character image is generated as an independent API call
- AI image models don't guarantee style consistency across separate calls
- No reference image (image-to-image) is used to enforce style consistency
- The style_prefix is the same but that alone can't enforce consistent art style

# 查看style_prefix构建逻辑
grep -n "_build_style_prefix_from_collection" /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py | head -5


554:    def _build_style_prefix_from_collection(self, task_data: dict) -> str:
1185:            style_prefix = self._build_style_prefix_from_collection(task_data)
1251:        style_prefix = self._build_style_prefix_from_collection(task_data)
1521:            style_prefix = self._build_style_prefix_from_collection(task_data)
2330:        _style_prefix = self._build_style_prefix_from_collection(task_data)


def _build_style_prefix_from_collection(self, task_data: dict) -> str:
        """从采集结果的visual_style动态构建图片风格前缀
        如果采集分析有视觉风格建议,用它;否则用默认的日系动漫风格"""
        collection = task_data.get("results", {}).get("collection", {})
        # collection可能是dict(有parsed字段)或str
        visual_style = None
        if isinstance(collection, dict):
            parsed = collection.get("parsed", {})
            visual_style = parsed.get("visual_style", {}) if isinstance(parsed, dict) else None

        if visual_style and isinstance(visual_style, dict):
            color_tone = visual_style.get("color_tone", "")
            mood = visual_style.get("mood", "")
            reference = visual_style.get("reference", "")
            # 构建风格前缀
            parts = []
            if color_tone:
                parts.append(color_tone)
            if mood:
                parts.append(mood)
            if reference:
                parts.append(f"参考{reference}风格")
            if parts:
                style = ",".join(parts) + ","
                logger.info(f"使用采集分析视觉风格: {style[:80]}")
                return style

        # 默认: 日系动漫风格
        return "日系动漫风格,色彩鲜艳,线条清晰,"

    def _generate_character_images(self, characters: list, images_dir: Path, style_prefix: str = "", image_model: str = None) -> list:
        """为角色列表生成设计图,返回带图片路径的角色列表


根因全部找到了。分析如下：

---

## 🔍 问题根因分析

### 问题1: 场景图不应有人物，但生成了人物

**根因**: v3.15的锚点注入把角色特征注入到了场景级的 `image_prompt` 中。

看实际数据：
- Scene 2: `image_prompt` 含 `林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套...蹲下身触摸石壁凿痕`
- Scene 3: `image_prompt` 含 `木尔(男性)，额头正中的浅疤...持青铜凿子立于群像前，小安扛镐头立旁`
- Scene 4-6: 同样包含角色描述和动作

**这不是锚点注入的bug**——锚点注入只替换已有的角色名。问题出在**Phase2的LLM生成的scene.image_prompt本身就包含了角色描述**。prompt要求场景图是"场景全景参考图"，但LLM把角色动作也写进了scene的image_prompt。

此外，代码注释说"使用scene自身的image_prompt(更丰富)"，但场景图本应是纯环境背景图，不含人物。

### 问题2: 3个角色图风格不一致

**根因**: `style_prefix` 来自采集分析的 `visual_style`，但角色图生成时每个角色是独立API调用(并发)，没有风格统一的强约束。

看实际prompt：
- 木尔: `暖色调+历史感,神秘+震撼+教育性,参考《国家宝藏》《如果国宝会说话》风格风格,角色三视图设计稿,木尔(male角色)...`
- 林晓: `暖色调+历史感,神秘+震撼+教育性,参考《国家宝藏》《如果国宝会说话》风格风格,角色三视图设计稿,林晓(female角色)...`

prompt前缀相同，但**Seedream模型对风格描述的理解不稳定**——"参考《国家宝藏》风格"这种抽象描述，模型每次生成的画风差异大。问题在于：
1. 风格前缀太抽象（"神秘+震撼+教育性"不是视觉风格指令）
2. 缺少**统一画风锚点**（如"与第一个角色相同画风"）
3. 并发生成时无法参考已生成角色的风格

---

## 🔧 修复方案

### 修复1: 场景图剥离人物描述

在`_generate_scene_images_batch`中，生成场景图前自动从scene的image_prompt中移除角色相关描述，只保留环境/氛围/光影信息：

edit

    def _generate_scene_images_batch(self, scenes: list, images_dir: Path, characters: list = None, style_prefix: str = "", image_model: str = None) -> list:
        """为场景列表批量生成背景图 - 优先使用组图API(高效)，回退逐张生成
        v3.14.2: 修复组图API视觉趋同问题
        - 组图API要求prompt明确描述每张图差异(官方示例: "生成3张...涵盖早晨、中午、晚上")
        - prompt建议不超过300汉字，过长则信息分散
        - 策略: 分批(每批≤3张)，每批用结构化prompt明确编号+差异点
        - 使用scene自身的image_prompt而非简化版纯环境背景图
        """
        if not style_prefix:
            style_prefix = "日系动漫风格,色彩鲜艳,线条清晰,"

        # 构建场景提示词列表 - 使用scene自身的image_prompt(更丰富)
        scene_prompts = []
        for i, scene in enumerate(scenes):
            img_prompt = scene.get("image_prompt", "")
            if not img_prompt:
                location = scene.get("location", "") or scene.get("description", "")[:30]
                if not location:
                    scene["image_path"] = None
                    continue
                img_prompt = f"{style_prefix}{location},电影感,氛围感,9:16竖屏"
            scene_prompts.append((i, scene, img_prompt))

    def _generate_scene_images_batch(self, scenes: list, images_dir: Path, characters: list = None, style_prefix: str = "", image_model: str = None) -> list:
        """为场景列表批量生成背景图 - 优先使用组图API(高效)，回退逐张生成
        v3.15: 场景图强制剥离人物描述，确保纯环境背景图
        v3.14.2: 修复组图API视觉趋同问题
        - 组图API要求prompt明确描述每张图差异(官方示例: "生成3张...涵盖早晨、中午、晚上")
        - prompt建议不超过300汉字，过长则信息分散
        - 策略: 分批(每批≤3张)，每批用结构化prompt明确编号+差异点
        """
        if not style_prefix:
            style_prefix = "日系动漫风格,色彩鲜艳,线条清晰,"

        # v3.15: 构建场景提示词列表 - 从scene的image_prompt中剥离人物描述
        # 场景图应该是纯环境背景图，不含角色人物
        scene_prompts = []
        for i, scene in enumerate(scenes):
            img_prompt = scene.get("image_prompt", "")
            if img_prompt and characters:
                # v3.15: 从场景图prompt中移除角色名及其描述
                for char in characters:
                    name = char.get("name", "")
                    if name and name in img_prompt:
                        # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮")
                        # 使用正则匹配: 角色名(性别)，...，直到遇到下一个独立描述段
                        import re
                        # 匹配模式: 角色名(性别)，一系列描述，直到句号或逗号+新描述段
                        pattern = re.escape(name) + r"\([^)]*\)[，,][^，,。；]*(?:[，,][^，,。；]*)*"
                        img_prompt = re.sub(pattern, "", img_prompt)
                # 清理多余逗号和空格
                img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
                # 确保包含"纯环境背景图，无人物"的约束
                if "无人物" not in img_prompt and "无人" not in img_prompt:
                    img_prompt += "，纯环境背景图，无人物无角色"
                scene["image_prompt_scene"] = img_prompt  # 保存场景专用prompt(不覆盖原始)

            if not img_prompt:
                location = scene.get("location", "") or scene.get("description", "")[:30]
                if not location:
                    scene["image_path"] = None
                    continue
                img_prompt = f"{style_prefix}{location},纯环境背景图,无人物,电影感,氛围感,9:16竖屏"
            scene_prompts.append((i, scene, img_prompt))


      ...
  677         return count
  678
  679     def _generate_scene_images_batch(self, scenes: list, images_dir: Path, characters: list = None, style_prefix: str = "", image_model: str = None) -> list:
  680         """为场景列表批量生成背景图 - 优先使用组图API(高效)，回退逐张生成
+ 681         v3.15: 场景图强制剥离人物描述，确保纯环境背景图
  681         v3.14.2: 修复组图API视觉趋同问题
  682         - 组图API要求prompt明确描述每张图差异(官方示例: "生成3张...涵盖早晨、中午、晚上")
  683         - prompt建议不超过300汉字，过长则信息分散
  684         - 策略: 分批(每批≤3张)，每批用结构化prompt明确编号+差异点
- 685         - 使用scene自身的image_prompt而非简化版纯环境背景图
  686         """
  687         if not style_prefix:
  688             style_prefix = "日系动漫风格,色彩鲜艳,线条清晰,"
  689
- 690         # 构建场景提示词列表 - 使用scene自身的image_prompt(更丰富)
+ 690         # v3.15: 构建场景提示词列表 - 从scene的image_prompt中剥离人物描述
+ 691         # 场景图应该是纯环境背景图，不含角色人物
  691         scene_prompts = []
  692         for i, scene in enumerate(scenes):
  693             img_prompt = scene.get("image_prompt", "")
+ 695             if img_prompt and characters:
+ 696                 # v3.15: 从场景图prompt中移除角色名及其描述
+ 697                 for char in characters:
+ 698                     name = char.get("name", "")
+ 699                     if name and name in img_prompt:
+ 700                         # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮")
+ 701                         # 使用正则匹配: 角色名(性别)，...，直到遇到下一个独立描述段
+ 702                         import re
+ 703                         # 匹配模式: 角色名(性别)，一系列描述，直到句号或逗号+新描述段
+ 704                         pattern = re.escape(name) + r"\([^)]*\)[，,][^，,。；]*(?:[，,][^，,。；]*)*"
+ 705                         img_prompt = re.sub(pattern, "", img_prompt)
+ 706                 # 清理多余逗号和空格
+ 707                 img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
+ 708                 # 确保包含"纯环境背景图，无人物"的约束
+ 709                 if "无人物" not in img_prompt and "无人" not in img_prompt:
+ 710                     img_prompt += "，纯环境背景图，无人物无角色"
+ 711                 scene["image_prompt_scene"] = img_prompt  # 保存场景专用prompt(不覆盖原始)
+ 712
  694             if not img_prompt:
  695                 location = scene.get("location", "") or scene.get("description", "")[:30]
  696                 if not location:
  697                     scene["image_path"] = None
  698                     continue
- 699                 img_prompt = f"{style_prefix}{location},电影感,氛围感,9:16竖屏"
+ 718                 img_prompt = f"{style_prefix}{location},纯环境背景图,无人物,电影感,氛围感,9:16竖屏"
  700             scene_prompts.append((i, scene, img_prompt))
  701
  702         if not scene_prompts:
  703             return scenes
      ...


### 修复2: 角色图风格统一——串行生成 + 风格参考

    def _generate_character_images(self, characters: list, images_dir: Path, style_prefix: str = "", image_model: str = None) -> list:
        """为角色列表生成设计图,返回带图片路径的角色列表
        v3.15: 串行生成(保证风格一致) + 后续角色参考首个角色画风
        v3.14.1: 每个角色用独立prompt含gender+distinctive_features"""
        if not style_prefix and characters:
            style_prefix = "日系动漫风格,色彩鲜艳,线条清晰,"

        # 构建所有任务参数
        tasks = []
        for i, char in enumerate(characters):
            name = char.get("name", "")
            gender = char.get("gender", "")
            desc = char.get("appearance", "") or char.get("description", "")
            distinctive = char.get("distinctive_features", "")
            if not name and not desc:
                char["image_path"] = None
                continue
            # v3.14.1: 必须含性别+标志性特征
            gender_tag = f"{name}({gender}角色)," if gender else f"{name},"
            feature_tag = f"标志性特征:{distinctive}," if distinctive else ""
            img_prompt = f"{style_prefix}角色三视图设计稿,{gender_tag}{feature_tag}{desc},正面+侧面+背面三视图,全身像,清晰细节,白色背景,9:16竖屏,角色设计参考图"
            char["image_prompt"] = img_prompt  # v3.14.1: 保存prompt到角色数据
            char["three_view_prompt"] = img_prompt
            tasks.append((i, char, img_prompt))

        # v3.15: 串行生成(而非并发)，保证风格一致
        # 第一个角色正常生成，后续角色用首个角色图作为参考图(图生图)
        first_char_image_path = None
        for task_idx, (idx, char, img_prompt) in enumerate(tasks):
            try:
                if task_idx == 0:
                    # 第一个角色: 直接生成
                    result = generate_image(
                        prompt=img_prompt,
                        model=image_model if image_model and image_model != "auto" else None,
                        size="1920x1920", n=1, download_dir=str(images_dir),
                    )
                    paths = result.get("local_paths", [])
                    char["image_path"] = paths[0] if paths else None
                    if paths:
                        first_char_image_path = paths[0]
                    logger.info(f"  角色三视图生成[首个]: {char.get('name','')} -> {char.get('image_path', 'N/A')}")
                else:
                    # v3.15: 后续角色: 用首个角色图作为参考图(图生图)，保证画风统一
                    if first_char_image_path and os.path.exists(first_char_image_path):
                        result = generate_image(
                            prompt=img_prompt + ",与第一个角色保持完全相同的画风和色彩风格",
                            model=image_model if image_model and image_model != "auto" else None,
                            size="1920x1920", n=1, download_dir=str(images_dir),
                            image=first_char_image_path,  # 参考首个角色图
                        )
                        logger.info(f"  角色三视图生成[参考首个画风]: {char.get('name','')} -> (ref: {first_char_image_path[-30:]})")
                    else:
                        # 无参考图时退回直接生成
                        result = generate_image(
                            prompt=img_prompt + ",与第一个角色保持完全相同的画风和色彩风格",
                            model=image_model if image_model and image_model != "auto" else None,
                            size="1920x1920", n=1, download_dir=str(images_dir),
                        )
                        logger.info(f"  角色三视图生成[无参考图]: {char.get('name','')}")
                    paths = result.get("local_paths", [])
                    char["image_path"] = paths[0] if paths else None
            except Exception as e:
                logger.warning(f"  角色{char.get('name','')}图片生成失败: {e}")
                char["image_path"] = None
        return characters

    def _generate_character_images(self, characters: list, images_dir: Path, style_prefix: str = "", image_model: str = None) -> list:
        """为角色列表生成设计图,返回带图片路径的角色列表
        v3.14.1: 并发生成(max_workers=2), 每个角色用独立prompt含gender+distinctive_features"""
        if not style_prefix and characters:
            style_prefix = "日系动漫风格,色彩鲜艳,线条清晰,"

        # 构建所有任务参数
        tasks = []
        for i, char in enumerate(characters):
            name = char.get("name", "")
            gender = char.get("gender", "")
            desc = char.get("appearance", "") or char.get("description", "")
            distinctive = char.get("distinctive_features", "")
            if not name and not desc:
                char["image_path"] = None
                continue
            # v3.14.1: 必须含性别+标志性特征
            gender_tag = f"{name}({gender}角色)," if gender else f"{name},"
            feature_tag = f"标志性特征:{distinctive}," if distinctive else ""
            img_prompt = f"{style_prefix}角色三视图设计稿,{gender_tag}{feature_tag}{desc},正面+侧面+背面三视图,全身像,清晰细节,白色背景,9:16竖屏,角色设计参考图"
            char["image_prompt"] = img_prompt  # v3.14.1: 保存prompt到角色数据
            char["three_view_prompt"] = img_prompt
            tasks.append((i, char, img_prompt))

        def _gen_one(item):
            idx, char, img_prompt = item
            try:
                result = generate_image(
                    prompt=img_prompt,
                    model=image_model if image_model and image_model != "auto" else None,
                    size="1920x1920", n=1, download_dir=str(images_dir),
                )
                paths = result.get("local_paths", [])
                char["image_path"] = paths[0] if paths else None
                logger.info(f"  角色三视图生成: {char.get('name','')} -> {char.get('image_path', 'N/A')}")
            except Exception as e:
                logger.warning(f"  角色{char.get('name','')}图片生成失败: {e}")
                char["image_path"] = None
            return idx, char

        # 并发执行(max_workers=2避免API并发限制)
        if tasks:
            logger.info(f"  并发生成{len(tasks)}个角色图(max_workers=2)...")
            with ThreadPoolExecutor(max_workers=2) as pool:
                for idx, char in pool.map(_gen_one, tasks):
                    pass  # 结果已直接写入char对象
        return characters


      ...
  582         return "日系动漫风格,色彩鲜艳,线条清晰,"
  583
  584     def _generate_character_images(self, characters: list, images_dir: Path, style_prefix: str = "", image_model: str = None) -> list:
  585         """为角色列表生成设计图,返回带图片路径的角色列表
- 586         v3.14.1: 并发生成(max_workers=2), 每个角色用独立prompt含gender+distinctive_features"""
+ 586         v3.15: 串行生成(保证风格一致) + 后续角色参考首个角色画风
+ 587         v3.14.1: 每个角色用独立prompt含gender+distinctive_features"""
  587         if not style_prefix and characters:
  588             style_prefix = "日系动漫风格,色彩鲜艳,线条清晰,"
  589
  590         # 构建所有任务参数
      ...
  604             char["image_prompt"] = img_prompt  # v3.14.1: 保存prompt到角色数据
  605             char["three_view_prompt"] = img_prompt
  606             tasks.append((i, char, img_prompt))
  607
- 608         def _gen_one(item):
- 609             idx, char, img_prompt = item
+ 609         # v3.15: 串行生成(而非并发)，保证风格一致
+ 610         # 第一个角色正常生成，后续角色用首个角色图作为参考图(图生图)
+ 611         first_char_image_path = None
+ 612         for task_idx, (idx, char, img_prompt) in enumerate(tasks):
  610             try:
- 611                 result = generate_image(
- 612                     prompt=img_prompt,
- 613                     model=image_model if image_model and image_model != "auto" else None,
- 614                     size="1920x1920", n=1, download_dir=str(images_dir),
- 615                 )
- 616                 paths = result.get("local_paths", [])
- 617                 char["image_path"] = paths[0] if paths else None
- 618                 logger.info(f"  角色三视图生成: {char.get('name','')} -> {char.get('image_path', 'N/A')}")
+ 614                 if task_idx == 0:
+ 615                     # 第一个角色: 直接生成
+ 616                     result = generate_image(
+ 617                         prompt=img_prompt,
+ 618                         model=image_model if image_model and image_model != "auto" else None,
+ 619                         size="1920x1920", n=1, download_dir=str(images_dir),
+ 620                     )
+ 621                     paths = result.get("local_paths", [])
+ 622                     char["image_path"] = paths[0] if paths else None
+ 623                     if paths:
+ 624                         first_char_image_path = paths[0]
+ 625                     logger.info(f"  角色三视图生成[首个]: {char.get('name','')} -> {char.get('image_path', 'N/A')}")
+ 626                 else:
+ 627                     # v3.15: 后续角色: 用首个角色图作为参考图(图生图)，保证画风统一
+ 628                     if first_char_image_path and os.path.exists(first_char_image_path):
+ 629                         result = generate_image(
+ 630                             prompt=img_prompt + ",与第一个角色保持完全相同的画风和色彩风格",
+ 631                             model=image_model if image_model and image_model != "auto" else None,
+ 632                             size="1920x1920", n=1, download_dir=str(images_dir),
+ 633                             image=first_char_image_path,  # 参考首个角色图
+ 634                         )
+ 635                         logger.info(f"  角色三视图生成[参考首个画风]: {char.get('name','')} -> (ref: {first_char_image_path[-30:]})")
+ 636                     else:
+ 637                         # 无参考图时退回直接生成
+ 638                         result = generate_image(
+ 639                             prompt=img_prompt + ",与第一个角色保持完全相同的画风和色彩风格",
+ 640                             model=image_model if image_model and image_model != "auto" else None,
+ 641                             size="1920x1920", n=1, download_dir=str(images_dir),
+ 642                         )
+ 643                         logger.info(f"  角色三视图生成[无参考图]: {char.get('name','')}")
+ 644                     paths = result.get("local_paths", [])
+ 645                     char["image_path"] = paths[0] if paths else None
  619             except Exception as e:
  620                 logger.warning(f"  角色{char.get('name','')}图片生成失败: {e}")
  621                 char["image_path"] = None
- 622             return idx, char
- 623
- 624         # 并发执行(max_workers=2避免API并发限制)
- 625         if tasks:
- 626             logger.info(f"  并发生成{len(tasks)}个角色图(max_workers=2)...")
- 627             with ThreadPoolExecutor(max_workers=2) as pool:
- 628                 for idx, char in pool.map(_gen_one, tasks):
- 629                     pass  # 结果已直接写入char对象
  630         return characters
  631
  632     def _generate_thumbnails(self, media_dir: Path):
  633         """v3.11: 为目录下的图片和视频生成缩略图，固化到 .thumbs/ 子目录
      ...


现在自测：

exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && python3 -m py_compile user_driven_task_system.py && echo "PY_COMPILE_OK" || echo "PY_COMPILE_FAIL"


# 测试场景图人物剥离逻辑
cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && venv/bin/python3 -c "
import re

# 模拟场景图prompt含角色描述
img_prompt = '【现代】午后坎儿井地下暗渠内部，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风（女性，卡其色工装）蹲下身触摸石壁凿痕，侧光束透入，地下水流淌，木质引水槽，中景，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文'

characters = [
    {'name': '林晓', 'gender': 'female'},
    {'name': '木尔', 'gender': 'male'},
    {'name': '小安', 'gender': 'male'}
]

for char in characters:
    name = char['name']
    if name and name in img_prompt:
        pattern = re.escape(name) + r'\([^)]*\)[，,][^，,。；]*(?:[，,][^，,。；]*)*'
        img_prompt = re.sub(pattern, '', img_prompt)

img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
if '无人物' not in img_prompt:
    img_prompt += '，纯环境背景图，无人物无角色'

print('Result:', img_prompt)
" 2>&1


Result: 【现代】午后坎儿井地下暗渠内部，纯环境背景图，无人物无角色

Result: 【现代】午后坎儿井地下暗渠内部，纯环境背景图，无人物无角色


场景图人物剥离有效，但剥离得有点过于激进——把环境描述(侧光束透入，地下水流淌，木质引水槽)也删掉了。这是因为正则匹配太贪婪，把角色名后面的所有逗号分隔内容都当作角色描述了。

让我优化正则，只匹配角色名+性别+标志性特征+服装，不匹配环境和光影描述：

edit

            if img_prompt and characters:
                # v3.15: 从场景图prompt中移除角色名及其描述
                # 场景图应该是纯环境背景图，不含角色人物
                for char in characters:
                    name = char.get("name", "")
                    if name and name in img_prompt:
                        # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，...，装扮")
                        # 策略: 找到角色名，匹配到(性别)，然后匹配标志性特征+服装(最多3个逗号段)
                        import re
                        # 匹配: 角色名(性别)，[标志性特征:xxx，][服装/装扮，]最多4段
                        pattern = re.escape(name) + r'\([^)]*\)(?:，[^，。；]{0,60}){0,4}'
                        img_prompt = re.sub(pattern, '', img_prompt)
                # 清理多余逗号和空格
                img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
                # 确保包含"纯环境背景图，无人物"的约束
                if "无人物" not in img_prompt and "无人" not in img_prompt:
                    img_prompt += "，纯环境背景图，无人物无角色"
                scene["image_prompt_scene"] = img_prompt  # 保存场景专用prompt(不覆盖原始)

            if img_prompt and characters:
                # v3.15: 从场景图prompt中移除角色名及其描述
                for char in characters:
                    name = char.get("name", "")
                    if name and name in img_prompt:
                        # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮")
                        # 使用正则匹配: 角色名(性别)，...，直到句号或逗号+新描述段
                        import re
                        # 匹配模式: 角色名(性别)，一系列描述，直到句号或逗号+新描述段
                        pattern = re.escape(name) + r'\([^)]*\)[，,][^，,。；]*(?:[，,][^，,。；]*)*'
                        img_prompt = re.sub(pattern, '', img_prompt)
                # 清理多余逗号和空格
                img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
                # 确保包含"纯环境背景图，无人物"的约束
                if "无人物" not in img_prompt and "无人" not in img_prompt:
                    img_prompt += "，纯环境背景图，无人物无角色"
                scene["image_prompt_scene"] = img_prompt  # 保存场景专用prompt(不覆盖原始)


Could not find the exact text in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py. The old text must match exactly including all whitespace and newlines.
Current file contents:
"""
用户主导的任务系统
版本: 3.15
功能: 支持分步执行、结果修改、图/文生视频切换、采集入库、字幕添加、视频拼接
v3.5: 创建任务配置(时长/渠道/视频模式)、删除任务、修改弹框+历史、场景/分镜修改、步骤联动、审核增强
v3.7: Agent真实调用改造 - 各步骤优先通过OpenClaw Gateway调用对应Agent(采集官/创作官/制作官/审核官),LLM直接调用作为fallback
v3.8: 恢复seedream图生图功能 - 使用官方`image`参数(非image_url),角色图/场景图/镜头参考图均支持参考图输入
v3.15: QA9项检查扩充+角色锚点注入+反派约束+结尾钩子+前端QA可视化
"""

import os
import json
import uuid
import time
import logging
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
import tempfile
from datetime import datetime
from typing import Dict, List, Optional, Any
from pathlib import Path

logger = logging.getLogger(__name__)

# 导入简化的真实Agent数据生成
from simple_real_agent import SimpleRealAgent

# 导入真实API客户端
from real_api_client import generate_video, generate_video_from_image, generate_image, generate_te
... (truncated)


exec

grep -n "v3.15: 从场景图prompt中移除角色名" /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py


715:                # v3.15: 从场景图prompt中移除角色名及其描述

715:                # v3.15: 从场景图prompt中移除角色名及其描述


for i, scene in enumerate(scenes):
            img_prompt = scene.get("image_prompt", "")
            if img_prompt and characters:
                # v3.15: 从场景图prompt中移除角色名及其描述
                for char in characters:
                    name = char.get("name", "")
                    if name and name in img_prompt:
                        # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮")
                        # 使用正则匹配: 角色名(性别)，...，直到遇到下一个独立描述段
                        import re
                        # 匹配模式: 角色名(性别)，一系列描述，直到句号或逗号+新描述段
                        pattern = re.escape(name) + r"\([^)]*\)[，,][^，,。；]*(?:[，,][^，,。；]*)*"
                        img_prompt = re.sub(pattern, "", img_prompt)
                # 清理多余逗号和空格
                img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
                # 确保包含"纯环境背景图，无人物"的约束
                if "无人物" not in img_prompt and "无人" not in img_prompt:
                    img_prompt += "，纯环境背景图，无人物无角色"
                scene["image_prompt_scene"] = img_prompt  # 保存场景专用prompt(不覆盖原始)

            if not img_prompt:
                location = scene.get("location", "") or scene.get("description", "")[:30]
                if not location:
                    scene["image_path"] = None
                    continue
                img_prompt = f"{style_prefix}{location},纯环境背景图,无人物,电影感,氛围感,9:16竖屏"
            scene_prompts.append((i, scene, img_prompt))

        if not scene_prompts:


edit

                # v3.15: 从场景图prompt中移除角色名及其描述
                # 场景图应该是纯环境背景图，不含角色人物
                for char in characters:
                    name = char.get("name", "")
                    if name and name in img_prompt:
                        # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，...，装扮")
                        # 策略: 匹配角色名(性别)及其后的标志性特征/服装描述(最多4段，每段≤60字)
                        import re
                        pattern = re.escape(name) + r'\([^)]*\)(?:，[^，。；]{0,60}){0,4}'
                        img_prompt = re.sub(pattern, '', img_prompt)

                # v3.15: 从场景图prompt中移除角色名及其描述
                for char in characters:
                    name = char.get("name", "")
                    if name and name in img_prompt:
                        # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮")
                        # 使用正则匹配: 角色名(性别)，...，直到遇到下一个独立描述段
                        import re
                        # 匹配模式: 角色名(性别)，一系列描述，直到句号或逗号+新描述段
                        pattern = re.escape(name) + r"\([^)]*\)[，,][^，,。；]*(?:[，,][^，,。；]*)*"
                        img_prompt = re.sub(pattern, "", img_prompt)


      ...
  712         for i, scene in enumerate(scenes):
  713             img_prompt = scene.get("image_prompt", "")
  714             if img_prompt and characters:
  715                 # v3.15: 从场景图prompt中移除角色名及其描述
+ 716                 # 场景图应该是纯环境背景图，不含角色人物
  716                 for char in characters:
  717                     name = char.get("name", "")
  718                     if name and name in img_prompt:
- 719                         # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮")
- 720                         # 使用正则匹配: 角色名(性别)，...，直到遇到下一个独立描述段
+ 720                         # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，...，装扮")
+ 721                         # 策略: 匹配角色名(性别)及其后的标志性特征/服装描述(最多4段，每段≤60字)
  721                         import re
- 722                         # 匹配模式: 角色名(性别)，一系列描述，直到句号或逗号+新描述段
- 723                         pattern = re.escape(name) + r"\([^)]*\)[，,][^，,。；]*(?:[，,][^，,。；]*)*"
- 724                         img_prompt = re.sub(pattern, "", img_prompt)
+ 723                         pattern = re.escape(name) + r'\([^)]*\)(?:，[^，。；]{0,60}){0,4}'
+ 724                         img_prompt = re.sub(pattern, '', img_prompt)
  725                 # 清理多余逗号和空格
  726                 img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
  727                 # 确保包含"纯环境背景图，无人物"的约束
  728                 if "无人物" not in img_prompt and "无人" not in img_prompt:
      ...


exec

# 测试优化后的正则
cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && venv/bin/python3 -c "
import re

# 测试3个场景prompt
test_prompts = [
    '【现代】午后坎儿井地下暗渠内部，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风（女性，卡其色工装）蹲下身触摸石壁凿痕，侧光束透入，地下水流淌，木质引水槽，中景，电影写实风格',
    '【汉代】黄昏戈壁营地，黄沙漫天，木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮（男性，古铜色皮肤粗麻褐衣）持青铜凿子立于群像前，小安扛镐头立旁，低角度仰拍，黄昏逆光剪影，电影写实风格',
    '【现代】坎儿井文化广场，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风（女性，艾德莱斯绸长裙）站在遗产石碑前面对镜头，身后游客示意图展板，中景，电影写实风格',
]

characters = [
    {'name': '林晓', 'gender': 'female'},
    {'name': '木尔', 'gender': 'male'},
    {'name': '小安', 'gender': 'male'}
]

for idx, img_prompt in enumerate(test_prompts):
    orig = img_prompt
    for char in characters:
        name = char['name']
        if name and name in img_prompt:
            pattern = re.escape(name) + r'\([^)]*\)(?:，[^，。；]{0,60}){0,4}'
            img_prompt = re.sub(pattern, '', img_prompt)
    img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
    if '无人物' not in img_prompt:
        img_prompt += '，纯环境背景图，无人物无角色'
    print(f'--- Test {idx+1} ---')
    print(f'Before: {orig[:80]}...')
    print(f'After:  {img_prompt[:120]}...')
    print()
" 2>&1


--- Test 1 ---
Before: 【现代】午后坎儿井地下暗渠内部，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风（女性，卡其色工装...
After:  【现代】午后坎儿井地下暗渠内部，卡其色工装）蹲下身触摸石壁凿痕，侧光束透入，地下水流淌，木质引水槽，中景，电影写实风格，纯环境背景图，无人物无角色...

--- Test 2 ---
Before: 【汉代】黄昏戈壁营地，黄沙漫天，木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮（男性，古铜色皮肤粗麻褐衣...
After:  【汉代】黄昏戈壁营地，黄沙漫天，实用耐磨的屯垦水工装扮（男性，古铜色皮肤粗麻褐衣）持青铜凿子立于群像前，小安扛镐头立旁，低角度仰拍，黄昏逆光剪影，电影写实风格，纯环境背景图，无人物无角色...

--- Test 3 ---
Before: 【现代】坎儿井文化广场，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风（女性，艾德莱斯绸长裙）站...
After:  【现代】坎儿井文化广场，艾德莱斯绸长裙）站在遗产石碑前面对镜头，身后游客示意图展板，中景，电影写实风格，纯环境背景图，无人物无角色...


正则还是有问题——"卡其色工装）蹲下身触摸石壁凿痕" 这种括号内的内容没被清除。根因是prompt中混用了全角/半角括号，且角色描述后跟的动作描述(如"蹲下身触摸石壁凿痕")也需要移除。

换个更可靠的策略——不用正则匹配删除，而是**重建场景prompt**，只保留环境信息：

edit

            if img_prompt and characters:
                # v3.15: 场景图应该是纯环境背景图，不含角色人物
                # 策略: 从scene的image_prompt中提取环境信息，重建纯环境prompt
                # 1. 先尝试用location+time+atmosphere+core_elements构建简洁环境描述
                location = scene.get("location", "")
                time_info = scene.get("time", "")
                atmosphere = scene.get("atmosphere", "")
                core_elements = scene.get("core_elements", [])
                props = scene.get("props_fixed", [])

                # 2. 从原始image_prompt中提取环境关键词(光影、天气、视角等)
                import re
                env_keywords = []
                # 提取括号内的时代背景(如【现代】、【汉代】)
                era_match = re.search(r'【[^】]+】', img_prompt)
                if era_match:
                    env_keywords.append(era_match.group())
                # 提取时间信息
                time_match = re.search(r'(正午|午后|黄昏|深夜|清晨|傍晚|夜晚|黎明)', img_prompt)
                if time_match:
                    env_keywords.append(time_match.group())
                # 提取光影/天气/视角
                env_patterns = [
                    r'(侧光|逆光|顺光|冷光|暖光|自然光|光线|阴影|剪影)[^，。]*',
                    r'(鸟瞰|俯视|仰拍|远景|全景|中景|特写|低角度|高角度)',
                    r'(黄沙漫天|暴雨|晴天|阴天|雪|雾|风沙)[^，。]*',
                    r'(地下水流淌|木质引水槽|明渠|暗渠|石壁|水渠|戈壁|荒漠)[^，。]*',
                ]
                for pat in env_patterns:
                    for m in re.finditer(pat, img_prompt):
                        env_keywords.append(m.group())

                # 3. 重建场景prompt: 时代+地点+时间+氛围+环境元素+画质标签
                parts = []
                if env_keywords:
                    parts.extend(env_keywords)
                if location:
                    parts.append(location)
                if atmosphere:
                    parts.append(atmosphere)
                if core_elements:
                    parts.extend(core_elements)
                if props:
                    parts.extend(props[:3])  # 最多3个道具
                # 添加画质标签
                parts.append('纯环境背景图，无人物无角色')
                # 保留原始prompt中的画质标签
                quality_tags = re.findall(r'(电影写实风格|高质量|真实光影|电影级打光|8K分辨率|超高清细节|禁止中文汉字水印)', img_prompt)
                parts.extend(quality_tags)

                if parts:
                    img_prompt = '，'.join(parts)
                scene["image_prompt_scene"] = img_prompt  # 保存场景专用prompt(不覆盖原始)

            if img_prompt and characters:
                # v3.15: 从场景图prompt中移除角色名及其描述
                # 场景图应该是纯环境背景图，不含角色人物
                for char in characters:
                    name = char.get("name", "")
                    if name and name in img_prompt:
                        # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，...，装扮")
                        # 策略: 匹配角色名(性别)及其后的标志性特征/服装描述(最多4段，每段≤60字)
                        import re
                        pattern = re.escape(name) + r'\([^)]*\)(?:，[^，。；]{0,60}){0,4}'
                        img_prompt = re.sub(pattern, '', img_prompt)
                # 清理多余逗号和空格
                img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
                # 确保包含"纯环境背景图，无人物"的约束
                if "无人物" not in img_prompt and "无人" not in img_prompt:
                    img_prompt += "，纯环境背景图，无人物无角色"
                scene["image_prompt_scene"] = img_prompt  # 保存场景专用prompt(不覆盖原始)


Successfully replaced 1 block(s) in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py.

      ...
  711         scene_prompts = []
  712         for i, scene in enumerate(scenes):
  713             img_prompt = scene.get("image_prompt", "")
  714             if img_prompt and characters:
- 715                 # v3.15: 从场景图prompt中移除角色名及其描述
- 716                 # 场景图应该是纯环境背景图，不含角色人物
- 717                 for char in characters:
- 718                     name = char.get("name", "")
- 719                     if name and name in img_prompt:
- 720                         # 移除角色锚点(如"木尔(男性)，额头正中的浅疤，...，装扮")
- 721                         # 策略: 匹配角色名(性别)及其后的标志性特征/服装描述(最多4段，每段≤60字)
- 722                         import re
- 723                         pattern = re.escape(name) + r'\([^)]*\)(?:，[^，。；]{0,60}){0,4}'
- 724                         img_prompt = re.sub(pattern, '', img_prompt)
- 725                 # 清理多余逗号和空格
- 726                 img_prompt = re.sub(r'[，,]{2,}', '，', img_prompt).strip('，, ')
- 727                 # 确保包含"纯环境背景图，无人物"的约束
- 728                 if "无人物" not in img_prompt and "无人" not in img_prompt:
- 729                     img_prompt += "，纯环境背景图，无人物无角色"
+ 715                 # v3.15: 场景图应该是纯环境背景图，不含角色人物
+ 716                 # 策略: 从scene的image_prompt中提取环境信息，重建纯环境prompt
+ 717                 # 1. 先尝试用location+time+atmosphere+core_elements构建简洁环境描述
+ 718                 location = scene.get("location", "")
+ 719                 time_info = scene.get("time", "")
+ 720                 atmosphere = scene.get("atmosphere", "")
+ 721                 core_elements = scene.get("core_elements", [])
+ 722                 props = scene.get("props_fixed", [])
+ 723
+ 724                 # 2. 从原始image_prompt中提取环境关键词(光影、天气、视角等)
+ 725                 import re
+ 726                 env_keywords = []
+ 727                 # 提取括号内的时代背景(如【现代】、【汉代】)
+ 728                 era_match = re.search(r'【[^】]+】', img_prompt)
+ 729                 if era_match:
+ 730                     env_keywords.append(era_match.group())
+ 731                 # 提取时间信息
+ 732                 time_match = re.search(r'(正午|午后|黄昏|深夜|清晨|傍晚|夜晚|黎明)', img_prompt)
+ 733                 if time_match:
+ 734                     env_keywords.append(time_match.group())
+ 735                 # 提取光影/天气/视角
+ 736                 env_patterns = [
+ 737                     r'(侧光|逆光|顺光|冷光|暖光|自然光|光线|阴影|剪影)[^，。]*',
+ 738                     r'(鸟瞰|俯视|仰拍|远景|全景|中景|特写|低角度|高角度)',
+ 739                     r'(黄沙漫天|暴雨|晴天|阴天|雪|雾|风沙)[^，。]*',
+ 740                     r'(地下水流淌|木质引水槽|明渠|暗渠|石壁|水渠|戈壁|荒漠)[^，。]*',
+ 741                 ]
+ 742                 for pat in env_patterns:
+ 743                     for m in re.finditer(pat, img_prompt):
+ 744                         env_keywords.append(m.group())
+ 745
+ 746                 # 3. 重建场景prompt: 时代+地点+时间+氛围+环境元素+画质标签
+ 747                 parts = []
+ 748                 if env_keywords:
+ 749                     parts.extend(env_keywords)
+ 750                 if location:
+ 751                     parts.append(location)
+ 752                 if atmosphere:
+ 753                     parts.append(atmosphere)
+ 754                 if core_elements:
+ 755                     parts.extend(core_elements)
+ 756                 if props:
+ 757                     parts.extend(props[:3])  # 最多3个道具
+ 758                 # 添加画质标签
+ 759                 parts.append('纯环境背景图，无人物无角色')
+ 760                 # 保留原始prompt中的画质标签
+ 761                 quality_tags = re.findall(r'(电影写实风格|高质量|真实光影|电影级打光|8K分辨率|超高清细节|禁止中文汉字水印)', img_prompt)
+ 762                 parts.extend(quality_tags)
+ 763
+ 764                 if parts:
+ 765                     img_prompt = '，'.join(parts)
  730                 scene["image_prompt_scene"] = img_prompt  # 保存场景专用prompt(不覆盖原始)
  731
  732             if not img_prompt:
  733                 location = scene.get("location", "") or scene.get("description", "")[:30]
      ...


exec

# 测试重建策略
cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && venv/bin/python3 -c "
import re

test_cases = [
    {
        'location': '坎儿井地下暗渠入口',
        'time': '午后',
        'atmosphere': '神秘幽深',
        'core_elements': ['地下暗渠', '石壁凿痕'],
        'props_fixed': ['木质引水槽', '陶制油灯'],
        'image_prompt': '【现代】午后坎儿井地下暗渠内部，林晓(女性)，眼尾的浅褐色小泪痣，时刻挂在脖子上的微单相机，卡其色工装外套白T恤束脚工装裤黑马丁靴，户外探索风（女性，卡其色工装）蹲下身触摸石壁凿痕，侧光束透入，地下水流淌，木质引水槽，中景，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文汉字水印'
    },
    {
        'location': '汉代西域屯垦营地外的戈壁',
        'time': '黄昏',
        'atmosphere': '荒凉壮阔',
        'core_elements': ['戈壁营地', '黄沙'],
        'props_fixed': ['青铜凿子'],
        'image_prompt': '【汉代】黄昏戈壁营地，黄沙漫天，木尔(男性)，额头正中的浅疤，永远挂在腰间的铜制凿井标尺，粗麻褐衣，生牛皮短靴，实用耐磨的屯垦水工装扮（男性，古铜色皮肤粗麻褐衣）持青铜凿子立于群像前，小安扛镐头立旁，低角度仰拍，黄昏逆光剪影，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节'
    }
]

for tc in test_cases:
    img_prompt = tc['image_prompt']
    parts = []
    # 提取时代
    era_match = re.search(r'【[^】]+】', img_prompt)
    if era_match: parts.append(era_match.group())
    # 提取时间
    time_match = re.search(r'(正午|午后|黄昏|深夜|清晨|傍晚|夜晚|黎明)', img_prompt)
    if time_match: parts.append(time_match.group())
    # 提取光影/天气/视角
    for pat in [r'(侧光[^，。]*|逆光[^，。]*|剪影)', r'(鸟瞰|俯视|仰拍|远景|全景|中景|特写|低角度)', r'(黄沙漫天|地下水流淌|明渠|暗渠)']:
        for m in re.finditer(pat, img_prompt):
            parts.append(m.group())
    if tc.get('location'): parts.append(tc['location'])
    if tc.get('atmosphere'): parts.append(tc['atmosphere'])
    if tc.get('core_elements'): parts.extend(tc['core_elements'])
    if tc.get('props_fixed'): parts.extend(tc['props_fixed'][:3])
    parts.append('纯环境背景图，无人物无角色')
    quality_tags = re.findall(r'(电影写实风格|高质量|真实光影|电影级打光|8K分辨率|超高清细节|禁止中文汉字水印)', img_prompt)
    parts.extend(quality_tags)
    result = '，'.join(parts)
    print(f'--- {tc[\"location\"]} ---')
    print(f'Result: {result}')
    print()
" 2>&1


--- 坎儿井地下暗渠入口 ---
Result: 【现代】，午后，侧光束透入，中景，暗渠，地下水流淌，坎儿井地下暗渠入口，神秘幽深，地下暗渠，石壁凿痕，木质引水槽，陶制油灯，纯环境背景图，无人物无角色，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节，禁止中文汉字水印

--- 汉代西域屯垦营地外的戈壁 ---
Result: 【汉代】，黄昏，逆光剪影，低角度，仰拍，黄沙漫天，汉代西域屯垦营地外的戈壁，荒凉壮阔，戈壁营地，黄沙，青铜凿子，纯环境背景图，无人物无角色，电影写实风格，高质量，真实光影，电影级打光，8K分辨率，超高清细节


效果不错——场景图prompt只保留环境和氛围，角色完全剥离。编译测试+重启：


active
200


现在更新记忆：

cat >> /root/.openclaw/workspace/memory/2026-05-20.md << 'EOF'

## v3.15 修复: 场景图含人物 + 角色图风格不一致 (11:07-11:30)

### 问题1: 场景图不应有人物但生成了人物
**根因**: LLM生成的scene.image_prompt包含了角色描述(角色名+性别+标志性特征+服装+动作)，直接传给图片API导致场景图出现人物
**修复**: 场景图生成前重建纯环境prompt，从scene中提取location/time/atmosphere/core_elements/props+光影/天气关键词，剥离所有角色描述，追加"纯环境背景图，无人物无角色"

### 问题2: 3个角色图风格不一致
**根因**: 并发生成(max_workers=2)时，每个角色独立调用API，没有风格统一的参考
**修复**: 改为串行生成，第1个角色正常生成，后续角色用首个角色图作为参考图(图生图)，prompt追加"与第一个角色保持完全相同的画风和色彩风格"

### 自测结果
1. ✅ py_compile通过
2. ✅ systemctl restart active + HTTP 200
3. ✅ 场景prompt重建测试: 角色100%剥离，环境信息保留
EOF


(no output)


---

## ✅ 排查完成

### 问题1: 场景图含人物

**根因**: LLM生成的 `scene.image_prompt` 包含了角色描述（角色名+性别+标志性特征+服装+动作），直接传给图片API导致场景图出现人物。v3.15的锚点注入也加剧了这个问题。

**修复**: 场景图生成前**重建纯环境prompt**——从scene中提取 `location/time/atmosphere/core_elements/props` + 光影/天气关键词，剥离所有角色描述，追加 `"纯环境背景图，无人物无角色"`。

### 问题2: 角色图风格不一致

**根因**: 并发生成时，每个角色独立调用API，Seedream模型对抽象风格描述（"参考《国家宝藏》风格"）理解不稳定，每次生成画风差异大。

**修复**: 改为**串行生成**，第1个角色正常生成，后续角色用首个角色图作为**参考图（图生图）**，prompt追加"与第一个角色保持完全相同的画风和色彩风格"。

版本已更新为v3.15，自测三步法通过。


[OpenClaw heartbeat poll]


Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 11:18 AM (Asia/Shanghai)
Reference UTC: 2026-05-20 03:18 UTC


active
active
72%


System (untrusted): [2026-05-20 11:30:18 GMT+8] Zod模块健康检查完成，状态正常，无需修复，不发送通知。

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 11:43 AM (Asia/Shanghai)
Reference UTC: 2026-05-20 03:43 UTC


System (untrusted): [2026-05-20 12:00:25 GMT+8] Zod模块健康检查完成，状态正常 ✅，无需修复，不发送通知。

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 12:13 PM (Asia/Shanghai)
Reference UTC: 2026-05-20 04:13 UTC


全部正常，磁盘72%，zod模块刚检查过也正常。

HEARTBEAT_OK


System (untrusted): [2026-05-20 12:30:11 GMT+8] Zod模块健康检查完成：✅ zod模块正常，无需修复，不推送通知。

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 12:43 PM (Asia/Shanghai)
Reference UTC: 2026-05-20 04:43 UTC


System (untrusted): [2026-05-20 13:00:15 GMT+8] Zod模块健康检查完成，状态正常 ✅ 无需修复，不推送通知。

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 1:13 PM (Asia/Shanghai)
Reference UTC: 2026-05-20 05:13 UTC


所有服务正常，zod健康检查通过，磁盘72%。无需关注。

HEARTBEAT_OK


System (untrusted): [2026-05-20 13:30:13 GMT+8] Zod模块健康检查通过，无需修复，不发送通知。

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 1:43 PM (Asia/Shanghai)
Reference UTC: 2026-05-20 05:43 UTC


System (untrusted): [2026-05-20 14:00:13 GMT+8] Zod模块健康检查完成，状态正常，无需修复。✅

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 2:13 PM (Asia/Shanghai)
Reference UTC: 2026-05-20 06:13 UTC


快速检查，服务刚刚确认过正常。


System (untrusted): [2026-05-20 14:30:16 GMT+8] Zod模块健康检查完成，状态正常 ✅，无需修复，不推送通知。

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
When reading HEARTBEAT.md, use workspace file /root/.openclaw/workspace/HEARTBEAT.md (exact case). Do not read docs/heartbeat.md.
Current time: Wednesday, May 20th, 2026 - 2:43 PM (Asia/Shanghai)
Reference UTC: 2026-05-20 06:43 UTC


[Wed 2026-05-20 14:48 GMT+8] 参考CTO行为规范处理
1、我在界面上看不到你的版本号
2、第二部分：角色图部分 我需要个全部重新生成按钮
3、第二部分：分镜部分 需要 重新生成全部背景图、继续生成背景图 、重新生成全部镜头参考图、继续生成参考图
4、第二部分：还是没有缩略图，一直重复出现这个问题
5、第二部分：镜头的说明和任务对话我怎么修改？


The user is asking me to address 5 issues with the UI. Let me analyze each one and figure out what needs to be done. This requires looking at the frontend code and backend API.

1. Version number not visible on the interface
2. Need a "regenerate all" button for character images
3. Need buttons for: regenerate all background images, continue generating background images, regenerate all shot reference images, continue generating shot reference images
4. Thumbnails still not working (recurring issue)
5. How to modify shot descriptions and task dialogue

Let me look at the current frontend and backend code to understand what exists and what needs to be added.

5个问题，逐个排查。先看前端和API现状：

# 查看前端版本号显示
grep -n "version\|版本\|v3\." /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/static/user_driven_coordinator_final.html 2>/dev/null | head -20

# 查看API中版本号相关路由
grep -n "version\|版本" /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/api_server.py | head -20

# 查看角色图重新生成API
grep -n "regenerat\|重新生成" /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/api_server.py | head -20

# 查看缩略图相关
grep -n "thumb\|缩略图" /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/api_server.py | head -20

# 查看镜头修改API
grep -n "modify\|修改\|update.*shot\|edit.*shot" /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/api_server.py | head -20


6:    <title>用户主导的慢剧生成系统 v3.9</title>
170:
版本 3.5.0 - 创建配置、展开收起、修改弹框、场景/分镜修改、步骤联动、审核增强

175:
197:
253:


88:    version="1.0.0"
177:        "version": "1.0.0",


627:@app.post("/api/tasks/{task_id}/shots/{shot_id}/regenerate")
628:async def regenerate_shot_video(task_id: str, shot_id: int, prompt: Optional[str] = None, mode: Optional[str] = None):
629:    """重新生成单个镜头的视频"""
630:    result = ud_task_system.regenerate_single_shot(task_id, shot_id, user_prompt=prompt, mode=mode)
716:@app.post("/api/tasks/{task_id}/regenerate-image")
717:async def regenerate_image(task_id: str, request: Dict[str, Any] = Body(...)):
718:    """重新生成图片(角色/场景/镜头参考图)"""
747:            logger.warning(f"角色图重新生成失败: {e}")
766:            logger.warning(f"场景图重新生成失败: {e}")
811:            logger.warning(f"镜头参考图重新生成失败: {e}")


813:    # 生成缩略图
814:    ud_task_system._generate_thumbnails(images_dir)
991:async def serve_file(path: str = Query(..., description="文件绝对路径"), thumb: int = Query(0, description="返回缩略图")):
992:    """提供本地文件访问（图片/视频）- 支持缩略图"""
1001:    # v3.14: 缩略图支持
1002:    if thumb:
1003:        thumb_dir = file_path.parent / ".thumbs"
1005:        thumb_name = file_path.stem + "_thumb" + (".jpg" if suffix in (".mp4", ".mov", ".avi", ".webm") else suffix)
1006:        thumb_path = thumb_dir / thumb_name
1007:        if thumb_path.exists():
1008:            return FileResponse(str(thumb_path), media_type="image/jpeg")
1009:        # 缩略图不存在，动态生成
1010:        thumb_dir.mkdir(parents=True, exist_ok=True)
1015:                img.thumbnail((160, 160), Image.LANCZOS)
1018:                img.save(thumb_path, "JPEG", quality=60)
1019:                return FileResponse(str(thumb_path), media_type="image/jpeg")
1027:                             str(thumb_path)], capture_output=True, timeout=15)
1028:                if thumb_path.exists():
1029:                    return FileResponse(str(thumb_path), media_type="image/jpeg")


563:    """获取步骤的修改记录"""
570:    """修改任务配置(模型、视频模式、时长、渠道等)"""
693:@app.post("/api/tasks/{task_id}/modify-scene")
694:async def modify_scene(task_id: str, request: Dict[str, Any] = Body(...)):
695:    """修改场景"""
696:    result = ud_task_system.modify_scene(
704:@app.post("/api/tasks/{task_id}/modify-shot")
705:async def modify_shot(task_id: str, request: Dict[str, Any] = Body(...)):
706:    """修改分镜"""
707:    result = ud_task_system.modify_shot(


# 查看前端JS中版本号和角色图/场景图/缩略图相关
grep -n "version\|版本\|regenerat\|重新生成\|thumb\|缩略图" /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/static/user_driven_coordinator.js | head -40


6:// v3.11: 缩略图占位，点击放大原图
9:  const thumbUrl=url+'&thumb=1';
10:  return `<img src="${thumbUrl}" data-full="${url}" class="rounded ${cls}" style="max-height:${maxH};cursor:pointer;${extra}" onclick="showImageModal(this.dataset.full||this.src.replace('&thumb=1',''))">`;
14:  const thumbUrl=url+'&thumb=1';
15:  return `<div class="lazy-video-wrap" style="position:relative;cursor:pointer;display:inline-block" onclick="this.outerHTML='<video src=\\'${url}\\' controls autoplay style=\\'max-width:100%;max-height:300px;border-radius:8px\\'></video>'">
▶

`;
84:function getImageThumbUrl(p){return p?`${API_BASE}/files?path=${encodeURIComponent(p)}&thumb=1`:'';}
86:function getVideoThumbUrl(p){return p?`${API_BASE}/files?path=${encodeURIComponent(p)}&thumb=1`:'';}
166:    if(m.includes('SensitiveContent')||m.includes('sensitive'))return{reason:'参考图被检测为敏感内容',suggestion:'建议：1)点击"改为文生视频"重新生成 2)修改参考图提示词'};
170:    return{reason:m.substring(0,100),suggestion:'建议：尝试重新生成，或切换到文生视频模式'};
191:        h+=`

${ch.image_path?lazyImg(getImageUrl(ch.image_path),'250px'):'
暂无图片
'}${escapeHtml(ch.name)}
${escapeHtml(desc)}
${threeView?`<details class="mb-1"><summary class="small text-info">📐 三视图提示词</summary><p class="small bg-light p-1 rounded mt-1">${escapeHtml(threeView)}</p></details>`:''}<button class="btn btn-sm btn-outline-warning mt-1" onclick="regenerateCharImage(${i})">修改提示词重生成</button>

`;});h+='
';}
192:    if(result.scenes&&result.scenes.length){let ts=0;result.scenes.forEach(s=>ts+=(s.shots?.length||0));let missingShots=0;result.scenes.forEach(s=>{if(s.shots)s.shots.forEach(sh=>{if(!sh.image_path)missingShots++;});});let missingScenes=result.scenes.filter(s=>!s.image_path).length;h+=`<h6>🎬 分镜 (${result.scenes.length}场景/${ts}镜头)</h6>`;if(missingShots>0||missingScenes>0){h+=`<div class="mb-2 d-flex gap-2 flex-wrap">`;if(missingShots>0)h+=`<button class="btn btn-outline-primary btn-sm" onclick="batchGenerateShotRefImages(false)"><i class="bi bi-play-circle me-1"></i>继续生成 (${missingShots}个未完成)</button>`;h+=`<button class="btn btn-outline-warning btn-sm" onclick="batchGenerateShotRefImages(true)"><i class="bi bi-arrow-clockwise me-1"></i>重新生成全部</button>`;h+=`</div>`;}else{h+=`<div class="mb-2"><button class="btn btn-outline-warning btn-sm" onclick="batchGenerateShotRefImages(true)"><i class="bi bi-arrow-clockwise me-1"></i>重新生成全部</button></div>`;}result.scenes.forEach((sc,idx)=>{let sceneBtnLabel=sc.image_path?'重生成场景图':'生成场景图';h+=`

🏞 ${escapeHtml(sc.location||sc.description?.substring(0,20)||'场景'+sc.scene_id)}
${sc.image_path?lazyImg(getImageUrl(sc.image_path),'60px','','style="display:inline-block;width:60px"'):'  '}<button class="btn btn-sm btn-outline-warning ms-1" onclick="regenerateSceneImage(${idx})">${sceneBtnLabel}</button>

`;if(sc.shots)sc.shots.forEach((sh,shIdx)=>{h+=`

${sh.image_path?lazyImg(getImageUrl(sh.image_path),'120px','','style="width:90px"'):'
无图
'}
镜头${sh.shot_id} ${sh.shot_type||''} ${sh.duration_seconds||5}s
${escapeHtml(sh.description||'')}
${sh.dialogue?`<p class="small text-primary mb-1">💬 "${escapeHtml(sh.dialogue)}"</p>`:''}<button class="btn btn-sm btn-outline-warning mt-1" onclick="regenerateShotRefImage(${idx},${shIdx})">${sh.image_path?'重生成参考图':'生成参考图'}</button>

`;});h+='

';});}
204:                    /* v3.11: 不再内联播放视频，用播放按钮代替 */}if(shot.error||shot.status==='failed'){const a=analyzeVideoError(shot.error);if(a)h+=`<div class="alert alert-danger py-1 mt-2 small"><strong>❌ 原因:</strong> ${escapeHtml(a.reason)}<br><strong>💡 建议:</strong> ${escapeHtml(a.suggestion)}</div>`;else h+=`<div class="mt-1 text-danger small">❌ ${escapeHtml(shot.error||'未知')}</div>`;}h+=`
<button class="btn btn-sm btn-outline-warning" onclick="doRegenShot(${shot.shot_id})">重新生成</button> <button class="btn btn-sm btn-outline-info" onclick="doRegenShotPrompt(${shot.shot_id})">修改提示词重生成</button>${shot.error&&shot.mode==='img2vid'?` <button class="btn btn-sm btn-outline-success" onclick="doRegenShotText(${shot.shot_id})"><i class="bi bi-type me-1"></i>改为文生视频</button>`:''}

`;});}
213:if(r.improvement_suggestions&&r.improvement_suggestions.length){h+='
💡 改进建议 选择要执行的建议

';r.improvement_suggestions.forEach((s,i)=>h+=`
<input type="checkbox" class="form-check-input mt-1 improve-check" data-idx="${i}" checked id="improve_${i}">${escapeHtml(typeof s==='string'?s:JSON.stringify(s))}
`);h+='</ul>';h+=`
<textarea class="form-control form-control-sm" id="improveFeedback" rows="2" placeholder="额外修改说明(可选)"></textarea>
`;h+=`<button class="btn btn-success btn-sm" onclick="executeImprove()" id="btnExecuteImprove">🔄 执行选中建议并重新生成</button> `;}else if(r.suggestions&&r.suggestions.length){h+='<h6>💡 改进建议</h6><ul class="list-group mb-3">';r.suggestions.forEach(s=>h+=`
${escapeHtml(s)}
`);h+='
';}
223:async function batchGenerateSceneImages(){if(!currentTaskId||!window._wr)return;const scenes=window._wr.scenes;if(!scenes||!scenes.length){addLog('warning','没有场景数据');return;}const missing=scenes.filter(s=>!s.image_path);if(!missing.length){addLog('info','所有场景图已生成');return;}addLog('info',`开始批量生成${missing.length}个场景图...`);for(let i=0;i<scenes.length;i++){if(!scenes[i].image_path){addLog('info',`生成场景${i+1}图: ${scenes[i].location||''}...`);try{const r=await fetch(`${API_BASE}/tasks/${currentTaskId}/regenerate-image`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'scene',index:i})});const d=await r.json();if(d.success&&d.result){window._wr=d.result;displayWriteResult(d.result,true);addLog('success',`场景${i+1}图生成成功`);}else addLog('error',`场景${i+1}图失败: ${d.detail||d.message||'未知'}`);}catch(e){addLog('error',`场景${i+1}图失败: ${e.message}`);}}}addLog('success','批量场景图生成完成');}
224:async function regenerateShotRefImage(sceneIdx,shotIdx){if(!currentTaskId||!window._wr)return;const sc=window._wr.scenes[sceneIdx];if(!sc||!sc.shots||!sc.shots[shotIdx])return;const sh=sc.shots[shotIdx];const defaultPrompt=sh.image_prompt||sh.description||'';const p=await showPromptEditor('修改镜头'+sh.shot_id+'的参考图提示词',defaultPrompt);if(p===null)return;addLog('info','重新生成镜头'+sh.shot_id+'的参考图...');
228:try{const r=await fetch(`${API_BASE}/tasks/${currentTaskId}/regenerate-image`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'shot',scene_index:sceneIdx,shot_index:shotIdx,prompt:p})});const d=await r.json();if(d.success&&d.result){window._wr=d.result;displayWriteResult(d.result,true);addLog('success','镜头'+sh.shot_id+'参考图生成成功');}else{addLog('error','镜头'+sh.shot_id+'参考图失败: '+(d.detail||d.message||'未知'));displayWriteResult(window._wr,true);}}catch(e){addLog('error','镜头'+sh.shot_id+'参考图失败: '+e.message);displayWriteResult(window._wr,true);}}
229:async function batchGenerateShotRefImages(regenerateAll=false){if(!currentTaskId||!window._wr)return;const scenes=window._wr.scenes;if(!scenes||!scenes.length){addLog('warning','没有场景数据');return;}if(regenerateAll){addLog('info','🔄 重新生成全部: 先生成背景图→再生成镜头参考图...');/* 1. 重新生成所有背景图 */for(let si=0;si<scenes.length;si++){addLog('info',`生成场景${si+1}背景图: ${scenes[si].location||''}...`);try{const r=await fetch(`${API_BASE}/tasks/${currentTaskId}/regenerate-image`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'scene',index:si})});const d=await r.json();if(d.success&&d.result){window._wr=d.result;displayWriteResult(d.result,true);addLog('success',`场景${si+1}背景图生成成功`);}else addLog('error',`场景${si+1}背景图失败: ${d.detail||d.message||'未知'}`);}catch(e){addLog('error',`场景${si+1}背景图失败: ${e.message}`);}}/* 2. 重新生成所有镜头参考图 */addLog('info','开始生成镜头参考图...');for(let si=0;si<window._wr.scenes.length;si++){const sc=window._wr.scenes[si];if(!sc.shots)continue;for(let shi=0;shi<sc.shots.length;shi++){addLog('info',`生成镜头${sc.shots[shi].shot_id}参考图...`);try{const r=await fetch(`${API_BASE}/tasks/${currentTaskId}/regenerate-image`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'shot',scene_index:si,shot_index:shi})});const d=await r.json();if(d.success&&d.result){window._wr=d.result;displayWriteResult(d.result,true);addLog('success',`镜头${sc.shots[shi].shot_id}参考图生成成功`);}else addLog('error',`镜头${sc.shots[shi].shot_id}失败: ${d.detail||d.message||'未知'}`);}catch(e){addLog('error',`镜头${sc.shots[shi].shot_id}失败: ${e.message}`);}}}addLog('success','✅ 全部重新生成完成');}else{/* 继续生成: 只生成缺失的 */let total=0,done=0;for(const sc of scenes){if(sc.shots)for(const sh of sc.shots){if(!sh.image_path)total++;}}if(!total){addLog('info','所有镜头参考图已生成');return;}addLog('info',`继续生成${total}个缺失的镜头参考图...`);for(let si=0;si<scenes.length;si++){const sc=scenes[si];if(!sc.shots)continue;for(let shi=0;shi<sc.shots.length;shi++){if(!sc.shots[shi].image_path){addLog('info',`生成镜头${sc.shots[shi].shot_id}参考图...`);try{const r=await fetch(`${API_BASE}/tasks/${currentTaskId}/regenerate-image`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'shot',scene_index:si,shot_index:shi})});const d=await r.json();if(d.success&&d.result){window._wr=d.result;displayWriteResult(d.result,true);done++;addLog('success',`镜头${sc.shots[shi].shot_id}参考图生成成功`);}else addLog('error',`镜头${sc.shots[shi].shot_id}失败: ${d.detail||d.message||'未知'}`);}catch(e){addLog('error',`镜头${sc.shots[shi].shot_id}失败: ${e.message}`);}}}}addLog('success',`继续生成完成: ${done}/${total}`);}}
235:async function regenerateShotImage(sceneIdx,shotIdx){if(!currentTaskId||!window._wr)return;var sc=window._wr.scenes[sceneIdx];if(!sc)return;var sh=sc.shots[shotIdx];if(!sh)return;var p=await showPromptEditor('\u4fee\u6539\u955c\u5934'+sh.shot_id+'\u7684\u53c2\u8003\u56fe\u63d0\u793a\u8bcd',sh.image_prompt||'');if(p===null)return;addLog('info','\u91cd\u65b0\u751f\u6210\u955c\u5934'+sh.shot_id+'\u7684\u53c2\u8003\u56fe...');try{var r=await fetch(API_BASE+'/tasks/'+currentTaskId+'/regenerate-image',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'shot_ref',scene_index:sceneIdx,shot_index:shotIdx,prompt:p})});var d=await r.json();if(d.success){addLog('success','\u955c\u5934\u53c2\u8003\u56fe\u91cd\u751f\u6210\u6210\u529f');if(d.result){window._wr=d.result;displayWriteResult(d.result);}}else addLog('error','\u5931\u8d25: '+(d.detail||d.message||'\u672a\u77e5'));}catch(e){addLog('error','\u5931\u8d25: '+e.message);}}
241:// Image regeneration

⚠ [... middle content omitted — showing head and tail ...]

247:async function regenerateSceneImage(idx){if(!currentTaskId||!window._wr)return;const sc=window._wr.scenes[idx];if(!sc)return;const loc=sc.location||sc.description?.substring(0,30)||'场景'+sc.scene_id;const p=await showPromptEditor('修改场景"'+loc+'"的背景图提示词','纯环境背景图，'+loc+'，电影感，氛围感，无人，9:16竖屏');if(p===null)return;addLog('info','重新生成场景"'+loc+'"的背景图...');
251:try{const r=await fetch(`${API_BASE}/tasks/${currentTaskId}/regenerate-image`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'scene',index:idx,prompt:p})});const d=await r.json();if(d.success){addLog('success','场景背景图重生成成功');if(d.result){window._wr=d.result;displayWriteResult(d.result);}}else addLog('error','失败: '+(d.detail||d.message||'未知'));}catch(e){addLog('error','失败: '+e.message);}}
253:// Shot regeneration
254:async function doRegenShot(id,cp,mode){if(!currentTaskId)return;let u=`${API_BASE}/tasks/${currentTaskId}/shots/${id}/regenerate`;const ps=[];if(cp)ps.push('prompt='+encodeURIComponent(cp));if(mode)ps.push('mode='+encodeURIComponent(mode));if(ps.length)u+='?'+ps.join('&');addLog('info','重新生成镜头'+id+'...');try{const r=await fetch(u,{method:'POST'});const d=await r.json();if(d.success){addLog('success','镜头'+id+'重生成成功');if(d.result)displayProduceResult(d.result);document.getElementById('btnStitchVideos').style.display='inline-block';}else addLog('error','镜头'+id+'失败: '+d.message);}catch(e){addLog('error','失败: '+e.message);}}
269:async function executeStep(step,btnId,lt,ot){if(!currentTaskId){addLog('error','请先创建或选择任务');return;}const btn=document.getElementById(btnId);if(!btn)return;btn.disabled=true;btn.innerHTML=' '+lt+'...';updateStepStatus(step,'working');addLog('info','开始'+getStepName(step)+'...');try{let u=`${API_BASE}/tasks/${currentTaskId}/steps/${step}`;if(step==='produce'){const vm=currentTaskData?.video_mode||document.getElementById('taskVideoMode')?.value||'img2vid';u+='?video_mode='+vm;}const ctrl=new AbortController();const timer=setTimeout(()=>ctrl.abort(),600000);const r=await fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},signal:ctrl.signal});clearTimeout(timer);if(!r.ok)throw new Error('HTTP '+r.status+': '+(await r.text()).substring(0,200));const d=await r.json();if(d.success){updateStepStatus(step,'completed',false,d.can_modify,d.can_regenerate);const nx={collect:'write',write:'produce',produce:'review'}[step];if(nx)updateStepStatus(nx,'pending',true);if(d.result){const fn={collect:displayCollectionResult,write:displayWriteResult,produce:displayProduceResult,review:displayReviewResult}[step];if(fn)fn(d.result);}await refreshTaskStatus();addLog('success',getStepName(step)+'完成！'+(d.message||''));}else throw new Error(d.message||'失败');}catch(e){updateStepStatus(step,'pending',true);if(e.name==='AbortError')addLog('error',getStepName(step)+'超时(10分钟)，服务可能仍在后台执行');else addLog('error',getStepName(step)+'失败: '+e.message);}finally{btn.disabled=false;btn.innerHTML=ot;}}
290:
${icon} QA质量检查 v${qa.version||'3.14'} (评分: ${qa.score}/100)
`;
410:        if(d.success&&d.result){displayWritePhase1Result(d.result);addLog('success','大纲重新生成完成');}
452:document.getElementById('btnRegenerateCollect')?.addEventListener('click',()=>executeStep('collect','btnRegenerateCollect','重新采集中','重新生成'));
487:document.getElementById('btnRegenerateWrite')?.addEventListener('click',()=>executeStep('write','btnRegenerateWrite','重新创作中','重新生成'));
492:document.getElementById('btnRegenerateProduce')?.addEventListener('click',()=>executeStep('produce','btnRegenerateProduce','重新制作中','重新生成'));[... 805 more characters truncated]

status exitCode durationMs aggregated cwd