Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -64,7 +64,7 @@ else:
|
|
| 64 |
ROOT_BASE = os.getcwd()
|
| 65 |
|
| 66 |
TASKS_ROOT_DIR = os.path.join(ROOT_BASE, "nihaisha_tasks_v14_final")
|
| 67 |
-
CORPUS_ROOT_DIR = os.path.join(ROOT_BASE, "nihaisha_corpus_lib")
|
| 68 |
PERSONAS_FILE = os.path.join(ROOT_BASE, "personas_config.json")
|
| 69 |
|
| 70 |
os.makedirs(TASKS_ROOT_DIR, exist_ok=True)
|
|
@@ -81,11 +81,13 @@ class PersonaManager:
|
|
| 81 |
"user_template": "【任务】:请根据参考资料总结核心内容。\n【主题】:{topic}"
|
| 82 |
}
|
| 83 |
}
|
| 84 |
-
|
|
|
|
|
|
|
| 85 |
|
| 86 |
def load_personas(self):
|
|
|
|
| 87 |
if not os.path.exists(self.filepath):
|
| 88 |
-
self.save_personas(self.default_personas)
|
| 89 |
return self.default_personas
|
| 90 |
try:
|
| 91 |
with open(self.filepath, 'r', encoding='utf-8') as f:
|
|
@@ -96,22 +98,27 @@ class PersonaManager:
|
|
| 96 |
return self.default_personas
|
| 97 |
|
| 98 |
def save_personas(self, new_data):
|
|
|
|
| 99 |
try:
|
| 100 |
with open(self.filepath, 'w', encoding='utf-8') as f:
|
| 101 |
json.dump(new_data, f, indent=4, ensure_ascii=False)
|
| 102 |
-
|
|
|
|
|
|
|
| 103 |
return True
|
| 104 |
except Exception as e:
|
| 105 |
print(f"❌ 保存失败: {e}")
|
| 106 |
return False
|
| 107 |
|
| 108 |
def get_persona(self, name):
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
| 112 |
|
| 113 |
def get_all_names(self):
|
| 114 |
-
|
|
|
|
| 115 |
|
| 116 |
persona_manager = PersonaManager()
|
| 117 |
|
|
@@ -532,31 +539,57 @@ def start_execution(topic_text, persona_name, p_sys, p_user, selected_refs):
|
|
| 532 |
threading.Thread(target=worker, args=(tid, topics, persona_name, p_sys, p_user, selected_refs, manual_topic_name)).start()
|
| 533 |
return f"任务已启动: {tid}", tid
|
| 534 |
|
| 535 |
-
# --- UI Helper ---
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
|
| 540 |
def on_select_persona_config(name):
|
|
|
|
|
|
|
| 541 |
p = persona_manager.get_persona(name)
|
|
|
|
|
|
|
| 542 |
return p.get("system"), p.get("user_template")
|
| 543 |
|
| 544 |
def save_new_persona(name, sys, user):
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
| 546 |
current = persona_manager.load_personas()
|
| 547 |
current[name] = {"system": sys, "user_template": user}
|
|
|
|
| 548 |
if persona_manager.save_personas(current):
|
| 549 |
-
|
|
|
|
|
|
|
| 550 |
else:
|
| 551 |
-
return f"❌ 保存失败", gr.update()
|
| 552 |
|
| 553 |
def delete_persona(name):
|
| 554 |
-
|
|
|
|
|
|
|
|
|
|
| 555 |
current = persona_manager.load_personas()
|
| 556 |
if name in current:
|
| 557 |
del current[name]
|
| 558 |
persona_manager.save_personas(current)
|
| 559 |
-
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
# --- 界面布局 ---
|
| 562 |
|
|
@@ -571,10 +604,11 @@ with gr.Blocks(title="AI 灵活内容工厂 (V14 Final)") as demo:
|
|
| 571 |
|
| 572 |
with gr.Tabs():
|
| 573 |
# --- Tab 1: 创作中心 ---
|
| 574 |
-
with gr.Tab("🚀 1. 创作中心"):
|
| 575 |
with gr.Row():
|
| 576 |
with gr.Column(scale=1):
|
| 577 |
gr.Markdown("### 配置与启动")
|
|
|
|
| 578 |
persona_select = gr.Dropdown(label="加载预设角色 (选填)", choices=persona_manager.get_all_names(), interactive=True)
|
| 579 |
topic_input = gr.Textbox(label="输入主题 (一行一个)", lines=3, placeholder="留空则自动拟题...")
|
| 580 |
|
|
@@ -591,10 +625,11 @@ with gr.Blocks(title="AI 灵活内容工厂 (V14 Final)") as demo:
|
|
| 591 |
start_msg = gr.Label(show_label=False)
|
| 592 |
|
| 593 |
# --- Tab 2: 角色配置 ---
|
| 594 |
-
with gr.Tab("🎭 2. 角色配置"):
|
| 595 |
with gr.Row():
|
| 596 |
with gr.Column(scale=1):
|
| 597 |
gr.Markdown("### 角色管理")
|
|
|
|
| 598 |
p_config_select = gr.Dropdown(label="选择角色编辑", choices=persona_manager.get_all_names(), interactive=True)
|
| 599 |
p_name_input = gr.Textbox(label="新建/重命名", placeholder="角色名")
|
| 600 |
p_save_btn = gr.Button("💾 保存/新建", variant="primary")
|
|
@@ -636,37 +671,61 @@ with gr.Blocks(title="AI 灵活内容工厂 (V14 Final)") as demo:
|
|
| 636 |
outputs=task_dd
|
| 637 |
)
|
| 638 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
# 角色配置联动
|
| 640 |
persona_select.select(on_select_persona_config, persona_select, [p_sys_input, p_user_input])
|
|
|
|
|
|
|
| 641 |
p_config_select.select(on_select_persona_config, p_config_select, [p_sys_input, p_user_input])
|
| 642 |
p_config_select.select(lambda x: x, p_config_select, p_name_input)
|
| 643 |
|
| 644 |
-
|
| 645 |
-
p_save_btn.click(
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
analyze_btn.click(analyze_matches, topic_input, [ref_checkboxes, analyze_status, state_all_files])
|
| 650 |
select_all_btn.click(toggle_select_all, inputs=[ref_checkboxes, state_all_files], outputs=ref_checkboxes)
|
| 651 |
upload_btn.click(save_to_corpus, files, [corpus_display, upload_res])
|
| 652 |
clear_btn.click(clear_corpus, outputs=[corpus_display, upload_res])
|
| 653 |
|
| 654 |
-
#
|
| 655 |
-
# 每2秒从后台读取日志和结果文件路径
|
| 656 |
timer.tick(
|
| 657 |
lambda tid: (task_manager.get_task_info(tid)["log"], task_manager.get_task_info(tid)["result"]) if tid else ("", None),
|
| 658 |
inputs=task_dd,
|
| 659 |
outputs=[log_box, result_dl]
|
| 660 |
)
|
| 661 |
|
| 662 |
-
# 手动刷新按钮
|
| 663 |
refresh_task.click(lambda: gr.Dropdown(choices=task_manager.list_tasks(), value=task_manager.list_tasks()[0] if task_manager.list_tasks() else None), outputs=task_dd)
|
| 664 |
task_dd.change(lambda tid: (task_manager.get_task_info(tid)["log"], task_manager.get_task_info(tid)["result"]) if tid else ("", None), task_dd, [log_box, result_dl])
|
| 665 |
|
| 666 |
-
#
|
| 667 |
demo.load(list_corpus_files, outputs=corpus_display)
|
| 668 |
-
|
| 669 |
-
demo.load(
|
|
|
|
| 670 |
|
| 671 |
if __name__ == "__main__":
|
| 672 |
demo.queue().launch()
|
|
|
|
| 64 |
ROOT_BASE = os.getcwd()
|
| 65 |
|
| 66 |
TASKS_ROOT_DIR = os.path.join(ROOT_BASE, "nihaisha_tasks_v14_final")
|
| 67 |
+
CORPUS_ROOT_DIR = os.path.join(ROOT_BASE, "nihaisha_corpus_lib")
|
| 68 |
PERSONAS_FILE = os.path.join(ROOT_BASE, "personas_config.json")
|
| 69 |
|
| 70 |
os.makedirs(TASKS_ROOT_DIR, exist_ok=True)
|
|
|
|
| 81 |
"user_template": "【任务】:请根据参考资料总结核心内容。\n【主题】:{topic}"
|
| 82 |
}
|
| 83 |
}
|
| 84 |
+
# 启动时确保文件存在
|
| 85 |
+
if not os.path.exists(self.filepath):
|
| 86 |
+
self.save_personas(self.default_personas)
|
| 87 |
|
| 88 |
def load_personas(self):
|
| 89 |
+
"""强制从硬盘读取,不使用内存缓存"""
|
| 90 |
if not os.path.exists(self.filepath):
|
|
|
|
| 91 |
return self.default_personas
|
| 92 |
try:
|
| 93 |
with open(self.filepath, 'r', encoding='utf-8') as f:
|
|
|
|
| 98 |
return self.default_personas
|
| 99 |
|
| 100 |
def save_personas(self, new_data):
|
| 101 |
+
"""保存并强制刷入磁盘"""
|
| 102 |
try:
|
| 103 |
with open(self.filepath, 'w', encoding='utf-8') as f:
|
| 104 |
json.dump(new_data, f, indent=4, ensure_ascii=False)
|
| 105 |
+
f.flush()
|
| 106 |
+
os.fsync(f.fileno()) # 物理写入
|
| 107 |
+
time.sleep(0.05) # 微小延迟确保系统文件锁释放
|
| 108 |
return True
|
| 109 |
except Exception as e:
|
| 110 |
print(f"❌ 保存失败: {e}")
|
| 111 |
return False
|
| 112 |
|
| 113 |
def get_persona(self, name):
|
| 114 |
+
data = self.load_personas()
|
| 115 |
+
if name not in data and data:
|
| 116 |
+
return list(data.values())[0]
|
| 117 |
+
return data.get(name)
|
| 118 |
|
| 119 |
def get_all_names(self):
|
| 120 |
+
data = self.load_personas()
|
| 121 |
+
return list(data.keys())
|
| 122 |
|
| 123 |
persona_manager = PersonaManager()
|
| 124 |
|
|
|
|
| 539 |
threading.Thread(target=worker, args=(tid, topics, persona_name, p_sys, p_user, selected_refs, manual_topic_name)).start()
|
| 540 |
return f"任务已启动: {tid}", tid
|
| 541 |
|
| 542 |
+
# --- UI Helper (核心修复部分) ---
|
| 543 |
+
|
| 544 |
+
def global_refresh_dropdowns(target_value=None):
|
| 545 |
+
"""
|
| 546 |
+
通用刷新函数:强制从硬盘读取最新列表,并生成两个gr.update对象。
|
| 547 |
+
"""
|
| 548 |
+
all_names = persona_manager.get_all_names()
|
| 549 |
+
|
| 550 |
+
# 确定选中的值:如果指定值在列表里,就选它;否则选第一个;如果列表为空,则None
|
| 551 |
+
final_val = target_value if (target_value and target_value in all_names) else (all_names[0] if all_names else None)
|
| 552 |
+
|
| 553 |
+
# 返回两个更新指令,分别对应 Tab1 和 Tab2 的下拉框
|
| 554 |
+
# 注意:使用 gr.update(choices=..., value=...) 是解决不同步的终极方案
|
| 555 |
+
return gr.update(choices=all_names, value=final_val), gr.update(choices=all_names, value=final_val)
|
| 556 |
|
| 557 |
def on_select_persona_config(name):
|
| 558 |
+
if not name:
|
| 559 |
+
return "", ""
|
| 560 |
p = persona_manager.get_persona(name)
|
| 561 |
+
if not p:
|
| 562 |
+
return "", ""
|
| 563 |
return p.get("system"), p.get("user_template")
|
| 564 |
|
| 565 |
def save_new_persona(name, sys, user):
|
| 566 |
+
"""保存角色并原子化更新两个下拉框"""
|
| 567 |
+
if not name:
|
| 568 |
+
return "❌ 名字不能为空", gr.update(), gr.update()
|
| 569 |
+
|
| 570 |
current = persona_manager.load_personas()
|
| 571 |
current[name] = {"system": sys, "user_template": user}
|
| 572 |
+
|
| 573 |
if persona_manager.save_personas(current):
|
| 574 |
+
# 保存成功后,直接调用通用刷新函数,选中新创建的角色
|
| 575 |
+
u1, u2 = global_refresh_dropdowns(target_value=name)
|
| 576 |
+
return f"✅ 角色 '{name}' 已保存", u1, u2
|
| 577 |
else:
|
| 578 |
+
return f"❌ 保存失败", gr.update(), gr.update()
|
| 579 |
|
| 580 |
def delete_persona(name):
|
| 581 |
+
"""删除角色并原子化更新两个下拉框"""
|
| 582 |
+
if not name:
|
| 583 |
+
return "❌ 未选择", gr.update(), gr.update()
|
| 584 |
+
|
| 585 |
current = persona_manager.load_personas()
|
| 586 |
if name in current:
|
| 587 |
del current[name]
|
| 588 |
persona_manager.save_personas(current)
|
| 589 |
+
|
| 590 |
+
# 删除后,刷新列表,默认选中第一个
|
| 591 |
+
u1, u2 = global_refresh_dropdowns(target_value=None)
|
| 592 |
+
return f"🗑️ 已删除", u1, u2
|
| 593 |
|
| 594 |
# --- 界面布局 ---
|
| 595 |
|
|
|
|
| 604 |
|
| 605 |
with gr.Tabs():
|
| 606 |
# --- Tab 1: 创作中心 ---
|
| 607 |
+
with gr.Tab("🚀 1. 创作中心") as tab_create:
|
| 608 |
with gr.Row():
|
| 609 |
with gr.Column(scale=1):
|
| 610 |
gr.Markdown("### 配置与启动")
|
| 611 |
+
# 给组件定义变量名,方便后续更新
|
| 612 |
persona_select = gr.Dropdown(label="加载预设角色 (选填)", choices=persona_manager.get_all_names(), interactive=True)
|
| 613 |
topic_input = gr.Textbox(label="输入主题 (一行一个)", lines=3, placeholder="留空则自动拟题...")
|
| 614 |
|
|
|
|
| 625 |
start_msg = gr.Label(show_label=False)
|
| 626 |
|
| 627 |
# --- Tab 2: 角色配置 ---
|
| 628 |
+
with gr.Tab("🎭 2. 角色配置") as tab_config:
|
| 629 |
with gr.Row():
|
| 630 |
with gr.Column(scale=1):
|
| 631 |
gr.Markdown("### 角色管理")
|
| 632 |
+
# 这里定义变量名
|
| 633 |
p_config_select = gr.Dropdown(label="选择角色编辑", choices=persona_manager.get_all_names(), interactive=True)
|
| 634 |
p_name_input = gr.Textbox(label="新建/重命名", placeholder="角色名")
|
| 635 |
p_save_btn = gr.Button("💾 保存/新建", variant="primary")
|
|
|
|
| 671 |
outputs=task_dd
|
| 672 |
)
|
| 673 |
|
| 674 |
+
# ================= 核心修复逻辑:Tab 切换自动刷新 =================
|
| 675 |
+
# 原理:只要用户点击 Tab 1 或 Tab 2,就强制刷新该页面下的下拉框
|
| 676 |
+
# 这确保了即使按钮更新失败,切换一下页面也能立刻修复
|
| 677 |
+
|
| 678 |
+
def tab_refresh_handler():
|
| 679 |
+
u1, u2 = global_refresh_dropdowns()
|
| 680 |
+
return u1 # 返回给当前Tab的组件
|
| 681 |
+
|
| 682 |
+
# 绑定 Tab 选中事件 (Gradio 4.x+)
|
| 683 |
+
tab_create.select(fn=lambda: global_refresh_dropdowns()[0], inputs=None, outputs=persona_select)
|
| 684 |
+
tab_config.select(fn=lambda: global_refresh_dropdowns()[1], inputs=None, outputs=p_config_select)
|
| 685 |
+
|
| 686 |
+
# =============================================================
|
| 687 |
+
|
| 688 |
# 角色配置联动
|
| 689 |
persona_select.select(on_select_persona_config, persona_select, [p_sys_input, p_user_input])
|
| 690 |
+
|
| 691 |
+
# 关键逻辑:选择角色配置时,自动填充名字输入框
|
| 692 |
p_config_select.select(on_select_persona_config, p_config_select, [p_sys_input, p_user_input])
|
| 693 |
p_config_select.select(lambda x: x, p_config_select, p_name_input)
|
| 694 |
|
| 695 |
+
# 核心修复:保存按钮同时更新两个下拉框,并设置Value,防止报错
|
| 696 |
+
p_save_btn.click(
|
| 697 |
+
save_new_persona,
|
| 698 |
+
inputs=[p_name_input, p_sys_input, p_user_input],
|
| 699 |
+
outputs=[p_status, persona_select, p_config_select] # 同时指向 Tab1 和 Tab2 的下拉框
|
| 700 |
+
)
|
| 701 |
+
|
| 702 |
+
# 核心修复:删除按钮同时更新两个下拉框
|
| 703 |
+
p_del_btn.click(
|
| 704 |
+
delete_persona,
|
| 705 |
+
inputs=p_config_select,
|
| 706 |
+
outputs=[p_status, persona_select, p_config_select]
|
| 707 |
+
)
|
| 708 |
|
| 709 |
analyze_btn.click(analyze_matches, topic_input, [ref_checkboxes, analyze_status, state_all_files])
|
| 710 |
select_all_btn.click(toggle_select_all, inputs=[ref_checkboxes, state_all_files], outputs=ref_checkboxes)
|
| 711 |
upload_btn.click(save_to_corpus, files, [corpus_display, upload_res])
|
| 712 |
clear_btn.click(clear_corpus, outputs=[corpus_display, upload_res])
|
| 713 |
|
| 714 |
+
# 自动刷新逻辑
|
|
|
|
| 715 |
timer.tick(
|
| 716 |
lambda tid: (task_manager.get_task_info(tid)["log"], task_manager.get_task_info(tid)["result"]) if tid else ("", None),
|
| 717 |
inputs=task_dd,
|
| 718 |
outputs=[log_box, result_dl]
|
| 719 |
)
|
| 720 |
|
|
|
|
| 721 |
refresh_task.click(lambda: gr.Dropdown(choices=task_manager.list_tasks(), value=task_manager.list_tasks()[0] if task_manager.list_tasks() else None), outputs=task_dd)
|
| 722 |
task_dd.change(lambda tid: (task_manager.get_task_info(tid)["log"], task_manager.get_task_info(tid)["result"]) if tid else ("", None), task_dd, [log_box, result_dl])
|
| 723 |
|
| 724 |
+
# 初始加载
|
| 725 |
demo.load(list_corpus_files, outputs=corpus_display)
|
| 726 |
+
# 初始化时确保两个下拉框都有值
|
| 727 |
+
demo.load(lambda: global_refresh_dropdowns()[0], outputs=persona_select)
|
| 728 |
+
demo.load(lambda: global_refresh_dropdowns()[1], outputs=p_config_select)
|
| 729 |
|
| 730 |
if __name__ == "__main__":
|
| 731 |
demo.queue().launch()
|