|
@@ -1,14 +1,15 @@
|
|
|
"""
|
|
"""
|
|
|
-逐 case 提取 workflow (v5版本)
|
|
|
|
|
|
|
+逐 case 提取 workflow + fragments (v6版本)
|
|
|
|
|
|
|
|
从 case.json 读取,按 index 遍历每个 case,
|
|
从 case.json 读取,按 index 遍历每个 case,
|
|
|
-调用 LLM 提取 workflow,按 index 原位回填到 case.json
|
|
|
|
|
-
|
|
|
|
|
-v5 架构特性:
|
|
|
|
|
-- 使用结构化 inputs/outputs(role, modality, artifact_type 等10个维度)
|
|
|
|
|
-- action 对象化:{main_action, mechanism}(替代旧的 method 字符串)
|
|
|
|
|
-- Stage 1 输出 apply_to_draft(自然语言),为 Stage 2 内容树映射做准备
|
|
|
|
|
-- strategy 顶层字段(method, inputs, outputs, tools, stage)由脚本自动推导
|
|
|
|
|
|
|
+调用 LLM 同时提取 workflow(薄壳 steps)和 fragments(原子操作,含完整 capability 字段),
|
|
|
|
|
+按 index 原位回填到 case.json
|
|
|
|
|
+
|
|
|
|
|
+v6 架构特性:
|
|
|
|
|
+- workflow.steps 是薄壳:step_id / order / phase / relation / body,不含 capability 字段
|
|
|
|
|
+- fragments 是原子操作列表:每个 fragment 含完整 capability 字段 + workflow_step_ref + is_alternative_to
|
|
|
|
|
+- 步内多原子操作 + 步内 alternative 都在 fragment 层表达
|
|
|
|
|
+- standalone fragment(workflow_step_ref=null)用于无 workflow 上下文的能力提及
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
import asyncio
|
|
import asyncio
|
|
@@ -91,93 +92,20 @@ def render_method_vocab_block(vocab: Dict[str, list]) -> str:
|
|
|
return "\n".join(lines)
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
-import re
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _infer_stage_from_action(action_obj: dict) -> str:
|
|
|
|
|
- """从 action 对象推断 stage(v5版本)"""
|
|
|
|
|
- main_action = action_obj.get("main_action", "")
|
|
|
|
|
- mechanism = action_obj.get("mechanism", "")
|
|
|
|
|
-
|
|
|
|
|
- # 根据主动作和动作方式推断阶段
|
|
|
|
|
- if main_action in ["提取", "改写", "模板化", "训练", "评估"]:
|
|
|
|
|
- return "preprocess"
|
|
|
|
|
- elif main_action in ["编辑", "修复", "增强", "剪辑", "排版"]:
|
|
|
|
|
- return "refine"
|
|
|
|
|
- elif mechanism in ["局部重绘", "扩图", "换背景", "换主体", "换装", "擦除", "调色",
|
|
|
|
|
- "前后景融合", "降噪", "补帧", "超分", "稳定化", "质感增强"]:
|
|
|
|
|
- return "refine"
|
|
|
|
|
- else:
|
|
|
|
|
- return "generate"
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def derive_strategy_rollup(strategy: dict) -> None:
|
|
|
|
|
- """
|
|
|
|
|
- 从 steps 自动推导 strategy 的顶层字段(v5版本):
|
|
|
|
|
- method, inputs, outputs, tools, stage
|
|
|
|
|
-
|
|
|
|
|
- v5 变化:
|
|
|
|
|
- - method 从 action.main_action 提取(不再从旧的 method 字符串解析)
|
|
|
|
|
- - stage 从 action 对象推断
|
|
|
|
|
- """
|
|
|
|
|
- steps = [s for s in (strategy.get("steps") or []) if isinstance(s, dict)]
|
|
|
|
|
- if not steps:
|
|
|
|
|
- return
|
|
|
|
|
-
|
|
|
|
|
- steps.sort(key=lambda s: s.get("order") if isinstance(s.get("order"), int) else 9999)
|
|
|
|
|
-
|
|
|
|
|
- # method = 所有步骤的 main_action 用 "-" 连接
|
|
|
|
|
- actions = []
|
|
|
|
|
- for s in steps:
|
|
|
|
|
- action_obj = s.get("action")
|
|
|
|
|
- if isinstance(action_obj, dict):
|
|
|
|
|
- main_action = action_obj.get("main_action", "")
|
|
|
|
|
- if main_action:
|
|
|
|
|
- actions.append(main_action)
|
|
|
|
|
-
|
|
|
|
|
- if actions:
|
|
|
|
|
- strategy["method"] = "-".join(actions)
|
|
|
|
|
-
|
|
|
|
|
- # inputs = 第一步的 inputs
|
|
|
|
|
- first_inputs = steps[0].get("inputs")
|
|
|
|
|
- strategy["inputs"] = first_inputs if isinstance(first_inputs, list) else []
|
|
|
|
|
-
|
|
|
|
|
- # outputs = 最后一步的 outputs
|
|
|
|
|
- last_outputs = steps[-1].get("outputs")
|
|
|
|
|
- strategy["outputs"] = last_outputs if isinstance(last_outputs, list) else []
|
|
|
|
|
-
|
|
|
|
|
- # tools = 所有步骤的 tools 去重合并
|
|
|
|
|
- tools = []
|
|
|
|
|
- for step in steps:
|
|
|
|
|
- for tool in step.get("tools") or []:
|
|
|
|
|
- if isinstance(tool, str) and tool and tool not in tools:
|
|
|
|
|
- tools.append(tool)
|
|
|
|
|
- strategy["tools"] = tools
|
|
|
|
|
-
|
|
|
|
|
- # stage = 从 action 对象推断
|
|
|
|
|
- stages = []
|
|
|
|
|
- for step in steps:
|
|
|
|
|
- action_obj = step.get("action")
|
|
|
|
|
- if isinstance(action_obj, dict):
|
|
|
|
|
- stage = _infer_stage_from_action(action_obj)
|
|
|
|
|
- if stage not in stages:
|
|
|
|
|
- stages.append(stage)
|
|
|
|
|
- strategy["stage"] = stages or ["generate"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def extract_workflow_from_case(
|
|
async def extract_workflow_from_case(
|
|
|
case_item: Dict[str, Any],
|
|
case_item: Dict[str, Any],
|
|
|
llm_call: Any,
|
|
llm_call: Any,
|
|
|
model: str = "anthropic/claude-sonnet-4-5"
|
|
model: str = "anthropic/claude-sonnet-4-5"
|
|
|
-) -> tuple[Optional[Dict[str, Any]], float]:
|
|
|
|
|
|
|
+) -> tuple[Optional[Dict[str, Any]], Optional[List[Dict[str, Any]]], float]:
|
|
|
"""
|
|
"""
|
|
|
- 从单个 case item 提取 workflow (v5版本)
|
|
|
|
|
|
|
+ 从单个 case item 同时提取 workflow(薄壳 steps)和 fragments(原子操作列表)。
|
|
|
|
|
|
|
|
- v5 特性:
|
|
|
|
|
- - 结构化 inputs/outputs(role, modality, artifact_type 等)
|
|
|
|
|
- - action 对象化:{main_action, mechanism}(替代旧的 method 字符串)
|
|
|
|
|
- - 输出 apply_to_draft(自然语言),为 Stage 2 内容树映射做准备
|
|
|
|
|
- - strategy 顶层字段由 derive_strategy_rollup 自动推导
|
|
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ (workflow_dict, fragments_list, cost)
|
|
|
|
|
+ workflow_dict 为 None 表示 skip 或提取失败
|
|
|
|
|
+ fragments_list 为 None 表示 skip 或提取失败
|
|
|
"""
|
|
"""
|
|
|
images = case_item.get("images", [])
|
|
images = case_item.get("images", [])
|
|
|
|
|
|
|
@@ -185,17 +113,17 @@ async def extract_workflow_from_case(
|
|
|
case_copy.pop("images", None)
|
|
case_copy.pop("images", None)
|
|
|
case_copy.pop("_raw", None)
|
|
case_copy.pop("_raw", None)
|
|
|
case_copy.pop("workflow", None)
|
|
case_copy.pop("workflow", None)
|
|
|
|
|
+ case_copy.pop("fragments", None)
|
|
|
case_copy.pop("capabilities", None)
|
|
case_copy.pop("capabilities", None)
|
|
|
|
|
|
|
|
if not case_copy and not images:
|
|
if not case_copy and not images:
|
|
|
- return None, 0.0
|
|
|
|
|
|
|
+ return None, None, 0.0
|
|
|
|
|
|
|
|
title = case_item.get("title", "")[:20] or "untitled"
|
|
title = case_item.get("title", "")[:20] or "untitled"
|
|
|
context = json.dumps(case_copy, ensure_ascii=False, indent=2)
|
|
context = json.dumps(case_copy, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
prompt_template = load_prompt_template("extract_workflow")
|
|
prompt_template = load_prompt_template("extract_workflow")
|
|
|
- # 添加 v5 词库说明
|
|
|
|
|
method_vocab = load_method_vocab()
|
|
method_vocab = load_method_vocab()
|
|
|
vocab_block = render_method_vocab_block(method_vocab)
|
|
vocab_block = render_method_vocab_block(method_vocab)
|
|
|
|
|
|
|
@@ -204,57 +132,56 @@ async def extract_workflow_from_case(
|
|
|
else:
|
|
else:
|
|
|
prompt = prompt_template + f"\n\n## 帖子内容\n{context}"
|
|
prompt = prompt_template + f"\n\n## 帖子内容\n{context}"
|
|
|
|
|
|
|
|
- # 如果 prompt 中有 {interface_vocab} 占位符,替换为词库说明
|
|
|
|
|
if "{interface_vocab}" in prompt:
|
|
if "{interface_vocab}" in prompt:
|
|
|
prompt = prompt.replace("{interface_vocab}", vocab_block)
|
|
prompt = prompt.replace("{interface_vocab}", vocab_block)
|
|
|
elif vocab_block not in prompt:
|
|
elif vocab_block not in prompt:
|
|
|
- # 如果 prompt 中没有词库说明,添加到末尾
|
|
|
|
|
prompt = prompt + "\n" + vocab_block
|
|
prompt = prompt + "\n" + vocab_block
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
print(f"Warning: Failed to load prompt template: {e}, using fallback")
|
|
print(f"Warning: Failed to load prompt template: {e}, using fallback")
|
|
|
method_vocab = load_method_vocab()
|
|
method_vocab = load_method_vocab()
|
|
|
vocab_block = render_method_vocab_block(method_vocab)
|
|
vocab_block = render_method_vocab_block(method_vocab)
|
|
|
- prompt = f"""将以下帖子内容总结为AI图片生成的工序,以JSON格式输出。
|
|
|
|
|
|
|
+ prompt = f"""将以下帖子内容总结为AI图片生成的工序和原子操作,以JSON格式输出。
|
|
|
|
|
|
|
|
-# 工序提取规则(v5)
|
|
|
|
|
-- 步骤粒度是"做了什么",而非"怎么做"
|
|
|
|
|
-- 以"触发生成 / 处理的动作"为步骤边界
|
|
|
|
|
-- 若本质上只有一步,也输出一步,不要返回 strategy=null
|
|
|
|
|
-- 本阶段严禁生成 apply_to,只生成 apply_to_draft
|
|
|
|
|
-
|
|
|
|
|
-# 输出格式(v5)
|
|
|
|
|
|
|
+# 输出格式(v6)
|
|
|
{{
|
|
{{
|
|
|
"skip": false,
|
|
"skip": false,
|
|
|
"skip_reason": "",
|
|
"skip_reason": "",
|
|
|
- "strategy": {{
|
|
|
|
|
|
|
+ "workflow": {{
|
|
|
|
|
+ "workflow_id": null,
|
|
|
"steps": [
|
|
"steps": [
|
|
|
{{
|
|
{{
|
|
|
|
|
+ "step_id": "s1",
|
|
|
"order": 1,
|
|
"order": 1,
|
|
|
- "action": {{"main_action": "生成", "mechanism": "直接生成"}},
|
|
|
|
|
- "body": "string | null",
|
|
|
|
|
- "inputs": [
|
|
|
|
|
- {{
|
|
|
|
|
- "role": "生成指令",
|
|
|
|
|
- "modality": "文本",
|
|
|
|
|
- "artifact_type": "正向提示词",
|
|
|
|
|
- "control_target": ["主体", "场景"],
|
|
|
|
|
- "target_scope": ["整图"],
|
|
|
|
|
- "constraint_strength": "硬约束",
|
|
|
|
|
- "source": "原帖文本",
|
|
|
|
|
- "lifecycle": "原始输入",
|
|
|
|
|
- "description": "用于触发图片生成的完整提示词"
|
|
|
|
|
- }}
|
|
|
|
|
- ],
|
|
|
|
|
- "outputs": [...],
|
|
|
|
|
- "tools": []
|
|
|
|
|
|
|
+ "phase": "生成",
|
|
|
|
|
+ "relation": "[去向.最终成品]",
|
|
|
|
|
+ "body": "string | null"
|
|
|
}}
|
|
}}
|
|
|
- ],
|
|
|
|
|
- "effects": ["实现 XX 效果"],
|
|
|
|
|
- "criterion": null,
|
|
|
|
|
- "apply_to_draft": {{"实质": ["相关 what"], "形式": ["相关呈现方式"]}},
|
|
|
|
|
- "unstructured_what": []
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ ]
|
|
|
|
|
+ }},
|
|
|
|
|
+ "fragments": [
|
|
|
|
|
+ {{
|
|
|
|
|
+ "fragment_id": "f_s1_0",
|
|
|
|
|
+ "action": {{"main_action": "生成", "mechanism": "直接生成"}},
|
|
|
|
|
+ "inputs": [{{"modality": "文本", "description": "...", "relation": "[来源.原始输入]"}}],
|
|
|
|
|
+ "outputs": [{{"modality": "图片", "description": "...", "relation": "[去向.最终成品]"}}],
|
|
|
|
|
+ "body": "string | null",
|
|
|
|
|
+ "effects": [
|
|
|
|
|
+ {{
|
|
|
|
|
+ "statement": "实现XXX",
|
|
|
|
|
+ "criteria": "判断标准",
|
|
|
|
|
+ "judge_method": "vlm",
|
|
|
|
|
+ "negative_examples": []
|
|
|
|
|
+ }}
|
|
|
|
|
+ ],
|
|
|
|
|
+ "control_target": [],
|
|
|
|
|
+ "artifact_type": null,
|
|
|
|
|
+ "tools": [],
|
|
|
|
|
+ "apply_to_draft": {{"实质": ["..."], "形式": ["..."]}},
|
|
|
|
|
+ "workflow_step_ref": {{"workflow_id": null, "step_id": "s1"}},
|
|
|
|
|
+ "is_alternative_to": []
|
|
|
|
|
+ }}
|
|
|
|
|
+ ]
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
{vocab_block}
|
|
{vocab_block}
|
|
@@ -281,27 +208,22 @@ async def extract_workflow_from_case(
|
|
|
messages=messages,
|
|
messages=messages,
|
|
|
model=model,
|
|
model=model,
|
|
|
temperature=0.1,
|
|
temperature=0.1,
|
|
|
- max_tokens=8000, # 从2000增加到4000,处理更长的输出
|
|
|
|
|
- max_retries=3, # 从3增加到5,增加重试机会
|
|
|
|
|
|
|
+ max_tokens=10000,
|
|
|
|
|
+ max_retries=3,
|
|
|
schema_name="extract_workflow",
|
|
schema_name="extract_workflow",
|
|
|
task_name=f"Workflow_{title}",
|
|
task_name=f"Workflow_{title}",
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- # Stage 1 格式:{"skip": bool, "skip_reason": str, "strategy": {...}}
|
|
|
|
|
- # 如果 skip=true 或 strategy=null,返回 None
|
|
|
|
|
if not result_data:
|
|
if not result_data:
|
|
|
- return None, cost
|
|
|
|
|
|
|
+ return None, None, cost
|
|
|
|
|
|
|
|
if result_data.get("skip"):
|
|
if result_data.get("skip"):
|
|
|
- return None, cost
|
|
|
|
|
-
|
|
|
|
|
- workflow_data = result_data.get("strategy")
|
|
|
|
|
|
|
+ return None, None, cost
|
|
|
|
|
|
|
|
- # 从 steps 自动推导顶层字段(v5版本)
|
|
|
|
|
- if workflow_data and isinstance(workflow_data, dict):
|
|
|
|
|
- derive_strategy_rollup(workflow_data)
|
|
|
|
|
|
|
+ workflow_data = result_data.get("workflow")
|
|
|
|
|
+ fragments_data = result_data.get("fragments", [])
|
|
|
|
|
|
|
|
- return workflow_data, cost
|
|
|
|
|
|
|
+ return workflow_data, fragments_data, cost
|
|
|
|
|
|
|
|
|
|
|
|
|
async def extract_workflow(
|
|
async def extract_workflow(
|
|
@@ -345,13 +267,15 @@ async def extract_workflow(
|
|
|
|
|
|
|
|
print(f" -> [{index}] [{case_id}] extracting workflow: {title[:60]}")
|
|
print(f" -> [{index}] [{case_id}] extracting workflow: {title[:60]}")
|
|
|
|
|
|
|
|
- workflow, cost = await extract_workflow_from_case(case_item, llm_call, model)
|
|
|
|
|
|
|
+ workflow, fragments, cost = await extract_workflow_from_case(case_item, llm_call, model)
|
|
|
|
|
|
|
|
- status = "ok" if workflow else "null"
|
|
|
|
|
|
|
+ frag_count = len(fragments) if fragments else 0
|
|
|
|
|
+ status = f"ok ({frag_count} fragments)" if workflow else "null"
|
|
|
print(f" <- [{index}] [{case_id}] workflow {status}")
|
|
print(f" <- [{index}] [{case_id}] workflow {status}")
|
|
|
|
|
|
|
|
result = dict(case_item)
|
|
result = dict(case_item)
|
|
|
result["workflow"] = workflow
|
|
result["workflow"] = workflow
|
|
|
|
|
+ result["fragments"] = fragments if fragments is not None else []
|
|
|
return result, cost
|
|
return result, cost
|
|
|
|
|
|
|
|
tasks = [process_with_semaphore(case) for case in cases_to_process]
|
|
tasks = [process_with_semaphore(case) for case in cases_to_process]
|
|
@@ -361,7 +285,7 @@ async def extract_workflow(
|
|
|
costs = [r[1] for r in results_with_costs]
|
|
costs = [r[1] for r in results_with_costs]
|
|
|
total_cost = sum(costs)
|
|
total_cost = sum(costs)
|
|
|
|
|
|
|
|
- success_count = sum(1 for r in results if r.get("workflow"))
|
|
|
|
|
|
|
+ success_count = sum(1 for r in results if r.get("workflow") and r.get("fragments"))
|
|
|
failed_count = len(results) - success_count
|
|
failed_count = len(results) - success_count
|
|
|
|
|
|
|
|
# 如果是部分更新,需要合并回原始 cases 列表
|
|
# 如果是部分更新,需要合并回原始 cases 列表
|
|
@@ -381,10 +305,13 @@ async def extract_workflow(
|
|
|
with open(case_file, "w", encoding="utf-8") as f:
|
|
with open(case_file, "w", encoding="utf-8") as f:
|
|
|
json.dump(case_data, f, ensure_ascii=False, indent=2)
|
|
json.dump(case_data, f, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
|
|
|
+ fragments_count = sum(len(r.get("fragments") or []) for r in results)
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
"total": len(results),
|
|
"total": len(results),
|
|
|
"success": success_count,
|
|
"success": success_count,
|
|
|
"failed": failed_count,
|
|
"failed": failed_count,
|
|
|
|
|
+ "fragments_total": fragments_count,
|
|
|
"total_cost": total_cost,
|
|
"total_cost": total_cost,
|
|
|
"output_file": str(case_file),
|
|
"output_file": str(case_file),
|
|
|
}
|
|
}
|