File size: 41,632 Bytes
9ec4d37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59c87b
 
9ec4d37
 
 
 
 
 
 
 
 
 
 
 
 
f59c87b
 
9ec4d37
 
 
 
 
 
 
 
 
 
 
f59c87b
 
9ec4d37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
import os
import sys 
import subprocess
import re
import time
import datetime
import shutil
import json
from concurrent.futures import ThreadPoolExecutor, as_completed

# 1. 自动安装依赖
def ensure_dependencies():
    try:
        import gradio
        import requests
    except ImportError:
        print("正在安装所需依赖: gradio, requests...")
        subprocess.check_call([sys.executable, "-m", "pip", "install", "gradio", "requests"])

ensure_dependencies()

import gradio as gr
import requests

# ================= 配置区域 =================
DEFAULT_LLM_API_KEY = "sk-DZ5g7Zu0lFDlR7mBkbNsZLFTt1KBqA8ocsAH1mcvsZDWtydx"
DEFAULT_VIDEO_API_KEY = "sk-G6LN0uC2BVclZjx1ObDJPkMZTZvtjau1Ss7GjCvRLJyI5euU"

MERCHANT_BASE_URL = "https://xingjiabiapi.com"
VEO_MODEL = "veo_3_1-fast"
VIDEO_SIZE = "16x9" 
TEXT_MODEL = "gemini-3-pro-preview-thinking" 
CONFIG_FILE = "prompt_config.json" 

# ================= 全局变量 =================
IS_PAUSED = False
BATCH_SIZE = 10 

def toggle_pause():
    global IS_PAUSED
    IS_PAUSED = not IS_PAUSED
    return "▶️ 恢复运行" if IS_PAUSED else "⏸️ 暂停任务"

# ===============================================
# --- 默认提示词备份 ---
# ===============================================

# 1. 工程师 (🔥保持不变:核心环节 50+ 微步骤,足以支撑 200+ 镜头)
FALLBACK_ENGINEER = """
你现在的身份是 **全链路工业冗余记录员 (The Full-Chain Industrial Logger)**。
你的任务是输出一份 **极度详细、甚至显得啰嗦** 的全流程生产日志。**不要替导演做减法,你的任务是提供海量素材!**

**⚠️ 核心指令 1:全链路覆盖与数量硬指标 (Mandatory Full Chain & Quota)**
你必须按顺序覆盖以下 **6 大阶段**,缺一不可:
1.  **[源头与收获]** (Harvest): **(🔥视频必须以此开始!)** 田间/矿场/海洋的机械化采集 -> 装车。要体现原材料的原始状态。
2.  **[进厂与净化]** (Intake & Cleaning): 卸货 -> 去石 -> 多级清洗 -> 分选。**(🔥死命令:此阶段微步骤总数不低于 50 个!要反复写清洗细节)**
3.  **[预处理]** (Prep): 去皮/破碎/切割/研磨。**(🔥死命令:此阶段微步骤总数不低于 50 个!)**
4.  **[核心质变]** (Transformation): 榨汁/油炸/冶炼/混合/反应。**(🔥死命令:此阶段微步骤总数不低于 50 个!要拆解每一秒的物理变化)**
5.  **[精炼与成型]** (Refinement): 过滤/杀菌/干燥/冷却/压制。**(🔥死命令:此阶段微步骤总数不低于 50 个!)**
6.  **[包装与物流]** (Packaging): 吹瓶/制罐 -> 灌装 -> 封口 -> 贴标 -> 喷码 -> 装箱 -> 码垛。

**⚠️ 核心指令 2:1:5 裂变法则 (The 1:5 Fission Law)**
不要概括!任何一个动作必须拆解为 **5个微步骤**。
* *Bad*: "清洗土豆。"
* *Good*:
    * 步骤 2.1: 土豆落入气泡清洗池,激起水花。
    * 步骤 2.2: 高压气泡翻滚,土豆表面泥土松动。
    * 步骤 2.3: 滚筒毛刷摩擦土豆表皮,去除顽固污渍。
    * 步骤 2.4: 喷淋装置用净水冲洗土豆表面。
    * 步骤 2.5: 风刀吹干土豆表面的残余水珠。

**⚠️ 核心指令 3:物理细节描述**
在每个微步骤中,必须包含:**力学** (撞击/摩擦)、**热力学** (蒸汽/起泡)、**流体力学** (飞溅/漩涡)。

**输出格式 (请严格执行,全部中文):**
**阶段 [序号]:[阶段名称]**
* **微步骤 [x.1]**: [动作描述] + [物理反馈细节]
* **微步骤 [x.2]**: [动作描述] + [物理反馈细节]
... (请确保总步骤数足以支撑 30 分钟的视频,总计输出不低于 250 个微步骤)
"""

# 2. 导演 1.0 (🔥已修改:预期调整为 200 镜头)
FALLBACK_DIRECTOR_V1 = """
你现在是 **AI 视频提示词架构师 (Veo Prompt Architect)** 兼 **高级工业纪录片总导演**。
你拥有一份由架构师提供的《工艺技术详情》作为**参考知识库**。
**你的核心任务**:基于这些技术步骤,构思一部 **超长篇幅 (200+ 镜头)、沉浸式、解压** 现代工业纪录片。

**⚠️ 核心指令 0:大片级叙事与节奏控制**
* **参考与重构**:架构师的输出只是**技术底稿**。你必须将其转化为**视觉脚本**。
* **黄金时间分配法则**:
    1.  **前奏 (Origin)**:原料采集 -> 进厂。**必须以此开场!** (Shot 1 必须是源头)
    2.  **高潮 (Core Processing)**:核心加工环节。**必须占据 80% 以上的篇幅**。这是观众最爱看的解压部分。
    3.  **尾声 (Packaging)**:最终封装。**必须严格控制在最后 20% 的镜头**。
    * **预期管理**:假设总镜头数为 **200 个**,你必须在 **第 161 个镜头** 左右才能开始进入包装环节。在此之前,请尽情展示加工细节!

**⚠️ 核心指令 1:全域视觉锚点与命名协议 (Naming Protocol)** [🔥新增补丁]
* **⚠️ 绝对主体铁律**:所有镜头的 `**主体**` 必须是 **[产品]**。
* **🛑 强制名词复诵 (Mandatory Noun Repetition)**:
    * **严禁代词**:绝对禁止使用 `It`, `They`, `The liquid`。
    * **全名锁定**:必须复诵全名!如 `The ruby-red Pomegranate Juice` (红宝石般的石榴汁),`Wet Potato Strips` (湿润的土豆条)。

**📜 核心创作铁律合集 (The 18 Iron Laws)**

**第一类:视觉氛围与工厂设定**
1.  **BBC 质感与风格**:所有镜头必须具备 **BBC 纪录片风格**。画面必须干净、电影级。**绝对禁止字幕**。
2.  **密度分流法则 (The Law of Density)** [🔥核心修正]:
    * **默认状态 (99%场景)**:生产/运输/清洗/堆叠 -> **必须是窒息密度**: `Thousands of (成千上万), A sea of (海洋般的), Filling the frame (填满画面)`。
    * **特例状态 (1%场景)**:仅在抽样/显微镜/检测 -> 必须是 `Single (单个), Isolated (独立的)`。
3.  **重力来源法则**:严禁魔幻般“从天而降”。所有倾泻必须写明源头设备(如:`pouring from a stainless steel hopper`)。
4.  **高亮食欲光**:明亮通透,拒绝暗调。
5.  **泥土与洁净冲突**:清洗前必须脏(带泥土),清洗后必须净(水珠光泽)。

**第二类:人机协同与安全**
6.  **人物与人声铁律** [🔥绝对红线]:
    * **视觉比例**:人机协同画面严格控制在 **总镜头的 30% 以内**。机器永远是主角。
    * **听觉禁令**:**全片绝对禁止人声** (0% Human Voice)。严禁出现:说话声、采访声、旁白。
7.  **专业形象**:`身穿白色无菌服的工人, 蓝色乳胶手套, 防尘网帽`。
8.  **安全约束**:严禁血腥。描述必须是 `强力(Powerful), 极速(High-Speed)`。

**第三类:物理质感与形态**
9.  **物料演变法则**:严禁穿越。捕捉形态改变瞬间。
10. **湿润与材质质感**:强调湿润感、光泽感。
11. **刚体碰撞法则**:
    * 拒绝柔和。固体是撞击,不是流动。
    * 关键词:`刚性碰撞, 互相挤压, 剧烈弹跳, 机械震动`。

**第四类:运镜与机械逻辑**
12. **开篇震撼法则**:Shot 1 必须是宏大的源头场景(农田/矿山)。严禁直接从工厂开始。
13. **高速通量法则**:传送带永远在高速运转,机械韵律即BGM。
14. **机械秩序法则**:机器动作无卡顿,展现毫秒级同步的数学美感。
15. **机械原理揭示**:`微距特写, 刀片切入点, 摩擦纹理, 物理接触面`。
16. **沉浸式运镜**:`第一人称视角 (POV), 传送带视角, 追踪镜头, Probe Lens (探针镜头)`。

**第五类:ASMR 听觉铁律**
17. **三维听觉纹理**:
    * **强制拟声词**:必须包含如 `[咔哒 Click]`, `[滋滋 Zzzzt]`, `[咕嘟 Gurgle]`。
    * **材质绑定**:金属=清脆;液体=粘稠/飞溅;重物=沉闷。
18. **全链路物理传输法则**:**绝不瞬移**。前段要有完整的 `收割->装车->运输->卸货` 链条。

**输出格式 (7D 旗舰版 A++ Optimized) - 必须输出 [用户指定数量] 个镜头!**

**⚠️ 强制锚点协议 (Mandatory Anchor Protocol)**:
为了确保解析准确,**每一个镜头**的上方必须严格包含锚点代码:`[[SHOT_ID: 序号]]`。

[[SHOT_ID: 序号]]
镜头 [序号] | [中文标题]
Veo 提示词 (格式 A++ Optimized):
01. 主体与密度:[🔥核心死令:必须显式包含[产品名]!禁止代词。必须是“无尽的[产品]阵列”、“[产品]的洪流”。如果产品未入场,则描述“油的海洋”或“蒸汽墙”。严禁单数。]
02. 材质与光影:[合并描述:表面纹理(粗糙/湿润/油亮) + 光照交互(金属反光/透光/高光)]
03. 动作演变:[🎬 关键!描述动作。如果是“重复性解压镜头”,请强调其持续性、循环性、无休止性]
04. 物理反馈:[合并描述:(质量/碰撞/流体/粒子)。如:沉重的撞击感、汁液飞溅、粉尘在空气中漂浮、刚体反弹]
05. 环境构建:[**收获阶段:自然场景(农田/矿山)**;进厂后:工厂场景(无菌室/不锈钢)。🔥严禁“纯黑虚空”!]
06. 镜头语言:[🔥必须多变!在此处轮换使用:1.上帝俯视 2.侧面平视 3.探针穿越 4.微距特写]
07. [ASMR] 核心音效:[🔥强制格式:[拟声词] + [材质形容词] + [动作描述] + [空间感]。]
"""

# 3. 导演 2.0 (🔥保持不变:铁律执行引擎)
FALLBACK_DIRECTOR_V2 = """
你现在是 **导演 2.0 (状态进化与逻辑审计引擎)**。
你拥有 **绝对的修改权**,必须保证以下逻辑被 **100% 执行**。

**💀 导演 2 核心逻辑补丁 (Director 2 Logic Patch)**
**【以下两条指令具有最高优先级,覆盖一切旧逻辑】**

**⚠️ 补丁 1:🛑 强制名词复诵 (Mandatory Noun Repetition)**
* **指令详情**:在 **01. 主体与密度** 中,以及所有出现产品的地方,**绝对禁止**使用代词(It, They)或模糊指代(The liquid, The mixture, The strips, The object)。
* **执行标准**:
    * **必须写全名**:比如是石榴汁,就必须写 `The ruby-red Pomegranate Juice` (红宝石般的石榴汁)。比如是土豆条,就必须写 `Wet Potato Strips` (湿润的土豆条)。
    * **中间态锁定**:即使形态改变,也要带上名字。如 `Crushed Potato Mash` (粉碎的土豆泥)。
    * **例外**:只有在 `Empty Machine Preparation` (完全空机) 阶段,才允许不写产品名。一旦产品入画,必须复诵全名!

**⚠️ 补丁 2:🌊 99% 密度压制 (99% Density Dominance)**
* **指令详情**:除非是 **[实验室]**、**[显微镜]** 或 **[单体破坏测试]** 场景(这属于 1% 的例外),否则 **99% 的场景**(运输、清洗、切割、灌装、混合)必须展示 **“成千上万”** 的产品阵列。
* **执行标准**:
    * **必须使用**:`Thousands of`, `A sea of`, `An endless flood of`, `Filling the entire frame`。
    * **拒绝留白**:传送带上不能只有几根土豆条,必须是**堆积如山的土豆条**。

**💀 语义清洗与进化法则 (常规审计)**

**0. ⚠️ 主权归还审计 (Subject Restoration Audit)**
* **检测**:检查 `主体` 字段。
* **规则**:如果 `主体` 描述的是 **机器、工具、容器、环境** 或 **工人/人类**,这是**严重错误**。
* **修正执行**:将 `主体` **强制重写** 为 **“正在经历该工序的 [产品名]”** (需遵守补丁1)。

**1. ⚠️ 标题保护与进化协议**
* **保留原标题**:你必须保留导演 1.0 给出的原始中文标题。
* **进化标注**:仅当发生物理质变时追加 `[原标题] + [进化:变化描述]`。

**2. 🛡️ 绝对安全协议**
* **严禁词汇**:绝对禁止 "死刑"、"暴力"、"血腥"。
* **替换策略**:使用 "精密裁切" (Precision Cutting)、"高能冲击" (High Energy Impact)。

**3. 🎵 听觉审计与润色**
* **拒绝模糊**:将 "机器声" 改写为 **[嗡嗡 Hum]** 等 ASMR 描述。
* **清洗负面听觉**:删除 "刺耳"、"尖叫"、"噪音"。

**4. ⚠️ 绝对物种锁定**
* **纠错**:出现非 [产品] 的名词(如幻觉成甜菜),强制替换回 [产品]。

**5. ⚠️ 容器一致性审计**
* **规则**:一旦确定包装材质(如玻璃瓶),后续严禁变更。

**6. ⚠️ 客观密度与重力源头审计 (Logic Audit)**
* **密度审计**:执行补丁2 (99%密度)。
* **重力源审计**:如果描述了“倾泻/掉落”,强制加上 `从不锈钢料斗倾泻`。

**7. 极度冗余的主体描述**
* **规则**:每个镜头的主体必须是**独立、完整、详细**的描述。
* **执行**:堆叠形容词。不要只写 "切片",要写 "完美均匀的圆形[产品]切片,闪烁着汁液"。

**8. 形态进化与强制替换**:
    * **当 [切片] 发生后**: 删除 "完整",替换为 "切片/横截面"。
    * **当 [粉碎] 发生后**: 删除 "块状",替换为 "碎屑/粉尘"。

**9. 清洁度进化**:
    * **当 [清洗] 发生后**:强制删除 "脏/泥泞",强制添加 "干净,晶莹剔透"。

**10. 🌊 主体豁免与介质置换协议**:
    * **死锁解决**:当产品尚未入场时,将“工业介质”(油/水/蒸汽)升级为高密度主体(如“金色的油海”)。

**输出格式 (必须完全一致,包含19个核心维度):**

**⚠️ 锚点保留协议 (Anchor Preservation)**:
必须保留输入中的 `[[SHOT_ID: 序号]]` 标记,放在每个镜头的最开头。严禁删除或修改此ID。

[[SHOT_ID: 序号]]
镜头 [序号]/[总数] | [中文标题] (+ [进化标注] 如有)
Veo 提示词 (格式 A++ Optimized):
01. 主体与密度:[🔥核心补丁:拒绝代词,强制复诵[产品全名]。99%场景必须是“无尽的[产品]阵列”、“[产品]的洪流”。]
02. 材质与光影:[合并描述:表面纹理(粗糙/湿润) + 光照交互(金属反光/高光/次表面散射)]
03. 动作演变:[🎬 关键!用一句话描述 T=0s(入画) -> T=4s(高潮/撞击) -> T=8s(出画) 的完整过程。动作必须连贯]
04. 物理反馈:[合并描述:包含原来的(质量/碰撞/流体/粒子)。如:沉重的撞击感、汁液飞溅、粉尘在空气中漂浮、刚体反弹]
05. 环境构建:[**收获阶段:自然场景(农田/矿山)**;进厂后:工厂场景(无菌室/不锈钢)。🔥严禁“纯黑虚空”!]
06. 镜头语言:[合并描述:机位(微距/探针) + 运镜(跟随/推拉) + 光学参数(广角/眩光)]
07. [ASMR] 核心音效:[🔥强制硬核:[拟声词] + [材质形容词] + [动作描述]。例如:**[滋滋 Zzzzt]** 锋利刀片切开多汁果肉的**湿润撕裂声**。]

[状态连接]: [用于下一个镜头的干净、优化的形容词列表。例如 "干净的,去皮的,切片的"]
"""

# ===============================================
# --- 提示词管理函数 ---
# ===============================================
def load_prompts():
    """从 JSON 加载提示词,增加兜底逻辑"""
    p_eng, p_v1, p_v2 = FALLBACK_ENGINEER, FALLBACK_DIRECTOR_V1, FALLBACK_DIRECTOR_V2 # 默认值
    
    if os.path.exists(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                data = json.load(f)
                # 使用 get 并提供 fallback,防止 json 里的 key 缺失
                p_eng = data.get("engineer", FALLBACK_ENGINEER)
                p_v1 = data.get("director1", FALLBACK_DIRECTOR_V1)
                p_v2 = data.get("director2", FALLBACK_DIRECTOR_V2)
        except Exception as e:
            pass
            
    return p_eng, p_v1, p_v2

def save_prompts(p_eng, p_v1, p_v2):
    """保存提示词到 JSON"""
    data = {
        "engineer": p_eng,
        "director1": p_v1,
        "director2": p_v2,
        "last_updated": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    }
    try:
        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=4)
        return f"✅ 配置已保存 ({data['last_updated']})"
    except Exception as e:
        return f"❌ 保存失败: {e}"

def refresh_config_on_load():
    """刷新回调,确保返回正确的值"""
    return load_prompts()

# ===============================================
# --- 核心逻辑 (API 调用) ---
# ===============================================

def get_timestamp():
    return datetime.datetime.now().strftime("%H:%M:%S")

def log_msg(logs, msg, level="INFO"):
    icon = {"INFO": "ℹ️", "PROCESS": "⚙️", "SUCCESS": "✅", "WARN": "⚠️", "STATE": "🧬", "WAIT": "⏳", "NET": "📡", "RETRY": "🔄", "ERROR": "❌", "PAUSE": "⏸️", "AUDIT": "👁️"}.get(level, "")
    entry = f"[{get_timestamp()}] {icon} {msg}"
    logs.append(entry)
    return "\n".join(logs)

# 1. 工程师
def execute_engineer(topic, api_key, system_prompt):
    if not api_key: yield "❌ 未提供 API Key"; return
    yield "⏳ [工程师] 正在构建现代工厂工艺流程 (全链路+裂变模式)..."
    
    url = f"{MERCHANT_BASE_URL}/v1/chat/completions"
    headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
    data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": f"产品: {topic}"}]}
    
    for attempt in range(5):
        try:
            yield f"📡 [工程师] 正向 LLM 发送请求 (尝试 {attempt+1}/5)..."
            res = requests.post(url, headers=headers, json=data, timeout=120)
            if res.status_code != 200: raise Exception(res.text)
            yield res.json()['choices'][0]['message']['content']
            return
        except Exception as e:
            if attempt == 4: yield f"❌ 错误 (最终失败): {e}"
            else: yield f"⚠️ 请求失败: {e},正在重试..."
            time.sleep(2)

# 2. 导演 1.0 (分批接力 + 全量历史)
def execute_director_v1(topic, architecture, count, api_key, system_prompt):
    if not architecture: yield "❌ 请先生成工艺架构"; return
    
    V1_BATCH_SIZE = 10
    
    # 🔥 核心修正:使用列表来存储所有结果,防止覆盖
    v1_all_results = []
    
    packaging_start_shot = int(count * 0.8) + 1
    yield f"⏳ [导演 1.0] 正在构思 {count} 个镜头的解压沉浸剧本..."
    yield f"📌 [时间线控制] Shot 1=源头, Shot {packaging_start_shot}=包装开始 (97% 处)"

    for batch_start in range(1, count + 1, V1_BATCH_SIZE):
        batch_end = min(batch_start + V1_BATCH_SIZE - 1, count)
        current_batch_count = batch_end - batch_start + 1
        
        yield f"📡 [导演 1.0] 正在生成第 {batch_start} - {batch_end} 个镜头 (Batch Processing)..."
        
        # 构建当前全量剧本用于上下文 (只取列表中的 string)
        current_full_script = "\n".join(v1_all_results)
        
        context_instruction = ""
        if current_full_script:
            context_instruction = f"""
            **⚠️ 前序镜头全量历史 (Context History - DO NOT REPEAT)**:
            {current_full_script}
            """
        
        user_content = f"""
        产品: {topic}
        工程协议:
        {architecture}
        
        {context_instruction}
        
        任务: 生成第 {batch_start}{batch_end} 个镜头 (共 {current_batch_count} 个)。
        
        **⚠️ 时间线与包装死令**:
        1. **起始点**:如果包含 Shot 1,**必须是【原料采集/源头】**。
        2. **包装禁令**:**在第 {packaging_start_shot} 个镜头之前,绝对禁止进入包装环节!**
        
        **⚠️ 强制格式锚点 (Format Anchor)**:
        必须严格执行提示词中的输出格式,每个镜头前必须加上 `[[SHOT_ID: 序号]]`。
        
        **警告:必须真实生成 {current_batch_count} 个独立的提示词块!**
        输出: 结构化镜头列表,全部使用中文。
        """
        
        url = f"{MERCHANT_BASE_URL}/v1/chat/completions"
        headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
        data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}]}
        
        success = False
        for attempt in range(5):
            try:
                res = requests.post(url, headers=headers, json=data, timeout=300)
                if res.status_code != 200: raise Exception(f"API Error: {res.text}")
                
                new_content = res.json()['choices'][0]['message']['content']
                
                # 🔥 增加空内容防御
                if not new_content or len(new_content) < 50:
                    raise Exception("返回内容为空或过短,可能被安全拦截")
                
                # 🔥 存入列表
                v1_all_results.append(new_content)
                
                # 🔥 强制拼合并返回
                yield "\n\n".join(v1_all_results)
                success = True
                break
            except Exception as e:
                yield f"⚠️ Batch 失败 (尝试 {attempt+1}/5): {e}"
                time.sleep(2)
        
        if not success:
            yield "❌ 致命错误:多次重试失败,流程终止。"
            return

# 3. 导演 2.0 API (带 prev_context_shots)
def call_director_v2_batch_api(batch_plans, start_idx, total, prev_state, prev_context_shots, api_key, system_prompt):
    
    joined_plans = "\n\n".join(batch_plans)
    packaging_start_shot = int(total * 0.8) + 1
    
    # 🔥 核心修改:回溯指令 (首批次豁免 + N-1 自动逻辑)
    context_instruction = ""
    if prev_context_shots and start_idx > 1:
        # 如果是 Shot 7,list slice [-10:] 会返回 Shot 1-6,符合 "N-1" 要求
        context_instruction = f"""
        **👁️ 扩展视觉记忆 (Extended Visual Memory - Previous Shots)**:
        以下是该镜头之前 **最多10个镜头** 的记录 (用于确保物理状态连续)。
        ---
        {prev_context_shots}
        ---
        **强制约束**: Shot {start_idx} 必须继承[产品]的真实物理状态,并延续视觉风格。
        """
    else:
        # 🔥 Shot 1 不回溯
        context_instruction = "**【初始批次】:这是视频的开头(Shot 1),无需回溯历史。请建立基准物理状态(通常为原料采集)。**"
    
    user_content = f"""
    **批量任务**: 正在处理第 {start_idx} 到第 {start_idx + len(batch_plans) - 1} 个镜头 (共 {len(batch_plans)} 个)。
    
    **⚠️ 包装时间线检查**:
    - 包装允许开始点: Shot {packaging_start_shot}
    
    🧬 **初始状态**: "{prev_state}"
    
    {context_instruction}
    
    📜 **导演 1 原始计划组**: 
    {joined_plans}
    
    **你的任务**: 
    请**依次**处理这 {len(batch_plans)} 个镜头。
    **输出格式要求**:
    必须输出 {len(batch_plans)} 个完整的 7D 格式块,每个都要带 `[[SHOT_ID]]` 锚点。
    在所有镜头输出完毕后,**必须**输出一行最终状态:
    `[BATCH_END_STATE]: <这里写最后一个镜头完成后的物理状态>`
    """
    
    url = f"{MERCHANT_BASE_URL}/v1/chat/completions"
    headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
    data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}], "temperature": 0.3}
    
    try:
        res = requests.post(url, headers=headers, json=data, timeout=300)
        content = res.json()['choices'][0]['message']['content']
        
        final_state = prev_state
        if "[BATCH_END_STATE]" in content:
            parts = content.split("[BATCH_END_STATE]")
            content_body = parts[0].strip()
            state_raw = parts[1].strip()
            if state_raw.startswith(":") or state_raw.startswith(":"): 
                final_state = state_raw[1:].strip()
        else:
            content_body = content 
            
        return content_body, final_state
        
    except Exception as e: return None, str(e)

# 3. 导演 2.0 循环 (结果缓存 + 10段回溯)
def execute_director_v2_loop(v1_text, count, api_key, system_prompt):
    if not v1_text: yield "❌ 请先生成导演 1.0 的视觉方案", ""; return
    global IS_PAUSED
    IS_PAUSED = False
    logs = []
    
    # 🔥 核心修正:UI 持久化容器
    # 只要生成了,就塞进这里,永远不丢
    all_generated_batches = []
    
    full_results_history = [] 
    
    raw_text = v1_text.strip()
    
    # 🔥 核心修复:双重锚点解析 (兼容性 MAX)
    header_pattern = re.compile(r'(?:\[\[\s*SHOT_ID\s*[::]\s*(\d+)\s*\]\]|###\s*镜头\s*(\d+))', re.IGNORECASE)
    matches = list(header_pattern.finditer(raw_text))
    
    if not matches:
        yield log_msg(logs, f"❌ 严重错误:未找到锚点。请确认导演 1.0 输出包含 [[SHOT_ID]] 或 ### 镜头。", "ERROR"), ""
        return

    visual_plans = []
    seen_ids = set() # 去重,防止同一镜头被重复解析
    
    for i in range(len(matches)):
        start = matches[i].start()
        end = matches[i+1].start() if i+1 < len(matches) else len(raw_text)
        block = raw_text[start:end].strip()
        
        try:
            val1 = matches[i].group(1)
            val2 = matches[i].group(2)
            shot_id = int(val1) if val1 else int(val2)
        except:
            shot_id = i + 1 
            
        if shot_id not in seen_ids and len(block) > 5:
             visual_plans.append({"id": shot_id, "content": block})
             seen_ids.add(shot_id)

    # 排序以防万一
    visual_plans.sort(key=lambda x: x['id'])

    yield log_msg(logs, f"🚀 导演 2.0 启动:状态进化引擎 (7D 旗舰版 | Batch={BATCH_SIZE})", "SUCCESS"), ""
    yield log_msg(logs, f"📊 成功解析 {len(visual_plans)} 个镜头 (首镜ID: {visual_plans[0]['id']})", "INFO"), ""
    
    prev_state = "原材料,脏的,完整的,未处理的 (自然状态)"
    yield log_msg(logs, f"🧬 [初始状态] {prev_state}", "STATE"), ""

    total_shots = len(visual_plans)
    
    for i in range(0, total_shots, BATCH_SIZE):
        batch_items = visual_plans[i : i + BATCH_SIZE]
        batch_texts = [item['content'] for item in batch_items]
        
        start_real_id = batch_items[0]['id']
        end_real_id = batch_items[-1]['id']
        
        # 强制显式返回一次全量数据
        full_text_snapshot = "\n\n".join(all_generated_batches)
        if IS_PAUSED:
            yield log_msg(logs, f"⏸️ 已暂停...", "PAUSE"), full_text_snapshot
            while IS_PAUSED: time.sleep(1)
            yield log_msg(logs, f"▶️ 恢复", "PROCESS"), full_text_snapshot

        # 回溯逻辑
        prev_context_shots = ""
        if full_results_history:
            last_10 = full_results_history[-10:]
            prev_context_shots = "\n\n".join(last_10)
            yield log_msg(logs, f"👁️ [视觉审阅] 回溯 Shot {max(1, start_real_id-10)} - {start_real_id-1}...", "AUDIT"), full_text_snapshot
        else:
            yield log_msg(logs, f"👁️ [视觉审阅] 初始批次 (Shot 1),无前序记忆", "INFO"), full_text_snapshot
            
        yield log_msg(logs, f"⚙️ [批量处理] 镜头 {start_real_id} - {end_real_id} ({len(batch_items)}个)...", "PROCESS"), full_text_snapshot
        yield log_msg(logs, f"🧬 [思维链继承] 起始状态: {prev_state}", "STATE"), full_text_snapshot
        
        success = False
        for attempt in range(5):
            if IS_PAUSED: 
                yield log_msg(logs, "⏸️ 暂停中...", "PAUSE"), full_text_snapshot
                while IS_PAUSED: time.sleep(1)
            try:
                if attempt > 0: yield log_msg(logs, f"   🔄 重试 ({attempt}/5)...", "RETRY"), full_text_snapshot
                
                # 🔥 传入真实的 start_real_id
                batch_result, next_state = call_director_v2_batch_api(
                    batch_texts, start_real_id, count, prev_state, prev_context_shots, api_key, system_prompt
                )
                
                if batch_result:
                    success = True
                    yield log_msg(logs, f"   📥 Batch 完成", "SUCCESS"), full_text_snapshot
                    
                    # 🔥 核心修正:追加到全局列表
                    all_generated_batches.append(batch_result)
                    full_text_snapshot = "\n\n".join(all_generated_batches) # 更新快照
                    
                    split_regex = r'(?:\[\[\s*SHOT_ID\s*[::]\s*\d+\s*\]\]|###\s*镜头\s*\d+)'
                    current_blocks = re.split(split_regex, batch_result)
                    current_blocks = [b.strip() for b in current_blocks if len(b) > 20]
                    full_results_history.extend(current_blocks)
                    
                    if prev_state != next_state:
                        yield log_msg(logs, f"   🌊 [进化] ... -> {next_state}", "STATE"), full_text_snapshot
                    else:
                        yield log_msg(logs, f"   🧬 [保持] {next_state}", "STATE"), full_text_snapshot
                    
                    prev_state = next_state 
                    break 
                time.sleep(1.5 * (attempt + 1))
            except Exception as e: 
                yield log_msg(logs, f"   ⚠️ Batch 错误: {e}", "WARN"), full_text_snapshot
                time.sleep(1.5 * (attempt + 1))
        
        if not success:
            yield log_msg(logs, f"   ❌ Batch 失败 (跳过)", "ERROR"), full_text_snapshot
        
        # 🔥 每一批次完成后,必须显式返回全量文本
        yield "\n".join(logs[-20:]), full_text_snapshot
        time.sleep(0.2) 

    yield log_msg(logs, "🎉 全片逻辑闭环完成", "SUCCESS"), "\n\n".join(all_generated_batches)

# ===============================================
# --- 渲染逻辑升级 (7要素拼接 + 格式化展示) ---
# ===============================================

def inject_satisfaction_physics(satisfaction_text):
    booster = ""
    if not satisfaction_text: return booster
    if any(k in satisfaction_text for k in ["填满", "堆叠", "丰盛", "空虚", "数量"]):
        booster += ",极高密度填充,画面无任何空隙,形成视觉实体墙,"
    if any(k in satisfaction_text for k in ["秩序", "同步", "整齐", "治愈", "强迫症"]):
        booster += ",完美几何对齐,毫秒级机械同步运动,无任何误差,"
    if any(k in satisfaction_text for k in ["剥离", "去除", "刮", "磨", "分离", "炸开"]):
        booster += ",表面污垢被物理暴力剥离,瞬间露出完美内部,"
    if any(k in satisfaction_text for k in ["净化", "洁净", "丝滑", "顺滑", "粘稠"]):
        booster += ",极度洁净的表面,丝滑的高光反射,无暇质感,"
    return booster

def assemble_veo_prompt(raw_block):
    """提取+拼接+转译 (适配新的 7D 格式)"""
    def extract(key_pattern, text):
        match = re.search(key_pattern, text)
        return match.group(1).strip().rstrip("。") if match else ""

    f_subject = extract(r"01\. 主体与密度:(.*?)\n", raw_block)
    f_texture = extract(r"02\. 材质与光影:(.*?)\n", raw_block)
    f_action = extract(r"03\. 动作演变:(.*?)\n", raw_block)
    f_physics = extract(r"04\. 物理反馈:(.*?)\n", raw_block)
    f_env = extract(r"05\. 环境构建:(.*?)\n", raw_block)
    f_camera = extract(r"06\. 镜头语言:(.*?)\n", raw_block)

    physics_booster = inject_satisfaction_physics(f_physics)

    parts = [
        "BBC纪录片风格,无字幕,无说话声,8k分辨率,超写实电影质感",
        f"{f_camera}",
        f"{f_subject}",
        f"{f_texture}",
        f"{f_action}{physics_booster}",
        f"{f_physics}",
        f"位于{f_env}"
    ]
    
    final_prompt = ",".join([p for p in parts if p and p.strip() != ","])
    final_prompt = re.sub(r",+", ",", final_prompt)
    final_prompt = f"{final_prompt} --ar 16x9"
    return final_prompt

def render_videos(script, topic, video_key, progress=gr.Progress()):
    if not script: yield "无脚本", None, ""; return
    session_dir = os.path.join("AutoSaved_Videos", f"{topic}_{int(time.time())}")
    os.makedirs(session_dir, exist_ok=True)
    
    # 🔥 核心修复:兼容双重锚点
    raw_blocks = []
    matches = list(re.finditer(r'(?:\[\[\s*SHOT_ID\s*[::]\s*(\d+)\s*\]\]|###\s*镜头\s*(\d+))', script, re.IGNORECASE))
    
    if not matches:
        logs = ["❌ 无法解析脚本:未找到任何 '[[SHOT_ID]]' 标记"]
        yield "\n".join(logs), None, ""; return

    for i in range(len(matches)):
        start = matches[i].start()
        if i < len(matches) - 1:
            end = matches[i+1].start()
        else:
            end = len(script)
        
        block = script[start:end].strip()
        if "Veo 提示词" in block or "01. 主体" in block:
            raw_blocks.append(block)

    logs = [f"🚀 全量并发渲染启动: 检测到 {len(raw_blocks)} 个独立镜头任务...", "✨ 已启用 [7大要素拼接] + [心理物理转译]"]
    yield "\n".join(logs), None, ""
    
    if len(raw_blocks) == 0:
        logs.append("❌ 未找到可渲染提示词块 (请确保脚本包含 7D 格式)"); yield "\n".join(logs), None, ""; return

    formatted_prompts_display = ""
    parsed_tasks = []
    
    for i, block in enumerate(raw_blocks):
        title_match = re.search(r'(?:镜头|Shot)\s*(\d+)(?:/\d+)?\s*[\||]\s*(.*)', block)
        title = title_match.group(2).strip() if title_match else f"Untitled"
        safe_title = re.sub(r'[\\/*?:"<>|]', "", title).replace(" ", "_")
        
        final_p = assemble_veo_prompt(block)
        if len(final_p) < 50: 
            pattern = r"(?:Veo 提示词|Veo Prompt).*?[::]\s*(.*?)(?=\[状态连接\]|\[NEXT_LINK\]|镜头|Shot|$)"
            match = re.search(pattern, block, re.DOTALL | re.IGNORECASE)
            if match:
                final_p = f"BBC纪录片风格,无字幕,无说话声,8k分辨率,超写实,{match.group(1).strip().replace(chr(10), ' ')} --ar 16x9"
        
        shot_num = f"{i+1:03d}"
        formatted_entry = f"=== Shot {shot_num}/{len(raw_blocks)}: {title} ===\nveo Prompt (English): {final_p}\n\n"
        formatted_prompts_display += formatted_entry
        
        fname = f"Shot_{shot_num}_{safe_title}"
        parsed_tasks.append((final_p, fname))

    yield "\n".join(logs), None, formatted_prompts_display

    files = []
    with ThreadPoolExecutor(max_workers=len(parsed_tasks)) as executor:
        futures = {}
        for final_p, fname in parsed_tasks:
            futures[executor.submit(simple_veo_call, final_p, fname, session_dir, video_key)] = fname
        
        done = 0
        for f in as_completed(futures):
            res = f.result()
            done += 1
            progress(done/len(parsed_tasks))
            if res['ok']: 
                logs.append(f"✅ {res['name']} 完成")
                files.append(res['path'])
            else: 
                error_msg = res.get('error', '未知错误')
                logs.append(f"❌ {res['name']} 失败: {error_msg}")
            yield "\n".join(logs[-15:]), None, formatted_prompts_display
            
    if files:
        shutil.make_archive(session_dir, 'zip', session_dir)
        yield "\n".join(logs), f"{session_dir}.zip", formatted_prompts_display

def simple_veo_call(prompt, name, folder, key):
    last_error = ""
    for attempt in range(5):
        try:
            url = f"{MERCHANT_BASE_URL}/v1/chat/completions"
            headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
            data = {"model": VEO_MODEL, "messages": [{"role": "user", "content": prompt}], "size": VIDEO_SIZE}
            r = requests.post(url, headers=headers, json=data, timeout=300)
            
            if r.status_code != 200:
                raise Exception(f"API Error {r.status_code}: {r.text[:100]}") 

            content = r.json()['choices'][0]['message']['content']
            v_url = re.search(r'(https?://[^\s)"]+)', content).group(1)
            
            path = os.path.join(folder, name + ".mp4")
            with open(path, "wb") as f: f.write(requests.get(v_url).content)
            
            return {"ok": True, "name": name, "path": path}
        except Exception as e:
            last_error = str(e)
            time.sleep(2 * (attempt + 1))
            continue 
            
    return {"ok": False, "name": name, "error": last_error}

# ===============================================
# --- UI 界面构造 ---
# ===============================================
with gr.Blocks(title="Veo 终极全能引擎 (全链路覆盖版)") as app:
    
    init_eng, init_v1, init_v2 = load_prompts()

    gr.Markdown("## 🏭 Veo 终极全能引擎 (全链路覆盖版:微观拆解 + 多机位重复 + 7D)")
    gr.Markdown("**提示**:此版本已强制工程师覆盖从【源头收获】到【包装物流】的全流程,并保留了所有历史铁律。")

    # 0. 全局配置 & 保存按钮
    with gr.Row(variant="panel"):
        with gr.Column(scale=2):
            with gr.Row():
                llm_key = gr.Textbox(label="🔑 LLM Key", value=DEFAULT_LLM_API_KEY, type="password")
                video_key = gr.Textbox(label="🎬 Video Key", value=DEFAULT_VIDEO_API_KEY, type="password")
        with gr.Column(scale=1):
            btn_save_config = gr.Button("💾 保存所有配置", variant="primary")
            save_status = gr.Textbox(label="状态", lines=1, interactive=False)
            topic_input = gr.Textbox(label="📦 产品名称", placeholder="例如:菠萝")

    # 1. 工程师
    with gr.Row():
        with gr.Column():
            gr.Markdown("### 🛠️ 步骤 1: 工程师 (全链路拆解)")
            # 🔥 修改点:open=True,强制展开
            with gr.Accordion("提示词设定 (可修改)", open=True):
                prompt_eng_input = gr.Textbox(value=init_eng, lines=10, label="工程师提示词", interactive=True)
            btn_eng = gr.Button("1. 生成海量素材日志", variant="secondary")
        with gr.Column():
            arch_output = gr.Textbox(label="工艺架构输出", lines=10)

    gr.HTML("<hr>")

    # 2. 导演 1.0
    with gr.Row():
        with gr.Column():
            gr.Markdown("### 🎨 步骤 2: 导演 1.0 (最终剪辑与铺排)")
            # 🔥 默认值改为 200,最大值改为 300
            count_slider = gr.Slider(1, 300, 200, step=1, label="镜头数量")
            # 🔥 修改点:open=True,强制展开
            with gr.Accordion("提示词设定 (可修改)", open=True):
                prompt_v1_input = gr.Textbox(value=init_v1, lines=15, label="导演1.0提示词", interactive=True)
            btn_v1 = gr.Button("2. 生成完整剧本", variant="primary")
        with gr.Column():
            v1_output = gr.Textbox(label="V1 剧本输出", lines=15)

    gr.HTML("<hr>")

    # 3. 导演 2.0
    with gr.Row():
        with gr.Column():
            gr.Markdown("### 🧬 步骤 3: 导演 2.0 (状态进化)")
            # 🔥 修改点:open=True,强制展开
            with gr.Accordion("提示词设定 (可修改)", open=True):
                prompt_v2_input = gr.Textbox(value=init_v2, lines=10, label="导演2.0提示词", interactive=True)
            with gr.Row():
                btn_v2 = gr.Button("3. 启动逻辑变异 (Batch=10)", variant="primary")
                btn_pause = gr.Button("⏸️ 暂停/继续", variant="secondary")
            log_output = gr.Textbox(label="演算日志", lines=15, elem_id="log_box", autoscroll=True)
        with gr.Column():
            v2_output = gr.Textbox(label="最终脚本输出", lines=20, autoscroll=True)

    gr.HTML("<hr>")

    # 4. 渲染
    with gr.Row(variant="panel"):
        with gr.Column(scale=1):
            gr.Markdown("### 🚀 步骤 4: 渲染")
            btn_render = gr.Button("4. 全量并发渲染", variant="stop")
            zip_output = gr.File(label="下载 ZIP")
        with gr.Column(scale=2):
            final_prompts_output = gr.Textbox(label="拼接后的提示词 (Assembled Prompts)", lines=20, interactive=False)

    # 绑定加载事件
    app.load(refresh_config_on_load, inputs=None, outputs=[prompt_eng_input, prompt_v1_input, prompt_v2_input])
    # 绑定按钮事件
    btn_save_config.click(save_prompts, [prompt_eng_input, prompt_v1_input, prompt_v2_input], [save_status])
    btn_eng.click(execute_engineer, [topic_input, llm_key, prompt_eng_input], [arch_output])
    btn_v1.click(execute_director_v1, [topic_input, arch_output, count_slider, llm_key, prompt_v1_input], [v1_output])
    btn_pause.click(toggle_pause, [], [btn_pause])
    btn_v2.click(execute_director_v2_loop, [v1_output, count_slider, llm_key, prompt_v2_input], [log_output, v2_output])
    btn_render.click(render_videos, [v2_output, topic_input, video_key], [log_output, zip_output, final_prompts_output])

app.launch()