AIGC-Director Agent Skill 本地运行 :这是一个 本地部署 的视频生成项目, 前后端都运行在本机 : - 后端: - 前端: - 所有 API 调用都请求本地服务器,不要请求其他地址! - 确保在调用任何 API 之前,后端和前端服务都已经启动并运行正常! 核心理念 :Agent 应该像"持续陪伴的智能视频制作助理",每完成一个用户可感知的重要任务,都应立即给用户一条简报,并等待用户确认。 核心原则 :每个阶段的产物都必须展示给用户,必须停下来等待用户确认后才能继续下一阶段。 防止遗忘 :在整个流程中,Agent 可能会忘记之前的用户输入或之前阶段的产物内容。 每当进入一个新的阶段时,Agent 都必须重新加载这篇SKILL文档,确保不会忘记任何细节 。 --- 项目结构 产物存放目录 : - - 剧本产物 - - 图片产物(角色、场景、参考图) - - 视频产物 --- 阶段与停点(共9个) | 停点 | 阶段 | phase 值 | 描述 | 操作 | |------|------|----------|------|------| | 1 | 项目配置 | - | 确认配置选项 | 展示配置 → 用户确认 | | 2 | 剧本生成 | suggest expand | 建议扩写 | 等待用户确认 | | 3 | 剧本生成 | logline sele…

, bn)\n if m:\n max_v = max(max_v, int(m.group(1)))\n\n return os.path.join(adir, f\"{asset_id}_v{max_v + 1}.png\")\n\n def _build_asset_info(self, sid: str, asset_type: str,\n asset_id: str, name: str, desc: str,\n selected_path: str = \"\") -> dict:\n \"\"\"构建单个素材的信息(含所有历史版本)\"\"\"\n versions = self._list_versions(sid, asset_type, asset_id)\n if not selected_path and versions:\n selected_path = versions[-1]\n return {\n \"id\": asset_id,\n \"name\": name,\n \"description\": desc,\n \"selected\": selected_path,\n \"versions\": versions,\n }\n\n # ─── 图片生成 ───\n\n def _build_preview(self, sid: str, chars_desc: dict, sets_desc: dict) -> dict:\n \"\"\"构建素材预览列表(含当前状态)用于前端实时显示\"\"\"\n preview = {\"characters\": [], \"settings\": []}\n for asset_id, info in chars_desc.items():\n existing = self._list_versions(sid, 'characters', asset_id)\n preview[\"characters\"].append({\n \"id\": asset_id,\n \"name\": info.get(\"name\", \"\"),\n \"description\": info.get(\"description\", \"\"),\n \"selected\": existing[-1] if existing else \"\",\n \"versions\": existing,\n \"status\": \"done\" if existing else \"pending\",\n })\n for asset_id, info in sets_desc.items():\n existing = self._list_versions(sid, 'settings', asset_id)\n name = info.get(\"name\", \"\") if isinstance(info, dict) else \"\"\n desc = info.get(\"description\", \"\") if isinstance(info, dict) else str(info)\n preview[\"settings\"].append({\n \"id\": asset_id,\n \"name\": name,\n \"description\": desc,\n \"selected\": existing[-1] if existing else \"\",\n \"versions\": existing,\n \"status\": \"done\" if existing else \"pending\",\n })\n return preview\n\n def _generate_one(self, img_client, asset_id: str, name: str, desc: str,\n asset_type: str, style: str, species: str,\n t2i_model: str, vlm_model: str, sid: str, max_iterations: int = 3) -> tuple:\n \"\"\"生成单个素材图并返回 (asset_id, path_or_None, eval_result)\n\n 评估-生成循环:如果 VLM 评估发现问题,最多重新生成 max_iterations 次\n \"\"\"\n self._check_cancel()\n\n # 初始提示词\n if asset_type == 'characters':\n base_prompt = self._char_prompt(name, desc, style, species)\n else:\n base_prompt = self._setting_prompt(name, desc, style)\n\n size = \"1920*1080\"\n current_prompt = base_prompt\n\n for iteration in range(max_iterations):\n self._check_cancel()\n\n save_path = self._next_version_path(sid, asset_type, asset_id)\n save_dir = os.path.dirname(save_path)\n\n try:\n paths = img_client.generate_image(\n prompt=current_prompt, model=t2i_model,\n session_id=str(sid), save_dir=save_dir, size=size,\n )\n if not paths:\n continue\n\n gen = paths[0]\n if gen != save_path:\n if os.path.exists(save_path):\n os.remove(save_path)\n os.rename(gen, save_path)\n\n # VLM 评估\n eval_result = self._evaluate_with_vlm(save_path, desc, asset_type, vlm_model)\n\n # 使用固定阈值判断是否接受(8分及以上通过)\n score = eval_result.get('score', 0)\n issues = eval_result.get('issues', [])\n suggestion = eval_result.get('suggestion', '')\n is_acceptable = score >= 8\n\n if is_acceptable:\n logger.info(f\"[{asset_type}] {name} ✓ VLM评估通过 - 评分: {score}/10\")\n else:\n logger.warning(f\"[{asset_type}] {name} ✗ VLM评估不通过 - 评分: {score}/10\")\n logger.warning(f\"[{asset_type}] 问题: {issues}\")\n if suggestion:\n logger.warning(f\"[{asset_type}] 建议: {suggestion}\")\n\n # 检查是否需要重新生成\n if is_acceptable:\n # 评估通过,返回结果\n return asset_id, save_path, eval_result\n else:\n # 评估不通过,记录问题并继续循环\n # 报告进度\n self._report_progress(\"角色设计\", f\"重新生成中 ({iteration + 2}/{max_iterations}): {name}\", 0)\n\n except Exception as e:\n logger.error(f\"Asset gen failed for {asset_type} {name}({asset_id}): {e}\")\n\n # 达到最大迭代次数,尝试使用 VLM 选择最佳图片\n logger.warning(f\"[{asset_type}] {name} reached max iterations ({max_iterations}), trying VLM selection\")\n\n # 收集所有生成过的版本\n all_versions = self._list_versions(sid, asset_type, asset_id)\n if len(all_versions) > 1:\n # 有多个版本,调用 VLM 选择最好的\n best_path, best_eval = self._select_best_with_vlm(\n all_versions, name, desc, asset_type, species, vlm_model\n )\n if best_path:\n logger.info(f\"[{asset_type}] {name} VLM selected best version: {best_path}\")\n return asset_id, best_path, best_eval\n\n # 没有多个版本或 VLM 选择失败,返回最后一次结果\n return asset_id, save_path if os.path.exists(save_path) else None, eval_result if 'eval_result' in locals() else None\n\n def _evaluate_with_vlm(self, image_path: str, description: str, asset_type: str, vlm_model: str = \"qwen3.5-plus\") -> dict:\n \"\"\"使用 VLM 评估生成的图片\"\"\"\n try:\n from tool.vlm_client import VLM\n vlm = VLM()\n\n # 选择评估提示词\n if asset_type == 'characters':\n eval_prompt = load_prompt('character', 'eval_character', 'zh').format(\n character_description=description\n )\n else:\n eval_prompt = load_prompt('setting', 'eval_setting', 'zh').format(\n setting_description=description\n )\n\n result = vlm.query(\n prompt=eval_prompt,\n image_paths=[image_path],\n model=vlm_model\n )\n\n # 解析结果\n if result and isinstance(result, list):\n result_text = result[0] if result else \"\"\n elif isinstance(result, str):\n result_text = result\n else:\n result_text = str(result)\n\n # 尝试提取 JSON\n import json\n try:\n # 找到 JSON 部分\n import re\n json_match = re.search(r'\\{[^{}]*\\}', result_text, re.DOTALL)\n if json_match:\n eval_result = json.loads(json_match.group())\n return eval_result\n except:\n pass\n\n return {\"score\": 5, \"issues\": [\"评估解析失败\"], \"is_acceptable\": True}\n\n except Exception as e:\n logger.warning(f\"VLM evaluation failed: {e}\")\n return {\"score\": 5, \"issues\": [str(e)], \"is_acceptable\": True}\n\n def _select_best_with_vlm(self, image_paths: List[str], name: str, description: str,\n asset_type: str, species: str = \"\", vlm_model: str = \"qwen3.5-plus\") -> tuple:\n \"\"\"使用 VLM 从多个版本中选择最好的一张\"\"\"\n from tool.vlm_client import VLM\n import re\n\n if not image_paths:\n return None, None\n\n try:\n vlm = VLM()\n\n # 选择评估提示词\n if asset_type == 'characters':\n select_prompt = load_prompt('character', 'eval_select_best', 'zh').format(\n num_images=len(image_paths),\n num_images_minus_1=len(image_paths) - 1,\n character_name=name,\n character_description=description,\n species=species,\n images_list=\"\\n\".join([f\"图片{i}: {p}\" for i, p in enumerate(image_paths)])\n )\n else:\n select_prompt = load_prompt('setting', 'eval_select_best', 'zh').format(\n num_images=len(image_paths),\n num_images_minus_1=len(image_paths) - 1,\n setting_name=name,\n setting_description=description,\n images_list=\"\\n\".join([f\"图片{i}: {p}\" for i, p in enumerate(image_paths)])\n )\n\n result = vlm.query(select_prompt, image_paths=image_paths, model=vlm_model)\n logger.info(f\"[{asset_type}] {name} VLM selection result: {result}\")\n\n # 解析 JSON 结果\n if result and isinstance(result, list):\n result_text = result[0] if result else \"\"\n elif isinstance(result, str):\n result_text = result\n else:\n result_text = str(result)\n\n logger.info(f\"[{asset_type}] {name} VLM selection raw response: {result_text[:500]}\")\n\n # 解析 JSON,提取 best_index\n best_index = 0\n try:\n # 找到 JSON 开始和结束\n json_start = result_text.find('{')\n json_end = result_text.rfind('}') + 1\n if json_start >= 0 and json_end > json_start:\n json_str = result_text[json_start:json_end]\n selection_result = json.loads(json_str)\n best_index = selection_result.get('best_index', 0)\n logger.info(f\"[{asset_type}] {name} Parsed best_index: {best_index}\")\n except Exception as e:\n logger.warning(f\"[{asset_type}] {name} JSON parse failed: {e}, using last image\")\n best_index = len(image_paths) - 1\n\n # 如果找到了 best_index,选择对应的图片\n if best_index is not None and 0 \u003c= best_index \u003c len(image_paths):\n best_path = image_paths[best_index]\n best_eval = {\"score\": 8, \"issues\": [], \"is_acceptable\": True, \"reason\": f\"VLM selected image {best_index + 1} of {len(image_paths)}\"}\n logger.info(f\"[{asset_type}] {name} Selected image: {best_path}\")\n return best_path, best_eval\n else:\n logger.warning(f\"[{asset_type}] {name} Invalid best_index: {best_index}, available images: {len(image_paths)}\")\n\n except Exception as e:\n logger.warning(f\"VLM selection failed: {e}\")\n\n return None, None\n\n # ─── 从剧本JSON读取角色/场景数据 ───\n\n @staticmethod\n def _read_script_data(sid: str) -> dict:\n \"\"\"从结果文件的 script_json 字段读取角色和场景数据(含唯一ID)\n\n Returns:\n {\n \"characters\": { character_id: { \"name\", \"description\", \"species\" } },\n \"settings\": { setting_id: { \"name\", \"description\" } },\n }\n \"\"\"\n from config import settings\n\n script_json_path = os.path.join(settings.RESULT_DIR, 'script', f'script_{sid}.json')\n if not os.path.exists(script_json_path):\n return {\"characters\": {}, \"settings\": {}}\n\n with open(script_json_path, 'r', encoding='utf-8') as f:\n data = json.load(f)\n\n sid_data = data.get(str(sid), {})\n\n # 优先从 script_json 读取(包含完整ID信息)\n sj = sid_data.get('script_json')\n if sj:\n chars = {}\n for c in sj.get(\"characters\", []):\n cid = c.get(\"character_id\", \"\")\n if cid:\n chars[cid] = {\n \"name\": c.get(\"name\", \"\"),\n \"description\": c.get(\"description\", \"\"),\n \"species\": c.get(\"species\", \"\"),\n }\n sets = {}\n for s in sj.get(\"settings\", []):\n sid_val = s.get(\"setting_id\", \"\")\n if sid_val:\n sets[sid_val] = {\n \"name\": s.get(\"name\", \"\"),\n \"description\": s.get(\"description\", \"\"),\n }\n return {\"characters\": chars, \"settings\": sets}\n\n return {\"characters\": {}, \"settings\": {}}\n\n # ─── 核心流程 ───\n\n async def process(self, input_data: Any, intervention: Optional[Dict] = None) -> Dict:\n from config import settings\n from tool.image_client import ImageClient\n\n sid = input_data[\"session_id\"]\n style = input_data.get(\"style\", \"anime\")\n t2i_model = input_data.get(\"image_t2i_model\", \"\") or settings.IMAGE_T2I_MODEL\n vlm_model = input_data.get(\"vlm_model\", \"qwen3.5-plus\")\n # 根据 enable_concurrency 决定并发数\n enable_concurrency = input_data.get(\"enable_concurrency\", True)\n logger.info(f\"[CharacterAgent] enable_concurrency={enable_concurrency}\")\n from config_model import get_max_concurrency\n max_concurrency = get_max_concurrency(t2i_model, enable_concurrency)\n logger.info(f\"[CharacterAgent] 使用并发数={max_concurrency}\")\n concurrency = max_concurrency\n\n img_client = ImageClient(\n dashscope_api_key=settings.DASHSCOPE_API_KEY,\n dashscope_base_url=settings.DASHSCOPE_BASE_URL,\n gpt_api_key=os.getenv(\"OPENAI_API_KEY\"),\n gpt_base_url=os.getenv(\"OPENAI_BASE_URL\"),\n gpt_official_api_key=settings.OPENAI_OFFICIAL_API_KEY,\n local_proxy=settings.LOCAL_PROXY,\n ark_api_key=settings.ARK_API_KEY,\n ark_base_url=settings.ARK_BASE_URL,\n )\n\n # ═══════════ 介入: 重新生成指定素材 ═══════════\n if intervention:\n regen_chars = intervention.get(\"regenerate_characters\", []) # list of asset_id\n regen_sets = intervention.get(\"regenerate_settings\", []) # list of asset_id\n select_chars = intervention.get(\"select_characters\", {}) # {asset_id: path}\n select_sets = intervention.get(\"select_settings\", {}) # {asset_id: path}\n update_descriptions = intervention.get(\"update_descriptions\", {}) # {characters: {}, settings: {}}\n\n script_data = self._read_script_data(sid)\n chars_desc = script_data[\"characters\"]\n sets_desc = script_data[\"settings\"]\n\n # 处理描述更新\n if update_descriptions:\n updated_chars = update_descriptions.get(\"characters\", {})\n updated_sets = update_descriptions.get(\"settings\", {})\n\n # 更新角色描述 (支持两种格式:字符串或字典)\n for asset_id, info in updated_chars.items():\n if asset_id in chars_desc:\n if isinstance(info, dict):\n if \"name\" in info:\n chars_desc[asset_id][\"name\"] = info[\"name\"]\n if \"description\" in info:\n chars_desc[asset_id][\"description\"] = info[\"description\"]\n if \"species\" in info:\n chars_desc[asset_id][\"species\"] = info[\"species\"]\n else:\n # 简单字符串格式:直接更新 description\n chars_desc[asset_id][\"description\"] = str(info)\n\n # 更新场景描述 (支持两种格式:字符串或字典)\n for asset_id, info in updated_sets.items():\n if asset_id in sets_desc:\n if isinstance(info, dict):\n if \"name\" in info:\n sets_desc[asset_id][\"name\"] = info[\"name\"]\n if \"description\" in info:\n sets_desc[asset_id][\"description\"] = info[\"description\"]\n else:\n # 简单字符串格式:直接更新 description\n sets_desc[asset_id][\"description\"] = str(info)\n\n # 写回剧本数据文件\n script_file = os.path.join(settings.RESULT_DIR, 'script', f'script_{sid}.json')\n with open(script_file, 'r', encoding='utf-8') as f:\n full_data = json.load(f)\n\n if str(sid) in full_data:\n full_data[str(sid)][\"characters\"] = chars_desc\n full_data[str(sid)][\"settings\"] = sets_desc\n\n with open(script_file, 'w', encoding='utf-8') as f:\n json.dump(full_data, f, ensure_ascii=False, indent=2)\n\n logger.info(f\"[CharacterAgent] Updated descriptions for session {sid}\")\n\n if regen_chars or regen_sets:\n self._report_progress(\"角色设计\", \"重新生成中...\", 10)\n tasks = []\n for asset_id in regen_chars:\n info = chars_desc.get(asset_id, {})\n tasks.append((\"characters\", asset_id, info.get(\"name\", \"\"), info.get(\"description\", \"\"), info.get(\"species\", \"\")))\n for asset_id in regen_sets:\n info = sets_desc.get(asset_id, {})\n tasks.append((\"settings\", asset_id, info.get(\"name\", \"\"), info.get(\"description\", \"\"), \"\"))\n\n def regen_run():\n total = len(tasks)\n done = 0\n with ThreadPoolExecutor(max_workers=concurrency) as executor:\n futs = {}\n for atype, aid, name, desc, species in tasks:\n fut = executor.submit(\n self._generate_one, img_client,\n aid, name, desc, atype, style, species, t2i_model, vlm_model, sid\n )\n futs[fut] = (atype, aid, name)\n for fut in as_completed(futs):\n atype, aid, fname = futs[fut]\n _, result_path, eval_result = fut.result()\n done += 1\n pct = 10 + int(85 * done / max(total, 1))\n if result_path:\n versions = self._list_versions(sid, atype, aid)\n self._report_progress(\"角色设计\", f\"完成: {fname}\", pct, data={\n \"asset_complete\": {\n \"type\": atype, \"id\": aid, \"status\": \"done\",\n \"selected\": result_path, \"versions\": versions,\n \"evaluation\": eval_result,\n }\n })\n else:\n self._report_progress(\"角色设计\", f\"失败: {fname}\", pct, data={\n \"asset_complete\": {\n \"type\": atype, \"id\": aid, \"status\": \"failed\",\n \"selected\": \"\", \"versions\": [],\n }\n })\n\n loop = asyncio.get_running_loop()\n await loop.run_in_executor(None, regen_run)\n\n self._report_progress(\"角色设计\", \"完成\", 100)\n return self._build_payload(sid, chars_desc, sets_desc, select_chars, select_sets)\n\n # ═══════════ 正常流程: 全量首次生成 ═══════════\n self._report_progress(\"角色设计\", \"读取剧本数据...\", 5)\n\n script_data = self._read_script_data(sid)\n chars_desc = script_data[\"characters\"]\n sets_desc = script_data[\"settings\"]\n\n if not chars_desc and not sets_desc:\n raise Exception(\"未能从剧本中读取到角色或场景描述数据\")\n\n # 发送素材预览(含所有素材和当前状态)\n preview = self._build_preview(sid, chars_desc, sets_desc)\n self._report_progress(\"角色设计\", \"加载素材列表\", 8, data={\"assets_preview\": preview})\n\n def run():\n all_tasks = []\n for asset_id, info in chars_desc.items():\n existing = self._list_versions(sid, 'characters', asset_id)\n if existing:\n continue\n all_tasks.append((\"characters\", asset_id, info.get(\"name\", \"\"), info.get(\"description\", \"\"), info.get(\"species\", \"\")))\n\n for asset_id, info in sets_desc.items():\n desc = info.get(\"description\", \"\") if isinstance(info, dict) else info\n existing = self._list_versions(sid, 'settings', asset_id)\n if existing:\n continue\n all_tasks.append((\"settings\", asset_id, info.get(\"name\", \"\") if isinstance(info, dict) else \"\", desc, \"\"))\n\n if not all_tasks:\n self._report_progress(\"角色设计\", \"所有素材已存在\", 95)\n return\n\n total = len(all_tasks)\n done = 0\n\n with ThreadPoolExecutor(max_workers=concurrency) as executor:\n futs = {}\n for atype, aid, name, desc, species in all_tasks:\n fut = executor.submit(\n self._generate_one, img_client,\n aid, name, desc, atype, style, species, t2i_model, vlm_model, sid,\n )\n futs[fut] = (atype, aid, name)\n\n for fut in as_completed(futs):\n atype, aid, fname = futs[fut]\n _, result_path, _ = fut.result()\n done += 1\n pct = 10 + int(85 * done / max(total, 1))\n if result_path:\n versions = self._list_versions(sid, atype, aid)\n self._report_progress(\"角色设计\", f\"完成: {fname}\", pct, data={\n \"asset_complete\": {\n \"type\": atype, \"id\": aid, \"status\": \"done\",\n \"selected\": result_path, \"versions\": versions,\n }\n })\n else:\n self._report_progress(\"角色设计\", f\"失败: {fname}\", pct, data={\n \"asset_complete\": {\n \"type\": atype, \"id\": aid, \"status\": \"failed\",\n \"selected\": \"\", \"versions\": self._list_versions(sid, atype, aid),\n }\n })\n\n self._report_progress(\"角色设计\", \"完成\", 100)\n\n loop = asyncio.get_running_loop()\n await loop.run_in_executor(None, run)\n\n return self._build_payload(sid, chars_desc, sets_desc)\n\n def _build_payload(self, sid: str, chars_desc: dict, sets_desc: dict,\n selected_chars: dict = None, selected_sets: dict = None) -> dict:\n \"\"\"构建返回给前端的 payload\"\"\"\n selected_chars = selected_chars or {}\n selected_sets = selected_sets or {}\n\n characters = []\n for asset_id, info in chars_desc.items():\n desc = info.get(\"description\", \"\") if isinstance(info, dict) else info\n name = info.get(\"name\", \"\") if isinstance(info, dict) else \"\"\n sel = selected_chars.get(asset_id, \"\")\n characters.append(self._build_asset_info(sid, 'characters', asset_id, name, desc, sel))\n\n settings_list = []\n for asset_id, info in sets_desc.items():\n desc = info.get(\"description\", \"\") if isinstance(info, dict) else info\n name = info.get(\"name\", \"\") if isinstance(info, dict) else \"\"\n sel = selected_sets.get(asset_id, \"\")\n settings_list.append(self._build_asset_info(sid, 'settings', asset_id, name, desc, sel))\n\n # 图片生成完成即为阶段完成,用户选择图片只是更新数据\n return {\n \"payload\": {\n \"session_id\": sid,\n \"characters\": characters,\n \"settings\": settings_list,\n },\n \"stage_completed\": True,\n }\n","content_type":"text/x-python; charset=utf-8","language":"python","size":27577,"content_sha256":"9e6f81b18e3d7b2eed71025437b3d2f4b48c09e58eed0fa3741bd02a31c74b3e"},{"filename":"aigc-claw/backend/core/agents/editor_agent.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\n阶段6: 后期制作智能体\n拼接用户在阶段5选定的视频片段 → 最终成片\n\"\"\"\n\nimport os\nimport re\nimport subprocess\nimport asyncio\nimport logging\nfrom typing import Any, Optional, Dict\n\nfrom .base_agent import AgentInterface\n\nlogger = logging.getLogger(__name__)\n\n\nclass VideoEditorAgent(AgentInterface):\n \"\"\"后期制作:拼接用户选择的视频片段 → 最终成片\"\"\"\n\n def __init__(self):\n super().__init__(name=\"VideoEditor\")\n\n async def process(self, input_data: Any, intervention: Optional[Dict] = None) -> Dict:\n sid = input_data[\"session_id\"]\n selected_clips: dict = input_data.get(\"selected_clips\", {})\n\n if not selected_clips:\n raise Exception(\"未收到用户选择的视频片段(selected_clips),请先完成阶段5\")\n\n self._report_progress(\"后期制作\", \"准备视频片段...\", 5)\n\n def run():\n video_dir = os.path.join('code/result/video', str(sid))\n\n # 按 shot_id 中的数字排序\n def sort_key(k: str) -> tuple:\n return tuple(int(n) for n in re.findall(r'\\d+', k)) or (999,)\n\n clip_paths = []\n for shot_id in sorted(selected_clips.keys(), key=sort_key):\n path = selected_clips[shot_id]\n if os.path.exists(path):\n clip_paths.append(path)\n else:\n logger.warning(f\"[{sid}] Clip missing: {shot_id} → {path}\")\n\n if not clip_paths:\n raise Exception(\"所有选定的视频片段文件均不存在\")\n\n logger.info(f\"[{sid}] Concat {len(clip_paths)} clips\")\n self._report_progress(\"后期制作\", f\"拼接 {len(clip_paths)} 个片段...\", 15)\n\n list_path = os.path.join(video_dir, 'file_list.txt')\n with open(list_path, 'w', encoding='utf-8') as f:\n for p in clip_paths:\n f.write(f\"file '{os.path.abspath(p)}'\\n\")\n\n self._report_progress(\"后期制作\", \"ffmpeg 拼接中...\", 30)\n\n output = os.path.join(video_dir, f'{sid}_final.mp4')\n cmd = ['ffmpeg', '-f', 'concat', '-safe', '0',\n '-i', list_path, '-c', 'copy', '-y', output]\n subprocess.run(cmd, check=True, capture_output=True)\n logger.info(f\"[{sid}] Concat success: {output}\")\n return output\n\n loop = asyncio.get_running_loop()\n final_path = await loop.run_in_executor(None, run)\n\n self._report_progress(\"后期制作\", \"成片完成\", 100)\n\n return {\n \"payload\": {\"session_id\": sid, \"final_video\": final_path},\n \"stage_completed\": True,\n }","content_type":"text/x-python; charset=utf-8","language":"python","size":2730,"content_sha256":"9416dc1cf70d98590a0fe48094507045e52294696a075d0076092ce331001bd4"},{"filename":"aigc-claw/backend/core/agents/script_agent.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\n阶段1: 编剧智能体 - 多轮LLM交互生成结构化剧本JSON\n流程: Logline → 节拍表(4幕) → 分场大纲(逐幕) → JSON结构化提取(带校验重试)\n\"\"\"\n\nimport os\nimport re\nimport json\nimport asyncio\nimport logging\nimport string\nimport random\nfrom datetime import datetime, timezone\nfrom typing import Any, Optional, Dict\n\nfrom .base_agent import AgentInterface\n\n# 导入提示词加载器\nfrom prompts.loader import load_prompt\n\nlogger = logging.getLogger(__name__)\n\n\n# 懒加载提示词 - 优先从外部文件加载,失败则返回空\ndef _p(category: str, name: str, lang: str = 'zh') -> str:\n \"\"\"Load prompt from external file, return empty string if not found\"\"\"\n try:\n return load_prompt(category, name, lang)\n except FileNotFoundError:\n return \"\"\n\n\n# 从外部文件加载提示词的辅助函数\ndef _load_prompt(category: str, name: str, fallback: str) -> str:\n \"\"\"Load prompt from external file, fallback to hardcoded string\"\"\"\n try:\n return load_prompt(category, name, 'zh')\n except FileNotFoundError:\n return fallback\n\n# =============================================================\n# Phase 1A: Logline 生成(扩写创意 → 3 个 Logline 方案)\n# =============================================================\n\nLOGLINE_GENERATE_PROMPT_ZH = (\n \"你是资深制片人。请将以下灵感扩展为 3 个不同的 Logline(故事大纲)。\\n\"\n \"要求:\\n\"\n \"- 明确主角(Who)、目标(Goal)、核心障碍(Conflict)和反转(Twist)\\n\"\n \"- 确定故事的 Theme(潜在主题)\\n\"\n \"- Logline 按照\\\"如果…会怎样\\\"的句式描述\\n\"\n \"- 3 个 Logline 应风格各异、各有侧重\\n\\n\"\n \"输入内容:{idea}\\n\\n\"\n \"请严格按如下 JSON 数组格式输出(直接输出纯JSON,不要用```包裹,不要添加任何其他文字):\\n\"\n '[{{\"logline\":\"如果...会怎样\",\"who\":\"主角描述\",\"goal\":\"目标\",\"conflict\":\"核心障碍\",\"twist\":\"反转\",\"theme\":\"潜在主题\"}}]\\n'\n \"输出恰好 3 个元素的 JSON 数组。\"\n)\n\nLOGLINE_GENERATE_PROMPT_EN = (\n \"You are a senior producer. Expand the following idea into 3 different Loglines.\\n\"\n \"Requirements:\\n\"\n \"- Define the protagonist (Who), Goal, core Conflict, and Twist\\n\"\n \"- Identify the story's Theme\\n\"\n \"- Each Logline should use 'What if...' format\\n\"\n \"- The 3 Loglines should be diverse in style and focus\\n\\n\"\n \"Input: {idea}\\n\\n\"\n \"Output ONLY a JSON array with exactly 3 elements (no code block markers, no other text):\\n\"\n '[{{\"logline\":\"What if...\",\"who\":\"protagonist\",\"goal\":\"goal\",\"conflict\":\"conflict\",\"twist\":\"twist\",\"theme\":\"theme\"}}]'\n)\n\n# =============================================================\n# Phase 1B: Logline 检测 & 提取(未勾选扩写时)\n# =============================================================\n\nLOGLINE_CHECK_PROMPT_ZH = (\n \"请判断以下文本是否包含足够的叙事要素,能够从中总结出一个完整故事的 Logline。\\n\"\n \"(完整 Logline 需涵盖:主角、目标、核心障碍、反转和主题)\\n\\n\"\n \"文本:{idea}\\n\\n\"\n \"只回答 Yes 或 No,不要添加任何其他内容。\"\n)\n\nLOGLINE_CHECK_PROMPT_EN = (\n \"Determine if the following text contains enough narrative elements for a complete story Logline.\\n\"\n \"(Needs: protagonist, goal, conflict, twist, and theme)\\n\\n\"\n \"Text: {idea}\\n\\n\"\n \"Answer only Yes or No.\"\n)\n\nLOGLINE_EXTRACT_PROMPT_ZH = (\n \"你是资深制片人。请从以下文本中总结提取 Logline 及故事五要素。\\n\"\n \"Logline 请使用\\\"如果…会怎样\\\"的句式。\\n\\n\"\n \"文本:{idea}\\n\\n\"\n \"请严格按如下 JSON 格式输出(直接输出纯JSON,不要用```包裹,不要添加任何其他文字):\\n\"\n '{{\"logline\":\"如果...会怎样\",\"who\":\"主角描述\",\"goal\":\"目标\",\"conflict\":\"核心障碍\",\"twist\":\"反转\",\"theme\":\"潜在主题\"}}'\n)\n\nLOGLINE_EXTRACT_PROMPT_EN = (\n \"You are a senior producer. Extract the Logline and five story elements from the following text.\\n\"\n \"The Logline should use 'What if...' format.\\n\\n\"\n \"Text: {idea}\\n\\n\"\n \"Output ONLY valid JSON (no code block markers, no other text):\\n\"\n '{{\"logline\":\"What if...\",\"who\":\"protagonist\",\"goal\":\"goal\",\"conflict\":\"conflict\",\"twist\":\"twist\",\"theme\":\"theme\"}}'\n)\n\n# =============================================================\n# Phase 2A: 节拍表 (Save the Cat! Beat Sheet)\n# =============================================================\n\nBEAT_SHEET_PROMPT_ZH = (\n \"你是好莱坞编剧导师。请根据以下故事线,使用 Save the Cat! 节拍表将故事拆解为四幕结构。\\n\\n\"\n \"必须包含以下四个关键节拍(起承转合):\\n\"\n \"第一幕 - 激励事件(Inciting Incident):建立世界观和主角现状,发生打破平衡的事件\\n\"\n \"第二幕 - 进入新世界(Break into Two):主角踏上旅程,面对挑战和考验,副线展开\\n\"\n \"第三幕 - 灵魂黑夜(Dark Night of the Soul):主角遭受最大打击,陷入低谷\\n\"\n \"第四幕 - 高潮决战(Finale):主角获得顿悟,最终决战,故事收束\\n\\n\"\n \"请确保:逻辑严密、冲突逐步升级、每一幕有清晰的转折点。\\n\\n\"\n \"故事线:\\n{draft}\\n\\n\"\n \"故事风格:{style}\\n\\n\"\n \"请直接输出四幕节拍表,每一幕用\\\"【第X幕 - 名称】\\\"标记开始,详细描述该幕的情节要点、角色发展和关键转折。\"\n)\n\nBEAT_SHEET_PROMPT_EN = (\n \"You are a Hollywood screenwriting mentor. Break down the following storyline into a 4-act structure \"\n \"using the Save the Cat! Beat Sheet.\\n\\n\"\n \"Must include these four key beats:\\n\"\n \"Act 1 - Inciting Incident: Establish the world and protagonist's status quo, then a disruptive event\\n\"\n \"Act 2 - Break into Two: Protagonist embarks on journey, faces challenges, B-story unfolds\\n\"\n \"Act 3 - Dark Night of the Soul: Protagonist suffers the biggest blow, hits rock bottom\\n\"\n \"Act 4 - Finale: Protagonist gains epiphany, final confrontation, story resolves\\n\\n\"\n \"Ensure: tight logic, escalating conflict, clear turning points in each act.\\n\\n\"\n \"Storyline:\\n{draft}\\n\\n\"\n \"Story style: {style}\\n\\n\"\n \"Output the 4-act beat sheet directly. Start each act with '[Act X - Name]' heading. \"\n \"Detail the plot points, character development and key turning points for each act.\"\n)\n\n# =============================================================\n# Phase 2B: 分场大纲 (Step Outline, 逐幕生成)\n# =============================================================\n\nSTEP_OUTLINE_PROMPT_ZH = (\n \"你是分场导演。以下是完整的四幕节拍表:\\n\\n\"\n \"{beat_sheet}\\n\\n\"\n \"请将第{act_number}幕({act_name})转化为详细的分场大纲。\\n\\n\"\n \"格式要求(每场一段):\\n\"\n \"[场次编号]. [地点(室内/室外)] - [日/夜]\\n\"\n \"[核心动作]:详细描述该场戏发生了什么,包含完整的对话、动作和表情描写。\\n\"\n \"[情感转变]:描述主角在本场戏开始到结束的情绪变化(+/-)。\\n\"\n \"[出场角色]:列出本场出现的所有角色。\\n\\n\"\n \"要求:\\n\"\n \"- 每个角色都要有详细的外貌描写(发型、眼睛颜色、体型、服装颜色和款式等视觉特征)\\n\"\n \"- **重要:外貌描写必须是静态的、贯穿全剧保持一致的特征,不要随剧情发展而变化**\\n\"\n \" - 例如:不要写\\\"他穿着破碎的衣服\\\"这种随情节变化的描写\\n\"\n \" - 应该写:\\\"他身穿蓝色衬衫,黑色长裤\\\"这种固定的服装描述\\n\"\n \"- 每个场景都要有详细的环境、布局、色彩与氛围描写\\n\"\n \"- 分场数量根据情节长度和节奏自行决定,确保叙事节奏合理\\n\"\n \"- 对话用双引号标注,对话内容真实生动\\n\"\n \"- 场次编号从 {scene_start} 开始递增\\n\"\n \"- 故事风格:{style}\\n\\n\"\n \"请直接输出分场大纲,不要添加幕次标题或额外说明。\"\n)\n\nSTEP_OUTLINE_PROMPT_EN = (\n \"You are a scene director. Here is the complete 4-act beat sheet:\\n\\n\"\n \"{beat_sheet}\\n\\n\"\n \"Convert Act {act_number} ({act_name}) into a detailed step outline.\\n\\n\"\n \"Format for each scene:\\n\"\n \"[Scene number]. [Location (Indoor/Outdoor)] - [Day/Night]\\n\"\n \"[Core Action]: Detailed description of what happens, including dialogue, actions and expressions.\\n\"\n \"[Emotional Shift]: Describe the protagonist's emotional change from start to end (+/-).\\n\"\n \"[Characters Present]: List all characters appearing in this scene.\\n\\n\"\n \"Requirements:\\n\"\n \"- Each character needs detailed physical descriptions (hair, eyes, build, clothing details)\\n\"\n \"- **IMPORTANT: Physical descriptions must be STATIC and consistent throughout the entire story - do NOT change with plot development**\\n\"\n \" - For example: do NOT write \\\"wearing torn clothes\\\" which changes with the plot\\n\"\n \" - Instead write: \\\"wearing a blue shirt and black pants\\\" which is a fixed clothing description\\n\"\n \"- Each scene needs detailed environment, layout, color and atmosphere descriptions\\n\"\n \"- Number of scenes based on plot length and pacing\\n\"\n \"- Dialogue marked with double quotes, natural and vivid\\n\"\n \"- Scene numbers start from {scene_start} and increment\\n\"\n \"- Story style: {style}\\n\\n\"\n \"Output the step outline directly, without act headings or extra notes.\"\n)\n\nACT_NAMES_ZH = {1: \"激励事件\", 2: \"进入新世界\", 3: \"灵魂黑夜\", 4: \"高潮决战\"}\nACT_NAMES_EN = {1: \"Inciting Incident\", 2: \"Break into Two\", 3: \"Dark Night of the Soul\", 4: \"Finale\"}\n\n# =============================================================\n# Micro-film 微电影模式专用提示词\n# =============================================================\n\nMICRO_BEAT_SHEET_PROMPT_ZH = (\n \"你是微电影编剧专家。请根据以下故事线,将故事压缩为一个紧凑的单幕剧情概要。\\n\\n\"\n \"要求:\\n\"\n \"- 叙事节奏快,情节紧凑精炼,没有拖沓的铺垫\\n\"\n \"- 全部内容在一幕内完成,不分幕\\n\"\n \"- 保留核心冲突和情感转折,去掉多余叙事\\n\"\n \"- 场景数量控制在 3-6 场\\n\"\n \"- 适合 1-3 分钟的微电影\\n\\n\"\n \"故事线:\\n{draft}\\n\\n\"\n \"故事风格:{style}\\n\\n\"\n \"请直接输出紧凑的剧情概要,描述场景发展、核心动作和情感转折。\"\n)\n\nMICRO_BEAT_SHEET_PROMPT_EN = (\n \"You are a micro-film screenwriting expert. Compress the following storyline \"\n \"into a compact single-act plot summary.\\n\\n\"\n \"Requirements:\\n\"\n \"- Fast narrative pacing, concise and tight plot\\n\"\n \"- All content in a single act, no act divisions\\n\"\n \"- Keep core conflict and emotional turns, remove unnecessary setup\\n\"\n \"- 3-6 scenes total\\n\"\n \"- Suitable for a 1-3 minute short film\\n\\n\"\n \"Storyline:\\n{draft}\\n\\n\"\n \"Story style: {style}\\n\\n\"\n \"Output a compact plot summary directly, describing scene progression, \"\n \"core actions and emotional shifts.\"\n)\n\nMICRO_STEP_OUTLINE_PROMPT_ZH = (\n \"你是分场导演。以下是微电影剧情概要:\\n\\n\"\n \"{beat_sheet}\\n\\n\"\n \"请将其转化为详细的分场大纲。\\n\\n\"\n \"格式要求(每场一段):\\n\"\n \"[场次编号]. [地点(室内/室外)] - [日/夜]\\n\"\n \"[核心动作]:详细描述该场戏发生了什么,包含完整的对话、动作和表情描写。\\n\"\n \"[情感转变]:描述主角在本场戏开始到结束的情绪变化(+/-)。\\n\"\n \"[出场角色]:列出本场出现的所有角色。\\n\\n\"\n \"要求:\\n\"\n \"- 每个角色都要有详细的外貌描写(发型、眼睛颜色、体型、服装颜色和款式等视觉特征)\\n\"\n \"- **重要:外貌描写必须是静态的、贯穿全剧保持一致的特征,不要随剧情发展而变化**\\n\"\n \" - 例如:不要写\\\"他穿着破碎的衣服\\\"这种随情节变化的描写\\n\"\n \" - 应该写:\\\"他身穿蓝色衬衫,黑色长裤\\\"这种固定的服装描述\\n\"\n \"- 每个场景都要有详细的环境、布局、色彩与氛围描写\\n\"\n \"- 分场数量 3-6 场,叙事紧凑快节奏\\n\"\n \"- 对话用双引号标注,对话内容真实生动\\n\"\n \"- 场次编号从 1 开始递增\\n\"\n \"- 故事风格:{style}\\n\\n\"\n \"请直接输出分场大纲,不要添加额外说明。\"\n)\n\nMICRO_STEP_OUTLINE_PROMPT_EN = (\n \"You are a scene director. Here is the micro-film plot summary:\\n\\n\"\n \"{beat_sheet}\\n\\n\"\n \"Convert it into a detailed step outline.\\n\\n\"\n \"Format for each scene:\\n\"\n \"[Scene number]. [Location (Indoor/Outdoor)] - [Day/Night]\\n\"\n \"[Core Action]: Detailed description including dialogue, actions and expressions.\\n\"\n \"[Emotional Shift]: Protagonist's emotional change from start to end (+/-).\\n\"\n \"[Characters Present]: All characters appearing in this scene.\\n\\n\"\n \"Requirements:\\n\"\n \"- Each character needs detailed physical descriptions (hair, eyes, build, clothing)\\n\"\n \"- **IMPORTANT: Physical descriptions must be STATIC and consistent throughout the entire story - do NOT change with plot development**\\n\"\n \" - For example: do NOT write \\\"wearing torn clothes\\\" which changes with the plot\\n\"\n \" - Instead write: \\\"wearing a blue shirt and black pants\\\" which is a fixed clothing description\\n\"\n \"- Each scene needs detailed environment, layout, color and atmosphere descriptions\\n\"\n \"- 3-6 scenes total, compact and fast-paced\\n\"\n \"- Dialogue marked with double quotes, natural and vivid\\n\"\n \"- Scene numbers start from 1 and increment\\n\"\n \"- Story style: {style}\\n\\n\"\n \"Output the step outline directly, without extra notes.\"\n)\n\nMICRO_META_EXTRACT_PROMPT_ZH = (\n \"你是专业的剧本分析师。以下是微电影剧情概要:\\n\\n\"\n \"{beat_sheet}\\n\\n\"\n \"请从中提取以下信息,以纯JSON格式输出(不要用```包裹):\\n\"\n '{{\"title\":\"故事标题(2-8字)\",\"logline\":\"一句话概括故事(30字以内)\",'\n '\"genre\":[\"类型1\",\"类型2\"],\"synopsis\":\"完整故事梗概(50-100字)\",'\n '\"mood\":\"影片情绪基调\"}}'\n)\n\nMICRO_META_EXTRACT_PROMPT_EN = (\n \"You are a professional script analyst. Here is the micro-film plot summary:\\n\\n\"\n \"{beat_sheet}\\n\\n\"\n \"Extract the following information as pure JSON (no code blocks):\\n\"\n '{{\"title\":\"Story title (short)\",\"logline\":\"One sentence summary\",'\n '\"genre\":[\"genre1\",\"genre2\"],\"synopsis\":\"Complete synopsis (50-100 words)\",'\n '\"mood\":\"Overall mood\"}}'\n)\n\n# =============================================================\n# Phase 3A: 单幕结构化 JSON 提取\n# =============================================================\n\nACT_EXTRACT_INTRO_ZH = (\n \"你是一个专业的剧本分析师。请仔细阅读以下第{act_number}幕的分场大纲,从中提取并整理为标准JSON格式。\\n\\n\"\n \"分场大纲:\\n{outline}\\n\\n\"\n \"请严格按照以下JSON结构输出(直接输出纯JSON,不要用```包裹,不要添加注释或任何其他文字):\\n\\n\"\n)\n\nACT_EXTRACT_INTRO_EN = (\n \"You are a professional script analyst. Read the following Act {act_number} step outline carefully \"\n \"and extract it into a structured JSON format.\\n\\n\"\n \"Step outline:\\n{outline}\\n\\n\"\n \"Output ONLY valid JSON in the following structure (no code block markers, no comments, no other text):\\n\\n\"\n)\n\nACT_EXTRACT_SCHEMA_ZH = \"\"\"{{\n \"characters\": [\n {{\n \"name\": \"角色全名\",\n \"character_id\": \"char_加8位随机字母数字\",\n \"description\": \"外貌描写: 含发型、体型、服装颜色和款式等视觉特征, 不要描述眼睛颜色, 面部不要有文字等特殊符号, 形象要正常(可以前卫时髦但不能恐怖灵异), 50-80字, 不要用比喻\",\n \"personality\": [\"性格特征1\", \"性格特征2\", \"性格特征3\"],\n \"motivation\": \"角色核心动机\",\n \"arc_description\": \"角色成长弧线\",\n \"role\": \"主角 或 配角 或 背景\",\n \"age\": \"年龄\",\n \"species\": \"人类 或 具体动物种类\"\n }}\n ],\n \"settings\": [\n {{\n \"name\": \"场景名称(室内) 或 场景名称(室外)\",\n \"setting_id\": \"set_加8位随机字母数字\",\n \"description\": \"环境布局、色彩、光线、氛围等视觉细节, 80-120字, 不要包含人物或动物\"\n }}\n ],\n \"scenes\": [\n {{\n \"scene_number\": {scene_start},\n \"act\": {act_number},\n \"location\": \"必须是settings中已定义的场景名称\",\n \"characters\": [\"出场角色名(必须与characters中的name一致)\"],\n \"plot\": \"该场景完整详细的剧情, 含对话、动作和表情描写, 必须完整表达分场内容, 无字数限制\"\n }}\n ]\n}}\"\"\"\n\nACT_EXTRACT_SCHEMA_EN = \"\"\"{{\n \"characters\": [\n {{\n \"name\": \"Full name\",\n \"character_id\": \"char_ plus 8 random alphanumeric\",\n \"description\": \"Visual description: hair, eyes, build, clothing details, 50-80 words\",\n \"personality\": [\"trait1\", \"trait2\", \"trait3\"],\n \"motivation\": \"Core motivation\",\n \"arc_description\": \"Character growth arc\",\n \"role\": \"protagonist or supporting or background\",\n \"age\": \"age\",\n \"species\": \"human or specific animal\"\n }}\n ],\n \"settings\": [\n {{\n \"name\": \"Location name (Indoor) or Location name (Outdoor)\",\n \"setting_id\": \"set_ plus 8 random alphanumeric\",\n \"description\": \"Layout, colors, lighting, atmosphere details, 80-120 words, no people or animals\"\n }}\n ],\n \"scenes\": [\n {{\n \"scene_number\": {scene_start},\n \"act\": {act_number},\n \"location\": \"Must be a name defined in settings\",\n \"characters\": [\"character names matching characters array\"],\n \"plot\": \"Complete detailed scene plot with dialogue, actions and expressions, no word limit\"\n }}\n ]\n}}\"\"\"\n\nACT_EXTRACT_RULES_ZH = (\n \"\\n\\n重要规则:\\n\"\n \"1. characters中的name必须与scenes中的角色名完全一致\\n\"\n \"2. scenes中的location必须是settings中已定义的场景名称之一\\n\"\n \"3. settings的name需标注(室内)或(室外)\\n\"\n \"4. 所有scene的act字段必须为{act_number}\\n\"\n \"5. scene_number从{scene_start}开始递增\\n\"\n \"6. 角色的description要有足够视觉细节用于AI图像生成\\n\"\n \"7. setting的description要有足够视觉细节用于AI背景图生成\\n\"\n \"8. 不要生成群体角色(如\\\"邻居们\\\"),每个角色都是独立个体\\n\"\n \"9. scene的plot字段必须完整表达该分场的全部内容\\n\"\n \"10. 只输出纯JSON\\n\"\n)\n\nACT_EXTRACT_RULES_EN = (\n \"\\n\\nImportant rules:\\n\"\n \"1. Character names in scenes must exactly match names in characters array\\n\"\n \"2. Scene locations must be names defined in settings array\\n\"\n \"3. Setting names must include (Indoor) or (Outdoor)\\n\"\n \"4. All scenes' act field must be {act_number}\\n\"\n \"5. scene_number starts from {scene_start} and increments\\n\"\n \"6. Character descriptions need enough visual detail for AI image generation\\n\"\n \"7. Setting descriptions need enough visual detail for AI background generation\\n\"\n \"8. No group characters, every character is an individual\\n\"\n \"9. Scene 'plot' field must fully express all content, do not truncate\\n\"\n \"10. Output ONLY the JSON\\n\"\n)\n\n# =============================================================\n# Phase 3B: 最终合并补充提示词(生成 title / logline / synopsis 等顶层字段)\n# =============================================================\n\nMETA_EXTRACT_PROMPT_ZH = (\n \"你是专业的剧本分析师。以下是完整的四幕节拍表:\\n\\n\"\n \"{beat_sheet}\\n\\n\"\n \"请从中提取以下信息,以纯JSON格式输出(不要用```包裹):\\n\"\n '{{\"title\":\"故事标题(2-8字)\",\"logline\":\"一句话概括故事(30字以内)\",'\n '\"genre\":[\"类型1\",\"类型2\"],\"synopsis\":\"完整故事梗概(100-200字)\",'\n '\"mood\":\"影片情绪基调\"}}'\n)\n\nMETA_EXTRACT_PROMPT_EN = (\n \"You are a professional script analyst. Here is the complete 4-act beat sheet:\\n\\n\"\n \"{beat_sheet}\\n\\n\"\n \"Extract the following information as pure JSON (no code blocks):\\n\"\n '{{\"title\":\"Story title (short)\",\"logline\":\"One sentence summary\",'\n '\"genre\":[\"genre1\",\"genre2\"],\"synopsis\":\"Complete synopsis (100-200 words)\",'\n '\"mood\":\"Overall mood\"}}'\n)\n\n# =============================================================\n# Phase 4: 场景 & 角色合并(去重相似条目)\n# =============================================================\n\nCONSOLIDATE_PROMPT_ZH = (\n \"你是剧本审校专家。请审查以下剧本中的场景列表和角色列表,找出可以合并的条目。\\n\\n\"\n \"合并规则:\\n\"\n \"- 场景合并:如果两个场景在物理空间上是同一个地点(只是拍摄角度、景别不同),应合并为一个\\n\"\n \" 例如:「车厢内」「车厢过道」「车厢全景」都是同一节车厢 → 合并为「车厢内」\\n\"\n \" 例如:「咖啡馆吧台」「咖啡馆角落」→ 合并为「咖啡馆」\\n\"\n \" 注意:不同物理空间不能合并(如「车厢内」和「车厢连接处」是不同空间)\\n\"\n \"- 角色合并:如果同名角色出现多次(可能描述略有不同),合并为一个\\n\"\n \"- 合并后保留最详细的描述\\n\\n\"\n \"当前场景列表:\\n{settings_json}\\n\\n\"\n \"当前角色列表:\\n{characters_json}\\n\\n\"\n \"请输出纯JSON(不要用```包裹),格式如下:\\n\"\n '{{\\n'\n ' \"setting_merges\": {{\"被合并的场景名\": \"合并到的目标场景名\", ...}},\\n'\n ' \"character_merges\": {{\"被合并的角色名\": \"合并到的目标角色名\", ...}}\\n'\n '}}\\n\\n'\n \"如果没有需要合并的条目,对应字段返回空对象 {{}}。\\n\"\n \"只输出纯JSON,不要添加任何其他文字。\"\n)\n\nCONSOLIDATE_PROMPT_EN = (\n \"You are a script review expert. Review the following settings and characters lists \"\n \"and identify entries that should be merged.\\n\\n\"\n \"Merge rules:\\n\"\n \"- Settings merge: If two settings are the same physical location (just different camera angles), \"\n \"merge them into one. E.g., 'Train interior', 'Train aisle', 'Train panorama' are all the same car.\\n\"\n \" Different physical spaces should NOT be merged.\\n\"\n \"- Character merge: If the same character appears with slightly different descriptions, merge them.\\n\"\n \"- Keep the most detailed description after merging.\\n\\n\"\n \"Current settings:\\n{settings_json}\\n\\n\"\n \"Current characters:\\n{characters_json}\\n\\n\"\n \"Output ONLY valid JSON (no code blocks):\\n\"\n '{{\\n'\n ' \"setting_merges\": {{\"merged_setting_name\": \"target_setting_name\", ...}},\\n'\n ' \"character_merges\": {{\"merged_char_name\": \"target_char_name\", ...}}\\n'\n '}}\\n\\n'\n \"If nothing to merge, return empty objects {{}}. Output ONLY the JSON.\"\n)\n\nclass ScriptWriterAgent(AgentInterface):\n \"\"\"编剧智能体:多轮LLM交互 → 结构化剧本JSON\"\"\"\n\n def __init__(self):\n super().__init__(name=\"ScriptWriter\")\n\n # ─── JSON 提取 ───\n\n @staticmethod\n def _extract_json_from_text(text: str) -> Optional[dict]:\n \"\"\"从LLM输出中提取JSON对象\"\"\"\n text = text.strip()\n text = re.sub(r'^```(?:json)?\\s*', '', text)\n text = re.sub(r'\\s*```

AIGC-Director Agent Skill 本地运行 :这是一个 本地部署 的视频生成项目, 前后端都运行在本机 : - 后端: - 前端: - 所有 API 调用都请求本地服务器,不要请求其他地址! - 确保在调用任何 API 之前,后端和前端服务都已经启动并运行正常! 核心理念 :Agent 应该像"持续陪伴的智能视频制作助理",每完成一个用户可感知的重要任务,都应立即给用户一条简报,并等待用户确认。 核心原则 :每个阶段的产物都必须展示给用户,必须停下来等待用户确认后才能继续下一阶段。 防止遗忘 :在整个流程中,Agent 可能会忘记之前的用户输入或之前阶段的产物内容。 每当进入一个新的阶段时,Agent 都必须重新加载这篇SKILL文档,确保不会忘记任何细节 。 --- 项目结构 产物存放目录 : - - 剧本产物 - - 图片产物(角色、场景、参考图) - - 视频产物 --- 阶段与停点(共9个) | 停点 | 阶段 | phase 值 | 描述 | 操作 | |------|------|----------|------|------| | 1 | 项目配置 | - | 确认配置选项 | 展示配置 → 用户确认 | | 2 | 剧本生成 | suggest expand | 建议扩写 | 等待用户确认 | | 3 | 剧本生成 | logline sele…

, '', text)\n text = text.strip()\n\n try:\n return json.loads(text)\n except json.JSONDecodeError:\n pass\n\n start = text.find('{')\n end = text.rfind('}')\n if start != -1 and end != -1 and end > start:\n try:\n return json.loads(text[start:end + 1])\n except json.JSONDecodeError:\n pass\n return None\n\n @staticmethod\n def _extract_json_array_from_text(text: str) -> Optional[list]:\n \"\"\"从LLM输出中提取JSON数组\"\"\"\n text = text.strip()\n # 去除 markdown 代码块标记(支持多行)\n text = re.sub(r'```(?:json)?\\s*\\n?', '', text)\n text = text.strip()\n\n try:\n result = json.loads(text)\n if isinstance(result, list):\n return result\n # LLM 有时返回对象而非数组,尝试包装\n if isinstance(result, dict):\n return [result]\n except json.JSONDecodeError:\n pass\n\n start = text.find('[')\n end = text.rfind(']')\n if start != -1 and end != -1 and end > start:\n try:\n result = json.loads(text[start:end + 1])\n if isinstance(result, list):\n return result\n except json.JSONDecodeError:\n pass\n\n # 尝试匹配多个独立 JSON 对象\n objects = []\n for m in re.finditer(r'\\{[^{}]*(?:\\{[^{}]*\\}[^{}]*)*\\}', text):\n try:\n obj = json.loads(m.group())\n if isinstance(obj, dict) and 'logline' in obj:\n objects.append(obj)\n except json.JSONDecodeError:\n continue\n if objects:\n return objects\n\n return None\n\n @staticmethod\n def _gen_id(prefix: str = \"char\") -> str:\n return f\"{prefix}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=8))}\"\n\n # ─── 保存结果 ───\n\n def _save_result(self, json_data: dict, sid: str, is_zh: bool):\n \"\"\"保存结构化剧本JSON到结果文件\"\"\"\n from config import settings\n script_dir = os.path.join(settings.RESULT_DIR, 'script')\n os.makedirs(script_dir, exist_ok=True)\n\n result_file = os.path.join(script_dir, f'script_{sid}.json')\n results = {}\n if os.path.exists(result_file) and os.path.getsize(result_file) > 0:\n with open(result_file, 'r', encoding='utf-8') as f:\n results = json.load(f)\n\n results.setdefault(str(sid), {})['script_json'] = json_data\n with open(result_file, 'w', encoding='utf-8') as f:\n json.dump(results, f, indent=4, ensure_ascii=False)\n\n def _save_progress(self, sid: str, phase: str, data: dict):\n \"\"\"保存剧本生成的中间进度状态到结果文件\"\"\"\n from config import settings\n script_dir = os.path.join(settings.RESULT_DIR, 'script')\n os.makedirs(script_dir, exist_ok=True)\n\n result_file = os.path.join(script_dir, f'script_{sid}.json')\n results = {}\n if os.path.exists(result_file) and os.path.getsize(result_file) > 0:\n with open(result_file, 'r', encoding='utf-8') as f:\n results = json.load(f)\n\n # 初始化 progress 字段\n results.setdefault(str(sid), {}).setdefault('progress', {})\n\n # 保存当前阶段信息\n results[str(sid)]['progress']['current_phase'] = phase\n results[str(sid)]['progress']['updated_at'] = datetime.now(timezone.utc).isoformat()\n\n # 根据阶段保存对应的数据\n if phase == 'logline_check':\n # 创意检查结果\n results[str(sid)]['progress']['idea_analyzed'] = data.get('idea_analyzed', False)\n results[str(sid)]['progress']['logline_extracted'] = data.get('logline_summary')\n elif phase == 'logline_generation':\n # 生成的候选 logline\n results[str(sid)]['progress']['logline_options'] = data.get('logline_options', [])\n elif phase == 'logline_confirmed':\n # 用户选择的 logline\n results[str(sid)]['progress']['selected_logline'] = data.get('selected_logline')\n elif phase == 'mode_selection':\n # 用户选择的模式\n results[str(sid)]['progress']['selected_mode'] = data.get('selected_mode')\n elif phase == 'script_generation':\n # 完整剧本已生成\n results[str(sid)]['progress']['script_generated'] = True\n results[str(sid)]['progress']['title'] = data.get('title')\n\n with open(result_file, 'w', encoding='utf-8') as f:\n json.dump(results, f, indent=4, ensure_ascii=False)\n\n # ─── 核心流程 ───\n\n async def process(self, input_data: Any, intervention: Optional[Dict] = None) -> Dict:\n\n # 检查当前阶段,避免重复生成 logline\n current_phase = input_data.get(\"phase\", \"\")\n selected_logline = input_data.get(\"selected_logline\")\n selected_mode = input_data.get(\"selected_mode\")\n\n # 如果已选择模式,直接生成剧本\n if selected_mode:\n # 如果 selected_logline 是索引(数字),需要转换为 logline 对象\n if isinstance(selected_logline, int):\n logline_options = input_data.get(\"logline_options\", [])\n if 0 \u003c= selected_logline \u003c len(logline_options):\n logline_data = logline_options[selected_logline]\n else:\n logline_data = {}\n else:\n logline_data = selected_logline if isinstance(selected_logline, dict) else {}\n\n if selected_mode == \"micro\":\n return await self._phase_micro_script_gen(input_data, logline_data)\n else:\n return await self._phase_script_gen(input_data, logline_data)\n\n # 如果已选择 logline 但未选择模式,返回模式选择\n if selected_logline and current_phase == \"mode_selection\":\n return {\n \"payload\": {\n \"phase\": \"mode_selection\",\n \"selected_logline\": selected_logline,\n \"session_id\": input_data.get(\"session_id\", \"\"),\n },\n \"requires_intervention\": True,\n \"openclaw_hint\": \"用户已选择情节,需要选择拍摄模式(电影模式4幕/微电影模式1幕)。请展示给用户并等待用户选择。\",\n }\n\n # (A) 用户介入修改最终剧本\n if intervention and \"modified_script\" in intervention:\n modified = intervention[\"modified_script\"]\n sid = input_data.get(\"session_id\", \"\")\n if isinstance(modified, str):\n modified = self._extract_json_from_text(modified) or {}\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in modified.get(\"title\", \"\"))\n modified[\"session_id\"] = sid\n self._save_result(modified, sid, is_zh)\n return {\"payload\": modified, \"requires_intervention\": False, \"stage_completed\": True}\n\n # (B1) 用户选择了创作模式 → 根据模式生成剧本\n if intervention and \"selected_mode\" in intervention:\n selected = input_data.get(\"selected_logline\", {})\n mode = intervention[\"selected_mode\"]\n sid = input_data.get(\"session_id\", \"\")\n\n # 如果 selected_logline 是索引(数字),转换为对应的 logline 对象\n if isinstance(selected, int):\n logline_options = input_data.get(\"logline_options\", [])\n if 0 \u003c= selected \u003c len(logline_options):\n logline_data = logline_options[selected]\n else:\n logline_data = {}\n else:\n logline_data = selected if isinstance(selected, dict) else {}\n\n # 保存进度状态\n self._save_progress(sid, \"mode_selection\", {\n \"selected_mode\": mode\n })\n\n if mode == \"micro\":\n return await self._phase_micro_script_gen(input_data, logline_data)\n else:\n return await self._phase_script_gen(input_data, logline_data)\n\n # (B2) 用户选择了一个 Logline → 显示模式选择\n if intervention and \"selected_logline\" in intervention:\n selected = intervention[\"selected_logline\"]\n sid = input_data.get(\"session_id\", \"\")\n\n # 如果 selected_logline 是索引(数字),转换为对应的 logline 对象\n if isinstance(selected, int):\n logline_options = input_data.get(\"logline_options\", [])\n if 0 \u003c= selected \u003c len(logline_options):\n logline_data = logline_options[selected]\n else:\n logline_data = {}\n else:\n logline_data = selected if isinstance(selected, dict) else {}\n\n # 保存进度状态\n self._save_progress(sid, \"logline_confirmed\", {\n \"selected_logline\": logline_data\n })\n\n return {\n \"payload\": {\n \"phase\": \"mode_selection\",\n \"selected_logline\": logline_data,\n \"session_id\": sid,\n },\n \"requires_intervention\": True,\n \"openclaw_hint\": \"用户已选择情节,需要选择拍摄模式(电影模式4幕/微电影模式1幕)。请展示给用户并等待用户选择。\",\n }\n\n # 首次运行:自动判断是直接提取 logline 还是进入扩写\n auto_mode = input_data.get(\"auto_mode\", False)\n\n # 代理(自动)模式:跳过 Logline 选择,直接生成剧本\n if auto_mode:\n return await self._phase_direct(input_data)\n\n # 自动判断:创意足够则提取 logline,否则自动扩写生成 3 个选项\n return await self._phase_logline_check(input_data)\n\n # ─── Phase 1A: 生成 3 个 Logline 方案 ───\n\n async def _phase_logline_gen(self, input_data: Dict) -> Dict:\n idea = input_data.get(\"idea\", \"\")\n sid = input_data.get(\"session_id\", \"\")\n llm_model = input_data.get(\"llm_model\", \"qwen3.5-plus\")\n web_search = input_data.get(\"web_search\", False)\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in idea)\n\n def run():\n from tool.llm_client import LLM\n llm = LLM()\n self._report_progress(\"剧本生成\", \"正在生成 Logline 方案...\", 5)\n prompt = (LOGLINE_GENERATE_PROMPT_ZH if is_zh else LOGLINE_GENERATE_PROMPT_EN).format(idea=idea)\n\n for attempt in range(3):\n self._check_cancel()\n raw = self._cancellable_query(llm, prompt, model=llm_model, task_id=sid, web_search=web_search)\n logger.info(f\"[ScriptWriter] Logline gen attempt {attempt + 1}, raw output ({len(raw)} chars): {raw[:500]}\")\n options = self._extract_json_array_from_text(raw)\n if options and len(options) > 0:\n # 确保每个选项包含必要字段\n required = {\"logline\", \"who\", \"goal\", \"conflict\", \"twist\", \"theme\"}\n valid = [o for o in options if isinstance(o, dict) and required.issubset(o.keys())]\n if valid:\n self._report_progress(\"剧本生成\", \"Logline 方案已生成\", 20)\n return valid[:3]\n\n logger.warning(f\"[ScriptWriter] Logline gen attempt {attempt + 1}: parse failed\")\n if is_zh:\n prompt = (\n \"上次输出格式不正确,请严格按要求重新输出。\"\n \"必须输出纯JSON数组,不要用```包裹,不要有任何多余文字。\\n\\n\"\n + (LOGLINE_GENERATE_PROMPT_ZH).format(idea=idea)\n )\n else:\n prompt = (\n \"Previous output format was incorrect. Please try again. \"\n \"Output ONLY a raw JSON array, no code blocks, no extra text.\\n\\n\"\n + (LOGLINE_GENERATE_PROMPT_EN).format(idea=idea)\n )\n\n raise Exception(\"Logline 生成失败:多次尝试均无法解析输出\")\n\n loop = asyncio.get_running_loop()\n options = await loop.run_in_executor(None, run)\n\n # 保存进度状态\n self._save_progress(sid, \"logline_generation\", {\n \"logline_options\": options\n })\n\n return {\n \"payload\": {\n \"phase\": \"logline_selection\",\n \"logline_options\": options,\n \"session_id\": sid,\n },\n \"requires_intervention\": True,\n \"openclaw_hint\": \"生成了多个情节候选(logline),需要用户选择。请展示给用户并等待用户选择。\",\n }\n\n # ─── Phase 1B: 检测输入是否可提取 Logline ───\n\n async def _phase_logline_check(self, input_data: Dict) -> Dict:\n idea = input_data.get(\"idea\", \"\")\n sid = input_data.get(\"session_id\", \"\")\n llm_model = input_data.get(\"llm_model\", \"qwen3.5-plus\")\n web_search = input_data.get(\"web_search\", False)\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in idea)\n\n def run():\n from tool.llm_client import LLM\n llm = LLM()\n self._report_progress(\"剧本生成\", \"分析创意文本...\", 5)\n check_prompt = (LOGLINE_CHECK_PROMPT_ZH if is_zh else LOGLINE_CHECK_PROMPT_EN).format(idea=idea)\n answer = self._cancellable_query(llm, check_prompt, model=llm_model, task_id=sid, web_search=web_search).strip()\n\n if answer.lower().startswith(\"yes\"):\n self._report_progress(\"剧本生成\", \"提取 Logline...\", 10)\n extract_prompt = (LOGLINE_EXTRACT_PROMPT_ZH if is_zh else LOGLINE_EXTRACT_PROMPT_EN).format(idea=idea)\n raw = self._cancellable_query(llm, extract_prompt, model=llm_model, task_id=sid, web_search=web_search)\n logger.info(f\"[ScriptWriter] Logline extract raw ({len(raw)} chars): {raw[:500]}\")\n logline_data = self._extract_json_from_text(raw)\n if logline_data and isinstance(logline_data, dict) and 'logline' in logline_data:\n self._report_progress(\"剧本生成\", \"Logline 提取完成\", 20)\n return {\"phase\": \"logline_confirm\", \"logline_summary\": logline_data}\n\n return {\"phase\": \"suggest_expand\"}\n\n loop = asyncio.get_running_loop()\n result = await loop.run_in_executor(None, run)\n result[\"session_id\"] = sid\n\n # 如果创意不够,自动进入扩写阶段,生成 3 个 logline 供选择\n if result.get(\"phase\") == \"suggest_expand\":\n return await self._phase_logline_gen(input_data)\n\n # 保存进度状态\n phase = result.get(\"phase\", \"logline_check\")\n self._save_progress(sid, \"logline_check\", {\n \"idea_analyzed\": phase == \"logline_confirm\",\n \"logline_summary\": result.get(\"logline_summary\")\n })\n\n return {\n \"payload\": result,\n \"requires_intervention\": True,\n \"openclaw_hint\": \"已提取情节概要,需要用户确认。请展示给用户并等待用户确认。\",\n }\n\n # ─── Phase 2: 根据选定 Logline 生成完整剧本 ───\n\n async def _phase_script_gen(self, input_data: Dict, logline_data: Dict) -> Dict:\n idea = input_data.get(\"idea\", \"\")\n sid = input_data.get(\"session_id\", \"\")\n style = input_data.get(\"style\", \"anime\")\n llm_model = input_data.get(\"llm_model\", \"qwen3.5-plus\")\n web_search = input_data.get(\"web_search\", False)\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in idea)\n\n # 用 Logline 六要素构建丰富的故事概要\n if is_zh:\n draft = (\n f\"故事核心 Logline:{logline_data.get('logline', '')}\\n\"\n f\"主角:{logline_data.get('who', '')}\\n\"\n f\"目标:{logline_data.get('goal', '')}\\n\"\n f\"核心障碍:{logline_data.get('conflict', '')}\\n\"\n f\"反转:{logline_data.get('twist', '')}\\n\"\n f\"主题:{logline_data.get('theme', '')}\\n\\n\"\n f\"原始灵感:{idea}\"\n )\n else:\n draft = (\n f\"Story Logline: {logline_data.get('logline', '')}\\n\"\n f\"Protagonist: {logline_data.get('who', '')}\\n\"\n f\"Goal: {logline_data.get('goal', '')}\\n\"\n f\"Core Conflict: {logline_data.get('conflict', '')}\\n\"\n f\"Twist: {logline_data.get('twist', '')}\\n\"\n f\"Theme: {logline_data.get('theme', '')}\\n\\n\"\n f\"Original idea: {idea}\"\n )\n\n def run():\n from config import settings as app_settings\n from tool.llm_client import LLM\n\n os.makedirs(app_settings.TEMP_DIR, exist_ok=True)\n os.makedirs(os.path.join(app_settings.RESULT_DIR, 'script'), exist_ok=True)\n\n llm = LLM()\n\n # 逐幕生成节拍表 + 分场大纲 + 结构化提取\n json_data = self._generate_script_incremental(\n llm, draft, style, llm_model, sid, is_zh, web_search=web_search, pct_start=20\n )\n\n # 补充元数据\n json_data.setdefault(\"project_id\", f\"proj_{sid}\")\n json_data.setdefault(\"version\", 1)\n json_data[\"created_at\"] = datetime.now(timezone.utc).isoformat()\n json_data[\"metadata\"] = {\n \"generation_model\": llm_model,\n \"generation_prompt\": idea,\n }\n json_data[\"session_id\"] = sid\n json_data[\"logline_data\"] = logline_data\n\n self._save_result(json_data, sid, is_zh)\n # 保存进度状态\n self._save_progress(sid, \"script_generation\", {\n \"title\": json_data.get(\"title\")\n })\n self._report_progress(\"剧本生成\", \"完成\", 100)\n return json_data\n\n loop = asyncio.get_running_loop()\n script_json = await loop.run_in_executor(None, run)\n\n # 保存进度状态\n self._save_progress(sid, \"script_generation\", {\n \"title\": script_json.get(\"title\")\n })\n\n return {\n \"payload\": script_json,\n \"requires_intervention\": False,\n \"stage_completed\": True, # 明确标记阶段完成\n \"openclaw_hint\": \"剧本生成完成。\",\n }\n\n # ─── Phase 2-Micro: 微电影模式 - 根据选定 Logline 生成紧凑单幕剧本 ───\n\n async def _phase_micro_script_gen(self, input_data: Dict, logline_data: Dict) -> Dict:\n idea = input_data.get(\"idea\", \"\")\n sid = input_data.get(\"session_id\", \"\")\n style = input_data.get(\"style\", \"anime\")\n llm_model = input_data.get(\"llm_model\", \"qwen3.5-plus\")\n web_search = input_data.get(\"web_search\", False)\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in idea)\n\n # 用 Logline 六要素构建故事概要\n if is_zh:\n draft = (\n f\"故事核心 Logline:{logline_data.get('logline', '')}\\n\"\n f\"主角:{logline_data.get('who', '')}\\n\"\n f\"目标:{logline_data.get('goal', '')}\\n\"\n f\"核心障碍:{logline_data.get('conflict', '')}\\n\"\n f\"反转:{logline_data.get('twist', '')}\\n\"\n f\"主题:{logline_data.get('theme', '')}\\n\\n\"\n f\"原始灵感:{idea}\"\n )\n else:\n draft = (\n f\"Story Logline: {logline_data.get('logline', '')}\\n\"\n f\"Protagonist: {logline_data.get('who', '')}\\n\"\n f\"Goal: {logline_data.get('goal', '')}\\n\"\n f\"Core Conflict: {logline_data.get('conflict', '')}\\n\"\n f\"Twist: {logline_data.get('twist', '')}\\n\"\n f\"Theme: {logline_data.get('theme', '')}\\n\\n\"\n f\"Original idea: {idea}\"\n )\n\n def run():\n from config import settings as app_settings\n from tool.llm_client import LLM\n\n os.makedirs(app_settings.TEMP_DIR, exist_ok=True)\n os.makedirs(os.path.join(app_settings.RESULT_DIR, 'script'), exist_ok=True)\n\n llm = LLM()\n\n # 微电影模式: 单幕生成\n json_data = self._generate_micro_script_incremental(\n llm, draft, style, llm_model, sid, is_zh, web_search=web_search, pct_start=20\n )\n\n # 补充元数据\n json_data.setdefault(\"project_id\", f\"proj_{sid}\")\n json_data.setdefault(\"version\", 1)\n json_data[\"created_at\"] = datetime.now(timezone.utc).isoformat()\n json_data[\"metadata\"] = {\n \"generation_model\": llm_model,\n \"generation_prompt\": idea,\n \"mode\": \"micro\",\n }\n json_data[\"session_id\"] = sid\n json_data[\"logline_data\"] = logline_data\n\n self._save_result(json_data, sid, is_zh)\n # 保存进度状态\n self._save_progress(sid, \"script_generation\", {\n \"title\": json_data.get(\"title\")\n })\n self._report_progress(\"剧本生成\", \"完成\", 100)\n return json_data\n\n loop = asyncio.get_running_loop()\n script_json = await loop.run_in_executor(None, run)\n\n # 保存进度状态\n self._save_progress(sid, \"script_generation\", {\n \"title\": script_json.get(\"title\")\n })\n\n return {\n \"payload\": script_json,\n \"requires_intervention\": False,\n \"stage_completed\": True, # 明确标记阶段完成\n \"openclaw_hint\": \"剧本生成完成。\",\n }\n\n # ─── 代理模式(自动生成 Logline 并选择第一个) ───\n\n async def _phase_direct(self, input_data: Dict) -> Dict:\n \"\"\"代理模式:生成 3 个 Logline → 自动选择第一个 → 生成完整剧本\"\"\"\n idea = input_data.get(\"idea\", \"\")\n sid = input_data.get(\"session_id\", \"\")\n style = input_data.get(\"style\", \"anime\")\n llm_model = input_data.get(\"llm_model\", \"qwen3.5-plus\")\n web_search = input_data.get(\"web_search\", False)\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in idea)\n\n def run():\n from config import settings as app_settings\n from tool.llm_client import LLM\n\n os.makedirs(app_settings.TEMP_DIR, exist_ok=True)\n os.makedirs(os.path.join(app_settings.RESULT_DIR, 'script'), exist_ok=True)\n\n llm = LLM()\n\n # Step 1: 生成 Logline 方案\n self._report_progress(\"剧本生成\", \"正在生成 Logline 方案...\", 5)\n prompt = (LOGLINE_GENERATE_PROMPT_ZH if is_zh else LOGLINE_GENERATE_PROMPT_EN).format(idea=idea)\n\n logline_data = None\n for attempt in range(3):\n self._check_cancel()\n raw = self._cancellable_query(llm, prompt, model=llm_model, task_id=sid, web_search=web_search)\n logger.info(f\"[ScriptWriter][auto] Logline gen attempt {attempt + 1}, raw ({len(raw)} chars): {raw[:500]}\")\n options = self._extract_json_array_from_text(raw)\n if options and len(options) > 0:\n required = {\"logline\", \"who\", \"goal\", \"conflict\", \"twist\", \"theme\"}\n valid = [o for o in options if isinstance(o, dict) and required.issubset(o.keys())]\n if valid:\n logline_data = valid[0] # 自动选择第一个\n logger.info(f\"[ScriptWriter][auto] Auto-selected logline: {logline_data.get('logline', '')[:80]}\")\n break\n\n logger.warning(f\"[ScriptWriter][auto] Logline gen attempt {attempt + 1}: parse failed\")\n if is_zh:\n prompt = (\n \"上次输出格式不正确,请严格按要求重新输出。\"\n \"必须输出纯JSON数组,不要用```包裹,不要有任何多余文字。\\n\\n\"\n + LOGLINE_GENERATE_PROMPT_ZH.format(idea=idea)\n )\n else:\n prompt = (\n \"Previous output format was incorrect. Please try again. \"\n \"Output ONLY a raw JSON array, no code blocks, no extra text.\\n\\n\"\n + LOGLINE_GENERATE_PROMPT_EN.format(idea=idea)\n )\n\n if logline_data is None:\n raise Exception(\"Logline 生成失败:多次尝试均无法解析输出\")\n\n self._report_progress(\"剧本生成\", \"已自动选择 Logline,开始创作情节...\", 15)\n\n # Step 2: 用 Logline 六要素构建故事概要\n if is_zh:\n draft = (\n f\"故事核心 Logline:{logline_data.get('logline', '')}\\n\"\n f\"主角:{logline_data.get('who', '')}\\n\"\n f\"目标:{logline_data.get('goal', '')}\\n\"\n f\"核心障碍:{logline_data.get('conflict', '')}\\n\"\n f\"反转:{logline_data.get('twist', '')}\\n\"\n f\"主题:{logline_data.get('theme', '')}\\n\\n\"\n f\"原始灵感:{idea}\"\n )\n else:\n draft = (\n f\"Story Logline: {logline_data.get('logline', '')}\\n\"\n f\"Protagonist: {logline_data.get('who', '')}\\n\"\n f\"Goal: {logline_data.get('goal', '')}\\n\"\n f\"Core Conflict: {logline_data.get('conflict', '')}\\n\"\n f\"Twist: {logline_data.get('twist', '')}\\n\"\n f\"Theme: {logline_data.get('theme', '')}\\n\\n\"\n f\"Original idea: {idea}\"\n )\n\n # Step 3: 逐幕生成节拍表 + 分场大纲 + 结构化提取\n json_data = self._generate_script_incremental(\n llm, draft, style, llm_model, sid, is_zh, web_search=web_search, pct_start=20\n )\n\n # 补充元数据\n json_data.setdefault(\"project_id\", f\"proj_{sid}\")\n json_data.setdefault(\"version\", 1)\n json_data[\"created_at\"] = datetime.now(timezone.utc).isoformat()\n json_data[\"metadata\"] = {\n \"generation_model\": llm_model,\n \"generation_prompt\": idea,\n }\n json_data[\"session_id\"] = sid\n json_data[\"logline_data\"] = logline_data\n\n self._save_result(json_data, sid, is_zh)\n # 保存进度状态\n self._save_progress(sid, \"script_generation\", {\n \"title\": json_data.get(\"title\")\n })\n self._report_progress(\"剧本生成\", \"完成\", 100)\n return json_data\n\n loop = asyncio.get_running_loop()\n script_json = await loop.run_in_executor(None, run)\n\n # 保存进度状态\n self._save_progress(sid, \"script_generation\", {\n \"title\": script_json.get(\"title\")\n })\n\n return {\n \"payload\": script_json,\n \"requires_intervention\": False,\n \"stage_completed\": True, # 明确标记阶段完成\n \"openclaw_hint\": \"剧本生成完成。\",\n }\n\n # ─── 公共: 逐幕生成节拍表 + 分场大纲 + 结构化提取 ───\n\n def _generate_script_incremental(self, llm, draft: str, style: str,\n llm_model: str, sid: str, is_zh: bool,\n web_search: bool = False,\n pct_start: int = 20) -> dict:\n \"\"\"生成 Save the Cat! 节拍表 → 逐幕生成分场大纲 + 结构化提取,返回合并的 JSON\"\"\"\n\n # Step 1: 生成四幕节拍表\n self._report_progress(\"剧本生成\", \"生成节拍表...\", pct_start)\n prompt = (BEAT_SHEET_PROMPT_ZH if is_zh else BEAT_SHEET_PROMPT_EN).format(draft=draft, style=style)\n beat_sheet = self._cancellable_query(llm, prompt, model=llm_model, task_id=sid, web_search=web_search)\n logger.info(f\"[ScriptWriter] Beat sheet generated ({len(beat_sheet)} chars)\")\n\n # 发送节拍表到前端\n self._report_progress(\"剧本生成\", \"节拍表生成完成\", pct_start + 5,\n data={\"beat_sheet\": beat_sheet})\n\n # Step 2: 提取顶层元数据 (title, logline, genre, synopsis, mood)\n self._report_progress(\"剧本生成\", \"提取故事元信息...\", pct_start + 7)\n meta_prompt = (META_EXTRACT_PROMPT_ZH if is_zh else META_EXTRACT_PROMPT_EN).format(beat_sheet=beat_sheet)\n meta_raw = self._cancellable_query(llm, meta_prompt, model=llm_model, task_id=sid, web_search=web_search)\n meta_data = self._extract_json_from_text(meta_raw) or {}\n logger.info(f\"[ScriptWriter] Meta extracted: {list(meta_data.keys())}\")\n\n # Step 3: 逐幕生成分场大纲 + 结构化提取\n act_names = ACT_NAMES_ZH if is_zh else ACT_NAMES_EN\n all_characters = [] # 累积角色\n all_settings = [] # 累积场景\n all_scenes = [] # 累积分场\n seen_char_names = set()\n seen_setting_names = set()\n scene_start = 1\n\n for act_num in range(1, 5):\n self._check_cancel()\n act_name = act_names[act_num]\n\n # --- 生成分场大纲 ---\n pct_outline = pct_start + 8 + (act_num - 1) * 15 # 28, 43, 58, 73\n progress_msg = (f\"生成第{act_num}幕分场大纲({act_name})...\"\n if is_zh else f\"Generating Act {act_num} outline ({act_name})...\")\n self._report_progress(\"剧本生成\", progress_msg, pct_outline)\n\n outline_prompt = (STEP_OUTLINE_PROMPT_ZH if is_zh else STEP_OUTLINE_PROMPT_EN).format(\n beat_sheet=beat_sheet,\n act_number=act_num,\n act_name=act_name,\n scene_start=scene_start,\n style=style,\n )\n outline = self._cancellable_query(llm, outline_prompt, model=llm_model, task_id=sid, web_search=web_search)\n logger.info(f\"[ScriptWriter] Act {act_num} outline generated ({len(outline)} chars)\")\n\n # --- 结构化提取本幕 JSON ---\n pct_extract = pct_outline + 8\n progress_msg = (f\"提取第{act_num}幕结构化数据...\"\n if is_zh else f\"Extracting Act {act_num} structured data...\")\n self._report_progress(\"剧本生成\", progress_msg, pct_extract)\n\n act_json = self._extract_act_json(\n llm, outline, act_num, scene_start, style, llm_model, sid, is_zh, web_search\n )\n\n # 累积角色(去重)\n for c in act_json.get(\"characters\", []):\n if c.get(\"name\") and c[\"name\"] not in seen_char_names:\n if not c.get(\"character_id\"):\n c[\"character_id\"] = self._gen_id(\"char\")\n all_characters.append(c)\n seen_char_names.add(c[\"name\"])\n\n # 累积场景(去重)\n for s in act_json.get(\"settings\", []):\n if s.get(\"name\") and s[\"name\"] not in seen_setting_names:\n if not s.get(\"setting_id\"):\n s[\"setting_id\"] = self._gen_id(\"set\")\n all_settings.append(s)\n seen_setting_names.add(s[\"name\"])\n\n # 累积分场\n act_scenes = act_json.get(\"scenes\", [])\n all_scenes.extend(act_scenes)\n\n # 计算下一幕场次起始\n if act_scenes:\n scene_start = max(s.get(\"scene_number\", scene_start) for s in act_scenes) + 1\n\n # 发送本幕结果到前端\n act_label = f\"第{act_num}幕 - {act_name}\" if is_zh else f\"Act {act_num} - {act_name}\"\n self._report_progress(\"剧本生成\",\n f\"{act_label} 完成\" if is_zh else f\"{act_label} done\",\n pct_extract + 2,\n data={\"act_complete\": {\n \"act\": act_num,\n \"act_name\": act_name,\n \"characters\": act_json.get(\"characters\", []),\n \"settings\": act_json.get(\"settings\", []),\n \"scenes\": act_scenes,\n }})\n\n # Step 4: 合并最终 JSON\n # 重新编号 scene_number\n for i, sc in enumerate(all_scenes):\n sc[\"scene_number\"] = i + 1\n\n json_data = {\n \"title\": meta_data.get(\"title\", \"\"),\n \"logline\": meta_data.get(\"logline\", \"\"),\n \"genre\": meta_data.get(\"genre\", []),\n \"target_duration\": 1800,\n \"synopsis\": meta_data.get(\"synopsis\", \"\"),\n \"characters\": all_characters,\n \"settings\": all_settings,\n \"scenes\": all_scenes,\n \"overall_style\": style,\n \"mood\": meta_data.get(\"mood\", \"\"),\n }\n\n # Step 5: 合并相似场景和角色\n self._report_progress(\"剧本生成\",\n \"审查并合并相似场景...\" if is_zh else \"Consolidating similar settings...\",\n 96)\n json_data = self._consolidate_script(llm, json_data, llm_model, sid, is_zh, web_search=web_search)\n\n return json_data\n\n # ─── 微电影模式: 单幕紧凑生成 ───\n\n def _generate_micro_script_incremental(self, llm, draft: str, style: str,\n llm_model: str, sid: str, is_zh: bool,\n web_search: bool = False,\n pct_start: int = 20) -> dict:\n \"\"\"微电影模式:生成紧凑单幕剧情概要 → 分场大纲 → 结构化提取,返回合并的 JSON\"\"\"\n\n # Step 1: 生成单幕剧情概要\n self._report_progress(\"剧本生成\", \"生成微电影剧情概要...\", pct_start)\n prompt = (MICRO_BEAT_SHEET_PROMPT_ZH if is_zh else MICRO_BEAT_SHEET_PROMPT_EN).format(\n draft=draft, style=style\n )\n beat_sheet = self._cancellable_query(llm, prompt, model=llm_model, task_id=sid, web_search=web_search)\n logger.info(f\"[ScriptWriter][micro] Beat sheet generated ({len(beat_sheet)} chars)\")\n\n # 发送概要到前端\n self._report_progress(\"剧本生成\", \"剧情概要生成完成\", pct_start + 10,\n data={\"beat_sheet\": beat_sheet})\n\n # Step 2: 提取顶层元数据\n self._report_progress(\"剧本生成\", \"提取故事元信息...\", pct_start + 15)\n meta_prompt = (MICRO_META_EXTRACT_PROMPT_ZH if is_zh else MICRO_META_EXTRACT_PROMPT_EN).format(\n beat_sheet=beat_sheet\n )\n meta_raw = self._cancellable_query(llm, meta_prompt, model=llm_model, task_id=sid, web_search=web_search)\n meta_data = self._extract_json_from_text(meta_raw) or {}\n logger.info(f\"[ScriptWriter][micro] Meta extracted: {list(meta_data.keys())}\")\n\n # Step 3: 生成分场大纲\n self._check_cancel()\n self._report_progress(\"剧本生成\",\n \"生成微电影分场大纲...\" if is_zh else \"Generating micro-film step outline...\",\n pct_start + 25)\n\n outline_prompt = (MICRO_STEP_OUTLINE_PROMPT_ZH if is_zh else MICRO_STEP_OUTLINE_PROMPT_EN).format(\n beat_sheet=beat_sheet, style=style\n )\n outline = self._cancellable_query(llm, outline_prompt, model=llm_model, task_id=sid, web_search=web_search)\n logger.info(f\"[ScriptWriter][micro] Outline generated ({len(outline)} chars)\")\n\n # Step 4: 结构化 JSON 提取\n self._report_progress(\"剧本生成\",\n \"提取结构化数据...\" if is_zh else \"Extracting structured data...\",\n pct_start + 45)\n\n act_json = self._extract_act_json(\n llm, outline, act_number=1, scene_start=1,\n style=style, llm_model=llm_model, sid=sid, is_zh=is_zh,\n web_search=web_search\n )\n\n characters = act_json.get(\"characters\", [])\n settings = act_json.get(\"settings\", [])\n scenes = act_json.get(\"scenes\", [])\n\n # 补全 ID\n for c in characters:\n if not c.get(\"character_id\"):\n c[\"character_id\"] = self._gen_id(\"char\")\n for s in settings:\n if not s.get(\"setting_id\"):\n s[\"setting_id\"] = self._gen_id(\"set\")\n\n # 重新编号并去掉 act 字段(微电影不分幕)\n for i, sc in enumerate(scenes):\n sc[\"scene_number\"] = i + 1\n sc.pop(\"act\", None)\n\n # 发送到前端\n act_label = \"微电影\" if is_zh else \"Micro-film\"\n self._report_progress(\"剧本生成\",\n f\"{act_label} 分场完成\" if is_zh else f\"{act_label} scenes done\",\n pct_start + 60,\n data={\"act_complete\": {\n \"act\": 1,\n \"act_name\": act_label,\n \"characters\": characters,\n \"settings\": settings,\n \"scenes\": scenes,\n }})\n\n # Step 5: 合并最终 JSON\n json_data = {\n \"title\": meta_data.get(\"title\", \"\"),\n \"logline\": meta_data.get(\"logline\", \"\"),\n \"genre\": meta_data.get(\"genre\", []),\n \"target_duration\": 180, # 微电影默认 3 分钟\n \"synopsis\": meta_data.get(\"synopsis\", \"\"),\n \"characters\": characters,\n \"settings\": settings,\n \"scenes\": scenes,\n \"overall_style\": style,\n \"mood\": meta_data.get(\"mood\", \"\"),\n }\n\n # Step 6: 合并相似场景和角色\n self._report_progress(\"剧本生成\",\n \"审查并合并相似场景...\" if is_zh else \"Consolidating similar settings...\",\n pct_start + 70)\n json_data = self._consolidate_script(llm, json_data, llm_model, sid, is_zh, web_search=web_search)\n\n return json_data\n\n # ─── 公共: 单幕结构化提取 (带校验重试) ───\n\n def _extract_act_json(self, llm, outline: str, act_number: int,\n scene_start: int, style: str,\n llm_model: str, sid: str, is_zh: bool,\n web_search: bool = False) -> dict:\n \"\"\"从单幕分场大纲提取结构化JSON,带校验重试\"\"\"\n schema = (ACT_EXTRACT_SCHEMA_ZH if is_zh else ACT_EXTRACT_SCHEMA_EN).format(\n act_number=act_number, scene_start=scene_start\n )\n intro = (ACT_EXTRACT_INTRO_ZH if is_zh else ACT_EXTRACT_INTRO_EN).format(\n act_number=act_number, outline=outline\n )\n rules = (ACT_EXTRACT_RULES_ZH if is_zh else ACT_EXTRACT_RULES_EN).format(\n act_number=act_number, scene_start=scene_start\n )\n extract_prompt = intro + schema + rules\n\n last_error = \"\"\n\n for attempt in range(3):\n self._check_cancel()\n raw = self._cancellable_query(llm, extract_prompt, model=llm_model, task_id=sid, web_search=web_search)\n parsed = self._extract_json_from_text(raw)\n\n if parsed is None:\n last_error = \"LLM输出无法解析为JSON\"\n logger.warning(f\"[ScriptWriter] Act {act_number} extract attempt {attempt + 1}: JSON parse failed\")\n extract_prompt = (\n (\"上次输出不是合法JSON,请重新输出。\" if is_zh\n else \"Previous output was not valid JSON. Please try again. \")\n + intro + schema + rules\n )\n continue\n\n # 基本校验: 必须有 scenes 列表\n scenes = parsed.get(\"scenes\")\n if not isinstance(scenes, list) or len(scenes) == 0:\n last_error = \"scenes 必须是非空列表\"\n logger.warning(f\"[ScriptWriter] Act {act_number} extract attempt {attempt + 1}: no scenes\")\n extract_prompt = (\n (f\"上次输出的JSON缺少scenes。请确保scenes是非空列表。\" if is_zh\n else \"Previous JSON had no scenes. Ensure scenes is a non-empty array. \")\n + intro + schema + rules\n )\n continue\n\n # 校验每个 scene 的必要字段\n valid = True\n for i, sc in enumerate(scenes):\n for key in [\"scene_number\", \"act\", \"location\", \"characters\", \"plot\"]:\n if key not in sc:\n last_error = f\"scenes[{i}] 缺少字段: {key}\"\n valid = False\n break\n if not valid:\n break\n\n if not valid:\n logger.warning(f\"[ScriptWriter] Act {act_number} extract attempt {attempt + 1}: {last_error}\")\n extract_prompt = (\n (f\"上次输出的JSON存在问题: {last_error}。请修正后重新输出。\" if is_zh\n else f\"Previous JSON issue: {last_error}. Please fix and retry. \")\n + intro + schema + rules\n )\n continue\n\n return parsed\n\n raise Exception(f\"第{act_number}幕JSON提取失败 (已重试3次): {last_error}\")\n\n # ─── 公共: 场景 & 角色合并 ───\n\n def _consolidate_script(self, llm, json_data: dict,\n llm_model: str, sid: str, is_zh: bool,\n web_search: bool = False) -> dict:\n \"\"\"审查并合并重复/相似的场景和角色\"\"\"\n characters = json_data.get(\"characters\", [])\n settings = json_data.get(\"settings\", [])\n scenes = json_data.get(\"scenes\", [])\n\n if len(settings) \u003c= 1 and len(characters) \u003c= 1:\n return json_data\n\n # 构建简洁的列表供 LLM 审查\n settings_summary = [{\"name\": s[\"name\"], \"description\": s.get(\"description\", \"\")}\n for s in settings]\n chars_summary = [{\"name\": c[\"name\"], \"description\": c.get(\"description\", \"\")}\n for c in characters]\n\n prompt = (CONSOLIDATE_PROMPT_ZH if is_zh else CONSOLIDATE_PROMPT_EN).format(\n settings_json=json.dumps(settings_summary, ensure_ascii=False, indent=2),\n characters_json=json.dumps(chars_summary, ensure_ascii=False, indent=2),\n )\n\n self._check_cancel()\n raw = self._cancellable_query(llm, prompt, model=llm_model, task_id=sid, web_search=web_search)\n result = self._extract_json_from_text(raw)\n\n if not result or not isinstance(result, dict):\n logger.warning(\"[ScriptWriter] Consolidation: failed to parse LLM response, skipping\")\n return json_data\n\n setting_merges = result.get(\"setting_merges\", {})\n char_merges = result.get(\"character_merges\", {})\n\n if not setting_merges and not char_merges:\n logger.info(\"[ScriptWriter] Consolidation: nothing to merge\")\n return json_data\n\n # --- 合并场景 ---\n if setting_merges:\n logger.info(f\"[ScriptWriter] Merging settings: {setting_merges}\")\n # 移除被合并的 settings\n merged_names = set(setting_merges.keys())\n json_data[\"settings\"] = [s for s in settings if s[\"name\"] not in merged_names]\n\n # 更新 scenes 中的 location 引用\n for sc in scenes:\n loc = sc.get(\"location\", \"\")\n if loc in setting_merges:\n sc[\"location\"] = setting_merges[loc]\n\n # --- 合并角色 ---\n if char_merges:\n logger.info(f\"[ScriptWriter] Merging characters: {char_merges}\")\n merged_char_names = set(char_merges.keys())\n json_data[\"characters\"] = [c for c in characters if c[\"name\"] not in merged_char_names]\n\n # 更新 scenes 中的 characters 引用\n for sc in scenes:\n new_chars = []\n seen = set()\n for cn in sc.get(\"characters\", []):\n target = char_merges.get(cn, cn)\n if target not in seen:\n new_chars.append(target)\n seen.add(target)\n sc[\"characters\"] = new_chars\n\n logger.info(f\"[ScriptWriter] After consolidation: \"\n f\"{len(json_data['settings'])} settings, \"\n f\"{len(json_data['characters'])} characters\")\n\n # 验证 scenes 的 location 是否都在 settings 中\n settings_names = {s[\"name\"] for s in json_data.get(\"settings\", [])}\n for sc in json_data.get(\"scenes\", []):\n loc = sc.get(\"location\", \"\")\n if loc and loc not in settings_names:\n logger.error(f\"[ScriptWriter] ERROR: Scene location '{loc}' not found in settings! available: {settings_names}\")\n\n return json_data\n","content_type":"text/x-python; charset=utf-8","language":"python","size":69336,"content_sha256":"ec400233a0ff15c8286bb47a74414e7b494404e07672e841d1266a4312fb5589"},{"filename":"aigc-claw/backend/core/agents/storyboard_agent.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\n阶段3: 分镜智能体\n基于剧本JSON,逐场景拆分为带时长标签的分镜(shots),按幕分组输出。\n\"\"\"\n\nimport os\nimport re\nimport json\nimport asyncio\nimport logging\nfrom typing import Any, Optional, Dict, List\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\nfrom .base_agent import AgentInterface\nfrom prompts.loader import load_prompt\n\nlogger = logging.getLogger(__name__)\n\n# ─── 幕名 ───\nACT_NAMES = {1: \"激励事件\", 2: \"进入新世界\", 3: \"灵魂黑夜\", 4: \"高潮决战\"}\n\n\ndef _get_shot_prompt(lang: str = 'zh') -> str:\n \"\"\"获取分镜提示词\"\"\"\n return load_prompt('storyboard', 'shot', lang)\n\n\ndef _get_continue_prompt(lang: str = 'zh') -> str:\n \"\"\"获取续写分镜提示词\"\"\"\n from prompts.loader import load_prompt_with_fallback\n return load_prompt_with_fallback('storyboard', 'continue', lang, 'zh')\n\n\ndef _get_prompt(name: str) -> str:\n \"\"\"Helper to get prompts\"\"\"\n if name == 'SHOT_PROMPT_ZH':\n return load_prompt('storyboard', 'shot', 'zh')\n elif name == 'SHOT_PROMPT_EN':\n return load_prompt('storyboard', 'shot', 'en')\n raise AttributeError(f\"module has no attribute {name!r}\")\n\n\nclass StoryboardAgent(AgentInterface):\n \"\"\"分镜智能体:逐场景拆分为带时长标签的分镜\"\"\"\n\n def __init__(self):\n super().__init__(name=\"Storyboard\")\n\n # ─── 辅助方法 ───\n\n @staticmethod\n def _read_script_json(sid: str) -> dict:\n \"\"\"从结果文件读取 script_json\"\"\"\n from config import settings\n result_file = os.path.join(settings.RESULT_DIR, 'script', f'script_{sid}.json')\n if not os.path.exists(result_file):\n return {}\n with open(result_file, 'r', encoding='utf-8') as f:\n data = json.load(f)\n return data.get(str(sid), {}).get('script_json', {})\n\n @staticmethod\n def _extract_json_array(text: str) -> Optional[List[dict]]:\n \"\"\"从LLM输出中提取JSON数组\"\"\"\n text = text.strip()\n text = re.sub(r'^```(?:json)?\\s*', '', text)\n text = re.sub(r'\\s*```

AIGC-Director Agent Skill 本地运行 :这是一个 本地部署 的视频生成项目, 前后端都运行在本机 : - 后端: - 前端: - 所有 API 调用都请求本地服务器,不要请求其他地址! - 确保在调用任何 API 之前,后端和前端服务都已经启动并运行正常! 核心理念 :Agent 应该像"持续陪伴的智能视频制作助理",每完成一个用户可感知的重要任务,都应立即给用户一条简报,并等待用户确认。 核心原则 :每个阶段的产物都必须展示给用户,必须停下来等待用户确认后才能继续下一阶段。 防止遗忘 :在整个流程中,Agent 可能会忘记之前的用户输入或之前阶段的产物内容。 每当进入一个新的阶段时,Agent 都必须重新加载这篇SKILL文档,确保不会忘记任何细节 。 --- 项目结构 产物存放目录 : - - 剧本产物 - - 图片产物(角色、场景、参考图) - - 视频产物 --- 阶段与停点(共9个) | 停点 | 阶段 | phase 值 | 描述 | 操作 | |------|------|----------|------|------| | 1 | 项目配置 | - | 确认配置选项 | 展示配置 → 用户确认 | | 2 | 剧本生成 | suggest expand | 建议扩写 | 等待用户确认 | | 3 | 剧本生成 | logline sele…

, '', text)\n text = text.strip()\n try:\n result = json.loads(text)\n if isinstance(result, list):\n return result\n except json.JSONDecodeError:\n pass\n # 尝试找到 [ ... ] 部分\n m = re.search(r'\\[.*\\]', text, re.DOTALL)\n if m:\n try:\n result = json.loads(m.group())\n if isinstance(result, list):\n return result\n except json.JSONDecodeError:\n pass\n return None\n\n @staticmethod\n def _validate_shots(shots: List[dict]) -> List[dict]:\n \"\"\"校验并清洗分镜数据\"\"\"\n valid = []\n for s in shots:\n if not isinstance(s, dict):\n continue\n dur = s.get(\"duration\", 10)\n if dur not in (5, 10, 15):\n dur = 10 # 默认10秒\n valid.append({\n \"shot_number\": s.get(\"shot_number\", len(valid) + 1),\n \"duration\": dur,\n \"characters\": s.get(\"characters\", []),\n \"location\": s.get(\"location\", \"\"),\n \"plot\": s.get(\"plot\", \"\"),\n \"visual_prompt\": s.get(\"visual_prompt\", \"\"),\n })\n return valid\n\n async def _continue_story(self, input_data: Dict, continue_info: Dict) -> Dict:\n \"\"\"智能续写:根据已有剧情续写新场景\"\"\"\n from config import settings\n from tool.llm_client import LLM\n\n sid = input_data[\"session_id\"]\n style = input_data.get(\"style\", \"anime\")\n llm_model = input_data.get(\"llm_model\", \"qwen3.5-plus\")\n\n result_file = os.path.join(settings.RESULT_DIR, 'script', f'script_{sid}.json')\n\n # 读取已有分镜\n with open(result_file, 'r', encoding='utf-8') as f:\n results = json.load(f)\n existing_shots = results.get(str(sid), {}).get('storyboard', {}).get('shots', [])\n if not existing_shots:\n raise Exception(\"没有已有分镜,无法续写\")\n\n # 读取剧本信息\n script_json = self._read_script_json(sid)\n if not script_json:\n raise Exception(\"未找到剧本数据\")\n\n title = script_json.get(\"title\", \"\")\n characters = script_json.get(\"characters\", [])\n settings_list = script_json.get(\"settings\", [])\n\n # 取所有分镜作为上下文\n last_shots = existing_shots\n\n # 构建上下文描述\n char_names = [c.get(\"name\", \"\") for c in characters]\n # 只使用现有分镜中实际使用的场景位置\n used_locations = set(s.get(\"location\", \"\") for s in existing_shots if s.get(\"location\"))\n setting_names = [s.get(\"name\", \"\") for s in settings_list if s.get(\"name\", \"\") in used_locations]\n\n # 获取续写提示词\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in title)\n prompt_template = _get_continue_prompt('zh' if is_zh else 'en')\n\n # 格式化已有分镜信息\n shots_context = \"\"\n for shot in last_shots:\n shots_context += f\"- 场景{shot.get('scene_number')} 分镜{shot.get('shot_number')}: {shot.get('plot', '')}\\n\"\n\n prompt = prompt_template.format(\n title=title,\n style=style,\n characters=\", \".join(char_names),\n settings=\", \".join(setting_names),\n existing_shots=shots_context,\n )\n\n self._report_progress(\"分镜\", \"智能续写中...\", 10)\n\n def run():\n llm = LLM()\n raw = self._cancellable_query(llm, prompt, model=llm_model, task_id=sid)\n new_shots = self._extract_json_array(raw)\n if not new_shots:\n raise Exception(\"续写失败,无法解析LLM输出\")\n new_shots = self._validate_shots(new_shots)\n return new_shots\n\n loop = asyncio.get_running_loop()\n new_shots = await loop.run_in_executor(None, run)\n\n # 为新分镜添加全局标识\n last_scene = last_shots[-1] if last_shots else None\n next_scene_num = (last_scene.get(\"scene_number\", 0) + 1) if last_scene else 1\n next_act = last_scene.get(\"act\", 1) if last_scene else 1\n\n for i, shot in enumerate(new_shots):\n shot[\"shot_id\"] = f\"shot_{next_scene_num:03d}_{i + 1:02d}\"\n shot[\"scene_number\"] = next_scene_num\n shot[\"act\"] = next_act\n shot[\"is_new\"] = True # 标记为新添加的分镜\n\n # 追加到已有分镜\n all_shots = existing_shots + new_shots\n\n # 保存(带有 is_new 标记,供前端识别)\n results.setdefault(str(sid), {})['storyboard'] = {\n 'shots': all_shots,\n 'user_modified': True,\n 'new_shot_ids': [s[\"shot_id\"] for s in new_shots], # 记录新分镜ID\n }\n with open(result_file, 'w', encoding='utf-8') as f:\n json.dump(results, f, indent=4, ensure_ascii=False)\n\n self._report_progress(\"分镜\", \"续写完成\", 100)\n\n return {\n \"payload\": {\n \"session_id\": sid,\n \"shots\": all_shots,\n \"new_shots\": new_shots,\n \"new_shot_ids\": [s[\"shot_id\"] for s in new_shots],\n \"continued\": True,\n },\n \"requires_intervention\": True,\n }\n\n # ─── 核心流程 ───\n\n async def process(self, input_data: Any, intervention: Optional[Dict] = None) -> Dict:\n from config import settings\n from tool.llm_client import LLM\n\n sid = input_data[\"session_id\"]\n style = input_data.get(\"style\", \"anime\")\n llm_model = input_data.get(\"llm_model\", \"qwen3.5-plus\")\n\n result_file = os.path.join(settings.RESULT_DIR, 'script', f'script_{sid}.json')\n\n # ── 用户修改了分镜 ──\n if intervention and \"modified_storyboard\" in intervention:\n modified_shots = intervention[\"modified_storyboard\"]\n if isinstance(modified_shots, str):\n modified_shots = json.loads(modified_shots)\n\n with open(result_file, 'r', encoding='utf-8') as f:\n results = json.load(f)\n\n # 同步更新 script_json 中的 scenes\n # 获取修改后的 shots 中包含的场景编号\n new_scene_numbers = set(s.get(\"scene_number\") for s in modified_shots if s.get(\"scene_number\"))\n\n # 更新 script_json.scenes\n script_json = results.get(str(sid), {}).get('script_json', {})\n if script_json and 'scenes' in script_json:\n original_scenes = script_json.get('scenes', [])\n # 保留新分镜中包含的场景,按原始顺序\n filtered_scenes = [s for s in original_scenes if s.get('scene_number') in new_scene_numbers]\n # 重新编号(确保 scene_number 连续)\n for i, scene in enumerate(filtered_scenes, 1):\n scene['scene_number'] = i\n script_json['scenes'] = filtered_scenes\n\n # 清除 new_shot_ids 标记,同时清除 is_new 标志\n for shot in modified_shots:\n if 'is_new' in shot:\n shot['is_new'] = False\n storyboard_data = {\n 'shots': modified_shots,\n 'user_modified': True,\n 'new_shot_ids': [], # 清除新分镜标记\n }\n\n results.setdefault(str(sid), {})['storyboard'] = storyboard_data\n if script_json:\n results.setdefault(str(sid), {})['script_json'] = script_json\n with open(result_file, 'w', encoding='utf-8') as f:\n json.dump(results, f, indent=4, ensure_ascii=False)\n\n # 返回完整的 payload 包含 user_modified 和 new_shot_ids\n return {\n \"payload\": {\n \"session_id\": sid,\n \"shots\": modified_shots,\n \"user_modified\": True,\n \"new_shot_ids\": [],\n },\n \"stage_completed\": True,\n }\n\n # ── 用户点击\"智能续写\" ──\n if intervention and \"continue_story\" in intervention:\n return await self._continue_story(input_data, intervention[\"continue_story\"])\n\n # ── 正常流程 ──\n script_json = self._read_script_json(sid)\n if not script_json:\n raise Exception(\"未找到剧本数据(script_json),请先完成阶段1\")\n\n scenes = script_json.get(\"scenes\", [])\n characters = {c[\"name\"]: c for c in script_json.get(\"characters\", [])}\n settings_map = {s[\"name\"]: s for s in script_json.get(\"settings\", [])}\n\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in script_json.get(\"title\", \"\"))\n prompt_template = _get_prompt('SHOT_PROMPT_ZH') if is_zh else _get_prompt('SHOT_PROMPT_EN')\n\n self._report_progress(\"分镜\", \"读取剧本数据...\", 5)\n\n # 获取并发数\n enable_concurrency = input_data.get(\"enable_concurrency\", True)\n from config_model import get_max_concurrency\n # LLM 调用的并发数\n concurrency = get_max_concurrency(\"llm\", enable_concurrency)\n logger.info(f\"[StoryboardAgent] enable_concurrency={enable_concurrency}, concurrency={concurrency}\")\n\n # 用于按场景顺序存储结果的字典\n shots_results: Dict[int, List[dict]] = {}\n done_count = 0\n total = len(scenes)\n\n def generate_one_scene(scene_index: int, scene: dict) -> tuple:\n \"\"\"生成单个场景的分镜\"\"\"\n llm = LLM()\n sn = scene.get(\"scene_number\", scene_index + 1)\n act = scene.get(\"act\", 1)\n\n location = scene.get(\"location\", \"\")\n scene_chars = scene.get(\"characters\", [])\n plot = scene.get(\"plot\", \"\")\n\n # 角色外貌描述\n char_descs = []\n for cn in scene_chars:\n c = characters.get(cn)\n if c:\n char_descs.append(f\"{cn}: {c.get('description', '')}\")\n char_desc_text = \"\\n\".join(char_descs) if char_descs else \"无\"\n\n # 场景环境描述\n setting_info = settings_map.get(location, {})\n setting_desc = setting_info.get(\"description\", location)\n\n prompt = prompt_template.format(\n style=style,\n scene_number=sn,\n location=location,\n characters=\", \".join(scene_chars),\n plot=plot,\n char_descriptions=char_desc_text,\n setting_description=setting_desc,\n )\n\n # 最多重试3次\n scene_shots = None\n for attempt in range(3):\n if hasattr(self, 'cancellation_check') and self.cancellation_check:\n if self.cancellation_check():\n break\n raw = self._cancellable_query(llm, prompt, model=llm_model, task_id=sid)\n parsed = self._extract_json_array(raw)\n if parsed:\n scene_shots = self._validate_shots(parsed)\n if scene_shots:\n break\n logger.warning(f\"Scene {sn} shot parse attempt {attempt+1} failed\")\n\n if not scene_shots:\n # 降级: 整个场景作为一个分镜\n scene_shots = [{\n \"shot_number\": 1,\n \"duration\": 15,\n \"characters\": scene_chars,\n \"location\": location,\n \"plot\": plot,\n \"visual_prompt\": plot,\n }]\n\n # 为每个分镜添加全局标识\n for shot in scene_shots:\n shot[\"shot_id\"] = f\"shot_{sn:03d}_{shot['shot_number']:02d}\"\n shot[\"scene_number\"] = sn\n shot[\"act\"] = act\n\n return scene_index, scene_shots, sn, act\n\n def run():\n nonlocal done_count\n # 检测是否有多幕结构(微电影模式只有一幕或无 act 字段)\n act_values = {s.get(\"act\") for s in scenes}\n multi_act = len(act_values - {None}) > 1\n\n with ThreadPoolExecutor(max_workers=concurrency) as executor:\n futs = {}\n for i, scene in enumerate(scenes):\n fut = executor.submit(generate_one_scene, i, scene)\n futs[fut] = i\n\n # 按提交顺序收集结果(不是按完成顺序)\n for scene_index in range(len(scenes)):\n # 找到对应的 future\n found_future = None\n for fut in futs:\n if futs[fut] == scene_index:\n found_future = fut\n break\n\n if found_future:\n try:\n idx, scene_shots, sn, act = found_future.result()\n shots_results[idx] = scene_shots\n\n # 报告进度\n done_count += 1\n pct = 5 + int(90 * done_count / max(total, 1))\n if multi_act:\n act_label = ACT_NAMES.get(act, f\"第{act}幕\")\n self._report_progress(\"分镜\", f\"第{act}幕「{act_label}」 场景{sn}/{total}完成\", pct)\n else:\n self._report_progress(\"分镜\", f\"场景{sn}完成 ({len(scene_shots)}个分镜)\", pct)\n\n # 发送逐场完成事件\n self._report_progress(\n \"分镜\",\n f\"场景{sn}完成 ({len(scene_shots)}个分镜)\",\n pct,\n data={\"scene_shots_complete\": {\n \"scene_number\": sn,\n \"act\": act,\n \"shots\": scene_shots,\n }},\n )\n except Exception as e:\n logger.error(f\"Scene {scene_index} generation error: {e}\")\n shots_results[scene_index] = []\n\n # 按场景顺序拼接所有分镜\n all_shots = []\n for i in range(len(scenes)):\n if i in shots_results:\n all_shots.extend(shots_results[i])\n\n self._report_progress(\"分镜\", \"保存结果...\", 96)\n\n # 写入结果文件\n with open(result_file, 'r', encoding='utf-8') as f:\n results = json.load(f)\n results.setdefault(str(sid), {})['storyboard'] = {\n 'shots': all_shots,\n }\n with open(result_file, 'w', encoding='utf-8') as f:\n json.dump(results, f, indent=4, ensure_ascii=False)\n\n self._report_progress(\"分镜\", \"完成\", 100)\n return {\n \"payload\": {\n \"session_id\": sid,\n \"shots\": all_shots,\n },\n \"stage_completed\": True,\n }\n\n loop = asyncio.get_running_loop()\n all_shots = await loop.run_in_executor(None, run)\n\n return {\n \"payload\": {\n \"session_id\": sid,\n \"shots\": all_shots,\n },\n \"stage_completed\": True,\n }\n","content_type":"text/x-python; charset=utf-8","language":"python","size":17796,"content_sha256":"ae37e5155e7399028422b8b4d7e608f2aa64220b16dbaa339d3e1b216aa9821e"},{"filename":"aigc-claw/backend/core/agents/video_agent.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\n阶段5: 视频生成智能体\n分镜参考图 → 各分镜视频片段\n- 视频提示词使用阶段3的原始分镜剧情描述(plot),而非视觉描述或首帧图像提示词\n- 参考图使用用户在阶段4选择的版本,而非第一版\n- 支持逐项实时预览、重新生成、多版本管理\n\"\"\"\n\nimport os\nimport re\nimport glob\nimport json\nimport asyncio\nimport logging\nfrom typing import Any, Optional, Dict, List\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\nfrom .base_agent import AgentInterface\n\nlogger = logging.getLogger(__name__)\n\n\nclass VideoDirectorAgent(AgentInterface):\n \"\"\"视频生成:分镜参考图(阶段4用户选择) + 分镜剧情描述(阶段3 plot) → 视频片段\"\"\"\n\n def __init__(self):\n super().__init__(name=\"VideoDirector\")\n\n # ─── 版本管理 ───\n\n @staticmethod\n def _video_base(sid: str) -> str:\n return os.path.join('code/result/video', str(sid))\n\n def _list_versions(self, sid: str, shot_id: str) -> List[str]:\n \"\"\"列出某个分镜视频的所有历史版本\"\"\"\n video_dir = self._video_base(sid)\n pattern = os.path.join(video_dir, f\"{shot_id}*.mp4\")\n files = [f for f in sorted(glob.glob(pattern), key=os.path.getmtime)\n if not f.endswith('_final.mp4')]\n return files\n\n def _next_version_path(self, sid: str, shot_id: str) -> str:\n \"\"\"获取下一个版本路径\"\"\"\n video_dir = self._video_base(sid)\n os.makedirs(video_dir, exist_ok=True)\n\n existing = self._list_versions(sid, shot_id)\n if not existing:\n return os.path.join(video_dir, f\"{shot_id}.mp4\")\n\n max_v = 1\n for fp in existing:\n bn = os.path.splitext(os.path.basename(fp))[0]\n m = re.search(r'_v(\\d+)

AIGC-Director Agent Skill 本地运行 :这是一个 本地部署 的视频生成项目, 前后端都运行在本机 : - 后端: - 前端: - 所有 API 调用都请求本地服务器,不要请求其他地址! - 确保在调用任何 API 之前,后端和前端服务都已经启动并运行正常! 核心理念 :Agent 应该像"持续陪伴的智能视频制作助理",每完成一个用户可感知的重要任务,都应立即给用户一条简报,并等待用户确认。 核心原则 :每个阶段的产物都必须展示给用户,必须停下来等待用户确认后才能继续下一阶段。 防止遗忘 :在整个流程中,Agent 可能会忘记之前的用户输入或之前阶段的产物内容。 每当进入一个新的阶段时,Agent 都必须重新加载这篇SKILL文档,确保不会忘记任何细节 。 --- 项目结构 产物存放目录 : - - 剧本产物 - - 图片产物(角色、场景、参考图) - - 视频产物 --- 阶段与停点(共9个) | 停点 | 阶段 | phase 值 | 描述 | 操作 | |------|------|----------|------|------| | 1 | 项目配置 | - | 确认配置选项 | 展示配置 → 用户确认 | | 2 | 剧本生成 | suggest expand | 建议扩写 | 等待用户确认 | | 3 | 剧本生成 | logline sele…

, bn)\n if m:\n max_v = max(max_v, int(m.group(1)))\n\n return os.path.join(video_dir, f\"{shot_id}_v{max_v + 1}.mp4\")\n\n # ─── 视频生成 ───\n\n def _generate_one(self, sid: str, shot_id: str, prompt: str,\n img_path: str, video_model: str,\n duration: int = 5, sound: str = \"\",\n shot_type: str = \"multi\") -> tuple:\n \"\"\"生成单个分镜视频,返回 (shot_id, path_or_None)\"\"\"\n # 取消时直接跳过,不抛异常,以保留已生成的部分结果\n if self.cancellation_check and self.cancellation_check():\n logger.info(f\"VideoDirectorAgent: {shot_id} 跳过(用户取消)\")\n return shot_id, None\n\n if not os.path.exists(img_path):\n logger.warning(f\"Image missing for {shot_id}: {img_path}\")\n return shot_id, None\n\n save_path = self._next_version_path(sid, shot_id)\n try:\n from tool.video_client import VideoClient\n client = VideoClient()\n client.generate_video(\n prompt=prompt,\n image_path=img_path,\n save_path=save_path,\n model=video_model,\n duration=duration,\n sound=sound,\n shot_type=shot_type,\n )\n return shot_id, save_path\n except Exception as e:\n logger.error(f\"Video gen failed for {shot_id}: {e}\")\n if os.path.exists(save_path):\n try:\n os.remove(save_path)\n except Exception:\n pass\n return shot_id, None\n\n # ─── 排序 ───\n\n @staticmethod\n def _sort_shot_keys(keys: list) -> list:\n \"\"\"对 shot_id 排序: shot_001_01, shot_001_02, shot_002_01, ...\"\"\"\n def sort_key(k):\n nums = re.findall(r'(\\d+)', k)\n return tuple(int(n) for n in nums) if nums else (0,)\n return sorted(keys, key=sort_key)\n\n # ─── 预览 / Payload 构建 ───\n\n @staticmethod\n def _shot_display_name(shot_id: str) -> str:\n \"\"\"shot_001_02 → 场景1-镜头2\"\"\"\n nums = re.findall(r'(\\d+)', shot_id)\n if len(nums) >= 2:\n return f\"场景{int(nums[0])}-镜头{int(nums[1])}\"\n elif len(nums) == 1:\n return f\"场景{int(nums[0])}\"\n return shot_id\n\n def _build_preview(self, sid: str, shot_keys: list, s2i: dict) -> list:\n \"\"\"构建视频片段预览列表\"\"\"\n preview = []\n for idx, shot_id in enumerate(shot_keys, 1):\n versions = self._list_versions(sid, shot_id)\n entry = s2i.get(shot_id, {})\n desc = entry.get('plot', '') or entry.get('video_prompt', '') or entry.get('prompt', '')\n preview.append({\n \"id\": shot_id,\n \"name\": self._shot_display_name(shot_id),\n \"index\": idx,\n \"description\": desc,\n \"duration\": entry.get('duration', 5),\n \"selected\": versions[-1] if versions else \"\",\n \"versions\": versions,\n \"status\": \"done\" if versions else \"pending\",\n })\n return preview\n\n def _build_payload(self, sid: str, shot_keys: list, s2i: dict, clip_descriptions: dict = None) -> dict:\n \"\"\"构建最终 payload\n\n Args:\n clip_descriptions: 用户修改的提示词,优先使用\n \"\"\"\n clips = []\n for idx, shot_id in enumerate(shot_keys, 1):\n versions = self._list_versions(sid, shot_id)\n entry = s2i.get(shot_id, {})\n # 优先使用用户修改的描述,否则用 scene2image 中的原始描述\n if clip_descriptions and shot_id in clip_descriptions:\n desc = clip_descriptions[shot_id]\n else:\n desc = entry.get('plot', '') or entry.get('video_prompt', '') or entry.get('prompt', '')\n clips.append({\n \"id\": shot_id,\n \"name\": self._shot_display_name(shot_id),\n \"index\": idx,\n \"description\": desc,\n \"duration\": entry.get('duration', 5),\n \"selected\": versions[-1] if versions else \"\",\n \"versions\": versions,\n \"status\": \"done\" if versions else \"failed\",\n })\n return {\n \"payload\": {\n \"session_id\": sid,\n \"clips\": clips,\n },\n \"stage_completed\": True,\n }\n\n # ─── 视频提示词前缀/后缀配置 ───\n\n # 尝试从模板文件加载前缀/后缀,失败则使用默认值\n _VIDEO_PROMPT_PREFIX = None\n _VIDEO_PROMPT_SUFFIX = None\n\n @classmethod\n def _load_video_enhance_prompt(cls) -> tuple:\n \"\"\"加载视频提示词优化模板\"\"\"\n if cls._VIDEO_PROMPT_PREFIX is not None:\n return cls._VIDEO_PROMPT_PREFIX, cls._VIDEO_PROMPT_SUFFIX\n\n try:\n from prompts.loader import PROMPTS_DIR\n import os\n enhance_file = os.path.join(PROMPTS_DIR, 'video', 'enhance.txt')\n if os.path.exists(enhance_file):\n with open(enhance_file, 'r', encoding='utf-8') as f:\n content = f.read()\n\n prefix = \"\"\n suffix = \"\"\n current_section = None\n\n for line in content.split('\\n'):\n line = line.strip()\n if not line or line.startswith('#'):\n continue\n if line == '[prefix]':\n current_section = 'prefix'\n elif line == '[suffix]':\n current_section = 'suffix'\n elif line.startswith('[') and line.endswith(']'):\n # 遇到新的 section(如 [style_keywords]),停止解析\n current_section = None\n elif current_section == 'prefix':\n prefix += line + \" \"\n elif current_section == 'suffix':\n suffix += \" \" + line\n\n cls._VIDEO_PROMPT_PREFIX = prefix.strip()\n cls._VIDEO_PROMPT_SUFFIX = suffix.strip()\n logger.info(f\"Loaded video enhance prompt from file: prefix={len(cls._VIDEO_PROMPT_PREFIX)} chars, suffix={len(cls._VIDEO_PROMPT_SUFFIX)} chars\")\n return cls._VIDEO_PROMPT_PREFIX, cls._VIDEO_PROMPT_SUFFIX\n except Exception as e:\n logger.warning(f\"Failed to load video enhance prompt: {e}\")\n\n # 默认值\n cls._VIDEO_PROMPT_PREFIX = (\n \"high quality, detailed, cinematic footage, smooth motion, natural movement, \"\n )\n cls._VIDEO_PROMPT_SUFFIX = (\n \", realistic, no blur, no distortion, professional lighting, film grain\"\n )\n return cls._VIDEO_PROMPT_PREFIX, cls._VIDEO_PROMPT_SUFFIX\n\n # 视频API字符限制(可灵2500,万象也类似)\n MAX_PROMPT_LENGTH = 2500\n\n # ─── 风格关键词映射 ───\n # 根据项目风格添加对应的视觉描述词\n\n STYLE_VIDEO_KEYWORDS = {\n # 动漫/动画风格\n \"anime\": \"anime style, animated, cel-shaded, vibrant colors, manga aesthetic, \",\n \"cartoon\": \"cartoon style, animated, colorful, fun, children's book illustration, \",\n # 写实风格\n \"realistic\": \"photorealistic, realistic, natural lighting, detailed textures, cinema photography, \",\n \"photorealistic\": \"photorealistic, realistic, natural lighting, detailed textures, cinema photography, \",\n # 3D 迪士尼风格\n \"3d-disney\": \"3D animation, Disney style, pixar, CGI, smooth textures, computer generated, \",\n \"3d\": \"3D animation, CGI, computer generated, smooth textures, digital cinema, \",\n # 油画风格\n \"oil-painting\": \"oil painting style, impasto, classical art, painterly, rich brushstrokes, \",\n \"watercolor\": \"watercolor style, delicate, soft colors, artistic, flowing, \",\n # 漫画风格\n \"comic-book\": \"comic book style, vibrant, bold outlines, pop art, graphic novel, \",\n # 赛博朋克\n \"cyberpunk\": \"cyberpunk, neon lights, futuristic, dark atmosphere, sci-fi, \",\n # 中国风\n \"chinese-ink\": \"Chinese ink painting style, traditional, minimalist, brush strokes, oriental art, \",\n \"ink\": \"Chinese ink painting style, traditional, minimalist, brush strokes, oriental art, \",\n }\n\n def _get_style_keywords(self, sid: str) -> str:\n \"\"\"获取项目风格对应的视频关键词\"\"\"\n try:\n from config import settings\n result_file = os.path.join(settings.RESULT_DIR, 'script', f'script_{sid}.json')\n if os.path.exists(result_file):\n with open(result_file, 'r', encoding='utf-8') as f:\n data = json.load(f)\n session_data = data.get(str(sid), {})\n style = session_data.get('overall_style', '').lower().strip()\n if style and style in self.STYLE_VIDEO_KEYWORDS:\n logger.info(f\"Using style keywords for video: {style}\")\n return self.STYLE_VIDEO_KEYWORDS[style]\n except Exception as e:\n logger.debug(f\"Could not get style for video prompt: {e}\")\n return \"\"\n\n # ─── 辅助:获取分镜的视频提示词和参考图路径 ───\n\n def _enhance_video_prompt(self, base_prompt: str, sid: str = None) -> str:\n \"\"\"\n 增强视频提示词:添加前缀后缀优化生成效果\n 不截断,发送完整提示词给API\n \"\"\"\n if not base_prompt:\n return base_prompt\n\n prefix, suffix = self._load_video_enhance_prompt()\n base_lower = base_prompt.lower().strip()\n\n # 获取风格关键词\n style_keywords = \"\"\n if sid:\n style_keywords = self._get_style_keywords(sid)\n\n # 获取对话语言要求\n dialog_language = \"\"\n if sid:\n from config import settings\n import os\n script_file = os.path.join(settings.RESULT_DIR, 'script', f'script_{sid}.json')\n if os.path.exists(script_file):\n import json\n with open(script_file, 'r', encoding='utf-8') as f:\n data = json.load(f)\n script_data = data.get(str(sid), {})\n # title 可能在 script_json 子对象中\n title = script_data.get('title') or script_data.get('script_json', {}).get('title', '')\n # 判断语言:检查标题是否包含中文字符\n is_zh = any('\\u4e00' \u003c= c \u003c= '\\u9fff' for c in title)\n if is_zh:\n dialog_language = \"注意:人物对话必须使用中文,不要使用英文。\"\n else:\n dialog_language = \"Note: Character dialogues must be in English.\"\n\n # 构建增强后的提示词\n # 检查是否已包含前缀关键词\n has_prefix = any(kw in base_lower for kw in [\"high quality\", \"cinematic\", \"smooth motion\", \"anime\", \"photorealistic\"])\n # 检查是否已包含后缀关键词\n has_suffix = any(kw in base_lower for kw in [\"film grain\", \"realistic\", \"professional lighting\", \"masterpiece\"])\n\n enhanced = base_prompt.strip()\n\n # 添加对话语言要求\n if dialog_language:\n enhanced = enhanced + \" \" + dialog_language\n\n # 添加风格关键词(前缀之前)\n if style_keywords:\n enhanced = style_keywords + enhanced\n\n if prefix and not has_prefix:\n enhanced = prefix + enhanced\n\n if suffix and not has_suffix:\n enhanced = enhanced + suffix\n\n logger.info(f\"Video prompt enhanced: {len(base_prompt)} -> {len(enhanced)} chars\")\n # 不截断,发送完整提示词\n return enhanced\n\n def _get_shot_prompt(self, entry: dict, enhance: bool = True, sid: str = None, shot_id: str = None, clip_descriptions: dict = None) -> str:\n \"\"\"获取视频提示词:优先用用户修改的提示词,其次用 plot(剧情描述),兼容旧数据回退到 video_prompt\"\"\"\n # 优先使用用户修改的提示词\n if not shot_id:\n shot_id = entry.get('shot_id') or entry.get('id', '')\n if clip_descriptions and shot_id and clip_descriptions.get(shot_id):\n base_prompt = clip_descriptions[shot_id]\n else:\n base_prompt = entry.get('plot', '') or entry.get('video_prompt', '') or entry.get('prompt', '')\n if enhance:\n return self._enhance_video_prompt(base_prompt, sid)\n return base_prompt\n\n def _get_shot_image(self, sid: str, shot_id: str, entry: dict,\n selected_images: dict) -> str:\n \"\"\"获取参考图路径:优先用前端传入的用户选择,再用 scene2image 的 local_path,\n 最后回退到扫描磁盘最新版本\"\"\"\n # 1. 前端传入的用户选择(stage 4 确认时携带)\n if selected_images.get(shot_id):\n path = selected_images[shot_id]\n if os.path.exists(path):\n return path\n logger.warning(f\"selected_images path missing for {shot_id}: {path}\")\n\n # 2. scene2image 中的 local_path\n local_path = entry.get('local_path', '')\n if local_path and os.path.exists(local_path):\n return local_path\n\n # 3. 回退:扫描磁盘,使用最新版本\n from core.agents.reference_agent import ReferenceGeneratorAgent\n versions = ReferenceGeneratorAgent._list_versions_static(sid, shot_id)\n if versions:\n logger.info(f\"Fallback to latest version for {shot_id}: {versions[-1]}\")\n return versions[-1]\n\n # 4. 最终回退:默认路径\n return os.path.join('code/result/image', str(sid), 'Scenes', f\"{shot_id}.jpg\")\n\n # ─── 核心流程 ───\n\n async def process(self, input_data: Any, intervention: Optional[Dict] = None) -> Dict:\n from config import settings\n\n sid = input_data[\"session_id\"]\n video_model = input_data.get(\"video_model\", \"\") or settings.VIDEO_MODEL\n # 根据 enable_concurrency 决定并发数\n enable_concurrency = input_data.get(\"enable_concurrency\", True)\n from config_model import get_max_concurrency\n concurrency = get_max_concurrency(video_model, enable_concurrency)\n selected_images = input_data.get(\"selected_images\", {})\n # 优先使用 input_data 中已有的 clips(包含用户修改的 description)\n existing_clips = input_data.get(\"clips\", [])\n clip_descriptions = {c['id']: c['description'] for c in existing_clips if c.get('id') and c.get('description')}\n video_sound = input_data.get(\"video_sound\", \"on\")\n video_shot_type = input_data.get(\"video_shot_type\", \"multi\")\n sound_param = \"\" if video_sound == \"off\" else video_sound\n\n logger.info(f\"VideoDirectorAgent: sid={sid}, video_model={video_model}, sound={video_sound}, shot_type={video_shot_type}\")\n\n result_file = os.path.join(settings.RESULT_DIR, 'script', f'script_{sid}.json')\n\n # 读取数据\n with open(result_file, 'r', encoding='utf-8') as f:\n results = json.load(f)\n story_data = results[str(sid)]\n\n s2i = story_data.get('scene2image', {})\n\n # 从 storyboard 获取时长数据(用户修改已同步到 clips 中)\n storyboard = story_data.get('storyboard', {})\n storyboard_shots_list = storyboard.get('shots', [])\n shot_duration_map = {s.get('shot_id'): s.get('duration', 10) for s in storyboard_shots_list if s.get('shot_id')}\n\n # 同步 scene2image 中的时长\n for shot_id in s2i:\n if shot_id in shot_duration_map:\n s2i[shot_id]['duration'] = shot_duration_map[shot_id]\n\n logger.info(f\"VideoDirectorAgent: duration sync from storyboard: {list(shot_duration_map.keys())}\")\n\n # 兼容 shot_xxx_xx 和旧 Scene_x 格式\n shot_keys = self._sort_shot_keys(list(s2i.keys()))\n total = len(shot_keys)\n\n logger.info(f\"VideoDirectorAgent: total shots in scene2image = {total}, shot_keys = {shot_keys}\")\n\n if not shot_keys:\n raise Exception(\"未找到场景图数据(scene2image),请先完成阶段4\")\n\n # ═══ 介入:重新生成指定片段 ═══\n if intervention:\n regen_clips = intervention.get(\"regenerate_clips\", [])\n\n if regen_clips:\n self._report_progress(\"视频生成\", \"重新生成中...\", 10)\n\n def regen_run():\n regen_total = len(regen_clips)\n done = 0\n with ThreadPoolExecutor(max_workers=concurrency) as executor:\n futs = {}\n for shot_id in regen_clips:\n entry = s2i.get(shot_id, {})\n prompt = self._get_shot_prompt(entry, sid=sid, shot_id=shot_id, clip_descriptions=clip_descriptions)\n img_path = self._get_shot_image(sid, shot_id, entry, selected_images)\n shot_duration = entry.get('duration', 5)\n fut = executor.submit(\n self._generate_one, sid, shot_id, prompt,\n img_path, video_model, shot_duration,\n sound_param, video_shot_type\n )\n futs[fut] = shot_id\n for fut in as_completed(futs):\n shot_id_done = futs[fut]\n try:\n _, result_path = fut.result()\n except Exception as e:\n logger.error(f\"Regen future error for {shot_id_done}: {e}\")\n result_path = None\n done += 1\n pct = 10 + int(85 * done / max(regen_total, 1))\n if result_path:\n versions = self._list_versions(sid, shot_id_done)\n self._report_progress(\"视频生成\", f\"完成: {shot_id_done}\", pct, data={\n \"asset_complete\": {\n \"type\": \"clips\", \"id\": shot_id_done,\n \"status\": \"done\",\n \"selected\": result_path,\n \"versions\": versions,\n }\n })\n else:\n self._report_progress(\"视频生成\", f\"失败: {shot_id_done}\", pct, data={\n \"asset_complete\": {\n \"type\": \"clips\", \"id\": shot_id_done,\n \"status\": \"failed\",\n \"selected\": \"\", \"versions\": [],\n }\n })\n # 检查取消:停止等待剩余任务\n if self.cancellation_check and self.cancellation_check():\n logger.info(\"VideoDirectorAgent: 用户取消重新生成,停止等待剩余任务\")\n for f in futs:\n if not f.done():\n f.cancel()\n break\n\n loop = asyncio.get_running_loop()\n await loop.run_in_executor(None, regen_run)\n\n self._report_progress(\"视频生成\", \"完成\", 100)\n return self._build_payload(sid, shot_keys, s2i, clip_descriptions)\n\n # ═══ 正常流程:全量生成 ═══\n self._report_progress(\"视频生成\", \"加载场景数据...\", 5)\n\n # 发送预览列表\n preview = self._build_preview(sid, shot_keys, s2i)\n self._report_progress(\"视频生成\", \"加载视频列表\", 8, data={\"assets_preview\": {\"clips\": preview}})\n\n def run():\n # 筛选需要生成的(跳过已有的)\n tasks = []\n for shot_id in shot_keys:\n existing = self._list_versions(sid, shot_id)\n if existing:\n continue\n entry = s2i.get(shot_id, {})\n prompt = self._get_shot_prompt(entry, sid=sid, shot_id=shot_id, clip_descriptions=clip_descriptions)\n img_path = self._get_shot_image(sid, shot_id, entry, selected_images)\n shot_duration = entry.get('duration', 5)\n tasks.append((shot_id, prompt, img_path, shot_duration))\n\n if not tasks:\n self._report_progress(\"视频生成\", \"所有视频已存在\", 95)\n return\n\n gen_total = len(tasks)\n done = 0\n\n cancelled = False\n with ThreadPoolExecutor(max_workers=concurrency) as executor:\n futs = {}\n for shot_id, prompt, img_path, shot_duration in tasks:\n fut = executor.submit(\n self._generate_one, sid, shot_id, prompt,\n img_path, video_model, shot_duration,\n sound_param, video_shot_type,\n )\n futs[fut] = shot_id\n for fut in as_completed(futs):\n shot_id_done = futs[fut]\n try:\n _, result_path = fut.result()\n except Exception as e:\n logger.error(f\"Video future error for {shot_id_done}: {e}\")\n result_path = None\n done += 1\n pct = 10 + int(85 * done / max(gen_total, 1))\n if result_path:\n versions = self._list_versions(sid, shot_id_done)\n self._report_progress(\"视频生成\", f\"完成: {shot_id_done}\", pct, data={\n \"asset_complete\": {\n \"type\": \"clips\", \"id\": shot_id_done,\n \"status\": \"done\",\n \"selected\": result_path,\n \"versions\": versions,\n }\n })\n else:\n self._report_progress(\"视频生成\", f\"失败: {shot_id_done}\", pct, data={\n \"asset_complete\": {\n \"type\": \"clips\", \"id\": shot_id_done,\n \"status\": \"failed\",\n \"selected\": \"\", \"versions\": [],\n }\n })\n # 检查取消:停止等待剩余任务\n if self.cancellation_check and self.cancellation_check():\n logger.info(\"VideoDirectorAgent: 用户取消,停止等待剩余任务\")\n for f in futs:\n if not f.done():\n f.cancel()\n cancelled = True\n break\n\n if cancelled:\n self._report_progress(\"视频生成\", \"已取消(保留已完成片段)\", 96)\n else:\n self._report_progress(\"视频生成\", \"保存结果...\", 96)\n\n # 写回结果文件\n with open(result_file, 'r', encoding='utf-8') as f:\n res = json.load(f)\n i2v_data = {}\n for shot_id in shot_keys:\n versions = self._list_versions(sid, shot_id)\n entry = s2i.get(shot_id, {})\n if versions:\n i2v_data[shot_id] = {\n \"video_prompt\": self._get_shot_prompt(entry, sid=sid, shot_id=shot_id, clip_descriptions=clip_descriptions),\n \"input_path\": self._get_shot_image(sid, shot_id, entry, selected_images),\n \"output_path\": versions[-1],\n \"duration\": entry.get('duration', 10),\n \"status\": \"done\",\n }\n res[str(sid)]['image2video'] = i2v_data\n with open(result_file, 'w', encoding='utf-8') as f:\n json.dump(res, f, indent=4, ensure_ascii=False)\n\n loop = asyncio.get_running_loop()\n try:\n await loop.run_in_executor(None, run)\n except Exception as e:\n # 即使异常也保留已完成的部分结果\n if \"cancel\" in str(e).lower():\n logger.info(\"VideoDirectorAgent: 用户取消,返回已完成的部分结果\")\n self._report_progress(\"视频生成\", \"已取消(保留已完成片段)\", 100)\n return self._build_payload(sid, shot_keys, s2i, clip_descriptions)\n raise\n\n self._report_progress(\"视频生成\", \"完成\", 100)\n return self._build_payload(sid, shot_keys, s2i, clip_descriptions)","content_type":"text/x-python; charset=utf-8","language":"python","size":27370,"content_sha256":"846a8a83b71a76bf1f1d2024d6b8dc219c27df22a2b0c391989728d5cc08a026"},{"filename":"aigc-claw/backend/core/orchestrator.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\n核心编排器 / 工作流引擎\n管理六阶段状态机,协调各智能体执行,支持用户在任意阶段介入\n\"\"\"\n\nimport json\nimport logging\nimport os\nimport threading\nimport time\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any, Callable, Dict, List, Optional\n\nfrom core.agents import (\n ScriptWriterAgent,\n CharacterDesignerAgent,\n StoryboardAgent,\n ReferenceGeneratorAgent,\n VideoDirectorAgent,\n VideoEditorAgent,\n)\n\nlogger = logging.getLogger(__name__)\n\n\nclass WorkflowStage(str, Enum):\n \"\"\"工作流阶段\"\"\"\n INIT = \"init\"\n SCRIPT_GENERATION = \"script_generation\"\n CHARACTER_DESIGN = \"character_design\"\n STORYBOARD = \"storyboard\"\n REFERENCE_GENERATION = \"reference_generation\"\n VIDEO_GENERATION = \"video_generation\"\n POST_PRODUCTION = \"post_production\"\n COMPLETED = \"session_completed\"\n\n\nSTAGE_ORDER = [\n WorkflowStage.SCRIPT_GENERATION,\n WorkflowStage.CHARACTER_DESIGN,\n WorkflowStage.STORYBOARD,\n WorkflowStage.REFERENCE_GENERATION,\n WorkflowStage.VIDEO_GENERATION,\n WorkflowStage.POST_PRODUCTION,\n]\n\n\nclass WorkflowState:\n \"\"\"工作流状态\"\"\"\n\n # 状态说明:\n # - idle: 新建会话,还没有任何数据,也没有在运行\n # - running: 会话正在运行\n # - waiting_in_stage: 会话在某一阶段内等待用户介入(如选择角色、选择图片等)\n # - stage_completed: 会话完成了某一阶段,等待用户确定开始下一阶段\n # - session_completed: 会话全部完成\n # - stopped: 用户手动停止\n # - error: 执行中遇到错误\n\n def __init__(self, session_id: str):\n self.session_id = session_id\n self.current_stage: WorkflowStage = WorkflowStage.INIT\n self.status: str = \"idle\"\n self.stages_completed: List[str] = [] # 已完成的阶段列表\n self.artifacts: Dict[str, Any] = {}\n self.error: Optional[str] = None\n self.started_at: Optional[datetime] = None\n self.updated_at: datetime = datetime.now()\n self.meta: Dict[str, Any] = {}\n\n def to_dict(self) -> Dict:\n return {\n \"session_id\": self.session_id,\n \"current_stage\": self.current_stage.value,\n \"status\": self.status,\n \"error\": self.error,\n \"stages_completed\": self.stages_completed,\n \"artifacts\": self.artifacts,\n \"meta\": self.meta,\n \"updated_at\": self.updated_at,\n }\n\n\nclass WorkflowEngine:\n \"\"\"工作流引擎 - 管理六阶段状态机\"\"\"\n\n def __init__(self):\n self.agents = {\n WorkflowStage.SCRIPT_GENERATION: ScriptWriterAgent(),\n WorkflowStage.CHARACTER_DESIGN: CharacterDesignerAgent(),\n WorkflowStage.STORYBOARD: StoryboardAgent(),\n WorkflowStage.REFERENCE_GENERATION: ReferenceGeneratorAgent(),\n WorkflowStage.VIDEO_GENERATION: VideoDirectorAgent(),\n WorkflowStage.POST_PRODUCTION: VideoEditorAgent(),\n }\n self.sessions: Dict[str, WorkflowState] = {}\n self._stop_events: Dict[str, threading.Event] = {}\n self._session_dir = os.path.join(\n os.path.dirname(os.path.abspath(__file__)), '..', 'code', 'data', 'sessions'\n )\n os.makedirs(self._session_dir, exist_ok=True)\n self._load_sessions_from_disk()\n\n def get_or_create_state(self, session_id: str) -> WorkflowState:\n if session_id not in self.sessions:\n self.sessions[session_id] = WorkflowState(session_id=session_id)\n if session_id not in self._stop_events:\n self._stop_events[session_id] = threading.Event()\n return self.sessions[session_id]\n\n def get_state(self, session_id: str) -> Optional[WorkflowState]:\n # 先从内存中获取\n if session_id in self.sessions:\n return self.sessions[session_id]\n\n # 内存中没有,从磁盘加载\n path = os.path.join(self._session_dir, f\"{session_id}.json\")\n if os.path.exists(path):\n try:\n with open(path, 'r', encoding='utf-8') as f:\n data = json.load(f)\n\n # 从磁盘数据恢复 WorkflowState\n state = WorkflowState(session_id=session_id)\n state.status = data.get('status', 'idle')\n stage_str = data.get('current_stage')\n state.current_stage = WorkflowStage(stage_str) if stage_str else WorkflowStage.INIT\n state.stages_completed = data.get('stages_completed', [])\n state.artifacts = data.get('artifacts', {})\n state.meta = data.get('meta', {})\n state.error = data.get('error')\n state.updated_at = data.get('updated_at', 0)\n\n # 缓存到内存\n self.sessions[session_id] = state\n return state\n except json.JSONDecodeError as e:\n logger.warning(f\"Session file {session_id} is corrupted, ignoring: {e}\")\n except Exception as e:\n logger.warning(f\"Failed to load session {session_id} from disk: {e}\")\n\n return None\n\n def get_stop_event(self, session_id: str) -> threading.Event:\n if session_id not in self._stop_events:\n self._stop_events[session_id] = threading.Event()\n return self._stop_events[session_id]\n\n def stop_session(self, session_id: str):\n self.get_stop_event(session_id).set()\n state = self.get_state(session_id)\n if state and state.status == \"running\":\n state.status = \"stopped\"\n state.error = None # 清除错误,因为是主动停止\n state.updated_at = datetime.now()\n self.save_session_to_disk(session_id)\n logger.info(f\"Session {session_id} stop signal sent\")\n\n def reset_stop_event(self, session_id: str):\n if session_id in self._stop_events:\n self._stop_events[session_id].clear()\n\n def _get_next_stage(self, current: WorkflowStage) -> Optional[WorkflowStage]:\n try:\n idx = STAGE_ORDER.index(current)\n if idx + 1 \u003c len(STAGE_ORDER):\n return STAGE_ORDER[idx + 1]\n except ValueError:\n pass\n return None\n\n async def execute_stage(self,\n state: WorkflowState,\n stage: WorkflowStage,\n input_data: Any,\n cancellation_check: Optional[Callable] = None,\n progress_callback: Optional[Callable] = None,\n intervention: Optional[Dict] = None) -> Dict:\n import time\n\n agent = self.agents[stage]\n\n # 合并会话级停止信号与请求级取消检查\n session_stop = self.get_stop_event(state.session_id)\n def combined_cancel_check():\n return session_stop.is_set() or (cancellation_check and cancellation_check())\n\n agent.set_cancellation_check(combined_cancel_check)\n\n # 包装 progress_callback,定期保存状态到 sessions json\n last_save_time = {\"time\": 0}\n SAVE_INTERVAL = 10 # 每10秒保存一次\n\n def wrapped_progress_callback(phase: str, step: str, percent: float, data: dict = None):\n # 调用原始 callback\n if progress_callback:\n progress_callback(phase, step, percent, data)\n\n # 如果有 asset_complete 数据,说明有新生成的图片/视频,立即保存到磁盘\n if data and data.get(\"asset_complete\"):\n self.save_session_to_disk(state.session_id)\n last_save_time[\"time\"] = time.time()\n return\n\n # 如果有 progress 回调(纯文字阶段),也定期保存\n # 这样前端可以实时看到文字生成进度\n current_time = time.time()\n if current_time - last_save_time[\"time\"] >= SAVE_INTERVAL:\n last_save_time[\"time\"] = current_time\n self.save_session_to_disk(state.session_id)\n\n if progress_callback:\n agent.set_progress_callback(wrapped_progress_callback)\n\n state.current_stage = stage\n state.status = \"running\"\n state.updated_at = datetime.now()\n\n try:\n result = await agent.process(input_data, intervention=intervention)\n state.artifacts[stage.value] = result.get(\"payload\")\n\n # 第三阶段修改分镜时:同步更新第四、第五阶段的相关内容\n if stage.value == \"storyboard\" and intervention and \"modified_storyboard\" in intervention:\n modified_shots = intervention[\"modified_storyboard\"]\n if isinstance(modified_shots, list):\n shot_durations = {s.get('shot_id'): s.get('duration', 10)\n for s in modified_shots if s.get('shot_id')}\n shot_visual_prompts = {s.get('shot_id'): s.get('visual_prompt', '')\n for s in modified_shots if s.get('shot_id')}\n shot_plots = {s.get('shot_id'): s.get('plot', '')\n for s in modified_shots if s.get('shot_id')}\n\n # 1. 同步 duration 到第五阶段 clips\n video_art = state.artifacts.get('video_generation', {})\n if isinstance(video_art, dict) and 'clips' in video_art:\n for clip in video_art['clips']:\n shot_id = clip.get('id')\n if shot_id in shot_durations:\n clip['duration'] = shot_durations[shot_id]\n if shot_id in shot_plots:\n clip['description'] = shot_plots[shot_id]\n\n # 2. 同步 visual_prompt 到第四阶段 scenes (description)\n ref_art = state.artifacts.get('reference_generation', {})\n if isinstance(ref_art, dict) and 'scenes' in ref_art:\n for scene in ref_art['scenes']:\n shot_id = scene.get('id')\n if shot_id in shot_visual_prompts:\n scene['description'] = shot_visual_prompts[shot_id]\n\n # 3. 新增分镜:同步到第四、第五阶段\n existing_shot_ids = {clip.get('id') for clip in video_art.get('clips', [])} if isinstance(video_art, dict) else set()\n existing_scene_ids = {scene.get('id') for scene in ref_art.get('scenes', [])} if isinstance(ref_art, dict) else set()\n\n for shot in modified_shots:\n shot_id = shot.get('shot_id')\n if not shot_id:\n continue\n # 新增到第五阶段\n if shot_id not in existing_shot_ids:\n if isinstance(video_art, dict):\n video_art.setdefault('clips', []).append({\n 'id': shot_id,\n 'name': f\"镜头{shot_id.split('_')[-1]}\",\n 'description': shot.get('plot', ''),\n 'duration': shot.get('duration', 10),\n 'selected': '',\n 'versions': [],\n 'status': 'pending'\n })\n # 新增到第四阶段\n if shot_id not in existing_scene_ids:\n if isinstance(ref_art, dict):\n ref_art.setdefault('scenes', []).append({\n 'id': shot_id,\n 'name': f\"场景{shot_id.split('_')[-2]}\",\n 'description': shot.get('visual_prompt', ''),\n 'selected': '',\n 'versions': [],\n 'status': 'pending'\n })\n\n logger.info(f\"Synced stage 3 modifications: {len(shot_durations)} durations, {len(shot_visual_prompts)} prompts\")\n\n # 调试日志\n logger.info(f\"[execute_stage] stage={stage.value}, intervention={intervention is not None}, requires_intervention={result.get('requires_intervention')}, stage_completed={result.get('stage_completed')}\")\n\n # 状态转换逻辑:\n # - stage_completed=True: 阶段已完成,等待用户确认进入下一阶段\n # - requires_intervention=True: 阶段内需要用户介入(如选择图片等)\n # - 其他(running):阶段正在执行中\n if result.get(\"stage_completed\"):\n # 阶段真正完成,标记到已完成的列表\n if stage.value not in state.stages_completed:\n state.stages_completed.append(stage.value)\n # 如果是最后一个阶段,设置为 session_completed\n if stage == WorkflowStage.POST_PRODUCTION:\n state.status = \"session_completed\"\n else:\n state.status = \"stage_completed\"\n elif result.get(\"requires_intervention\"):\n # 阶段内需要用户介入,等待用户选择\n state.status = \"waiting_in_stage\"\n else:\n # 阶段正在执行中(中间步骤),保持 running\n state.status = \"running\"\n\n state.updated_at = datetime.now()\n # 立即保存状态到磁盘,确保前端能获取到最新状态\n self.save_session_to_disk(state.session_id)\n return result\n\n except Exception as e:\n state.status = \"error\"\n state.error = str(e)\n state.updated_at = datetime.now()\n raise\n\n async def handle_intervention(self,\n session_id: str,\n stage: str,\n modifications: Dict[str, Any]) -> Dict:\n state = self.sessions[session_id]\n stage_enum = WorkflowStage(stage)\n current_artifact = state.artifacts.get(stage, {})\n\n input_data = current_artifact if isinstance(current_artifact, dict) else {}\n input_data.update(modifications)\n\n return await self.execute_stage(state, stage_enum, input_data, intervention=modifications)\n\n async def continue_workflow(self, session_id: str) -> Dict:\n state = self.sessions[session_id]\n logger.info(f\"[continue_workflow] session={session_id}, current_stage={state.current_stage}, status={state.status}\")\n\n # 检查当前阶段是否已完成\n current_stage_str = state.current_stage.value if hasattr(state.current_stage, 'value') else str(state.current_stage)\n\n # 如果当前状态是 running,说明阶段还在执行中,不能继续\n if state.status == \"running\":\n return {\n \"status\": \"waiting\",\n \"openclaw\": f\"当前阶段({current_stage_str})还在执行中,请等待完成后再调用 /continue。\",\n \"message\": f\"当前阶段({current_stage_str})还在执行中,请等待完成后再调用 /continue。\",\n \"current_status\": state.status,\n }\n\n # 状态转换逻辑:\n # - waiting_in_stage 或 stage_completed: 用户确认后直接进入下一阶段\n # 注意:只有当阶段真正完成(waiting_in_stage 或 stage_completed)时才允许继续\n\n if state.status == \"waiting_in_stage\" or state.status == \"stage_completed\":\n # 用户确认后标记阶段完成\n if current_stage_str not in state.stages_completed:\n state.stages_completed.append(current_stage_str)\n\n # 直接进入下一阶段\n state.status = \"running\"\n next_stage = self._get_next_stage(state.current_stage)\n\n if not next_stage:\n state.status = \"session_completed\"\n self.save_session_to_disk(state.session_id)\n return {\"status\": \"session_completed\"}\n\n self.save_session_to_disk(state.session_id)\n return {\"status\": \"ready\", \"next_stage\": next_stage.value}\n\n # 其他状态(如 idle, stopped, error, session_completed)不允许继续\n return {\n \"status\": \"error\",\n \"openclaw\": f\"当前状态 {state.status} 不允许继续,请检查会话状态。\",\n \"message\": f\"当前状态不允许继续\",\n \"current_status\": state.status,\n }\n\n # ──────────── 会话持久化 ────────────\n\n def save_session_to_disk(self, session_id: str, meta: Dict = None):\n \"\"\"保存 / 更新会话到磁盘(原子写入)\"\"\"\n import tempfile\n import shutil\n\n path = os.path.join(self._session_dir, f\"{session_id}.json\")\n data: Dict[str, Any] = {}\n if os.path.exists(path):\n try:\n with open(path, 'r', encoding='utf-8') as f:\n data = json.load(f)\n except (json.JSONDecodeError, Exception):\n # 文件损坏,忽略旧数据\n pass\n data[\"session_id\"] = session_id\n if meta:\n for k, v in meta.items():\n data[k] = v\n if \"created_at\" not in data:\n data[\"created_at\"] = time.time()\n data[\"updated_at\"] = time.time()\n state = self.sessions.get(session_id)\n if state:\n data[\"current_stage\"] = state.current_stage.value\n data[\"status\"] = state.status\n data[\"stages_completed\"] = state.stages_completed\n data[\"artifacts\"] = state.artifacts\n data[\"error\"] = state.error\n # datetime 对象需要转换为时间戳\n data[\"updated_at\"] = state.updated_at.timestamp() if isinstance(state.updated_at, datetime) else state.updated_at\n # 保存元数据(包含模型配置)\n if state.meta:\n for k, v in state.meta.items():\n if v is not None:\n data[k] = v\n\n # 原子写入:先写临时文件,再重命名\n dir_path = os.path.dirname(path)\n fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix='.json')\n try:\n with os.fdopen(fd, 'w', encoding='utf-8') as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n shutil.move(tmp_path, path)\n except Exception:\n # 写入失败,删除临时文件\n if os.path.exists(tmp_path):\n os.remove(tmp_path)\n raise\n\n def _load_sessions_from_disk(self):\n \"\"\"启动时从磁盘加载所有已保存的会话\"\"\"\n if not os.path.exists(self._session_dir):\n return\n for filename in os.listdir(self._session_dir):\n if not filename.endswith('.json'):\n continue\n try:\n fpath = os.path.join(self._session_dir, filename)\n with open(fpath, 'r', encoding='utf-8') as f:\n data = json.load(f)\n sid = data[\"session_id\"]\n state = WorkflowState(sid)\n try:\n state.current_stage = WorkflowStage(data.get(\"current_stage\", \"init\"))\n except ValueError:\n state.current_stage = WorkflowStage.INIT\n # 旧版本兼容:状态名称转换\n old_status = data.get(\"status\", \"idle\")\n if old_status == \"waiting_intervention\":\n state.status = \"waiting_in_stage\"\n elif old_status == \"completed\":\n state.status = \"session_completed\"\n else:\n state.status = old_status\n\n state.stages_completed = data.get(\"stages_completed\", [])\n state.artifacts = data.get(\"artifacts\", {})\n state.error = data.get(\"error\")\n state.updated_at = data.get(\"updated_at\", 0)\n state.meta = {k: data[k] for k in\n (\"idea\", \"style\", \"llm_model\", \"image_t2i_model\",\n \"image_it2i_model\", \"video_model\")\n if k in data}\n self.sessions[sid] = state\n except json.JSONDecodeError:\n logger.warning(f\"Skipping corrupted session file: {filename}\")\n except Exception as e:\n logger.warning(f\"Failed to load session {filename}: {e}\")\n\n def delete_session(self, session_id: str) -> bool:\n \"\"\"删除指定会话(内存 + 磁盘 + 结果文件)\"\"\"\n from config import settings\n\n # 从内存中移除\n self.sessions.pop(session_id, None)\n self._stop_events.pop(session_id, None)\n\n # 1. 删除会话元数据文件\n path = os.path.join(self._session_dir, f\"{session_id}.json\")\n if os.path.exists(path):\n os.remove(path)\n\n # 2. 删除结果文件(剧本、图片、视频)\n result_base = settings.RESULT_DIR\n\n # 删除剧本文件\n script_file = os.path.join(result_base, 'script', f'script_{session_id}.json')\n if os.path.exists(script_file):\n os.remove(script_file)\n\n # 删除图片目录\n image_dir = os.path.join(result_base, 'image', session_id)\n if os.path.exists(image_dir):\n import shutil\n shutil.rmtree(image_dir)\n\n # 删除视频目录\n video_dir = os.path.join(result_base, 'video', session_id)\n if os.path.exists(video_dir):\n import shutil\n shutil.rmtree(video_dir)\n\n logger.info(f\"Session and results deleted: {session_id}\")\n return True\n\n def list_saved_sessions(self) -> List[Dict]:\n \"\"\"列出所有已保存的会话概要\"\"\"\n sessions: List[Dict] = []\n if not os.path.exists(self._session_dir):\n return sessions\n for filename in os.listdir(self._session_dir):\n if not filename.endswith('.json'):\n continue\n try:\n fpath = os.path.join(self._session_dir, filename)\n with open(fpath, 'r', encoding='utf-8') as f:\n data = json.load(f)\n sessions.append({\n \"id\": data[\"session_id\"],\n \"idea\": data.get(\"idea\", \"\"),\n \"style\": data.get(\"style\", \"\"),\n \"date\": data.get(\"updated_at\", 0),\n \"stages\": data.get(\"stages_completed\", []),\n })\n except Exception:\n continue\n sessions.sort(key=lambda x: x.get(\"date\", 0), reverse=True)\n return sessions\n","content_type":"text/x-python; charset=utf-8","language":"python","size":23177,"content_sha256":"3fa272f58dddd5be3d9846c0c681b596fe78102cfe0a579c5ab1e0fa75a1d247"},{"filename":"aigc-claw/backend/docs/api.md","content":"# MovieAssistant API 文档\n\n## 概述\n\nMovieAssistant 是一个 AI 视频生成系统,提供 REST API 供外部调用。API 与前端共享同一个历史数据库,支持在 API 和前端之间无缝切换。\n\n**基础 URL**: `http://localhost:8000`\n\n---\n\n## 认证\n\n当前版本无需认证,所有接口均可公开访问。\n\n---\n\n## 可用阶段\n\n| 阶段 ID | 名称 | 说明 |\n|---------|------|------|\n| `script_generation` | 剧本生成 | 将灵感转化为结构化剧本 |\n| `character_design` | 角色/场景设计 | 生成角色设计图和场景背景 |\n| `storyboard` | 分镜设计 | 设计镜头语言和分镜脚本 |\n| `reference_generation` | 参考图生成 | 生成高精度参考图 |\n| `video_generation` | 视频生成 | 将参考图/分镜图生成视频 |\n| `post_production` | 后期剪辑 | 拼接视频片段为最终成片 |\n\n---\n\n## API 接口列表\n\n### 1. 创建项目\n\n创建一个新的视频生成项目。\n\n**接口**: `POST /api/project/start`\n\n**请求体**:\n```json\n{\n \"idea\": \"故事线描述\",\n \"style\": \"anime\",\n \"llm_model\": \"qwen3.5-plus\",\n \"vlm_model\": \"qwen-vl-plus\",\n \"image_t2i_model\": \"doubao-seedream-5-0\",\n \"image_it2i_model\": \"doubao-seedream-5-0\",\n \"video_model\": \"wan2.6-i2v-flash\",\n \"enable_concurrency\": true\n}\n```\n\n**参数说明**:\n| 参数 | 类型 | 必填 | 说明 | 默认值 |\n|------|------|------|------|--------|\n| idea | string | 是 | 故事线描述 | - |\n| style | string | 否 | 视频风格 | anime |\n| llm_model | string | 否 | LLM 模型 | qwen3.5-plus |\n| vlm_model | string | 否 | VLM 评估模型 | qwen-vl-plus |\n| image_t2i_model | string | 否 | 文生图模型 | doubao-seedream-5-0 |\n| image_it2i_model | string | 否 | 图生图模型 | doubao-seedream-5-0 |\n| video_model | string | 否 | 视频模型 | wan2.6-i2v-flash |\n| enable_concurrency | bool | 否 | 开启并发生成(可同时生成多张图片/视频) | true |\n\n**响应示例**:\n```json\n{\n \"session_id\": \"1773208355389\",\n \"status\": \"running\",\n \"params\": {\n \"idea\": \"故事线描述\",\n \"style\": \"anime\",\n \"llm_model\": \"gemini-3-flash-preview\"\n }\n}\n```\n\n---\n\n### 2. 执行阶段\n\n执行指定的生成阶段。\n\n**接口**: `POST /api/project/{session_id}/execute/{stage}`\n\n**路径参数**:\n- `session_id`: 项目会话 ID\n- `stage`: 阶段 ID(见上表)\n\n**请求体**:\n```json\n{\n \"style\": \"anime\"\n}\n```\n\n> 请求体参数与创建项目相同(可选),会覆盖项目中已有的对应参数。\n\n**响应**: SSE 流式返回,包含以下事件类型:\n- `progress`: 进度更新\n- `stage_complete`: 阶段完成\n- `error`: 执行错误\n\n**示例 - 进度事件**:\n```json\n{\n \"type\": \"progress\",\n \"message\": \"剧本生成: 正在生成...\",\n \"phase\": \"剧本生成\",\n \"step_desc\": \"正在生成...\",\n \"percent\": 50\n}\n```\n\n**示例 - 阶段完成事件**:\n```json\n{\n \"type\": \"stage_complete\",\n \"stage\": \"script_generation\",\n \"status\": \"stage_completed\",\n \"requires_intervention\": false\n}\n```\n\n---\n\n### 3. 获取项目状态\n\n获取项目的当前状态。\n\n**接口**: `GET /api/project/{session_id}/status`\n\n**响应示例**:\n```json\n{\n \"session_id\": \"1773208355389\",\n \"current_stage\": \"script_generation\",\n \"status\": \"running\",\n \"error\": null,\n \"stages_completed\": [],\n \"artifacts\": {},\n \"meta\": {\n \"idea\": \"故事线描述\",\n \"style\": \"anime\"\n },\n \"updated_at\": 1773208355389\n}\n```\n\n---\n\n### 4. 获取阶段产物\n\n获取指定阶段的产物数据。\n\n**接口**: `GET /api/project/{session_id}/artifact/{stage}`\n\n**响应示例** (剧本生成阶段):\n```json\n{\n \"stage\": \"script_generation\",\n \"artifact\": {\n \"title\": \"影弑\",\n \"logline\": \"...\",\n \"characters\": [...],\n \"settings\": [...],\n \"scenes\": [...]\n }\n}\n```\n\n---\n\n### 5. 更新阶段产物\n\n更新指定阶段的产物数据(如用户修改提示词、选择版本)。\n\n**接口**: `PATCH /api/project/{session_id}/artifact/{stage}`\n\n请求体格式**因阶段而异**:\n\n#### storyboard — 修改分镜(时长/剧情/视觉提示词)\n```json\n{\n \"shots\": [\n {\"shot_id\": \"shot_001_01\", \"duration\": 5, \"plot\": \"新剧情描述\", \"visual_prompt\": \"新视觉提示词\"}\n ]\n}\n```\n\n#### reference_generation — 修改视觉提示词\n```json\n{\n \"shots\": [\n {\"shot_id\": \"shot_001_01\", \"visual_prompt\": \"新提示词\"}\n ]\n}\n```\n\n#### reference_generation — 选择参考图版本\n```json\n{\n \"shot_001_01\": \"code/result/image/xxx/shot_001_01_v2.jpg\"\n}\n```\n\n#### video_generation — 修改片段描述/时长\n```json\n{\n \"shot_001_01\": {\"description\": \"新描述\", \"duration\": 5}\n}\n```\n\n#### video_generation — 选择视频版本\n```json\n{\n \"shot_001_01\": \"code/result/video/xxx/shot_001_01_v2.mp4\"\n}\n```\n\n**响应**: `{\"status\": \"ok\"}`\n\n---\n\n### 6. 干预阶段\n\n对已完成的阶段进行修改并重新执行(重新生成部分产物)。\n\n**接口**: `POST /api/project/{session_id}/intervene`\n\n**请求体**:\n```json\n{\n \"stage\": \"reference_generation\",\n \"modifications\": {\n \"regenerate_scenes\": [\"shot_001_01\", \"shot_001_02\"]\n }\n}\n```\n\n- `stage`:要干预的阶段\n- `modifications`:修改内容,目前支持 `regenerate_scenes`(要重新生成的镜头 ID 列表)\n\n**响应**: SSE 流式返回,包含以下事件类型:\n- `progress`: 进度更新\n- `stage_complete`: 阶段完成\n- `error`: 执行错误\n\n---\n\n### 7. 确认并继续\n\n确认当前阶段的修改,进入下一阶段。\n\n**接口**: `POST /api/project/{session_id}/continue`\n\n**响应示例**:\n```json\n{\n \"status\": \"ready\",\n \"next_stage\": \"character_design\"\n}\n```\n\n---\n\n### 8. 停止执行\n\n停止当前正在执行的阶段。\n\n**接口**: `POST /api/project/{session_id}/stop`\n\n**响应示例**:\n```json\n{\n \"status\": \"stopped\"\n}\n```\n\n---\n\n### 9. 获取会话列表\n\n获取所有历史项目列表。\n\n**接口**: `GET /api/sessions`\n\n**响应示例**:\n```json\n{\n \"sessions\": [\n {\n \"id\": \"1773208355389\",\n \"idea\": \"故事线\",\n \"style\": \"anime\",\n \"date\": 1773208355389,\n \"stages\": [\"script_generation\", \"character_design\"]\n }\n ]\n}\n```\n\n---\n\n### 10. 获取阶段列表\n\n获取所有可用阶段列表。\n\n**接口**: `GET /api/stages`\n\n**响应示例**:\n```json\n{\n \"stages\": [\n {\"id\": \"script_generation\", \"name\": \"剧本生成\", \"order\": 1, \"description\": \"将灵感转化为结构化剧本\"},\n {\"id\": \"character_design\", \"name\": \"角色/场景设计\", \"order\": 2},\n {\"id\": \"storyboard\", \"name\": \"分镜设计\", \"order\": 3},\n {\"id\": \"reference_generation\", \"name\": \"参考图生成\", \"order\": 4},\n {\"id\": \"video_generation\", \"name\": \"视频生成\", \"order\": 5},\n {\"id\": \"post_production\", \"name\": \"后期剪辑\", \"order\": 6}\n ]\n}\n```\n\n---\n\n## 调用示例\n\n### 完整流程示例\n\n```bash\n# 1. 创建项目\nSESSION_ID=$(curl -s -X POST http://localhost:8000/api/project/start \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"idea\": \"失忆女刺客刺杀目标时恢复记忆,与爱人联手复仇师兄\",\n \"style\": \"anime\"\n }' | jq -r '.session_id')\n\necho \"Session ID: $SESSION_ID\"\n\n# 2. 执行第一阶段(剧本生成)- 监听 SSE\ncurl -X POST \"http://localhost:8000/api/project/${SESSION_ID}/execute/script_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"style\": \"anime\"}'\n\n# 3. 获取剧本产物\ncurl -s \"http://localhost:8000/api/project/${SESSION_ID}/artifact/script_generation\"\n\n# 4. 确认并继续到下一阶段\ncurl -s -X POST \"http://localhost:8000/api/project/${SESSION_ID}/continue\"\n\n# 5. 执行第二阶段(角色设计)\ncurl -X POST \"http://localhost:8000/api/project/${SESSION_ID}/execute/character_design\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"style\": \"anime\"}'\n\n# 6. 确认并继续\ncurl -s -X POST \"http://localhost:8000/api/project/${SESSION_ID}/continue\"\n\n# 7. 执行第三阶段(分镜设计)\ncurl -X POST \"http://localhost:8000/api/project/${SESSION_ID}/execute/storyboard\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"style\": \"anime\"}'\n\n# 8. 确认并继续\ncurl -s -X POST \"http://localhost:8000/api/project/${SESSION_ID}/continue\"\n\n# 9. 执行第四阶段(参考图生成)\ncurl -X POST \"http://localhost:8000/api/project/${SESSION_ID}/execute/reference_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"style\": \"anime\"}'\n\n# 10. 确认并继续\ncurl -s -X POST \"http://localhost:8000/api/project/${SESSION_ID}/continue\"\n\n# 11. 执行第五阶段(视频生成)\ncurl -X POST \"http://localhost:8000/api/project/${SESSION_ID}/execute/video_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"style\": \"anime\"}'\n\n# 12. 确认并继续\ncurl -s -X POST \"http://localhost:8000/api/project/${SESSION_ID}/continue\"\n\n# 13. 执行第六阶段(后期剪辑)\ncurl -X POST \"http://localhost:8000/api/project/${SESSION_ID}/execute/post_production\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"style\": \"anime\"}'\n```\n\n### 使用 jq 简化\n\n```bash\n# 创建项目并提取 session_id\nSESSION_ID=$(curl -s -X POST http://localhost:8000/api/project/start \\\n -H \"Content-Type: application/json\" \\\n -d '{\"idea\": \"故事线\", \"style\": \"anime\"}' | python3 -c \"import json,sys; print(json.load(sys.stdin)['session_id'])\")\n\n# 查看项目状态\ncurl -s \"http://localhost:8000/api/project/${SESSION_ID}/status\" | python3 -m json.tool\n```\n\n---\n\n## 错误处理\n\n### HTTP 状态码\n\n| 状态码 | 说明 |\n|--------|------|\n| 200 | 请求成功 |\n| 400 | 请求参数错误 |\n| 404 | 资源不存在 |\n| 500 | 服务器内部错误 |\n\n### 错误响应格式\n\n```json\n{\n \"detail\": \"错误描述\"\n}\n```\n\n### 常见错误\n\n| 错误 | 说明 |\n|------|------|\n| Session not found | 指定的 session_id 不存在 |\n| Artifact for stage 'xxx' not found | 指定阶段的产物不存在 |\n| 阶段执行失败 | 阶段执行过程中发生错误 |\n\n---\n\n## 前端与 API 共享\n\nAPI 和前端共享同一个 session 存储:\n- Session 文件位置: `backend/code/data/sessions/{session_id}.json`\n- 产物位置: `backend/code/result/`\n\n这意味着:\n1. API 创建的项目可以在前端查看和继续\n2. 前端创建的项目可以继续使用 API 操作\n3. 两种方式可以随时切换\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10146,"content_sha256":"2339a356873a6c5b8d0c9ccccd079f05a68034f4c4af6f25babbaeafa9e489ff"},{"filename":"aigc-claw/backend/docs/session_format.md","content":"# Session 数据格式\n\nSession 数据存储在 `code/data/sessions/{session_id}.json`,包含完整的项目会话信息。\n\n## 根级字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `session_id` | string | 会话唯一 ID |\n| `idea` | string | 用户原始创意/故事想法 |\n| `style` | string | 视觉风格,如 `realistic`、`anime` |\n| `video_ratio` | string | 视频比例,如 `16:9`、`9:16` |\n| `expand_idea` | bool | 是否扩展创意 |\n| `llm_model` | string | LLM 模型名称 |\n| `vlm_model` | string | VLM 模型名称 |\n| `image_t2i_model` | string | 文生图模型 |\n| `image_it2i_model` | string | 图生图模型 |\n| `video_model` | string | 视频生成模型 |\n| `enable_concurrency` | string/bool | 是否启用并发 |\n| `web_search` | bool | 是否启用网络搜索 |\n| `created_at` | float | 创建时间戳 |\n| `updated_at` | float | 更新时间戳 |\n| `current_stage` | string | 当前阶段 |\n| `status` | string | 会话状态 |\n| `stages_completed` | string[] | 已完成的阶段列表 |\n| `error` | null / string | 错误信息 |\n| `artifacts` | object | 各阶段产物(见下文) |\n\n## Status 状态值\n\n| 状态 | 说明 |\n|------|------|\n| `idle` | 初始状态 |\n| `running` | 运行中 |\n| `waiting` | 等待用户确认 |\n| `stage_completed` | 阶段完成 |\n| `session_completed` | 会话完成 |\n\n## Stages Completed 阶段列表\n\n按顺序完成的所有阶段:\n```\nscript_generation → character_design → storyboard → reference_generation → video_generation → post_production\n```\n\n---\n\n## Artifacts 各阶段产物\n\n### 1. script_generation\n\n```json\n{\n \"session_id\": \"...\",\n \"title\": \"守\",\n \"logline\": \"一句话概括故事\",\n \"genre\": [\"奇幻\", \"温情\"],\n \"target_duration\": 180,\n \"synopsis\": \"详细故事梗概\",\n \"characters\": [\n {\n \"name\": \"角色名\",\n \"character_id\": \"char_xxx\",\n \"description\": \"角色描述\",\n \"personality\": [\"特质1\", \"特质2\"],\n \"motivation\": \"角色动机\",\n \"arc_description\": \"角色弧线描述\",\n \"role\": \"主角/配角/反派\",\n \"age\": \"年龄\",\n \"species\": \"物种(人类/狗等)\"\n }\n ],\n \"settings\": [\n {\n \"name\": \"场景名\",\n \"setting_id\": \"set_xxx\",\n \"description\": \"场景描述\"\n }\n ],\n \"scenes\": [\n {\n \"scene_number\": 1,\n \"location\": \"场景位置\",\n \"characters\": [\"角色名\"],\n \"plot\": \"场景剧情描述\"\n }\n ],\n \"overall_style\": \"realistic\",\n \"mood\": \"整体情绪基调\",\n \"project_id\": \"proj_xxx\",\n \"version\": 1,\n \"metadata\": {\n \"generation_model\": \"模型名\",\n \"generation_prompt\": \"原始生成提示词\",\n \"mode\": \"movie/micro\"\n },\n \"logline_data\": {\n \"logline\": \"完整logline\",\n \"who\": \"主角是谁\",\n \"goal\": \"主角目标\",\n \"conflict\": \"核心冲突\",\n \"twist\": \"反转点\",\n \"theme\": \"主题\"\n }\n}\n```\n\n### 2. character_design\n\n```json\n{\n \"session_id\": \"...\",\n \"characters\": [\n {\n \"id\": \"char_xxx\",\n \"name\": \"角色名\",\n \"description\": \"角色外观描述\",\n \"selected\": \"选中的角色图路径\",\n \"versions\": [\"所有版本路径\"]\n }\n ],\n \"settings\": [\n {\n \"id\": \"set_xxx\",\n \"name\": \"场景名\",\n \"description\": \"场景描述\",\n \"selected\": \"选中的场景图路径\",\n \"versions\": [\"所有版本路径\"]\n }\n ]\n}\n```\n\n### 3. storyboard\n\n```json\n{\n \"session_id\": \"...\",\n \"shots\": [\n {\n \"shot_number\": 1,\n \"duration\": 5,\n \"characters\": [\"角色名\"],\n \"location\": \"场景位置\",\n \"plot\": \"镜头剧情描述\",\n \"visual_prompt\": \"视觉生成提示词\",\n \"shot_id\": \"shot_001_01\",\n \"scene_number\": 1,\n \"act\": 1\n }\n ],\n \"user_modified\": true,\n \"new_shot_ids\": []\n}\n```\n\n> **注意**:storyboard 结构是 `{shots: [...]}`,**没有** `payload` 包装层。\n\n### 4. reference_generation\n\n```json\n{\n \"session_id\": \"...\",\n \"scenes\": [\n {\n \"id\": \"shot_001_01\",\n \"name\": \"场景1-镜头1\",\n \"index\": 1,\n \"description\": \"视觉生成提示词(由 storyboard.visual_prompt 同步过来)\",\n \"selected\": \"用户选中的参考图路径\",\n \"versions\": [\"所有版本路径\"],\n \"status\": \"done/pending/failed\"\n }\n ],\n \"shots\": [\n {\n \"shot_id\": \"shot_001_01\",\n \"video_prompt\": \"视频生成提示词\"\n }\n ]\n}\n```\n\n> **注意**:\n> - `scenes[].id` = `storyboard.shots[].shot_id`,用于跨阶段关联\n> - `shots[]` 是给视频生成用的提示词,格式为 `{shot_id, video_prompt}`\n\n### 5. video_generation\n\n```json\n{\n \"session_id\": \"...\",\n \"clips\": [\n {\n \"id\": \"shot_001_01\",\n \"name\": \"场景1-镜头1\",\n \"index\": 1,\n \"description\": \"视频片段描述(由 storyboard.plot 同步)\",\n \"duration\": 5,\n \"selected\": \"用户选中的视频路径\",\n \"versions\": [\"所有版本路径\"],\n \"status\": \"done/pending/failed\"\n }\n ]\n}\n```\n\n> **注意**:`clips[].id` = `storyboard.shots[].shot_id`\n\n### 6. post_production\n\n```json\n{\n \"session_id\": \"...\",\n \"final_video\": \"code/result/video/xxx_final.mp4\"\n}\n```\n\n---\n\n## 跨阶段数据同步关系\n\n```\nstoryboard (修改 plot/visual_prompt/duration)\n ↓\nvideo_generation.clips (description/duration)\n ↑ (修改 description/duration)\n ↑\nvideo_generation (修改 clips.description/clips.duration)\n ↓ (修改后同步回 storyboard)\nstoryboard (修改后同步回 storyboard.shots.plot)\n\nreference_generation (修改 scenes.description)\n ↓\nstoryboard.shots.visual_prompt (由 reference_generation 同步)\n ↑ (修改后同步)\n ↑\nreference_generation (修改 scenes.description)\n```\n\n---\n\n## PATCH /artifact/{stage} 请求格式\n\n### storyboard\n```json\n{\n \"shots\": [\n {\"shot_id\": \"shot_001_01\", \"duration\": 5, \"plot\": \"新描述\", \"visual_prompt\": \"新提示词\"}\n ]\n}\n```\n\n### reference_generation(修改视觉提示词)\n```json\n{\n \"shots\": [\n {\"shot_id\": \"shot_001_01\", \"visual_prompt\": \"新提示词\"}\n ]\n}\n```\n\n### reference_generation(选择图片版本)\n```json\n{\n \"shot_001_01\": \"code/result/image/xxx/shot_001_01_v2.jpg\"\n}\n```\n\n### video_generation(修改片段描述/时长)\n```json\n{\n \"shot_001_01\": {\"description\": \"新描述\", \"duration\": 5}\n}\n```\n\n### video_generation(选择视频版本)\n```json\n{\n \"shot_001_01\": \"code/result/video/xxx/shot_001_01_v2.mp4\"\n}\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6416,"content_sha256":"73ce580d997ff83a77909d5f8184ce1029441a54e9bdcf83441c02b76ef6a622"},{"filename":"aigc-claw/backend/prompts/character/character_styles.txt","content":"# Character Reference Sheet Prompt Templates\n# 角色参考图提示词模板(按风格分类)\n# 共用 4 视图结构,风格特定视觉描述\n# 使用方式:根据 {style} 变量选择对应的模板\n\n# ============================================================\n# 共用 4 视图结构定义 (所有风格通用)\n# ============================================================\n# Four-view structure template (shared across all styles):\n# 1) front-facing portrait (head and shoulders)\n# 2) front-facing full body (standing in natural pose)\n# 3) side-view full body (standing in natural pose)\n# 4) back-view full body (standing in natural pose)\n# ALL views show identical character with perfectly consistent appearance, outfit, and lighting\n\n# ============================================================\n# 共用面部要求 (所有风格通用)\n# ============================================================\nDO NOT describe or specify eye color. Leave eye color unspecified.\nSmooth, clear face, no blemishes, no spots, perfect skin texture.\n\n# ============================================================\n# realistic - 写实摄影风格\n# ============================================================\n[realistic]\nPhotorealistic character reference sheet, actual photograph style, pure white background.\nFour views of the same real person arranged in a single row from left to right:\n1) front-facing portrait (head and shoulders),\n2) front-facing full body (standing in natural resting pose),\n3) side-view full body (natural resting pose),\n4) back-view full body (natural resting pose).\nALL views show identical character with perfectly consistent appearance, outfit, and lighting.\nPortrait shows head to waist; others show complete body head to feet.\nUltra high quality photography, 8k resolution, sharp focus, 85mm lens, f/2.8, soft studio lighting, even illumination.\nRealistic skin texture with subtle imperfections, fabric has natural folds and highlights.\nNO text, NO labels, NO annotations, NO captions, NO watermarks on image.\nNOT an illustration, NOT a drawing, NOT anime style.\nCharacter: {name} — {desc}\n\n# ============================================================\n# anime - 动漫风格\n# ============================================================\n[anime]\nAnime character reference sheet, clean vector art style, white background.\nFour views of the same anime character arranged in a single row from left to right:\n1) front-facing portrait (head and shoulders),\n2) front-facing full body (standing in natural pose),\n3) side-view full body (natural pose),\n4) back-view full body (natural pose).\nALL views show IDENTICAL character with perfectly consistent appearance, outfit, hair, and facial features.\nPortrait shows head to waist; others show complete body head to feet.\nClean anime linework, vibrant colors, cel-shaded rendering style.\nHigh quality anime illustration, masterpiece, crisp details.\nDO NOT describe or specify eye color. Leave eye color unspecified.\nSmooth, clear face, no blemishes, no spots, perfect skin.\nNO text, NO labels, NO annotations, NO captions, NO watermarks on image.\nCharacter: {name} — {desc}\n\n# ============================================================\n# comic-book - 漫画风格\n# ============================================================\n[comic-book]\nComic book character reference sheet, comic panel art style, white background.\nFour views of the same comic character arranged in a single row from left to right:\n1) front-facing portrait (head and shoulders),\n2) front-facing full body (heroic pose),\n3) side-view full body (action pose),\n4) back-view full body (standing pose).\nALL views show IDENTICAL character with perfectly consistent appearance, costume, and signature features.\nPortrait shows head to waist; others show complete body head to feet.\nBold ink lines, comic shading (hatching/cross-hatching), dynamic comic book art style.\nHigh quality comic illustration, vibrant ink colors, dramatic lighting.\nDO NOT describe or specify eye color. Leave eye color unspecified.\nSmooth, clear face, no blemishes, no spots, perfect skin.\nNO text, NO labels, NO annotations, NO captions, NO speech bubbles on image.\nCharacter: {name} — {desc}\n\n# ============================================================\n# 3d-disney - 迪士尼3D风格\n# ============================================================\n[3d-disney]\n3D Disney-style character reference sheet, CGI animated movie style, pure white background.\nFour views of the same 3D animated character arranged in a single row from left to right:\n1) front-facing portrait (head and shoulders),\n2) front-facing full body (friendly pose),\n3) side-view full body (character profile),\n4) back-view full body (standing pose).\nALL views show IDENTICAL character with perfectly consistent appearance, costume, and features.\nPortrait shows head to waist; others show complete body head to feet.\nDisney Pixar CGI quality, toon shader rendering, smooth 3D geometry, clean textures.\nHigh quality 3D render, soft shadows, cinematic lighting, feature film quality.\nDO NOT describe or specify eye color. Leave eye color unspecified.\nSmooth, clear face, no blemishes, no spots, perfect skin.\nNO text, NO labels, NO annotations, NO captions, NO watermarks on image.\nCharacter: {name} — {desc}\n\n# ============================================================\n# watercolor - 水彩风格\n# ============================================================\n[watercolor]\nWatercolor character reference sheet, painterly art style, white background.\nFour views of the same character arranged in a single row from left to right:\n1) front-facing portrait (head and shoulders),\n2) front-facing full body (elegant pose),\n3) side-view full body (graceful pose),\n4) back-view full body (standing pose).\nALL views show IDENTICAL character with perfectly consistent appearance, outfit, and features.\nPortrait shows head to waist; others show complete body head to feet.\nSoft watercolor painting texture, flowing color washes, delicate brush strokes, translucent layers.\nHigh quality watercolor illustration, artistic masterpiece, organic color blending.\nDO NOT describe or specify eye color. Leave eye color unspecified.\nSmooth, clear face, no blemishes, no spots, perfect skin.\nNO text, NO labels, NO annotations, NO captions, NO watermarks on image.\nCharacter: {name} — {desc}\n\n# ============================================================\n# oil-painting - 油画风格\n# ============================================================\n[oil-painting]\nOil painting character reference sheet, classical fine art style, white background.\nFour views of the same character arranged in a single row from left to right:\n1) front-facing portrait (head and shoulders),\n2) front-facing full body (noble pose),\n3) side-view full body (aristocratic pose),\n4) back-view full body (standing pose).\nALL views show IDENTICAL character with perfectly consistent appearance, attire, and features.\nPortrait shows head to waist; others show complete body head to feet.\nRich oil paint texture, impasto brushwork, classical portraiture technique, deep colors.\nHigh quality oil painting, museum-quality masterpiece, dramatic chiaroscuro lighting.\nDO NOT describe or specify eye color. Leave eye color unspecified.\nSmooth, clear face, no blemishes, no spots, perfect skin.\nNO text, NO labels, NO annotations, NO captions, NO watermarks on image.\nCharacter: {name} — {desc}\n\n# ============================================================\n# cyberpunk - 赛博朋克风格\n# ============================================================\n[cyberpunk]\nCyberpunk character reference sheet, futuristic sci-fi style, white background.\nFour views of the same cybernetic character arranged in a single row from left to right:\n1) front-facing portrait (head and shoulders, showing cybernetics),\n2) front-facing full body (tech-enhanced pose),\n3) side-view full body (cyberpunk stance),\n4) back-view full body (with tech gear visible).\nALL views show IDENTICAL character with perfectly consistent appearance, cybernetic augmentations, and outfit.\nPortrait shows head to waist; others show complete body head to feet.\nNeon lighting, chrome/metallic surfaces, holographic elements, RGB accents.\nHigh quality cyberpunk illustration, sci-fi concept art, futuristic tech aesthetic.\nDO NOT describe or specify eye color. Leave eye color unspecified.\nSmooth, clear face, no blemishes, no spots, perfect skin.\nNO text, NO labels, NO annotations, NO captions, NO HUD elements on image.\nCharacter: {name} — {desc}\n\n# ============================================================\n# chinese-ink - 中国水墨风格\n# ============================================================\n[chinese-ink]\nChinese ink painting (水墨画) character reference sheet, traditional East Asian art style, white background.\nFour views of the same character arranged in a single row from left to right:\n1) front-facing portrait (head and shoulders),\n2) front-facing full body (classical pose),\n3) side-view full body (traditional stance),\n4) back-view full body (standing pose).\nALL views show IDENTICAL character with perfectly consistent appearance, clothing, and features.\nPortrait shows head to waist; others show complete body head to feet.\nInk wash (墨色) rendering, Gongbi/Boneless brush technique, subtle ink gradients, rice paper texture.\nHigh quality Chinese ink painting, traditional scroll art aesthetics, elegant brushwork.\nDO NOT describe or specify eye color. Leave eye color unspecified.\nSmooth, clear face, no blemishes, no spots, perfect skin.\nNO text, NO labels, NO annotations, NO captions, NO seals/stamps on image.\nCharacter: {name} — {desc}\n","content_type":"text/plain; charset=utf-8","language":null,"size":9693,"content_sha256":"f1d1e5ba7243a36e9ec16e940207be7fd27d4d274bf5667282c5d59bd0882563"},{"filename":"aigc-claw/backend/prompts/character/character_zh.txt","content":"Character reference sheet, {style} style, white background,\n4 views of the same character arranged in a single row from left to right:\n1) front-facing close-up portrait (head and upper body),\n2) front-facing full body ({pose_note}),\n3) side-view full body ({pose_note}),\n4) back-view full body ({pose_note}).\nAll views show the SAME character with consistent appearance and outfit.\nThe close-up shows head-to-waist; the other three show complete body from head to feet.\nPure image only, absolutely NO text, NO labels, NO annotations, NO captions, NO watermarks on the image.\nHigh quality, masterpiece, clean line art.\nCharacter: {name} — {desc}\n","content_type":"text/plain; charset=utf-8","language":null,"size":647,"content_sha256":"45b7b956d1b040ac9dd7b92fdd877a34d0d6ddcde3266169b50bdaa671c2669f"},{"filename":"aigc-claw/backend/prompts/character/eval_character_zh.txt","content":"你是一个专业的AI图像质量评估师。请根据以下信息评估生成的4视图角色参考图。\n\n角色描述:\n{character_description}\n\n注意:这是一张4视图角色参考图,包含:\n1. 正面特写(头部及上半身)\n2. 正面全身照\n3. 侧面全身照\n4. 背面全身照\n\n请严格按照以下标准评估并给出1-10分:\n\n【评分标准】\n满分10分,每个检查项符合得1分,不符合扣1分,最高10分:\n\n1. 四视图一致性(+1/-1):四个视图是否为同一角色(面貌、服装、体型是否一致)\n2. 面貌还原(+1/-1):面孔是否与描述一致\n3. 发型还原(+1/-1):发型是否与描述一致\n4. 服装还原(+1/-1):服装颜色、款式是否与描述一致\n5. 体型还原(+1/-1):体型是否与描述一致\n6. 完整性(+1/-1):四个视图是否都存在且完整\n7. 光线质量(+1/-1):光线是否自然、合理\n8. 构图质量(+1/-1):构图是否合理、均衡\n9. 图片质量(+1/-1):清晰度良好,无模糊、无畸变\n10. 无无关元素(+1/-1):无文字、标签、注释、水印等\n\n【扣分项】(发现以下问题必须扣分并在issues中列出):\n- 四个视图角色面貌不一致:-1分\n- 服装颜色与描述不符:-1分\n- 发型与描述不符:-1分\n- 面孔与描述不符:-1分\n- 缺少某个视图:-1分\n- 出现文字/水印:-1分\n\n【最终评分规则】\n- 8分及以上:可接受(is_acceptable: true)\n- 7分及以下:需要重新生成(is_acceptable: false)\n\n请输出纯JSON格式(不要用```包裹):\n{{\n \"score\": 1-10,\n \"issues\": [\"问题1\", \"问题2\", ...],\n \"is_acceptable\": true/false,\n \"suggestion\": \"修改建议(如果有问题)\"\n}}\n\n如果图片可接受,issues 返回空数组 []。\n","content_type":"text/plain; charset=utf-8","language":null,"size":1807,"content_sha256":"202940cd46e23a5f6e1a6de2a524e41497a6852ea47bf5228be87f4d04390a10"},{"filename":"aigc-claw/backend/prompts/character/eval_select_best_zh.txt","content":"你是一个专业的AI图像质量评估师。请从以下{num_images}张角色设计图中选择最好的一张。\n\n角色名称:{character_name}\n角色描述:{character_description}\n角色物种:{species}\n\n图片列表:\n{images_list}\n\n请严格按照以下标准评估并选择最好的一张:\n\n【评分标准】\n满分10分,每个检查项符合得1分,不符合扣1分,最高10分:\n\n1. 外观一致性(+1/-1):角色外观是否与描述一致(发型、服装、配饰)\n2. 面部特征(+1/-1):面部特征是否清晰、符合描述\n3. 身体比例(+1/-1):身体比例是否正常、协调\n4. 风格一致性(+1/-1):整体风格是否符合设定风格\n5. 细节完整(+1/-1):细节是否丰富,无明显缺失\n6. 色彩还原(+1/-1):色彩是否与描述一致\n7. 画面质量(+1/-1):清晰度良好,无模糊、无畸变\n8. 构图质量(+1/-1):构图是否均衡、美观\n9. 视角一致性(+1/-1):四个视角是否一致\n10. 无无关元素(+1/-1):无文字、水印、标签等无关内容\n\n【最终选择规则】\n- 选择分数最高的一张作为最终选择\n- 如果最高分有多张相同,选择第一张\n\n请输出纯JSON格式(不要用```包裹):\n{{\n \"scores\": [\n {{\"image_index\": 0, \"score\": 8, \"reasons\": [\"原因1\", \"原因2\"]}},\n {{\"image_index\": 1, \"score\": 6, \"reasons\": [\"原因1\"]}},\n ...\n ],\n \"best_index\": 0,\n \"best_reason\": \"选择这张的原因\"\n}}","content_type":"text/plain; charset=utf-8","language":null,"size":1489,"content_sha256":"b047c8273e3383744aec788ed9ac379a4a75da24c69bfeb432b6cd7e525e81f1"},{"filename":"aigc-claw/backend/prompts/loader.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\n统一提示词加载器\n从 prompts/ 目录加载提示词模板文件\n\"\"\"\n\nimport os\nfrom typing import Optional\n\n# 获取 prompts 目录的绝对路径 (loader.py is in backend/prompts/, prompts are in backend/prompts/)\nPROMPTS_DIR = os.path.dirname(os.path.abspath(__file__))\n\n\ndef load_prompt(category: str, name: str, lang: str = 'zh') -> str:\n \"\"\"\n 加载提示词文件\n\n Args:\n category: 提示词分类 (script, character, setting, storyboard, reference, video, logline)\n name: 提示词文件名 (不含扩展名)\n lang: 语言版本 ('zh' 或 'en')\n\n Returns:\n 提示词内容字符串\n\n Example:\n prompt = load_prompt('logline', 'generate', 'zh')\n \"\"\"\n # 尝试加载语言版本\n file_path = os.path.join(PROMPTS_DIR, category, f\"{name}_{lang}.txt\")\n if os.path.exists(file_path):\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read().strip()\n\n # 回退到中文版本\n file_path = os.path.join(PROMPTS_DIR, category, f\"{name}.txt\")\n if os.path.exists(file_path):\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read().strip()\n\n raise FileNotFoundError(f\"Prompt not found: {category}/{name}_{lang}.txt or {category}/{name}.txt\")\n\n\ndef load_prompt_with_fallback(category: str, name: str, lang: str = 'zh', fallback_lang: str = 'zh') -> str:\n \"\"\"\n 加载提示词,如果指定语言不存在则回退\n\n Args:\n category: 提示词分类\n name: 提示词文件名\n lang: 首选语言\n fallback_lang: 回退语言\n \"\"\"\n # 先尝试首选语言\n file_path = os.path.join(PROMPTS_DIR, category, f\"{name}_{lang}.txt\")\n if os.path.exists(file_path):\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read().strip()\n\n # 回退到指定语言\n if fallback_lang != lang:\n file_path = os.path.join(PROMPTS_DIR, category, f\"{name}_{fallback_lang}.txt\")\n if os.path.exists(file_path):\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read().strip()\n\n raise FileNotFoundError(f\"Prompt not found: {category}/{name}_{lang}.txt\")\n\n\ndef format_prompt(template: str, **kwargs) -> str:\n \"\"\"\n 格式化提示词模板\n\n Args:\n template: 提示词模板字符串\n **kwargs: 格式化参数\n\n Returns:\n 格式化后的提示词\n\n Example:\n prompt = format_prompt(\"Hello {name}, you are {age} years old\", name=\"John\", age=30)\n \"\"\"\n return template.format(**kwargs)\n\n\n# 风格提示词缓存\n_STYLE_PROMPTS_CACHE = {}\n\n\ndef load_style_prompt(category: str, style: str) -> str:\n \"\"\"\n 加载指定风格的提示词\n\n Args:\n category: 提示词分类 (character, setting)\n style: 风格名称 (realistic, anime, comic-book, 3d-disney, watercolor, oil-painting, cyberpunk, chinese-ink)\n\n Returns:\n 风格提示词模板\n\n Example:\n prompt = load_style_prompt('character', 'realistic')\n \"\"\"\n # 先检查缓存\n cache_key = f\"{category}:{style}\"\n if cache_key in _STYLE_PROMPTS_CACHE:\n return _STYLE_PROMPTS_CACHE[cache_key]\n\n # 加载风格提示词文件\n file_path = os.path.join(PROMPTS_DIR, category, f\"{style}_styles.txt\")\n if not os.path.exists(file_path):\n # 回退到通用的风格文件 (character_styles.txt 或 setting_styles.txt)\n fallback_file = \"character_styles.txt\" if category == \"character\" else \"setting_styles.txt\"\n file_path = os.path.join(PROMPTS_DIR, category, fallback_file)\n\n if not os.path.exists(file_path):\n fallback_file = \"character_styles.txt\" if category == \"character\" else \"setting_styles.txt\"\n raise FileNotFoundError(f\"Style prompt file not found: {category}/{fallback_file}\")\n\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n\n # 解析风格块\n current_style = None\n style_templates = {}\n for line in content.split('\\n'):\n line = line.strip()\n # Skip empty lines and comment lines\n if not line or line.startswith('#'):\n continue\n if line.startswith('[') and line.endswith(']'):\n current_style = line[1:-1]\n style_templates[current_style] = []\n elif current_style and line:\n style_templates[current_style].append(line)\n\n # 转换为字符串\n for s in style_templates:\n style_templates[s] = '\\n'.join(style_templates[s])\n\n # 缓存所有风格\n _STYLE_PROMPTS_CACHE.update(style_templates)\n\n # 返回指定风格的提示词\n if style in style_templates:\n return style_templates[style]\n\n # 回退到 anime 或第一个可用风格\n if 'anime' in style_templates:\n return style_templates['anime']\n\n # 返回第一个可用的风格\n return list(style_templates.values())[0] if style_templates else \"\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4960,"content_sha256":"ed9ccbb9508ea2eaa755aa765c8055929e4279493f80707e19d6f7b407263257"},{"filename":"aigc-claw/backend/prompts/logline/check_en.txt","content":"Determine if the following text contains enough narrative elements for a complete story Logline.\n(Needs: protagonist, goal, conflict, twist, and theme)\n\nText: {idea}\n\nAnswer only Yes or No.\n","content_type":"text/plain; charset=utf-8","language":null,"size":190,"content_sha256":"18b77f71fda51317f82d53fbdf1e0e1097d4ced50ea7d3d759aaa93e674ecb07"},{"filename":"aigc-claw/backend/prompts/logline/check_zh.txt","content":"请判断以下文本是否包含足够的叙事要素,能够从中总结出一个完整故事的 Logline。\n(完整 Logline 需涵盖:主角、目标、核心障碍、反转和主题)\n\n文本:{idea}\n\n只回答 Yes 或 No,不要添加任何其他内容。\n","content_type":"text/plain; charset=utf-8","language":null,"size":268,"content_sha256":"cd8beb0a4d84e4eaad9be6926da2ba171e1287228f603066a533ba646ad9afac"},{"filename":"aigc-claw/backend/prompts/logline/extract_zh.txt","content":"你是资深制片人。请从以下文本中总结提取 Logline 及故事五要素。\nLogline 请使用\"如果…会怎样\"的句式。\n\n文本:{idea}\n\n请严格按如下 JSON 格式输出(直接输出纯JSON,不要用```包裹,不要添加任何其他文字):\n{{\"logline\":\"如果...会怎样\",\"who\":\"主角描述\",\"goal\":\"目标\",\"conflict\":\"核心障碍\",\"twist\":\"反转\",\"theme\":\"潜在主题\"}}\n","content_type":"text/plain; charset=utf-8","language":null,"size":413,"content_sha256":"c1f035356cbb66d673489eecd75b9e252a604b21484d71b0f4fa11f4683dc8b3"},{"filename":"aigc-claw/backend/prompts/logline/generate_en.txt","content":"You are a senior producer. Expand the following idea into 3 different Loglines.\nRequirements:\n- Define the protagonist (Who), Goal, core Conflict, and Twist\n- Identify the story's Theme\n- Each Logline should use 'What if...' format\n- The 3 Loglines should be diverse in style and focus\n\nInput: {idea}\n\nOutput ONLY a JSON array with exactly 3 elements (no code block markers, no other text):\n[{{\"logline\":\"What if...\",\"who\":\"protagonist\",\"goal\":\"goal\",\"conflict\":\"conflict\",\"twist\":\"twist\",\"theme\":\"theme\"}}]\n","content_type":"text/plain; charset=utf-8","language":null,"size":508,"content_sha256":"d004b7243b4ef856dda2b8a27b15512cfa77fab31bdfb4bde34fd42f0579ad80"},{"filename":"aigc-claw/backend/prompts/logline/generate_zh.txt","content":"你是资深制片人。请将以下灵感扩展为 3 个不同的 Logline(故事大纲)。\n要求:\n- 明确主角(Who)、目标(Goal)、核心障碍(Conflict)和反转(Twist)\n- 确定故事的 Theme(潜在主题)\n- Logline 按照\"如果…会怎样\"的句式描述\n- 3 个 Logline 应风格各异、各有侧重\n\n输入内容:{idea}\n\n请严格按如下 JSON 数组格式输出(直接输出纯JSON,不要用```包裹,不要添加任何其他文字):\n[{{\"logline\":\"如果...会怎样\",\"who\":\"主角描述\",\"goal\":\"目标\",\"conflict\":\"核心障碍\",\"twist\":\"反转\",\"theme\":\"潜在主题\"}}]\n输出恰好 3 个元素的 JSON 数组。\n","content_type":"text/plain; charset=utf-8","language":null,"size":671,"content_sha256":"8c6a0f660caed7084aac88b5a7fab0a0a7e3a73a45ae09aa4622082a449d25eb"},{"filename":"aigc-claw/backend/prompts/reference/eval_first_frame_zh.txt","content":"你是一个专业的AI图像质量评估师。请根据以下信息评估生成的视频首帧参考图。\n\n分镜剧情:\n{plot}\n\n分镜视觉描述:\n{visual_prompt}\n\n角色描述:\n{character_description}\n\n场景描述:\n{setting_description}\n\n请严格按照以下标准评估并给出1-10分:\n\n【评分标准】\n满分10分,每个检查项符合得1分,不符合扣1分,最高10分:\n\n1. 角色外观一致性(+1/-1):角色发型、服装、面孔是否与角色描述一致\n2. 场景一致性(+1/-1):场景地点、布局是否与场景描述一致\n3. 剧情相关性(+1/-1):图片是否体现分镜剧情的核心内容和情感\n4. 姿态合理性(+1/-1):角色姿态是否体现动作的起始瞬间\n5. 光线质量(+1/-1):光线是否自然、合理、有层次\n6. 色调氛围(+1/-1):色调是否符合分镜设定的氛围(日/夜、暖/冷等)\n7. 构图美感(+1/-1):构图是否平衡、美观、符合影视镜头语言\n8. 图片质量(+1/-1):无模糊、无畸变、无明显瑕疵\n9. 细节完整(+1/-1):画面细节丰富,无缺失或残缺\n10. 无无关元素(+1/-1):无文字、水印、标签等无关内容\n\n【扣分项】(发现以下问题必须扣分并在issues中列出):\n- 角色服装颜色与描述不符:-1分\n- 角色发型/面孔与描述不符:-1分\n- 场景环境与描述不符:-1分\n- 角色数量错误(多或少):-1分\n- 光线过暗或过亮:-1分\n- 构图严重偏斜:-1分\n- 出现文字/水印:-1分\n\n【最终评分规则】\n- 8分及以上:可接受(is_acceptable: true)\n- 7分及以下:需要重新生成(is_acceptable: false)\n\n重要:如果发现问题需要重新生成,请同时提供一个优化后的提示词,用于改善图片质量。优化提示词时:\n- **不要改变角色的外貌描述**(如发型、服装、面孔等)\n- 可以让光线、构图、氛围、姿态等描述更清晰具体\n- 保持与角色外观相关的描述不变\n\n请输出纯JSON格式(不要用```包裹):\n{{\n \"score\": 1-10,\n \"issues\": [\"问题1\", \"问题2\", ...],\n \"is_acceptable\": true/false,\n \"suggestion\": \"修改建议(如果有问题)\",\n \"suggested_prompt\": \"优化后的提示词(如果需要重新生成),如果不需重新生成则为空字符串\"\n}}\n\n如果图片可接受,issues 返回空数组 [],suggested_prompt 为空字符串。\n","content_type":"text/plain; charset=utf-8","language":null,"size":2432,"content_sha256":"ed7bf082fde412d24df0186d737a3d8b2fc3e1f524bd784f183493eab65a482e"},{"filename":"aigc-claw/backend/prompts/reference/eval_select_best_zh.txt","content":"你是一个专业的AI图像质量评估师。请从以下{num_images}张图片中选择最好的一张作为视频参考图。\n\n分镜剧情:\n{plot}\n\n分镜视觉描述:\n{visual_prompt}\n\n角色描述:\n{character_description}\n\n场景描述:\n{setting_description}\n\n{images_list}\n\n【选择标准】\n请严格按照以下标准选择最佳图片:\n\n1. 角色外观一致性:角色发型、服装、面孔是否与角色描述一致\n2. 场景一致性:场景地点、布局是否与场景描述一致\n3. 剧情相关性:图片是否体现分镜剧情的核心内容和情感\n4. 姿态合理性:角色姿态是否体现动作的起始瞬间\n5. 光线质量:光线是否自然、合理、有层次\n6. 色调氛围:色调是否符合分镜设定的氛围(日/夜、暖/冷等)\n7. 构图美感:构图是否平衡、美观、符合影视镜头语言\n8. 图片质量:无模糊、无畸变、无明显瑕疵\n9. 细节完整:画面细节丰富,无缺失或残缺\n10. 无无关元素:无文字、水印、标签等无关内容\n\n【排除原则】\n以下情况必须排除:\n- 角色服装颜色与描述严重不符\n- 角色发型/面孔与描述严重不符\n- 场景环境与描述严重不符\n- 角色数量错误(多或少)\n- 光线过暗或过亮导致无法看清内容\n- 构图严重偏斜导致主体不清晰\n- 出现文字/水印/标签等无关元素\n\n请输出纯JSON格式(不要用```包裹):\n{{\n \"selected_index\": 0到{num_images_minus_1}之间的整数,\n \"reason\": \"选择该图片的主要原因\",\n \"score\": 1-10,\n \"issues\": [\"该图片存在的问题列表,如果完美则为空数组\"]\n}}\n\nselected_index 必须是从 0 到 {num_images_minus_1} 的整数,表示你选择的图片编号。","content_type":"text/plain; charset=utf-8","language":null,"size":1749,"content_sha256":"8ebf7a4be5dce08507816ead4ab6c8ab94df9ba720c61c13caa7d39b32b23563"},{"filename":"aigc-claw/backend/prompts/reference/first_frame_en.txt","content":"You are a professional AI image prompt engineer.\nI need to generate a **first-frame reference image** for a {duration}-second video clip.\n\nShot plot description:\n{plot}\n\nShot visual description:\n{visual_prompt}\n\nRequirements:\n1. Output a pure English image generation prompt for an AI image model\n2. Describe the **first frame** (static image) of the video, capturing the starting moment\n3. Must include: character appearance/pose/expression, environment, lighting/color tone, composition/shot type\n4. For {duration}s video: {duration_hint}\n5. 80-150 English words\n6. Output ONLY the prompt text, no explanations or markers\n","content_type":"text/plain; charset=utf-8","language":null,"size":624,"content_sha256":"94a7ae14278f6a27f25a0557bf9e4f9ee1d7a3a5f23912b121624f3ccc85247d"},{"filename":"aigc-claw/backend/prompts/reference/first_frame_zh.txt","content":"你是一位专业的AI绘画提示词工程师。\n我需要为一段{duration}秒的视频生成**首帧参考图**。\n\n分镜剧情描述:\n{plot}\n\n分镜视觉描述:\n{visual_prompt}\n\n要求:\n1. 输出一段纯英文的图像生成提示词(image prompt),用于AI绘图模型\n2. 描述的是视频**第一帧**的静态画面,要体现动作的起始瞬间\n3. 必须包含:角色外貌/姿态/表情、场景环境、光线/色调、构图/景别\n4. {duration}秒视频:{duration_hint}\n5. 字数控制在80-150个英文单词\n6. 只输出提示词文本,不要任何解释或标记\n","content_type":"text/plain; charset=utf-8","language":null,"size":600,"content_sha256":"04f132957f9f22dff6d2af84fefd4d64c78422c663908dcd8d9a66321e99bddd"},{"filename":"aigc-claw/backend/prompts/script/beat_sheet_en.txt","content":"You are a Hollywood screenwriting mentor. Break down the following storyline into a 4-act structure using the Save the Cat! Beat Sheet.\n\nMust include these four key beats:\nAct 1 - Inciting Incident: Establish the world and protagonist's status quo, then a disruptive event\nAct 2 - Break into Two: Protagonist embarks on journey, faces challenges, B-story unfolds\nAct 3 - Dark Night of the Soul: Protagonist suffers the biggest blow, hits rock bottom\nAct 4 - Finale: Protagonist gains epiphany, final confrontation, story resolves\n\nEnsure: tight logic, escalating conflict, clear turning points in each act.\n\nStoryline:\n{draft}\n\nStory style: {style}\n\nOutput the 4-act beat sheet directly. Start each act with '[Act X - Name]' heading. Detail the plot points, character development and key turning points for each act.\n","content_type":"text/plain; charset=utf-8","language":null,"size":817,"content_sha256":"cc571db8cfd48bb6bcd5de762230f561985bfbe670298e5265aca714f8e98c8c"},{"filename":"aigc-claw/backend/prompts/script/beat_sheet_zh.txt","content":"你是好莱坞编剧导师。请根据以下故事线,使用 Save the Cat! 节拍表将故事拆解为四幕结构。\n\n必须包含以下四个关键节拍(起承转合):\n第一幕 - 激励事件(Inciting Incident):建立世界观和主角现状,发生打破平衡的事件\n第二幕 - 进入新世界(Break into Two):主角踏上旅程,面对挑战和考验,副线展开\n第三幕 - 灵魂黑夜(Dark Night of the Soul):主角遭受最大打击,陷入低谷\n第四幕 - 高潮决战(Finale):主角获得顿悟,最终决战,故事收束\n\n请确保:逻辑严密、冲突逐步升级、每一幕有清晰的转折点。\n\n故事线:\n{draft}\n\n故事风格:{style}\n\n请直接输出四幕节拍表,每一幕用\"【第X幕 - 名称】\"标记开始,详细描述该幕的情节要点、角色发展和关键转折。\n","content_type":"text/plain; charset=utf-8","language":null,"size":863,"content_sha256":"4e2302c349a88306f6545081079f730b2c33991a19c884ae25ef0c7a36108948"},{"filename":"aigc-claw/backend/prompts/script/micro_beat_sheet_en.txt","content":"You are a micro-film screenwriting expert. Compress the following storyline into a compact single-act plot summary.\n\nRequirements:\n- Fast narrative pacing, concise and tight plot\n- All content in a single act, no act divisions\n- Keep core conflict and emotional turns, remove unnecessary setup\n- 3-6 scenes total\n- Suitable for a 1-3 minute short film\n\nStoryline:\n{draft}\n\nStory style: {style}\n\nOutput a compact plot summary directly, describing scene progression, core actions and emotional shifts.\n","content_type":"text/plain; charset=utf-8","language":null,"size":500,"content_sha256":"564f7f2d005176fbe5c1f06eea8d1ba5048a9c34e72f61e0e14f98e967097f09"},{"filename":"aigc-claw/backend/prompts/script/micro_beat_sheet_zh.txt","content":"你是微电影编剧专家。请根据以下故事线,将故事压缩为一个紧凑的单幕剧情概要。\n\n要求:\n- 叙事节奏快,情节紧凑精炼,没有拖沓的铺垫\n- 全部内容在一幕内完成,不分幕\n- 保留核心冲突和情感转折,去掉多余叙事\n- 场景数量控制在 3-6 场\n- 适合 1-3 分钟的微电影\n\n故事线:\n{draft}\n\n故事风格:{style}\n\n请直接输出紧凑的剧情概要,描述场景发展、核心动作和情感转折。\n","content_type":"text/plain; charset=utf-8","language":null,"size":490,"content_sha256":"d7280a4c9c33329144558700a3d9e55f297cd21dbc7e29cddab17f3080322bb9"},{"filename":"aigc-claw/backend/prompts/script/micro_step_outline_zh.txt","content":"你是微电影编剧专家。请根据以下故事线,将故事压缩为一个紧凑的单幕剧情概要。\n\n{draft}\n\n故事风格:{style}\n\n请将其转化为详细的分场大纲。\n\n格式要求(每场一段):\n[场次编号]. [地点(室内/室外)] - [日/夜]\n[核心动作]:详细描述该场戏发生了什么,包含完整的对话、动作和表情描写。\n[情感转变]:描述主角在本场戏开始到结束的情绪变化(+/-)。\n[出场角色]:列出本场出现的所有角色。\n\n要求:\n- 每个角色都要有详细的外貌描写(发型、眼睛颜色、体型、服装颜色和款式等视觉特征)\n- **重要:外貌描写必须是静态的、贯穿全剧保持一致的特征,不要随剧情发展而变化**\n - 例如:不要写\"他穿着破碎的衣服\"这种随情节变化的描写\n - 应该写:\"他身穿蓝色衬衫,黑色长裤\"这种固定的服装描述\n- 每个场景都要有详细的环境、布局、色彩与氛围描写\n- 分场数量根据情节长度和节奏自行决定,确保叙事节奏合理\n- 对话用双引号标注,对话内容真实生动\n- 场次编号从 1 开始递增\n\n请直接输出分场大纲。\n","content_type":"text/plain; charset=utf-8","language":null,"size":1198,"content_sha256":"9ee4d110b0db2874386b611b26e79830ff8f2e4dfbdb0e880b4a67dbc81df420"},{"filename":"aigc-claw/backend/prompts/script/step_outline_en.txt","content":"You are a scene director. Here is the complete 4-act beat sheet:\n\n{beat_sheet}\n\nConvert Act {act_number} ({act_name}) into a detailed step outline.\n\nFormat for each scene:\n[Scene number]. [Location (Indoor/Outdoor)] - [Day/Night]\n[Core Action]: Detailed description of what happens, including dialogue, actions and expressions.\n[Emotional Shift]: Describe the protagonist's emotional change from start to end (+/-).\n[Characters Present]: List all characters appearing in this scene.\n\nRequirements:\n- Each character needs detailed physical descriptions (hair, eyes, build, clothing details)\n- **IMPORTANT: Physical descriptions must be STATIC and consistent throughout the entire story - do NOT change with plot development**\n - For example: do NOT write \"wearing torn clothes\" which changes with the plot\n - Instead write: \"wearing a blue shirt and black pants\" which is a fixed clothing description\n- Each scene needs detailed environment, layout, color and atmosphere descriptions\n- Number of scenes based on plot length and pacing\n- Dialogue marked with double quotes, natural and vivid\n- Scene numbers start from {scene_start} and increment\n- Story style: {style}\n\nOutput the step outline directly, without act headings or extra notes.\n","content_type":"text/plain; charset=utf-8","language":null,"size":1242,"content_sha256":"3da66dad192fbc81c2959df33c152d8eaca3417009865b32a0218dc06239f918"},{"filename":"aigc-claw/backend/prompts/script/step_outline_zh.txt","content":"你是分场导演。以下是完整的四幕节拍表:\n\n{beat_sheet}\n\n请将第{act_number}幕({act_name})转化为详细的分场大纲。\n\n格式要求(每场一段):\n[场次编号]. [地点(室内/室外)] - [日/夜]\n[核心动作]:详细描述该场戏发生了什么,包含完整的对话、动作和表情描写。\n[情感转变]:描述主角在本场戏开始到结束的情绪变化(+/-)。\n[出场角色]:列出本场出现的所有角色。\n\n要求:\n- 每个角色都要有详细的外貌描写(发型、眼睛颜色、体型、服装颜色和款式等视觉特征)\n- **重要:外貌描写必须是静态的、贯穿全剧保持一致的特征,不要随剧情发展而变化**\n - 例如:不要写\"他穿着破碎的衣服\"这种随情节变化的描写\n - 应该写:\"他身穿蓝色衬衫,黑色长裤\"这种固定的服装描述\n- 每个场景都要有详细的环境、布局、色彩与氛围描写\n- 分场数量根据情节长度和节奏自行决定,确保叙事节奏合理\n- 对话用双引号标注,对话内容真实生动\n- 场次编号从 {scene_start} 开始递增\n- 故事风格:{style}\n\n请直接输出分场大纲,不要添加幕次标题或额外说明。\n","content_type":"text/plain; charset=utf-8","language":null,"size":1235,"content_sha256":"d10920e8b1cc4fa7ed82068e080cc85e5034973563af11a65d52befacf85c616"},{"filename":"aigc-claw/backend/prompts/script/validate_characters_zh.txt","content":"你是专业的剧本分析师和AI绘画提示词工程师。请审查以下角色列表,识别需要修改或删除的角色。\n\n角色列表:\n{characters_json}\n\n审查标准:\n1. 群体角色(如\"邻居们\"、\"群众\"、\"乘客们\"、\"学生们\")应该删除,改为在场景描述中用一句话表示(如\"车厢里坐满了乘客\")\n2. 角色描述必须包含足够的视觉细节用于AI图像生成:\n - 外貌:发型、眼睛颜色、面部特征、体型\n - 服装:颜色、款式、材质配饰:帽子\n - 、眼镜、珠宝等\n3. 避免抽象比喻(如\"看起来很凶\"),要具体描述\n\n请输出纯JSON(不要用```包裹),格式如下:\n{{\n \"characters_to_remove\": [\"需要删除的角色名1\", \"角色名2\", ...],\n \"characters_to_fix\": [\n {{\n \"name\": \"角色名\",\n \"issue\": \"问题描述\",\n \"fix_suggestion\": \"修改建议\"\n }}\n ]\n}}\n\n如果没有需要删除的角色,characters_to_remove 返回空数组 []。\n如果没有需要修改的角色,characters_to_fix 返回空数组 []。\n只输出纯JSON,不要添加任何其他文字。\n","content_type":"text/plain; charset=utf-8","language":null,"size":1121,"content_sha256":"b4857d8c026fe122d9b0e95efc322498a9982e9dcd9204142b8b83f5144e0890"},{"filename":"aigc-claw/backend/prompts/script/validate_settings_zh.txt","content":"你是专业的剧本分析师和AI绘画提示词工程师。请审查以下场景列表,识别需要修改的场景。\n\n场景列表:\n{settings_json}\n\n审查标准:\n1. 场景描述必须包含足够的视觉细节用于AI图像生成:\n - 空间布局:室内/室外、主要物体位置\n - 光线:自然光/人工光、光源方向、亮度\n - 色彩:主色调、氛围\n - 构图:景别、视角\n2. 场景应该是具体的、可视觉化的地点\n3. 避免过于笼统的描述(如\"某个地方\")\n\n请输出纯JSON(不要用```包裹),格式如下:\n{{\n \"settings_to_fix\": [\n {{\n \"name\": \"场景名\",\n \"issue\": \"问题描述\",\n \"fix_suggestion\": \"修改建议\"\n }}\n ]\n}}\n\n如果没有需要修改的场景,settings_to_fix 返回空数组 []。\n只输出纯JSON,不要添加任何其他文字。\n","content_type":"text/plain; charset=utf-8","language":null,"size":859,"content_sha256":"9ababe6167a2377244289ab20e5678886fc6fc9067b6bb28307bf9ed0c89bc43"},{"filename":"aigc-claw/backend/prompts/setting/eval_select_best_zh.txt","content":"你是一个专业的AI图像质量评估师。请从以下{num_images}张场景设计图中选择最好的一张。\n\n场景名称:{setting_name}\n场景描述:{setting_description}\n\n图片列表:\n{images_list}\n\n请严格按照以下标准评估并选择最好的一张:\n\n【评分标准】\n满分10分,每个检查项符合得1分,不符合扣1分,最高10分:\n\n1. 地点一致性(+1/-1):场景地点是否与描述一致\n2. 布局还原(+1/-1):场景布局、物品摆放是否与描述一致\n3. 光线氛围(+1/-1):光线是否与描述一致(日/夜、暖/冷)\n4. 色彩还原(+1/-1):场景色彩是否与描述一致\n5. 风格一致性(+1/-1):整体风格是否符合设定(室内/室外、年代感等)\n6. 细节完整(+1/-1):细节是否丰富,无明显缺失\n7. 空间感(+1/-1):空间透视是否合理\n8. 构图质量(+1/-1):构图是否均衡、美观\n9. 图片质量(+1/-1):清晰度良好,无模糊、无畸变\n10. 无无关元素(+1/-1):无文字、水印、标签等无关内容\n\n【最终选择规则】\n- 选择分数最高的一张作为最终选择\n- 如果最高分有多张相同,选择第一张\n\n请输出纯JSON格式(不要用```包裹):\n{{\n \"scores\": [\n {{\"image_index\": 0, \"score\": 8, \"reasons\": [\"原因1\", \"原因2\"]}},\n {{\"image_index\": 1, \"score\": 6, \"reasons\": [\"原因1\"]}},\n ...\n ],\n \"best_index\": 0,\n \"best_reason\": \"选择这张的原因\"\n}}","content_type":"text/plain; charset=utf-8","language":null,"size":1484,"content_sha256":"afb89d4ea2ae8aa81a600165bf040e3434724391472a546367c9fd0547a7aabf"},{"filename":"aigc-claw/backend/prompts/setting/eval_setting_zh.txt","content":"你是一个专业的AI图像质量评估师。请根据以下信息评估生成的场景参考图。\n\n场景描述:\n{setting_description}\n\n请严格按照以下标准评估并给出1-10分:\n\n【评分标准】\n满分10分,每个检查项符合得1分,不符合扣1分,最高10分:\n\n1. 地点一致性(+1/-1):场景地点是否与描述一致\n2. 布局还原(+1/-1):场景布局、物品摆放是否与描述一致\n3. 光线氛围(+1/-1):光线是否与描述一致(日/夜、暖/冷)\n4. 色彩还原(+1/-1):场景色彩是否与描述一致\n5. 风格一致性(+1/-1):整体风格是否符合设定(室内/室外、年代感等)\n6. 细节完整(+1/-1):细节是否丰富,无明显缺失\n7. 空间感(+1/-1):空间透视是否合理\n8. 构图质量(+1/-1):构图是否均衡、美观\n9. 图片质量(+1/-1):清晰度良好,无模糊、无畸变\n10. 无无关元素(+1/-1):无文字、水印、标签等无关内容\n\n【扣分项】(发现以下问题必须扣分并在issues中列出):\n- 场景地点与描述不符:-1分\n- 光线氛围与描述不符:-1分\n- 出现不需要的元素:-1分\n- 构图严重偏斜:-1分\n- 出现文字/水印:-1分\n\n【最终评分规则】\n- 8分及以上:可接受(is_acceptable: true)\n- 7分及以下:需要重新生成(is_acceptable: false)\n\n请输出纯JSON格式(不要用```包裹):\n{{\n \"score\": 1-10,\n \"issues\": [\"问题1\", \"问题2\", ...],\n \"is_acceptable\": true/false,\n \"suggestion\": \"修改建议(如果有问题)\"\n}}\n\n如果图片可接受,issues 返回空数组 []。\n","content_type":"text/plain; charset=utf-8","language":null,"size":1639,"content_sha256":"ed0790b865421929957add179fc7d9f4a201b711e2b79bbb9992266248ec7402"},{"filename":"aigc-claw/backend/prompts/setting/setting_styles.txt","content":"# Setting Scene Prompt Templates\n# 场景背景图提示词模板(按风格分类)\n# 使用方式:根据 {style} 变量选择对应的模板\n\n# ============================================================\n# realistic - 写实摄影风格\n# ============================================================\n[realistic]\nPhotorealistic landscape photography, actual photograph style.\nWide establishing shot, eye-level view, panoramic perspective.\nNatural lighting, golden hour or blue hour atmosphere, realistic textures.\nNo characters, no people, no animals in frame.\nCinematic composition, movie scene quality, 8K resolution.\nHigh quality photography, sharp focus, wide angle lens, f/8, professional camera.\nTrue-to-life environment, detailed textures, atmospheric depth.\nNO text, NO labels, NO watermarks on image.\nNOT an illustration, NOT a drawing, NOT anime style.\nScene: {name} — {desc}\n\n# ============================================================\n# anime - 动漫风格\n# ============================================================\n[anime]\nAnime style landscape illustration, clean vector art aesthetic.\nWide establishing shot, eye-level view, panoramic perspective.\nVibrant colors, cel-shaded rendering, anime background art style.\nNo characters, no people, no animals in frame.\nCinematic composition, high quality anime illustration, masterpiece.\nCrisp details, atmospheric background, anime movie quality.\nNO text, NO labels, NO watermarks on image.\nScene: {name} — {desc}\n\n# ============================================================\n# comic-book - 漫画风格\n# ============================================================\n[comic-book]\nComic book style landscape, comic panel art aesthetic.\nWide establishing shot, eye-level view, dynamic perspective.\nBold ink lines, comic shading (hatching/cross-hatching), vibrant colors.\nNo characters, no people, no animals in frame.\nComic book art style, dramatic lighting, comic panel composition.\nHigh quality comic illustration, vibrant ink colors, graphic novel aesthetic.\nNO text, NO speech bubbles, NO watermarks on image.\nScene: {name} — {desc}\n\n# ============================================================\n# 3d-disney - 迪士尼3D风格\n# ============================================================\n[3d-disney]\n3D CGI animated movie style landscape, Disney Pixar aesthetic.\nWide establishing shot, eye-level view, smooth 3D geometry.\nToon shader rendering, soft shadows, cinematic lighting, feature film quality.\nNo characters, no people, no animals in frame.\nDisney Pixar CGI quality, clean 3D environment, volumetric lighting.\nHigh quality 3D render, animated movie background, toon shading.\nNO text, NO labels, NO watermarks on image.\nScene: {name} — {desc}\n\n# ============================================================\n# watercolor - 水彩风格\n# ============================================================\n[watercolor]\nWatercolor painting style landscape, painterly art aesthetic.\nWide establishing shot, eye-level view, soft perspective.\nFlowing color washes, delicate brush strokes, translucent layers.\nNo characters, no people, no animals in frame.\nWatercolor art style, organic color blending, artistic masterpiece.\nSoft texture, artistic render, traditional painting aesthetic.\nNO text, NO labels, NO watermarks on image.\nScene: {name} — {desc}\n\n# ============================================================\n# oil-painting - 油画风格\n# ============================================================\n[oil-painting]\nOil painting style landscape, classical fine art aesthetic.\nWide establishing shot, eye-level view, classical composition.\nRich oil paint texture, impasto brushwork, classical portraiture technique.\nNo characters, no people, no animals in frame.\nOil painting art style, dramatic chiaroscuro lighting, museum-quality masterpiece.\nDeep colors, detailed brushwork, traditional canvas texture.\nNO text, NO labels, NO watermarks on image.\nScene: {name} — {desc}\n\n# ============================================================\n# cyberpunk - 赛博朋克风格\n# ============================================================\n[cyberpunk]\nCyberpunk style landscape, futuristic sci-fi aesthetic.\nWide establishing shot, eye-level view, dystopian perspective.\nNeon lighting, chrome/metallic surfaces, holographic elements, RGB accents.\nNo characters, no people, no animals in frame.\nCyberpunk cityscape, high-tech environment, volumetric fog.\nHigh quality cyberpunk illustration, sci-fi concept art, futuristic aesthetic.\nNO text, NO HUD elements, NO watermarks on image.\nScene: {name} — {desc}\n\n# ============================================================\n# chinese-ink - 中国水墨风格\n# ============================================================\n[chinese-ink]\nChinese ink painting (水墨画) style landscape, traditional East Asian art aesthetic.\nWide establishing shot, eye-level view, traditional Chinese composition.\nInk wash (墨色) rendering, Gongbi/Boneless brush technique, subtle ink gradients.\nNo characters, no people, no animals in frame.\nChinese landscape painting style, rice paper texture, traditional scroll art.\nHigh quality ink painting, elegant brushwork, classical Chinese aesthetics.\nNO text, NO seals/stamps, NO watermarks on image.\nScene: {name} — {desc}","content_type":"text/plain; charset=utf-8","language":null,"size":5294,"content_sha256":"954f38c7eaf285f712bd5b6c49c6bdcc6fc0c03b2bcc362cec79e0c0812a142e"},{"filename":"aigc-claw/backend/prompts/setting/setting_zh.txt","content":"Panoramic landscape, eye-level wide shot, {style} style,\nno characters, no people, no animals,\ncinematic composition, establishing shot,\nhigh quality, masterpiece, Movie picture quality.\nScene: {name} — {desc}\n","content_type":"text/plain; charset=utf-8","language":null,"size":212,"content_sha256":"142b49db6e75418f7c7e4f52611de601c0bb7a9895b517dbb5e6bd5a3901573d"},{"filename":"aigc-claw/backend/prompts/storyboard/continue_zh.txt","content":"你是一位专业的影视编剧。根据已有的剧情内容,续写接下来的故事情节。\n\n已有剧情信息:\n- 标题:{title}\n- 风格:{style}\n- 角色:{characters}\n- 场景位置:{settings}\n\n当前分镜列表:\n{existing_shots}\n\n请根据以上信息,续写1个新场景(包含多个分镜,总时长40-60秒)。\n\n要求:\n1. 新场景要承接上文剧情自然发展\n2. 分镜时长为 5、10 或 15 秒\n3. 包含4-8个分镜,总时长控制在40-60秒\n4. 每个分镜描述一个独立的画面段落\n5. plot 字段必须按时间顺序描述:角色动作、表情、对话(用引号标注)、背景音效\n6. visual_prompt 字段是适合AI图像生成的视觉描述,包含角色站位、姿态、表情、环境光线,色调、构图\n7. 保持{style}风格\n8. 用角色全名,不要用代词\n9. location 字段必须从上面给出的\"场景位置\"中选择一个,名称必须完全一致,不能有任何修改或添加\n\n请直接输出JSON数组(不要用```包裹),每个元素格式如下:\n[{{\"shot_number\":1,\"duration\":5,\"characters\":[\"角色名\"],\"location\":\"场景位置\",\"plot\":\"按时间顺序描述: 角色动作、表情、对话、背景音效,50-100字\",\"visual_prompt\":\"AI图像生成用的视觉描述,含站位、姿态、表情,光线、构图,80-120字\"}}]\n\n注意:\n- duration只能是5、10、15三个值之一\n- 新场景的 scene_number 应该延续上一个场景的编号\n- 新场景应该延续当前幕(act)的内容,或者开启新一幕(如果故事需要)\n- 新场景中出现的人物、场景位置必须与上面给出的完全一致,禁止添加额外的人物或场景位置\n","content_type":"text/plain; charset=utf-8","language":null,"size":1681,"content_sha256":"855894f1ff06207c6a81e831a56036ba936b9b4f853060be99e092a69bc8dadd"},{"filename":"aigc-claw/backend/prompts/storyboard/shot_zh.txt","content":"你是一位专业的影视分镜师。根据以下场景信息,将该场景拆分为若干分镜(shot)。\n\n要求:\n1. 每个分镜时长为 5、10 或 15 秒(根据内容复杂度选择)\n2. 每个分镜描述一个独立的画面段落(一个动作/一句对话/一个反应)\n3. plot 字段必须按时间顺序描述:角色动作、表情、对话(用引号标注)、背景音效\n4. visual_prompt 字段是适合AI图像生成的视觉描述,包含角色站位、姿态、表情、环境光线、色调、构图\n5. 保持{style}风格\n6. 用角色全名,不要用代词\n\n场景编号:{scene_number}\n场景位置:{location}\n出场角色:{characters}\n场景剧情:{plot}\n\n角色外貌参考:\n{char_descriptions}\n\n场景环境参考:\n{setting_description}\n\n请直接输出JSON数组(不要用```包裹),每个元素格式如下:\n[{{\"shot_number\":1,\"duration\":5,\"characters\":[\"角色名\"],\"location\":\"场景位置\",\"plot\":\"按时间顺序描述: 角色动作、表情、对话、背景音效,50-100字\",\"visual_prompt\":\"AI图像生成用的视觉描述,含站位、姿态、表情、光线、构图,80-120字\"}}]\n\n注意:duration只能是5、10、15三个值之一。\n","content_type":"text/plain; charset=utf-8","language":null,"size":1216,"content_sha256":"9a08c8d364adaca0cffe5674fe76581869c6f5442ef008a59a3e04a74b28db5b"},{"filename":"aigc-claw/backend/prompts/video/enhance.txt","content":"# 视频生成提示词优化模板\n# 在基础提示词前后添加这些内容来提升生成质量\n# 使用方法:从 prompts/loader import load_prompt('video', 'enhance', 'zh')\n\n[prefix]\nhigh quality, 4K, detailed, cinematic footage, film style, smooth motion, natural movement, professional camera work,\n\n[suffix]\n, realistic, high definition, no blur, no distortion, no artifacts, perfect composition, professional lighting, film grain, masterpiece\n\n# 风格关键词(会自动从会话中读取项目风格并添加对应关键词)\n[style_keywords]\nanime: anime style, animated, cel-shaded, vibrant colors, manga aesthetic,\ncartoon: cartoon style, animated, colorful, fun, children's book illustration,\nrealistic: photorealistic, realistic, natural lighting, detailed textures, cinema photography,\nphotorealistic: photorealistic, realistic, natural lighting, detailed textures, cinema photography,\n3d-disney: 3D animation, Disney style, pixar, CGI, smooth textures, computer generated,\n3d: 3D animation, CGI, computer generated, smooth textures, digital cinema,\noil-painting: oil painting style, impasto, classical art, painterly, rich brushstrokes,\nwatercolor: watercolor style, delicate, soft colors, artistic, flowing,\ncomic-book: comic book style, vibrant, bold outlines, pop art, graphic novel,\ncyberpunk: cyberpunk, neon lights, futuristic, dark atmosphere, sci-fi,\nchinese-ink: Chinese ink painting style, traditional, minimalist, brush strokes, oriental art,\nink: Chinese ink painting style, traditional, minimalist, brush strokes, oriental art,\n","content_type":"text/plain; charset=utf-8","language":null,"size":1567,"content_sha256":"60cad69a13e42653510b0b6aa8a3b8e1a56dfd765b9a17e6fe4e5d3f8db77a7e"},{"filename":"aigc-claw/backend/requirements.txt","content":"# MovieAssistant Backend Dependencies\n# Python 3.9+\n\n# Web Framework\nfastapi>=0.100.0\nuvicorn>=0.23.0\n\n# Data Validation\npydantic>=2.0.0\n\n# Environment Variables\npython-dotenv>=1.0.0\n\n# HTTP Clients\nrequests>=2.28.0\nhttpx>=0.24.0\n\n# Image Processing\nPillow>=10.0.0\nnumpy>=1.24.0\n\n# AI/ML Clients\nopenai>=1.0.0\ndashscope>=1.10.0\n\n# Video Generation\nPyJWT>=2.8.0\n","content_type":"text/plain; charset=utf-8","language":null,"size":361,"content_sha256":"b053d11b8c78465e45e7b48676493c02efd5ee6cd11e0c02bff76aa6ddd842c8"},{"filename":"aigc-claw/backend/session.py","content":"# -*- coding: utf-8 -*-\nimport json\nimport os\nimport time\n\nclass SessionManager:\n \"\"\"Manages chat sessions persistence\"\"\"\n def __init__(self, data_dir=\"code/data\"):\n self.data_dir = data_dir\n if not os.path.exists(data_dir):\n os.makedirs(data_dir)\n\n def _get_file(self, session_id):\n return os.path.join(self.data_dir, f\"{session_id}.json\")\n\n def list_sessions(self):\n \"\"\"List all sessions ordered by modification time\"\"\"\n sessions = []\n if not os.path.exists(self.data_dir):\n return sessions\n\n files = [f for f in os.listdir(self.data_dir) if f.endswith('.json')]\n for f in files:\n try:\n path = os.path.join(self.data_dir, f)\n with open(path, 'r', encoding='utf-8') as fs:\n data = json.load(fs)\n # title, id, last_updated\n sessions.append({\n \"id\": data.get(\"id\"),\n \"title\": data.get(\"title\", \"Untitled\"),\n \"date\": \"7days\", # Simplification. Real logic would calc date diff\n \"timestamp\": os.path.getmtime(path)\n })\n except Exception:\n continue\n \n # Sort by timestamp desc\n sessions.sort(key=lambda x: x['timestamp'], reverse=True)\n return sessions\n\n def get_session(self, session_id):\n \"\"\"Get full history of a session\"\"\"\n path = self._get_file(session_id)\n if os.path.exists(path):\n try:\n with open(path, 'r', encoding='utf-8') as f:\n return json.load(f)\n except Exception:\n pass\n return None\n\n def save_session(self, session_id, title, messages, asset_library=None):\n \"\"\"Save or update session\"\"\"\n data = {\n \"id\": session_id,\n \"title\": title,\n \"last_updated\": time.time(),\n \"messages\": messages,\n \"asset_library\": asset_library or {}\n }\n with open(self._get_file(session_id), 'w', encoding='utf-8') as f:\n json.dump(data, f, indent=2, ensure_ascii=False)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2209,"content_sha256":"0b66a502e3cbb24521bea489435db49d16a1f8fb64d3cfeabe3b48fbd255e8fa"},{"filename":"aigc-claw/backend/tool/image_client.py","content":"import os\nimport time\nimport uuid\nimport logging\nfrom typing import List, Optional\nfrom config import Config\n\ntry:\n from tool.image_dashscope import DashScopeClient\n from tool.image_jimeng import JiMengClient\n from tool.image_seedream import SeedreamClient\n from tool.image_gpt import ImageGPT\n from tool.image_processor import ImageProcessor\nexcept ImportError:\n from image_dashscope import DashScopeClient\n from image_jimeng import JiMengClient\n from image_seedream import SeedreamClient\n from image_gpt import ImageGPT\n from image_processor import ImageProcessor\n\nclass ImageClient:\n def __init__(self,\n dashscope_api_key: Optional[str] = None,\n dashscope_base_url: Optional[str] = None,\n jimeng_base_url: Optional[str] = None,\n jimeng_access_key: Optional[str] = None,\n jimeng_secret_key: Optional[str] = None,\n gpt_api_key: Optional[str] = None,\n gpt_base_url: Optional[str] = None,\n gpt_official_api_key: Optional[str] = None,\n local_proxy: Optional[str] = None,\n ark_api_key: Optional[str] = None,\n ark_base_url: Optional[str] = None):\n \"\"\"\n Unified Image Generation Client\n Routes requests to DashScope, JiMeng, Seedream, or GPT based on model name.\n \"\"\"\n # Initialize DashScope Client\n self.dashscope_client = DashScopeClient(\n api_key=dashscope_api_key,\n base_url=dashscope_base_url\n )\n\n # Initialize JiMeng Client\n self.jimeng_client = JiMengClient(\n base_url=jimeng_base_url,\n access_key=jimeng_access_key,\n secret_key=jimeng_secret_key\n )\n\n # Initialize Seedream Client\n self.seedream_client = SeedreamClient(\n api_key=ark_api_key,\n base_url=ark_base_url\n )\n\n # Initialize GPT Image Client\n self.gpt_client = ImageGPT(\n api_key=gpt_api_key,\n base_url=gpt_base_url,\n official_api_key=gpt_official_api_key or '',\n local_proxy=local_proxy or ''\n )\n\n # Initialize Image Processor for downloads\n self.image_processor = ImageProcessor()\n\n # Default save directory\n self.base_save_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), \"code\", \"result\", \"image_client\")\n\n def generate_image(self, \n prompt: str, \n image_paths: Optional[List[str]] = None, \n model: str = \"wan2.6-t2i\",\n save_dir: Optional[str] = None,\n session_id: Optional[str] = None,\n size: Optional[str] = \"1920*1080\") -> List[str]:\n \"\"\"\n Generate images based on prompt and optional reference images.\n \n Args:\n prompt: Text prompt for generation.\n image_paths: List of local file paths or URLs for reference images.\n model: Model name to determine which provider to use.\n save_dir: Custom directory to save downloaded images.\n session_id: Session ID for organizing saved files (especially for JiMeng)\n size: Desired size of the generated image, e.g., \"1024*1024\" or \"1920*1080\".\n \n Returns:\n List of absolute file paths of the generated images.\n \"\"\"\n if not model:\n model = \"wan2.6-t2i\"\n\n if Config.PRINT_MODEL_INPUT:\n print(\"---- IMAGE GENERATION REQUEST ----\")\n print(f\"Prompt: {prompt}\")\n if image_paths:\n print(f\"Refs: {len(image_paths)}\")\n for p in image_paths:\n if str(p).startswith(\"data:\"):\n print(f\" - [Base64图片]\")\n else:\n print(f\" - {p}\")\n print(f\"Model: {model}\")\n if session_id:\n print(f\"Session ID: {session_id}\")\n print(\"-\" * 30)\n \n # Determine backend provider\n is_jimeng = \"jimeng\" in model.lower()\n is_seedream = \"seedream\" in model.lower()\n is_sora = \"sora\" in model.lower() or \"gpt\" in model.lower()\n \n # Prepare save directory\n if not save_dir:\n if session_id:\n save_dir = os.path.join(self.base_save_dir, session_id)\n else:\n save_dir = self.base_save_dir\n os.makedirs(save_dir, exist_ok=True)\n \n generated_local_paths = []\n\n if is_jimeng:\n # --- JiMeng Logic ---\n try:\n # JiMengClient handles local paths and typically saves results to code/result internally\n # or we rely on its return value.\n # JiMengClient.generate_image returns list of local paths (saved from base64)\n logging.info(f\"ImageClient requesting JiMeng: {model}\")\n paths = self.jimeng_client.generate_image(\n prompt=prompt,\n image_paths=image_paths if image_paths else [],\n model=model,\n session_id=session_id,\n size=size\n )\n \n # If JiMengClient saves to a default location, we might want to move them or just return them.\n # The provided JiMengClient saves to 'backend/code/result'.\n # We simply return those paths.\n generated_local_paths.extend(paths)\n \n except Exception as e:\n logging.error(f\"JiMeng generation failed: {e}\")\n\n elif is_seedream:\n # --- Seedream Logic ---\n try:\n logging.info(f\"ImageClient requesting Seedream: {model}\")\n\n paths = self.seedream_client.generate_image(\n prompt=prompt,\n model=model,\n session_id=session_id or \"default\",\n size=size or \"2048*2048\",\n image_paths=image_paths\n )\n\n if paths:\n generated_local_paths.extend(paths)\n\n except Exception as e:\n logging.error(f\"Seedream generation failed: {e}\")\n\n elif is_sora:\n # --- GPT/Sora Logic ---\n try:\n logging.info(f\"ImageClient requesting GPT/Sora: {model}\")\n if image_paths:\n logging.warning(\"Sora/GPT model only supports Text-to-Image. Ignoring reference images.\")\n \n # OpenAI uses 'x' separator, e.g. 1024x1024\n # Attempt to map size if needed or just replace '*'\n gpt_size = size.replace('*', 'x') if size else \"1024x1024\"\n\n path = self.gpt_client.generate_image(\n prompt=prompt,\n size=gpt_size,\n model=model,\n save_dir=save_dir\n )\n \n if path and os.path.exists(path):\n generated_local_paths.append(path)\n else:\n logging.error(f\"GPT/Sora returned invalid path or download failed: {path}\")\n\n except Exception as e:\n logging.error(f\"GPT/Sora generation failed: {e}\")\n\n else:\n # --- DashScope Logic ---\n try:\n logging.info(f\"ImageClient requesting DashScope: {model}\")\n\n if image_paths and len(image_paths) > 0:\n # Pre-process image paths for DashScope\n # Convert local paths to file:// URIs if they aren't already URLs\n # DashScope SDK (via MultiModalConversation) handles file://\n formatted_urls = []\n for p in image_paths:\n if p.startswith(\"http\") or p.startswith(\"file://\"):\n formatted_urls.append(p)\n else:\n abs_path = os.path.abspath(p)\n formatted_urls.append(f\"file://{abs_path}\")\n \n paths = self.dashscope_client.edit_image(\n prompt=prompt,\n image_urls=formatted_urls,\n model=model,\n size=size,\n session_id=session_id,\n save_dir=save_dir\n )\n else:\n # Text to Image\n # Assuming default size 1024*1024 or similar\n paths = self.dashscope_client.generate_image(\n prompt=prompt,\n model=model,\n size=size,\n session_id=session_id,\n save_dir=save_dir\n )\n \n if paths:\n generated_local_paths.extend(paths)\n \n except Exception as e:\n logging.error(f\"DashScope generation failed: {e}\")\n\n return generated_local_paths\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9279,"content_sha256":"24a5a4c5251b4754e9561ebf6bf6ecd80a87acd80c1bf4cb23bbfc29e995e396"},{"filename":"aigc-claw/backend/tool/image_dashscope.py","content":"import os\nimport json\nimport logging\nimport time\nimport uuid\nimport dashscope\nfrom dashscope import MultiModalConversation\nfrom dashscope.aigc.image_generation import ImageGeneration\ntry:\n from tool.image_processor import ImageProcessor\nexcept ImportError:\n from image_processor import ImageProcessor\n\nclass DashScopeClient:\n def __init__(self, api_key=None, base_url=None):\n self.api_key = api_key or os.getenv(\"DASHSCOPE_API_KEY\")\n # 默认使用中国(北京)地域 API,如果环境变量或参数未设置则使用默认地址\n self.base_url = base_url or os.getenv(\"DASHSCOPE_BASE_URL\")\n dashscope.api_key = self.api_key\n dashscope.base_http_api_url = self.base_url\n self.image_processor = ImageProcessor()\n\n def generate_image(self, prompt, model=\"wan2.6-t2i\", size=\"1024*1024\", n=1, session_id=None, save_dir=None):\n \"\"\"\n Text to Image generation using DashScope\n \"\"\"\n try:\n messages = [{\"role\": \"user\", \"content\": [{\"text\": prompt}]}]\n response = ImageGeneration.call(\n model=model,\n api_key=self.api_key,\n messages=messages,\n n=n,\n size=size,\n watermark=False,\n )\n\n if response.status_code == 200:\n results = []\n try:\n # 标准 ImageGeneration 返回结果解析\n if response.output and response.output.choices:\n for item in response.output.choices:\n if 'message' in item and 'content' in item['message']:\n results.append(item['message']['content'][0]['image'])\n except Exception as e:\n logging.error(f\"Failed to parse ImageGeneration outputs: {e}\")\n \n # Check if we should download\n if save_dir:\n os.makedirs(save_dir, exist_ok=True)\n local_files = []\n for i, url in enumerate(results):\n file_name = f\"ds_{session_id if session_id else 'nosess'}_{int(time.time())}_{i}_{uuid.uuid4().hex[:6]}.png\"\n file_path = os.path.join(save_dir, file_name)\n if self.image_processor.download_image(url, file_path):\n local_files.append(file_path)\n return local_files\n \n return results\n else:\n logging.error(f\"Image generation failed: {response.code}, {response.message}, status={response.status_code}\")\n return []\n except Exception as e:\n logging.error(f\"Error in generate_image (DashScope): {e}\")\n return []\n\n def edit_image(self, prompt, image_urls, model=\"wan2.6-image\", size=\"1920*1080\", n=1, session_id=None, save_dir=None):\n \"\"\"\n Image editing/compositing using DashScope ImageGeneration\n \"\"\"\n # Prepare content\n content_list = []\n for img_url in image_urls:\n content_list.append({\"image\": img_url})\n content_list.append({\"text\": prompt})\n\n messages = [\n {\n \"role\": \"user\",\n \"content\": content_list\n }\n ]\n\n try:\n # Use ImageGeneration.call with messages, same as generate_image\n response = ImageGeneration.call(\n model=model,\n api_key=self.api_key,\n messages=messages,\n n=n,\n size=size,\n watermark=False,\n )\n\n if response.status_code == 200:\n results = []\n try:\n # 标准 ImageGeneration 返回结果解析\n if response.output and response.output.choices:\n for item in response.output.choices:\n # 简化解析逻辑以处理多张图片的返回结构\n if isinstance(item, dict):\n if 'image' in item: # 部分新模型直接返回 {'image': 'url', 'finish_reason': ...}\n results.append(item['image'])\n elif 'url' in item:\n results.append(item['url'])\n elif 'message' in item and 'content' in item['message']: # 兼容 Message 结构\n content = item['message']['content']\n if isinstance(content, list):\n for c in content:\n if isinstance(c, dict) and 'image' in c:\n results.append(c['image'])\n except Exception as e:\n logging.error(f\"Failed to parse ImageGeneration outputs: {e}\")\n\n # Check if we should download\n if save_dir:\n os.makedirs(save_dir, exist_ok=True)\n local_files = []\n for i, url in enumerate(results):\n file_name = f\"ds_{session_id if session_id else 'nosess'}_{int(time.time())}_{i}_{uuid.uuid4().hex[:6]}.png\"\n file_path = os.path.join(save_dir, file_name)\n if self.image_processor.download_image(url, file_path):\n local_files.append(file_path)\n return local_files\n\n return results\n else:\n logging.error(f\"Image edit failed: {response.code}, {response.message}, status={response.status_code}\")\n return []\n except Exception as e:\n logging.error(f\"Error in edit_image: {e}\")\n return []\n\n\nif __name__ == \"__main__\":\n import sys\n import tempfile\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n print(\"=== DashScope 图片生成可用性测试 ===\")\n api_key = Config.DASHSCOPE_API_KEY\n base_url = Config.DASHSCOPE_BASE_URL\n if not api_key:\n print(\"✗ DASHSCOPE_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***{api_key[-4:]}\")\n print(f\" Base URL: {base_url}\")\n client = DashScopeClient(api_key=api_key, base_url=base_url)\n prompt = \"一只橘猫躺在阳光下的窗台上,水彩画风格\"\n print(f\"Prompt: {prompt}\")\n with tempfile.TemporaryDirectory() as tmp:\n t0 = time.time()\n try:\n paths = client.generate_image(\n prompt=prompt, model=\"wan2.6-t2i\",\n size=\"1024*1024\", save_dir=tmp,\n )\n elapsed = time.time() - t0\n if paths:\n print(f\"✓ 生成 {len(paths)} 张图片 ({elapsed:.1f}s): {paths}\")\n else:\n print(f\"✗ 返回空列表 ({elapsed:.1f}s)\")\n except Exception as e:\n print(f\"✗ 失败: {e}\")\n sys.exit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7163,"content_sha256":"51d9492c0a2b757ea58255ee3e1d94022926a0426df92b37f69c1fa7da8ac387"},{"filename":"aigc-claw/backend/tool/image_gpt.py","content":"import os\nimport time\nimport uuid\nimport base64\nimport httpx\nfrom openai import OpenAI\ntry:\n from tool.image_processor import ImageProcessor\nexcept ImportError:\n from image_processor import ImageProcessor\n\n\nclass ImageGPT:\n \"\"\"\n OpenAI 图片生成客户端\n 支持模型:\n - sora_image → Images API(通过代理/中转服务器)\n - gpt-image-1.5 → Responses API(直连官方,走本地代理)\n \"\"\"\n def __init__(self, base_url=\"\", api_key=\"\", timeout=300,\n official_api_key=\"\", local_proxy=\"\"):\n # 普通客户端(可连接代理/中转服务器,用于 sora_image 等)\n if base_url:\n self.client = OpenAI(api_key=api_key, base_url=base_url, timeout=timeout)\n else:\n self.client = OpenAI(api_key=api_key, timeout=timeout)\n self.max_attempts = 10\n self.image_processor = ImageProcessor()\n\n # 官方直连客户端(仅 gpt-image-1.5 等需要直连官方 API 的模型使用)\n self._official_client = None\n self._official_api_key = official_api_key\n self._local_proxy = local_proxy\n self._timeout = timeout\n\n def _get_official_client(self):\n \"\"\"懒加载官方 OpenAI 客户端,通过本地代理访问\"\"\"\n if self._official_client is None:\n if not self._official_api_key:\n raise ValueError(\"使用 gpt-image-1.5 需要设置 OPENAI_OFFICIAL_API_KEY(官方 OpenAI API Key)\")\n kwargs = {\"timeout\": self._timeout}\n if self._local_proxy:\n kwargs[\"http_client\"] = httpx.Client(\n proxy=self._local_proxy,\n timeout=self._timeout,\n )\n self._official_client = OpenAI(api_key=self._official_api_key, **kwargs)\n return self._official_client\n\n def generate_image(self, prompt, size=\"1024x1024\", quality=\"standard\", model=None,\n save_dir=None, image_urls=None):\n \"\"\"Generate a single image, download it, and return the local file path.\n\n Args:\n prompt: 图片描述提示词\n size: 图片尺寸\n quality: 图片质量\n model: 模型名称 (sora_image / gpt-image-1.5)\n save_dir: 保存目录(不传则返回 URL 或 base64)\n image_urls: 参考图片 URL 列表(仅 gpt-image-1.5 支持)\n \"\"\"\n if model is None:\n model = \"sora_image\"\n\n # gpt-image-1.5 走官方 Responses API\n if model == \"gpt-image-1.5\":\n return self._generate_image_official(prompt, size=size, quality=quality,\n save_dir=save_dir, image_urls=image_urls)\n\n # 其他模型走普通 Images API\n return self._generate_image_legacy(prompt, size=size, quality=quality,\n model=model, save_dir=save_dir)\n\n def _generate_image_official(self, prompt, size=\"1024x1024\", quality=\"standard\",\n save_dir=None, image_urls=None):\n \"\"\"通过官方 Responses API 生成图片 (gpt-image-1.5),走本地代理\"\"\"\n client = self._get_official_client()\n\n # 构建 input content\n content = [{\"type\": \"input_text\", \"text\": prompt}]\n if image_urls:\n for url in image_urls:\n content.append({\"type\": \"input_image\", \"image_url\": url})\n\n attempts = 0\n last_error = None\n while attempts \u003c self.max_attempts:\n try:\n response = client.responses.create(\n model=\"gpt-image-1.5\",\n input=[{\"role\": \"user\", \"content\": content}],\n tools=[{\"type\": \"image_generation\", \"size\": size, \"quality\": quality}],\n )\n\n # 从 output 中提取 image_generation_call 结果\n image_data = [\n output.result for output in response.output\n if output.type == \"image_generation_call\"\n ]\n\n if image_data:\n b64 = image_data[0]\n if save_dir:\n os.makedirs(save_dir, exist_ok=True)\n file_name = f\"gptimg_{int(time.time())}_{uuid.uuid4().hex[:6]}.png\"\n file_path = os.path.join(save_dir, file_name)\n with open(file_path, \"wb\") as f:\n f.write(base64.b64decode(b64))\n return file_path\n else:\n return b64 # 返回 base64 字符串\n else:\n text_output = \" \".join(\n getattr(o, \"text\", \"\") for o in response.output if hasattr(o, \"text\")\n ).strip()\n print(f\"gpt-image-1.5: 未返回图片。模型回复: {text_output[:200]}\")\n except Exception as e:\n last_error = e\n print(f\"gpt-image-1.5 Error: {e}. Retrying in 10s.\")\n time.sleep(10)\n attempts += 1\n\n raise Exception(f\"gpt-image-1.5: 达到最大重试次数。Last error: {last_error}\")\n\n def _generate_image_legacy(self, prompt, size=\"1024x1024\", quality=\"standard\",\n model=\"sora_image\", save_dir=None):\n \"\"\"通过 Images API 生成图片 (sora_image 等)\"\"\"\n # Fallback chain: user's choice -> sora_image\n models_to_try = [model, \"sora_image\"]\n models_to_try = list(dict.fromkeys(models_to_try)) # Remove duplicates\n\n attempts = 0\n last_error = None\n while attempts \u003c self.max_attempts:\n for m in models_to_try:\n try:\n response = self.client.images.generate(\n model=m,\n prompt=prompt,\n size=size,\n quality=quality,\n n=1,\n )\n if response and response.data and response.data[0].url:\n url = response.data[0].url\n if save_dir:\n os.makedirs(save_dir, exist_ok=True)\n file_name = f\"sora_{int(time.time())}_{uuid.uuid4().hex[:6]}.png\"\n file_path = os.path.join(save_dir, file_name)\n if self.image_processor.download_image(url, file_path):\n return file_path\n else:\n print(f\"Failed to save image from {url}\")\n else:\n return url\n except Exception as e:\n last_error = e\n msg = str(e)\n # Model not found or no distributor: try next model\n if \"model_not_found\" in msg or \"无可用渠道\" in msg or \"distributor\" in msg:\n continue\n # Other errors: wait before retry\n print(f\"Image generation error: {e}. Retrying in 10 seconds.\")\n time.sleep(10)\n break # Break inner loop to retry all models\n attempts += 1\n raise Exception(f\"Max attempts reached, failed to generate image. Last error: {last_error}\")\n\n def generate_images(self, prompt, count=4, size=\"1024x1024\", quality=\"standard\", model=None):\n \"\"\"Generate multiple image URLs by calling Images API 'count' times.\"\"\"\n urls = []\n for _ in range(count):\n url = self.generate_image(prompt=prompt, size=size, quality=quality, model=model)\n urls.append(url)\n return urls\n\n\nif __name__ == \"__main__\":\n import sys\n import tempfile\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n print(\"=== GPT 图片生成可用性测试 ===\")\n api_key = Config.OPENAI_API_KEY\n base_url = Config.OPENAI_BASE_URL\n official_key = Config.OPENAI_OFFICIAL_API_KEY\n local_proxy = Config.LOCAL_PROXY\n\n if not api_key and not official_key:\n print(\"✗ OPENAI_API_KEY 和 OPENAI_OFFICIAL_API_KEY 均未设置,跳过\")\n sys.exit(1)\n\n print(f\" API Key: {api_key[:6]}***{api_key[-4:] if api_key else '(未设置)'}\")\n print(f\" Base URL: {base_url}\")\n print(f\" Official Key: {official_key[:6]}***{official_key[-4:] if official_key else '(未设置)'}\")\n print(f\" Local Proxy: {local_proxy or '(未设置)'}\")\n\n client = ImageGPT(api_key=api_key, base_url=base_url,\n official_api_key=official_key, local_proxy=local_proxy)\n\n # 1. sora_image 图片生成(仅尝试 1 次)\n if api_key:\n img_prompt = \"一只橘猫躺在阳光下的窗台上\"\n print(f\"\\n[1/2 sora_image] Prompt: {img_prompt}\")\n client.max_attempts = 1\n t0 = time.time()\n try:\n with tempfile.TemporaryDirectory() as tmp:\n path = client.generate_image(prompt=img_prompt, size=\"1024x1024\", model=\"sora_image\", save_dir=tmp)\n elapsed = time.time() - t0\n print(f\"✓ 生成成功 ({elapsed:.1f}s): {path}\")\n except Exception as e:\n elapsed = time.time() - t0\n print(f\"✗ 图片生成失败 ({elapsed:.1f}s): {e}\\n (该代理可能不支持 sora_image 模型)\")\n else:\n print(\"\\n[1/2 sora_image] 跳过(OPENAI_API_KEY 未设置)\")\n\n # 2. gpt-image-1.5 图片生成(官方 API + 本地代理)\n if official_key:\n img_prompt = \"A cute orange cat lying on a sunny windowsill, watercolor style\"\n print(f\"\\n[2/2 gpt-image-1.5] Prompt: {img_prompt}\")\n client.max_attempts = 1\n t0 = time.time()\n try:\n with tempfile.TemporaryDirectory() as tmp:\n path = client.generate_image(prompt=img_prompt, size=\"1024x1024\",\n model=\"gpt-image-1.5\", save_dir=tmp)\n elapsed = time.time() - t0\n print(f\"✓ 生成成功 ({elapsed:.1f}s): {path}\")\n except Exception as e:\n elapsed = time.time() - t0\n print(f\"✗ gpt-image-1.5 失败 ({elapsed:.1f}s): {e}\")\n else:\n print(\"\\n[2/2 gpt-image-1.5] 跳过(OPENAI_OFFICIAL_API_KEY 未设置)\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10434,"content_sha256":"a6889c8d7d79e71c180a2bd05b9c9e400c3e1e8a9a61a970fe19a6932fed3bcf"},{"filename":"aigc-claw/backend/tool/image_jimeng.py","content":"\"\"\"\n即梦 Ti2V (Text+Image to Video) API 客户端\n火山引擎视觉智能服务 - jimeng_ti2v_v30_pro 模型\n\"\"\"\n\nimport os\nimport time\nimport base64\nimport json\nimport logging\nimport io\nfrom typing import Optional, Dict, Any\nimport requests\nfrom PIL import Image\n\n\nclass JiMengClient:\n \"\"\"\n 即梦图生视频客户端(火山引擎)\n 支持基于文本+图片生成视频的功能\n \"\"\"\n\n # API操作类型\n SUBMIT_ACTION = \"CVSync2AsyncSubmitTask\" # 提交异步任务\n RESULT_ACTION = \"CVSync2AsyncGetResult\" # 获取异步任务结果\n API_VERSION = \"2022-08-31\" # API版本号\n\n def __init__(\n self,\n base_url: Optional[str] = None,\n access_key: Optional[str] = None,\n secret_key: Optional[str] = None,\n timeout: int = 120,\n poll_interval: int = 2,\n max_polls: int = 60,\n ) -> None:\n \"\"\"\n 初始化即梦客户端\n\n Args:\n timeout: HTTP请求超时时间(秒)\n poll_interval: 轮询间隔(秒)\n max_polls: 最大轮询次数\n \"\"\"\n self.base_url = base_url or os.getenv(\"VOLC_BASE_URL\")\n self.access_key = access_key or os.getenv(\"VOLC_ACCESS_KEY\")\n self.secret_key = secret_key or os.getenv(\"VOLC_SECRET_KEY\")\n self.timeout = timeout\n self.poll_interval = poll_interval\n self.max_polls = max_polls\n self.region = \"cn-north-1\" # 火山引擎区域\n self.service = \"cv\" # 服务名称:计算机视觉\n\n if not self.access_key or not self.secret_key:\n logging.warning(\n \"JiMengClient missing access_key/secret_key. Set VOLC_ACCESS_KEY and VOLC_SECRET_KEY.\"\n )\n\n def _headers(self, method: str, path: str, query: str, body: str) -> Dict[str, str]:\n \"\"\"\n 生成带火山引擎签名v4的请求头\n \n Args:\n method: HTTP方法(如 POST)\n path: 请求路径\n query: 查询字符串\n body: 请求体\n \n Returns:\n 包含认证信息的请求头字典\n \"\"\"\n import hashlib\n import hmac\n from datetime import datetime, timezone\n \n if not self.access_key or not self.secret_key:\n return {\"Content-Type\": \"application/json\"}\n \n # 生成时间戳\n now = datetime.now(timezone.utc)\n timestamp = now.strftime('%Y%m%dT%H%M%SZ')\n date_stamp = now.strftime('%Y%m%d')\n \n # 计算请求体的SHA256哈希\n payload_hash = hashlib.sha256(body.encode('utf-8')).hexdigest()\n \n # 构建规范请求(Canonical Request)\n canonical_uri = path\n canonical_querystring = query\n host = self.base_url.split(\"//\")[1]\n signed_headers = 'content-type;host;x-content-sha256;x-date'\n canonical_headers = (\n f'content-type:application/json\\n'\n f'host:{host}\\n'\n f'x-content-sha256:{payload_hash}\\n'\n f'x-date:{timestamp}\\n'\n )\n \n canonical_request = (\n f'{method}\\n{canonical_uri}\\n{canonical_querystring}\\n'\n f'{canonical_headers}\\n{signed_headers}\\n{payload_hash}'\n )\n \n # 构建待签名字符串(String to Sign)\n algorithm = 'HMAC-SHA256'\n credential_scope = f'{date_stamp}/{self.region}/{self.service}/request'\n string_to_sign = (\n f'{algorithm}\\n{timestamp}\\n{credential_scope}\\n'\n f'{hashlib.sha256(canonical_request.encode(\"utf-8\")).hexdigest()}'\n )\n \n # 生成签名密钥(Signing Key)\n def sign(key, msg):\n return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()\n \n k_date = sign(self.secret_key.encode('utf-8'), date_stamp)\n k_region = sign(k_date, self.region)\n k_service = sign(k_region, self.service)\n k_signing = sign(k_service, 'request')\n \n # 计算签名\n signature = hmac.new(k_signing, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()\n \n # 构建Authorization请求头\n authorization_header = (\n f'{algorithm} Credential={self.access_key}/{credential_scope}, '\n f'SignedHeaders={signed_headers}, Signature={signature}'\n )\n \n return {\n 'Content-Type': 'application/json',\n 'X-Date': timestamp,\n 'X-Content-Sha256': payload_hash,\n 'Authorization': authorization_header\n }\n\n def _submit_url(self) -> str:\n \"\"\"构建提交任务的URL\"\"\"\n return f\"{self.base_url}?Action={self.SUBMIT_ACTION}&Version={self.API_VERSION}\"\n\n def _result_url(self) -> str:\n \"\"\"构建查询结果的URL\"\"\"\n return f\"{self.base_url}?Action={self.RESULT_ACTION}&Version={self.API_VERSION}\"\n\n @staticmethod\n def _encode_image_to_base64(image_path: str, quality: int = 80) -> str:\n \"\"\"\n 将本地图片压缩并编码为base64字符串(仅转换格式和压缩质量,不调整尺寸)\n \n Args:\n image_path: 图片文件路径\n quality: JPEG压缩质量\n \n Returns:\n base64编码的图片字符串\n \"\"\"\n try:\n with Image.open(image_path) as img:\n # 转换为RGB(去除PNG的Alpha通道,兼容JPEG)\n if img.mode in (\"RGBA\", \"P\"):\n img = img.convert(\"RGB\")\n \n # 保存为JPEG字节流\n buffer = io.BytesIO()\n img.save(buffer, format=\"JPEG\", quality=quality)\n return base64.b64encode(buffer.getvalue()).decode(\"utf-8\")\n except Exception as e:\n logging.warning(f\"Image compression failed for {image_path}, falling back to raw read: {e}\")\n # 降级方案:直接读取原文件\n with open(image_path, \"rb\") as f:\n return base64.b64encode(f.read()).decode(\"utf-8\")\n \n @staticmethod\n def download_video(video_url: str, save_path: str, timeout: int = 60) -> None:\n \"\"\"\n 从URL下载视频文件到本地\n \n Args:\n video_url: 视频文件URL\n save_path: 本地保存路径\n timeout: 下载超时时间(秒)\n \"\"\"\n with requests.get(video_url, stream=True, timeout=timeout) as r:\n r.raise_for_status()\n os.makedirs(os.path.dirname(save_path), exist_ok=True)\n with open(save_path, \"wb\") as f:\n for chunk in r.iter_content(chunk_size=8192):\n if chunk:\n f.write(chunk)\n \n def poll_task(self, model: str, task_id: str) -> Dict[str, Any]:\n \"\"\"\n 轮询任务直到完成或失败\n \n Args:\n model: 模型key\n task_id: 任务ID\n \n Returns:\n 任务结果数据字典,包含 video_url 等信息\n \n Raises:\n RuntimeError: 当API Key未设置或任务失败时\n TimeoutError: 当超过最大轮询次数时\n \"\"\"\n if not self.access_key or not self.secret_key:\n raise RuntimeError(\"VOLC_ACCESS_KEY and VOLC_SECRET_KEY not set; cannot call JiMeng Ti2V API.\")\n\n for attempt in range(self.max_polls):\n payload = {\"req_key\": model, \"task_id\": task_id}\n body = json.dumps(payload)\n query = f\"Action={self.RESULT_ACTION}&Version={self.API_VERSION}\"\n headers = self._headers(\"POST\", \"/\", query, body)\n \n # 查询任务状态\n response = requests.post(\n self._result_url(),\n data=body,\n headers=headers,\n timeout=self.timeout,\n )\n response.raise_for_status()\n data = response.json()\n \n # 检查返回码\n if data.get(\"code\") != 10000:\n raise RuntimeError(f\"GetResult failed: {data}\")\n\n status = data.get(\"data\", {}).get(\"status\")\n \n # 任务完成\n if status == \"done\":\n return data[\"data\"]\n \n # 任务进行中,继续等待\n if status in {\"in_queue\", \"generating\"}:\n time.sleep(self.poll_interval)\n continue\n \n # 任务失败或过期\n if status in {\"not_found\", \"expired\"}:\n raise RuntimeError(f\"Task {task_id} {status}\")\n\n raise TimeoutError(f\"Polling timeout for task {task_id}\")\n\n def generate_video(\n self,\n prompt: Optional[str],\n image_path: Optional[str],\n seed: int = -1,\n frames: int = 121,\n aspect_ratio: str = \"16:9\",\n ) -> str:\n \"\"\"\n 提交视频生成任务\n \n Args:\n prompt: 文本提示词(最多800字符)\n image_path: 输入图片的本地路径\n seed: 随机种子,-1表示随机\n frames: 生成视频的帧数,默认121帧\n aspect_ratio: 视频宽高比,默认 \"16:9\"\n \n Returns:\n task_id: 任务ID,用于后续查询结果\n \n Raises:\n RuntimeError: 当API Key未设置或任务提交失败时\n \"\"\"\n if not self.access_key or not self.secret_key:\n raise RuntimeError(\"VOLC_ACCESS_KEY and VOLC_SECRET_KEY not set; cannot call JiMeng Ti2V API.\")\n\n # 构建请求载荷\n payload: Dict[str, Any] = {\n \"req_key\": self.req_key,\n \"seed\": seed,\n \"frames\": frames,\n \"aspect_ratio\": aspect_ratio,\n }\n\n # 添加文本提示词\n if prompt:\n payload[\"prompt\"] = prompt\n\n # 添加base64编码的图片数据\n if image_path:\n payload[\"binary_data_base64\"] = [self._encode_image_to_base64(image_path)]\n\n body = json.dumps(payload)\n query = f\"Action={self.SUBMIT_ACTION}&Version={self.API_VERSION}\"\n headers = self._headers(\"POST\", \"/\", query, body)\n \n # 发送请求\n response = requests.post(\n self._submit_url(),\n data=body,\n headers=headers,\n timeout=self.timeout,\n )\n response.raise_for_status()\n data = response.json()\n \n # 检查返回码\n if data.get(\"code\") != 10000:\n raise RuntimeError(f\"Submit failed: {data}\")\n \n task_id = data[\"data\"][\"task_id\"]\n return task_id\n\n def _save_base64_images(self, b64_list: list, session_id: str) -> list:\n \"\"\"保存Base64图片到本地\"\"\"\n image_urls = []\n # 定位到 backend/code/result\n # 当前文件在 backend/tool/jimeng_api.py\n base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n \n # 直接使用 session_id 构建路径\n result_dir = os.path.join(base_dir, \"code\", \"result\", \"image\", str(session_id))\n os.makedirs(result_dir, exist_ok=True)\n\n for idx, b64_str in enumerate(b64_list):\n file_name = f\"jimeng_{int(time.time())}_{idx}.png\"\n file_path = os.path.join(result_dir, file_name)\n \n try:\n with open(file_path, \"wb\") as f:\n f.write(base64.b64decode(b64_str))\n image_urls.append(file_path)\n except Exception as e:\n logging.error(f\"Failed to save base64 image: {e}\")\n \n return image_urls\n\n def generate_image(self,\n prompt: str,\n session_id: str,\n image_paths: list = [],\n model: str = \"jimeng_t2i_v40\",\n size: str = \"1920*1080\",\n **kwargs) -> list:\n \"\"\"\n 生成图片\n \n Args:\n prompt: 提示词\n session_id: 任务或会话ID,用于构建存储路径\n image_paths: 参考图路径或URL列表 (最多10张)\n model: 模型 req_key\n size: 生成图片的分辨率 \"1024*1024\" or \"1920*1080\".\n **kwargs: 其他生成参数 (size, width, height, scale, force_single, min_ratio, max_ratio)\n \n Returns:\n 生成的图片路径或URL列表\n \"\"\"\n if not self.access_key or not self.secret_key:\n raise RuntimeError(\"VOLC_ACCESS_KEY and VOLC_SECRET_KEY not set.\")\n \n # 处理图片分辨率,长宽比\n width = int(size.split(\"*\")[0]) if size else None\n height = int(size.split(\"*\")[1]) if size else None\n size = width * height if width and height else None\n\n # 1. 构造请求参数\n payload = {\n \"req_key\": model,\n \"prompt\": prompt,\n \"size\": size,\n \"width\": width,\n \"height\": height,\n }\n \n # 处理可选参数\n valid_keys = [\"width\", \"height\", \"scale\", \"force_single\", \"min_ratio\", \"max_ratio\"]\n for key in valid_keys:\n if key in kwargs:\n payload[key] = kwargs[key]\n \n # 处理参考图\n if image_paths:\n img_urls = []\n img_b64s = []\n for p in image_paths:\n if p.startswith(\"http\") or p.startswith(\"https\"):\n img_urls.append(p)\n elif os.path.exists(p):\n img_b64s.append(self._encode_image_to_base64(p))\n \n if img_urls:\n payload[\"image_urls\"] = img_urls\n if img_b64s:\n payload[\"binary_data_base64\"] = img_b64s\n\n body = json.dumps(payload)\n query = f\"Action={self.SUBMIT_ACTION}&Version={self.API_VERSION}\"\n headers = self._headers(\"POST\", \"/\", query, body)\n \n # 2. 提交任务\n response = requests.post(\n self._submit_url(),\n data=body,\n headers=headers,\n timeout=self.timeout,\n )\n response.raise_for_status()\n response = response.json()\n if response.get(\"code\") != 10000:\n raise RuntimeError(f\"Jimeng Generate Failed: {response}\")\n data = response.get(\"data\", {})\n \n # 3. 处理异步任务\n task_id = data.get(\"task_id\", None)\n if not task_id:\n raise RuntimeError(f\"No task_id or binary data returned: {data}\")\n logging.info(f\"Jimeng image task submitted: {task_id}, waiting for result...\")\n # 复用 poll_task\n result = self.poll_task(model=model, task_id=task_id)\n return self._save_base64_images(result.get(\"binary_data_base64\", []), session_id=session_id)\n \nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config # 加载 .env\n\n print(\"=== 即梦 (JiMeng) 可用性测试 ===\")\n ak = os.getenv(\"VOLC_ACCESS_KEY\", \"\")\n sk = os.getenv(\"VOLC_SECRET_KEY\", \"\")\n base_url = os.getenv(\"VOLC_BASE_URL\", \"\")\n if not ak or not sk:\n print(\"✗ VOLC_ACCESS_KEY / VOLC_SECRET_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" Access Key: {ak[:6]}***{ak[-4:]}\")\n if base_url:\n print(f\" Base URL: {base_url}\")\n client = JiMengClient(access_key=ak, secret_key=sk, base_url=base_url)\n prompt = \"一只橘猫躺在阳光下的窗台上,水彩画风格\"\n print(f\"\\n[图片] Prompt: {prompt}\")\n t0 = time.time()\n try:\n paths = client.generate_image(\n prompt=prompt, session_id=\"test_avail\",\n model=\"jimeng_t2i_v40\", size=\"1024*1024\",\n )\n elapsed = time.time() - t0\n if paths:\n print(f\"✓ 生成 {len(paths)} 张图片 ({elapsed:.1f}s): {paths}\")\n else:\n print(f\"✗ 返回空列表 ({elapsed:.1f}s)\")\n except Exception as e:\n print(f\"✗ 图片生成失败: {e}\")\n ","content_type":"text/x-python; charset=utf-8","language":"python","size":16196,"content_sha256":"95ca225908192ce49a07064f9fd123ff3ca581c424f407c2410b949f8670e286"},{"filename":"aigc-claw/backend/tool/image_processor.py","content":"import os\nimport requests\nimport numpy as np\nfrom pathlib import Path\nfrom datetime import datetime, timedelta\nfrom PIL import Image\nimport logging\n\n\nclass ImageProcessor:\n \"\"\"\n 图片处理和上传集合类\n 支持:图片处理、分割、拼接,以及上传到阿里云OSS\n \"\"\"\n \n # 阿里云DashScope上传配置\n UPLOAD_API_URL = \"https://dashscope.aliyuncs.com/api/v1/uploads\"\n \n def __init__(self,\n image_path='',\n api_key: str = os.environ.get(\"AIGC_CLAW_API_KEY\", \"\"),\n model_name: str = \"wan2.6-i2v-flash\"):\n \"\"\"\n 初始化图片处理器\n \n Args:\n image_path: 图片文件路径(可选,用于处理已有图片)\n api_key: DashScope API Key(用于上传,可从环境变量 DASHSCOPE_API_KEY 读取)\n model_name: 模型名称,默认使用 wan2.6-i2v-flash\n \"\"\"\n # 图片处理部分\n if image_path != '':\n self.image_path = image_path\n self.image = Image.open(image_path)\n self.image_np = np.array(self.image)\n self.width, self.height = self.image_np.shape[1], self.image_np.shape[0]\n else:\n self.image_path = None\n self.image = None\n self.image_np = None\n self.width = None\n self.height = None\n \n # 上传功能部分\n self.api_key = api_key or os.getenv(\"DASHSCOPE_API_KEY\")\n self.model_name = model_name\n\n @staticmethod\n def check_column_white(column_pixels):\n \"\"\"检查列是否几乎全白\"\"\"\n is_almost_white = np.logical_or(column_pixels == 254, column_pixels == 255)\n white_pixels_ratio = np.mean(np.all(is_almost_white, axis=-1))\n return white_pixels_ratio >= 0.98 # 至少98%的像素为白色\n\n def find_white_section(self, start, end):\n \"\"\"查找指定范围内的白色区间\"\"\"\n white_sections = []\n in_white_section = False\n start_index = 0\n\n for col in range(start, end):\n column_pixels = self.image_np[:, col, :]\n if self.check_column_white(column_pixels):\n if not in_white_section:\n start_index = col\n in_white_section = True\n else:\n if in_white_section:\n white_sections.append((start_index, col))\n in_white_section = False\n\n if in_white_section:\n white_sections.append((start_index, end))\n\n return white_sections\n\n def split_image(self):\n \"\"\"将图片从中间分割为左右两部分\"\"\"\n start_col = self.width * 2 // 5\n end_col = self.width * 3 // 5\n white_sections = self.find_white_section(start_col, end_col)\n\n if white_sections:\n middle_section = white_sections[len(white_sections) // 2]\n mid_col = (middle_section[0] + middle_section[1]) // 2\n else:\n raise ValueError(\"No suitable white column found within the specified range\")\n\n left_box = (0, 0, mid_col, self.height)\n right_box = (mid_col, 0, self.width, self.height)\n left_image = self.image.crop(left_box)\n right_image = self.image.crop(right_box)\n\n save_dir, filename = os.path.split(self.image_path)\n base, extension = os.path.splitext(filename)\n\n left_image_path = os.path.join(save_dir, base + '_front' + extension)\n right_image_path = os.path.join(save_dir, base + '_back' + extension)\n left_image.save(left_image_path)\n right_image.save(right_image_path)\n\n return left_image_path, right_image_path\n \n def stitch_images(self, image_paths, output_path):\n \"\"\"拼接多张图片\"\"\"\n if not image_paths:\n raise ValueError(\"No image paths provided\")\n sample_image = Image.open(image_paths[0])\n single_width, single_height = sample_image.size\n num_images = len(image_paths)\n total_desired_width = single_width\n total_current_width = single_width * num_images\n total_width_to_cut = max(0, total_current_width - total_desired_width)\n width_to_cut_per_image = total_width_to_cut // num_images\n stitched_image = Image.new('RGB', (total_desired_width, single_height), \"white\")\n current_x = 0\n \n for path in image_paths:\n image = Image.open(path)\n if width_to_cut_per_image > 0:\n left_margin = width_to_cut_per_image // 2\n right_margin = image.width - width_to_cut_per_image + left_margin\n image = image.crop((left_margin, 0, right_margin, image.height))\n stitched_image.paste(image, (current_x, 0))\n current_x += image.width\n \n output_dir = os.path.dirname(output_path)\n if not os.path.exists(output_dir):\n os.makedirs(output_dir)\n stitched_image.save(output_path)\n return output_path\n \n def download_image(self, image_url, save_path, max_retries=3):\n \"\"\"\n 下载图片,带有重试机制和SSL错误处理\n \n Args:\n image_url: 图片URL\n save_path: 本地保存路径\n max_retries: 最大重试次数\n \"\"\"\n import time\n import urllib3\n \n urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n \n for attempt in range(max_retries):\n try:\n response = requests.get(\n image_url, \n timeout=(10, 30),\n stream=True,\n verify=True,\n headers={\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'\n }\n )\n \n if response.status_code == 200:\n with open(save_path, 'wb') as file:\n for chunk in response.iter_content(chunk_size=8192):\n if chunk:\n file.write(chunk)\n print(f\"✓ 图片下载成功: {save_path}\")\n return True\n else:\n print(f\"下载失败,状态码: {response.status_code}\")\n \n except requests.exceptions.SSLError as e:\n print(f\"SSL错误 (尝试 {attempt + 1}/{max_retries}): {str(e)[:100]}\")\n if attempt \u003c max_retries - 1:\n wait_time = (attempt + 1) * 2\n print(f\"等待 {wait_time} 秒后重试...\")\n time.sleep(wait_time)\n else:\n print(\"尝试禁用SSL验证重新下载...\")\n try:\n response = requests.get(\n image_url, \n timeout=(10, 30),\n stream=True,\n verify=False,\n headers={\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'\n }\n )\n if response.status_code == 200:\n with open(save_path, 'wb') as file:\n for chunk in response.iter_content(chunk_size=8192):\n if chunk:\n file.write(chunk)\n print(f\"✓ 图片下载成功(已禁用SSL验证): {save_path}\")\n return True\n except Exception as fallback_error:\n print(f\"禁用SSL验证后仍然失败: {fallback_error}\")\n raise\n \n except requests.exceptions.Timeout as e:\n print(f\"超时错误 (尝试 {attempt + 1}/{max_retries}): {e}\")\n if attempt \u003c max_retries - 1:\n time.sleep((attempt + 1) * 2)\n else:\n raise\n \n except Exception as e:\n print(f\"下载错误 (尝试 {attempt + 1}/{max_retries}): {e}\")\n if attempt \u003c max_retries - 1:\n time.sleep((attempt + 1) * 2)\n else:\n raise\n \n return False\n\n def resize_image(self, image_path):\n \"\"\"调整图片大小(添加顶部空白)\"\"\"\n original_image = Image.open(image_path)\n width, height = original_image.size\n top_blank_height = height // 2\n final_height = height + top_blank_height\n final_width = int(final_height * 5 / 3)\n new_image = Image.new(\"RGB\", (final_width, final_height), color=\"white\")\n left = (final_width - width) // 2\n top = top_blank_height\n new_image.paste(original_image, (left, top))\n new_image.save(image_path)\n return image_path\n\n def has_black_borders(self, image_path, threshold=10, black_limit=20):\n \"\"\"检查图片是否有黑色边框\"\"\"\n img = Image.open(image_path)\n pixels = img.load()\n width, height = img.size\n \n def is_black_pixel(pixel):\n return all(x \u003c= black_limit for x in pixel)\n \n # 检查顶部和底部边框\n for y in range(threshold):\n if all(is_black_pixel(pixels[x, y]) for x in range(width)):\n return True\n if all(is_black_pixel(pixels[x, height - 1 - y]) for x in range(width)):\n return True\n \n # 检查左右边框\n for x in range(threshold):\n if all(is_black_pixel(pixels[x, y]) for y in range(height)):\n return True\n if all(is_black_pixel(pixels[width - 1 - x, y]) for y in range(height)):\n return True\n \n return False\n\n # ===== 图片上传功能 =====\n \n def get_upload_policy(self):\n \"\"\"\n 获取文件上传凭证\n \n Returns:\n policy_data: 包含上传所需凭证的字典\n \n Raises:\n Exception: 获取上传凭证失败时\n \"\"\"\n if not self.api_key:\n raise RuntimeError(\"DASHSCOPE_API_KEY 未设置,无法使用图片上传服务\")\n \n headers = {\n \"Authorization\": f\"Bearer {self.api_key}\",\n \"Content-Type\": \"application/json\"\n }\n params = {\n \"action\": \"getPolicy\",\n \"model\": self.model_name\n }\n \n response = requests.get(self.UPLOAD_API_URL, headers=headers, params=params)\n if response.status_code != 200:\n raise Exception(f\"Failed to get upload policy: {response.text}\")\n \n return response.json()['data']\n \n def upload_file_to_oss(self, policy_data: dict, file_path: str) -> str:\n \"\"\"\n 将文件上传到临时存储OSS\n \n Args:\n policy_data: 上传凭证数据\n file_path: 本地文件路径\n \n Returns:\n oss_url: OSS URL (格式: oss://...)\n \n Raises:\n Exception: 上传失败时\n \"\"\"\n file_name = Path(file_path).name\n # Sanitize filename for upload to avoid issues with spaces/characters\n safe_file_name = \"\".join([c if c.isalnum() or c in ('-','_','.') else '_' for c in file_name])\n \n key = f\"{policy_data['upload_dir']}/{safe_file_name}\"\n \n with open(file_path, 'rb') as file:\n files = {\n 'OSSAccessKeyId': (None, policy_data['oss_access_key_id']),\n 'Signature': (None, policy_data['signature']),\n 'policy': (None, policy_data['policy']),\n 'x-oss-object-acl': (None, policy_data['x_oss_object_acl']),\n 'x-oss-forbid-overwrite': (None, policy_data['x_oss_forbid_overwrite']),\n 'key': (None, key),\n 'success_action_status': (None, '200'),\n 'file': (safe_file_name, file)\n }\n \n response = requests.post(policy_data['upload_host'], files=files)\n if response.status_code != 200:\n raise Exception(f\"Failed to upload file: {response.text}\")\n \n # Construct OSS URL correctly: oss://\u003cbucket>/\u003ckey>\n # Extract bucket from upload_host (e.g., https://dashscope-instant.oss-cn-beijing.aliyuncs.com)\n upload_host = policy_data['upload_host']\n bucket_name = \"\"\n if '://' in upload_host:\n domain = upload_host.split('://')[1]\n bucket_name = domain.split('.')[0]\n \n if bucket_name:\n return f\"oss://{bucket_name}/{key}\"\n else:\n # Fallback if parsing fails (though unlikely for standard OSS hosts)\n # If the original code's assumption that key was self-sufficient was somehow valid, logic is here.\n # But normally, oss://\u003ckey> is wrong if key doesn't have bucket.\n return f\"oss://{key}\"\n \n def upload(self, file_path: str) -> str:\n \"\"\"\n 上传文件到阿里云OSS并获取URL(统一接口方法)\n \n Args:\n file_path: 本地文件路径\n \n Returns:\n oss_url: OSS URL,可在48小时内使用\n \n Raises:\n FileNotFoundError: 文件不存在时\n RuntimeError: API Key未设置时\n Exception: 上传失败时\n \"\"\"\n # 检查文件是否存在\n if not os.path.exists(file_path):\n raise FileNotFoundError(f\"文件不存在: {file_path}\")\n \n if not self.api_key:\n raise RuntimeError(\"DASHSCOPE_API_KEY 未设置,无法使用图片上传服务\")\n \n # 1. 获取上传凭证(注意:上传凭证接口有限流)\n policy_data = self.get_upload_policy()\n \n # 2. 上传文件到OSS\n oss_url = self.upload_file_to_oss(policy_data, file_path)\n \n # 3. 计算过期时间\n expire_time = datetime.now() + timedelta(hours=48)\n \n logging.info(f\"文件上传成功: {file_path}\")\n logging.info(f\" OSS URL: {oss_url}\")\n logging.info(f\" 过期时间: {expire_time.strftime('%Y-%m-%d %H:%M:%S')} (48小时)\")\n \n return oss_url\n\n def collage_images(self, image_paths, output_path):\n \"\"\"\n 拼图功能:将多张图片水平拼接\n Args:\n image_paths: 图片路径列表\n output_path: 输出文件路径\n \"\"\"\n if not image_paths:\n return None\n \n images = []\n for p in image_paths:\n try:\n img = Image.open(p)\n images.append(img)\n except Exception as e:\n logging.error(f\"Cannot open image {p}: {e}\")\n \n if not images:\n return None\n\n # 统一高度,按第一张图片的高度调整其他图片\n base_height = images[0].height\n resized_images = []\n for img in images:\n if img.height != base_height:\n ratio = base_height / img.height\n new_width = int(img.width * ratio)\n resized_images.append(img.resize((new_width, base_height)))\n else:\n resized_images.append(img)\n \n total_width = sum(img.width for img in resized_images)\n new_im = Image.new('RGB', (total_width, base_height))\n \n x_offset = 0\n for img in resized_images:\n new_im.paste(img, (x_offset, 0))\n x_offset += img.width\n \n new_im.save(output_path)\n return output_path\n\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15888,"content_sha256":"06c9a2ea679bed97c21f2fa8e36365ec616e220937673f57f5e87743d1dee8c3"},{"filename":"aigc-claw/backend/tool/image_seedream.py","content":"\"\"\"\nSeedream 图像生成 API 客户端\n字节跳动 ARK - doubao-seedream-5-0-260128 模型\n\"\"\"\n\nimport os\nimport time\nimport logging\nfrom typing import Optional, List, Dict\nfrom openai import OpenAI\n\n# 模型名称映射表(旧名称 -> 新名称)\nMODEL_NAME_MAP: Dict[str, str] = {\n # doubao-seedream-5-0 系列\n \"doubao-seedream-5-0\": \"doubao-seedream-5-0-260128\",\n # doubao-seedream-4-5 系列\n \"doubao-seedream-4-5\": \"doubao-seedream-4-5-251128\",\n # doubao-seedream-4-0 系列\n \"doubao-seedream-4-0\": \"doubao-seedream-4-0-250828\",\n}\n\n\ndef normalize_model_name(model: str) -> str:\n \"\"\"\n 规范化模型名称\n\n Args:\n model: 传入的模型名称\n\n Returns:\n 规范化后的模型名称\n \"\"\"\n return MODEL_NAME_MAP.get(model, model)\n\n\nclass SeedreamClient:\n \"\"\"\n Seedream 图像生成客户端(字节跳动 ARK)\n 支持文生图功能\n \"\"\"\n\n def __init__(\n self,\n api_key: Optional[str] = None,\n base_url: Optional[str] = None,\n timeout: int = 120,\n ) -> None:\n \"\"\"\n 初始化 Seedream 客户端\n\n Args:\n api_key: ARK API Key\n base_url: ARK API 基础 URL\n timeout: HTTP请求超时时间(秒)\n \"\"\"\n self.api_key = api_key or os.getenv(\"ARK_API_KEY\")\n self.base_url = base_url or \"https://ark.cn-beijing.volces.com/api/v3\"\n self.timeout = timeout\n\n if not self.api_key:\n logging.warning(\n \"SeedreamClient missing api_key. Set ARK_API_KEY.\"\n )\n\n self.client = OpenAI(\n base_url=self.base_url,\n api_key=self.api_key,\n timeout=timeout,\n )\n\n def generate_image(\n self,\n prompt: str,\n session_id: str,\n model: str = \"doubao-seedream-4-5-251128\",\n size: str = \"1920*1080\",\n image_paths: Optional[List[str]] = None,\n **kwargs\n ) -> List[str]:\n \"\"\"\n 生成图片\n\n Args:\n prompt: 提示词\n session_id: 任务或会话ID,用于构建存储路径\n model: 模型名称\n size: 生成图片的分辨率,如 \"1920*1080\", \"1024*1024\"\n image_paths: 参考图路径或URL列表 (图生图)\n **kwargs: 其他生成参数\n\n Returns:\n 生成的图片路径列表\n \"\"\"\n if not self.api_key:\n raise RuntimeError(\"ARK_API_KEY not set.\")\n\n # 规范化模型名称(旧名称 -> 新名称)\n model = normalize_model_name(model)\n\n # 处理分辨率 (Seedream 要求至少 3686400 像素)\n # 常用 2K/4K 分辨率\n size_map = {\n # 16:9\n \"1920*1080\": (1920, 1080),\n \"2048*1080\": (2048, 1080), # 2K 电影\n \"2560*1440\": (2560, 1440), # 2K QHD\n \"3840*2160\": (3840, 2160), # 4K UHD\n \"4096*2160\": (4096, 2160), # 4K 电影\n # 9:16\n \"1080*1920\": (1080, 1920),\n \"1080*2048\": (1080, 2048),\n \"1440*2560\": (1440, 2560),\n \"2160*3840\": (2160, 3840),\n \"2160*4096\": (2160, 4096),\n # 1:1\n \"1024*1024\": (1024, 1024),\n \"2048*2048\": (2048, 2048), # 2K 正方\n # 4:3\n \"1920*1440\": (1920, 1440),\n \"2560*1920\": (2560, 1920),\n # 3:4\n \"1440*1920\": (1440, 1920),\n \"1920*2560\": (1920, 2560),\n }\n\n width, height = 1920, 1080 # 默认\n min_pixels = 3686400\n\n if size:\n parts = size.split(\"*\")\n if len(parts) == 2:\n w, h = int(parts[0]), int(parts[1])\n width, height = w, h\n\n # 确保满足最小像素要求\n if width * height \u003c min_pixels:\n # 查找相同宽高比的常用分辨率\n aspect_ratio = width / height\n for (w, h) in size_map.values():\n if abs(w / h - aspect_ratio) \u003c 0.01 and w * h >= min_pixels:\n width, height = w, h\n break\n else:\n # 没有找到合适的,按比例放大\n scale = (min_pixels / (width * height)) ** 0.5\n width = int(width * scale)\n height = int(height * scale)\n width = width if width % 2 == 0 else width + 1\n height = height if height % 2 == 0 else height + 1\n\n # 构建 extra_body\n extra_body = {\n \"watermark\": False,\n \"sequential_image_generation\": \"disabled\",\n }\n\n # 添加其他参数\n if \"seed\" in kwargs:\n extra_body[\"seed\"] = kwargs[\"seed\"]\n if \"quality\" in kwargs:\n extra_body[\"quality\"] = kwargs[\"quality\"]\n if \"style\" in kwargs:\n extra_body[\"style\"] = kwargs[\"style\"]\n\n # 处理参考图 (图生图)\n image_urls = []\n if image_paths and len(image_paths) > 0:\n # 处理参考图:支持 URL 和本地文件\n ref_images = []\n for p in image_paths:\n if p.startswith(\"http\"):\n ref_images.append(p)\n elif os.path.exists(p):\n # 转换为 base64 URL\n import base64\n with open(p, \"rb\") as f:\n img_data = base64.b64encode(f.read()).decode(\"utf-8\")\n ext = os.path.splitext(p)[1].lower()\n mime = \"image/png\" if ext == \".png\" else \"image/jpeg\"\n ref_images.append(f\"data:{mime};base64,{img_data}\")\n extra_body[\"image\"] = ref_images\n\n # 调用 API\n if image_paths and len(image_paths) > 0:\n # 图生图 - image 放在 extra_body 中\n response = self.client.images.generate(\n model=model,\n prompt=prompt,\n size=f\"{width}x{height}\",\n response_format=\"url\",\n extra_body=extra_body,\n )\n else:\n # 文生图\n response = self.client.images.generate(\n model=model,\n prompt=prompt,\n size=f\"{width}x{height}\",\n response_format=\"url\",\n extra_body=extra_body,\n )\n\n # 下载图片到本地\n generated_paths = []\n if response.data:\n for idx, img_data in enumerate(response.data):\n if img_data.url:\n local_path = self._download_image(\n img_data.url, session_id, idx\n )\n if local_path:\n generated_paths.append(local_path)\n\n return generated_paths\n\n def _download_image(self, url: str, session_id: str, idx: int) -> Optional[str]:\n \"\"\"从URL下载图片到本地\"\"\"\n import requests\n\n # 构建存储路径\n base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n result_dir = os.path.join(base_dir, \"code\", \"result\", \"image\", str(session_id))\n os.makedirs(result_dir, exist_ok=True)\n\n file_name = f\"seedream_{int(time.time())}_{idx}.png\"\n file_path = os.path.join(result_dir, file_name)\n\n try:\n response = requests.get(url, timeout=self.timeout)\n response.raise_for_status()\n with open(file_path, \"wb\") as f:\n f.write(response.content)\n return file_path\n except Exception as e:\n logging.error(f\"Failed to download image from {url}: {e}\")\n return None\n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config # 加载 .env\n\n print(\"=== Seedream 可用性测试 ===\")\n api_key = os.getenv(\"ARK_API_KEY\", \"\")\n base_url = os.getenv(\"ARK_BASE_URL\", \"https://ark.cn-beijing.volces.com/api/v3\")\n if not api_key:\n print(\"✗ ARK_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***{api_key[-4:]}\")\n print(f\" Base URL: {base_url}\")\n\n client = SeedreamClient(api_key=api_key, base_url=base_url)\n\n # === 测试1: 文生图 ===\n prompt = \"星际穿越,黑洞,黑洞里冲出一辆支离破碎的复古列车,视觉冲击力,电影大片,末日既视感\"\n print(f\"\\n[测试1: 文生图] Prompt: {prompt}\")\n t0 = time.time()\n try:\n paths = client.generate_image(\n prompt=prompt,\n session_id=\"test_avail\",\n model=\"doubao-seedream-5-0-260128\",\n size=\"1920*1080\",\n )\n elapsed = time.time() - t0\n if paths:\n print(f\"✓ 生成 {len(paths)} 张图片 ({elapsed:.1f}s): {paths}\")\n else:\n print(f\"✗ 返回空列表 ({elapsed:.1f}s)\")\n except Exception as e:\n print(f\"✗ 图片生成失败: {e}\")\n\n # === 测试2: 图生图 ===\n # 需要一张已有的参考图路径\n ref_image_path = \"code/result/image/test_avail/test_input.png\"\n if os.path.exists(ref_image_path):\n prompt_i2i = \"将这只猫变成赛博朋克风格\"\n print(f\"\\n[测试2: 图生图] Prompt: {prompt_i2i}\")\n print(f\" 参考图: {ref_image_path}\")\n t0 = time.time()\n try:\n paths = client.generate_image(\n prompt=prompt_i2i,\n session_id=\"test_avail\",\n model=\"doubao-seedream-5-0-260128\",\n size=\"1920*1080\",\n image_paths=[ref_image_path],\n )\n elapsed = time.time() - t0\n if paths:\n print(f\"✓ 生成 {len(paths)} 张图片 ({elapsed:.1f}s): {paths}\")\n else:\n print(f\"✗ 返回空列表 ({elapsed:.1f}s)\")\n except Exception as e:\n print(f\"✗ 图生图失败: {e}\")\n else:\n print(f\"\\n[测试2: 图生图] ✗ 参考图不存在: {ref_image_path}\")\n print(\" 跳过图生图测试,请先运行文生图测试生成参考图\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10131,"content_sha256":"7cf9d50a25d4ef0235d1f9f505768cb681c24e58b9a6564e5deb209007c5a373"},{"filename":"aigc-claw/backend/tool/llm_client.py","content":"import os\nimport logging\n\ntry:\n from tool.llm_gpt import GPT\n from tool.llm_gemini import Gemini\n from tool.llm_deepseek import DeepSeek\n from tool.llm_dashscope import QwenLLM\n from tool.relay_client import RelayClient\nexcept ImportError:\n from llm_gpt import GPT\n from llm_gemini import Gemini\n from llm_deepseek import DeepSeek\n from llm_dashscope import QwenLLM\n from relay_client import RelayClient\n\nfrom config import Config\n\nlogger = logging.getLogger(__name__)\n\nclass LLM:\n def __init__(self, gemini_base_url=\"\", gemini_api_key=\"\", gpt_base_url=\"\", gpt_api_key=\"\", deepseek_base_url=\"\", deepseek_api_key=\"\", dashscope_api_key=\"\"):\n self.gemini_base_url = gemini_base_url or os.getenv(\"GOOGLE_GEMINI_BASE_URL\", \"\")\n self.gemini_api_key = gemini_api_key or os.getenv(\"GEMINI_API_KEY\", \"\")\n self.gpt_base_url = gpt_base_url or os.getenv(\"OPENAI_BASE_URL\", \"\")\n self.gpt_api_key = gpt_api_key or os.getenv(\"OPENAI_API_KEY\", \"\")\n self.deepseek_base_url = deepseek_base_url or os.getenv(\"DEEPSEEK_BASE_URL\", \"\")\n self.deepseek_api_key = deepseek_api_key or os.getenv(\"DEEPSEEK_API_KEY\", \"\")\n self.dashscope_api_key = dashscope_api_key or os.getenv(\"DASHSCOPE_API_KEY\", \"\")\n\n # 初始化中转站客户端(如果配置了)\n self._relay_client = None\n relay_key = os.getenv(\"RELAY_API_KEY\", \"\")\n relay_url = os.getenv(\"RELAY_BASE_URL\", \"\")\n if relay_key and relay_url:\n try:\n self._relay_client = RelayClient(api_key=relay_key, base_url=relay_url)\n logger.info(\"Relay client 初始化成功\")\n except Exception as e:\n logger.warning(f\"Relay client 初始化失败: {e}\")\n\n def full_to_half(self, text):\n if not isinstance(text, str):\n return text\n \n translation_table = {0x3000: 0x0020}\n for i in range(65281, 65375):\n translation_table[i] = i - 65248\n \n return text.translate(translation_table)\n\n def query(self, prompt, image_urls=[], model=\"gemini-3-flash-preview\", safe_content=True, task_id=None, web_search=False):\n \"\"\"\n Query the LLM with a prompt and optional image URLs.\n Selects the backend (GPT or Gemini) based on the model name.\n\n :param web_search: Enable web search for supported providers\n \"\"\"\n if safe_content:\n prompt = self.full_to_half(prompt)\n\n if not model:\n model = \"gemini-3-flash-preview\"\n \n if Config.PRINT_MODEL_INPUT:\n print(\"---- LLM QUERY REQUEST ----\")\n print(f\"Model: {model}\")\n if task_id:\n print(f\"Task ID: {task_id}\")\n if image_urls:\n print(f\"Images: {len(image_urls)}\")\n for u in image_urls:\n print(f\" - {u}\")\n print(f\"Prompt: {prompt[:200]}{'...' if len(prompt) > 200 else ''}\")\n print(\"-\" * 30)\n \n result = \"\"\n model_lower = model.lower()\n\n # 判断是否通过中转站调用(优先级:config_model.json 中 provider=relay)\n is_relay_model = self._is_relay_model(model_lower)\n\n if is_relay_model and self._relay_client:\n # 通过中转站统一调用\n result = self._relay_client.chat(prompt, model=model, image_urls=image_urls, web_search=web_search)\n elif model_lower.startswith(\"gemini\"):\n # Gemini client handles its own credentials internally in the current implementation,\n # but we pass args for consistency/future compatibility.\n # Note: Gemini doesn't have built-in web search parameter, user needs to use Function Calling\n client = Gemini(base_url=self.gemini_base_url, api_key=self.gemini_api_key)\n result = client.query(prompt, image_urls=image_urls, model=model)\n elif model_lower.startswith(\"deepseek\"):\n client = DeepSeek(base_url=self.deepseek_base_url, api_key=self.deepseek_api_key)\n result = client.query(prompt, image_urls=image_urls, model=model, web_search=web_search)\n elif \"qwen\" in model_lower:\n # Qwen models via DashScope Generation API (text-only mode)\n client = QwenLLM(api_key=self.dashscope_api_key)\n result = client.query(prompt, image_urls=image_urls, model=model, web_search=web_search)\n else:\n # OpenAI 系列: gpt-4, gpt-4o, gpt-5, gpt-5.1, o3 等\n client = GPT(base_url=self.gpt_base_url, api_key=self.gpt_api_key)\n result = client.query(prompt, image_urls=image_urls, model=model, web_search=web_search)\n\n if safe_content:\n result = self.full_to_half(result)\n \n # Remove empty lines\n return '\\n'.join([line for line in result.split('\\n') if line.strip() != ''])\n\n def _is_relay_model(self, model_lower: str) -> bool:\n \"\"\"\n 判断模型是否应通过中转站调用\n 优先读取 config_model.json 中的 provider 字段,\n 如果未配置或中转站不可用,则回退到直连。\n \"\"\"\n if not self._relay_client:\n return False\n\n # 尝试从 config_model.json 读取 provider\n try:\n import json\n config_path = os.path.join(\n os.path.dirname(os.path.dirname(os.path.abspath(__file__))),\n \"config_model.json\"\n )\n if os.path.exists(config_path):\n with open(config_path, \"r\", encoding=\"utf-8\") as f:\n config = json.load(f)\n model_info = config.get(\"models\", {}).get(model_lower)\n if model_info and model_info.get(\"provider\") == \"relay\":\n return True\n except Exception:\n pass\n\n return False\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5886,"content_sha256":"ec43bbd2b56d17fc5668917648c0dd0fafd705707097ebbc751427e683d8538d"},{"filename":"aigc-claw/backend/tool/llm_dashscope.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\nQwen LLM API 客户端(DashScope Generation API)\n支持 qwen3.5-plus, qwen3.5-max 等模型\n\"\"\"\n\nimport os\nimport time\nimport logging\nfrom typing import Optional\n\nlogger = logging.getLogger(__name__)\n\ntry:\n import dashscope\n from dashscope import Generation\nexcept ImportError:\n dashscope = None\n Generation = None\n\n\nclass QwenLLM:\n \"\"\"\n Qwen LLM 客户端,使用 DashScope Generation API\n 支持纯文本生成(可作为 LLM 使用)\n \"\"\"\n def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):\n \"\"\"\n :param api_key: DashScope API Key\n :param base_url: DashScope API Base URL (可选)\n \"\"\"\n self.api_key = api_key or os.getenv(\"DASHSCOPE_API_KEY\")\n self.base_url = base_url or os.getenv(\"DASHSCOPE_BASE_URL\", \"https://dashscope.aliyuncs.com/api/v1\")\n\n if not self.api_key:\n print(\"Warning: DASHSCOPE_API_KEY is not set.\")\n\n if dashscope:\n dashscope.api_key = self.api_key\n\n self.max_attempts = 3\n\n def query(self, prompt: str, image_urls: list = None, model: str = \"qwen3.5-vl\", web_search: bool = False):\n \"\"\"\n Query Qwen model for text generation.\n Note: This is for text-only LLM use. For image+text, use VLM client.\n\n :param prompt: Text prompt\n :param image_urls: Ignored in this LLM implementation (use VLM for multimodal)\n :param model: Model name (e.g., qwen3.5-vl, qwen3.5-plus, qwen3.5-max)\n :param web_search: If True, adds enable_search: True to API call\n \"\"\"\n if dashscope is None:\n raise RuntimeError(\"dashscope package not installed. Run: pip install dashscope\")\n\n if not model:\n model = \"qwen3.5-vl\"\n\n # Map common model names to DashScope API names\n model_mapping = {\n \"qwen3.5-plus\": \"qwen-plus\",\n \"qwen3.5-max\": \"qwen-max\",\n \"qwen2.5-plus\": \"qwen-plus\",\n \"qwen2.5-max\": \"qwen-max\",\n }\n api_model = model_mapping.get(model.lower(), model)\n\n messages = [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}]\n messages.append({\"role\": \"user\", \"content\": prompt})\n\n attempts = 0\n while attempts \u003c self.max_attempts:\n try:\n # Build request parameters\n request_params = {\n \"model\": api_model,\n \"messages\": messages,\n \"result_format\": \"message\",\n \"stream\": False\n }\n # Add web search if enabled\n if web_search:\n request_params[\"enable_search\"] = True\n\n response = Generation.call(**request_params)\n\n if response.status_code == 200:\n if response.output.choices and response.output.choices[0].message.content:\n return response.output.choices[0].message.content\n else:\n print(\"Received an empty response from Qwen. Retrying.\")\n time.sleep(2)\n else:\n error_msg = f\"Qwen API error: {response.code} - {response.message}\"\n logger.error(error_msg)\n raise RuntimeError(error_msg)\n\n except Exception as e:\n logger.error(f\"Error occurred with Qwen: {e}. Retrying.\")\n time.sleep(5)\n\n attempts += 1\n\n raise Exception(\"Max attempts reached, failed to get a response from Qwen.\")\n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n # 支持的模型列表\n MODELS = [\"qwen3.5-plus\", \"qwen3.5-max\"]\n\n print(\"=== Qwen LLM (DashScope) 可用性测试 ===\")\n api_key = os.getenv(\"DASHSCOPE_API_KEY\", \"\")\n if not api_key:\n print(\"✗ DASHSCOPE_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***{api_key[-4:]}\")\n\n client = QwenLLM(api_key=api_key)\n prompt = \"用一句话介绍你自己。\"\n print(f\" Prompt: {prompt}\")\n\n for model in MODELS:\n print(f\"\\n--- 测试模型: {model} ---\")\n t0 = time.time()\n try:\n resp = client.query(prompt, model=model)\n elapsed = time.time() - t0\n print(f\"✓ 响应 ({elapsed:.1f}s): {resp.strip()[:200]}\")\n except Exception as e:\n print(f\"✗ 失败: {e}\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4567,"content_sha256":"44375211ed432f915eaabcac2b3dd90069b63e5fc99aebaec7d35b4c98689e99"},{"filename":"aigc-claw/backend/tool/llm_deepseek.py","content":"import os\nimport time\nfrom openai import OpenAI\n\nclass DeepSeek:\n \"\"\"\n deepseek-chat: DeepSeek-V3.2 非思考模式\n deepseek-reasoner: DeepSeek-V3.2 思考模式\n \"\"\"\n def __init__(self, base_url=\"\", api_key=\"\"):\n self.base_url = base_url or os.getenv(\"DEEPSEEK_BASE_URL\") or \"https://api.deepseek.com/v1\"\n self.api_key = api_key or os.getenv(\"DEEPSEEK_API_KEY\")\n \n if not self.api_key:\n print(\"Warning: DEEPSEEK_API_KEY is not set.\")\n\n self.client = OpenAI(\n api_key=self.api_key, \n base_url=self.base_url\n )\n self.max_attempts = 3\n\n def query(self, prompt, image_urls=[], model=\"deepseek-chat\", web_search=False):\n \"\"\"\n Query DeepSeek model.\n\n :param web_search: If True, adds enable_web_search: True to API call\n \"\"\"\n if not model:\n model = \"deepseek-chat\"\n\n messages = [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}]\n messages.append({\"role\": \"user\", \"content\": prompt})\n\n attempts = 0\n while attempts \u003c self.max_attempts:\n try:\n # Build request parameters\n request_params = {\n \"model\": model,\n \"messages\": messages,\n \"stream\": False\n }\n # Add web search if enabled\n if web_search:\n request_params[\"enable_web_search\"] = True\n\n response = self.client.chat.completions.create(**request_params)\n \n # DeepSeek might return reasoning_content for reasoner models, \n # but standard content is what we return conform to other interfaces.\n if response.choices and response.choices[0].message.content:\n return response.choices[0].message.content\n else:\n print(\"Received an empty response from DeepSeek. Retrying.\")\n time.sleep(2)\n except Exception as e:\n print(f\"Error occurred with DeepSeek: {e}. Retrying.\")\n time.sleep(5)\n attempts += 1\n \n raise Exception(\"Max attempts reached, failed to get a response from DeepSeek.\")\n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n # 支持的模型列表\n MODELS = [\"deepseek-chat\", \"deepseek-reasoner\"]\n\n print(\"=== DeepSeek 可用性测试 ===\")\n api_key = os.getenv(\"DEEPSEEK_API_KEY\", \"\")\n base_url = os.getenv(\"DEEPSEEK_BASE_URL\", \"\")\n if not api_key:\n print(\"✗ DEEPSEEK_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***{api_key[-4:]}\")\n if base_url:\n print(f\" Base URL: {base_url}\")\n\n client = DeepSeek(api_key=api_key, base_url=base_url)\n prompt = \"用一句话介绍你自己。\"\n print(f\" Prompt: {prompt}\")\n\n for model in MODELS:\n print(f\"\\n--- 测试模型: {model} ---\")\n t0 = time.time()\n try:\n resp = client.query(prompt, model=model)\n elapsed = time.time() - t0\n print(f\"✓ 响应 ({elapsed:.1f}s): {resp.strip()[:200]}\")\n except Exception as e:\n print(f\"✗ 失败: {e}\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3373,"content_sha256":"49e7503eb15e25511b9082e62cec8fdc53fb422fd1c7e5097c7340ad453aa03e"},{"filename":"aigc-claw/backend/tool/llm_gemini.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\nGoogle Gemini LLM 客户端 (OpenAI 兼容格式)\n支持 gemini-2.5-flash, gemini-2.5-pro 等模型\n\n可用模型:\n - gemini-2.5-flash (性价比高)\n - gemini-2.5-flash-preview\n - gemini-2.5-pro (效果最好)\n - gemini-2.5-pro-preview\n - gemini-2.0-flash\n\"\"\"\n\nimport os\nimport time\nfrom openai import OpenAI\nfrom typing import List\n\nclass Gemini:\n \"\"\"\n Gemini LLM 客户端,使用 OpenAI 兼容格式调用\n \"\"\"\n def __init__(self, base_url: str = \"\", api_key: str = \"\"):\n \"\"\"\n 初始化 Gemini 客户端\n :param base_url: OpenAI 兼容的 Base URL\n :param api_key: Gemini API Key\n \"\"\"\n # 确保 base_url 以 /v1 结尾\n default_url = \"http://35.164.11.19:3887/v1\"\n self.base_url = base_url or os.getenv(\"GOOGLE_GEMINI_BASE_URL\", default_url)\n if self.base_url and not self.base_url.endswith(\"/v1\"):\n self.base_url = self.base_url.rstrip(\"/\") + \"/v1\"\n self.api_key = api_key or os.getenv(\"GEMINI_API_KEY\", \"\")\n self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)\n self.max_attempts = 10\n self.max_tokens = 20000\n\n def query(self, prompt: str, image_urls: List[str] = [], model: str = \"gemini-2.5-flash\") -> str:\n \"\"\"\n 调用 Gemini LLM\n :param prompt: 文本提示\n :param image_urls: 图片 URL 列表(可选,用于多模态模型)\n :param model: 模型名\n :return: 生成的文本\n \"\"\"\n if not model:\n model = \"gemini-2.5-flash\"\n\n # 构建消息格式\n content: list = [{\"type\": \"text\", \"text\": prompt}]\n\n # 添加图片 (如果有多模态模型支持)\n if image_urls:\n for img_url in image_urls:\n if img_url.startswith(\"http\"):\n content.append({\n \"type\": \"image_url\",\n \"image_url\": {\"url\": img_url}\n })\n\n messages = [{\"role\": \"user\", \"content\": content}]\n\n attempts = 0\n while attempts \u003c self.max_attempts:\n try:\n # 直接使用模型名(代理服务会处理格式转换)\n response = self.client.chat.completions.create(\n model=model,\n messages=messages,\n max_tokens=self.max_tokens,\n temperature=0.7\n )\n\n # 检查响应类型\n if isinstance(response, str):\n print(f\"Gemini 返回字符串响应: {response}\")\n raise Exception(f\"API 返回错误: {response}\")\n\n if response.choices and len(response.choices) > 0:\n return response.choices[0].message.content\n\n except Exception as e:\n print(f\"Gemini 请求错误: {e}\")\n attempts += 1\n if attempts \u003c self.max_attempts:\n time.sleep(10)\n\n raise Exception(\"Gemini: 达到最大重试次数,仍未获得有效响应。\")\n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n # 支持的模型列表\n MODELS = [\"gemini-2.5-flash\", \"gemini-2.0-flash\"]\n\n print(\"=== Gemini LLM 可用性测试 ===\")\n api_key = Config.GEMINI_API_KEY\n base_url = Config.GOOGLE_GEMINI_BASE_URL\n if not api_key:\n print(\"✗ GEMINI_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***\")\n # 实际使用的 URL(会自动添加 /v1)\n actual_url = base_url if base_url and base_url.endswith(\"/v1\") else (base_url + \"/v1\" if base_url else \"http://35.164.11.19:3887/v1\")\n print(f\" Base URL: {actual_url}\")\n client = Gemini(api_key=api_key, base_url=base_url)\n prompt = \"用一句话介绍你自己。\"\n print(f\" Prompt: {prompt}\")\n\n for model in MODELS:\n print(f\"\\n--- 测试模型: {model} ---\")\n t0 = time.time()\n try:\n resp = client.query(prompt, model=model)\n elapsed = time.time() - t0\n print(f\"✓ 响应 ({elapsed:.1f}s): {resp.strip()[:200]}\")\n except Exception as e:\n print(f\"✗ 失败: {e}\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4322,"content_sha256":"1aaff74f3dfd700a7e123a512413205e4507a77b59e474cddb670f9cbd61ee6e"},{"filename":"aigc-claw/backend/tool/llm_gpt.py","content":"\nimport os\nimport time\nfrom openai import OpenAI\n\n\nclass GPT:\n \"\"\"\n OpenAI 文本生成客户端\n 可选模型:gpt-4o 等\n \"\"\"\n def __init__(self, base_url=\"\", api_key=\"\", timeout=300):\n if base_url:\n self.client = OpenAI(api_key=api_key, base_url=base_url, timeout=timeout)\n else:\n self.client = OpenAI(api_key=api_key, timeout=timeout)\n self.max_attempts = 10\n self.max_tokens = 8192\n\n def query(self, prompt, image_urls=[], model=\"\", web_search=False):\n self.model = model\n if self.model == \"\":\n self.model = \"gpt-5\"\n\n # Switch to search model if web_search is enabled\n # OpenAI uses gpt-4o-search-preview for web search\n if web_search and not self.model.endswith(\"-search\"):\n search_model_map = {\n \"gpt-4o\": \"gpt-4o-search-preview\",\n \"gpt-4\": \"gpt-4-search-preview\",\n \"gpt-5\": \"gpt-5-search\",\n }\n self.model = search_model_map.get(self.model, self.model + \"-search\")\n\n messages = [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}]\n content = [{\"type\": \"text\", \"text\": prompt}]\n if image_urls:\n content.extend([{\"type\": \"image_url\", \"image_url\": {\"url\": url}} for url in image_urls])\n messages.append({\"role\": \"user\", \"content\": content})\n\n attempts = 0\n while attempts \u003c self.max_attempts:\n try:\n # Build request parameters\n request_params = {\n \"model\": self.model,\n \"messages\": messages,\n \"max_tokens\": self.max_tokens\n }\n # Add search tool if web_search is enabled\n if web_search:\n request_params[\"search_tool\"] = \"auto\"\n\n response = self.client.chat.completions.create(**request_params)\n if response.choices[0].message.content.strip():\n return response.choices[0].message.content\n else:\n print(\"Received an empty response. Retrying in 10 seconds.\")\n except Exception as e:\n print(messages)\n print(f\"Error occurred: {e}. Retrying in 10 seconds.\")\n time.sleep(10)\n attempts += 1\n\n raise Exception(\"Max attempts reached, failed to get a response from OpenAI.\") \n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n # 支持的模型列表\n MODELS = [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4.1\", \"gpt-4.1-mini\", \"gpt-5\"]\n\n print(\"=== GPT 文本生成可用性测试 ===\")\n api_key = Config.OPENAI_API_KEY\n base_url = Config.OPENAI_BASE_URL\n if not api_key:\n print(\"✗ OPENAI_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***{api_key[-4:]}\")\n print(f\" Base URL: {base_url}\")\n client = GPT(api_key=api_key, base_url=base_url)\n\n prompt = \"用一句话介绍你自己。\"\n print(f\" Prompt: {prompt}\")\n\n for model in MODELS:\n print(f\"\\n--- 测试模型: {model} ---\")\n t0 = time.time()\n try:\n resp = client.query(prompt, model=model)\n elapsed = time.time() - t0\n print(f\"✓ 响应 ({elapsed:.1f}s): {resp.strip()[:200]}\")\n except Exception as e:\n print(f\"✗ 失败: {e}\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3507,"content_sha256":"969f2c4bed463c8cc395200372cc5863173b1d41b6451a62c77b3b45cd30bbf7"},{"filename":"aigc-claw/backend/tool/relay_client.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\n统一中转站 (Relay) 客户端\n通过第三方中转站(如青云 API)统一调用各种模型\n使用 OpenAI 兼容格式,一个 API Key + Base URL 调用所有模型\n\n支持的模型类型:\n - LLM 文本生成(chat.completions)\n - VLM 视觉理解(chat.completions + 图片)\n - 图片生成(images.generate 或 chat.completions)\n - 视频生成(异步任务:创建 → 轮询 → 获取结果)\n - Embedding(embeddings)\n\n参考:青云聚合 API (https://api.qingyuntop.top)\n\"\"\"\n\nimport os\nimport time\nimport json\nimport base64\nimport logging\nimport requests\nfrom typing import List, Optional, Dict, Any\nfrom openai import OpenAI\n\nlogger = logging.getLogger(__name__)\n\n\nclass RelayClient:\n \"\"\"\n 统一中转站客户端\n 所有模型通过同一个 OpenAI 兼容 API 调用\n \"\"\"\n\n def __init__(\n self,\n api_key: Optional[str] = None,\n base_url: Optional[str] = None,\n timeout: int = 300,\n ):\n self.api_key = api_key or os.getenv(\"RELAY_API_KEY\", \"\")\n self.base_url = base_url or os.getenv(\"RELAY_BASE_URL\", \"\")\n self.provider_name = os.getenv(\"RELAY_PROVIDER_NAME\", \"relay\")\n self.timeout = timeout\n\n if not self.api_key:\n raise ValueError(\n \"RELAY_API_KEY 未设置。请在 .env 中配置 RELAY_API_KEY 或传入 api_key 参数。\\n\"\n \"示例: RELAY_API_KEY=sk-xxx RELAY_BASE_URL=https://api.qingyuntop.top/v1\"\n )\n if not self.base_url:\n raise ValueError(\n \"RELAY_BASE_URL 未设置。请在 .env 中配置 RELAY_BASE_URL 或传入 base_url 参数。\\n\"\n \"示例: RELAY_BASE_URL=https://api.qingyuntop.top/v1\"\n )\n\n # 确保 base_url 以 /v1 结尾\n if not self.base_url.endswith(\"/v1\"):\n self.base_url = self.base_url.rstrip(\"/\") + \"/v1\"\n\n self.client = OpenAI(\n api_key=self.api_key,\n base_url=self.base_url,\n timeout=timeout,\n )\n self._base_url_no_v1 = self.base_url.rstrip(\"/v1\").rstrip(\"/v\")\n\n logger.info(f\"RelayClient 初始化: provider={self.provider_name}, base_url={self.base_url}\")\n\n # ==================== LLM 文本生成 ====================\n\n def chat(\n self,\n prompt: str,\n model: str = \"qwen3.5-plus\",\n image_urls: Optional[List[str]] = None,\n max_tokens: int = 8192,\n temperature: float = 0.7,\n system_prompt: str = \"You are a helpful assistant.\",\n web_search: bool = False,\n ) -> str:\n \"\"\"\n LLM 文本生成(OpenAI chat.completions 格式)\n\n Args:\n prompt: 用户提示词\n model: 模型名称\n image_urls: 图片 URL 列表(多模态模型)\n max_tokens: 最大生成 token 数\n temperature: 温度参数\n system_prompt: 系统提示词\n web_search: 是否启用联网搜索\n\n Returns:\n 生成的文本内容\n \"\"\"\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n # 构建用户消息\n content: list = [{\"type\": \"text\", \"text\": prompt}]\n if image_urls:\n for url in image_urls:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": url}})\n\n messages.append({\"role\": \"user\", \"content\": content})\n\n max_attempts = 5\n for attempt in range(max_attempts):\n try:\n request_params = {\n \"model\": model,\n \"messages\": messages,\n \"max_tokens\": max_tokens,\n \"temperature\": temperature,\n }\n if web_search:\n request_params[\"search_tool\"] = \"auto\"\n\n response = self.client.chat.completions.create(**request_params)\n\n if response.choices and response.choices[0].message.content:\n return response.choices[0].message.content.strip()\n else:\n logger.warning(f\"[{self.provider_name}] 空响应,重试 {attempt + 1}/{max_attempts}\")\n except Exception as e:\n logger.error(f\"[{self.provider_name}] chat 错误 (model={model}): {e}\")\n if attempt \u003c max_attempts - 1:\n time.sleep(5 * (attempt + 1))\n\n raise Exception(f\"[{self.provider_name}] 达到最大重试次数,模型 {model} 调用失败\")\n\n # ==================== VLM 视觉理解 ====================\n\n def vlm_chat(\n self,\n prompt: str,\n image_paths: Optional[List[str]] = None,\n model: str = \"gemini-3-flash-preview\",\n max_tokens: int = 4096,\n ) -> str:\n \"\"\"\n VLM 视觉语言模型调用(图片理解)\n\n Args:\n prompt: 文本提示\n image_paths: 图片路径列表(支持本地路径、URL、data: URI)\n model: 视觉模型名称\n\n Returns:\n 模型回复文本\n \"\"\"\n content: list = [{\"type\": \"text\", \"text\": prompt}]\n\n if image_paths:\n for path in image_paths:\n if path.startswith(\"data:\"):\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": path}})\n elif path.startswith(\"http\"):\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": path}})\n else:\n # 本地文件转 base64\n with open(path, \"rb\") as f:\n img_data = base64.b64encode(f.read()).decode(\"utf-8\")\n ext = os.path.splitext(path)[1].lower()\n mime = \"image/png\" if ext == \".png\" else \"image/jpeg\"\n content.append({\n \"type\": \"image_url\",\n \"image_url\": {\"url\": f\"data:{mime};base64,{img_data}\"}\n })\n\n messages = [{\"role\": \"user\", \"content\": content}]\n\n max_attempts = 5\n for attempt in range(max_attempts):\n try:\n response = self.client.chat.completions.create(\n model=model,\n messages=messages,\n max_tokens=max_tokens,\n )\n if response.choices and response.choices[0].message.content:\n return response.choices[0].message.content.strip()\n except Exception as e:\n logger.error(f\"[{self.provider_name}] vlm_chat 错误: {e}\")\n if attempt \u003c max_attempts - 1:\n time.sleep(5 * (attempt + 1))\n\n raise Exception(f\"[{self.provider_name}] VLM 调用失败,模型 {model}\")\n\n # ==================== 图片生成 ====================\n\n def generate_image(\n self,\n prompt: str,\n model: str = \"gemini-3-pro-image-preview\",\n size: str = \"1024x1024\",\n save_dir: Optional[str] = None,\n image_urls: Optional[List[str]] = None,\n ) -> str:\n \"\"\"\n 图片生成(通过中转站)\n\n 支持两种格式:\n 1. images.generate 格式(如 sora_image)\n 2. chat.completions 格式(如 gemini-3-pro-image-preview,通过 chat 返回 base64 图片)\n\n Args:\n prompt: 图片描述\n model: 图片模型名\n size: 图片尺寸\n save_dir: 保存目录(不传则返回 URL/base64)\n image_urls: 参考图片 URL\n\n Returns:\n 本地文件路径、URL 或 base64 字符串\n \"\"\"\n # 判断是否使用 chat 格式(gemini/grok 系列图片模型)\n chat_image_models = [\n \"gemini-3-pro-image-preview\",\n \"gemini-2.5-pro-image\",\n \"gemini-2.5-flash-image\",\n \"grok-imagine-image-pro\",\n ]\n use_chat_format = any(m in model for m in chat_image_models)\n\n if use_chat_format:\n return self._generate_image_chat(prompt, model, size, save_dir, image_urls)\n else:\n return self._generate_image_api(prompt, model, size, save_dir)\n\n def _generate_image_chat(\n self,\n prompt: str,\n model: str,\n size: str = \"1024x1024\",\n save_dir: Optional[str] = None,\n image_urls: Optional[List[str]] = None,\n ) -> str:\n \"\"\"通过 chat.completions 格式生成图片(Gemini/Grok 系列)\"\"\"\n content: list = [{\"type\": \"text\", \"text\": prompt}]\n if image_urls:\n for url in image_urls:\n content.append({\"type\": \"image_url\", \"image_url\": {\"url\": url}})\n\n messages = [{\"role\": \"user\", \"content\": content}]\n\n max_attempts = 5\n for attempt in range(max_attempts):\n try:\n response = self.client.chat.completions.create(\n model=model,\n messages=messages,\n )\n\n if not response.choices:\n continue\n\n choice = response.choices[0]\n msg = choice.message\n\n # 检查是否有 inline_data(base64 图片)\n if hasattr(msg, 'content') and isinstance(msg.content, list):\n for part in msg.content:\n if isinstance(part, dict) and part.get(\"type\") == \"image_url\":\n img_url = part[\"image_url\"][\"url\"]\n return self._save_or_return_image(img_url, save_dir)\n\n # 文本响应中可能包含 base64\n text = msg.content if isinstance(msg.content, str) else \"\"\n if text and \";base64,\" in text:\n # 从文本中提取 base64\n import re\n match = re.search(r'data:image/[^;]+;base64,([A-Za-z0-9+/=]+)', text)\n if match:\n b64_data = match.group(1)\n return self._save_b64_image(b64_data, save_dir)\n\n # 直接返回文本(可能包含 URL)\n if text:\n # 尝试提取 URL\n url_match = re.search(r'https?://[^\\s)]+\\.(png|jpg|jpeg|webp)', text)\n if url_match:\n return self._download_and_save(url_match.group(0), save_dir)\n return text\n\n except Exception as e:\n logger.error(f\"[{self.provider_name}] 图片生成(chat) 错误: {e}\")\n if attempt \u003c max_attempts - 1:\n time.sleep(5)\n\n raise Exception(f\"[{self.provider_name}] 图片生成失败,模型 {model}\")\n\n def _generate_image_api(\n self,\n prompt: str,\n model: str,\n size: str = \"1024x1024\",\n save_dir: Optional[str] = None,\n ) -> str:\n \"\"\"通过 images.generate 格式生成图片\"\"\"\n max_attempts = 5\n for attempt in range(max_attempts):\n try:\n response = self.client.images.generate(\n model=model,\n prompt=prompt,\n size=size,\n n=1,\n )\n if response.data and response.data[0]:\n img = response.data[0]\n if img.url:\n return self._download_and_save(img.url, save_dir)\n elif hasattr(img, 'b64_json') and img.b64_json:\n return self._save_b64_image(img.b64_json, save_dir)\n except Exception as e:\n logger.error(f\"[{self.provider_name}] 图片生成(api) 错误: {e}\")\n if attempt \u003c max_attempts - 1:\n time.sleep(5)\n\n raise Exception(f\"[{self.provider_name}] 图片生成失败,模型 {model}\")\n\n # ==================== 视频生成(异步任务) ====================\n\n def generate_video(\n self,\n prompt: str,\n model: str = \"sora-2-all\",\n image_url: Optional[str] = None,\n save_path: Optional[str] = None,\n poll_interval: int = 10,\n max_wait: int = 600,\n **kwargs,\n ) -> str:\n \"\"\"\n 视频生成(异步任务模式)\n 创建任务 → 轮询状态 → 下载结果\n\n 支持模型:\n - sora-2-all, sora-2-pro-all (Sora 格式)\n - veo_3_1-fast-4K, veo_3_1-components-4K (Veo 格式)\n - grok-video-3-10s (Grok 格式)\n - doubao-seedance-* (豆包格式)\n\n Args:\n prompt: 视频描述\n model: 视频模型名\n image_url: 首帧图片 URL(图生视频)\n save_path: 保存路径\n poll_interval: 轮询间隔(秒)\n max_wait: 最大等待时间(秒)\n **kwargs: 额外参数(duration, ratio 等)\n\n Returns:\n 视频本地路径或 URL\n \"\"\"\n # 1. 创建任务\n task_id = self._create_video_task(prompt, model, image_url, **kwargs)\n logger.info(f\"[{self.provider_name}] 视频任务创建: task_id={task_id}, model={model}\")\n\n # 2. 轮询等待\n result = self._poll_task(task_id, poll_interval, max_wait)\n\n # 3. 提取视频 URL\n video_url = self._extract_video_url(result, model)\n\n # 4. 下载保存\n if save_path and video_url:\n return self._download_video(video_url, save_path)\n\n return video_url\n\n def _create_video_task(\n self,\n prompt: str,\n model: str,\n image_url: Optional[str] = None,\n **kwargs,\n ) -> str:\n \"\"\"创建视频生成任务\"\"\"\n body: Dict[str, Any] = {\n \"model\": model,\n \"prompt\": prompt,\n }\n if image_url:\n body[\"image_url\"] = image_url\n body.update(kwargs)\n\n # 使用 raw HTTP 请求(视频 API 通常是自定义格式)\n url = f\"{self._base_url_no_v1}/v1/video/generations\"\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\",\n }\n\n resp = requests.post(url, json=body, headers=headers, timeout=self.timeout)\n data = resp.json()\n\n if resp.status_code >= 400:\n raise RuntimeError(f\"视频任务创建失败: {data}\")\n\n # 兼容多种响应格式\n task_id = (\n data.get(\"id\")\n or data.get(\"task_id\")\n or data.get(\"data\", {}).get(\"task_id\")\n or data.get(\"data\", {}).get(\"id\")\n )\n if not task_id:\n raise RuntimeError(f\"无法提取 task_id: {data}\")\n\n return str(task_id)\n\n def _poll_task(self, task_id: str, interval: int = 10, max_wait: int = 600) -> dict:\n \"\"\"轮询异步任务状态\"\"\"\n url = f\"{self._base_url_no_v1}/v1/video/generations/{task_id}\"\n headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n\n elapsed = 0\n while elapsed \u003c max_wait:\n resp = requests.get(url, headers=headers, timeout=30)\n data = resp.json()\n\n status = (\n data.get(\"status\")\n or data.get(\"data\", {}).get(\"status\")\n or \"unknown\"\n ).lower()\n\n if status in (\"completed\", \"succeed\", \"succeeded\", \"success\"):\n return data\n elif status in (\"failed\", \"error\"):\n raise RuntimeError(f\"视频任务失败: {data}\")\n\n logger.info(f\"[{self.provider_name}] 视频任务 {task_id}: {status} ({elapsed}s)\")\n time.sleep(interval)\n elapsed += interval\n\n raise TimeoutError(f\"视频任务超时 ({max_wait}s): task_id={task_id}\")\n\n def _extract_video_url(self, result: dict, model: str) -> str:\n \"\"\"从任务结果中提取视频 URL\"\"\"\n # 多种响应格式兼容\n candidates = [\n result.get(\"data\", {}).get(\"video_url\"),\n result.get(\"data\", {}).get(\"url\"),\n result.get(\"output\", {}).get(\"video_url\"),\n result.get(\"video_url\"),\n ]\n\n # 嵌套在 results 数组中\n results_list = result.get(\"data\", {}).get(\"results\", [])\n if results_list and isinstance(results_list, list):\n for item in results_list:\n if isinstance(item, dict):\n url = item.get(\"url\") or item.get(\"video_url\")\n if url:\n candidates.append(url)\n\n for url in candidates:\n if url and isinstance(url, str) and url.startswith(\"http\"):\n return url\n\n raise RuntimeError(f\"无法提取视频 URL: {json.dumps(result, ensure_ascii=False)[:500]}\")\n\n def _download_video(self, url: str, save_path: str) -> str:\n \"\"\"下载视频到本地\"\"\"\n os.makedirs(os.path.dirname(save_path), exist_ok=True)\n resp = requests.get(url, stream=True, timeout=300)\n resp.raise_for_status()\n with open(save_path, \"wb\") as f:\n for chunk in resp.iter_content(chunk_size=8192):\n if chunk:\n f.write(chunk)\n logger.info(f\"[{self.provider_name}] 视频已保存: {save_path}\")\n return save_path\n\n # ==================== Embedding ====================\n\n def embed(self, text: str, model: str = \"gemini-embedding-2-preview\") -> List[float]:\n \"\"\"\n 文本 Embedding\n\n Args:\n text: 输入文本\n model: Embedding 模型名\n\n Returns:\n 向量列表\n \"\"\"\n response = self.client.embeddings.create(\n model=model,\n input=text,\n )\n return response.data[0].embedding\n\n # ==================== 工具方法 ====================\n\n def _save_or_return_image(self, url_or_data: str, save_dir: Optional[str]) -> str:\n if url_or_data.startswith(\"data:\"):\n # base64 data URI\n _, b64_data = url_or_data.split(\",\", 1)\n return self._save_b64_image(b64_data, save_dir)\n elif url_or_data.startswith(\"http\"):\n return self._download_and_save(url_or_data, save_dir)\n else:\n return url_or_data\n\n def _save_b64_image(self, b64_data: str, save_dir: Optional[str]) -> str:\n if save_dir:\n os.makedirs(save_dir, exist_ok=True)\n file_name = f\"relay_{int(time.time())}_{id(b64_data) % 10000}.png\"\n file_path = os.path.join(save_dir, file_name)\n with open(file_path, \"wb\") as f:\n f.write(base64.b64decode(b64_data))\n return file_path\n return b64_data\n\n def _download_and_save(self, url: str, save_dir: Optional[str]) -> str:\n if not save_dir:\n return url\n os.makedirs(save_dir, exist_ok=True)\n ext = os.path.splitext(url.split(\"?\")[0])[1] or \".png\"\n file_name = f\"relay_{int(time.time())}_{hash(url) % 10000}{ext}\"\n file_path = os.path.join(save_dir, file_name)\n resp = requests.get(url, timeout=120)\n resp.raise_for_status()\n with open(file_path, \"wb\") as f:\n f.write(resp.content)\n return file_path\n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n print(\"=== Relay Client (统一中转站) 可用性测试 ===\")\n api_key = Config.RELAY_API_KEY\n base_url = Config.RELAY_BASE_URL\n provider = Config.RELAY_PROVIDER_NAME\n\n if not api_key or not base_url:\n print(\"✗ RELAY_API_KEY 或 RELAY_BASE_URL 未设置,跳过\")\n print(\" 请在 .env 中配置:\")\n print(\" RELAY_API_KEY=your_relay_api_key\")\n print(\" RELAY_BASE_URL=https://api.qingyuntop.top\")\n sys.exit(1)\n\n print(f\" Provider: {provider}\")\n print(f\" API Key: {api_key[:6]}***{api_key[-4:]}\")\n print(f\" Base URL: {base_url}\")\n\n client = RelayClient(api_key=api_key, base_url=base_url)\n\n # 测试 1: LLM\n print(\"\\n--- 测试 LLM (qwen3.5-plus) ---\")\n try:\n resp = client.chat(\"用一句话介绍你自己。\", model=\"qwen3.5-plus\")\n print(f\"✓ LLM 响应: {resp[:200]}\")\n except Exception as e:\n print(f\"✗ LLM 失败: {e}\")\n\n # 测试 2: Embedding\n print(\"\\n--- 测试 Embedding (gemini-embedding-2-preview) ---\")\n try:\n vec = client.embed(\"测试文本\", model=\"gemini-embedding-2-preview\")\n print(f\"✓ Embedding 维度: {len(vec)}, 前5: {vec[:5]}\")\n except Exception as e:\n print(f\"✗ Embedding 失败: {e}\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":20633,"content_sha256":"ce9ed2c78a6d6a7c44344f12cd2d746a5c9b378426b0c59798ff1556a100f4f8"},{"filename":"aigc-claw/backend/tool/video_client.py","content":"\"\"\"\n统一视频生成客户端\n根据 model 名称自动路由到对应后端:\n - wan* → WanVideoClient (DashScope VideoSynthesis)\n - jimeng* → JiMengClient (火山引擎即梦)\n - kling* → KlingVideoClient (可灵 AI)\n\"\"\"\n\nimport os\nimport logging\nfrom typing import Optional\nfrom config import Config\n\ntry:\n from tool.video_wan import WanVideoClient\n from tool.image_jimeng import JiMengClient\n from tool.video_kling import KlingVideoClient\nexcept ImportError:\n from video_wan import WanVideoClient\n from image_jimeng import JiMengClient\n from video_kling import KlingVideoClient\n\nlogger = logging.getLogger(__name__)\n\n\nclass VideoClient:\n \"\"\"\n 统一视频生成客户端\n 参照 ImageClient 模式,按模型名路由到不同后端\n \"\"\"\n\n def __init__(\n self,\n dashscope_api_key: Optional[str] = None,\n dashscope_base_url: Optional[str] = None,\n jimeng_base_url: Optional[str] = None,\n jimeng_access_key: Optional[str] = None,\n jimeng_secret_key: Optional[str] = None,\n kling_access_key: Optional[str] = None,\n kling_secret_key: Optional[str] = None,\n kling_base_url: Optional[str] = None,\n ):\n # 万象客户端\n self.wan_client = WanVideoClient(\n api_key=dashscope_api_key,\n base_url=dashscope_base_url,\n )\n\n # 即梦客户端(图片+视频共用 HMAC 鉴权)\n self.jimeng_client = JiMengClient(\n base_url=jimeng_base_url,\n access_key=jimeng_access_key,\n secret_key=jimeng_secret_key,\n )\n\n # 可灵客户端\n self.kling_client = KlingVideoClient(\n access_key=kling_access_key,\n secret_key=kling_secret_key,\n base_url=kling_base_url,\n )\n\n def generate_video(\n self,\n prompt: str,\n image_path: str,\n save_path: str,\n model: str = \"wan2.6-i2v-flash\",\n duration: int = 5,\n shot_type: str = \"multi\",\n sound: str = \"\",\n ) -> str:\n \"\"\"\n 生成视频\n\n Args:\n prompt: 视频描述提示词\n image_path: 输入图片本地路径\n save_path: 输出视频保存路径\n model: 模型名,决定使用哪个后端\n duration: 视频时长(秒)\n shot_type: 镜头类型 \"single\" / \"multi\"\n\n Returns:\n video_url: 远端视频 URL(万象)或 task_id(即梦)\n\n Raises:\n FileNotFoundError: 输入图片不存在\n RuntimeError: 生成或下载失败\n \"\"\"\n if not model:\n model = \"wan2.6-i2v-flash\"\n\n if Config.PRINT_MODEL_INPUT:\n print(\"---- VIDEO GENERATION REQUEST ----\")\n print(f\"Prompt: {prompt}\")\n if image_path and str(image_path).startswith(\"data:\"):\n print(f\"Image: [Base64图片]\")\n else:\n print(f\"Image: {image_path}\")\n print(f\"Model: {model}\")\n print(f\"Duration: {duration}s\")\n print(f\"Shot Type: {shot_type}\")\n print(f\"Save: {save_path}\")\n print(\"-\" * 30)\n\n model_lower = model.lower()\n\n if \"jimeng\" in model_lower:\n return self._generate_jimeng(prompt, image_path, save_path, model)\n elif \"kling\" in model_lower:\n return self._generate_kling(prompt, image_path, save_path, model, duration, sound)\n else:\n return self._generate_wan(prompt, image_path, save_path, model, duration, shot_type)\n\n def _generate_wan(\n self,\n prompt: str,\n image_path: str,\n save_path: str,\n model: str,\n duration: int,\n shot_type: str,\n ) -> str:\n \"\"\"通过万象模型生成视频\"\"\"\n logger.info(f\"VideoClient: 路由至万象 model={model}\")\n return self.wan_client.generate_video(\n prompt=prompt,\n image_path=image_path,\n save_path=save_path,\n model=model,\n duration=duration,\n shot_type=shot_type,\n )\n\n def _generate_jimeng(\n self,\n prompt: str,\n image_path: str,\n save_path: str,\n model: str,\n ) -> str:\n \"\"\"通过即梦模型生成视频\"\"\"\n logger.info(f\"VideoClient: 路由至即梦 model={model}\")\n task_id = self.jimeng_client.generate_video(\n prompt=prompt,\n image_path=image_path,\n )\n\n # 轮询获取结果\n result = self.jimeng_client.poll_task(model=model, task_id=task_id)\n\n # 即梦返回的视频数据可能是 URL 或 base64\n video_url = result.get(\"video_url\", \"\")\n if video_url:\n import requests\n os.makedirs(os.path.dirname(save_path), exist_ok=True)\n resp = requests.get(video_url, stream=True, timeout=120)\n resp.raise_for_status()\n with open(save_path, 'wb') as f:\n for chunk in resp.iter_content(chunk_size=8192):\n if chunk:\n f.write(chunk)\n return video_url\n\n raise RuntimeError(f\"即梦视频生成未返回有效结果: {result}\")\n\n def _generate_kling(\n self,\n prompt: str,\n image_path: str,\n save_path: str,\n model: str,\n duration: int = 5,\n sound: str = \"\",\n ) -> str:\n \"\"\"通过可灵模型生成视频\"\"\"\n logger.info(f\"VideoClient: 路由至可灵 model={model}\")\n return self.kling_client.generate_video(\n prompt=prompt,\n image_path=image_path,\n save_path=save_path,\n model=model,\n duration=duration,\n sound=sound,\n )\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5794,"content_sha256":"f929dc5ddea3774f11dfed0eae66ba1f8fbd874b07f78ec52602846564b805a8"},{"filename":"aigc-claw/backend/tool/video_kling.py","content":"\"\"\"\n可灵(Kling AI)视频生成客户端\n基于可灵 API 的图生视频功能 (image2video)\n支持模型: kling-v3, kling-v2-6, kling-v2-5-turbo\n\"\"\"\n\nimport os\nimport io\nimport ssl\nimport time\nimport base64\nimport logging\nfrom typing import Optional\n\nimport jwt\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\nfrom PIL import Image\n\nlogger = logging.getLogger(__name__)\n\n# 可灵 API 基础地址\nKLING_BASE_URL = \"https://api-beijing.klingai.com\"\n\n\nclass _TLSAdapter(HTTPAdapter):\n \"\"\"强制 TLS 1.2 的 HTTPS 适配器,兼容老版本 LibreSSL\"\"\"\n\n def init_poolmanager(self, *args, **kwargs):\n ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)\n ctx.minimum_version = ssl.TLSVersion.TLSv1_2\n ctx.maximum_version = ssl.TLSVersion.TLSv1_2\n ctx.load_default_certs()\n kwargs[\"ssl_context\"] = ctx\n return super().init_poolmanager(*args, **kwargs)\n\n\ndef _build_session(max_retries: int = 3) -> requests.Session:\n \"\"\"创建带 TLS 适配器和自动重试的 requests Session\"\"\"\n session = requests.Session()\n retry = Retry(\n total=max_retries,\n backoff_factor=1,\n status_forcelist=[502, 503, 504],\n allowed_methods=[\"GET\", \"POST\"],\n )\n adapter = _TLSAdapter(max_retries=retry)\n session.mount(\"https://\", adapter)\n return session\n\n\nclass KlingVideoClient:\n \"\"\"\n 可灵 AI 图生视频客户端\n 使用 JWT (HMAC-SHA256) 鉴权,调用 /v1/videos/image2video 接口\n \"\"\"\n\n def __init__(\n self,\n access_key: Optional[str] = None,\n secret_key: Optional[str] = None,\n base_url: Optional[str] = None,\n token_ttl: int = 1800,\n poll_interval: int = 5,\n max_polls: int = 120,\n ) -> None:\n \"\"\"\n Args:\n access_key: 可灵 API Access Key\n secret_key: 可灵 API Secret Key\n base_url: 可灵 API 基础 URL (默认北京节点)\n token_ttl: JWT 有效期(秒),默认 30 分钟\n poll_interval: 轮询间隔(秒)\n max_polls: 最大轮询次数\n \"\"\"\n self.access_key = access_key or os.getenv(\"KLING_ACCESS_KEY\", \"\")\n self.secret_key = secret_key or os.getenv(\"KLING_SECRET_KEY\", \"\")\n self.base_url = (base_url or os.getenv(\"KLING_BASE_URL\", \"\")).rstrip(\"/\") or KLING_BASE_URL\n self.token_ttl = token_ttl\n self.poll_interval = poll_interval\n self.max_polls = max_polls\n\n if not self.access_key or not self.secret_key:\n logger.warning(\n \"KlingVideoClient: KLING_ACCESS_KEY / KLING_SECRET_KEY 未设置,请检查配置\"\n )\n\n # 使用强制 TLS 1.2 + 自动重试的 Session\n self._session = _build_session()\n\n # ─── JWT 鉴权 ───\n\n def _generate_token(self) -> str:\n \"\"\"\n 使用 Access Key / Secret Key 生成 JWT Token\n 算法: HS256\n Payload:\n - iss: Access Key\n - iat: 签发时间\n - exp: 过期时间\n - nbf: 生效时间\n \"\"\"\n now = int(time.time())\n payload = {\n \"iss\": self.access_key,\n \"iat\": now,\n \"exp\": now + self.token_ttl,\n \"nbf\": now - 5, # 允许 5 秒时钟偏差\n }\n token = jwt.encode(payload, self.secret_key, algorithm=\"HS256\")\n return token\n\n def _auth_headers(self) -> dict:\n \"\"\"构建带 JWT 鉴权的请求头\"\"\"\n token = self._generate_token()\n return {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {token}\",\n }\n\n # ─── 图片处理 ───\n\n @staticmethod\n def _encode_image(image_path: str, quality: int = 85) -> str:\n \"\"\"\n 将本地图片编码为 Base64 字符串\n 可灵要求:不添加 data:image/xxx;base64, 前缀,直接传 Base64 字符串\n 图片大小 ≤ 10MB,宽高 ≥ 300px,宽高比 1:2.5 ~ 2.5:1\n \"\"\"\n try:\n with Image.open(image_path) as img:\n if img.mode in (\"RGBA\", \"P\"):\n img = img.convert(\"RGB\")\n buf = io.BytesIO()\n img.save(buf, format=\"JPEG\", quality=quality)\n return base64.b64encode(buf.getvalue()).decode(\"utf-8\")\n except Exception as e:\n logger.warning(f\"图片压缩失败 ({image_path}),使用原始文件: {e}\")\n with open(image_path, \"rb\") as f:\n return base64.b64encode(f.read()).decode(\"utf-8\")\n\n # ─── 创建任务 ───\n\n def _submit_task(\n self,\n image_path: str,\n prompt: str = \"\",\n negative_prompt: str = \"\",\n model_name: str = \"kling-v3\",\n mode: str = \"std\",\n duration: str = \"5\",\n cfg_scale: float = 0.5,\n sound: str = \"\",\n ) -> str:\n \"\"\"\n 提交图生视频任务\n\n Args:\n image_path: 本地图片路径\n prompt: 正向提示词(≤2500字符)\n negative_prompt: 负向提示词(≤2500字符)\n model_name: 可灵模型名 (kling-v3 / kling-v2-6 / kling-v2-5-turbo)\n mode: 生成模式 std (标准) / pro (高品质)\n duration: 视频时长,v3: \"3\"~\"15\", v2: \"5\"或\"10\"\n cfg_scale: 自由度 [0,1],越大越贴合提示词\n sound: 是否生成声音 \"on\"/\"off\"\n\n Returns:\n task_id: 任务 ID\n \"\"\"\n if not os.path.exists(image_path):\n raise FileNotFoundError(f\"输入图片不存在: {image_path}\")\n\n # 根据模型系列确定 duration 范围\n model_lower = model_name.lower()\n is_v3 = \"v3\" in model_lower or \"video-o1\" in model_lower\n is_v26 = any(tag in model_lower for tag in (\"v2-6\", \"v2.6\"))\n\n if is_v3:\n # v3 系列支持 3~15s\n clamped = str(min(max(int(duration), 3), 15))\n else:\n # v2 系列仅支持 5 或 10\n clamped = str(min(max(int(duration), 5), 10))\n\n image_b64 = self._encode_image(image_path)\n\n body = {\n \"model_name\": model_name,\n \"image\": image_b64,\n \"mode\": mode,\n \"duration\": clamped,\n }\n\n # sound 参数处理\n # v3 / v2-6: 默认开启声音,除非显式 sound=\"off\"\n # v2-6 的 sound=on 必须搭配 pro 模式\n # kling-v2-5-turbo 不支持 sound\n if is_v3 or is_v26:\n if sound == \"off\":\n body[\"sound\"] = \"off\"\n else:\n body[\"sound\"] = \"on\"\n # v2-6 的 sound=on 必须搭配 pro 模式; v3 无此限制\n if is_v26 and mode != \"pro\":\n mode = \"pro\"\n body[\"mode\"] = mode\n logger.info(\"KlingVideoClient: v2-6 sound=on 需要 pro 模式,已自动切换\")\n elif sound == \"on\":\n logger.warning(f\"KlingVideoClient: 模型 {model_name} 不支持 sound 参数,已忽略\")\n\n if prompt:\n body[\"prompt\"] = prompt\n if negative_prompt:\n body[\"negative_prompt\"] = negative_prompt\n\n url = f\"{self.base_url}/v1/videos/image2video\"\n headers = self._auth_headers()\n\n logger.info(f\"KlingVideoClient: 提交任务 model={model_name}, mode={mode}, duration={clamped}s, sound={body.get('sound', 'off')}\")\n\n resp = self._session.post(url, json=body, headers=headers, timeout=120)\n if not resp.ok:\n try:\n err_body = resp.json()\n except Exception:\n err_body = resp.text\n logger.error(f\"KlingVideoClient: HTTP {resp.status_code}, 响应: {err_body}\")\n resp.raise_for_status()\n data = resp.json()\n\n if data.get(\"code\") != 0:\n raise RuntimeError(\n f\"可灵 API 错误: code={data.get('code')}, message={data.get('message')}\"\n )\n\n task_id = data[\"data\"][\"task_id\"]\n logger.info(f\"KlingVideoClient: 任务已提交 task_id={task_id}\")\n return task_id\n\n # ─── 查询任务 ───\n\n def _query_task(self, task_id: str) -> dict:\n \"\"\"\n 查询单个任务状态\n\n Returns:\n API 响应中的 data 字段\n \"\"\"\n url = f\"{self.base_url}/v1/videos/image2video/{task_id}\"\n headers = self._auth_headers()\n\n resp = self._session.get(url, headers=headers, timeout=30)\n resp.raise_for_status()\n data = resp.json()\n\n if data.get(\"code\") != 0:\n raise RuntimeError(\n f\"可灵查询 API 错误: code={data.get('code')}, message={data.get('message')}\"\n )\n\n return data[\"data\"]\n\n # ─── 轮询等待 ───\n\n def _poll_until_done(self, task_id: str) -> dict:\n \"\"\"\n 轮询任务直到完成或失败\n\n Returns:\n 任务结果数据\n\n Raises:\n RuntimeError: 任务失败\n TimeoutError: 超过最大轮询次数\n \"\"\"\n for attempt in range(self.max_polls):\n result = self._query_task(task_id)\n status = result.get(\"task_status\", \"\")\n\n if status == \"succeed\":\n logger.info(f\"KlingVideoClient: 任务完成 task_id={task_id}\")\n return result\n elif status == \"failed\":\n msg = result.get(\"task_status_msg\", \"未知错误\")\n raise RuntimeError(f\"可灵视频生成失败: {msg} (task_id={task_id})\")\n else:\n # submitted / processing\n logger.debug(\n f\"KlingVideoClient: 任务进行中 task_id={task_id}, \"\n f\"status={status}, attempt={attempt + 1}/{self.max_polls}\"\n )\n time.sleep(self.poll_interval)\n\n raise TimeoutError(f\"可灵视频生成超时 (task_id={task_id}, 已等待 {self.max_polls * self.poll_interval}s)\")\n\n # ─── 下载视频 ───\n\n @staticmethod\n def _download_video(video_url: str, save_path: str) -> None:\n \"\"\"从 URL 下载视频到本地\"\"\"\n save_dir = os.path.dirname(save_path)\n if save_dir:\n os.makedirs(save_dir, exist_ok=True)\n # 下载也用 TLS 安全 Session\n dl_session = _build_session(max_retries=2)\n resp = dl_session.get(video_url, stream=True, timeout=120)\n resp.raise_for_status()\n with open(save_path, \"wb\") as f:\n for chunk in resp.iter_content(chunk_size=8192):\n if chunk:\n f.write(chunk)\n logger.info(f\"KlingVideoClient: 视频已保存: {save_path}\")\n\n # ─── 主入口 ───\n\n def generate_video(\n self,\n prompt: str,\n image_path: str,\n save_path: str,\n model: str = \"kling-v3\",\n duration: int = 5,\n mode: str = \"std\",\n cfg_scale: float = 0.5,\n negative_prompt: str = \"\",\n sound: str = \"\",\n ) -> str:\n \"\"\"\n 图生视频完整流程:提交任务 → 轮询等待 → 下载视频\n\n Args:\n prompt: 视频描述提示词\n image_path: 输入图片本地路径\n save_path: 输出视频保存路径\n model: 可灵模型名 (kling-v3 / kling-v2-6 / kling-v2-5-turbo)\n duration: 视频时长(秒),v3: 3~15, v2: 5或10\n mode: 生成模式 \"std\" (标准) 或 \"pro\" (高品质)\n cfg_scale: 自由度 [0,1]\n negative_prompt: 负向提示词\n sound: 是否生成声音 \"on\"/\"off\"\n\n Returns:\n video_url: 远端视频 URL\n \"\"\"\n # 1. 提交任务\n task_id = self._submit_task(\n image_path=image_path,\n prompt=prompt,\n negative_prompt=negative_prompt,\n model_name=model,\n mode=mode,\n duration=str(duration),\n cfg_scale=cfg_scale,\n sound=sound,\n )\n\n # 2. 轮询等待\n result = self._poll_until_done(task_id)\n\n # 3. 提取视频 URL\n videos = result.get(\"task_result\", {}).get(\"videos\", [])\n if not videos:\n raise RuntimeError(f\"可灵任务成功但未返回视频数据 (task_id={task_id})\")\n\n video_url = videos[0].get(\"url\", \"\")\n if not video_url:\n raise RuntimeError(f\"可灵任务成功但视频 URL 为空 (task_id={task_id})\")\n\n # 4. 下载到本地\n self._download_video(video_url, save_path)\n\n return video_url\n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n logging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\")\n\n # ── 测试参数(按需修改) ──\n IMAGE_PATH = \"code/result/image/test_avail/test_input.png\"\n OUTPUT_PATH = \"code/result/video/test_avail/kling_test_output.mp4\"\n PROMPT = \"\"\n MODEL = \"kling-v3\" # kling-v3 / kling-v2-6 / kling-v2-5-turbo\n DURATION = 5 # v3: 3~15, v2: 5 或 10\n MODE = \"std\" # std 或 pro\n SOUND = \"\" # \"\" = 自动开启, \"on\", \"off\"\n\n print(\"=== 可灵 (Kling) 图生视频测试 ===\")\n ak = Config.KLING_ACCESS_KEY\n sk = Config.KLING_SECRET_KEY\n base_url = Config.KLING_BASE_URL\n if not ak or not sk:\n print(\"✗ KLING_ACCESS_KEY / KLING_SECRET_KEY 未设置,请检查 .env 配置\")\n sys.exit(1)\n\n if not os.path.exists(IMAGE_PATH):\n print(f\"✗ 输入图片不存在: {IMAGE_PATH}\")\n sys.exit(1)\n\n print(f\" Access Key : {ak[:6]}***{ak[-4:]}\")\n print(f\" Base URL : {base_url}\")\n print(f\" 输入图片 : {IMAGE_PATH}\")\n print(f\" 输出路径 : {OUTPUT_PATH}\")\n print(f\" 模型 : {MODEL}\")\n print(f\" 时长 : {DURATION}s\")\n print(f\" 模式 : {MODE}\")\n print(f\" 声音 : {SOUND or '自动'}\")\n if PROMPT:\n print(f\" 提示词 : {PROMPT[:80]}\")\n print(\"-\" * 40)\n\n try:\n client = KlingVideoClient(access_key=ak, secret_key=sk, base_url=base_url)\n print(\"✓ 客户端初始化成功\")\n\n start = time.time()\n video_url = client.generate_video(\n prompt=PROMPT,\n image_path=IMAGE_PATH,\n save_path=OUTPUT_PATH,\n model=MODEL,\n duration=DURATION,\n mode=MODE,\n sound=SOUND,\n )\n elapsed = time.time() - start\n\n print(f\"✓ 视频生成完成!耗时 {elapsed:.1f}s\")\n print(f\" 远端 URL : {video_url}\")\n print(f\" 本地文件 : {os.path.abspath(OUTPUT_PATH)}\")\n print(f\" 文件大小 : {os.path.getsize(OUTPUT_PATH) / 1024 / 1024:.2f} MB\")\n except Exception as e:\n print(f\"✗ 失败: {e}\")\n sys.exit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15004,"content_sha256":"35e71711039d64cedee20e0690687e8a0f3f3ebb6a048c8363c171e04952bbc6"},{"filename":"aigc-claw/backend/tool/video_wan.py","content":"\"\"\"\n通义万象(Wan)视频生成客户端\n基于 DashScope SDK (dashscope.VideoSynthesis)\n支持 wan2.6-i2v-flash 等模型的图生视频功能\n\"\"\"\n\nimport os\nimport logging\nfrom typing import Optional\nfrom http import HTTPStatus\n\nimport dashscope\nfrom dashscope import VideoSynthesis\nimport requests\n\nlogger = logging.getLogger(__name__)\n\n\nclass WanVideoClient:\n \"\"\"\n 阿里云通义万象视频生成客户端\n 使用 dashscope SDK 的 VideoSynthesis 接口\n \"\"\"\n\n def __init__(\n self,\n api_key: Optional[str] = None,\n base_url: Optional[str] = None,\n ) -> None:\n self.api_key = api_key or os.getenv(\"DASHSCOPE_API_KEY\")\n self.base_url = base_url or os.getenv(\"DASHSCOPE_BASE_URL\")\n\n if self.api_key:\n dashscope.api_key = self.api_key\n if self.base_url:\n dashscope.base_http_api_url = self.base_url\n\n def generate_video(\n self,\n prompt: str,\n image_path: str,\n save_path: str,\n model: str = \"wan2.6-i2v-flash\",\n duration: int = 10,\n shot_type: str = \"multi\",\n ) -> str:\n \"\"\"\n 图生视频:提交任务 → 等待完成 → 下载到本地\n\n Args:\n prompt: 视频描述提示词\n image_path: 输入图片本地路径\n save_path: 输出视频保存路径\n model: 万象视频模型名\n duration: 视频时长(秒),5-10\n shot_type: 镜头类型,\"single\" 或 \"multi\"\n\n Returns:\n video_url: 远端视频 URL\n\n Raises:\n FileNotFoundError: 输入图片不存在\n RuntimeError: API 调用或下载失败\n \"\"\"\n if not os.path.exists(image_path):\n raise FileNotFoundError(f\"输入图片不存在: {image_path}\")\n\n abs_img = os.path.abspath(image_path)\n img_url = f\"file://{abs_img}\"\n\n logger.info(f\"WanVideoClient: model={model}, prompt={prompt[:60]}...\")\n\n rsp = VideoSynthesis.call(\n api_key=self.api_key,\n model=model,\n prompt=prompt,\n img_url=img_url,\n duration=duration,\n shot_type=shot_type,\n )\n\n if rsp.status_code != HTTPStatus.OK:\n raise RuntimeError(\n f\"万象视频 API 错误: status={rsp.status_code}, \"\n f\"code={rsp.code}, message={rsp.message}\"\n )\n\n video_url = rsp.output.video_url\n # 检查是否返回了有效的视频URL\n if not video_url:\n raise RuntimeError(f\"万象视频 API 返回空URL,可能生成失败: code={rsp.code}, message={rsp.message}\")\n\n logger.info(f\"WanVideoClient: 视频生成成功: {video_url}\")\n\n # 确保输出目录存在\n os.makedirs(os.path.dirname(save_path), exist_ok=True)\n\n # 下载视频\n resp = requests.get(video_url, stream=True, timeout=120)\n if resp.status_code != 200:\n raise RuntimeError(f\"视频下载失败: HTTP {resp.status_code}\")\n\n with open(save_path, 'wb') as f:\n for chunk in resp.iter_content(chunk_size=8192):\n if chunk:\n f.write(chunk)\n\n logger.info(f\"WanVideoClient: 视频已保存: {save_path}\")\n return video_url\n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n print(\"=== 万象 (Wan) 视频客户端可用性测试 ===\")\n api_key = Config.DASHSCOPE_API_KEY\n base_url = Config.DASHSCOPE_BASE_URL\n if not api_key:\n print(\"✗ DASHSCOPE_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***{api_key[-4:]}\")\n print(f\" Base URL: {base_url}\")\n try:\n client = WanVideoClient(api_key=api_key, base_url=base_url)\n print(\"✓ 客户端初始化成功\")\n print(\" (视频生成需要图片输入且耗时数分钟,仅验证初始化)\")\n except Exception as e:\n print(f\"✗ 初始化失败: {e}\")\n sys.exit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4112,"content_sha256":"e475c3ad1ce79f9a040ac58279c517f9019def59121099b6420db18c2882368b"},{"filename":"aigc-claw/backend/tool/vlm_client.py","content":"import os\nfrom typing import List, Optional\nfrom config import Config\n\ntry:\n from tool.vlm_dashscope import QwenVLClient\n from tool.vlm_gemini import GeminiVLClient\n from tool.relay_client import RelayClient\nexcept ImportError:\n from vlm_dashscope import QwenVLClient\n from vlm_gemini import GeminiVLClient\n from relay_client import RelayClient\n\nclass VLM:\n def __init__(self,\n dashscope_api_key: Optional[str] = None,\n dashscope_base_url: Optional[str] = None,\n gemini_api_key: Optional[str] = None,\n gemini_base_url: Optional[str] = None,\n local_proxy: Optional[str] = None):\n \"\"\"\n Unified VLM (Vision Language Model) Client\n Routes requests to DashScope (QwenVL), Gemini, or Relay based on model name.\n \"\"\"\n # Initialize DashScope Client\n self.dashscope_client = QwenVLClient(\n api_key=dashscope_api_key,\n base_url=dashscope_base_url\n )\n # Initialize Gemini Client\n self.gemini_client = GeminiVLClient(\n api_key=gemini_api_key,\n base_url=gemini_base_url\n )\n\n # Initialize Relay Client (中转站)\n self._relay_client = None\n relay_key = os.getenv(\"RELAY_API_KEY\", \"\")\n relay_url = os.getenv(\"RELAY_BASE_URL\", \"\")\n if relay_key and relay_url:\n try:\n self._relay_client = RelayClient(api_key=relay_key, base_url=relay_url)\n except Exception:\n pass\n\n def query(self,\n prompt: str,\n image_paths: Optional[List[str]] = None,\n model: str = \"qwen3.5-plus\",\n session_id: Optional[str] = None) -> str:\n if Config.PRINT_MODEL_INPUT:\n print(\"---- VLM REQUEST ----\")\n print(f\"Prompt: {prompt}\")\n if image_paths:\n print(f\"Images: {len(image_paths)}\")\n for p in image_paths:\n if p.startswith(\"data:\"):\n print(f\" - [Base64图片]\")\n else:\n print(f\" - {p}\")\n print(f\"Model: {model}\")\n if session_id:\n print(f\"Session ID: {session_id}\")\n print(\"-\" * 30)\n\n # Determine backend provider\n model_lower = model.lower()\n is_relay = self._is_relay_model(model_lower)\n\n if is_relay and self._relay_client:\n # 通过中转站调用 VLM\n return self._relay_client.vlm_chat(\n prompt=prompt, image_paths=image_paths, model=model\n )\n elif \"gemini\" in model_lower:\n # 处理图片路径\n processed_images = []\n for p in image_paths or []:\n if p.startswith(\"data:\") or p.startswith(\"http\") or p.startswith(\"file://\"):\n processed_images.append(p)\n else:\n processed_images.append(p) # 传递原始路径,内部会处理\n return self.gemini_client.chat(text=prompt, images=processed_images, model=model)\n else:\n # Qwen (DashScope) - 需要将 base64 保存为临时文件\n file_urls = []\n import tempfile\n import base64 as b64\n\n for p in image_paths or []:\n if p.startswith(\"data:\"):\n # Base64 数据 URL,需要解码并保存为临时文件\n try:\n # 解析 data URL: data:image/png;base64,xxxxx\n header, b64_data = p.split(\",\", 1)\n mime_type = header.split(\";\")[0].replace(\"data:\", \"\")\n image_data = b64.b64decode(b64_data)\n\n # 创建临时文件\n suffix = f\".{mime_type.split('/')[-1]}\" if '/' in mime_type else \".png\"\n with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:\n tmp.write(image_data)\n temp_path = tmp.name\n\n abs_path = os.path.abspath(temp_path)\n file_urls.append(f\"file://{abs_path}\")\n except Exception as e:\n print(f\"Error processing base64 image: {e}\")\n raise ValueError(f\"无法解析 base64 图片: {e}\")\n elif p.startswith(\"http\") or p.startswith(\"file://\"):\n file_urls.append(p)\n else:\n abs_path = os.path.abspath(p)\n file_urls.append(f\"file://{abs_path}\")\n return self.dashscope_client.chat(text=prompt, images=file_urls, model=model, stream=False)\n\n def _is_relay_model(self, model_lower: str) -> bool:\n \"\"\"判断模型是否应通过中转站调用\"\"\"\n if not self._relay_client:\n return False\n try:\n import json\n config_path = os.path.join(\n os.path.dirname(os.path.dirname(os.path.abspath(__file__))),\n \"config_model.json\"\n )\n if os.path.exists(config_path):\n with open(config_path, \"r\", encoding=\"utf-8\") as f:\n config = json.load(f)\n model_info = config.get(\"models\", {}).get(model_lower)\n if model_info and model_info.get(\"provider\") == \"relay\":\n return True\n except Exception:\n pass\n return False","content_type":"text/x-python; charset=utf-8","language":"python","size":5522,"content_sha256":"b51e6ac01762fcb28f2cc448c6b0535e70ad41be9f75e2b4890d9e8180b40a97"},{"filename":"aigc-claw/backend/tool/vlm_dashscope.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\nQwen3.5-VL 多模态大模型 API 客户端(DashScope 多模态接口专用)\n只支持 Qwen3.5-VL 及兼容 DashScope 多模态对话接口\n参考官方文档:https://help.aliyun.com/zh/model-studio/qwen-api-reference\n\"\"\"\n\nimport os\n\nimport dashscope\nfrom dashscope import MultiModalConversation\nimport logging\n\nlogger = logging.getLogger(__name__)\nfrom typing import Any, Dict, List, Optional\n\nclass QwenVLClient:\n def __init__(self,\n api_key: Optional[str] = None, \n base_url: Optional[str] = None):\n \"\"\"\n Qwen3.5-VL 多模态客户端\n :param api_key: DashScope/Qwen3.5 API Key\n :param model: 模型名(如 qwen3.5-plus/qwen3.5-max 等)\n \"\"\"\n self.api_key = api_key or os.getenv(\"DASHSCOPE_API_KEY\")\n\n def chat(self, text: str, images: List[str], model: str, stream: bool = False, parameters: Optional[Dict] = None, **kwargs) -> Any:\n \"\"\"\n 使用阿里云 dashscope SDK 进行多模态对话(文本+图片),风格与 image_dashscope.py 一致。\n :param text: 文本内容\n :param images: 图片路径列表(支持本地路径或URL,内部会转换为file://绝对路径)\n :param model: 模型名(支持qwen3.5-plus, qwen3-vl-plus)\n :param stream: 是否流式输出(暂不支持流式)\n :param parameters: 其他API参数\n :return: API响应内容 dict\n \"\"\"\n dashscope.api_key = self.api_key\n # 只支持非流式\n try:\n messages = [{\"role\": \"user\", \"content\": [\n {\"text\": text},\n *({\"image\": p} for p in images)\n ]}]\n response = MultiModalConversation.call(\n model=model,\n messages=messages,\n api_key=self.api_key,\n enable_thinking=False,\n **(parameters or {})\n )\n if hasattr(response, 'status_code') and response.status_code == 200:\n # qwen3.5-plus 的返回格式为 { choices: [ { message: { content: [...] } } ] }\n resp = response.output.choices[0].message.content[0]\n if resp.get('text'):\n return resp['text']\n return resp\n else:\n raise RuntimeError(f\"DashScope QwenVLClient failed: {getattr(response, 'message', response)}\")\n except Exception as e:\n raise RuntimeError(f\"DashScope QwenVLClient error: {e}\")\n\n\nif __name__ == \"__main__\":\n import sys\n import time\n import json\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n # 支持的 VLM 模型列表\n MODELS = [\"qwen3.5-plus\", \"qwen-vl-plus\", \"qwen3.5-max\"]\n\n print(\"=== Qwen VL (DashScope) 多模态可用性测试 ===\")\n api_key = getattr(Config, \"DASHSCOPE_API_KEY\", None) or os.getenv(\"DASHSCOPE_API_KEY\", \"\")\n if not api_key:\n print(\"✗ DASHSCOPE_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***{api_key[-4:]}\")\n client = QwenVLClient(api_key=api_key)\n\n # 测试图片\n img_path = ''\n abs_img_path = os.path.abspath(img_path)\n if not os.path.exists(img_path):\n img_path = \"backend/code/result/image/test_avail/test_input.png\"\n abs_img_path = os.path.abspath(img_path)\n if not os.path.exists(img_path):\n print(\"✗ 测试图片不存在,跳过\")\n sys.exit(0)\n\n text = \"请描述这张图片的内容\"\n print(f\"\\n[多模态] Prompt: {text}\")\n print(f\" 图片: {img_path}\")\n\n for model in MODELS:\n print(f\"\\n--- 测试模型: {model} ---\")\n t0 = time.time()\n try:\n result = client.chat(text=text, images=[img_path], model=model, stream=False)\n elapsed = time.time() - t0\n if result:\n print(f\"✓ 返回结果 ({elapsed:.1f}s): {str(result)[:200]}\")\n else:\n print(f\"✗ 返回空结果 ({elapsed:.1f}s)\")\n except Exception as e:\n print(f\"✗ 失败: {e}\")","content_type":"text/x-python; charset=utf-8","language":"python","size":4149,"content_sha256":"beaf54484a54ffd90fb4cfc91da0571d8d3101ee5877294e05b770ae4450111f"},{"filename":"aigc-claw/backend/tool/vlm_gemini.py","content":"# -*- coding: utf-8 -*-\n\"\"\"\nGoogle Gemini 多模态大模型 API 客户端 (OpenAI 兼容格式)\n支持 gemini-2.5-flash-image, gemini-2.5-pro 等视觉模型\n\n可用模型:\n - gemini-2.5-flash-image (性价比最高)\n - gemini-2.5-pro (效果最好)\n - gemini-3-pro-preview\n - gemini-3-pro-image-preview\n\"\"\"\n\nimport os\nimport time\nimport base64\nfrom openai import OpenAI\nfrom typing import Dict, List, Optional\n\nclass GeminiVLClient:\n \"\"\"\n Gemini VLM 客户端,使用 OpenAI 兼容格式调用\n \"\"\"\n def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):\n \"\"\"\n Gemini 多模态客户端\n :param api_key: Gemini API Key\n :param base_url: 自定义 Base URL(可选,用于代理)\n \"\"\"\n self.api_key = api_key or os.getenv(\"GEMINI_API_KEY\")\n default_url = \"http://35.164.11.19:3887/v1\"\n self.base_url = base_url or os.getenv(\"GOOGLE_GEMINI_BASE_URL\", default_url)\n if self.base_url and not self.base_url.endswith(\"/v1\"):\n self.base_url = self.base_url.rstrip(\"/\") + \"/v1\"\n self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)\n self.max_attempts = 10\n self.max_tokens = 20000\n\n def _encode_image(self, image_path: str) -> str:\n \"\"\"将本地图片编码为 base64\"\"\"\n abs_path = os.path.abspath(image_path)\n with open(abs_path, \"rb\") as f:\n return base64.b64encode(f.read()).decode(\"utf-8\")\n\n def _get_mime_type(self, image_path: str) -> str:\n \"\"\"根据文件扩展名获取 MIME 类型\"\"\"\n ext = os.path.splitext(image_path)[1].lower()\n mime_types = {\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".png\": \"image/png\",\n \".webp\": \"image/webp\",\n \".gif\": \"image/gif\"\n }\n return mime_types.get(ext, \"image/jpeg\")\n\n def chat(self, text: str, images: List[str], model: str = \"gemini-2.5-flash-image\",\n parameters: Optional[Dict] = None) -> str:\n \"\"\"\n 使用 Gemini 进行多模态对话(文本+图片)\n :param text: 文本内容\n :param images: 图片路径列表(支持本地路径或URL)\n :param model: 模型名(如 gemini-2.5-flash-image, gemini-2.5-pro)\n :param parameters: 其他API参数\n :return: API响应内容\n \"\"\"\n # 构建消息格式\n content: list = [{\"type\": \"text\", \"text\": text}]\n\n # 处理图片\n if images:\n for img_path in images:\n if img_path.startswith(\"data:\"):\n # Base64 数据 URL,直接使用\n content.append({\n \"type\": \"image_url\",\n \"image_url\": {\"url\": img_path}\n })\n elif img_path.startswith(\"http\"):\n # URL 图片\n content.append({\n \"type\": \"image_url\",\n \"image_url\": {\"url\": img_path}\n })\n else:\n # 本地图片 - 转为 base64\n mime_type = self._get_mime_type(img_path)\n base64_data = self._encode_image(img_path)\n data_url = f\"data:{mime_type};base64,{base64_data}\"\n content.append({\n \"type\": \"image_url\",\n \"image_url\": {\"url\": data_url}\n })\n\n messages = [{\"role\": \"user\", \"content\": content}]\n\n attempts = 0\n while attempts \u003c self.max_attempts:\n try:\n # 直接使用模型名\n response = self.client.chat.completions.create(\n model=model,\n messages=messages,\n max_tokens=self.max_tokens,\n temperature=parameters.get(\"temperature\", 0.7) if parameters else 0.7\n )\n\n if response.choices and len(response.choices) > 0:\n return response.choices[0].message.content\n\n except Exception as e:\n print(f\"GeminiVL 请求错误: {e}\")\n attempts += 1\n if attempts \u003c self.max_attempts:\n time.sleep(10)\n\n raise Exception(\"GeminiVL: 达到最大重试次数,仍未获得有效响应。\")\n\n\nif __name__ == \"__main__\":\n import sys\n sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n from config import Config\n\n # 支持的 VLM 模型列表\n MODELS = [\"gemini-2.5-flash-image\", \"gemini-2.0-flash\"]\n\n print(\"=== Gemini VL 多模态可用性测试 ===\")\n api_key = Config.GEMINI_API_KEY\n if not api_key:\n print(\"✗ GEMINI_API_KEY 未设置,跳过\")\n sys.exit(1)\n print(f\" API Key: {api_key[:6]}***\")\n client = GeminiVLClient(api_key=api_key)\n\n # 测试图片(使用示例图片)\n img_path = \"\"\n if not os.path.exists(img_path):\n print(f\"✗ 测试图片不存在: {img_path}\")\n img_path = \"backend/code/result/image/test_avail/test_input.png\"\n if not os.path.exists(img_path):\n print(\"✗ 跳过图片测试(无测试图片)\")\n sys.exit(0)\n\n text = \"请描述这张图片的内容\"\n print(f\"\\n[多模态] Prompt: {text}\")\n print(f\" 图片: {img_path}\")\n\n for model in MODELS:\n print(f\"\\n--- 测试模型: {model} ---\")\n t0 = time.time()\n try:\n result = client.chat(text=text, images=[img_path], model=model)\n elapsed = time.time() - t0\n if result:\n print(f\"✓ 返回结果 ({elapsed:.1f}s): {result[:200]}\")\n else:\n print(f\"✗ 返回空结果 ({elapsed:.1f}s)\")\n except Exception as e:\n print(f\"✗ 失败: {e}\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5887,"content_sha256":"2040cb140a5d521c33564f5295b354b44bd0ebcdb5957938b6fb14766d94253c"},{"filename":"aigc-claw/frontend/app/globals.css","content":"@import \"tailwindcss\";\n\n:root {\n --background: #ffffff;\n --foreground: #101014;\n}\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-ds-bg: #ffffff;\n --color-ds-sidebar: #f9fafb;\n --color-ds-input: #e5e7eb;\n --color-ds-input-bg: #ffffff;\n --color-ds-text: #111827;\n --color-ds-hover: #f3f4f6;\n --color-ds-blue: #4d6bfe;\n}\n\nbody {\n background-color: var(--background);\n color: var(--foreground);\n}\n\n/* Custom Scrollbar */\n::-webkit-scrollbar {\n width: 6px;\n}\n::-webkit-scrollbar-track {\n background: transparent;\n}\n::-webkit-scrollbar-thumb {\n background: #d1d5db;\n border-radius: 3px;\n}\n","content_type":"text/css; charset=utf-8","language":"css","size":671,"content_sha256":"162fed8deaa1e41b90f35645771a16e74c2478c5237608f1f5b223a5e4033d40"},{"filename":"aigc-claw/frontend/app/layout.tsx","content":"import type { Metadata } from \"next\";\nimport \"./globals.css\";\n\nexport const metadata: Metadata = {\n title: \"AI导演工作室\",\n description: \"AI视频生成工具\",\n};\n\nexport default function RootLayout({\n children,\n}: Readonly\u003c{\n children: React.ReactNode;\n}>) {\n return (\n \u003chtml lang=\"zh-CN\">\n \u003cbody\n className=\"antialiased\"\n >\n {children}\n \u003c/body>\n \u003c/html>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":409,"content_sha256":"ce793ca9abbd90175e6f3e1e9a70c43498103a6584a71e0222b17062eaa5d43c"},{"filename":"aigc-claw/frontend/app/page.tsx","content":"'use client';\n\nimport { Suspense } from 'react';\nimport WorkflowPanel from '@/components/WorkflowPanel';\n\nfunction Loading() {\n return (\n \u003cdiv className=\"flex items-center justify-center h-screen bg-gray-50\">\n \u003cdiv className=\"text-center\">\n \u003cdiv className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4\">\u003c/div>\n \u003cp className=\"text-gray-500\">加载中...\u003c/p>\n \u003c/div>\n \u003c/div>\n );\n}\n\nexport default function Home() {\n return (\n \u003cSuspense fallback={\u003cLoading />}>\n \u003cWorkflowPanel />\n \u003c/Suspense>\n );\n}\n\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":573,"content_sha256":"9cfe292674ebf67de8bf02f2fbafaa87f17a5b67fc5b5f12d868dda2a5e92d6f"},{"filename":"aigc-claw/frontend/app/sandbox/page.tsx","content":"import { Suspense } from 'react';\nimport Sandbox from '@/components/Sandbox/Sandbox';\n\nexport default function SandboxPage() {\n return (\n \u003cSuspense fallback={\u003cdiv className=\"p-8\">加载中...\u003c/div>}>\n \u003cSandbox />\n \u003c/Suspense>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":245,"content_sha256":"a18cfbb371458dff3488aafa590918bb765bdb526fa26b91f3e43ef6cda6ca17"},{"filename":"aigc-claw/frontend/components/HomePage.tsx","content":"'use client';\n\nimport React, { useState } from 'react';\nimport { Sparkles, Play, Settings2, Clock, ArrowRight, Zap, CheckCircle, Trash2, X, Lock, Globe } from 'lucide-react';\nimport clsx from 'clsx';\nimport { PROMPT_EXAMPLES } from '@/config/examples';\nimport { LLM_MODELS, T2I_MODELS, I2I_MODELS, VIDEO_MODELS, VLM_MODELS, STYLES, VIDEO_RATIOS, LLM_PROVIDERS, T2I_PROVIDERS, I2I_PROVIDERS, VIDEO_PROVIDERS, VLM_PROVIDERS, ProviderGroup } from '@/config/models';\nimport { STAGES } from './TopBar';\n\nexport interface ProjectParams {\n idea: string;\n style: string;\n video_ratio: string;\n llm_model: string;\n vlm_model: string;\n image_t2i_model: string;\n image_it2i_model: string;\n video_model: string;\n expand_idea?: boolean;\n enable_concurrency?: boolean;\n web_search?: boolean;\n}\n\ninterface HistoryItem {\n id: string;\n idea: string;\n style?: string;\n date: string;\n status: string;\n stages?: string[];\n}\n\ninterface HomePageProps {\n onStartProject: (params: ProjectParams, autoMode?: boolean) => void;\n onResumeProject: (sessionId: string) => void;\n onDeleteSession: (sessionId: string, password: string) => Promise\u003cvoid>;\n history: HistoryItem[];\n}\n\n/* 根据 stages_completed 生成进度文本 */\nfunction stageProgressLabel(stages?: string[]): { text: string; color: string } {\n if (!stages || stages.length === 0) return { text: '未开始', color: 'text-gray-400' };\n if (stages.length >= STAGES.length) return { text: '已完成', color: 'text-green-600' };\n // 显示最后完成的阶段名称\n const lastStageId = stages[stages.length - 1];\n const stageDef = STAGES.find(s => s.id === lastStageId);\n const name = stageDef?.shortName || lastStageId;\n return { text: `已完成: ${name} (${stages.length}/${STAGES.length})`, color: 'text-blue-600' };\n}\n\nexport default function HomePage({ onStartProject, onResumeProject, onDeleteSession, history }: HomePageProps) {\n const [idea, setIdea] = useState('');\n const [showSettings, setShowSettings] = useState(false);\n const [selectedStyle, setSelectedStyle] = useState('realistic');\n const [selectedLLM, setSelectedLLM] = useState(LLM_MODELS[0].id);\n const [selectedVLM, setSelectedVLM] = useState(VLM_MODELS[0].id);\n const [selectedT2I, setSelectedT2I] = useState(T2I_MODELS[0].id);\n const [selectedI2I, setSelectedI2I] = useState(I2I_MODELS[0].id);\n const [selectedVideo, setSelectedVideo] = useState(VIDEO_MODELS[0].id);\n const [selectedRatio, setSelectedRatio] = useState('16:9');\n const [enableConcurrency, setEnableConcurrency] = useState(true);\n const [webSearch, setWebSearch] = useState(false);\n\n // 管理模式状态\n const [manageMode, setManageMode] = useState(false);\n const [deleteTarget, setDeleteTarget] = useState\u003cstring | null>(null);\n const [adminPassword, setAdminPassword] = useState('');\n const [deleteError, setDeleteError] = useState('');\n const [deleting, setDeleting] = useState(false);\n\n const handleDelete = async () => {\n if (!deleteTarget || !adminPassword) return;\n setDeleting(true);\n setDeleteError('');\n try {\n await onDeleteSession(deleteTarget, adminPassword);\n setDeleteTarget(null);\n setAdminPassword('');\n } catch (e: any) {\n setDeleteError(e.message || '删除失败');\n } finally {\n setDeleting(false);\n }\n };\n\n const handleStart = (auto?: boolean) => {\n if (!idea.trim()) return;\n onStartProject({\n idea,\n style: selectedStyle,\n video_ratio: selectedRatio,\n llm_model: selectedLLM,\n vlm_model: selectedVLM,\n image_t2i_model: selectedT2I,\n image_it2i_model: selectedI2I,\n video_model: selectedVideo,\n enable_concurrency: enableConcurrency,\n web_search: webSearch,\n }, auto);\n };\n\n const handleExampleClick = (text: string) => {\n setIdea(text);\n };\n\n return (\n \u003cdiv className=\"h-full flex flex-col items-center overflow-y-auto bg-gray-50/50\">\n {/* 主区域 - 居中 */}\n \u003cdiv className=\"w-full max-w-6xl px-6 pt-16 pb-8 flex-shrink-0\">\n {/* 标题 */}\n \u003cdiv className=\"text-center mb-10\">\n \u003cdiv className=\"inline-flex items-center gap-2 mb-3\">\n \u003cSparkles className=\"w-7 h-7 text-blue-500\" />\n \u003ch1 className=\"text-2xl font-bold text-gray-800\">AIGC-Claw\u003c/h1>\n \u003c/div>\n \u003cp className=\"text-sm text-gray-500\">\n 输入你的创意,AI 将为你分步生成完整短片\n \u003c/p>\n \u003c/div>\n\n {/* 输入区域 */}\n \u003cdiv className=\"bg-white rounded-2xl shadow-sm border border-gray-200 p-5 mb-6\">\n \u003ctextarea\n value={idea}\n onChange={e => setIdea(e.target.value)}\n placeholder=\"描述你的视频创意... 例如:一只叫Luna的猫意外进入太空站,遇到一个孤独的宇航员\"\n className=\"w-full bg-transparent text-sm text-gray-800 placeholder-gray-400 resize-none outline-none min-h-[100px]\"\n onKeyDown={e => {\n if (e.key === 'Enter' && !e.shiftKey && idea.trim()) {\n e.preventDefault();\n handleStart(false);\n }\n }}\n />\n\n \u003cdiv className=\"flex items-center justify-between mt-3 pt-3 border-t border-gray-100\">\n \u003cdiv className=\"flex items-center gap-3\">\n \u003cbutton\n onClick={() => setShowSettings(!showSettings)}\n className={clsx(\n 'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',\n showSettings\n ? 'bg-blue-50 text-blue-600'\n : 'text-gray-400 hover:text-gray-600 hover:bg-gray-50'\n )}\n >\n \u003cSettings2 className=\"w-3.5 h-3.5\" />\n 生成配置\n \u003c/button>\n \u003cbutton\n onClick={() => setWebSearch(!webSearch)}\n className={clsx(\n 'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',\n webSearch\n ? 'bg-blue-50 text-blue-600'\n : 'text-gray-400 hover:text-gray-600 hover:bg-gray-50'\n )}\n >\n \u003cGlobe className=\"w-3.5 h-3.5\" />\n 联网搜索\n \u003c/button>\n \u003c/div>\n \u003cdiv className=\"flex items-center gap-2\">\n \u003cbutton\n onClick={() => handleStart(false)}\n disabled={!idea.trim()}\n className={clsx(\n 'flex items-center gap-2 px-5 py-2 rounded-xl text-sm font-medium transition-colors',\n idea.trim()\n ? 'bg-blue-500 text-white hover:bg-blue-600 shadow-sm'\n : 'bg-gray-100 text-gray-400 cursor-not-allowed'\n )}\n >\n \u003cPlay className=\"w-4 h-4\" />\n 逐步创作\n \u003c/button>\n \u003cbutton\n onClick={() => handleStart(true)}\n disabled={!idea.trim()}\n className={clsx(\n 'flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors',\n idea.trim()\n ? 'bg-amber-500 text-white hover:bg-amber-600 shadow-sm'\n : 'bg-gray-100 text-gray-400 cursor-not-allowed'\n )}\n title=\"自动执行全部六个阶段,无需手动确认\"\n >\n \u003cZap className=\"w-4 h-4\" />\n 一键生成\n \u003c/button>\n \u003c/div>\n \u003c/div>\n\n {/* 模型设置折叠面板 */}\n {showSettings && (\n \u003cdiv className=\"mt-4 p-4 bg-gray-50 rounded-xl space-y-3 text-xs\">\n \u003cdiv className=\"grid grid-cols-2 gap-3\">\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-gray-500 font-medium\">风格\u003c/span>\n \u003cselect\n value={selectedStyle}\n onChange={e => setSelectedStyle(e.target.value)}\n className=\"bg-white border border-gray-200 rounded-lg px-2.5 py-2 text-gray-700 outline-none\"\n >\n {STYLES.map(s => (\n \u003coption key={s.id} value={s.id}>{s.label}\u003c/option>\n ))}\n \u003c/select>\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-gray-500 font-medium\">视频比例\u003c/span>\n \u003cdiv className=\"flex gap-1\">\n {VIDEO_RATIOS.map(r => (\n \u003cbutton\n key={r.id}\n onClick={() => setSelectedRatio(r.id)}\n className={`flex flex-col items-center gap-1 p-2 rounded-lg border transition-all ${\n selectedRatio === r.id\n ? 'border-indigo-500 bg-indigo-50'\n : 'border-gray-200 hover:border-gray-300'\n }`}\n title={r.label}\n >\n \u003cdiv\n className=\"bg-gray-700 rounded-sm\"\n style={{\n width: r.ratio === '16:9' ? '32px' :\n r.ratio === '9:16' ? '18px' :\n r.ratio === '1:1' ? '24px' :\n r.ratio === '4:3' ? '28px' :\n r.ratio === '3:4' ? '20px' :\n '36px',\n height: r.ratio === '16:9' ? '18px' :\n r.ratio === '9:16' ? '32px' :\n r.ratio === '1:1' ? '24px' :\n r.ratio === '4:3' ? '21px' :\n r.ratio === '3:4' ? '28px' :\n '15px',\n }}\n />\n \u003cspan className=\"text-[10px] text-gray-500\">{r.label}\u003c/span>\n \u003c/button>\n ))}\n \u003c/div>\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-gray-500 font-medium\">LLM 模型\u003c/span>\n \u003cselect\n value={selectedLLM}\n onChange={e => setSelectedLLM(e.target.value)}\n className=\"bg-white border border-gray-200 rounded-lg px-2.5 py-2 text-gray-700 outline-none\"\n >\n {LLM_PROVIDERS.map(pg => (\n \u003coptgroup key={pg.provider} label={pg.label}>\n {pg.models.map(m => (\n \u003coption key={m.id} value={m.id}>{m.label}\u003c/option>\n ))}\n \u003c/optgroup>\n ))}\n \u003c/select>\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-gray-500 font-medium\">VLM 评估模型\u003c/span>\n \u003cselect\n value={selectedVLM}\n onChange={e => setSelectedVLM(e.target.value)}\n className=\"bg-white border border-gray-200 rounded-lg px-2.5 py-2 text-gray-700 outline-none\"\n >\n {VLM_PROVIDERS.map(pg => (\n \u003coptgroup key={pg.provider} label={pg.label}>\n {pg.models.map(m => (\n \u003coption key={m.id} value={m.id}>{m.label}\u003c/option>\n ))}\n \u003c/optgroup>\n ))}\n \u003c/select>\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-gray-500 font-medium\">文生图\u003c/span>\n \u003cselect\n value={selectedT2I}\n onChange={e => setSelectedT2I(e.target.value)}\n className=\"bg-white border border-gray-200 rounded-lg px-2.5 py-2 text-gray-700 outline-none\"\n >\n {T2I_PROVIDERS.map(pg => (\n \u003coptgroup key={pg.provider} label={pg.label}>\n {pg.models.map(m => (\n \u003coption key={m.id} value={m.id}>{m.label}\u003c/option>\n ))}\n \u003c/optgroup>\n ))}\n \u003c/select>\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-gray-500 font-medium\">图生图\u003c/span>\n \u003cselect\n value={selectedI2I}\n onChange={e => setSelectedI2I(e.target.value)}\n className=\"bg-white border border-gray-200 rounded-lg px-2.5 py-2 text-gray-700 outline-none\"\n >\n {I2I_PROVIDERS.map(pg => (\n \u003coptgroup key={pg.provider} label={pg.label}>\n {pg.models.map(m => (\n \u003coption key={m.id} value={m.id}>{m.label}\u003c/option>\n ))}\n \u003c/optgroup>\n ))}\n \u003c/select>\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1 col-span-2\">\n \u003cspan className=\"text-gray-500 font-medium\">视频模型\u003c/span>\n \u003cselect\n value={selectedVideo}\n onChange={e => setSelectedVideo(e.target.value)}\n className=\"bg-white border border-gray-200 rounded-lg px-2.5 py-2 text-gray-700 outline-none\"\n >\n {VIDEO_PROVIDERS.map(pg => (\n \u003coptgroup key={pg.provider} label={pg.label}>\n {pg.models.map(m => (\n \u003coption key={m.id} value={m.id}>{m.label}\u003c/option>\n ))}\n \u003c/optgroup>\n ))}\n \u003c/select>\n \u003c/label>\n \u003clabel className=\"flex items-center gap-2 text-sm cursor-pointer select-none\">\n \u003cinput\n type=\"checkbox\"\n checked={enableConcurrency}\n onChange={e => setEnableConcurrency(e.target.checked)}\n className=\"w-4 h-4 rounded border-gray-300 text-blue-500 focus:ring-blue-500/30\"\n />\n \u003cspan className=\"text-gray-600\">并发生成\u003c/span>\n \u003c/label>\n \u003c/div>\n \u003c/div>\n )}\n \u003c/div>\n\n {/* 示例卡片 */}\n \u003cdiv className=\"mb-10\">\n \u003ch3 className=\"text-xs font-medium text-gray-400 uppercase tracking-wider mb-3\">\n 灵感示例\n \u003c/h3>\n \u003cdiv className=\"grid grid-cols-2 md:grid-cols-3 gap-2.5\">\n {PROMPT_EXAMPLES.map((ex, idx) => (\n \u003cbutton\n key={idx}\n onClick={() => handleExampleClick(ex.text)}\n className=\"text-left p-3.5 bg-white rounded-xl border border-gray-200 hover:border-blue-300 hover:shadow-sm transition-all group\"\n >\n \u003cdiv className=\"text-sm font-medium text-gray-700 group-hover:text-blue-600 transition-colors mb-1\">\n {ex.title}\n \u003c/div>\n \u003cdiv className=\"text-xs text-gray-400 line-clamp-2\">\n {ex.description}\n \u003c/div>\n \u003c/button>\n ))}\n \u003c/div>\n \u003c/div>\n \u003c/div>\n\n {/* 历史记录区域 */}\n {history.length > 0 && (\n \u003cdiv className=\"w-full max-w-6xl px-6 pb-12 flex-shrink-0\">\n \u003cdiv className=\"flex items-center gap-2 mb-4\">\n \u003cClock className=\"w-4 h-4 text-gray-400\" />\n \u003ch3 className=\"text-sm font-medium text-gray-600\">历史记录\u003c/h3>\n \u003cbutton\n onClick={() => setManageMode(m => !m)}\n className={`ml-auto text-xs px-2 py-0.5 rounded transition-colors ${\n manageMode ? 'bg-red-100 text-red-600' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'\n }`}\n >\n {manageMode ? '完成' : '管理'}\n \u003c/button>\n \u003c/div>\n \u003cdiv className=\"max-h-[60vh] overflow-y-auto pr-1\">\n \u003cdiv className=\"grid grid-cols-2 gap-3\">\n {history.map(item => {\n const progress = stageProgressLabel(item.stages);\n return (\n \u003cdiv key={item.id} className=\"relative group\">\n \u003cdiv\n onClick={() => !manageMode && onResumeProject(item.id)}\n className={`w-full text-left p-4 bg-white rounded-xl border border-gray-200 hover:border-blue-300 hover:shadow-sm transition-all ${!manageMode ? 'cursor-pointer' : ''}`}\n >\n \u003cdiv className=\"flex items-start justify-between gap-2\">\n \u003cdiv className=\"flex-1 min-w-0\">\n \u003cdiv className=\"text-sm font-medium text-gray-700 group-hover:text-blue-600 transition-colors truncate\">\n {item.idea}\n \u003c/div>\n \u003cdiv className=\"flex items-center gap-2 mt-1.5 flex-wrap\">\n {item.style && (\n \u003cspan className=\"text-[10px] bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded\">\n {item.style}\n \u003c/span>\n )}\n \u003cspan className=\"text-[10px] text-gray-400\">{item.date}\u003c/span>\n \u003c/div>\n \u003cdiv className={`flex items-center gap-1 mt-1.5 text-[10px] font-medium ${progress.color}`}>\n {item.stages && item.stages.length >= STAGES.length ? (\n \u003cCheckCircle className=\"w-3 h-3\" />\n ) : (\n \u003cspan className=\"w-1.5 h-1.5 rounded-full bg-current flex-shrink-0\" />\n )}\n \u003cspan>{progress.text}\u003c/span>\n \u003c/div>\n \u003c/div>\n {manageMode ? (\n \u003cbutton\n onClick={(e) => { e.stopPropagation(); setDeleteTarget(item.id); setDeleteError(''); setAdminPassword(''); }}\n className=\"w-6 h-6 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors flex-shrink-0 mt-0.5\"\n title=\"删除\"\n >\n \u003cTrash2 className=\"w-3 h-3\" />\n \u003c/button>\n ) : (\n \u003cArrowRight className=\"w-4 h-4 text-gray-300 group-hover:text-blue-400 transition-colors flex-shrink-0 mt-0.5\" />\n )}\n \u003c/div>\n \u003c/div>\n \u003c/div>\n );\n })}\n \u003c/div>\n \u003c/div>\n \u003c/div>\n )}\n\n {/* 删除确认弹窗 */}\n {deleteTarget && (\n \u003cdiv className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/40\">\n \u003cdiv className=\"bg-white rounded-2xl shadow-xl w-80 p-6 relative\">\n \u003cbutton\n onClick={() => { setDeleteTarget(null); setDeleteError(''); }}\n className=\"absolute top-3 right-3 text-gray-400 hover:text-gray-600\"\n >\n \u003cX className=\"w-4 h-4\" />\n \u003c/button>\n \u003cdiv className=\"flex items-center gap-2 mb-4\">\n \u003cLock className=\"w-4 h-4 text-gray-500\" />\n \u003ch4 className=\"text-sm font-semibold text-gray-700\">确认删除\u003c/h4>\n \u003c/div>\n \u003cp className=\"text-xs text-gray-500 mb-3\">删除后不可恢复,请输入管理员密码确认操作。\u003c/p>\n \u003cinput\n type=\"password\"\n placeholder=\"管理员密码\"\n value={adminPassword}\n onChange={e => { setAdminPassword(e.target.value); setDeleteError(''); }}\n onKeyDown={e => { if (e.key === 'Enter') handleDelete(); }}\n className=\"w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300 mb-2\"\n autoFocus\n />\n {deleteError && \u003cp className=\"text-xs text-red-500 mb-2\">{deleteError}\u003c/p>}\n \u003cdiv className=\"flex gap-2 mt-2\">\n \u003cbutton\n onClick={() => { setDeleteTarget(null); setDeleteError(''); }}\n className=\"flex-1 text-sm py-1.5 rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50\"\n >\n 取消\n \u003c/button>\n \u003cbutton\n onClick={handleDelete}\n disabled={deleting || !adminPassword}\n className=\"flex-1 text-sm py-1.5 rounded-lg bg-red-500 text-white hover:bg-red-600 disabled:opacity-50\"\n >\n {deleting ? '删除中…' : '确认删除'}\n \u003c/button>\n \u003c/div>\n \u003c/div>\n \u003c/div>\n )}\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":21305,"content_sha256":"f74d73b1f58a657b6ed736ac1f86e62344a863004dc702ba6855c7ecf82e0dd9"},{"filename":"aigc-claw/frontend/components/Sandbox/Sandbox.tsx","content":"'use client';\n\nimport { useState, useEffect, useRef, DragEvent } from 'react';\nimport { Sparkles, Image, Video, MessageSquare, Zap, Loader2, ArrowLeft, Copy, Check, Trash2, Settings2, X, FolderOpen, Upload, FileImage, Globe } from 'lucide-react';\nimport Link from 'next/link';\nimport { useSearchParams } from 'next/navigation';\nimport { LLM_MODELS, T2I_MODELS, I2I_MODELS, VIDEO_MODELS, VLM_MODELS } from '@/config/models';\n\n// 辅助函数:将相对路径转换为完整 URL\nconst toMediaUrl = (path: string) => {\n if (!path) return '';\n // 如果已经是完整 URL,直接返回\n if (path.startsWith('http://') || path.startsWith('https://')) return path;\n // 相对路径添加 /code/ 前缀(result/xxx 格式)\n if (path.startsWith('result/')) {\n return `/code/${path}`;\n } else if (!path.startsWith('/code/')) {\n return `/code/result/${path}`;\n }\n return path;\n};\n\n// 工具类型\ntype ToolType = 'llm' | 'vlm' | 't2i' | 'i2i' | 'video';\n\ninterface Tool {\n id: ToolType;\n name: string;\n description: string;\n icon: React.ReactNode;\n}\n\nconst tools: Tool[] = [\n { id: 'llm', name: 'LLM 对话', description: '文字生成', icon: \u003cMessageSquare className=\"w-5 h-5\" /> },\n { id: 'vlm', name: '图片理解', description: '分析图片内容', icon: \u003cImage className=\"w-5 h-5\" /> },\n { id: 't2i', name: '文生图', description: '文字生成图片', icon: \u003cSparkles className=\"w-5 h-5\" /> },\n { id: 'i2i', name: '图生图', description: '图片风格转换', icon: \u003cZap className=\"w-5 h-5\" /> },\n { id: 'video', name: '视频生成', description: '图生视频/文生视频', icon: \u003cVideo className=\"w-5 h-5\" /> },\n];\n\n// 历史记录类型\ninterface HistoryRecord {\n id: string;\n tool: string;\n model: string;\n input: {\n prompt?: string;\n images?: string[];\n reference_image?: string;\n };\n output?: {\n response?: string;\n images?: string[];\n video?: string;\n video_path?: string;\n };\n created_at: string;\n}\n\n// 图片上传组件\nfunction ImageUploader({\n value,\n onChange,\n required,\n label,\n}: {\n value: string;\n onChange: (url: string) => void;\n required?: boolean;\n label: string;\n}) {\n const [isDragging, setIsDragging] = useState(false);\n const [uploading, setUploading] = useState(false);\n const [inputMode, setInputMode] = useState\u003c'url' | 'file'>('file');\n const fileInputRef = useRef\u003cHTMLInputElement>(null);\n\n const handleDragOver = (e: DragEvent) => {\n e.preventDefault();\n setIsDragging(true);\n };\n\n const handleDragLeave = (e: DragEvent) => {\n e.preventDefault();\n setIsDragging(false);\n };\n\n const handleDrop = async (e: DragEvent) => {\n e.preventDefault();\n setIsDragging(false);\n\n const files = e.dataTransfer.files;\n if (files.length > 0) {\n await uploadFile(files[0]);\n }\n };\n\n const handleFileSelect = async (e: React.ChangeEvent\u003cHTMLInputElement>) => {\n const files = e.target.files;\n if (files && files.length > 0) {\n await uploadFile(files[0]);\n }\n };\n\n const uploadFile = async (file: File) => {\n if (!file.type.startsWith('image/')) {\n alert('请选择图片文件');\n return;\n }\n\n setUploading(true);\n try {\n const reader = new FileReader();\n reader.onload = () => {\n const base64 = reader.result as string;\n onChange(base64);\n setUploading(false);\n };\n reader.onerror = () => {\n alert('文件读取失败');\n setUploading(false);\n };\n reader.readAsDataURL(file);\n } catch (e) {\n alert('上传失败');\n setUploading(false);\n }\n };\n\n // 判断是否为 URL\n const isUrl = value.startsWith('http://') || value.startsWith('https://');\n\n return (\n \u003cdiv className=\"mb-4\">\n \u003clabel className=\"block text-sm font-medium text-gray-700 mb-2\">\n {label} {required && \u003cspan className=\"text-red-500\">*\u003c/span>}\n \u003c/label>\n\n {/* 切换 URL / 文件上传 */}\n \u003cdiv className=\"flex gap-2 mb-2\">\n \u003cbutton\n type=\"button\"\n onClick={() => setInputMode('url')}\n className={`text-xs px-3 py-1.5 rounded-lg transition-colors ${\n inputMode === 'url' ? 'bg-indigo-100 text-indigo-700' : 'text-gray-500 hover:bg-gray-100'\n }`}\n >\n URL 地址\n \u003c/button>\n \u003cbutton\n type=\"button\"\n onClick={() => setInputMode('file')}\n className={`text-xs px-3 py-1.5 rounded-lg transition-colors ${\n inputMode === 'file' ? 'bg-indigo-100 text-indigo-700' : 'text-gray-500 hover:bg-gray-100'\n }`}\n >\n 本地上传\n \u003c/button>\n \u003c/div>\n\n {/* URL 输入模式 */}\n {inputMode === 'url' && (\n \u003cdiv className=\"space-y-2\">\n \u003cinput\n type=\"text\"\n value={isUrl ? value : ''}\n onChange={e => onChange(e.target.value)}\n placeholder=\"https://example.com/image.jpg\"\n className=\"w-full px-4 py-2.5 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none\"\n />\n {value && isUrl && (\n \u003cdiv className=\"relative group\">\n \u003cimg src={value} alt=\"预览\" className=\"max-h-48 rounded-lg border border-gray-200\" />\n \u003cbutton\n onClick={() => onChange('')}\n className=\"absolute top-2 right-2 p-1.5 bg-red-500 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n \u003cX className=\"w-4 h-4\" />\n \u003c/button>\n \u003c/div>\n )}\n \u003c/div>\n )}\n\n {/* 文件上传模式 */}\n {inputMode === 'file' && (\n \u003c>\n {value && !isUrl ? (\n \u003cdiv className=\"relative group\">\n \u003cimg src={value} alt=\"上传的图片\" className=\"max-h-48 rounded-lg border border-gray-200\" />\n \u003cbutton\n onClick={() => onChange('')}\n className=\"absolute top-2 right-2 p-1.5 bg-red-500 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n \u003cX className=\"w-4 h-4\" />\n \u003c/button>\n \u003c/div>\n ) : (\n \u003cdiv\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n onClick={() => fileInputRef.current?.click()}\n className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${\n isDragging ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300 hover:border-gray-400'\n }`}\n >\n \u003cinput\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n {uploading ? (\n \u003cLoader2 className=\"w-8 h-8 mx-auto mb-2 text-indigo-500 animate-spin\" />\n ) : (\n \u003cUpload className=\"w-8 h-8 mx-auto mb-2 text-gray-400\" />\n )}\n \u003cp className=\"text-sm text-gray-500\">\n 拖拽图片到此处,或 \u003cspan className=\"text-indigo-600\">点击选择文件\u003c/span>\n \u003c/p>\n \u003cp className=\"text-xs text-gray-400 mt-1\">支持 PNG、JPG、WebP 等格式\u003c/p>\n \u003c/div>\n )}\n \u003c/>\n )}\n \u003c/div>\n );\n}\n\nexport default function SandboxPage() {\n const [activeTool, setActiveTool] = useState\u003cToolType>('llm');\n const [prompt, setPrompt] = useState('');\n const [imageUrl, setImageUrl] = useState('');\n const [loading, setLoading] = useState(false);\n const [result, setResult] = useState\u003cstring | null>(null);\n const [error, setError] = useState\u003cstring | null>(null);\n const [copied, setCopied] = useState(false);\n\n // 历史记录状态\n const [history, setHistory] = useState\u003cHistoryRecord[]>([]);\n const [showHistory, setShowHistory] = useState(false);\n const [manageMode, setManageMode] = useState(false);\n const [deleting, setDeleting] = useState\u003cstring | null>(null);\n const [selectedRecord, setSelectedRecord] = useState\u003cHistoryRecord | null>(null);\n const searchParams = useSearchParams();\n\n // 获取历史记录\n const fetchHistory = async () => {\n try {\n const resp = await fetch('/api/sandbox/history');\n const data = await resp.json();\n if (data.success) {\n setHistory(data.records);\n }\n } catch (e) {\n console.error('Failed to fetch history:', e);\n }\n };\n\n useEffect(() => {\n fetchHistory();\n }, [showHistory]);\n\n // 检查 URL 参数,自动加载历史记录\n useEffect(() => {\n const recordId = searchParams.get('record');\n if (recordId && history.length > 0) {\n const record = history.find(r => r.id === recordId);\n if (record) {\n setSelectedRecord(record);\n // 填充输入框\n setActiveTool(record.tool as ToolType);\n setPrompt(record.input.prompt || '');\n setImageUrl(record.input.reference_image || record.input.images?.[0] || '');\n // 显示结果\n if (record.output?.response) {\n setResult(record.output.response);\n } else if (record.output?.images?.length) {\n setResult(`生成完成,共 ${record.output.images.length} 张图片`);\n } else if (record.output?.video_path) {\n setResult('视频生成完成');\n }\n }\n }\n }, [searchParams, history]);\n\n // 删除历史记录\n const deleteRecord = async (id: string) => {\n setDeleting(id);\n try {\n const resp = await fetch(`/api/sandbox/history/${id}`, { method: 'DELETE' });\n const data = await resp.json();\n if (data.success) {\n setHistory(history.filter(r => r.id !== id));\n }\n } catch (e) {\n console.error('Failed to delete:', e);\n } finally {\n setDeleting(null);\n }\n };\n\n // 根据工具类型获取模型列表\n const getModels = () => {\n switch (activeTool) {\n case 'llm': return LLM_MODELS;\n case 'vlm': return VLM_MODELS;\n case 't2i': return T2I_MODELS;\n case 'i2i': return I2I_MODELS;\n case 'video': return VIDEO_MODELS;\n default: return LLM_MODELS;\n }\n };\n\n const [selectedModel, setSelectedModel] = useState(getModels()[0]?.id || '');\n const [webSearch, setWebSearch] = useState(false);\n\n // 工具切换时重置模型选择\n const handleToolChange = (tool: ToolType) => {\n setActiveTool(tool);\n // 立即更新模型选择\n const models = (() => {\n switch (tool) {\n case 'llm': return LLM_MODELS;\n case 'vlm': return VLM_MODELS;\n case 't2i': return T2I_MODELS;\n case 'i2i': return I2I_MODELS;\n case 'video': return VIDEO_MODELS;\n default: return LLM_MODELS;\n }\n })();\n setSelectedModel(models[0]?.id || '');\n setResult(null);\n setError(null);\n setImageUrl('');\n };\n\n // 监听工具变化,确保模型选择同步\n useEffect(() => {\n const models = (() => {\n switch (activeTool) {\n case 'llm': return LLM_MODELS;\n case 'vlm': return VLM_MODELS;\n case 't2i': return T2I_MODELS;\n case 'i2i': return I2I_MODELS;\n case 'video': return VIDEO_MODELS;\n default: return LLM_MODELS;\n }\n })();\n // 只有当前模型不在新工具的模型列表中时才更新\n const currentInList = models.some(m => m.id === selectedModel);\n if (!currentInList) {\n setSelectedModel(models[0]?.id || '');\n }\n }, [activeTool]);\n\n // 检查是否可以提交\n const canSubmit = () => {\n if (!prompt.trim() && activeTool !== 't2i') return false;\n if ((activeTool === 'i2i') && !imageUrl) return false;\n return true;\n };\n\n const handleSubmit = async () => {\n if (!canSubmit()) return;\n\n setLoading(true);\n setResult(null);\n setError(null);\n\n try {\n let apiUrl = '';\n let body: Record\u003cstring, unknown> = {\n model: selectedModel,\n prompt: prompt,\n };\n\n switch (activeTool) {\n case 'llm':\n apiUrl = '/api/sandbox/llm';\n // web_search 只对 LLM 有效\n if (webSearch) {\n body.web_search = true;\n }\n break;\n case 'vlm':\n apiUrl = '/api/sandbox/vlm';\n body.images = [imageUrl];\n break;\n case 't2i':\n apiUrl = '/api/sandbox/t2i';\n break;\n case 'i2i':\n apiUrl = '/api/sandbox/i2i';\n body.image = imageUrl;\n break;\n case 'video':\n apiUrl = '/api/sandbox/video';\n body.image = imageUrl;\n break;\n }\n\n const response = await fetch(apiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n const data = await response.json();\n\n if (data.success) {\n if (activeTool === 't2i' || activeTool === 'i2i' || activeTool === 'video') {\n setResult(JSON.stringify(data.result || data.video_path, null, 2));\n } else {\n setResult(data.result);\n }\n fetchHistory();\n } else {\n setError(data.error || '未知错误');\n }\n } catch (e: unknown) {\n setError(e instanceof Error ? e.message : '请求失败');\n } finally {\n setLoading(false);\n }\n };\n\n const copyResult = () => {\n if (result) {\n navigator.clipboard.writeText(result);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }\n };\n\n // 获取工具名称\n const getToolName = (tool: string) => {\n const t = tools.find(x => x.id === tool);\n return t?.name || tool;\n };\n\n // 格式化日期\n const formatDate = (dateStr: string) => {\n const date = new Date(dateStr);\n return date.toLocaleString('zh-CN', {\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n });\n };\n\n // 获取图片输入的标签\n const getImageLabel = () => {\n switch (activeTool) {\n case 'vlm': return '上传图片';\n case 'i2i': return '参考图片';\n case 'video': return '首帧图片';\n default: return '图片';\n }\n };\n\n return (\n \u003cdiv className=\"min-h-screen bg-gradient-to-br from-indigo-50 via-white to-purple-50\">\n {/* 顶部导航 */}\n \u003cheader className=\"bg-white border-b border-gray-200 px-6 py-4\">\n \u003cdiv className=\"max-w-5xl mx-auto flex items-center justify-between\">\n \u003cdiv className=\"flex items-center gap-4\">\n \u003cLink\n href=\"/\"\n className=\"p-2 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-colors\"\n >\n \u003cArrowLeft className=\"w-5 h-5\" />\n \u003c/Link>\n \u003cdiv>\n \u003ch1 className=\"text-xl font-bold text-gray-800\">临时工作台\u003c/h1>\n \u003cp className=\"text-sm text-gray-500\">独立调用各种 AI 工具\u003c/p>\n \u003c/div>\n \u003c/div>\n \u003cdiv className=\"flex items-center gap-2\">\n \u003cbutton\n onClick={() => { setShowHistory(!showHistory); setManageMode(false); }}\n className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${\n showHistory ? 'bg-indigo-100 text-indigo-700' : 'text-gray-500 hover:bg-gray-100'\n }`}\n >\n \u003cFolderOpen className=\"w-3.5 h-3.5\" />\n \u003cspan>历史记录\u003c/span>\n \u003c/button>\n {showHistory && (\n \u003cbutton\n onClick={() => setManageMode(!manageMode)}\n className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${\n manageMode ? 'bg-red-100 text-red-700' : 'text-gray-500 hover:bg-gray-100'\n }`}\n >\n \u003cSettings2 className=\"w-3.5 h-3.5\" />\n \u003cspan>{manageMode ? '取消管理' : '管理'}\u003c/span>\n \u003c/button>\n )}\n \u003c/div>\n \u003c/div>\n \u003c/header>\n\n \u003cmain className=\"max-w-5xl mx-auto px-6 py-8\">\n {showHistory ? (\n \u003cdiv>\n \u003cdiv className=\"flex items-center justify-between mb-6\">\n \u003ch2 className=\"text-lg font-semibold text-gray-800\">历史记录\u003c/h2>\n \u003cspan className=\"text-sm text-gray-500\">{history.length} 条记录\u003c/span>\n \u003c/div>\n\n {history.length === 0 ? (\n \u003cdiv className=\"text-center py-12 text-gray-400\">\n \u003cFolderOpen className=\"w-12 h-12 mx-auto mb-3 opacity-50\" />\n \u003cp>暂无历史记录\u003c/p>\n \u003c/div>\n ) : (\n \u003cdiv className=\"space-y-4\">\n {history.map(record => (\n \u003cdiv\n key={record.id}\n className=\"bg-white rounded-xl border border-gray-200 overflow-hidden cursor-pointer hover:border-indigo-300 transition-colors\"\n onClick={() => {\n setSelectedRecord(record);\n setActiveTool(record.tool as ToolType);\n setPrompt(record.input.prompt || '');\n setImageUrl(record.input.reference_image || record.input.images?.[0] || '');\n if (record.output?.response) {\n setResult(record.output.response);\n } else if (record.output?.images?.length) {\n setResult(`生成完成,共 ${record.output.images.length} 张图片`);\n } else if (record.output?.video_path) {\n setResult('视频生成完成');\n }\n setShowHistory(false);\n }}\n >\n \u003cdiv className=\"p-4 bg-gray-50 border-b border-gray-100\">\n \u003cdiv className=\"flex items-center justify-between mb-2\">\n \u003cdiv className=\"flex items-center gap-2\">\n \u003cspan className={`px-2 py-0.5 rounded text-xs font-medium ${\n record.tool === 'llm' ? 'bg-blue-100 text-blue-700' :\n record.tool === 'vlm' ? 'bg-green-100 text-green-700' :\n record.tool === 't2i' ? 'bg-purple-100 text-purple-700' :\n record.tool === 'i2i' ? 'bg-orange-100 text-orange-700' :\n 'bg-pink-100 text-pink-700'\n }`}>\n {getToolName(record.tool)}\n \u003c/span>\n \u003cspan className=\"text-xs text-gray-400\">{record.model}\u003c/span>\n \u003c/div>\n \u003cdiv className=\"flex items-center gap-2\">\n \u003cspan className=\"text-xs text-gray-400\">{formatDate(record.created_at)}\u003c/span>\n {manageMode && (\n \u003cbutton\n onClick={() => deleteRecord(record.id)}\n disabled={deleting === record.id}\n className=\"p-1.5 rounded-lg text-red-500 hover:bg-red-50 transition-colors\"\n >\n {deleting === record.id ? (\n \u003cLoader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n \u003cTrash2 className=\"w-4 h-4\" />\n )}\n \u003c/button>\n )}\n \u003c/div>\n \u003c/div>\n \u003cp className=\"text-sm text-gray-700 line-clamp-2\">\n {record.input.prompt || record.input.reference_image || '(无提示词)'}\n \u003c/p>\n {record.input.images && record.input.images.length > 0 && (\n \u003cdiv className=\"mt-2 flex gap-2 overflow-x-auto\">\n {record.input.images.map((img, i) => (\n \u003cimg key={i} src={toMediaUrl(img)} alt=\"input\" className=\"h-16 w-auto rounded border border-gray-200\" />\n ))}\n \u003c/div>\n )}\n \u003c/div>\n\n {record.output && (\n \u003cdiv className=\"p-4\">\n {record.output.response && (\n \u003cpre className=\"text-sm text-gray-700 whitespace-pre-wrap break-words max-h-48 overflow-y-auto\">\n {record.output.response}\n \u003c/pre>\n )}\n {record.output.images && record.output.images.length > 0 && (\n \u003cdiv className=\"grid grid-cols-2 md:grid-cols-4 gap-3\">\n {record.output.images.map((img, i) => (\n \u003cdiv key={i} className=\"relative group\">\n \u003cimg src={toMediaUrl(img)} alt={`output-${i}`} className=\"w-full h-auto rounded-lg border border-gray-200\" />\n \u003ca href={toMediaUrl(img)} target=\"_blank\" rel=\"noopener noreferrer\" className=\"absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center rounded-lg\">\n \u003cspan className=\"text-white text-xs\">查看大图\u003c/span>\n \u003c/a>\n \u003c/div>\n ))}\n \u003c/div>\n )}\n {record.output.video_path && (\n \u003cdiv>\n \u003cvideo src={toMediaUrl(record.output.video_path)} controls className=\"w-full max-w-md rounded-lg border border-gray-200\" />\n \u003ca href={toMediaUrl(record.output.video_path)} target=\"_blank\" rel=\"noopener noreferrer\" className=\"text-sm text-indigo-600 hover:underline mt-2 inline-block\">\n 查看视频\n \u003c/a>\n \u003c/div>\n )}\n \u003c/div>\n )}\n \u003c/div>\n ))}\n \u003c/div>\n )}\n \u003c/div>\n ) : (\n \u003c>\n {/* 工具选择 */}\n \u003cdiv className=\"grid grid-cols-5 gap-3 mb-8\">\n {tools.map(tool => (\n \u003cbutton\n key={tool.id}\n onClick={() => handleToolChange(tool.id)}\n className={`p-4 rounded-xl border-2 transition-all text-center ${\n activeTool === tool.id\n ? 'border-indigo-500 bg-indigo-50 text-indigo-700 shadow-md'\n : 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:shadow-sm'\n }`}\n >\n \u003cdiv className=\"flex justify-center mb-2\">{tool.icon}\u003c/div>\n \u003cdiv className=\"font-medium text-sm\">{tool.name}\u003c/div>\n \u003cdiv className=\"text-xs text-gray-400\">{tool.description}\u003c/div>\n \u003c/button>\n ))}\n \u003c/div>\n\n {/* 输入区域 */}\n \u003cdiv className=\"bg-white rounded-2xl shadow-sm border border-gray-200 p-6 mb-6\">\n {/* 模型选择 */}\n \u003cdiv className=\"mb-4\">\n \u003cdiv className=\"flex items-center justify-between\">\n \u003cdiv className=\"flex-1\">\n \u003clabel className=\"block text-sm font-medium text-gray-700 mb-2\">选择模型\u003c/label>\n \u003cselect\n value={selectedModel}\n onChange={e => setSelectedModel(e.target.value)}\n className=\"w-full px-4 py-2.5 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none\"\n >\n {getModels().map(m => (\n \u003coption key={m.id} value={m.id}>{m.label}\u003c/option>\n ))}\n \u003c/select>\n \u003c/div>\n {/* 联网搜索开关 */}\n {activeTool === 'llm' && (\n \u003cbutton\n onClick={() => setWebSearch(!webSearch)}\n className={`ml-4 px-4 py-2.5 rounded-lg border-2 flex items-center gap-2 transition-colors ${\n webSearch\n ? 'border-indigo-500 bg-indigo-50 text-indigo-700'\n : 'border-gray-200 text-gray-500 hover:border-gray-300'\n }`}\n >\n \u003cGlobe className=\"w-4 h-4\" />\n \u003cspan className=\"text-sm font-medium\">联网搜索\u003c/span>\n \u003c/button>\n )}\n \u003c/div>\n \u003c/div>\n\n {/* 图片上传(部分工具需要) */}\n {(activeTool === 'vlm' || activeTool === 'i2i' || activeTool === 'video') && (\n \u003cImageUploader\n value={imageUrl}\n onChange={setImageUrl}\n required={activeTool === 'i2i'}\n label={getImageLabel()}\n />\n )}\n\n {/* 提示词输入 */}\n \u003cdiv className=\"mb-4\">\n \u003clabel className=\"block text-sm font-medium text-gray-700 mb-2\">\n {activeTool === 'llm' ? '对话内容' :\n activeTool === 'vlm' ? '想了解图片的什么问题?' :\n activeTool === 't2i' ? '图片描述(英文效果更好)' :\n activeTool === 'i2i' ? '希望生成什么样的图片?' :\n '视频描述(希望生成什么样的视频?)'}\n \u003c/label>\n \u003ctextarea\n value={prompt}\n onChange={e => setPrompt(e.target.value)}\n placeholder={\n activeTool === 'llm' ? '输入你想问的问题...' :\n activeTool === 'vlm' ? '描述这张图片的内容...' :\n 'A cute cat sitting on a couch, realistic style'\n }\n rows={4}\n className=\"w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none resize-none\"\n />\n \u003c/div>\n\n {/* 提交按钮 */}\n \u003cbutton\n onClick={handleSubmit}\n disabled={loading || !canSubmit()}\n className=\"w-full py-3 px-6 bg-indigo-600 text-white font-medium rounded-xl hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2\"\n >\n {loading ? (\n \u003c>\n \u003cLoader2 className=\"w-5 h-5 animate-spin\" />\n \u003cspan>处理中...\u003c/span>\n \u003c/>\n ) : (\n \u003c>\n \u003cSparkles className=\"w-5 h-5\" />\n \u003cspan>生成\u003c/span>\n \u003c/>\n )}\n \u003c/button>\n \u003c/div>\n\n {/* 结果展示 */}\n {(result || error) && (\n \u003cdiv className={`rounded-2xl border p-6 ${error ? 'bg-red-50 border-red-200' : 'bg-green-50 border-green-200'}`}>\n \u003cdiv className=\"flex items-center justify-between mb-3\">\n \u003ch3 className={`font-medium ${error ? 'text-red-700' : 'text-green-700'}`}>\n {error ? '错误' : '结果'}\n \u003c/h3>\n {!error && result && (\n \u003cbutton onClick={copyResult} className=\"p-2 rounded-lg hover:bg-white/50 transition-colors\" title=\"复制结果\">\n {copied ? \u003cCheck className=\"w-4 h-4 text-green-600\" /> : \u003cCopy className=\"w-4 h-4 text-gray-500\" />}\n \u003c/button>\n )}\n \u003c/div>\n {error ? (\n \u003cp className=\"text-red-600 text-sm\">{error}\u003c/p>\n ) : (\n \u003cpre className=\"text-sm text-gray-700 whitespace-pre-wrap break-words max-h-96 overflow-y-auto\">\n {result}\n \u003c/pre>\n )}\n \u003c/div>\n )}\n \u003c/>\n )}\n \u003c/main>\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":28550,"content_sha256":"309d3d4c117d9a88e3b74c25ac2f55de18b094eb8763d6c5404d2c786b3961e8"},{"filename":"aigc-claw/frontend/components/stages/CharacterStage.tsx","content":"'use client';\n\nimport React, { useState, useRef, useCallback, useEffect } from 'react';\nimport { Users, MapPin, RefreshCw, Save, X, ChevronLeft, ChevronRight, Loader, AlertCircle, ZoomIn } from 'lucide-react';\nimport type { StageViewProps } from './types';\nimport { assetUrl } from './utils';\nimport StageActions from './StageActions';\nimport StageProgress from './StageProgress';\nimport ImageLightbox from './ImageLightbox';\n\n/* ─── 类型 ─── */\ninterface AssetVersion {\n id: string; // 唯一标识 (character_id / setting_id)\n name: string;\n description: string;\n selected: string; // 当前选中的文件路径\n versions: string[]; // 所有历史版本路径\n status?: 'pending' | 'done' | 'failed'; // 生成状态\n}\n\n/* ─── 水平滚动图片画廊 ─── */\nfunction ImageGallery({\n versions,\n selected,\n onSelect,\n showPlaceholder,\n}: {\n versions: string[];\n selected: string;\n onSelect: (path: string) => void;\n showPlaceholder?: boolean;\n}) {\n const scrollRef = useRef\u003cHTMLDivElement>(null);\n const [lightboxIndex, setLightboxIndex] = useState\u003cnumber | null>(null);\n\n const scroll = (dir: 'left' | 'right') => {\n if (!scrollRef.current) return;\n const amount = 260;\n scrollRef.current.scrollBy({ left: dir === 'left' ? -amount : amount, behavior: 'smooth' });\n };\n\n if (!versions.length) {\n return (\n \u003cdiv className=\"flex items-center justify-center h-full text-gray-400 text-xs\">\n 暂无图片\n \u003c/div>\n );\n }\n\n return (\n \u003cdiv className=\"relative group\">\n {versions.length > 1 && (\n \u003c>\n \u003cbutton\n onClick={() => scroll('left')}\n className=\"absolute left-0 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white/90 shadow border border-gray-200 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n \u003cChevronLeft className=\"w-4 h-4 text-gray-600\" />\n \u003c/button>\n \u003cbutton\n onClick={() => scroll('right')}\n className=\"absolute right-0 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white/90 shadow border border-gray-200 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n \u003cChevronRight className=\"w-4 h-4 text-gray-600\" />\n \u003c/button>\n \u003c/>\n )}\n \u003cdiv\n ref={scrollRef}\n className=\"flex gap-3 overflow-x-auto scrollbar-hide py-1 px-1\"\n style={{ scrollbarWidth: 'none' }}\n >\n {versions.map((path, i) => {\n const isSelected = path === selected;\n return (\n \u003cdiv\n key={path}\n onClick={() => onSelect(path)}\n className={`flex-shrink-0 cursor-pointer rounded-lg overflow-hidden transition-all ${\n isSelected\n ? 'ring-3 ring-violet-500 shadow-lg shadow-violet-200'\n : 'ring-1 ring-gray-200 hover:ring-gray-300 hover:shadow-md'\n }`}\n >\n \u003cdiv className=\"relative group/img\">\n \u003cimg\n src={assetUrl(path)}\n alt={`v${i + 1}`}\n className=\"h-28 w-auto object-cover\"\n onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}\n />\n \u003cbutton\n onClick={(e) => { e.stopPropagation(); setLightboxIndex(i); }}\n className=\"absolute top-1 right-1 w-6 h-6 rounded-full bg-black/40 flex items-center justify-center opacity-0 group-hover/img:opacity-100 transition-opacity hover:bg-black/60\"\n title=\"放大查看\"\n >\n \u003cZoomIn className=\"w-3 h-3 text-white\" />\n \u003c/button>\n \u003c/div>\n \u003cdiv className={`text-center text-[10px] py-0.5 ${\n isSelected ? 'bg-violet-500 text-white font-medium' : 'bg-gray-50 text-gray-400'\n }`}>\n v{i + 1}\n \u003c/div>\n \u003c/div>\n );\n })}\n {showPlaceholder && (\n \u003cdiv className=\"flex-shrink-0 flex items-center justify-center h-28 aspect-video bg-gray-50 rounded-lg border border-dashed border-gray-200\">\n \u003cdiv className=\"flex items-center gap-2 text-gray-400 text-xs\">\n \u003cLoader className=\"w-4 h-4 animate-spin\" />\n \u003cspan>生成中...\u003c/span>\n \u003c/div>\n \u003c/div>\n )}\n \u003c/div>\n {lightboxIndex !== null && (\n \u003cImageLightbox\n images={versions}\n initialIndex={lightboxIndex}\n onClose={() => setLightboxIndex(null)}\n />\n )}\n \u003c/div>\n );\n}\n\n/* ─── 素材行 ─── */\nfunction AssetRow({\n asset,\n type,\n isEditing,\n editDesc,\n onDescChange,\n onRegenerate,\n onSelectVersion,\n isStageRunning,\n isRegenerating,\n}: {\n asset: AssetVersion;\n type: 'character' | 'setting';\n isEditing: boolean;\n editDesc: string;\n onDescChange: (val: string) => void;\n onRegenerate: () => void;\n onSelectVersion: (path: string) => void;\n isStageRunning?: boolean;\n isRegenerating?: boolean;\n}) {\n const isPending = asset.status === 'pending' || isRegenerating;\n const isFailed = asset.status === 'failed' && !isRegenerating;\n\n return (\n \u003cdiv className={`flex border rounded-xl overflow-hidden bg-white ${\n isFailed ? 'border-red-200' : 'border-gray-200'\n }`}>\n {/* 左侧: 描述 */}\n \u003cdiv className=\"w-[320px] flex-shrink-0 p-4 border-r border-gray-100 flex flex-col\">\n \u003cdiv className=\"flex items-center gap-2 mb-2\">\n {type === 'character'\n ? \u003cUsers className=\"w-3.5 h-3.5 text-violet-500\" />\n : \u003cMapPin className=\"w-3.5 h-3.5 text-emerald-500\" />\n }\n \u003cspan className=\"text-sm font-semibold text-gray-800 truncate\">{asset.name}\u003c/span>\n {isPending && (\n \u003cspan className=\"inline-flex items-center gap-1 text-[10px] bg-amber-50 text-amber-600 px-1.5 py-0.5 rounded\">\n \u003cLoader className=\"w-2.5 h-2.5 animate-spin\" />生成中\n \u003c/span>\n )}\n {isFailed && (\n \u003cspan className=\"text-[10px] bg-red-50 text-red-500 px-1.5 py-0.5 rounded\">失败\u003c/span>\n )}\n \u003c/div>\n {isEditing ? (\n \u003ctextarea\n value={editDesc}\n onChange={e => onDescChange(e.target.value)}\n rows={5}\n className=\"flex-1 text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded-lg p-2 resize-none focus:outline-none focus:ring-1 focus:ring-violet-300\"\n />\n ) : (\n \u003cp className=\"flex-1 text-xs text-gray-600 leading-relaxed\">{asset.description}\u003c/p>\n )}\n {/* 仅在非运行状态显示重新生成/重试按钮 */}\n {!isStageRunning && (\n \u003cbutton\n onClick={onRegenerate}\n className={`mt-3 flex items-center gap-1.5 self-start px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${\n isFailed\n ? 'text-red-600 bg-red-50 hover:bg-red-100'\n : 'text-violet-600 bg-violet-50 hover:bg-violet-100'\n }`}\n >\n \u003cRefreshCw className=\"w-3 h-3\" />\n {isFailed ? '点击重试' : '重新生成'}\n \u003c/button>\n )}\n \u003c/div>\n\n {/* 右侧: 图片画廊 / 占位 */}\n \u003cdiv className=\"flex-1 min-w-0 p-3 flex items-center\">\n {isPending && !asset.versions.length ? (\n \u003cdiv className=\"flex items-center justify-center h-28 aspect-video bg-gray-50 rounded-lg border border-dashed border-gray-200\">\n \u003cdiv className=\"flex items-center gap-2 text-gray-400 text-xs\">\n \u003cLoader className=\"w-4 h-4 animate-spin\" />\n \u003cspan>正在生成...\u003c/span>\n \u003c/div>\n \u003c/div>\n ) : isFailed && !asset.versions.length ? (\n \u003cdiv\n className=\"flex items-center justify-center h-28 aspect-video bg-red-50/50 rounded-lg border border-dashed border-red-200 cursor-pointer hover:bg-red-100/50 transition-colors\"\n onClick={onRegenerate}\n >\n \u003cdiv className=\"flex flex-col items-center gap-1 text-red-400 text-xs\">\n \u003cAlertCircle className=\"w-4 h-4\" />\n \u003cspan>生成失败,点击重试\u003c/span>\n \u003c/div>\n \u003c/div>\n ) : (\n \u003cdiv className=\"relative w-full\">\n \u003cImageGallery\n versions={asset.versions}\n selected={asset.selected}\n onSelect={onSelectVersion}\n showPlaceholder={isRegenerating}\n />\n {isFailed && (\n \u003cbutton\n onClick={onRegenerate}\n className=\"absolute top-1 right-1 z-10 flex items-center gap-1 px-2 py-1 rounded-lg text-[10px] font-medium text-white bg-red-500/80 hover:bg-red-600 shadow transition-colors\"\n >\n \u003cRefreshCw className=\"w-2.5 h-2.5\" />\n 重试\n \u003c/button>\n )}\n \u003c/div>\n )}\n \u003c/div>\n \u003c/div>\n );\n}\n\n/* ─── 主组件 ─── */\nexport default function CharacterStage({ state, sessionId, onConfirm, onIntervene, onRegenerate, onSaveSelections, showConfirm, isRunning, hasPendingItems, hasNextStageStarted }: StageViewProps) {\n const characters: AssetVersion[] = state.artifact?.characters || [];\n const settingsData: AssetVersion[] = state.artifact?.settings || [];\n\n const [isEditing, setIsEditing] = useState(false);\n const [editChars, setEditChars] = useState\u003cRecord\u003cstring, string>>({});\n const [editSets, setEditSets] = useState\u003cRecord\u003cstring, string>>({});\n // 跟踪前端选择的版本(覆盖后端返回的 selected)\n const [selectedChars, setSelectedChars] = useState\u003cRecord\u003cstring, string>>({});\n const [selectedSets, setSelectedSets] = useState\u003cRecord\u003cstring, string>>({});\n const [regeneratingIds, setRegeneratingIds] = useState\u003cSet\u003cstring>>(new Set());\n\n // 当 artifact 更新时清除重新生成状态\n useEffect(() => {\n if (regeneratingIds.size > 0) setRegeneratingIds(new Set());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [state.artifact]);\n\n const hasChars = characters.length > 0;\n const hasSets = settingsData.length > 0;\n const hasAssets = hasChars || hasSets;\n\n const startEdit = useCallback(() => {\n const cd: Record\u003cstring, string> = {};\n characters.forEach(c => { cd[c.id] = c.description; });\n setEditChars(cd);\n const sd: Record\u003cstring, string> = {};\n settingsData.forEach(s => { sd[s.id] = s.description; });\n setEditSets(sd);\n setIsEditing(true);\n }, [characters, settingsData]);\n\n const cancelEdit = () => setIsEditing(false);\n\n const saveEdit = () => {\n // 发送修改后的描述,后端可以用于下次生成\n onIntervene({\n update_descriptions: {\n characters: editChars,\n settings: editSets,\n },\n });\n setIsEditing(false);\n };\n\n const handleRegenerate = (type: 'characters' | 'settings', id: string) => {\n setRegeneratingIds(prev => new Set(prev).add(id));\n if (type === 'characters') {\n onIntervene({ regenerate_characters: [id] });\n } else {\n onIntervene({ regenerate_settings: [id] });\n }\n };\n\n const handleSelectCharVersion = async (id: string, path: string) => {\n setSelectedChars(prev => ({ ...prev, [id]: path }));\n // 自动保存选择\n const selections: Record\u003cstring, string> = {};\n characters.forEach(c => { selections[c.id] = selectedChars[c.id] || c.selected; });\n selections[id] = path;\n settingsData.forEach(s => { selections[s.id] = selectedSets[s.id] || s.selected; });\n if (onSaveSelections) {\n await onSaveSelections(selections);\n }\n };\n\n const handleSelectSetVersion = async (id: string, path: string) => {\n setSelectedSets(prev => ({ ...prev, [id]: path }));\n // 自动保存选择\n const selections: Record\u003cstring, string> = {};\n characters.forEach(c => { selections[c.id] = selectedChars[c.id] || c.selected; });\n settingsData.forEach(s => { selections[s.id] = selectedSets[s.id] || s.selected; });\n selections[id] = path;\n if (onSaveSelections) {\n await onSaveSelections(selections);\n }\n };\n\n const getCharSelected = (asset: AssetVersion) => selectedChars[asset.id] || asset.selected;\n const getSetSelected = (asset: AssetVersion) => selectedSets[asset.id] || asset.selected;\n\n return (\n \u003cdiv className=\"flex flex-col h-full\">\n \u003cdiv className=\"flex-1 overflow-y-auto p-6\">\n {/* 标题栏 */}\n \u003cdiv className=\"flex items-center justify-between mb-1\">\n \u003ch2 className=\"text-lg font-semibold text-gray-800\">角色 / 场景设计\u003c/h2>\n {hasAssets && state.status === 'waiting' && !isEditing && (\n \u003cbutton\n onClick={startEdit}\n className=\"flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 transition-colors\"\n >\n 修改描述\n \u003c/button>\n )}\n {isEditing && (\n \u003cdiv className=\"flex gap-2\">\n \u003cbutton onClick={cancelEdit}\n className=\"flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium text-gray-500 hover:bg-gray-100\">\n \u003cX className=\"w-3.5 h-3.5\" />取消\n \u003c/button>\n \u003cbutton onClick={saveEdit}\n className=\"flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium text-white bg-violet-500 hover:bg-violet-600\">\n \u003cSave className=\"w-3.5 h-3.5\" />保存\n \u003c/button>\n \u003c/div>\n )}\n \u003c/div>\n \u003cp className=\"text-sm text-gray-500 mb-6\">\n 生成角色4视图 (正面特写·正面全身·侧面全身·背面全身) 和场景全景图\n \u003c/p>\n\n {/* 运行中 */}\n {state.status === 'running' && (\n \u003cStageProgress message={state.progressMessage} fallback=\"正在生成角色与场景...\" progress={state.progress} color=\"violet\" />\n )}\n\n {state.error && (\n \u003cdiv className=\"text-sm text-red-600 bg-red-50 border border-red-200 p-4 rounded-xl mb-4\">{state.error}\u003c/div>\n )}\n\n {/* ═══ 角色列表 ═══ */}\n {hasChars && (\n \u003csection className=\"mb-8\">\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cUsers className=\"w-4 h-4 text-violet-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">角色 ({characters.length})\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"space-y-3\">\n {characters.map(asset => (\n \u003cAssetRow\n key={asset.id}\n asset={{ ...asset, selected: getCharSelected(asset) }}\n type=\"character\"\n isEditing={isEditing}\n editDesc={editChars[asset.id] || asset.description}\n onDescChange={val => setEditChars(prev => ({ ...prev, [asset.id]: val }))}\n onRegenerate={() => handleRegenerate('characters', asset.id)}\n onSelectVersion={path => handleSelectCharVersion(asset.id, path)}\n isStageRunning={state.status === 'running'}\n isRegenerating={regeneratingIds.has(asset.id)}\n />\n ))}\n \u003c/div>\n \u003c/section>\n )}\n\n {/* ═══ 场景列表 ═══ */}\n {hasSets && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cMapPin className=\"w-4 h-4 text-emerald-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">场景 ({settingsData.length})\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"space-y-3\">\n {settingsData.map(asset => (\n \u003cAssetRow\n key={asset.id}\n asset={{ ...asset, selected: getSetSelected(asset) }}\n type=\"setting\"\n isEditing={isEditing}\n editDesc={editSets[asset.id] || asset.description}\n onDescChange={val => setEditSets(prev => ({ ...prev, [asset.id]: val }))}\n onRegenerate={() => handleRegenerate('settings', asset.id)}\n onSelectVersion={path => handleSelectSetVersion(asset.id, path)}\n isStageRunning={state.status === 'running'}\n isRegenerating={regeneratingIds.has(asset.id)}\n />\n ))}\n \u003c/div>\n \u003c/section>\n )}\n\n {state.status === 'pending' && (\n \u003cdiv className=\"text-center text-gray-400 text-sm py-20\">等待上一阶段完成...\u003c/div>\n )}\n \u003c/div>\n\n {/* 底部操作栏 */}\n \u003cStageActions\n status={state.status}\n onConfirm={onConfirm}\n showConfirm={showConfirm}\n onEdit={startEdit}\n onSave={isEditing ? saveEdit : undefined}\n onRegenerate={onRegenerate}\n stageId=\"character_design\"\n hasPendingItems={hasPendingItems}\n hasNextStageStarted={hasNextStageStarted}\n isRunning={isRunning}\n />\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":17239,"content_sha256":"ef0137437d6ae952a97264e5376911bf975ef7f20c83b87ba3d041961ecfab80"},{"filename":"aigc-claw/frontend/components/stages/ImageLightbox.tsx","content":"'use client';\n\nimport React, { useCallback, useEffect, useState } from 'react';\nimport { X, ChevronLeft, ChevronRight, ZoomIn, ZoomOut, RotateCcw } from 'lucide-react';\nimport { assetUrl } from './utils';\n\ninterface ImageLightboxProps {\n images: string[]; // 所有可浏览的图片路径\n initialIndex: number; // 初始展示的索引\n onClose: () => void;\n}\n\nexport default function ImageLightbox({ images, initialIndex, onClose }: ImageLightboxProps) {\n const [index, setIndex] = useState(initialIndex);\n const [scale, setScale] = useState(1);\n const [translate, setTranslate] = useState({ x: 0, y: 0 });\n const [dragging, setDragging] = useState(false);\n const [dragStart, setDragStart] = useState({ x: 0, y: 0 });\n\n const current = images[index];\n const hasPrev = index > 0;\n const hasNext = index \u003c images.length - 1;\n\n const resetView = useCallback(() => {\n setScale(1);\n setTranslate({ x: 0, y: 0 });\n }, []);\n\n const goPrev = useCallback(() => {\n if (hasPrev) { setIndex(i => i - 1); resetView(); }\n }, [hasPrev, resetView]);\n\n const goNext = useCallback(() => {\n if (hasNext) { setIndex(i => i + 1); resetView(); }\n }, [hasNext, resetView]);\n\n const zoomIn = useCallback(() => setScale(s => Math.min(s * 1.5, 5)), []);\n const zoomOut = useCallback(() => {\n setScale(s => {\n const next = Math.max(s / 1.5, 1);\n if (next === 1) setTranslate({ x: 0, y: 0 });\n return next;\n });\n }, []);\n\n // 键盘导航\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'Escape': onClose(); break;\n case 'ArrowLeft': goPrev(); break;\n case 'ArrowRight': goNext(); break;\n case '+': case '=': zoomIn(); break;\n case '-': zoomOut(); break;\n case '0': resetView(); break;\n }\n };\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [onClose, goPrev, goNext, zoomIn, zoomOut, resetView]);\n\n // 滚轮缩放\n const handleWheel = useCallback((e: React.WheelEvent) => {\n e.preventDefault();\n if (e.deltaY \u003c 0) {\n setScale(s => Math.min(s * 1.15, 5));\n } else {\n setScale(s => {\n const next = Math.max(s / 1.15, 1);\n if (next === 1) setTranslate({ x: 0, y: 0 });\n return next;\n });\n }\n }, []);\n\n // 拖拽平移(缩放 > 1 时)\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n if (scale \u003c= 1) return;\n setDragging(true);\n setDragStart({ x: e.clientX - translate.x, y: e.clientY - translate.y });\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n }, [scale, translate]);\n\n const handlePointerMove = useCallback((e: React.PointerEvent) => {\n if (!dragging) return;\n setTranslate({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });\n }, [dragging, dragStart]);\n\n const handlePointerUp = useCallback(() => setDragging(false), []);\n\n // 禁止背景滚动\n useEffect(() => {\n document.body.style.overflow = 'hidden';\n return () => { document.body.style.overflow = ''; };\n }, []);\n\n return (\n \u003cdiv className=\"fixed inset-0 z-[9999] flex items-center justify-center\">\n {/* 遮罩 */}\n \u003cdiv\n className=\"absolute inset-0 bg-black/80 backdrop-blur-sm\"\n onClick={onClose}\n />\n\n {/* 顶部工具栏 */}\n \u003cdiv className=\"absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-4 py-3\">\n \u003cdiv className=\"text-white/70 text-sm font-medium\">\n {images.length > 1 && `${index + 1} / ${images.length}`}\n \u003c/div>\n \u003cdiv className=\"flex items-center gap-1\">\n \u003cbutton onClick={zoomOut} className=\"w-8 h-8 rounded-full flex items-center justify-center text-white/70 hover:text-white hover:bg-white/10 transition-colors\" title=\"缩小 (-)\">\n \u003cZoomOut className=\"w-4 h-4\" />\n \u003c/button>\n \u003cspan className=\"text-white/50 text-xs w-12 text-center\">{Math.round(scale * 100)}%\u003c/span>\n \u003cbutton onClick={zoomIn} className=\"w-8 h-8 rounded-full flex items-center justify-center text-white/70 hover:text-white hover:bg-white/10 transition-colors\" title=\"放大 (+)\">\n \u003cZoomIn className=\"w-4 h-4\" />\n \u003c/button>\n \u003cbutton onClick={resetView} className=\"w-8 h-8 rounded-full flex items-center justify-center text-white/70 hover:text-white hover:bg-white/10 transition-colors\" title=\"重置 (0)\">\n \u003cRotateCcw className=\"w-4 h-4\" />\n \u003c/button>\n \u003cdiv className=\"w-px h-5 bg-white/20 mx-1\" />\n \u003cbutton onClick={onClose} className=\"w-8 h-8 rounded-full flex items-center justify-center text-white/70 hover:text-white hover:bg-white/10 transition-colors\" title=\"关闭 (Esc)\">\n \u003cX className=\"w-5 h-5\" />\n \u003c/button>\n \u003c/div>\n \u003c/div>\n\n {/* 左右切换 */}\n {hasPrev && (\n \u003cbutton\n onClick={goPrev}\n className=\"absolute left-4 top-1/2 -translate-y-1/2 z-10 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-white transition-colors\"\n >\n \u003cChevronLeft className=\"w-6 h-6\" />\n \u003c/button>\n )}\n {hasNext && (\n \u003cbutton\n onClick={goNext}\n className=\"absolute right-4 top-1/2 -translate-y-1/2 z-10 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-white transition-colors\"\n >\n \u003cChevronRight className=\"w-6 h-6\" />\n \u003c/button>\n )}\n\n {/* 图片 */}\n \u003cdiv\n className=\"relative z-[1] max-w-[90vw] max-h-[85vh] select-none\"\n onWheel={handleWheel}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n style={{ cursor: scale > 1 ? (dragging ? 'grabbing' : 'grab') : 'zoom-in' }}\n >\n \u003cimg\n src={assetUrl(current)}\n alt={`image ${index + 1}`}\n className=\"max-w-[90vw] max-h-[85vh] object-contain rounded-lg shadow-2xl\"\n style={{\n transform: `scale(${scale}) translate(${translate.x / scale}px, ${translate.y / scale}px)`,\n transition: dragging ? 'none' : 'transform 0.2s ease',\n }}\n draggable={false}\n onClick={(e) => {\n if (scale === 1) {\n e.stopPropagation();\n zoomIn();\n }\n }}\n />\n \u003c/div>\n\n {/* 底部缩略图条 */}\n {images.length > 1 && (\n \u003cdiv className=\"absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex gap-2 bg-black/40 rounded-full px-3 py-2\">\n {images.map((path, i) => (\n \u003cbutton\n key={path}\n onClick={() => { setIndex(i); resetView(); }}\n className={`w-10 h-10 rounded overflow-hidden border-2 transition-all ${\n i === index ? 'border-white shadow-lg scale-110' : 'border-transparent opacity-60 hover:opacity-100'\n }`}\n >\n \u003cimg src={assetUrl(path)} alt={`thumb ${i + 1}`} className=\"w-full h-full object-cover\" />\n \u003c/button>\n ))}\n \u003c/div>\n )}\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":7208,"content_sha256":"19a551d775e610f5e3964c9f463f43f6604405b70cd5bf98ee7f2b27654cb83e"},{"filename":"aigc-claw/frontend/components/stages/index.ts","content":"export { default as ScriptStage } from './ScriptStage';\nexport { default as CharacterStage } from './CharacterStage';\nexport { default as StoryboardStage } from './StoryboardStage';\nexport { default as ReferenceStage } from './ReferenceStage';\nexport { default as VideoStage } from './VideoStage';\nexport { default as PostProductionStage } from './PostProductionStage';\nexport type { StageState, StageStatus, StageViewProps } from './types';\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":442,"content_sha256":"99ca5d2ed4483904ca224141b77c0919b0e563da1d963aa1fe261d7714d3c186"},{"filename":"aigc-claw/frontend/components/stages/PostProductionStage.tsx","content":"'use client';\n\nimport React from 'react';\nimport { Download, Film } from 'lucide-react';\nimport type { StageViewProps } from './types';\nimport { assetUrl } from './utils';\nimport StageProgress from './StageProgress';\nimport StageActions from './StageActions';\n\nexport default function PostProductionStage({ state, onConfirm, onRegenerate, showConfirm, isRunning, hasPendingItems, hasNextStageStarted }: StageViewProps) {\n // payload: { session_id, final_video: \"code/result/video/{sid}/{sid}_final.mp4\" }\n const finalVideoPath: string = state.artifact?.final_video || '';\n const finalVideoUrl = assetUrl(finalVideoPath);\n\n return (\n \u003cdiv className=\"flex flex-col h-full\">\n \u003cdiv className=\"flex-1 overflow-y-auto p-6\">\n \u003ch2 className=\"text-lg font-semibold text-gray-800 mb-1\">后期剪辑\u003c/h2>\n \u003cp className=\"text-sm text-gray-500 mb-6\">拼接所有视频片段,合成最终成片\u003c/p>\n\n {/* 运行中 */}\n {state.status === 'running' && (\n \u003cStageProgress message={state.progressMessage} fallback=\"正在合成最终视频...\" progress={state.progress} color=\"cyan\" />\n )}\n\n {state.error && (\n \u003cdiv className=\"text-sm text-red-600 bg-red-50 border border-red-200 p-4 rounded-xl mb-4\">{state.error}\u003c/div>\n )}\n\n {/* 最终视频 */}\n {finalVideoUrl && (\n \u003cdiv className=\"space-y-4\">\n \u003cdiv className=\"bg-black rounded-xl overflow-hidden shadow-lg\">\n \u003cvideo src={finalVideoUrl} controls className=\"w-full max-h-[65vh]\" />\n \u003c/div>\n \u003cdiv className=\"flex items-center justify-center gap-3\">\n \u003ca\n href={finalVideoUrl}\n download\n className=\"flex items-center gap-2 px-5 py-2.5 bg-blue-500 text-white rounded-xl text-sm font-medium hover:bg-blue-600 transition-colors\"\n >\n \u003cDownload className=\"w-4 h-4\" />\n 下载成片\n \u003c/a>\n \u003c/div>\n \u003c/div>\n )}\n\n {state.status === 'completed' && !finalVideoUrl && (\n \u003cdiv className=\"flex flex-col items-center justify-center py-20 text-gray-400\">\n \u003cFilm className=\"w-12 h-12 mb-3\" />\n \u003cdiv className=\"text-sm\">视频合成完成\u003c/div>\n \u003c/div>\n )}\n\n {state.status === 'pending' && (\n \u003cdiv className=\"text-center text-gray-400 text-sm py-20\">等待上一阶段完成...\u003c/div>\n )}\n \u003c/div>\n\n \u003cStageActions\n status={state.status}\n onConfirm={onConfirm}\n showConfirm={false}\n onRegenerate={onRegenerate}\n stageId=\"post_production\"\n hasPendingItems={hasPendingItems}\n hasNextStageStarted={hasNextStageStarted}\n isRunning={isRunning}\n />\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":2811,"content_sha256":"4674a72c022389cf47cff607e0505da0b88e3e52e97c0fa7e5eb4874bf893fd9"},{"filename":"aigc-claw/frontend/components/stages/ReferenceStage.tsx","content":"'use client';\n\nimport React, { useState, useRef, useEffect } from 'react';\nimport { Image as ImageIcon, RefreshCw, ChevronLeft, ChevronRight, Loader, AlertCircle, ZoomIn, ImagePlus, Edit2, Save } from 'lucide-react';\nimport type { StageViewProps } from './types';\nimport { assetUrl } from './utils';\nimport StageActions from './StageActions';\nimport StageProgress from './StageProgress';\nimport ImageLightbox from './ImageLightbox';\n\n/* ─── 类型 ─── */\ninterface SceneItem {\n id: string; // shot_001_01, shot_001_02, ...\n name: string; // 场景1-镜头1\n index?: number; // 全局编号\n description: string; // 视觉提示词\n selected: string; // 当前选中的文件路径\n versions: string[]; // 所有历史版本路径\n status?: 'pending' | 'done' | 'failed';\n}\n\n/* ─── 水平滚动图片画廊 ─── */\nfunction ImageGallery({\n versions,\n selected,\n onSelect,\n showPlaceholder,\n}: {\n versions: string[];\n selected: string;\n onSelect: (path: string) => void;\n showPlaceholder?: boolean;\n}) {\n const scrollRef = useRef\u003cHTMLDivElement>(null);\n const [lightboxIndex, setLightboxIndex] = useState\u003cnumber | null>(null);\n\n const scroll = (dir: 'left' | 'right') => {\n if (!scrollRef.current) return;\n scrollRef.current.scrollBy({ left: dir === 'left' ? -260 : 260, behavior: 'smooth' });\n };\n\n if (!versions.length) {\n return (\n \u003cdiv className=\"flex items-center justify-center h-full text-gray-400 text-xs\">\n 暂无图片\n \u003c/div>\n );\n }\n\n return (\n \u003cdiv className=\"relative group\">\n {versions.length > 1 && (\n \u003c>\n \u003cbutton\n onClick={() => scroll('left')}\n className=\"absolute left-0 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white/90 shadow border border-gray-200 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n \u003cChevronLeft className=\"w-4 h-4 text-gray-600\" />\n \u003c/button>\n \u003cbutton\n onClick={() => scroll('right')}\n className=\"absolute right-0 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white/90 shadow border border-gray-200 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n \u003cChevronRight className=\"w-4 h-4 text-gray-600\" />\n \u003c/button>\n \u003c/>\n )}\n \u003cdiv\n ref={scrollRef}\n className=\"flex gap-3 overflow-x-auto scrollbar-hide py-1 px-1\"\n style={{ scrollbarWidth: 'none' }}\n >\n {versions.map((path, i) => {\n const isSelected = path === selected;\n return (\n \u003cdiv\n key={path}\n onClick={() => onSelect(path)}\n className={`flex-shrink-0 cursor-pointer rounded-lg overflow-hidden transition-all ${\n isSelected\n ? 'ring-3 ring-emerald-500 shadow-lg shadow-emerald-200'\n : 'ring-1 ring-gray-200 hover:ring-gray-300 hover:shadow-md'\n }`}\n >\n \u003cdiv className=\"relative group/img\">\n \u003cimg\n src={assetUrl(path)}\n alt={`v${i + 1}`}\n className=\"h-28 w-auto object-cover\"\n onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}\n />\n \u003cbutton\n onClick={(e) => { e.stopPropagation(); setLightboxIndex(i); }}\n className=\"absolute top-1 right-1 w-6 h-6 rounded-full bg-black/40 flex items-center justify-center opacity-0 group-hover/img:opacity-100 transition-opacity hover:bg-black/60\"\n title=\"放大查看\"\n >\n \u003cZoomIn className=\"w-3 h-3 text-white\" />\n \u003c/button>\n \u003c/div>\n \u003cdiv className={`text-center text-[10px] py-0.5 ${\n isSelected ? 'bg-emerald-500 text-white font-medium' : 'bg-gray-50 text-gray-400'\n }`}>\n v{i + 1}\n \u003c/div>\n \u003c/div>\n );\n })}\n {showPlaceholder && (\n \u003cdiv className=\"flex-shrink-0 flex items-center justify-center h-28 aspect-video bg-gray-50 rounded-lg border border-dashed border-gray-200\">\n \u003cdiv className=\"flex items-center gap-2 text-gray-400 text-xs\">\n \u003cLoader className=\"w-4 h-4 animate-spin\" />\n \u003cspan>生成中...\u003c/span>\n \u003c/div>\n \u003c/div>\n )}\n \u003c/div>\n {lightboxIndex !== null && (\n \u003cImageLightbox\n images={versions}\n initialIndex={lightboxIndex}\n onClose={() => setLightboxIndex(null)}\n />\n )}\n \u003c/div>\n );\n}\n\n/* ─── 场景行 ─── */\nfunction SceneRow({\n scene,\n canEdit,\n editDesc,\n onDescChange,\n onRegenerate,\n onSelectVersion,\n onSavePrompt,\n isStageRunning,\n isRegenerating,\n isEditing,\n onToggleEdit,\n}: {\n scene: SceneItem;\n canEdit: boolean;\n editDesc: string;\n onDescChange: (val: string) => void;\n onRegenerate: () => void;\n onSelectVersion: (path: string) => void;\n onSavePrompt: () => void;\n isStageRunning?: boolean;\n isRegenerating?: boolean;\n isEditing?: boolean;\n onToggleEdit?: () => void;\n}) {\n const isPending = scene.status === 'pending' || isRegenerating;\n const isFailed = scene.status === 'failed' && !isRegenerating;\n const isPendingEmpty = scene.status === 'pending' && !scene.versions.length && !isRegenerating;\n const hasChanges = editDesc !== scene.description;\n\n return (\n \u003cdiv className={`flex border rounded-xl overflow-hidden bg-white ${\n isFailed ? 'border-red-200' : 'border-gray-200'\n }`}>\n {/* 左侧: 提示词 */}\n \u003cdiv className=\"w-[360px] flex-shrink-0 p-4 border-r border-gray-100 flex flex-col\">\n \u003cdiv className=\"flex items-center gap-2 mb-2\">\n \u003cspan className=\"flex items-center justify-center w-6 h-6 rounded-full bg-emerald-100 text-emerald-700 text-xs font-bold flex-shrink-0\">\n {scene.index ?? scene.id.replace('Scene_', '')}\n \u003c/span>\n \u003cspan className=\"text-sm font-semibold text-gray-800 truncate\">{scene.name}\u003c/span>\n {isPending && (\n \u003cspan className=\"inline-flex items-center gap-1 text-[10px] bg-amber-50 text-amber-600 px-1.5 py-0.5 rounded\">\n \u003cLoader className=\"w-2.5 h-2.5 animate-spin\" />生成中\n \u003c/span>\n )}\n {isFailed && (\n \u003cspan className=\"text-[10px] bg-red-50 text-red-500 px-1.5 py-0.5 rounded\">失败\u003c/span>\n )}\n {/* 编辑/保存按钮 */}\n {canEdit && !isStageRunning && (\n isEditing ? (\n \u003cbutton\n onClick={onSavePrompt}\n disabled={!hasChanges}\n className={`ml-auto flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium transition-colors ${\n hasChanges\n ? 'text-white bg-emerald-500 hover:bg-emerald-600'\n : 'text-gray-400 bg-gray-100 cursor-not-allowed'\n }`}\n >\n \u003cSave className=\"w-3 h-3\" />\n 保存\n \u003c/button>\n ) : (\n \u003cbutton\n onClick={onToggleEdit}\n className=\"ml-auto flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 transition-colors\"\n >\n \u003cEdit2 className=\"w-3 h-3\" />\n 编辑\n \u003c/button>\n )\n )}\n \u003c/div>\n {isEditing ? (\n \u003ctextarea\n value={editDesc}\n onChange={e => onDescChange(e.target.value)}\n rows={6}\n className=\"flex-1 text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded-lg p-2 resize-none focus:outline-none focus:ring-1 focus:ring-emerald-300\"\n />\n ) : (\n \u003cp className=\"flex-1 text-xs text-gray-600 leading-relaxed line-clamp-6\">{scene.description}\u003c/p>\n )}\n {/* 重新生成按钮 */}\n {!isStageRunning && (\n \u003cbutton\n onClick={onRegenerate}\n disabled={isRegenerating}\n className={`mt-3 flex items-center gap-1.5 self-start px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${\n isRegenerating\n ? 'text-gray-400 bg-gray-100 cursor-not-allowed'\n : isFailed\n ? 'text-red-600 bg-red-50 hover:bg-red-100'\n : 'text-emerald-600 bg-emerald-50 hover:bg-emerald-100'\n }`}\n >\n \u003cRefreshCw className={`w-3 h-3 ${isRegenerating ? 'animate-spin' : ''}`} />\n {isRegenerating ? '生成中...' : isFailed ? '点击重试' : '重新生成'}\n \u003c/button>\n )}\n \u003c/div>\n\n {/* 右侧: 图片画廊 / 占位 */}\n \u003cdiv className=\"flex-1 min-w-0 p-3 flex items-center\">\n {isPendingEmpty ? (\n \u003cdiv\n className=\"flex items-center justify-center h-28 aspect-video bg-emerald-50/50 rounded-lg border border-dashed border-emerald-200 cursor-pointer hover:bg-emerald-100/50 transition-colors\"\n onClick={onRegenerate}\n >\n \u003cdiv className=\"flex flex-col items-center gap-1 text-emerald-500 text-xs\">\n \u003cImagePlus className=\"w-4 h-4\" />\n \u003cspan>生成首帧参考图\u003c/span>\n \u003c/div>\n \u003c/div>\n ) : isPending && !scene.versions.length ? (\n \u003cdiv className=\"flex items-center justify-center h-28 aspect-video bg-gray-50 rounded-lg border border-dashed border-gray-200\">\n \u003cdiv className=\"flex items-center gap-2 text-gray-400 text-xs\">\n \u003cLoader className=\"w-4 h-4 animate-spin\" />\n \u003cspan>正在生成...\u003c/span>\n \u003c/div>\n \u003c/div>\n ) : isFailed && !scene.versions.length ? (\n \u003cdiv\n className=\"flex items-center justify-center h-28 aspect-video bg-red-50/50 rounded-lg border border-dashed border-red-200 cursor-pointer hover:bg-red-100/50 transition-colors\"\n onClick={onRegenerate}\n >\n \u003cdiv className=\"flex flex-col items-center gap-1 text-red-400 text-xs\">\n \u003cAlertCircle className=\"w-4 h-4\" />\n \u003cspan>生成失败,点击重试\u003c/span>\n \u003c/div>\n \u003c/div>\n ) : (\n \u003cdiv className=\"relative w-full\">\n \u003cImageGallery\n versions={scene.versions}\n selected={scene.selected}\n onSelect={onSelectVersion}\n showPlaceholder={isRegenerating}\n />\n {isFailed && (\n \u003cbutton\n onClick={onRegenerate}\n className=\"absolute top-1 right-1 z-10 flex items-center gap-1 px-2 py-1 rounded-lg text-[10px] font-medium text-white bg-red-500/80 hover:bg-red-600 shadow transition-colors\"\n >\n \u003cRefreshCw className=\"w-2.5 h-2.5\" />\n 重试\n \u003c/button>\n )}\n \u003c/div>\n )}\n \u003c/div>\n \u003c/div>\n );\n}\n\n/* ─── 主组件 ─── */\nexport default function ReferenceStage({ state, sessionId, onConfirm, onIntervene, onRegenerate, onUpdateArtifact, onSaveSelections, showConfirm, isRunning, hasPendingItems, hasNextStageStarted }: StageViewProps) {\n // 兼容旧格式: scene_images: {Scene_1: \"path\"} → scenes: [{id, ...}]\n const scenes: SceneItem[] = (() => {\n if (state.artifact?.scenes?.length) return state.artifact.scenes;\n if (state.artifact?.scene_images) {\n const si = state.artifact.scene_images as Record\u003cstring, string>;\n return Object.entries(si)\n .sort(([a], [b]) => {\n const na = parseInt(a.replace(/\\D/g, '')) || 0;\n const nb = parseInt(b.replace(/\\D/g, '')) || 0;\n return na - nb;\n })\n .map(([id, path]) => ({\n id,\n name: `场景 ${id.replace('Scene_', '')}`,\n description: '',\n selected: path,\n versions: [path],\n status: 'done' as const,\n }));\n }\n return [];\n })();\n\n const [editDescs, setEditDescs] = useState\u003cRecord\u003cstring, string>>({});\n const [selectedVersions, setSelectedVersions] = useState\u003cRecord\u003cstring, string>>({});\n const [regeneratingIds, setRegeneratingIds] = useState\u003cSet\u003cstring>>(new Set());\n const [editingIds, setEditingIds] = useState\u003cSet\u003cstring>>(new Set());\n const [savingIds, setSavingIds] = useState\u003cSet\u003cstring>>(new Set());\n\n const canEdit = state.status === 'waiting' || state.status === 'completed';\n\n // 当场景数据变化时,初始化编辑描述\n useEffect(() => {\n if (scenes.length > 0) {\n setEditDescs(prev => {\n const next: Record\u003cstring, string> = {};\n scenes.forEach(s => { next[s.id] = prev[s.id] ?? s.description; });\n return next;\n });\n }\n }, [scenes]);\n\n // 当 artifact 更新时清除重新生成状态\n useEffect(() => {\n if (regeneratingIds.size > 0) setRegeneratingIds(new Set());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [state.artifact]);\n\n const hasScenes = scenes.length > 0;\n\n // 保存单个提示词到后端 JSON\n const handleSavePrompt = async (sceneId: string) => {\n const newPrompt = editDescs[sceneId];\n if (!newPrompt) return;\n\n setSavingIds(prev => new Set(prev).add(sceneId));\n try {\n // 调用后端 API 保存提示词\n const response = await fetch(`/api/project/${sessionId}/artifact/reference_generation`, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n shots: scenes.map(s => ({\n shot_id: s.id,\n visual_prompt: s.id === sceneId ? newPrompt : s.description\n }))\n })\n });\n if (response.ok) {\n // 保存成功后关闭编辑模式\n setEditingIds(prev => {\n const next = new Set(prev);\n next.delete(sceneId);\n return next;\n });\n // 更新本地状态,使用保存后的值\n setEditDescs(prev => ({ ...prev, [sceneId]: newPrompt }));\n // 同步更新 artifact 以便后续阶段能获取最新的提示词\n if (onUpdateArtifact && state.artifact?.scenes) {\n const updatedScenes = state.artifact.scenes.map((s: SceneItem) =>\n s.id === sceneId ? { ...s, description: newPrompt } : s\n );\n onUpdateArtifact({ scenes: updatedScenes });\n }\n }\n } catch (error) {\n console.error('保存提示词失败:', error);\n } finally {\n setSavingIds(prev => {\n const next = new Set(prev);\n next.delete(sceneId);\n return next;\n });\n }\n };\n\n // 切换编辑模式\n const handleToggleEdit = (sceneId: string) => {\n setEditingIds(prev => {\n const next = new Set(prev);\n if (next.has(sceneId)) {\n next.delete(sceneId);\n } else {\n next.add(sceneId);\n }\n return next;\n });\n };\n\n const handleRegenerate = (sceneId: string) => {\n setRegeneratingIds(prev => new Set(prev).add(sceneId));\n onIntervene({ regenerate_scenes: [sceneId] });\n };\n\n const handleSelectVersion = async (sceneId: string, path: string) => {\n setSelectedVersions(prev => ({ ...prev, [sceneId]: path }));\n // 同步更新 artifact 以便确认时能传递正确的选中图片给阶段5\n if (onUpdateArtifact && state.artifact?.scenes) {\n const updatedScenes = state.artifact.scenes.map((s: SceneItem) =>\n s.id === sceneId ? { ...s, selected: path } : s\n );\n onUpdateArtifact({ scenes: updatedScenes });\n }\n // 自动保存选择\n const selections: Record\u003cstring, string> = {};\n scenes.forEach(s => { selections[s.id] = selectedVersions[s.id] || s.selected; });\n selections[sceneId] = path;\n if (onSaveSelections) {\n await onSaveSelections(selections);\n }\n };\n\n const getSelected = (scene: SceneItem) => selectedVersions[scene.id] || scene.selected;\n\n return (\n \u003cdiv className=\"flex flex-col h-full\">\n \u003cdiv className=\"flex-1 overflow-y-auto p-6\">\n {/* 标题栏 */}\n \u003cdiv className=\"flex items-center justify-between mb-1\">\n \u003ch2 className=\"text-lg font-semibold text-gray-800\">参考图生成\u003c/h2>\n \u003c/div>\n \u003cp className=\"text-sm text-gray-500 mb-6\">\n 基于角色/场景素材 + 分镜视觉描述,使用图生图生成场景参考图\n \u003c/p>\n\n {/* 运行中 */}\n {state.status === 'running' && (\n \u003cStageProgress message={state.progressMessage} fallback=\"正在生成参考图...\" progress={state.progress} color=\"emerald\" />\n )}\n\n {state.error && (\n \u003cdiv className=\"text-sm text-red-600 bg-red-50 border border-red-200 p-4 rounded-xl mb-4\">{state.error}\u003c/div>\n )}\n\n {/* ═══ 场景列表 ═══ */}\n {hasScenes && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cImageIcon className=\"w-4 h-4 text-emerald-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">场景参考图 ({scenes.length})\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"space-y-3\">\n {scenes.map(scene => (\n \u003cSceneRow\n key={scene.id}\n scene={{ ...scene, selected: getSelected(scene) }}\n canEdit={canEdit}\n editDesc={editDescs[scene.id] || scene.description}\n onDescChange={val => setEditDescs(prev => ({ ...prev, [scene.id]: val }))}\n onRegenerate={() => handleRegenerate(scene.id)}\n onSelectVersion={path => handleSelectVersion(scene.id, path)}\n onSavePrompt={() => handleSavePrompt(scene.id)}\n isStageRunning={state.status === 'running'}\n isRegenerating={regeneratingIds.has(scene.id) || savingIds.has(scene.id)}\n isEditing={editingIds.has(scene.id)}\n onToggleEdit={() => handleToggleEdit(scene.id)}\n />\n ))}\n \u003c/div>\n \u003c/section>\n )}\n\n {/* 如果有 artifact 数据(即使 status 是 pending),也显示内容 */}\n {state.status === 'pending' && !hasScenes && (\n \u003cdiv className=\"text-center text-gray-400 text-sm py-20\">等待上一阶段完成...\u003c/div>\n )}\n \u003c/div>\n\n {/* 底部操作栏 */}\n \u003cStageActions\n status={state.status}\n onConfirm={onConfirm}\n showConfirm={showConfirm}\n onSave={undefined}\n onRegenerate={onRegenerate}\n stageId=\"reference_generation\"\n hasPendingItems={hasPendingItems}\n hasNextStageStarted={hasNextStageStarted}\n isRunning={isRunning}\n />\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":18923,"content_sha256":"26578e6759babe15af75c41339b5e805cfeb7b925d01373371fca6bc77d8b4ae"},{"filename":"aigc-claw/frontend/components/stages/ScriptStage.tsx","content":"'use client';\n\nimport React, { useState, useCallback } from 'react';\nimport { Save, X, Code, LayoutList, Users, MapPin, Film, Sparkles, BookOpen, Lightbulb, Target, User, Crosshair, RefreshCw, Palette } from 'lucide-react';\nimport type { StageViewProps } from './types';\nimport StageActions from './StageActions';\nimport StageProgress from './StageProgress';\n\n/* ─── 类型 ─── */\n\ninterface LoglineData {\n logline: string;\n who: string;\n goal: string;\n conflict: string;\n twist: string;\n theme: string;\n}\n\ninterface ScriptCharacter {\n name: string;\n character_id?: string;\n description: string;\n personality: string[];\n motivation?: string;\n arc_description?: string;\n role: string;\n age?: string;\n species?: string;\n occupation?: string;\n}\n\ninterface ScriptSetting {\n name: string;\n description: string;\n}\n\ninterface ScriptScene {\n scene_number: number;\n act?: number;\n location: string;\n characters: string[];\n plot: string;\n}\n\ninterface ActCompleteData {\n act: number;\n act_name: string;\n characters: ScriptCharacter[];\n settings: ScriptSetting[];\n scenes: ScriptScene[];\n}\n\ninterface ScriptData {\n title?: string;\n logline?: string;\n genre?: string[];\n synopsis?: string;\n characters?: ScriptCharacter[];\n settings?: ScriptSetting[];\n scenes?: ScriptScene[];\n overall_style?: string;\n mood?: string;\n session_id?: string;\n [key: string]: any;\n}\n\n/* ─── 角色色彩 ─── */\nconst ROLE_COLORS: Record\u003cstring, string> = {\n '主角': 'bg-amber-100 text-amber-700',\n 'protagonist': 'bg-amber-100 text-amber-700',\n '配角': 'bg-sky-100 text-sky-700',\n 'supporting': 'bg-sky-100 text-sky-700',\n '背景': 'bg-gray-100 text-gray-500',\n 'background': 'bg-gray-100 text-gray-500',\n};\n\n/* ─── Logline 六要素展示卡 ─── */\nfunction LoglineSummaryBar({ logline }: { logline: LoglineData }) {\n const items = [\n { icon: Lightbulb, label: 'Logline', value: logline.logline, color: 'text-amber-600' },\n { icon: User, label: '主角', value: logline.who, color: 'text-blue-600' },\n { icon: Target, label: '目标', value: logline.goal, color: 'text-green-600' },\n { icon: Crosshair, label: '障碍', value: logline.conflict, color: 'text-red-500' },\n { icon: RefreshCw, label: '反转', value: logline.twist, color: 'text-purple-600' },\n { icon: Palette, label: '主题', value: logline.theme, color: 'text-cyan-600' },\n ];\n return (\n \u003cdiv className=\"bg-gradient-to-r from-amber-50 to-orange-50 border border-amber-200 rounded-xl p-4\">\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cLightbulb className=\"w-4 h-4 text-amber-500\" />\n \u003cspan className=\"text-xs font-semibold text-amber-700\">Logline 核心\u003c/span>\n \u003c/div>\n \u003cdiv className=\"grid grid-cols-6 gap-3\">\n {items.map(({ icon: Icon, label, value, color }) => (\n \u003cdiv key={label} className=\"min-w-0\">\n \u003cdiv className={`flex items-center gap-1 mb-1 ${color}`}>\n \u003cIcon className=\"w-3 h-3 flex-shrink-0\" />\n \u003cspan className=\"text-[10px] font-semibold\">{label}\u003c/span>\n \u003c/div>\n \u003cp className=\"text-xs text-gray-600 leading-relaxed break-words\">{value}\u003c/p>\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/div>\n );\n}\n\nexport default function ScriptStage({ state, onConfirm, onIntervene, onRegenerate, onSaveSelections, showConfirm, isRunning, hasPendingItems, hasNextStageStarted }: StageViewProps) {\n const data: ScriptData = state.artifact || {};\n\n const isLoglinePhase = data.phase === 'logline_selection' || data.phase === 'logline_confirm' || data.phase === 'mode_selection';\n\n const [isEditing, setIsEditing] = useState(false);\n const [editMode, setEditMode] = useState\u003c'structured' | 'raw'>('structured');\n const [editData, setEditData] = useState\u003cScriptData>({});\n const [rawText, setRawText] = useState('');\n\n const hasContent = Boolean(data.title || data.characters?.length || data.scenes?.length);\n\n const startEdit = useCallback(() => {\n setEditData(JSON.parse(JSON.stringify(data)));\n setRawText(JSON.stringify(data, null, 2));\n setIsEditing(true);\n setEditMode('structured');\n }, [data]);\n\n const switchEditMode = useCallback((mode: 'structured' | 'raw') => {\n if (mode === 'raw') {\n setRawText(JSON.stringify(editData, null, 2));\n } else {\n try { setEditData(JSON.parse(rawText)); } catch { /* keep current */ }\n }\n setEditMode(mode);\n }, [editData, rawText]);\n\n const handleSave = useCallback(() => {\n let finalData: ScriptData;\n if (editMode === 'raw') {\n try { finalData = JSON.parse(rawText); } catch { return; }\n } else {\n finalData = editData;\n }\n onIntervene({ modified_script: finalData });\n setIsEditing(false);\n }, [editMode, rawText, editData, onIntervene]);\n\n const cancelEdit = useCallback(() => setIsEditing(false), []);\n\n /* ─── 编辑辅助 ─── */\n const updateField = (field: string, value: any) => setEditData(prev => ({ ...prev, [field]: value }));\n\n const updateCharacter = (idx: number, patch: Partial\u003cScriptCharacter>) => {\n setEditData(prev => ({\n ...prev,\n characters: prev.characters?.map((c, i) => i === idx ? { ...c, ...patch } : c),\n }));\n };\n\n const updateSetting = (idx: number, patch: Partial\u003cScriptSetting>) => {\n setEditData(prev => ({\n ...prev,\n settings: prev.settings?.map((s, i) => i === idx ? { ...s, ...patch } : s),\n }));\n };\n\n const updateScene = (idx: number, patch: Partial\u003cScriptScene>) => {\n setEditData(prev => ({\n ...prev,\n scenes: prev.scenes?.map((s, i) => i === idx ? { ...s, ...patch } : s),\n }));\n };\n\n return (\n \u003cdiv className=\"flex flex-col h-full\">\n \u003cdiv className=\"flex-1 overflow-y-auto p-6\">\n\n {/* 标题栏 */}\n \u003cdiv className=\"flex items-center justify-between mb-1\">\n \u003ch2 className=\"text-lg font-semibold text-gray-800\">剧本生成\u003c/h2>\n {isEditing && (\n \u003cdiv className=\"flex gap-1 bg-gray-100 rounded-lg p-1 text-xs\">\n \u003cbutton onClick={() => switchEditMode('structured')}\n className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-colors ${editMode === 'structured' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}>\n \u003cLayoutList className=\"w-3.5 h-3.5\" />结构编辑\n \u003c/button>\n \u003cbutton onClick={() => switchEditMode('raw')}\n className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-colors ${editMode === 'raw' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}>\n \u003cCode className=\"w-3.5 h-3.5\" />JSON编辑\n \u003c/button>\n \u003c/div>\n )}\n \u003c/div>\n \u003cp className=\"text-sm text-gray-500 mb-6\">多轮 LLM 交互,生成结构化剧本数据\u003c/p>\n\n {/* 运行中 - 进度条 & 已选 Logline & 增量生成结果 */}\n {state.status === 'running' && (\n \u003c>\n {data.selected_logline && (\n \u003cdiv className=\"mb-4\">\n \u003cLoglineSummaryBar logline={data.selected_logline as LoglineData} />\n \u003c/div>\n )}\n\n {/* 节拍表展示 */}\n {data.beat_sheet && (\n \u003cdiv className=\"mb-4\">\n \u003cdiv className=\"flex items-center gap-2 mb-2\">\n \u003cBookOpen className=\"w-4 h-4 text-orange-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">节拍表 (Beat Sheet)\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"bg-white border border-gray-200 rounded-xl p-4\">\n \u003cpre className=\"text-sm text-gray-600 leading-relaxed whitespace-pre-wrap font-sans\">{data.beat_sheet as string}\u003c/pre>\n \u003c/div>\n \u003c/div>\n )}\n\n {/* 逐幕完成的分场结果 */}\n {data.completed_acts && (data.completed_acts as ActCompleteData[]).length > 0 && (\n \u003cdiv className=\"mb-4 space-y-4\">\n {(data.completed_acts as ActCompleteData[]).map((actData) => (\n \u003cdiv key={actData.act}>\n {/* 幕分隔线 */}\n \u003cdiv className=\"flex items-center gap-3 mb-3\">\n \u003cdiv className=\"flex-1 h-px bg-gradient-to-r from-purple-200 to-transparent\" />\n \u003cspan className=\"px-3 py-1 bg-purple-50 text-purple-600 text-xs font-semibold rounded-full whitespace-nowrap\">\n 第{actData.act}幕 — {actData.act_name}\n \u003c/span>\n \u003cdiv className=\"flex-1 h-px bg-gradient-to-l from-purple-200 to-transparent\" />\n \u003c/div>\n\n {/* 本幕场景 */}\n \u003cdiv className=\"space-y-2\">\n {actData.scenes.map((sc, i) => (\n \u003cdiv key={i} className=\"bg-white border border-gray-200 rounded-xl p-4 hover:shadow-sm transition-shadow\">\n \u003cdiv className=\"flex items-center gap-3 mb-2\">\n \u003cspan className=\"flex items-center justify-center w-7 h-7 rounded-full bg-purple-100 text-purple-700 text-xs font-bold flex-shrink-0\">{sc.scene_number}\u003c/span>\n \u003cspan className=\"px-2 py-0.5 bg-green-50 text-green-600 text-xs rounded-full\">{sc.location}\u003c/span>\n \u003cdiv className=\"flex flex-wrap gap-1\">\n {(sc.characters || []).map((c: any, ci: number) => (\n \u003cspan key={ci} className=\"px-2 py-0.5 bg-blue-50 text-blue-600 text-xs rounded-full\">{c}\u003c/span>\n ))}\n \u003c/div>\n \u003c/div>\n \u003cp className=\"text-sm text-gray-600 leading-relaxed pl-10\">{sc.plot}\u003c/p>\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/div>\n ))}\n \u003c/div>\n )}\n\n \u003cStageProgress message={state.progressMessage} fallback=\"正在生成剧本...\" progress={state.progress} color=\"blue\" />\n \u003c/>\n )}\n\n {/* 错误 */}\n {state.error && (\n \u003cdiv className=\"text-sm text-red-600 bg-red-50 border border-red-200 p-4 rounded-xl mb-4\">{state.error}\u003c/div>\n )}\n\n {/* ===== Logline 选择/确认阶段 ===== */}\n {isLoglinePhase && state.status === 'waiting' && (\n \u003cdiv className=\"space-y-4\">\n {/* 3 个 Logline 选项卡 */}\n {data.phase === 'logline_selection' && data.logline_options && (\n \u003c>\n \u003cdiv className=\"flex items-center gap-2 mb-2\">\n \u003cLightbulb className=\"w-4 h-4 text-amber-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">选择一个 Logline 方案\u003c/h3>\n \u003cspan className=\"text-xs text-gray-400\">点击卡片以选择\u003c/span>\n \u003c/div>\n \u003cdiv className=\"grid grid-cols-3 gap-3\">\n {(data.logline_options as LoglineData[]).map((opt, i) => (\n \u003cbutton\n key={i}\n onClick={() => onIntervene({ selected_logline: opt })}\n className=\"text-left p-4 bg-white border border-gray-200 rounded-xl hover:border-blue-400 hover:shadow-md transition-all group cursor-pointer\"\n >\n \u003cp className=\"text-sm font-medium text-gray-800 group-hover:text-blue-600 mb-3 leading-relaxed\">\n {opt.logline}\n \u003c/p>\n \u003cdiv className=\"space-y-1.5 text-xs text-gray-500\">\n \u003cp>\u003cspan className=\"text-gray-600 font-medium\">主角:\u003c/span> {opt.who}\u003c/p>\n \u003cp>\u003cspan className=\"text-gray-600 font-medium\">目标:\u003c/span> {opt.goal}\u003c/p>\n \u003cp>\u003cspan className=\"text-gray-600 font-medium\">障碍:\u003c/span> {opt.conflict}\u003c/p>\n \u003cp>\u003cspan className=\"text-gray-600 font-medium\">反转:\u003c/span> {opt.twist}\u003c/p>\n \u003cp>\u003cspan className=\"text-gray-600 font-medium\">主题:\u003c/span> {opt.theme}\u003c/p>\n \u003c/div>\n \u003c/button>\n ))}\n \u003c/div>\n \u003c/>\n )}\n\n {/* 单个 Logline 确认 */}\n {data.phase === 'logline_confirm' && data.logline_summary && (\n \u003c>\n \u003cdiv className=\"flex items-center gap-2 mb-2\">\n \u003cLightbulb className=\"w-4 h-4 text-amber-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">Logline 提取结果\u003c/h3>\n \u003c/div>\n \u003cLoglineSummaryBar logline={data.logline_summary as LoglineData} />\n \u003cdiv className=\"flex justify-center pt-2\">\n \u003cbutton\n onClick={() => onIntervene({ selected_logline: data.logline_summary })}\n className=\"flex items-center gap-2 px-5 py-2.5 bg-blue-500 text-white rounded-lg text-sm font-medium hover:bg-blue-600 transition-colors\"\n >\n \u003cSparkles className=\"w-4 h-4\" />\n 确认 Logline 并生成剧本\n \u003c/button>\n \u003c/div>\n \u003c/>\n )}\n\n {/* 创作模式选择 */}\n {data.phase === 'mode_selection' && (\n \u003c>\n {data.selected_logline && (\n \u003cdiv className=\"mb-4\">\n \u003cLoglineSummaryBar logline={data.selected_logline as LoglineData} />\n \u003c/div>\n )}\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cFilm className=\"w-4 h-4 text-purple-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">选择创作模式\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"grid grid-cols-2 gap-4\">\n \u003cbutton\n onClick={() => onIntervene({ selected_mode: 'movie' })}\n className=\"text-left p-5 bg-white border-2 border-gray-200 rounded-xl hover:border-purple-400 hover:shadow-lg transition-all group cursor-pointer\"\n >\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cspan className=\"flex items-center justify-center w-10 h-10 rounded-xl bg-purple-100 text-purple-600 text-lg\">🎬\u003c/span>\n \u003cspan className=\"text-base font-semibold text-gray-800 group-hover:text-purple-600\">电影模式\u003c/span>\n \u003c/div>\n \u003cp className=\"text-sm text-gray-600 leading-relaxed mb-3\">\n 按照四幕结构生成完整情节,叙事连贯丰富,有完整的起承转合。\n \u003c/p>\n \u003cdiv className=\"flex flex-wrap gap-1.5\">\n \u003cspan className=\"px-2 py-0.5 bg-purple-50 text-purple-500 text-xs rounded-full\">四幕结构\u003c/span>\n \u003cspan className=\"px-2 py-0.5 bg-purple-50 text-purple-500 text-xs rounded-full\">叙事完整\u003c/span>\n \u003cspan className=\"px-2 py-0.5 bg-purple-50 text-purple-500 text-xs rounded-full\">情节丰富\u003c/span>\n \u003c/div>\n \u003c/button>\n \u003cbutton\n onClick={() => onIntervene({ selected_mode: 'micro' })}\n className=\"text-left p-5 bg-white border-2 border-gray-200 rounded-xl hover:border-cyan-400 hover:shadow-lg transition-all group cursor-pointer\"\n >\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cspan className=\"flex items-center justify-center w-10 h-10 rounded-xl bg-cyan-100 text-cyan-600 text-lg\">🎞️\u003c/span>\n \u003cspan className=\"text-base font-semibold text-gray-800 group-hover:text-cyan-600\">微电影模式\u003c/span>\n \u003c/div>\n \u003cp className=\"text-sm text-gray-600 leading-relaxed mb-3\">\n 所有内容生成在一幕内,叙事节奏快,情节紧凑,适合短片创作。\n \u003c/p>\n \u003cdiv className=\"flex flex-wrap gap-1.5\">\n \u003cspan className=\"px-2 py-0.5 bg-cyan-50 text-cyan-500 text-xs rounded-full\">单幕结构\u003c/span>\n \u003cspan className=\"px-2 py-0.5 bg-cyan-50 text-cyan-500 text-xs rounded-full\">节奏紧凑\u003c/span>\n \u003cspan className=\"px-2 py-0.5 bg-cyan-50 text-cyan-500 text-xs rounded-full\">3-6场景\u003c/span>\n \u003c/div>\n \u003c/button>\n \u003c/div>\n \u003c/>\n )}\n \u003c/div>\n )}\n\n {/* ===== 查看模式 ===== */}\n {hasContent && !isEditing && (\n \u003cdiv className=\"space-y-8\">\n\n {/* Logline 六要素摘要 */}\n {data.logline_data && (\n \u003cLoglineSummaryBar logline={data.logline_data as LoglineData} />\n )}\n\n {/* 标题 / Logline / 标签 */}\n {data.title && (\n \u003csection className=\"bg-white border border-gray-200 rounded-xl p-5\">\n \u003ch3 className=\"text-xl font-bold text-gray-800 mb-2\">{data.title}\u003c/h3>\n {data.logline && \u003cp className=\"text-sm text-gray-500 mb-3\">{data.logline}\u003c/p>}\n \u003cdiv className=\"flex flex-wrap gap-1.5\">\n {data.genre?.map((g, i) => (\n \u003cspan key={i} className=\"px-2.5 py-0.5 bg-violet-50 text-violet-600 text-xs rounded-full font-medium\">{g}\u003c/span>\n ))}\n {data.mood && \u003cspan className=\"px-2.5 py-0.5 bg-pink-50 text-pink-600 text-xs rounded-full font-medium\">{data.mood}\u003c/span>}\n {data.overall_style && \u003cspan className=\"px-2.5 py-0.5 bg-cyan-50 text-cyan-600 text-xs rounded-full font-medium\">{data.overall_style}\u003c/span>}\n \u003c/div>\n \u003c/section>\n )}\n\n {/* 故事梗概 */}\n {data.synopsis && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cBookOpen className=\"w-4 h-4 text-orange-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">故事梗概\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"bg-white border border-gray-200 rounded-xl p-5\">\n \u003cp className=\"text-sm text-gray-600 leading-relaxed\">{data.synopsis}\u003c/p>\n \u003c/div>\n \u003c/section>\n )}\n\n {/* 角色 */}\n {data.characters && data.characters.length > 0 && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cUsers className=\"w-4 h-4 text-blue-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">角色 ({data.characters.length})\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {data.characters.map((c, i) => (\n \u003cdiv key={i} className=\"bg-white border border-gray-200 rounded-xl p-4 hover:shadow-sm transition-shadow\">\n \u003cdiv className=\"flex items-center gap-2 mb-2\">\n \u003cspan className=\"font-medium text-gray-800\">{c.name}\u003c/span>\n \u003cspan className={`px-1.5 py-0.5 text-[10px] rounded ${ROLE_COLORS[c.role] || 'bg-gray-100 text-gray-500'}`}>{c.role}\u003c/span>\n {c.species && c.species !== '人类' && c.species !== 'human' && (\n \u003cspan className=\"px-1.5 py-0.5 bg-emerald-50 text-emerald-600 text-[10px] rounded\">{c.species}\u003c/span>\n )}\n \u003c/div>\n \u003cp className=\"text-sm text-gray-500 leading-relaxed mb-2\">{c.description}\u003c/p>\n {c.personality && (\n \u003cdiv className=\"flex flex-wrap gap-1 mb-2\">\n {(Array.isArray(c.personality) ? c.personality : String(c.personality).split(/[,,]/)).map((p: string, pi: number) => (\n \u003cspan key={pi} className=\"px-1.5 py-0.5 bg-blue-50 text-blue-500 text-[10px] rounded\">{p.trim()}\u003c/span>\n ))}\n \u003c/div>\n )}\n {c.motivation && \u003cp className=\"text-xs text-gray-400\">\u003cspan className=\"text-gray-500 font-medium\">动机:\u003c/span> {c.motivation}\u003c/p>}\n {c.arc_description && \u003cp className=\"text-xs text-gray-400\">\u003cspan className=\"text-gray-500 font-medium\">成长:\u003c/span> {c.arc_description}\u003c/p>}\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/section>\n )}\n\n {/* 场景设置 */}\n {data.settings && data.settings.length > 0 && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cMapPin className=\"w-4 h-4 text-green-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">场景 ({data.settings.length})\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {data.settings.map((s, i) => (\n \u003cdiv key={i} className=\"bg-white border border-gray-200 rounded-xl p-4 hover:shadow-sm transition-shadow\">\n \u003cdiv className=\"font-medium text-gray-800 mb-1\">{s.name}\u003c/div>\n \u003cp className=\"text-sm text-gray-500 leading-relaxed\">{s.description}\u003c/p>\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/section>\n )}\n\n {/* 故事线 */}\n {data.scenes && data.scenes.length > 0 && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cFilm className=\"w-4 h-4 text-purple-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">故事线 ({data.scenes.length} 场)\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"space-y-3\">\n {data.scenes.map((sc, i) => {\n // 幕分隔线:当场景有 act 字段,且是第一场或与上一场不同幕时显示\n const showActSep = sc.act != null && (i === 0 || data.scenes![i - 1].act !== sc.act);\n const actNames: Record\u003cnumber, string> = { 1: '激励事件', 2: '进入新世界', 3: '灵魂黑夜', 4: '高潮决战' };\n return (\n \u003cReact.Fragment key={i}>\n {showActSep && (\n \u003cdiv className=\"flex items-center gap-3 pt-2\">\n \u003cdiv className=\"flex-1 h-px bg-gradient-to-r from-purple-200 to-transparent\" />\n \u003cspan className=\"px-3 py-1 bg-purple-50 text-purple-600 text-xs font-semibold rounded-full whitespace-nowrap\">\n 第{sc.act}幕 — {actNames[sc.act!] || ''}\n \u003c/span>\n \u003cdiv className=\"flex-1 h-px bg-gradient-to-l from-purple-200 to-transparent\" />\n \u003c/div>\n )}\n \u003cdiv className=\"bg-white border border-gray-200 rounded-xl p-4 hover:shadow-sm transition-shadow\">\n \u003cdiv className=\"flex items-center gap-3 mb-2\">\n \u003cspan className=\"flex items-center justify-center w-7 h-7 rounded-full bg-purple-100 text-purple-700 text-xs font-bold flex-shrink-0\">{sc.scene_number}\u003c/span>\n \u003cspan className=\"px-2 py-0.5 bg-green-50 text-green-600 text-xs rounded-full\">{sc.location}\u003c/span>\n \u003cdiv className=\"flex flex-wrap gap-1\">\n {(sc.characters || []).map((c: any, ci: number) => (\n \u003cspan key={ci} className=\"px-2 py-0.5 bg-blue-50 text-blue-600 text-xs rounded-full\">{c}\u003c/span>\n ))}\n \u003c/div>\n \u003c/div>\n \u003cp className=\"text-sm text-gray-600 leading-relaxed pl-10\">{sc.plot}\u003c/p>\n \u003c/div>\n \u003c/React.Fragment>\n );\n })}\n \u003c/div>\n \u003c/section>\n )}\n \u003c/div>\n )}\n\n {/* ===== 结构编辑模式 ===== */}\n {isEditing && editMode === 'structured' && (\n \u003cdiv className=\"space-y-6\">\n\n {/* 基础信息 */}\n \u003csection className=\"bg-white border border-gray-200 rounded-xl p-4 space-y-3\">\n \u003ch4 className=\"text-sm font-semibold text-gray-700 flex items-center gap-2\">\u003cSparkles className=\"w-3.5 h-3.5 text-violet-500\" />基本信息\u003c/h4>\n \u003cdiv className=\"grid grid-cols-2 gap-3\">\n \u003clabel className=\"flex flex-col gap-1 text-xs\">\n \u003cspan className=\"text-gray-500 font-medium\">标题\u003c/span>\n \u003cinput type=\"text\" value={editData.title || ''} onChange={e => updateField('title', e.target.value)}\n className=\"border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none\" />\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1 text-xs\">\n \u003cspan className=\"text-gray-500 font-medium\">情绪基调\u003c/span>\n \u003cinput type=\"text\" value={editData.mood || ''} onChange={e => updateField('mood', e.target.value)}\n className=\"border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none\" />\n \u003c/label>\n \u003c/div>\n \u003clabel className=\"flex flex-col gap-1 text-xs\">\n \u003cspan className=\"text-gray-500 font-medium\">Logline\u003c/span>\n \u003cinput type=\"text\" value={editData.logline || ''} onChange={e => updateField('logline', e.target.value)}\n className=\"border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none\" />\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1 text-xs\">\n \u003cspan className=\"text-gray-500 font-medium\">故事梗概\u003c/span>\n \u003ctextarea value={editData.synopsis || ''} onChange={e => updateField('synopsis', e.target.value)} rows={3}\n className=\"border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none resize-none\" />\n \u003c/label>\n \u003c/section>\n\n {/* 角色编辑 */}\n {editData.characters && editData.characters.length > 0 && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\u003cUsers className=\"w-4 h-4 text-blue-500\" />\u003ch4 className=\"text-sm font-semibold text-gray-700\">角色\u003c/h4>\u003c/div>\n \u003cdiv className=\"space-y-3\">\n {editData.characters.map((c, i) => (\n \u003cdiv key={i} className=\"bg-white border border-gray-200 rounded-xl p-4 space-y-2\">\n \u003cdiv className=\"flex gap-3\">\n \u003clabel className=\"flex flex-col gap-1 text-xs flex-1\">\u003cspan className=\"text-gray-500 font-medium\">名字\u003c/span>\n \u003cinput type=\"text\" value={c.name} onChange={e => updateCharacter(i, { name: e.target.value })}\n className=\"border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none\" />\u003c/label>\n \u003clabel className=\"flex flex-col gap-1 text-xs w-24\">\u003cspan className=\"text-gray-500 font-medium\">角色\u003c/span>\n \u003cselect value={c.role} onChange={e => updateCharacter(i, { role: e.target.value })}\n className=\"border border-gray-200 rounded-lg px-2 py-2 text-sm text-gray-700 outline-none\">\n \u003coption value=\"主角\">主角\u003c/option>\u003coption value=\"配角\">配角\u003c/option>\u003coption value=\"背景\">背景\u003c/option>\n \u003c/select>\u003c/label>\n \u003c/div>\n \u003clabel className=\"flex flex-col gap-1 text-xs\">\u003cspan className=\"text-gray-500 font-medium\">外貌描述\u003c/span>\n \u003ctextarea value={c.description} onChange={e => updateCharacter(i, { description: e.target.value })} rows={2}\n className=\"border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none resize-none\" />\u003c/label>\n \u003clabel className=\"flex flex-col gap-1 text-xs\">\u003cspan className=\"text-gray-500 font-medium\">动机\u003c/span>\n \u003cinput type=\"text\" value={c.motivation || ''} onChange={e => updateCharacter(i, { motivation: e.target.value })}\n className=\"border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none\" />\u003c/label>\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/section>\n )}\n\n {/* 场景编辑 */}\n {editData.settings && editData.settings.length > 0 && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\u003cMapPin className=\"w-4 h-4 text-green-500\" />\u003ch4 className=\"text-sm font-semibold text-gray-700\">场景\u003c/h4>\u003c/div>\n \u003cdiv className=\"space-y-3\">\n {editData.settings.map((s, i) => (\n \u003cdiv key={i} className=\"bg-white border border-gray-200 rounded-xl p-4\">\n \u003clabel className=\"block text-xs text-gray-500 font-medium mb-1.5\">{s.name}\u003c/label>\n \u003ctextarea value={s.description} onChange={e => updateSetting(i, { description: e.target.value })} rows={2}\n className=\"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none resize-none\" />\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/section>\n )}\n\n {/* 故事线编辑 */}\n {editData.scenes && editData.scenes.length > 0 && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\u003cFilm className=\"w-4 h-4 text-purple-500\" />\u003ch4 className=\"text-sm font-semibold text-gray-700\">故事线\u003c/h4>\u003c/div>\n \u003cdiv className=\"space-y-3\">\n {editData.scenes.map((sc, i) => (\n \u003cdiv key={i} className=\"bg-white border border-gray-200 rounded-xl p-4\">\n \u003cdiv className=\"flex items-center gap-3 mb-2\">\n \u003cspan className=\"flex items-center justify-center w-7 h-7 rounded-full bg-purple-100 text-purple-700 text-xs font-bold flex-shrink-0\">{sc.scene_number}\u003c/span>\n \u003cspan className=\"text-xs text-green-600\">{sc.location}\u003c/span>\n \u003cdiv className=\"flex flex-wrap gap-1\">\n {sc.characters.map((c, ci) => \u003cspan key={ci} className=\"px-2 py-0.5 bg-blue-50 text-blue-600 text-[10px] rounded-full\">{c}\u003c/span>)}\n \u003c/div>\n \u003c/div>\n \u003ctextarea value={sc.plot} onChange={e => updateScene(i, { plot: e.target.value })} rows={3}\n className=\"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none resize-none\" />\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/section>\n )}\n\n \u003cdiv className=\"flex gap-2 pt-2\">\n \u003cbutton onClick={handleSave} className=\"flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm font-medium hover:bg-blue-600 transition-colors\">\n \u003cSave className=\"w-4 h-4\" />保存修改\u003c/button>\n \u003cbutton onClick={cancelEdit} className=\"flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-600 rounded-lg text-sm hover:bg-gray-200 transition-colors\">\n \u003cX className=\"w-4 h-4\" />取消\u003c/button>\n \u003c/div>\n \u003c/div>\n )}\n\n {/* ===== JSON编辑模式 ===== */}\n {isEditing && editMode === 'raw' && (\n \u003cdiv className=\"space-y-3\">\n \u003ctextarea value={rawText} onChange={e => setRawText(e.target.value)}\n className=\"w-full bg-white border border-gray-200 rounded-xl p-4 text-sm text-gray-700 font-mono leading-relaxed resize-none outline-none min-h-[400px] focus:ring-2 focus:ring-blue-500/30\" />\n \u003cdiv className=\"flex gap-2\">\n \u003cbutton onClick={handleSave} className=\"flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm font-medium hover:bg-blue-600 transition-colors\">\n \u003cSave className=\"w-4 h-4\" />保存修改\u003c/button>\n \u003cbutton onClick={cancelEdit} className=\"flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-600 rounded-lg text-sm hover:bg-gray-200 transition-colors\">\n \u003cX className=\"w-4 h-4\" />取消\u003c/button>\n \u003c/div>\n \u003c/div>\n )}\n\n {/* 等待状态 */}\n {state.status === 'pending' && (\n \u003cdiv className=\"text-center text-gray-400 text-sm py-20\">等待生成...\u003c/div>\n )}\n \u003c/div>\n\n {!isEditing && !isLoglinePhase && (\n \u003cStageActions\n status={state.status}\n onConfirm={onConfirm}\n showConfirm={showConfirm}\n onEdit={startEdit}\n onRegenerate={onRegenerate}\n stageId=\"script_generation\"\n hasPendingItems={hasPendingItems}\n hasNextStageStarted={hasNextStageStarted}\n isRunning={isRunning}\n />\n )}\n {isEditing && (\n \u003cStageActions\n status={state.status}\n onConfirm={onConfirm}\n showConfirm={showConfirm}\n onSave={handleSave}\n onRegenerate={onRegenerate}\n stageId=\"script_generation\"\n hasPendingItems={hasPendingItems}\n hasNextStageStarted={hasNextStageStarted}\n isRunning={isRunning}\n />\n )}\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":34398,"content_sha256":"7862619259f7e6b4f7d2fe72fa0ebfae6ff97b2070b09183e978caa533b60ee6"},{"filename":"aigc-claw/frontend/components/stages/StageActions.tsx","content":"'use client';\n\nimport React, { useState, useCallback } from 'react';\nimport { CheckCircle, Edit3, Save, RefreshCw, Check, Loader2, Play } from 'lucide-react';\nimport type { StageStatus } from './types';\n\n// 需要\"继续生成\"的阶段(2、4、5)\nconst STAGES_WITH_CONTINUE = ['character_design', 'reference_generation', 'video_generation'];\n// 需要检查后续阶段的阶段(1、3)\nconst STAGES_WITH_NEXT_CHECK = ['script_generation', 'storyboard'];\n\ninterface StageActionsProps {\n status: StageStatus;\n onConfirm: () => void;\n onEdit?: () => void;\n onSave?: () => void;\n onSaveSelections?: () => Promise\u003cvoid>;\n onRegenerate?: () => void;\n /** 阶段ID */\n stageId?: string;\n /** 是否有待生成的项(用于阶段2、4、5) */\n hasPendingItems?: boolean;\n /** 后续阶段是否已开始(用于阶段1、3) */\n hasNextStageStarted?: boolean;\n /** 是否显示\"确认并继续\"按钮(后续阶段已执行过时隐藏) */\n showConfirm?: boolean;\n isRunning: boolean;\n}\n\nexport default function StageActions({\n status,\n onConfirm,\n onEdit,\n onSave,\n onSaveSelections,\n onRegenerate,\n stageId = '',\n hasPendingItems = true,\n hasNextStageStarted = false,\n showConfirm = true,\n isRunning\n}: StageActionsProps) {\n const [saveState, setSaveState] = useState\u003c'idle' | 'saving' | 'saved'>('idle');\n\n // 是否是\"继续生成\"阶段(2、4、5)\n const isContinueStage = STAGES_WITH_CONTINUE.includes(stageId);\n // 是否是\"重新生成\"阶段(1、3)\n const isRegenStage = STAGES_WITH_NEXT_CHECK.includes(stageId);\n\n // 重新生成/继续生成按钮:在 waiting / completed / error 状态下显示\n const showRegen = onRegenerate && (status === 'waiting' || status === 'completed' || status === 'error');\n\n // 判断按钮是否可用\n let canRegenerate = isRunning;\n if (isContinueStage) {\n // 阶段2、4、5:如果没有待生成的项,禁止点击\n canRegenerate = canRegenerate || !hasPendingItems;\n } else if (isRegenStage) {\n // 阶段1、3:如果后续阶段已开始,禁止点击\n canRegenerate = canRegenerate || hasNextStageStarted;\n }\n\n // 其余按钮在 waiting、running、completed 状态显示\n const showActions = status === 'waiting' || status === 'running' || status === 'completed';\n // 保存选项:在 waiting 和 completed 状态下都显示\n const showSaveSelections = onSaveSelections && (status === 'waiting' || status === 'completed');\n // \"确认并继续\"在 showConfirm=true 且 waiting/running/completed 时显示\n const showConfirmBtn = showConfirm && (status === 'waiting' || status === 'running' || status === 'completed');\n\n const handleSaveClick = useCallback(async () => {\n if (!onSaveSelections || saveState !== 'idle') return;\n setSaveState('saving');\n try {\n await onSaveSelections();\n setSaveState('saved');\n setTimeout(() => setSaveState('idle'), 1500);\n } catch {\n setSaveState('idle');\n }\n }, [onSaveSelections, saveState]);\n\n if (!showRegen && !showActions && !showSaveSelections) return null;\n\n return (\n \u003cdiv className=\"border-t border-gray-200 bg-white px-6 py-4 flex items-center justify-between flex-shrink-0\">\n \u003cdiv>\n {showRegen && (\n \u003cbutton\n onClick={onRegenerate}\n disabled={canRegenerate}\n className=\"flex items-center gap-2 px-4 py-2 bg-white border border-orange-300 text-orange-600 rounded-lg text-sm font-medium hover:bg-orange-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n title={isContinueStage && !hasPendingItems ? '所有项已生成完毕' : (isRegenStage && hasNextStageStarted ? '后续阶段已开始,无法重新生成' : '')}\n >\n {isContinueStage ? \u003cPlay className=\"w-4 h-4\" /> : \u003cRefreshCw className=\"w-4 h-4\" />}\n {isContinueStage ? '继续生成' : '重新生成'}\n \u003c/button>\n )}\n \u003c/div>\n \u003cdiv className=\"flex items-center gap-3\">\n {showActions && onEdit && (\n \u003cbutton\n onClick={onEdit}\n disabled={isRunning}\n className=\"flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors disabled:opacity-50\"\n >\n \u003cEdit3 className=\"w-4 h-4\" />\n 修改\n \u003c/button>\n )}\n {showActions && onSave && (\n \u003cbutton\n onClick={onSave}\n disabled={isRunning}\n className=\"flex items-center gap-2 px-4 py-2 bg-white border border-blue-300 text-blue-600 rounded-lg text-sm font-medium hover:bg-blue-50 transition-colors disabled:opacity-50\"\n >\n \u003cSave className=\"w-4 h-4\" />\n 保存编辑\n \u003c/button>\n )}\n {showSaveSelections && (\n \u003cbutton\n onClick={handleSaveClick}\n disabled={isRunning || saveState !== 'idle'}\n className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 ${\n saveState === 'saved'\n ? 'bg-green-50 border border-green-300 text-green-600'\n : 'bg-white border border-indigo-300 text-indigo-600 hover:bg-indigo-50'\n }`}\n >\n {saveState === 'saving' ? (\n \u003c>\u003cLoader2 className=\"w-4 h-4 animate-spin\" />正在保存\u003c/> ) : saveState === 'saved' ? (\n \u003c>\u003cCheck className=\"w-4 h-4\" />保存成功\u003c/> ) : (\n \u003c>\u003cSave className=\"w-4 h-4\" />保存修改\u003c/> )}\n \u003c/button>\n )}\n {showConfirmBtn && (\n \u003cbutton\n onClick={onConfirm}\n disabled={isRunning}\n className=\"flex items-center gap-2 px-5 py-2 bg-green-500 text-white rounded-lg text-sm font-medium hover:bg-green-600 transition-colors disabled:opacity-50\"\n >\n \u003cCheckCircle className=\"w-4 h-4\" />\n 确认并继续\n \u003c/button>\n )}\n \u003c/div>\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":6088,"content_sha256":"e6cc2dc6f2a6971364347de43a63901d8f08d1e953b1a34f8f4f71c02d09398c"},{"filename":"aigc-claw/frontend/components/stages/StageProgress.tsx","content":"'use client';\n\nimport React from 'react';\nimport { Loader } from 'lucide-react';\n\ninterface StageProgressProps {\n /** progressMessage from StageState */\n message: string;\n /** fallback text when message is empty */\n fallback?: string;\n /** 0-100 */\n progress: number;\n /** accent color class, default blue */\n color?: 'blue' | 'amber' | 'emerald' | 'violet' | 'rose' | 'cyan';\n}\n\nconst BAR_COLORS: Record\u003cstring, { bg: string; bar: string; text: string; ring: string }> = {\n blue: { bg: 'bg-blue-100', bar: 'bg-gradient-to-r from-blue-400 to-blue-600', text: 'text-blue-600', ring: 'ring-blue-200' },\n amber: { bg: 'bg-amber-100', bar: 'bg-gradient-to-r from-amber-400 to-amber-600', text: 'text-amber-600', ring: 'ring-amber-200' },\n emerald: { bg: 'bg-emerald-100', bar: 'bg-gradient-to-r from-emerald-400 to-emerald-600', text: 'text-emerald-600', ring: 'ring-emerald-200' },\n violet: { bg: 'bg-violet-100', bar: 'bg-gradient-to-r from-violet-400 to-violet-600', text: 'text-violet-600', ring: 'ring-violet-200' },\n rose: { bg: 'bg-rose-100', bar: 'bg-gradient-to-r from-rose-400 to-rose-600', text: 'text-rose-600', ring: 'ring-rose-200' },\n cyan: { bg: 'bg-cyan-100', bar: 'bg-gradient-to-r from-cyan-400 to-cyan-600', text: 'text-cyan-600', ring: 'ring-cyan-200' },\n};\n\nexport default function StageProgress({\n message,\n fallback = '处理中...',\n progress,\n color = 'blue',\n}: StageProgressProps) {\n const p = Math.min(100, Math.max(0, Math.round(progress)));\n const c = BAR_COLORS[color] || BAR_COLORS.blue;\n\n // Extract step description: progressMessage format is \"阶段名: 步骤描述\"\n const stepDesc = message?.includes(': ') ? message.split(': ').slice(1).join(': ') : (message || fallback);\n\n return (\n \u003cdiv className=\"mb-6\">\n \u003cdiv className=\"flex items-center justify-between mb-2\">\n \u003cdiv className={`flex items-center gap-2 text-sm font-medium ${c.text}`}>\n \u003cLoader className=\"w-4 h-4 animate-spin\" />\n \u003cspan className=\"truncate\">{stepDesc}\u003c/span>\n \u003c/div>\n \u003cspan className={`text-xs font-mono tabular-nums ${c.text} opacity-70`}>{p}%\u003c/span>\n \u003c/div>\n \u003cdiv className={`w-full ${c.bg} rounded-full h-2.5 overflow-hidden ring-1 ${c.ring}`}>\n \u003cdiv\n className={`${c.bar} h-2.5 rounded-full transition-all duration-700 ease-out`}\n style={{ width: `${p}%` }}\n />\n \u003c/div>\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":2466,"content_sha256":"f7760d7b9d57531a196b204f0779ea08fd3347ac05d884a680f81d57d5cb1795"},{"filename":"aigc-claw/frontend/components/stages/StoryboardStage.tsx","content":"'use client';\n\nimport React, { useState, useMemo, useCallback, useEffect } from 'react';\nimport { Save, X, Code, LayoutList, Camera, Clock, MapPin, Users, Plus, Trash2, Sparkles, Check, AlertTriangle } from 'lucide-react';\nimport type { StageViewProps } from './types';\nimport StageActions from './StageActions';\nimport StageProgress from './StageProgress';\nimport { checkSceneAssets } from '@/lib/workflowApi';\n\n// ─── 类型 ───\n\ninterface Shot {\n shot_id: string;\n scene_number: number;\n act: number;\n shot_number: number;\n duration: number; // 5 | 10 | 15\n location: string;\n characters: string[];\n plot: string;\n visual_prompt: string;\n is_new?: boolean; // 新添加的分镜标记\n}\n\nconst ACT_NAMES: Record\u003cnumber, string> = {\n 1: '激励事件', 2: '进入新世界', 3: '灵魂黑夜', 4: '高潮决战',\n};\n\nconst DURATION_COLORS: Record\u003cnumber, string> = {\n 5: 'bg-green-100 text-green-700',\n 10: 'bg-amber-100 text-amber-700',\n 15: 'bg-red-100 text-red-700',\n};\n\n// ─── 分镜视图 ───\n\nexport default function StoryboardStage({ state, sessionId, onConfirm, onIntervene, onRegenerate, onSaveSelections, showConfirm, isRunning, hasPendingItems, hasNextStageStarted }: StageViewProps) {\n // 处理嵌套的 shots 结构:可能是数组,也可能是 {payload: {shots: []}, requires_intervention: true}\n const artifactShots = state.artifact?.shots;\n let shots: Shot[] = [];\n if (Array.isArray(artifactShots)) {\n shots = artifactShots;\n } else if (artifactShots?.payload?.shots) {\n shots = artifactShots.payload.shots;\n }\n const newShotIds = new Set(state.artifact?.new_shot_ids || state.artifact?.payload?.new_shot_ids || []);\n\n // 跟踪新添加但未确认的分镜\n const [pendingNewShots, setPendingNewShots] = useState\u003cstring[]>([]);\n\n const [isEditing, setIsEditing] = useState(false);\n const [editMode, setEditMode] = useState\u003c'structured' | 'raw'>('structured');\n const [editShots, setEditShots] = useState\u003cShot[]>([]);\n const [rawJson, setRawJson] = useState('');\n const [isContinuing, setIsContinuing] = useState(false);\n\n // 删除场景时的确认对话框\n const [deleteConfirm, setDeleteConfirm] = useState\u003c{\n show: boolean;\n sceneNumber: number;\n assetInfo: { reference_images: number; videos: number } | null;\n }>({ show: false, sceneNumber: 0, assetInfo: null });\n\n // 检测新添加的分镜 - 使用shots的JSON字符串作为依赖避免无限循环\n const shotsJson = JSON.stringify(shots || []);\n useEffect(() => {\n try {\n const parsed = JSON.parse(shotsJson);\n if (Array.isArray(parsed)) {\n const newIds = parsed.filter((s: Shot) => s.is_new).map((s: Shot) => s.shot_id);\n setPendingNewShots(newIds);\n }\n } catch { /* ignore parse errors */ }\n }, [shotsJson]);\n\n // 确认新分镜(清除 is_new 标记)\n const handleConfirmNewShots = useCallback(async () => {\n // 保留原始 shots 用于第4阶段(带有 is_new 标记)\n const originalShots = [...shots];\n\n // 使用 shots 中已有的 is_new 字段来确认(因为 new_shot_ids 保存后会被清除)\n const confirmedShots = shots.map(s => {\n if (s.is_new) {\n return { ...s, is_new: false };\n }\n return s;\n });\n // 保存时清除 new_shot_ids 标记,同时传递原始 shots 用于刷新第4阶段\n await onSaveSelections?.({ shots: confirmedShots, new_shot_ids: [], original_shots: originalShots });\n setPendingNewShots([]);\n }, [shots, onSaveSelections]);\n\n // 取消新分镜(删除)\n const handleCancelNewShots = useCallback(async () => {\n // 使用 shots 中已有的 is_new 字段来过滤\n const filteredShots = shots.filter(s => !s.is_new);\n await onSaveSelections?.({ shots: filteredShots });\n setPendingNewShots([]);\n }, [shots, onSaveSelections]);\n\n // 生成新的 shot_id\n const generateShotId = (sceneNum: number, shotNum: number) => {\n return `shot_${sceneNum.toString().padStart(3, '0')}_${shotNum.toString().padStart(2, '0')}`;\n };\n\n // 添加新场景\n const addScene = useCallback(() => {\n if (editShots.length === 0) return;\n const lastShot = editShots[editShots.length - 1];\n const newSceneNum = (lastShot?.scene_number || 0) + 1;\n const newAct = lastShot?.act || 1;\n\n // 添加一个空分镜\n const newShot: Shot = {\n shot_id: generateShotId(newSceneNum, 1),\n scene_number: newSceneNum,\n act: newAct,\n shot_number: 1,\n duration: 10,\n location: '',\n characters: [],\n plot: '',\n visual_prompt: '',\n };\n setEditShots(prev => [...prev, newShot]);\n }, [editShots]);\n\n // 删除场景(先检查是否有生成的资产)\n const deleteScene = useCallback(async (sceneNum: number) => {\n // 检查该场景是否有生成的参考图或视频\n try {\n const assets = await checkSceneAssets(sessionId, sceneNum);\n if (assets.reference_images > 0 || assets.videos > 0) {\n // 有资产,显示确认对话框\n setDeleteConfirm({\n show: true,\n sceneNumber: sceneNum,\n assetInfo: { reference_images: assets.reference_images, videos: assets.videos },\n });\n return;\n }\n } catch (e) {\n // API 调用失败,直接删除\n console.error('Failed to check scene assets:', e);\n }\n // 没有资产,直接删除\n setEditShots(prev => prev.filter(s => s.scene_number !== sceneNum));\n }, [sessionId]);\n\n // 确认删除场景\n const confirmDeleteScene = useCallback(() => {\n setEditShots(prev => prev.filter(s => s.scene_number !== deleteConfirm.sceneNumber));\n setDeleteConfirm({ show: false, sceneNumber: 0, assetInfo: null });\n }, [deleteConfirm.sceneNumber]);\n\n // 取消删除\n const cancelDeleteScene = useCallback(() => {\n setDeleteConfirm({ show: false, sceneNumber: 0, assetInfo: null });\n }, []);\n\n // 添加分镜\n const addShot = useCallback((sceneNum: number) => {\n setEditShots(prev => {\n const sceneShots = prev.filter(s => s.scene_number === sceneNum);\n const lastShot = sceneShots[sceneShots.length - 1];\n const newShotNum = (lastShot?.shot_number || 0) + 1;\n const act = lastShot?.act || 1;\n\n const newShot: Shot = {\n shot_id: generateShotId(sceneNum, newShotNum),\n scene_number: sceneNum,\n act: act,\n shot_number: newShotNum,\n duration: 10,\n location: lastShot?.location || '',\n characters: lastShot?.characters || [],\n plot: '',\n visual_prompt: '',\n };\n\n // 插入到该场景的最后\n const otherShots = prev.filter(s => s.scene_number !== sceneNum);\n return [...otherShots, ...sceneShots, newShot];\n });\n }, []);\n\n // 删除分镜\n const deleteShot = useCallback((shotId: string) => {\n setEditShots(prev => prev.filter(s => s.shot_id !== shotId));\n }, []);\n\n // 智能续写\n const handleContinueStory = useCallback(async () => {\n setIsContinuing(true);\n try {\n await onIntervene({ continue_story: true });\n } finally {\n setIsContinuing(false);\n }\n }, [onIntervene]);\n\n // 按幕 → 场景分组\n const grouped = useMemo(() => {\n const map = new Map\u003cnumber, Map\u003cnumber, Shot[]>>();\n if (!shots || !Array.isArray(shots)) return map;\n for (const shot of shots) {\n const act = shot.act ?? 1;\n const sceneNum = shot.scene_number ?? 1;\n if (!map.has(act)) map.set(act, new Map());\n const actMap = map.get(act)!;\n if (!actMap.has(sceneNum)) actMap.set(sceneNum, []);\n actMap.get(sceneNum)!.push(shot);\n }\n return map;\n }, [shots]);\n\n const hasContent = shots && Array.isArray(shots) && shots.length > 0;\n\n // 统计\n const totalDuration = useMemo(() => {\n if (!shots || !Array.isArray(shots)) return 0;\n return shots.reduce((s, sh) => s + (sh.duration || 0), 0);\n }, [shots]);\n\n const startEdit = useCallback(() => {\n const safeShots = Array.isArray(shots) ? shots : [];\n setEditShots(JSON.parse(JSON.stringify(safeShots)));\n setRawJson(JSON.stringify(safeShots, null, 2));\n setIsEditing(true);\n setEditMode('structured');\n }, [shots]);\n\n const switchEditMode = useCallback((mode: 'structured' | 'raw') => {\n if (mode === 'raw') {\n setRawJson(JSON.stringify(editShots, null, 2));\n } else {\n try { setEditShots(JSON.parse(rawJson)); } catch { /* keep current */ }\n }\n setEditMode(mode);\n }, [editShots, rawJson]);\n\n const handleSave = useCallback(() => {\n let finalShots: Shot[];\n if (editMode === 'raw') {\n try { finalShots = JSON.parse(rawJson); } catch { finalShots = editShots; }\n } else {\n finalShots = editShots;\n }\n onIntervene({ modified_storyboard: finalShots });\n setIsEditing(false);\n }, [editMode, rawJson, editShots, onIntervene]);\n\n const cancelEdit = useCallback(() => setIsEditing(false), []);\n\n const updateShotField = (idx: number, field: keyof Shot, value: any) => {\n setEditShots(prev => prev.map((s, i) => i === idx ? { ...s, [field]: value } : s));\n };\n\n // 按幕→场景分组(编辑数据)\n const editGrouped = useMemo(() => {\n const map = new Map\u003cnumber, Map\u003cnumber, { shot: Shot; globalIdx: number }[]>>();\n editShots.forEach((shot, globalIdx) => {\n const act = shot.act ?? 1;\n const sceneNum = shot.scene_number ?? 1;\n if (!map.has(act)) map.set(act, new Map());\n const actMap = map.get(act)!;\n if (!actMap.has(sceneNum)) actMap.set(sceneNum, []);\n actMap.get(sceneNum)!.push({ shot, globalIdx });\n });\n return map;\n }, [editShots]);\n\n return (\n \u003cdiv className=\"flex flex-col h-full\">\n \u003cdiv className=\"flex-1 overflow-y-auto p-6\">\n {/* 标题 + 编辑模式切换 */}\n \u003cdiv className=\"flex items-center justify-between mb-1\">\n \u003ch2 className=\"text-lg font-semibold text-gray-800\">分镜设计\u003c/h2>\n {isEditing && (\n \u003cdiv className=\"flex gap-1 bg-gray-100 rounded-lg p-1 text-xs\">\n \u003cbutton\n onClick={() => switchEditMode('structured')}\n className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-colors ${editMode === 'structured' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}\n >\n \u003cLayoutList className=\"w-3.5 h-3.5\" />\n 结构编辑\n \u003c/button>\n \u003cbutton\n onClick={() => switchEditMode('raw')}\n className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-colors ${editMode === 'raw' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}\n >\n \u003cCode className=\"w-3.5 h-3.5\" />\n JSON编辑\n \u003c/button>\n \u003c/div>\n )}\n \u003c/div>\n \u003cp className=\"text-sm text-gray-500 mb-6\">\n 按场景拆分分镜,每个分镜带时长标签\n {hasContent && (\n \u003cspan className=\"ml-2 text-gray-400\">\n 共 {shots.length} 个分镜 · 总时长 {totalDuration}s\n \u003c/span>\n )}\n \u003c/p>\n\n {/* 运行中 */}\n {state.status === 'running' && (\n \u003c>\n \u003cStageProgress message={state.progressMessage} fallback=\"正在拆分分镜...\" progress={state.progress} color=\"amber\" />\n {/* 增量显示已完成的分镜 */}\n {hasContent && (\n \u003cdiv className=\"mt-4 space-y-4\">\n {renderShotGroups(grouped)}\n \u003c/div>\n )}\n \u003c/>\n )}\n\n {state.error && (\n \u003cdiv className=\"text-sm text-red-600 bg-red-50 border border-red-200 p-4 rounded-xl mb-4\">{state.error}\u003c/div>\n )}\n\n {/* ===== 查看模式 ===== */}\n {hasContent && !isEditing && state.status !== 'running' && (\n \u003cdiv className=\"space-y-4\">\n {renderShotGroups(grouped)}\n \u003c/div>\n )}\n\n {/* ===== 结构编辑模式 ===== */}\n {isEditing && editMode === 'structured' && (\n \u003cdiv className=\"space-y-4\">\n {Array.from(editGrouped.entries()).sort(([a], [b]) => a - b).map(([act, sceneMap]) => (\n \u003cReact.Fragment key={act}>\n {/* 幕分隔线:仅多幕时显示 */}\n {editGrouped.size > 1 && (\n \u003cdiv className=\"flex items-center gap-3 pt-2\">\n \u003cdiv className=\"flex-1 h-px bg-gradient-to-r from-amber-200 to-transparent\" />\n \u003cspan className=\"px-3 py-1 bg-amber-50 text-amber-600 text-xs font-semibold rounded-full whitespace-nowrap\">\n 第{act}幕 — {ACT_NAMES[act] || ''}\n \u003c/span>\n \u003cdiv className=\"flex-1 h-px bg-gradient-to-l from-amber-200 to-transparent\" />\n \u003c/div>\n )}\n\n {Array.from(sceneMap.entries()).sort(([a], [b]) => a - b).map(([sceneNum, items]) => (\n \u003cdiv key={sceneNum} className=\"bg-white border border-gray-200 rounded-xl overflow-hidden\">\n \u003cdiv className=\"bg-gray-50 px-4 py-2 border-b border-gray-100 flex items-center gap-2\">\n \u003cCamera className=\"w-3.5 h-3.5 text-amber-500\" />\n \u003cspan className=\"text-sm font-medium text-gray-700\">场景 {sceneNum}\u003c/span>\n \u003cspan className=\"text-xs text-gray-400\">{items.length} 个分镜\u003c/span>\n \u003cdiv className=\"ml-auto flex items-center gap-1\">\n \u003cbutton\n onClick={() => addShot(sceneNum)}\n className=\"p-1 text-blue-500 hover:bg-blue-50 rounded\"\n title=\"添加分镜\"\n >\n \u003cPlus className=\"w-4 h-4\" />\n \u003c/button>\n \u003cbutton\n onClick={() => deleteScene(sceneNum)}\n className=\"p-1 text-red-500 hover:bg-red-50 rounded\"\n title=\"删除场景\"\n >\n \u003cTrash2 className=\"w-4 h-4\" />\n \u003c/button>\n \u003c/div>\n \u003c/div>\n \u003cdiv className=\"divide-y divide-gray-100\">\n {items.map(({ shot, globalIdx }) => (\n \u003cdiv key={shot.shot_id} className=\"p-4 space-y-2\">\n \u003cdiv className=\"flex items-center gap-2\">\n \u003cspan className=\"flex items-center justify-center w-6 h-6 rounded-full bg-amber-100 text-amber-700 text-xs font-bold flex-shrink-0\">\n {shot.shot_number}\n \u003c/span>\n \u003cselect\n value={shot.duration}\n onChange={e => updateShotField(globalIdx, 'duration', Number(e.target.value))}\n className=\"text-xs border border-gray-200 rounded px-2 py-1 text-gray-700\"\n >\n \u003coption value={5}>5s\u003c/option>\n \u003coption value={10}>10s\u003c/option>\n \u003coption value={15}>15s\u003c/option>\n \u003c/select>\n \u003cdiv className=\"flex flex-wrap gap-1 ml-2\">\n {shot.characters.map((c, ci) => (\n \u003cspan key={ci} className=\"px-2 py-0.5 bg-blue-50 text-blue-600 text-xs rounded-full\">{c}\u003c/span>\n ))}\n \u003cspan className=\"px-2 py-0.5 bg-green-50 text-green-600 text-xs rounded-full\">{shot.location}\u003c/span>\n \u003c/div>\n \u003c/div>\n \u003cdiv>\n \u003clabel className=\"block text-xs text-gray-500 mb-1\">剧情\u003c/label>\n \u003ctextarea\n value={shot.plot}\n onChange={e => updateShotField(globalIdx, 'plot', e.target.value)}\n rows={2}\n className=\"w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none resize-none\"\n />\n \u003c/div>\n \u003cdiv>\n \u003clabel className=\"block text-xs text-gray-500 mb-1\">视觉提示词\u003c/label>\n \u003cdiv className=\"flex gap-2\">\n \u003ctextarea\n value={shot.visual_prompt}\n onChange={e => updateShotField(globalIdx, 'visual_prompt', e.target.value)}\n rows={2}\n className=\"flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:ring-2 focus:ring-blue-500/30 outline-none resize-none\"\n />\n \u003cbutton\n onClick={() => deleteShot(shot.shot_id)}\n className=\"p-2 text-red-400 hover:bg-red-50 rounded self-start\"\n title=\"删除分镜\"\n >\n \u003cTrash2 className=\"w-4 h-4\" />\n \u003c/button>\n \u003c/div>\n \u003c/div>\n \u003c/div>\n ))}\n \u003c/div>\n {/* 场景底部添加分镜按钮 */}\n \u003cdiv className=\"px-4 py-2 bg-gray-50 border-t border-gray-100\">\n \u003cbutton\n onClick={() => addShot(sceneNum)}\n className=\"flex items-center gap-1 text-xs text-blue-500 hover:text-blue-600\"\n >\n \u003cPlus className=\"w-3.5 h-3.5\" />\n 添加分镜\n \u003c/button>\n \u003c/div>\n \u003c/div>\n ))}\n \u003c/React.Fragment>\n ))}\n\n {/* 添加场景按钮 */}\n \u003cbutton\n onClick={addScene}\n className=\"w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-gray-300 rounded-xl text-gray-500 hover:border-blue-400 hover:text-blue-500 transition-colors\"\n >\n \u003cPlus className=\"w-5 h-5\" />\n 添加场景\n \u003c/button>\n\n \u003cdiv className=\"flex gap-2 pt-2\">\n \u003cbutton onClick={handleSave} className=\"flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm font-medium hover:bg-blue-600 transition-colors\">\n \u003cSave className=\"w-4 h-4\" />\n 保存修改\n \u003c/button>\n \u003cbutton onClick={cancelEdit} className=\"flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-600 rounded-lg text-sm hover:bg-gray-200 transition-colors\">\n \u003cX className=\"w-4 h-4\" />\n 取消\n \u003c/button>\n \u003c/div>\n \u003c/div>\n )}\n\n {/* ===== JSON编辑模式 ===== */}\n {isEditing && editMode === 'raw' && (\n \u003cdiv className=\"space-y-3\">\n \u003ctextarea\n value={rawJson}\n onChange={e => setRawJson(e.target.value)}\n className=\"w-full bg-white border border-gray-200 rounded-xl p-4 text-sm text-gray-700 font-mono leading-relaxed resize-none outline-none min-h-[400px] focus:ring-2 focus:ring-blue-500/30\"\n />\n \u003cdiv className=\"flex gap-2\">\n \u003cbutton onClick={handleSave} className=\"flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm font-medium hover:bg-blue-600 transition-colors\">\n \u003cSave className=\"w-4 h-4\" />\n 保存修改\n \u003c/button>\n \u003cbutton onClick={cancelEdit} className=\"flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-600 rounded-lg text-sm hover:bg-gray-200 transition-colors\">\n \u003cX className=\"w-4 h-4\" />\n 取消\n \u003c/button>\n \u003c/div>\n \u003c/div>\n )}\n\n {state.status === 'pending' && (\n \u003cdiv className=\"text-center text-gray-400 text-sm py-20\">等待上一阶段完成...\u003c/div>\n )}\n\n {/* 智能续写按钮 - 仅在分镜完成后显示(pending/running 时不显示) */}\n {hasContent && !isEditing && !['pending', 'running'].includes(state.status) && (\n \u003cdiv className=\"mt-6 pt-6 border-t border-gray-200\">\n \u003cbutton\n onClick={handleContinueStory}\n disabled={isContinuing}\n className=\"w-full flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-xl text-sm font-medium hover:from-purple-600 hover:to-indigo-600 transition-colors disabled:opacity-50\"\n >\n {isContinuing ? (\n \u003c>\n \u003cdiv className=\"w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin\" />\n 智能续写中...\n \u003c/>\n ) : (\n \u003c>\n \u003cSparkles className=\"w-4 h-4\" />\n 智能续写\n \u003c/>\n )}\n \u003c/button>\n \u003cp className=\"text-xs text-gray-400 text-center mt-2\">根据已有剧情,自动续写新的分镜\u003c/p>\n \u003c/div>\n )}\n\n {/* 新分镜确认/取消按钮 */}\n {pendingNewShots.length > 0 && !isEditing && (\n \u003cdiv className=\"mt-4 p-4 bg-purple-50 border border-purple-200 rounded-xl\">\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cSparkles className=\"w-4 h-4 text-purple-500\" />\n \u003cspan className=\"text-sm font-medium text-purple-700\">检测到 {pendingNewShots.length} 个新分镜\u003c/span>\n \u003c/div>\n \u003cdiv className=\"flex gap-3\">\n \u003cbutton\n onClick={handleConfirmNewShots}\n disabled={isContinuing}\n className=\"flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-purple-500 text-white rounded-lg text-sm font-medium hover:bg-purple-600 transition-colors disabled:opacity-50\"\n >\n \u003cCheck className=\"w-4 h-4\" />\n 保存\n \u003c/button>\n \u003cbutton\n onClick={handleCancelNewShots}\n disabled={isContinuing}\n className=\"flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-white border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors disabled:opacity-50\"\n >\n \u003cX className=\"w-4 h-4\" />\n 取消\n \u003c/button>\n \u003c/div>\n \u003c/div>\n )}\n \u003c/div>\n\n {/* 删除场景确认对话框 */}\n {deleteConfirm.show && (\n \u003cdiv className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n \u003cdiv className=\"bg-white rounded-xl p-6 max-w-sm w-full mx-4 shadow-xl\">\n \u003cdiv className=\"flex items-center gap-3 mb-4\">\n \u003cdiv className=\"w-10 h-10 rounded-full bg-red-100 flex items-center justify-center\">\n \u003cAlertTriangle className=\"w-5 h-5 text-red-500\" />\n \u003c/div>\n \u003ch3 className=\"text-lg font-semibold text-gray-800\">确认删除场景\u003c/h3>\n \u003c/div>\n \u003cp className=\"text-gray-600 mb-4\">\n 场景 {deleteConfirm.sceneNumber} 中包含已生成的资产:\n \u003c/p>\n \u003cdiv className=\"bg-gray-50 rounded-lg p-3 mb-4 space-y-1 text-sm\">\n \u003cdiv className=\"flex justify-between\">\n \u003cspan className=\"text-gray-500\">参考图:\u003c/span>\n \u003cspan className=\"font-medium text-gray-800\">{deleteConfirm.assetInfo?.reference_images || 0} 张\u003c/span>\n \u003c/div>\n \u003cdiv className=\"flex justify-between\">\n \u003cspan className=\"text-gray-500\">视频片段:\u003c/span>\n \u003cspan className=\"font-medium text-gray-800\">{deleteConfirm.assetInfo?.videos || 0} 个\u003c/span>\n \u003c/div>\n \u003c/div>\n \u003cp className=\"text-amber-600 text-sm mb-4\">\n 删除后这些资产将无法恢复!\n \u003c/p>\n \u003cdiv className=\"flex gap-3\">\n \u003cbutton\n onClick={cancelDeleteScene}\n className=\"flex-1 px-4 py-2 border border-gray-200 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors\"\n >\n 取消\n \u003c/button>\n \u003cbutton\n onClick={confirmDeleteScene}\n className=\"flex-1 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors\"\n >\n 确认删除\n \u003c/button>\n \u003c/div>\n \u003c/div>\n \u003c/div>\n )}\n\n {!isEditing && (\n \u003cStageActions\n status={state.status}\n onConfirm={onConfirm}\n showConfirm={showConfirm}\n onEdit={startEdit}\n onRegenerate={onRegenerate}\n stageId=\"storyboard\"\n hasPendingItems={hasPendingItems}\n hasNextStageStarted={hasNextStageStarted}\n isRunning={isRunning}\n />\n )}\n {isEditing && (\n \u003cStageActions\n status={state.status}\n onConfirm={onConfirm}\n showConfirm={showConfirm}\n onSave={handleSave}\n onRegenerate={onRegenerate}\n stageId=\"storyboard\"\n hasPendingItems={hasPendingItems}\n hasNextStageStarted={hasNextStageStarted}\n isRunning={isRunning}\n />\n )}\n \u003c/div>\n );\n}\n\n// ─── 分镜分组渲染 ───\n\nfunction renderShotGroups(grouped: Map\u003cnumber, Map\u003cnumber, Shot[]>>) {\n const multiAct = grouped.size > 1;\n return Array.from(grouped.entries()).sort(([a], [b]) => a - b).map(([act, sceneMap]) => (\n \u003cReact.Fragment key={act}>\n {/* 幕分隔线:仅多幕时显示 */}\n {multiAct && (\n \u003cdiv className=\"flex items-center gap-3 pt-2\">\n \u003cdiv className=\"flex-1 h-px bg-gradient-to-r from-amber-200 to-transparent\" />\n \u003cspan className=\"px-3 py-1 bg-amber-50 text-amber-600 text-xs font-semibold rounded-full whitespace-nowrap\">\n 第{act}幕 — {ACT_NAMES[act] || ''}\n \u003c/span>\n \u003cdiv className=\"flex-1 h-px bg-gradient-to-l from-amber-200 to-transparent\" />\n \u003c/div>\n )}\n\n {Array.from(sceneMap.entries()).sort(([a], [b]) => a - b).map(([sceneNum, sceneShots]) => (\n \u003cReact.Fragment key={sceneNum}>\n \u003cdiv className=\"bg-white border border-gray-200 rounded-xl overflow-hidden\">\n {/* 场景标题栏 */}\n \u003cdiv className=\"bg-gray-50 px-4 py-2 border-b border-gray-100 flex items-center gap-2\">\n \u003cCamera className=\"w-3.5 h-3.5 text-amber-500\" />\n \u003cspan className=\"text-sm font-medium text-gray-700\">场景 {sceneNum}\u003c/span>\n \u003cspan className=\"text-xs text-gray-400\">\n {sceneShots.length} 个分镜 · {sceneShots.reduce((s, sh) => s + sh.duration, 0)}s\n \u003c/span>\n {sceneShots[0]?.location && (\n \u003cspan className=\"ml-auto px-2 py-0.5 bg-green-50 text-green-600 text-xs rounded-full flex items-center gap-1\">\n \u003cMapPin className=\"w-3 h-3\" />\n {sceneShots[0].location}\n \u003c/span>\n )}\n \u003c/div>\n {/* 分镜列表 */}\n \u003cdiv className=\"divide-y divide-gray-100\">\n {sceneShots.map(shot => (\n \u003cdiv key={shot.shot_id} className={`px-4 py-3 hover:bg-gray-50/50 transition-colors ${shot.is_new ? 'bg-purple-50 border-l-4 border-l-purple-400' : ''}`}>\n \u003cdiv className=\"flex items-start gap-3\">\n {/* 编号 */}\n \u003cspan className=\"flex items-center justify-center w-6 h-6 rounded-full bg-amber-100 text-amber-700 text-xs font-bold flex-shrink-0 mt-0.5\">\n {shot.shot_number}\n \u003c/span>\n \u003cdiv className=\"flex-1 min-w-0\">\n {/* 标签行 */}\n \u003cdiv className=\"flex items-center gap-2 mb-1.5\">\n \u003cspan className={`inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full ${DURATION_COLORS[shot.duration] || 'bg-gray-100 text-gray-600'}`}>\n \u003cClock className=\"w-3 h-3\" />\n {shot.duration}s\n \u003c/span>\n {(shot.characters || []).map((c, ci) => (\n \u003cspan key={ci} className=\"px-2 py-0.5 bg-blue-50 text-blue-600 text-xs rounded-full\">{c}\u003c/span>\n ))}\n \u003c/div>\n {/* 剧情 */}\n \u003cp className=\"text-sm text-gray-700 leading-relaxed\">{shot.plot}\u003c/p>\n {/* 视觉提示词 */}\n {shot.visual_prompt && (\n \u003cp className=\"text-xs text-gray-400 leading-relaxed mt-1 italic\">{shot.visual_prompt}\u003c/p>\n )}\n \u003c/div>\n \u003c/div>\n \u003c/div>\n ))}\n \u003c/div>\n \u003c/div>\n \u003c/React.Fragment>\n ))}\n \u003c/React.Fragment>\n ));\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":29554,"content_sha256":"2b9cfd596de38ad62ba76f15bd5cafa927d1c2dc0cb354443ca9c357cf95119f"},{"filename":"aigc-claw/frontend/components/stages/types.ts","content":"export type StageStatus = 'pending' | 'running' | 'waiting' | 'completed' | 'error';\n\nexport interface StageState {\n status: StageStatus;\n progress: number;\n progressMessage: string;\n artifact: any;\n error: string | null;\n}\n\nexport interface StageViewProps {\n state: StageState;\n sessionId: string;\n onConfirm: () => void;\n onIntervene: (modifications: Record\u003cstring, any>) => void;\n onRegenerate: () => void;\n onUpdateArtifact?: (patch: Record\u003cstring, any>) => void;\n onSaveSelections?: (selections: Record\u003cstring, any>) => Promise\u003cvoid>;\n /** 是否显示\"确认并继续\"按钮(后续阶段已执行过时为 false) */\n showConfirm?: boolean;\n isRunning: boolean;\n /** 是否有待生成的项(阶段2、4、5使用) */\n hasPendingItems?: boolean;\n /** 后续阶段是否已开始(阶段1、3使用) */\n hasNextStageStarted?: boolean;\n /** 视频生成参数(仅 VideoStage 使用) */\n videoSound?: string;\n videoShotType?: string;\n onVideoParamsChange?: (params: { videoSound?: string; videoShotType?: string }) => void;\n /** 参考图阶段的 artifact(仅 VideoStage 使用,用于检查依赖) */\n referenceArtifact?: any;\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":1178,"content_sha256":"56f0bbfedf35fd7a06902a10f56687cd5104d2b87d88e0f3dd5ae370908721b6"},{"filename":"aigc-claw/frontend/components/stages/utils.ts","content":"/**\n * 阶段数据工具函数\n * - 路径转 URL\n * - 剧本/分镜结构化文本解析与重建\n */\n\n/** 将后端本地文件路径转换为浏览器可访问的 URL */\nexport function assetUrl(path: string): string {\n if (!path) return '';\n if (path.startsWith('http') || path.startsWith('/') || path.startsWith('blob:') || path.startsWith('data:')) return path;\n return '/' + path;\n}\n\n/* ─── 剧本 / 分镜 结构化文本解析 ─── */\n\nexport interface ParsedCharacter {\n name: string;\n description: string;\n}\n\nexport interface ParsedSetting {\n name: string;\n description: string;\n}\n\nexport interface ParsedScene {\n id: string;\n characters: string[];\n settings: string[];\n description: string;\n raw: string;\n}\n\nexport interface ParsedScript {\n characters: ParsedCharacter[];\n settings: ParsedSetting[];\n scenes: ParsedScene[];\n isZh: boolean;\n}\n\n/** 解析结构化剧本/分镜文本 → 角色 + 场景 + 故事线 */\nexport function parseScriptText(text: string): ParsedScript {\n if (!text) return { characters: [], settings: [], scenes: [], isZh: false };\n\n const normalized = text.replace(/\\\\n/g, '\\n');\n const isZh = /角色[::]|场景设置[::]|视频片段/.test(normalized);\n\n const charHeader = isZh ? /角色[::]/ : /Characters[::]/;\n const settingHeader = isZh ? /场景设置[::]/ : /Settings[::]/;\n const sceneHeader = isZh ? /视频片段[::]/ : /Scenes[::]/;\n\n function findMatch(src: string, pat: RegExp) {\n const m = src.match(pat);\n return m ? { index: src.indexOf(m[0]), length: m[0].length } : null;\n }\n\n function extractBlock(src: string, startPat: RegExp, endPat: RegExp | null): string {\n const sm = findMatch(src, startPat);\n if (!sm) return '';\n const sp = sm.index + sm.length;\n if (endPat) {\n const em = findMatch(src.slice(sp), endPat);\n return em ? src.slice(sp, sp + em.index) : src.slice(sp);\n }\n return src.slice(sp);\n }\n\n const charBlock = extractBlock(normalized, charHeader, settingHeader).trim();\n const settingBlock = extractBlock(normalized, settingHeader, sceneHeader).trim();\n const sceneBlock = extractBlock(normalized, sceneHeader, null).trim();\n\n const characters: ParsedCharacter[] = [];\n for (const line of charBlock.split('\\n')) {\n const t = line.trim();\n if (!t) continue;\n const ci = t.search(/[::]/);\n if (ci > 0) {\n characters.push({\n name: t.slice(0, ci).trim(),\n description: t.slice(ci + 1).trim().replace(/\\.\\s*$/, ''),\n });\n }\n }\n\n const settings: ParsedSetting[] = [];\n for (const line of settingBlock.split('\\n')) {\n const t = line.trim();\n if (!t) continue;\n const ci = t.search(/[::]/);\n if (ci > 0) {\n settings.push({\n name: t.slice(0, ci).trim(),\n description: t.slice(ci + 1).trim().replace(/\\.\\s*$/, ''),\n });\n }\n }\n\n const scenes: ParsedScene[] = [];\n const scenePat = isZh\n ? /^视频片段\\s*(\\d+)\\s*[::]\\s*(.*)/\n : /^Scene\\s+(\\d+)\\s*[::]\\s*(.*)/i;\n\n for (const line of sceneBlock.split('\\n')) {\n const m = line.trim().match(scenePat);\n if (m) {\n const raw = m[2];\n const cm = raw.match(/\\[(?:Characters|角色)\\s*[::]\\s*([^\\]]+)\\]/i);\n const sm2 = raw.match(/\\[(?:Settings|场景(?:设置)?)\\s*[::]\\s*([^\\]]+)\\]/i);\n const chars = cm ? cm[1].split(/[,,]/).map(s => s.trim()).filter(Boolean) : [];\n const sets = sm2 ? sm2[1].split(/[,,]/).map(s => s.trim()).filter(Boolean) : [];\n const desc = raw.replace(/\\[.*?\\]/g, '').trim();\n scenes.push({ id: m[1], characters: chars, settings: sets, description: desc, raw });\n }\n }\n\n return { characters, settings, scenes, isZh };\n}\n\n/** 将解析后的结构重建为标准文本格式 */\nexport function reconstructScriptText(parsed: ParsedScript): string {\n const { isZh, characters, settings, scenes } = parsed;\n const charH = isZh ? '角色:' : 'Characters:';\n const settH = isZh ? '场景设置:' : 'Settings:';\n const sceneH = isZh ? '视频片段:' : 'Scenes:';\n const scenePrefix = isZh ? '视频片段' : 'Scene';\n const charLabel = isZh ? '角色' : 'Characters';\n const settLabel = isZh ? '场景' : 'Settings';\n\n const lines: string[] = [];\n lines.push(charH);\n for (const c of characters) {\n lines.push(`${c.name}: ${c.description}`);\n }\n lines.push(settH);\n for (const s of settings) {\n lines.push(`${s.name}: ${s.description}`);\n }\n lines.push(sceneH);\n for (const sc of scenes) {\n const cp = sc.characters.length ? `[${charLabel}: ${sc.characters.join(', ')}]` : '';\n const sp = sc.settings.length ? `[${settLabel}: ${sc.settings.join(', ')}]` : '';\n const parts = [`${scenePrefix} ${sc.id}:`, cp, sp, sc.description].filter(Boolean);\n lines.push(parts.join(' '));\n }\n return lines.join('\\n');\n}\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":4812,"content_sha256":"084858116454ce6168bafd78faa9e7a7ff7e5257a4b6eeb32270092163d686c8"},{"filename":"aigc-claw/frontend/components/stages/VideoStage.tsx","content":"'use client';\n\nimport React, { useState, useRef, useCallback, useEffect } from 'react';\nimport { Film, RefreshCw, ChevronLeft, ChevronRight, Loader, AlertCircle, AlertTriangle, Play, Volume2, VolumeX, Clapperboard, Edit2, Save } from 'lucide-react';\nimport type { StageViewProps } from './types';\nimport { assetUrl } from './utils';\nimport StageActions from './StageActions';\nimport StageProgress from './StageProgress';\n\n/* ─── 类型 ─── */\ninterface ClipItem {\n id: string; // shot_001_01, shot_001_02, ...\n name: string; // 场景1-镜头1\n index?: number; // 全局编号\n description: string; // 提示词\n duration?: number; // 视频时长(秒)\n selected: string; // 当前选中的视频路径\n versions: string[]; // 所有历史版本路径\n status?: 'pending' | 'done' | 'failed';\n}\n\n/* ─── 水平滚动视频画廊 ─── */\nfunction VideoGallery({\n versions,\n selected,\n onSelect,\n showPlaceholder,\n}: {\n versions: string[];\n selected: string;\n onSelect: (path: string) => void;\n showPlaceholder?: boolean;\n}) {\n const scrollRef = useRef\u003cHTMLDivElement>(null);\n\n const scroll = (dir: 'left' | 'right') => {\n if (!scrollRef.current) return;\n scrollRef.current.scrollBy({ left: dir === 'left' ? -300 : 300, behavior: 'smooth' });\n };\n\n if (!versions.length && !showPlaceholder) {\n return (\n \u003cdiv className=\"flex items-center justify-center h-full text-gray-400 text-xs\">\n 暂无视频\n \u003c/div>\n );\n }\n\n return (\n \u003cdiv className=\"relative group\">\n {(versions.length > 1 || (versions.length >= 1 && showPlaceholder)) && (\n \u003c>\n \u003cbutton\n onClick={() => scroll('left')}\n className=\"absolute left-0 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white/90 shadow border border-gray-200 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n \u003cChevronLeft className=\"w-4 h-4 text-gray-600\" />\n \u003c/button>\n \u003cbutton\n onClick={() => scroll('right')}\n className=\"absolute right-0 top-1/2 -translate-y-1/2 z-10 w-7 h-7 rounded-full bg-white/90 shadow border border-gray-200 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n \u003cChevronRight className=\"w-4 h-4 text-gray-600\" />\n \u003c/button>\n \u003c/>\n )}\n \u003cdiv\n ref={scrollRef}\n className=\"flex gap-3 overflow-x-auto scrollbar-hide py-1 px-1\"\n style={{ scrollbarWidth: 'none' }}\n >\n {versions.map((path, i) => {\n const isSelected = path === selected;\n return (\n \u003cdiv\n key={path}\n onClick={() => onSelect(path)}\n className={`flex-shrink-0 cursor-pointer rounded-lg overflow-hidden transition-all ${\n isSelected\n ? 'ring-3 ring-rose-500 shadow-lg shadow-rose-200'\n : 'ring-1 ring-gray-200 hover:ring-gray-300 hover:shadow-md'\n }`}\n >\n \u003cdiv className=\"relative bg-black\">\n \u003cvideo\n src={assetUrl(path)}\n controls={isSelected}\n preload=\"metadata\"\n className=\"h-32 aspect-video object-cover\"\n />\n {!isSelected && (\n \u003cdiv className=\"absolute inset-0 flex items-center justify-center bg-black/10\">\n \u003cPlay className=\"w-6 h-6 text-white/70\" />\n \u003c/div>\n )}\n \u003c/div>\n \u003cdiv className={`text-center text-[10px] py-0.5 ${\n isSelected ? 'bg-rose-500 text-white font-medium' : 'bg-gray-50 text-gray-400'\n }`}>\n v{i + 1}\n \u003c/div>\n \u003c/div>\n );\n })}\n {showPlaceholder && (\n \u003cdiv className=\"flex-shrink-0 flex items-center justify-center h-32 aspect-video bg-gray-50 rounded-lg border border-dashed border-gray-200\">\n \u003cdiv className=\"flex items-center gap-2 text-gray-400 text-xs\">\n \u003cLoader className=\"w-4 h-4 animate-spin\" />\n \u003cspan>生成中...\u003c/span>\n \u003c/div>\n \u003c/div>\n )}\n \u003c/div>\n \u003c/div>\n );\n}\n\n/* ─── 视频行 ─── */\nfunction ClipRow({\n clip,\n editDesc,\n onDescChange,\n onSavePrompt,\n onRegenerate,\n onSelectVersion,\n onToggleEdit,\n isStageRunning,\n isRegenerating,\n isEditing,\n canEdit,\n disabled,\n isSaving,\n}: {\n clip: ClipItem;\n editDesc?: string;\n onDescChange?: (val: string) => void;\n onSavePrompt?: () => void;\n onRegenerate: () => void;\n onSelectVersion: (path: string) => void;\n onToggleEdit?: () => void;\n isStageRunning?: boolean;\n isRegenerating?: boolean;\n isEditing?: boolean;\n canEdit?: boolean;\n disabled?: boolean;\n isSaving?: boolean;\n}) {\n const isPending = clip.status === 'pending' || isRegenerating;\n const isFailed = clip.status === 'failed' && !isRegenerating;\n const hasChanges = editDesc !== clip.description;\n\n return (\n \u003cdiv className={`flex border rounded-xl overflow-hidden bg-white ${disabled ? 'opacity-50' : ''} ${\n isFailed ? 'border-red-200' : 'border-gray-200'\n }`}>\n {/* 左侧: 描述信息 */}\n \u003cdiv className=\"w-[280px] flex-shrink-0 p-4 border-r border-gray-100 flex flex-col\">\n \u003cdiv className=\"flex items-center gap-2 mb-2\">\n \u003cspan className=\"flex items-center justify-center w-6 h-6 rounded-full bg-rose-100 text-rose-700 text-xs font-bold flex-shrink-0\">\n {clip.index ?? clip.id.replace('Scene_', '')}\n \u003c/span>\n \u003cspan className=\"text-sm font-semibold text-gray-800 truncate\">{clip.name}\u003c/span>\n {clip.duration && (\n \u003cspan className=\"text-[10px] bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded\">{clip.duration}s\u003c/span>\n )}\n {isPending && (\n \u003cspan className=\"inline-flex items-center gap-1 text-[10px] bg-amber-50 text-amber-600 px-1.5 py-0.5 rounded\">\n \u003cLoader className=\"w-2.5 h-2.5 animate-spin\" />生成中\n \u003c/span>\n )}\n {isFailed && (\n \u003cspan className=\"text-[10px] bg-red-50 text-red-500 px-1.5 py-0.5 rounded\">失败\u003c/span>\n )}\n {/* 编辑/保存按钮 */}\n {canEdit && !isStageRunning && (\n isEditing ? (\n \u003cbutton\n onClick={onSavePrompt}\n disabled={!hasChanges || isSaving}\n className={`ml-auto flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium transition-colors ${\n hasChanges && !isSaving\n ? 'text-white bg-emerald-500 hover:bg-emerald-600'\n : 'text-gray-400 bg-gray-100 cursor-not-allowed'\n }`}\n >\n \u003cSave className=\"w-3 h-3\" />\n {isSaving ? '保存中' : '保存'}\n \u003c/button>\n ) : (\n \u003cbutton\n onClick={onToggleEdit}\n className=\"ml-auto flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 transition-colors\"\n >\n \u003cEdit2 className=\"w-3 h-3\" />\n 编辑\n \u003c/button>\n )\n )}\n \u003c/div>\n {isEditing ? (\n \u003ctextarea\n value={editDesc ?? clip.description}\n onChange={e => onDescChange?.(e.target.value)}\n rows={4}\n className=\"flex-1 text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded-lg p-2 resize-none focus:outline-none focus:ring-1 focus:ring-rose-300\"\n />\n ) : clip.description ? (\n \u003cp className=\"flex-1 text-xs text-gray-600 leading-relaxed line-clamp-4\">{clip.description}\u003c/p>\n ) : (\n \u003cp className=\"flex-1 text-xs text-gray-400 italic\">无提示词\u003c/p>\n )}\n {/* 重新生成按钮 */}\n {!isStageRunning && (\n \u003cbutton\n onClick={onRegenerate}\n disabled={disabled}\n className={`mt-3 flex items-center gap-1.5 self-start px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${\n disabled\n ? 'text-gray-400 bg-gray-100 cursor-not-allowed'\n : isFailed\n ? 'text-red-600 bg-red-50 hover:bg-red-100'\n : 'text-rose-600 bg-rose-50 hover:bg-rose-100'\n }`}\n >\n \u003cRefreshCw className=\"w-3 h-3\" />\n {isFailed ? '点击重试' : '重新生成'}\n \u003c/button>\n )}\n \u003c/div>\n\n {/* 右侧: 视频画廊 / 占位 */}\n \u003cdiv className=\"flex-1 min-w-0 p-3 flex items-center\">\n {isPending && !clip.versions.length ? (\n \u003cdiv className=\"flex items-center justify-center h-32 aspect-video bg-gray-50 rounded-lg border border-dashed border-gray-200\">\n \u003cdiv className=\"flex items-center gap-2 text-gray-400 text-xs\">\n \u003cLoader className=\"w-4 h-4 animate-spin\" />\n \u003cspan>正在生成视频...\u003c/span>\n \u003c/div>\n \u003c/div>\n ) : isFailed && !clip.versions.length ? (\n \u003cdiv\n className=\"flex items-center justify-center h-32 aspect-video bg-red-50/50 rounded-lg border border-dashed border-red-200 cursor-pointer hover:bg-red-100/50 transition-colors\"\n onClick={onRegenerate}\n >\n \u003cdiv className=\"flex flex-col items-center gap-1 text-red-400 text-xs\">\n \u003cAlertCircle className=\"w-4 h-4\" />\n \u003cspan>生成失败,点击重试\u003c/span>\n \u003c/div>\n \u003c/div>\n ) : (\n \u003cdiv className=\"relative w-full\">\n \u003cVideoGallery\n versions={clip.versions}\n selected={clip.selected}\n onSelect={onSelectVersion}\n showPlaceholder={isRegenerating}\n />\n {isFailed && (\n \u003cbutton\n onClick={onRegenerate}\n className=\"absolute top-1 right-1 z-10 flex items-center gap-1 px-2 py-1 rounded-lg text-[10px] font-medium text-white bg-red-500/80 hover:bg-red-600 shadow transition-colors\"\n >\n \u003cRefreshCw className=\"w-2.5 h-2.5\" />\n 重试\n \u003c/button>\n )}\n \u003c/div>\n )}\n \u003c/div>\n \u003c/div>\n );\n}\n\n/* ─── 主组件 ─── */\nexport default function VideoStage({ state, sessionId, onConfirm, onIntervene, onRegenerate, onUpdateArtifact, onSaveSelections, showConfirm, isRunning, videoSound = 'on', videoShotType = 'multi', onVideoParamsChange, referenceArtifact, hasPendingItems, hasNextStageStarted }: StageViewProps) {\n // 检查每个 clip 是否有对应的参考图\n const hasReferenceImage = useCallback((clipId: string): boolean => {\n if (!referenceArtifact?.scenes) return false;\n const refScene = referenceArtifact.scenes.find((s: any) => s.id === clipId);\n return !!(refScene?.selected || refScene?.versions?.length);\n }, [referenceArtifact]);\n\n // 兼容旧格式: video_clips: {Scene_1: \"path\"} → clips: [{id, ...}]\n const clips: ClipItem[] = (() => {\n if (state.artifact?.clips?.length) return state.artifact.clips;\n if (state.artifact?.video_clips) {\n const vc = state.artifact.video_clips as Record\u003cstring, string>;\n return Object.entries(vc)\n .sort(([a], [b]) => {\n const na = parseInt(a.replace(/\\D/g, '')) || 0;\n const nb = parseInt(b.replace(/\\D/g, '')) || 0;\n return na - nb;\n })\n .map(([id, path]) => ({\n id,\n name: `片段 ${id.replace('Scene_', '')}`,\n description: '',\n selected: path,\n versions: [path],\n status: 'done' as const,\n }));\n }\n return [];\n })();\n\n const [selectedVersions, setSelectedVersions] = useState\u003cRecord\u003cstring, string>>({});\n const [editDescs, setEditDescs] = useState\u003cRecord\u003cstring, string>>({});\n const [regeneratingIds, setRegeneratingIds] = useState\u003cSet\u003cstring>>(new Set());\n const [editingIds, setEditingIds] = useState\u003cSet\u003cstring>>(new Set());\n const [savingIds, setSavingIds] = useState\u003cSet\u003cstring>>(new Set());\n\n // 当分镜数据变化时,初始化编辑描述\n useEffect(() => {\n if (clips.length > 0) {\n setEditDescs(prev => {\n const next: Record\u003cstring, string> = {};\n clips.forEach(c => { next[c.id] = prev[c.id] ?? c.description; });\n return next;\n });\n }\n }, [clips]);\n\n // 当 artifact 更新时清除重新生成状态\n useEffect(() => {\n if (regeneratingIds.size > 0) setRegeneratingIds(new Set());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [state.artifact]);\n\n const hasClips = clips.length > 0;\n const canEdit = state.status === 'waiting' || state.status === 'completed';\n\n // 保存单个提示词到后端 JSON\n const handleSavePrompt = async (clipId: string) => {\n const newPrompt = editDescs[clipId];\n if (!newPrompt) return;\n\n setSavingIds(prev => new Set(prev).add(clipId));\n try {\n const response = await fetch(`/api/project/${sessionId}/artifact/video_generation`, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n [clipId]: { description: newPrompt }\n })\n });\n if (response.ok) {\n // 更新前端缓存的 clips.description\n if (onUpdateArtifact && state.artifact?.clips) {\n const updatedClips = state.artifact.clips.map((c: ClipItem) =>\n c.id === clipId ? { ...c, description: newPrompt } : c\n );\n onUpdateArtifact({ clips: updatedClips });\n }\n setEditingIds(prev => {\n const next = new Set(prev);\n next.delete(clipId);\n return next;\n });\n setEditDescs(prev => ({ ...prev, [clipId]: newPrompt }));\n }\n } catch (error) {\n console.error('保存提示词失败:', error);\n } finally {\n setSavingIds(prev => {\n const next = new Set(prev);\n next.delete(clipId);\n return next;\n });\n }\n };\n\n // 切换编辑模式\n const handleToggleEdit = (clipId: string) => {\n setEditingIds(prev => {\n const next = new Set(prev);\n if (next.has(clipId)) {\n next.delete(clipId);\n } else {\n next.add(clipId);\n }\n return next;\n });\n };\n\n const handleRegenerate = (clipId: string) => {\n setRegeneratingIds(prev => new Set(prev).add(clipId));\n onIntervene({ regenerate_clips: [clipId] });\n };\n\n const handleSelectVersion = async (clipId: string, path: string) => {\n setSelectedVersions(prev => ({ ...prev, [clipId]: path }));\n // 同步更新 artifact 以便确认时能传递正确的选中片段给阶段6\n if (onUpdateArtifact && state.artifact?.clips) {\n const updatedClips = state.artifact.clips.map((c: ClipItem) =>\n c.id === clipId ? { ...c, selected: path } : c\n );\n onUpdateArtifact({ clips: updatedClips });\n }\n // 自动保存选择\n const selections: Record\u003cstring, string> = {};\n clips.forEach(c => { selections[c.id] = selectedVersions[c.id] || c.selected; });\n selections[clipId] = path;\n if (onSaveSelections) {\n await onSaveSelections(selections);\n }\n };\n\n const getSelected = (clip: ClipItem) => selectedVersions[clip.id] || clip.selected;\n\n return (\n \u003cdiv className=\"flex flex-col h-full\">\n \u003cdiv className=\"flex-1 overflow-y-auto p-6\">\n {/* 标题栏 */}\n \u003cdiv className=\"flex items-center justify-between mb-1\">\n \u003ch2 className=\"text-lg font-semibold text-gray-800\">视频生成\u003c/h2>\n \u003c/div>\n \u003cp className=\"text-sm text-gray-500 mb-4\">\n 将场景参考图转化为视频片段,支持逐项重新生成\n \u003c/p>\n\n {/* ── 视频生成参数 ── */}\n \u003cdiv className=\"flex items-center gap-6 mb-6 px-4 py-3 bg-white border border-gray-200 rounded-xl\">\n {/* 音效开关 */}\n \u003cdiv className=\"flex items-center gap-2\">\n \u003cspan className=\"text-xs text-gray-500 font-medium\">音效\u003c/span>\n \u003cbutton\n onClick={() => onVideoParamsChange?.({ videoSound: videoSound === 'on' ? 'off' : 'on' })}\n className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${\n videoSound === 'on' ? 'bg-rose-500' : 'bg-gray-300'\n }`}\n >\n \u003cspan\n className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${\n videoSound === 'on' ? 'translate-x-6' : 'translate-x-1'\n }`}\n />\n \u003c/button>\n {videoSound === 'on' ? (\n \u003cVolume2 className=\"w-3.5 h-3.5 text-rose-500\" />\n ) : (\n \u003cVolumeX className=\"w-3.5 h-3.5 text-gray-400\" />\n )}\n \u003c/div>\n\n \u003cdiv className=\"w-px h-5 bg-gray-200\" />\n\n {/* 镜头模式 */}\n \u003cdiv className=\"flex items-center gap-2\">\n \u003cClapperboard className=\"w-3.5 h-3.5 text-gray-400\" />\n \u003cspan className=\"text-xs text-gray-500 font-medium\">镜头\u003c/span>\n \u003cdiv className=\"flex gap-1 bg-gray-100 rounded-lg p-0.5\">\n \u003cbutton\n onClick={() => onVideoParamsChange?.({ videoShotType: 'single' })}\n className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${\n videoShotType === 'single'\n ? 'bg-white text-rose-600 shadow-sm'\n : 'text-gray-500 hover:text-gray-700'\n }`}\n >\n 单镜头\n \u003c/button>\n \u003cbutton\n onClick={() => onVideoParamsChange?.({ videoShotType: 'multi' })}\n className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${\n videoShotType === 'multi'\n ? 'bg-white text-rose-600 shadow-sm'\n : 'text-gray-500 hover:text-gray-700'\n }`}\n >\n 多镜头\n \u003c/button>\n \u003c/div>\n \u003c/div>\n \u003c/div>\n\n {/* 运行中 */}\n {state.status === 'running' && (\n \u003cStageProgress message={state.progressMessage} fallback=\"正在生成视频...\" progress={state.progress} color=\"rose\" />\n )}\n\n {state.error && (\n \u003cdiv className=\"text-sm text-red-600 bg-red-50 border border-red-200 p-4 rounded-xl mb-4\">{state.error}\u003c/div>\n )}\n\n {/* ═══ 视频列表 ═══ */}\n {hasClips && (\n \u003csection>\n \u003cdiv className=\"flex items-center gap-2 mb-3\">\n \u003cFilm className=\"w-4 h-4 text-rose-500\" />\n \u003ch3 className=\"text-sm font-semibold text-gray-700\">视频片段 ({clips.length})\u003c/h3>\n \u003c/div>\n \u003cdiv className=\"space-y-3\">\n {clips.map(clip => {\n // 检查是否有参考图\n const hasRef = hasReferenceImage(clip.id);\n return (\n \u003cdiv key={clip.id} className=\"relative\">\n {!hasRef && (\n \u003cdiv className=\"mb-2 px-3 py-2 bg-amber-50 border border-amber-200 rounded-lg text-xs text-amber-700 flex items-center gap-2\">\n \u003cAlertTriangle className=\"w-3.5 h-3.5\" />\n 未检测到首帧参考图,请先完成参考图生成\n \u003c/div>\n )}\n \u003cClipRow\n clip={{ ...clip, selected: getSelected(clip) }}\n editDesc={editDescs[clip.id]}\n onDescChange={canEdit ? (val => setEditDescs(prev => ({ ...prev, [clip.id]: val }))) : undefined}\n onSavePrompt={() => handleSavePrompt(clip.id)}\n onRegenerate={() => handleRegenerate(clip.id)}\n onSelectVersion={path => handleSelectVersion(clip.id, path)}\n onToggleEdit={() => handleToggleEdit(clip.id)}\n isStageRunning={state.status === 'running'}\n isRegenerating={regeneratingIds.has(clip.id)}\n isEditing={editingIds.has(clip.id)}\n canEdit={canEdit}\n disabled={!hasRef}\n isSaving={savingIds.has(clip.id)}\n />\n \u003c/div>\n );\n })}\n \u003c/div>\n \u003c/section>\n )}\n\n {/* 如果有 artifact 数据(即使 status 是 pending),也显示内容 */}\n {state.status === 'pending' && !hasClips && (\n \u003cdiv className=\"text-center text-gray-400 text-sm py-20\">等待上一阶段完成...\u003c/div>\n )}\n \u003c/div>\n\n {/* 底部操作栏 */}\n \u003cStageActions\n status={state.status}\n onConfirm={onConfirm}\n showConfirm={showConfirm}\n onRegenerate={onRegenerate}\n stageId=\"video_generation\"\n hasPendingItems={hasPendingItems}\n hasNextStageStarted={hasNextStageStarted}\n isRunning={isRunning}\n />\n \u003c/div>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":21347,"content_sha256":"de7c23684feb734eb14d265d60238798eaae855a95402f562322aed96c369bbc"},{"filename":"aigc-claw/frontend/components/TopBar.tsx","content":"'use client';\n\nimport React, { useState, useRef, useEffect } from 'react';\nimport { CheckCircle, Circle, Loader, Edit3, AlertCircle, Square, Zap, Settings2, ChevronDown, Hexagon } from 'lucide-react';\nimport Link from 'next/link';\nimport clsx from 'clsx';\nimport { LLM_PROVIDERS, T2I_PROVIDERS, I2I_PROVIDERS, VIDEO_PROVIDERS, VLM_PROVIDERS, VIDEO_RATIOS, ProviderGroup } from '@/config/models';\n\nexport type StageStatus = 'pending' | 'running' | 'waiting' | 'completed' | 'error' | 'stopped';\n\nexport const STAGES = [\n { id: 'script_generation', name: '剧本生成', shortName: '剧本' },\n { id: 'character_design', name: '角色设计', shortName: '角色' },\n { id: 'storyboard', name: '分镜设计', shortName: '分镜' },\n { id: 'reference_generation', name: '参考图', shortName: '参考图' },\n { id: 'video_generation', name: '视频生成', shortName: '视频' },\n { id: 'post_production', name: '后期剪辑', shortName: '后期' },\n] as const;\n\nexport type StageId = typeof STAGES[number]['id'];\n\nexport interface ModelConfig {\n llm_model: string;\n vlm_model: string;\n image_t2i_model: string;\n image_it2i_model: string;\n video_model: string;\n video_ratio: string;\n enable_concurrency: boolean;\n}\n\ninterface TopBarProps {\n /** null = 首页 */\n activeStage: string | null;\n stageStatuses: Record\u003cstring, StageStatus>;\n onStageClick: (stageId: string) => void;\n onHomeClick: () => void;\n /** 是否处于工作流中(有 sessionId) */\n hasSession: boolean;\n /** 是否正在执行 */\n isRunning: boolean;\n /** 停止执行 */\n onStop: () => void;\n /** 代理模式(自动执行全流程) */\n autoMode: boolean;\n onAutoModeChange: (auto: boolean) => void;\n /** 当前模型配置 */\n modelConfig?: ModelConfig;\n /** 模型配置变更 */\n onModelConfigChange?: (config: ModelConfig) => void;\n /** 项目状态(如 running, waiting_in_stage, stage_completed, session_completed, idle, error 等) */\n projectStatus?: string;\n}\n\n/* ─── 带 Provider 分组的 \u003cselect> ─── */\nfunction ProviderSelect({\n value,\n providers,\n onChange,\n}: {\n value: string;\n providers: ProviderGroup[];\n onChange: (val: string) => void;\n}) {\n return (\n \u003cselect\n value={value}\n onChange={e => onChange(e.target.value)}\n className=\"bg-gray-50 border border-gray-200 rounded-lg px-2 py-1.5 text-xs text-gray-700 outline-none w-full\"\n >\n {providers.map(pg => (\n \u003coptgroup key={pg.provider} label={pg.label}>\n {pg.models.map(m => (\n \u003coption key={m.id} value={m.id}>{m.label}\u003c/option>\n ))}\n \u003c/optgroup>\n ))}\n \u003c/select>\n );\n}\n\n/* ─── 模型选择下拉面板 ─── */\nfunction ModelSelector({\n config,\n onChange,\n}: {\n config: ModelConfig;\n onChange: (config: ModelConfig) => void;\n}) {\n const [open, setOpen] = useState(false);\n const ref = useRef\u003cHTMLDivElement>(null);\n\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);\n };\n if (open) document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n }, [open]);\n\n const update = (key: keyof ModelConfig, val: string | boolean) => {\n onChange({ ...config, [key]: val });\n };\n\n return (\n \u003cdiv ref={ref} className=\"relative\">\n \u003cbutton\n onClick={() => setOpen(!open)}\n className={clsx(\n 'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all',\n open\n ? 'bg-blue-50 text-blue-700 ring-1 ring-blue-200'\n : 'text-gray-500 hover:bg-gray-50'\n )}\n title=\"生成配置\"\n >\n \u003cSettings2 className=\"w-3.5 h-3.5\" />\n \u003cspan>生成配置\u003c/span>\n \u003cChevronDown className={clsx('w-3 h-3 transition-transform', open && 'rotate-180')} />\n \u003c/button>\n\n {open && (\n \u003cdiv className=\"absolute right-0 top-full mt-1 w-72 bg-white rounded-xl shadow-lg border border-gray-200 p-3 z-50 space-y-2.5\">\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-[10px] text-gray-400 font-medium\">LLM 模型\u003c/span>\n \u003cProviderSelect value={config.llm_model} providers={LLM_PROVIDERS} onChange={v => update('llm_model', v)} />\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-[10px] text-gray-400 font-medium\">VLM 评估模型\u003c/span>\n \u003cProviderSelect value={config.vlm_model} providers={VLM_PROVIDERS} onChange={v => update('vlm_model', v)} />\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-[10px] text-gray-400 font-medium\">文生图\u003c/span>\n \u003cProviderSelect value={config.image_t2i_model} providers={T2I_PROVIDERS} onChange={v => update('image_t2i_model', v)} />\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-[10px] text-gray-400 font-medium\">图生图\u003c/span>\n \u003cProviderSelect value={config.image_it2i_model} providers={I2I_PROVIDERS} onChange={v => update('image_it2i_model', v)} />\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-[10px] text-gray-400 font-medium\">视频模型\u003c/span>\n \u003cProviderSelect value={config.video_model} providers={VIDEO_PROVIDERS} onChange={v => update('video_model', v)} />\n \u003c/label>\n \u003clabel className=\"flex flex-col gap-1\">\n \u003cspan className=\"text-[10px] text-gray-400 font-medium\">视频比例\u003c/span>\n \u003cdiv className=\"flex gap-0.5\">\n {VIDEO_RATIOS.map(r => (\n \u003cbutton\n key={r.id}\n onClick={() => update('video_ratio', r.id)}\n className={`flex flex-col items-center gap-0.5 p-1 rounded border transition-all ${\n config.video_ratio === r.id\n ? 'border-indigo-500 bg-indigo-50'\n : 'border-gray-200 hover:border-gray-300'\n }`}\n title={r.label}\n >\n \u003cdiv\n className=\"bg-gray-600 rounded-sm\"\n style={{\n width: r.ratio === '16:9' ? '16px' :\n r.ratio === '9:16' ? '9px' :\n r.ratio === '1:1' ? '12px' :\n r.ratio === '4:3' ? '14px' :\n r.ratio === '3:4' ? '10px' :\n '18px',\n height: r.ratio === '16:9' ? '9px' :\n r.ratio === '9:16' ? '16px' :\n r.ratio === '1:1' ? '12px' :\n r.ratio === '4:3' ? '10px' :\n r.ratio === '3:4' ? '14px' :\n '7px',\n }}\n />\n \u003cspan className=\"text-[8px] text-gray-500\">{r.label}\u003c/span>\n \u003c/button>\n ))}\n \u003c/div>\n \u003c/label>\n \u003clabel className=\"flex items-center gap-2 text-xs cursor-pointer select-none\">\n \u003cinput\n type=\"checkbox\"\n checked={!!config.enable_concurrency}\n onChange={e => update('enable_concurrency', e.target.checked)}\n className=\"w-3.5 h-3.5 rounded border-gray-300 text-blue-500 focus:ring-blue-500/30\"\n />\n \u003cspan className=\"text-gray-500\">并发生成\u003c/span>\n \u003c/label>\n \u003c/div>\n )}\n \u003c/div>\n );\n}\n\nexport default function TopBar({\n activeStage,\n stageStatuses,\n onStageClick,\n onHomeClick,\n hasSession,\n isRunning,\n onStop,\n autoMode,\n onAutoModeChange,\n modelConfig,\n onModelConfigChange,\n projectStatus,\n}: TopBarProps) {\n const getStageIcon = (status: StageStatus, isActive: boolean) => {\n switch (status) {\n case 'completed':\n return \u003cCheckCircle className=\"w-4 h-4 text-green-500\" />;\n case 'running':\n return \u003cLoader className=\"w-4 h-4 text-blue-500 animate-spin\" />;\n case 'waiting':\n return \u003cEdit3 className=\"w-4 h-4 text-amber-500\" />;\n case 'error':\n return \u003cAlertCircle className=\"w-4 h-4 text-red-500\" />;\n default:\n return (\n \u003cCircle\n className={clsx('w-4 h-4', isActive ? 'text-blue-400' : 'text-gray-300')}\n />\n );\n }\n };\n\n return (\n \u003cheader className=\"h-14 bg-white border-b border-gray-200 flex items-center px-4 flex-shrink-0 z-10\">\n {/* Logo & 名称 */}\n \u003cbutton\n onClick={onHomeClick}\n className=\"flex items-center gap-2 mr-6 hover:opacity-80 transition-opacity flex-shrink-0\"\n >\n \u003cimg\n src=\"/logo.jpg\"\n alt=\"Logo\"\n className=\"w-8 h-8 rounded-lg object-contain\"\n />\n \u003cdiv className=\"flex flex-col leading-tight\">\n \u003cspan className=\"font-bold text-sm text-gray-800 tracking-tight\">\n AIGC-Claw\n \u003c/span>\n \u003c/div>\n \u003c/button>\n\n {/* 分隔线 */}\n {hasSession && \u003cdiv className=\"w-px h-6 bg-gray-200 mr-4 flex-shrink-0\" />}\n\n {/* 阶段进度条 */}\n {hasSession && (\n \u003cnav className=\"flex items-center gap-1 overflow-x-auto flex-1 min-w-0\">\n {STAGES.map((stage, idx) => {\n const status = stageStatuses[stage.id] || 'pending';\n const isActive = activeStage === stage.id;\n\n return (\n \u003cReact.Fragment key={stage.id}>\n {idx > 0 && (\n \u003cdiv\n className={clsx(\n 'w-6 h-px flex-shrink-0',\n stageStatuses[STAGES[idx - 1].id] === 'completed'\n ? 'bg-green-300'\n : 'bg-gray-200'\n )}\n />\n )}\n \u003cbutton\n onClick={() => onStageClick(stage.id)}\n className={clsx(\n 'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all whitespace-nowrap flex-shrink-0',\n isActive\n ? 'bg-blue-50 text-blue-700 ring-1 ring-blue-200'\n : status === 'completed'\n ? 'text-green-700 hover:bg-green-50'\n : status === 'error'\n ? 'text-red-600 hover:bg-red-50'\n : 'text-gray-500 hover:bg-gray-50'\n )}\n >\n {getStageIcon(status, isActive)}\n \u003cspan>{stage.shortName}\u003c/span>\n \u003c/button>\n \u003c/React.Fragment>\n );\n })}\n \u003c/nav>\n )}\n\n {/* 右侧控制区 */}\n \u003cdiv className=\"ml-auto flex items-center gap-2 flex-shrink-0\">\n {/* 模型选择 */}\n {hasSession && modelConfig && onModelConfigChange && (\n \u003cModelSelector config={modelConfig} onChange={onModelConfigChange} />\n )}\n\n {/* 代理模式切换 */}\n {hasSession && (\n \u003cbutton\n onClick={() => onAutoModeChange(!autoMode)}\n className={clsx(\n 'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all',\n autoMode\n ? 'bg-amber-50 text-amber-700 ring-1 ring-amber-200'\n : 'text-gray-500 hover:bg-gray-50'\n )}\n title={autoMode ? '代理模式:自动执行全流程' : '手动模式:每阶段需确认'}\n >\n \u003cZap className=\"w-3.5 h-3.5\" />\n \u003cspan>{autoMode ? '自动' : '手动'}\u003c/span>\n \u003c/button>\n )}\n\n {/* 停止按钮 */}\n {isRunning && (\n \u003cbutton\n onClick={onStop}\n className=\"flex items-center gap-1.5 px-3 py-1.5 bg-red-50 text-red-600 hover:bg-red-100 rounded-lg text-xs font-medium transition-colors ring-1 ring-red-200\"\n title=\"停止执行\"\n >\n \u003cSquare className=\"w-3.5 h-3.5 fill-current\" />\n \u003cspan>停止\u003c/span>\n \u003c/button>\n )}\n\n {/* 项目状态 */}\n {hasSession && projectStatus && (\n \u003cdiv\n className={clsx(\n 'px-2 py-1 rounded-lg text-xs font-medium flex items-center gap-1',\n projectStatus === 'running' && 'bg-blue-50 text-blue-700',\n projectStatus === 'waiting_in_stage' && 'bg-amber-50 text-amber-700',\n projectStatus === 'stage_completed' && 'bg-green-50 text-green-700',\n projectStatus === 'idle' && 'bg-gray-50 text-gray-600',\n projectStatus === 'error' && 'bg-red-50 text-red-700',\n projectStatus === 'stopped' && 'bg-orange-50 text-orange-700'\n )}\n title=\"项目状态\"\n >\n {projectStatus === 'running' && \u003cLoader className=\"w-3 h-3 animate-spin\" />}\n {projectStatus === 'waiting_in_stage' && \u003cEdit3 className=\"w-3 h-3\" />}\n {projectStatus === 'error' && \u003cAlertCircle className=\"w-3 h-3\" />}\n \u003cspan>\n {projectStatus === 'running' ? '执行中' :\n projectStatus === 'waiting_in_stage' ? '等待确认' :\n projectStatus === 'stage_completed' ? '阶段完成' :\n projectStatus === 'session_completed' ? '已完成' :\n projectStatus === 'idle' ? '空闲' :\n projectStatus === 'stopped' ? '已停止' :\n projectStatus === 'error' ? '出错' : projectStatus}\n \u003c/span>\n \u003c/div>\n )}\n\n\n {/* 临时工作台入口 */}\n \u003cLink\n href=\"/sandbox\"\n className=\"flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors text-xs font-medium\"\n title=\"临时工作台\"\n >\n \u003cHexagon className=\"w-3.5 h-3.5\" />\n \u003cspan>工作台\u003c/span>\n \u003c/Link>\n \u003c/div>\n \u003c/header>\n );\n}\n","content_type":"text/typescript; charset=utf-8","language":"tsx","size":14126,"content_sha256":"d00d3b1d51f36cf90d5c2bc643fc748a7192574bfb8fb7578bc40a2dbcc0a48b"},{"filename":"aigc-claw/frontend/config/examples.ts","content":"export const PROMPT_EXAMPLES = [\n {\n title: \"生成科幻短片\",\n description: \"怪兽宇宙中的激烈对决...\",\n text: \"以哥斯拉大战金刚主题,生成一个科幻风格短片\"\n },\n {\n title: \"水墨风格动画\",\n description: \"山水之间扁舟一叶...\",\n text: \"请帮我制作一个水墨中国风的动画\"\n },\n {\n title: \"悬疑侦探故事\",\n description: \"雨夜中的神秘黑影...\",\n text: \"请生成一个发生在上世纪伦敦的悬疑侦探故事短片\"\n },\n {\n title: \"治愈系微电影\",\n description: \"午后阳光下的猫咪...\",\n text: \"制作一个温馨治愈的日常风格短片,主角是一只橘猫\"\n },\n {\n title: \"太空探索纪录片\",\n description: \"穿越星云的壮丽旅程...\",\n text: \"生成一段关于人类探索火星的伪纪录片风格视频\"\n },\n {\n title: \"古风武侠打斗\",\n description: \"竹林深处的剑客对决...\",\n text: \"制作一段节奏紧凑的古风武侠打斗场面\"\n }\n];\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":1121,"content_sha256":"8b53e9a14afb8b09bb8db279f020df0eb3ac293cc3f8cb5e2c8ff1fc3f4907b8"},{"filename":"aigc-claw/frontend/config/models.ts","content":"/* ─── Provider + Model 分组结构 ─── */\nexport interface ModelOption {\n id: string;\n label: string;\n default?: boolean;\n}\n\nexport interface ProviderGroup {\n provider: string;\n label: string;\n models: ModelOption[];\n}\n\n/* ─── LLM 模型(按 Provider 分组) ─── */\nexport const LLM_PROVIDERS: ProviderGroup[] = [\n {\n provider: 'qwen',\n label: 'Qwen (DashScope)',\n models: [\n { id: 'qwen3.5-plus', label: 'qwen3.5-plus', default: true },\n { id: 'qwen3.5-max', label: 'qwen3.5-max' },\n ],\n },\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n models: [\n { id: 'deepseek-chat', label: 'deepseek-chat' },\n { id: 'deepseek-reasoner', label: 'deepseek-reasoner' },\n ],\n },\n {\n provider: 'openai',\n label: 'OpenAI',\n models: [\n { id: 'gpt-4o', label: 'gpt-4o' },\n { id: 'gpt-4', label: 'gpt-4' },\n { id: 'gpt-5', label: 'gpt-5' },\n { id: 'gpt-5.1', label: 'gpt-5.1' },\n { id: 'o3', label: 'o3' },\n ],\n },\n {\n provider: 'gemini',\n label: 'Gemini',\n models: [\n { id: 'gemini-3-flash-preview', label: 'gemini-3-flash-preview' },\n { id: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' },\n ],\n },\n];\n\n/** 扁平 LLM 列表(向后兼容) */\nexport const LLM_MODELS: ModelOption[] = LLM_PROVIDERS.flatMap(p => p.models);\n\n/* ─── 文生图 ─── */\nexport const T2I_PROVIDERS: ProviderGroup[] = [\n {\n provider: 'seedream',\n label: 'Seedream',\n models: [\n { id: 'doubao-seedream-5-0-260128', label: 'Seedream 5.0', default: true },\n { id: 'doubao-seedream-4-5-251128', label: 'Seedream 4.5' },\n { id: 'doubao-seedream-4-0-250828', label: 'Seedream 4.0' },\n ],\n },\n {\n provider: 'jimeng',\n label: 'JiMeng',\n models: [\n { id: 'jimeng_t2i_v40', label: 'jimeng_t2i_v40' },\n ],\n },\n {\n provider: 'dashscope',\n label: 'DashScope',\n models: [\n { id: 'wan2.6-t2i', label: 'wan2.6-t2i' },\n ],\n },\n {\n provider: 'openai',\n label: 'OpenAI',\n models: [\n { id: 'sora_image', label: 'sora_image' },\n { id: 'gpt-image-1.5', label: 'gpt-image-1.5' },\n ],\n },\n];\n\nexport const T2I_MODELS: ModelOption[] = T2I_PROVIDERS.flatMap(p => p.models);\n\n/* ─── 图生图 ─── */\nexport const I2I_PROVIDERS: ProviderGroup[] = [\n {\n provider: 'seedream',\n label: 'Seedream',\n models: [\n { id: 'doubao-seedream-5-0-260128', label: 'Seedream 5.0', default: true },\n { id: 'doubao-seedream-4-5-251128', label: 'Seedream 4.5' },\n { id: 'doubao-seedream-4-0-250828', label: 'Seedream 4.0' },\n ],\n },\n {\n provider: 'jimeng',\n label: 'JiMeng',\n models: [\n { id: 'jimeng_t2i_v40', label: 'jimeng_t2i_v40' },\n ],\n },\n {\n provider: 'dashscope',\n label: 'DashScope',\n models: [\n { id: 'wan2.6-image', label: 'wan2.6-image' },\n ],\n },\n];\n\nexport const I2I_MODELS: ModelOption[] = I2I_PROVIDERS.flatMap(p => p.models);\n\n/* ─── 视频 ─── */\nexport const VIDEO_PROVIDERS: ProviderGroup[] = [\n {\n provider: 'dashscope',\n label: 'DashScope',\n models: [\n { id: 'wan2.6-i2v-flash', label: 'wan2.6-i2v-flash', default: true },\n ],\n },\n {\n provider: 'kling',\n label: 'Kling',\n models: [\n { id: 'kling-v3', label: 'kling-v3' },\n { id: 'kling-v2-6', label: 'kling-v2-6' },\n { id: 'kling-v2-5-turbo', label: 'kling-v2-5-turbo' },\n ],\n },\n];\n\nexport const VIDEO_MODELS: ModelOption[] = VIDEO_PROVIDERS.flatMap(p => p.models);\n\n/* ─── VLM 评估模型 ─── */\nexport const VLM_PROVIDERS: ProviderGroup[] = [\n {\n provider: 'qwen',\n label: 'Qwen (DashScope)',\n models: [\n { id: 'qwen-vl-plus', label: 'qwen-vl-plus', default: true },\n { id: 'qwen3.5-plus', label: 'qwen3.5-plus' },\n { id: 'qwen3.5-max', label: 'qwen3.5-max' },\n ],\n },\n {\n provider: 'gemini',\n label: 'Gemini (Google)',\n models: [\n { id: 'gemini-2.5-flash-image', label: 'gemini-2.5-flash-image' },\n { id: 'gemini-2.5-pro-image', label: 'gemini-2.5-pro-image' },\n { id: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' },\n { id: 'gemini-3-pro-image-preview', label: 'gemini-3-pro-image-preview' },\n ],\n },\n];\n\nexport const VLM_MODELS: ModelOption[] = VLM_PROVIDERS.flatMap(p => p.models);\n\nexport const STYLES = [\n { id: 'comic-book', label: 'Comic Book / 漫画' },\n { id: 'anime', label: 'Anime / 动漫' },\n { id: 'realistic', label: 'Realistic / 写实' },\n { id: '3d-disney', label: '3D Disney / 迪士尼' },\n { id: 'watercolor', label: 'Watercolor / 水彩' },\n { id: 'oil-painting', label: 'Oil Painting / 油画' },\n { id: 'cyberpunk', label: 'Cyberpunk / 赛博朋克' },\n { id: 'chinese-ink', label: 'Chinese Ink / 水墨' },\n];\n\n/* ─── 视频比例 ─── */\nexport const VIDEO_RATIOS = [\n { id: '16:9', label: '16:9', ratio: '16:9' },\n { id: '9:16', label: '9:16', ratio: '9:16' },\n { id: '1:1', label: '1:1', ratio: '1:1' },\n { id: '4:3', label: '4:3', ratio: '4:3' },\n { id: '3:4', label: '3:4', ratio: '3:4' },\n { id: '21:9', label: '21:9', ratio: '21:9' },\n];\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":5712,"content_sha256":"e947d3abb43dde64aa1777caba6ea03193136b8b496bfb17cc8c8e54aad037b2"},{"filename":"aigc-claw/frontend/eslint.config.mjs","content":"import nextCoreVitals from \"eslint-config-next/core-web-vitals.js\";\nimport nextTypescript from \"eslint-config-next/typescript.js\";\n\nconst eslintConfig = [\n nextCoreVitals,\n nextTypescript,\n {\n ignores: [\".next/**\", \"out/**\", \"build/**\", \"next-env.d.ts\"],\n },\n];\n\nexport default eslintConfig;\n","content_type":"text/javascript","language":"javascript","size":299,"content_sha256":"25c9dcf6df454830d8138f043dc81a52c036d18aa0812dabca234d109b143653"},{"filename":"aigc-claw/frontend/next.config.ts","content":"import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n async rewrites() {\n return [\n {\n source: '/code/:path*',\n destination: 'http://127.0.0.1:8000/code/:path*',\n },\n {\n source: '/api/sessions',\n destination: 'http://127.0.0.1:8000/api/sessions',\n },\n {\n source: '/api/sessions/:path*',\n destination: 'http://127.0.0.1:8000/api/sessions/:path*',\n },\n // 工作流 API\n {\n source: '/api/project/:path*',\n destination: 'http://127.0.0.1:8000/api/project/:path*',\n },\n {\n source: '/api/stages',\n destination: 'http://127.0.0.1:8000/api/stages',\n },\n // 临时工作台 API\n {\n source: '/api/sandbox/:path*',\n destination: 'http://127.0.0.1:8000/api/sandbox/:path*',\n },\n ];\n },\n};\n\nexport default nextConfig;\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":889,"content_sha256":"9833972db63952a2b984f18c04f5b91c216c627b908262dd461db848497fcb34"},{"filename":"aigc-claw/frontend/package-lock.json","content":"{\n \"name\": \"aigc-claw\",\n \"version\": \"0.1.0\",\n \"lockfileVersion\": 3,\n \"requires\": true,\n \"packages\": {\n \"\": {\n \"name\": \"aigc-claw\",\n \"version\": \"0.1.0\",\n \"dependencies\": {\n \"clsx\": \"^2.1.1\",\n \"lucide-react\": \"^0.563.0\",\n \"next\": \"15.2.4\",\n \"react\": \"19.2.3\",\n \"react-dom\": \"19.2.3\",\n \"tailwind-merge\": \"^3.4.0\"\n },\n \"devDependencies\": {\n \"@tailwindcss/postcss\": \"^4\",\n \"@types/node\": \"^20\",\n \"@types/react\": \"^19\",\n \"@types/react-dom\": \"^19\",\n \"eslint\": \"^9\",\n \"eslint-config-next\": \"15.2.4\",\n \"tailwindcss\": \"^4\",\n \"typescript\": \"^5\"\n }\n },\n \"node_modules/@alloc/quick-lru\": {\n \"version\": \"5.2.0\",\n \"resolved\": \"https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz\",\n \"integrity\": \"sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=10\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/@emnapi/core\": {\n \"version\": \"1.9.1\",\n \"resolved\": \"https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz\",\n \"integrity\": \"sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"@emnapi/wasi-threads\": \"1.2.0\",\n \"tslib\": \"^2.4.0\"\n }\n },\n \"node_modules/@emnapi/runtime\": {\n \"version\": \"1.9.1\",\n \"resolved\": \"https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz\",\n \"integrity\": \"sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==\",\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"tslib\": \"^2.4.0\"\n }\n },\n \"node_modules/@emnapi/wasi-threads\": {\n \"version\": \"1.2.0\",\n \"resolved\": \"https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz\",\n \"integrity\": \"sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"tslib\": \"^2.4.0\"\n }\n },\n \"node_modules/@eslint-community/eslint-utils\": {\n \"version\": \"4.9.1\",\n \"resolved\": \"https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz\",\n \"integrity\": \"sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"eslint-visitor-keys\": \"^3.4.3\"\n },\n \"engines\": {\n \"node\": \"^12.22.0 || ^14.17.0 || >=16.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/eslint\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^6.0.0 || ^7.0.0 || >=8.0.0\"\n }\n },\n \"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys\": {\n \"version\": \"3.4.3\",\n \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz\",\n \"integrity\": \"sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \"^12.22.0 || ^14.17.0 || >=16.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/eslint\"\n }\n },\n \"node_modules/@eslint-community/regexpp\": {\n \"version\": \"4.12.2\",\n \"resolved\": \"https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz\",\n \"integrity\": \"sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \"^12.0.0 || ^14.0.0 || >=16.0.0\"\n }\n },\n \"node_modules/@eslint/config-array\": {\n \"version\": \"0.21.2\",\n \"resolved\": \"https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz\",\n \"integrity\": \"sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"dependencies\": {\n \"@eslint/object-schema\": \"^2.1.7\",\n \"debug\": \"^4.3.1\",\n \"minimatch\": \"^3.1.5\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n }\n },\n \"node_modules/@eslint/config-helpers\": {\n \"version\": \"0.4.2\",\n \"resolved\": \"https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz\",\n \"integrity\": \"sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"dependencies\": {\n \"@eslint/core\": \"^0.17.0\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n }\n },\n \"node_modules/@eslint/core\": {\n \"version\": \"0.17.0\",\n \"resolved\": \"https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz\",\n \"integrity\": \"sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"dependencies\": {\n \"@types/json-schema\": \"^7.0.15\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n }\n },\n \"node_modules/@eslint/eslintrc\": {\n \"version\": \"3.3.5\",\n \"resolved\": \"https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz\",\n \"integrity\": \"sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"ajv\": \"^6.14.0\",\n \"debug\": \"^4.3.2\",\n \"espree\": \"^10.0.1\",\n \"globals\": \"^14.0.0\",\n \"ignore\": \"^5.2.0\",\n \"import-fresh\": \"^3.2.1\",\n \"js-yaml\": \"^4.1.1\",\n \"minimatch\": \"^3.1.5\",\n \"strip-json-comments\": \"^3.1.1\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/eslint\"\n }\n },\n \"node_modules/@eslint/js\": {\n \"version\": \"9.39.4\",\n \"resolved\": \"https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz\",\n \"integrity\": \"sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"url\": \"https://eslint.org/donate\"\n }\n },\n \"node_modules/@eslint/object-schema\": {\n \"version\": \"2.1.7\",\n \"resolved\": \"https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz\",\n \"integrity\": \"sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n }\n },\n \"node_modules/@eslint/plugin-kit\": {\n \"version\": \"0.4.1\",\n \"resolved\": \"https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz\",\n \"integrity\": \"sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"dependencies\": {\n \"@eslint/core\": \"^0.17.0\",\n \"levn\": \"^0.4.1\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n }\n },\n \"node_modules/@humanfs/core\": {\n \"version\": \"0.19.1\",\n \"resolved\": \"https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz\",\n \"integrity\": \"sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \">=18.18.0\"\n }\n },\n \"node_modules/@humanfs/node\": {\n \"version\": \"0.16.7\",\n \"resolved\": \"https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz\",\n \"integrity\": \"sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"dependencies\": {\n \"@humanfs/core\": \"^0.19.1\",\n \"@humanwhocodes/retry\": \"^0.4.0\"\n },\n \"engines\": {\n \"node\": \">=18.18.0\"\n }\n },\n \"node_modules/@humanwhocodes/module-importer\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz\",\n \"integrity\": \"sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \">=12.22\"\n },\n \"funding\": {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/nzakas\"\n }\n },\n \"node_modules/@humanwhocodes/retry\": {\n \"version\": \"0.4.3\",\n \"resolved\": \"https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz\",\n \"integrity\": \"sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \">=18.18\"\n },\n \"funding\": {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/nzakas\"\n }\n },\n \"node_modules/@img/sharp-darwin-arm64\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz\",\n \"integrity\": \"sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-libvips-darwin-arm64\": \"1.0.4\"\n }\n },\n \"node_modules/@img/sharp-darwin-x64\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz\",\n \"integrity\": \"sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-libvips-darwin-x64\": \"1.0.4\"\n }\n },\n \"node_modules/@img/sharp-libvips-darwin-arm64\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz\",\n \"integrity\": \"sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-libvips-darwin-x64\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz\",\n \"integrity\": \"sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-libvips-linux-arm\": {\n \"version\": \"1.0.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz\",\n \"integrity\": \"sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==\",\n \"cpu\": [\n \"arm\"\n ],\n \"license\": \"LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-libvips-linux-arm64\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz\",\n \"integrity\": \"sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-libvips-linux-s390x\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz\",\n \"integrity\": \"sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==\",\n \"cpu\": [\n \"s390x\"\n ],\n \"license\": \"LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-libvips-linux-x64\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz\",\n \"integrity\": \"sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-libvips-linuxmusl-arm64\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz\",\n \"integrity\": \"sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-libvips-linuxmusl-x64\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz\",\n \"integrity\": \"sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-linux-arm\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz\",\n \"integrity\": \"sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==\",\n \"cpu\": [\n \"arm\"\n ],\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-libvips-linux-arm\": \"1.0.5\"\n }\n },\n \"node_modules/@img/sharp-linux-arm64\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz\",\n \"integrity\": \"sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-libvips-linux-arm64\": \"1.0.4\"\n }\n },\n \"node_modules/@img/sharp-linux-s390x\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz\",\n \"integrity\": \"sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==\",\n \"cpu\": [\n \"s390x\"\n ],\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-libvips-linux-s390x\": \"1.0.4\"\n }\n },\n \"node_modules/@img/sharp-linux-x64\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz\",\n \"integrity\": \"sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-libvips-linux-x64\": \"1.0.4\"\n }\n },\n \"node_modules/@img/sharp-linuxmusl-arm64\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz\",\n \"integrity\": \"sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-libvips-linuxmusl-arm64\": \"1.0.4\"\n }\n },\n \"node_modules/@img/sharp-linuxmusl-x64\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz\",\n \"integrity\": \"sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-libvips-linuxmusl-x64\": \"1.0.4\"\n }\n },\n \"node_modules/@img/sharp-wasm32\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz\",\n \"integrity\": \"sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==\",\n \"cpu\": [\n \"wasm32\"\n ],\n \"license\": \"Apache-2.0 AND LGPL-3.0-or-later AND MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"@emnapi/runtime\": \"^1.2.0\"\n },\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-win32-ia32\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz\",\n \"integrity\": \"sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==\",\n \"cpu\": [\n \"ia32\"\n ],\n \"license\": \"Apache-2.0 AND LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@img/sharp-win32-x64\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz\",\n \"integrity\": \"sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"Apache-2.0 AND LGPL-3.0-or-later\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n }\n },\n \"node_modules/@jridgewell/gen-mapping\": {\n \"version\": \"0.3.13\",\n \"resolved\": \"https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz\",\n \"integrity\": \"sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@jridgewell/sourcemap-codec\": \"^1.5.0\",\n \"@jridgewell/trace-mapping\": \"^0.3.24\"\n }\n },\n \"node_modules/@jridgewell/remapping\": {\n \"version\": \"2.3.5\",\n \"resolved\": \"https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz\",\n \"integrity\": \"sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@jridgewell/gen-mapping\": \"^0.3.5\",\n \"@jridgewell/trace-mapping\": \"^0.3.24\"\n }\n },\n \"node_modules/@jridgewell/resolve-uri\": {\n \"version\": \"3.1.2\",\n \"resolved\": \"https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz\",\n \"integrity\": \"sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=6.0.0\"\n }\n },\n \"node_modules/@jridgewell/sourcemap-codec\": {\n \"version\": \"1.5.5\",\n \"resolved\": \"https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz\",\n \"integrity\": \"sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/@jridgewell/trace-mapping\": {\n \"version\": \"0.3.31\",\n \"resolved\": \"https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz\",\n \"integrity\": \"sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@jridgewell/resolve-uri\": \"^3.1.0\",\n \"@jridgewell/sourcemap-codec\": \"^1.4.14\"\n }\n },\n \"node_modules/@napi-rs/wasm-runtime\": {\n \"version\": \"0.2.12\",\n \"resolved\": \"https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz\",\n \"integrity\": \"sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"@emnapi/core\": \"^1.4.3\",\n \"@emnapi/runtime\": \"^1.4.3\",\n \"@tybys/wasm-util\": \"^0.10.0\"\n }\n },\n \"node_modules/@next/env\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz\",\n \"integrity\": \"sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==\",\n \"license\": \"MIT\"\n },\n \"node_modules/@next/eslint-plugin-next\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.4.tgz\",\n \"integrity\": \"sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"fast-glob\": \"3.3.1\"\n }\n },\n \"node_modules/@next/swc-darwin-arm64\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz\",\n \"integrity\": \"sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-darwin-x64\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz\",\n \"integrity\": \"sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-arm64-gnu\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz\",\n \"integrity\": \"sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-arm64-musl\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz\",\n \"integrity\": \"sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-x64-gnu\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz\",\n \"integrity\": \"sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-linux-x64-musl\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz\",\n \"integrity\": \"sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-win32-arm64-msvc\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz\",\n \"integrity\": \"sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@next/swc-win32-x64-msvc\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz\",\n \"integrity\": \"sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==\",\n \"cpu\": [\n \"x64\"\n ],\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 10\"\n }\n },\n \"node_modules/@nodelib/fs.scandir\": {\n \"version\": \"2.1.5\",\n \"resolved\": \"https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz\",\n \"integrity\": \"sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@nodelib/fs.stat\": \"2.0.5\",\n \"run-parallel\": \"^1.1.9\"\n },\n \"engines\": {\n \"node\": \">= 8\"\n }\n },\n \"node_modules/@nodelib/fs.stat\": {\n \"version\": \"2.0.5\",\n \"resolved\": \"https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz\",\n \"integrity\": \"sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 8\"\n }\n },\n \"node_modules/@nodelib/fs.walk\": {\n \"version\": \"1.2.8\",\n \"resolved\": \"https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz\",\n \"integrity\": \"sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@nodelib/fs.scandir\": \"2.1.5\",\n \"fastq\": \"^1.6.0\"\n },\n \"engines\": {\n \"node\": \">= 8\"\n }\n },\n \"node_modules/@nolyfill/is-core-module\": {\n \"version\": \"1.0.39\",\n \"resolved\": \"https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz\",\n \"integrity\": \"sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=12.4.0\"\n }\n },\n \"node_modules/@rtsao/scc\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz\",\n \"integrity\": \"sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/@rushstack/eslint-patch\": {\n \"version\": \"1.16.1\",\n \"resolved\": \"https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz\",\n \"integrity\": \"sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/@swc/counter\": {\n \"version\": \"0.1.3\",\n \"resolved\": \"https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz\",\n \"integrity\": \"sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==\",\n \"license\": \"Apache-2.0\"\n },\n \"node_modules/@swc/helpers\": {\n \"version\": \"0.5.15\",\n \"resolved\": \"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz\",\n \"integrity\": \"sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==\",\n \"license\": \"Apache-2.0\",\n \"dependencies\": {\n \"tslib\": \"^2.8.0\"\n }\n },\n \"node_modules/@tailwindcss/node\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz\",\n \"integrity\": \"sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@jridgewell/remapping\": \"^2.3.5\",\n \"enhanced-resolve\": \"^5.19.0\",\n \"jiti\": \"^2.6.1\",\n \"lightningcss\": \"1.32.0\",\n \"magic-string\": \"^0.30.21\",\n \"source-map-js\": \"^1.2.1\",\n \"tailwindcss\": \"4.2.2\"\n }\n },\n \"node_modules/@tailwindcss/oxide\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz\",\n \"integrity\": \"sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 20\"\n },\n \"optionalDependencies\": {\n \"@tailwindcss/oxide-android-arm64\": \"4.2.2\",\n \"@tailwindcss/oxide-darwin-arm64\": \"4.2.2\",\n \"@tailwindcss/oxide-darwin-x64\": \"4.2.2\",\n \"@tailwindcss/oxide-freebsd-x64\": \"4.2.2\",\n \"@tailwindcss/oxide-linux-arm-gnueabihf\": \"4.2.2\",\n \"@tailwindcss/oxide-linux-arm64-gnu\": \"4.2.2\",\n \"@tailwindcss/oxide-linux-arm64-musl\": \"4.2.2\",\n \"@tailwindcss/oxide-linux-x64-gnu\": \"4.2.2\",\n \"@tailwindcss/oxide-linux-x64-musl\": \"4.2.2\",\n \"@tailwindcss/oxide-wasm32-wasi\": \"4.2.2\",\n \"@tailwindcss/oxide-win32-arm64-msvc\": \"4.2.2\",\n \"@tailwindcss/oxide-win32-x64-msvc\": \"4.2.2\"\n }\n },\n \"node_modules/@tailwindcss/oxide-android-arm64\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz\",\n \"integrity\": \"sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"android\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-darwin-arm64\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz\",\n \"integrity\": \"sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-darwin-x64\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz\",\n \"integrity\": \"sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-freebsd-x64\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz\",\n \"integrity\": \"sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"freebsd\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz\",\n \"integrity\": \"sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==\",\n \"cpu\": [\n \"arm\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-linux-arm64-gnu\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz\",\n \"integrity\": \"sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-linux-arm64-musl\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz\",\n \"integrity\": \"sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-linux-x64-gnu\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz\",\n \"integrity\": \"sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-linux-x64-musl\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz\",\n \"integrity\": \"sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-wasm32-wasi\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz\",\n \"integrity\": \"sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==\",\n \"bundleDependencies\": [\n \"@napi-rs/wasm-runtime\",\n \"@emnapi/core\",\n \"@emnapi/runtime\",\n \"@tybys/wasm-util\",\n \"@emnapi/wasi-threads\",\n \"tslib\"\n ],\n \"cpu\": [\n \"wasm32\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"@emnapi/core\": \"^1.8.1\",\n \"@emnapi/runtime\": \"^1.8.1\",\n \"@emnapi/wasi-threads\": \"^1.1.0\",\n \"@napi-rs/wasm-runtime\": \"^1.1.1\",\n \"@tybys/wasm-util\": \"^0.10.1\",\n \"tslib\": \"^2.8.1\"\n },\n \"engines\": {\n \"node\": \">=14.0.0\"\n }\n },\n \"node_modules/@tailwindcss/oxide-win32-arm64-msvc\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz\",\n \"integrity\": \"sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/oxide-win32-x64-msvc\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz\",\n \"integrity\": \"sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 20\"\n }\n },\n \"node_modules/@tailwindcss/postcss\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz\",\n \"integrity\": \"sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@alloc/quick-lru\": \"^5.2.0\",\n \"@tailwindcss/node\": \"4.2.2\",\n \"@tailwindcss/oxide\": \"4.2.2\",\n \"postcss\": \"^8.5.6\",\n \"tailwindcss\": \"4.2.2\"\n }\n },\n \"node_modules/@tybys/wasm-util\": {\n \"version\": \"0.10.1\",\n \"resolved\": \"https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz\",\n \"integrity\": \"sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"tslib\": \"^2.4.0\"\n }\n },\n \"node_modules/@types/estree\": {\n \"version\": \"1.0.8\",\n \"resolved\": \"https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz\",\n \"integrity\": \"sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/@types/json-schema\": {\n \"version\": \"7.0.15\",\n \"resolved\": \"https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz\",\n \"integrity\": \"sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/@types/json5\": {\n \"version\": \"0.0.29\",\n \"resolved\": \"https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz\",\n \"integrity\": \"sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/@types/node\": {\n \"version\": \"20.19.37\",\n \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz\",\n \"integrity\": \"sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"undici-types\": \"~6.21.0\"\n }\n },\n \"node_modules/@types/react\": {\n \"version\": \"19.2.14\",\n \"resolved\": \"https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz\",\n \"integrity\": \"sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"csstype\": \"^3.2.2\"\n }\n },\n \"node_modules/@types/react-dom\": {\n \"version\": \"19.2.3\",\n \"resolved\": \"https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz\",\n \"integrity\": \"sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"peerDependencies\": {\n \"@types/react\": \"^19.2.0\"\n }\n },\n \"node_modules/@typescript-eslint/eslint-plugin\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz\",\n \"integrity\": \"sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@eslint-community/regexpp\": \"^4.12.2\",\n \"@typescript-eslint/scope-manager\": \"8.57.2\",\n \"@typescript-eslint/type-utils\": \"8.57.2\",\n \"@typescript-eslint/utils\": \"8.57.2\",\n \"@typescript-eslint/visitor-keys\": \"8.57.2\",\n \"ignore\": \"^7.0.5\",\n \"natural-compare\": \"^1.4.0\",\n \"ts-api-utils\": \"^2.4.0\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n },\n \"peerDependencies\": {\n \"@typescript-eslint/parser\": \"^8.57.2\",\n \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n \"typescript\": \">=4.8.4 \u003c6.0.0\"\n }\n },\n \"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore\": {\n \"version\": \"7.0.5\",\n \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz\",\n \"integrity\": \"sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 4\"\n }\n },\n \"node_modules/@typescript-eslint/parser\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz\",\n \"integrity\": \"sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@typescript-eslint/scope-manager\": \"8.57.2\",\n \"@typescript-eslint/types\": \"8.57.2\",\n \"@typescript-eslint/typescript-estree\": \"8.57.2\",\n \"@typescript-eslint/visitor-keys\": \"8.57.2\",\n \"debug\": \"^4.4.3\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n \"typescript\": \">=4.8.4 \u003c6.0.0\"\n }\n },\n \"node_modules/@typescript-eslint/project-service\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz\",\n \"integrity\": \"sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@typescript-eslint/tsconfig-utils\": \"^8.57.2\",\n \"@typescript-eslint/types\": \"^8.57.2\",\n \"debug\": \"^4.4.3\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n },\n \"peerDependencies\": {\n \"typescript\": \">=4.8.4 \u003c6.0.0\"\n }\n },\n \"node_modules/@typescript-eslint/scope-manager\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz\",\n \"integrity\": \"sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@typescript-eslint/types\": \"8.57.2\",\n \"@typescript-eslint/visitor-keys\": \"8.57.2\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n }\n },\n \"node_modules/@typescript-eslint/tsconfig-utils\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz\",\n \"integrity\": \"sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n },\n \"peerDependencies\": {\n \"typescript\": \">=4.8.4 \u003c6.0.0\"\n }\n },\n \"node_modules/@typescript-eslint/type-utils\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz\",\n \"integrity\": \"sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@typescript-eslint/types\": \"8.57.2\",\n \"@typescript-eslint/typescript-estree\": \"8.57.2\",\n \"@typescript-eslint/utils\": \"8.57.2\",\n \"debug\": \"^4.4.3\",\n \"ts-api-utils\": \"^2.4.0\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n \"typescript\": \">=4.8.4 \u003c6.0.0\"\n }\n },\n \"node_modules/@typescript-eslint/types\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz\",\n \"integrity\": \"sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n }\n },\n \"node_modules/@typescript-eslint/typescript-estree\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz\",\n \"integrity\": \"sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@typescript-eslint/project-service\": \"8.57.2\",\n \"@typescript-eslint/tsconfig-utils\": \"8.57.2\",\n \"@typescript-eslint/types\": \"8.57.2\",\n \"@typescript-eslint/visitor-keys\": \"8.57.2\",\n \"debug\": \"^4.4.3\",\n \"minimatch\": \"^10.2.2\",\n \"semver\": \"^7.7.3\",\n \"tinyglobby\": \"^0.2.15\",\n \"ts-api-utils\": \"^2.4.0\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n },\n \"peerDependencies\": {\n \"typescript\": \">=4.8.4 \u003c6.0.0\"\n }\n },\n \"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match\": {\n \"version\": \"4.0.4\",\n \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz\",\n \"integrity\": \"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \"18 || 20 || >=22\"\n }\n },\n \"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion\": {\n \"version\": \"5.0.5\",\n \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz\",\n \"integrity\": \"sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"balanced-match\": \"^4.0.2\"\n },\n \"engines\": {\n \"node\": \"18 || 20 || >=22\"\n }\n },\n \"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch\": {\n \"version\": \"10.2.4\",\n \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz\",\n \"integrity\": \"sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==\",\n \"dev\": true,\n \"license\": \"BlueOak-1.0.0\",\n \"dependencies\": {\n \"brace-expansion\": \"^5.0.2\"\n },\n \"engines\": {\n \"node\": \"18 || 20 || >=22\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/isaacs\"\n }\n },\n \"node_modules/@typescript-eslint/utils\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz\",\n \"integrity\": \"sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@eslint-community/eslint-utils\": \"^4.9.1\",\n \"@typescript-eslint/scope-manager\": \"8.57.2\",\n \"@typescript-eslint/types\": \"8.57.2\",\n \"@typescript-eslint/typescript-estree\": \"8.57.2\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n \"typescript\": \">=4.8.4 \u003c6.0.0\"\n }\n },\n \"node_modules/@typescript-eslint/visitor-keys\": {\n \"version\": \"8.57.2\",\n \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz\",\n \"integrity\": \"sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@typescript-eslint/types\": \"8.57.2\",\n \"eslint-visitor-keys\": \"^5.0.0\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/typescript-eslint\"\n }\n },\n \"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys\": {\n \"version\": \"5.0.1\",\n \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz\",\n \"integrity\": \"sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/eslint\"\n }\n },\n \"node_modules/@unrs/resolver-binding-android-arm-eabi\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz\",\n \"integrity\": \"sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==\",\n \"cpu\": [\n \"arm\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"android\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-android-arm64\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz\",\n \"integrity\": \"sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"android\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-darwin-arm64\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz\",\n \"integrity\": \"sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-darwin-x64\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz\",\n \"integrity\": \"sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-freebsd-x64\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz\",\n \"integrity\": \"sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"freebsd\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-arm-gnueabihf\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz\",\n \"integrity\": \"sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==\",\n \"cpu\": [\n \"arm\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-arm-musleabihf\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz\",\n \"integrity\": \"sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==\",\n \"cpu\": [\n \"arm\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-arm64-gnu\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz\",\n \"integrity\": \"sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-arm64-musl\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz\",\n \"integrity\": \"sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-ppc64-gnu\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz\",\n \"integrity\": \"sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==\",\n \"cpu\": [\n \"ppc64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-riscv64-gnu\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz\",\n \"integrity\": \"sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==\",\n \"cpu\": [\n \"riscv64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-riscv64-musl\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz\",\n \"integrity\": \"sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==\",\n \"cpu\": [\n \"riscv64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-s390x-gnu\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz\",\n \"integrity\": \"sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==\",\n \"cpu\": [\n \"s390x\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-x64-gnu\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz\",\n \"integrity\": \"sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-linux-x64-musl\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz\",\n \"integrity\": \"sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-wasm32-wasi\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz\",\n \"integrity\": \"sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==\",\n \"cpu\": [\n \"wasm32\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"@napi-rs/wasm-runtime\": \"^0.2.11\"\n },\n \"engines\": {\n \"node\": \">=14.0.0\"\n }\n },\n \"node_modules/@unrs/resolver-binding-win32-arm64-msvc\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz\",\n \"integrity\": \"sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-win32-ia32-msvc\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz\",\n \"integrity\": \"sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==\",\n \"cpu\": [\n \"ia32\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ]\n },\n \"node_modules/@unrs/resolver-binding-win32-x64-msvc\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz\",\n \"integrity\": \"sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MIT\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ]\n },\n \"node_modules/acorn\": {\n \"version\": \"8.16.0\",\n \"resolved\": \"https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz\",\n \"integrity\": \"sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"bin\": {\n \"acorn\": \"bin/acorn\"\n },\n \"engines\": {\n \"node\": \">=0.4.0\"\n }\n },\n \"node_modules/acorn-jsx\": {\n \"version\": \"5.3.2\",\n \"resolved\": \"https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz\",\n \"integrity\": \"sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"peerDependencies\": {\n \"acorn\": \"^6.0.0 || ^7.0.0 || ^8.0.0\"\n }\n },\n \"node_modules/ajv\": {\n \"version\": \"6.14.0\",\n \"resolved\": \"https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz\",\n \"integrity\": \"sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"fast-deep-equal\": \"^3.1.1\",\n \"fast-json-stable-stringify\": \"^2.0.0\",\n \"json-schema-traverse\": \"^0.4.1\",\n \"uri-js\": \"^4.2.2\"\n },\n \"funding\": {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/epoberezkin\"\n }\n },\n \"node_modules/ansi-styles\": {\n \"version\": \"4.3.0\",\n \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz\",\n \"integrity\": \"sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"color-convert\": \"^2.0.1\"\n },\n \"engines\": {\n \"node\": \">=8\"\n },\n \"funding\": {\n \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n }\n },\n \"node_modules/argparse\": {\n \"version\": \"2.0.1\",\n \"resolved\": \"https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz\",\n \"integrity\": \"sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==\",\n \"dev\": true,\n \"license\": \"Python-2.0\"\n },\n \"node_modules/aria-query\": {\n \"version\": \"5.3.2\",\n \"resolved\": \"https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz\",\n \"integrity\": \"sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/array-buffer-byte-length\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz\",\n \"integrity\": \"sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"is-array-buffer\": \"^3.0.5\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/array-includes\": {\n \"version\": \"3.1.9\",\n \"resolved\": \"https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz\",\n \"integrity\": \"sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.4\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.24.0\",\n \"es-object-atoms\": \"^1.1.1\",\n \"get-intrinsic\": \"^1.3.0\",\n \"is-string\": \"^1.1.1\",\n \"math-intrinsics\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/array.prototype.findlast\": {\n \"version\": \"1.2.5\",\n \"resolved\": \"https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz\",\n \"integrity\": \"sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.7\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.2\",\n \"es-errors\": \"^1.3.0\",\n \"es-object-atoms\": \"^1.0.0\",\n \"es-shim-unscopables\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/array.prototype.findlastindex\": {\n \"version\": \"1.2.6\",\n \"resolved\": \"https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz\",\n \"integrity\": \"sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.4\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.9\",\n \"es-errors\": \"^1.3.0\",\n \"es-object-atoms\": \"^1.1.1\",\n \"es-shim-unscopables\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/array.prototype.flat\": {\n \"version\": \"1.3.3\",\n \"resolved\": \"https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz\",\n \"integrity\": \"sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.5\",\n \"es-shim-unscopables\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/array.prototype.flatmap\": {\n \"version\": \"1.3.3\",\n \"resolved\": \"https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz\",\n \"integrity\": \"sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.5\",\n \"es-shim-unscopables\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/array.prototype.tosorted\": {\n \"version\": \"1.1.4\",\n \"resolved\": \"https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz\",\n \"integrity\": \"sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.7\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.3\",\n \"es-errors\": \"^1.3.0\",\n \"es-shim-unscopables\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/arraybuffer.prototype.slice\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz\",\n \"integrity\": \"sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"array-buffer-byte-length\": \"^1.0.1\",\n \"call-bind\": \"^1.0.8\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.5\",\n \"es-errors\": \"^1.3.0\",\n \"get-intrinsic\": \"^1.2.6\",\n \"is-array-buffer\": \"^3.0.4\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/ast-types-flow\": {\n \"version\": \"0.0.8\",\n \"resolved\": \"https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz\",\n \"integrity\": \"sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/async-function\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz\",\n \"integrity\": \"sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/available-typed-arrays\": {\n \"version\": \"1.0.7\",\n \"resolved\": \"https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz\",\n \"integrity\": \"sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"possible-typed-array-names\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/axe-core\": {\n \"version\": \"4.11.1\",\n \"resolved\": \"https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz\",\n \"integrity\": \"sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==\",\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"engines\": {\n \"node\": \">=4\"\n }\n },\n \"node_modules/axobject-query\": {\n \"version\": \"4.1.0\",\n \"resolved\": \"https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz\",\n \"integrity\": \"sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/balanced-match\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz\",\n \"integrity\": \"sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/brace-expansion\": {\n \"version\": \"1.1.12\",\n \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz\",\n \"integrity\": \"sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"balanced-match\": \"^1.0.0\",\n \"concat-map\": \"0.0.1\"\n }\n },\n \"node_modules/braces\": {\n \"version\": \"3.0.3\",\n \"resolved\": \"https://registry.npmjs.org/braces/-/braces-3.0.3.tgz\",\n \"integrity\": \"sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"fill-range\": \"^7.1.1\"\n },\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/busboy\": {\n \"version\": \"1.6.0\",\n \"resolved\": \"https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz\",\n \"integrity\": \"sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==\",\n \"dependencies\": {\n \"streamsearch\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">=10.16.0\"\n }\n },\n \"node_modules/call-bind\": {\n \"version\": \"1.0.8\",\n \"resolved\": \"https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz\",\n \"integrity\": \"sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind-apply-helpers\": \"^1.0.0\",\n \"es-define-property\": \"^1.0.0\",\n \"get-intrinsic\": \"^1.2.4\",\n \"set-function-length\": \"^1.2.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/call-bind-apply-helpers\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz\",\n \"integrity\": \"sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\",\n \"function-bind\": \"^1.1.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/call-bound\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz\",\n \"integrity\": \"sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind-apply-helpers\": \"^1.0.2\",\n \"get-intrinsic\": \"^1.3.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/callsites\": {\n \"version\": \"3.1.0\",\n \"resolved\": \"https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz\",\n \"integrity\": \"sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=6\"\n }\n },\n \"node_modules/caniuse-lite\": {\n \"version\": \"1.0.30001781\",\n \"resolved\": \"https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz\",\n \"integrity\": \"sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==\",\n \"funding\": [\n {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/browserslist\"\n },\n {\n \"type\": \"tidelift\",\n \"url\": \"https://tidelift.com/funding/github/npm/caniuse-lite\"\n },\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ],\n \"license\": \"CC-BY-4.0\"\n },\n \"node_modules/chalk\": {\n \"version\": \"4.1.2\",\n \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz\",\n \"integrity\": \"sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"ansi-styles\": \"^4.1.0\",\n \"supports-color\": \"^7.1.0\"\n },\n \"engines\": {\n \"node\": \">=10\"\n },\n \"funding\": {\n \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n }\n },\n \"node_modules/client-only\": {\n \"version\": \"0.0.1\",\n \"resolved\": \"https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz\",\n \"integrity\": \"sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==\",\n \"license\": \"MIT\"\n },\n \"node_modules/clsx\": {\n \"version\": \"2.1.1\",\n \"resolved\": \"https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz\",\n \"integrity\": \"sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==\",\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=6\"\n }\n },\n \"node_modules/color\": {\n \"version\": \"4.2.3\",\n \"resolved\": \"https://registry.npmjs.org/color/-/color-4.2.3.tgz\",\n \"integrity\": \"sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==\",\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"color-convert\": \"^2.0.1\",\n \"color-string\": \"^1.9.0\"\n },\n \"engines\": {\n \"node\": \">=12.5.0\"\n }\n },\n \"node_modules/color-convert\": {\n \"version\": \"2.0.1\",\n \"resolved\": \"https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz\",\n \"integrity\": \"sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==\",\n \"devOptional\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"color-name\": \"~1.1.4\"\n },\n \"engines\": {\n \"node\": \">=7.0.0\"\n }\n },\n \"node_modules/color-name\": {\n \"version\": \"1.1.4\",\n \"resolved\": \"https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz\",\n \"integrity\": \"sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==\",\n \"devOptional\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/color-string\": {\n \"version\": \"1.9.1\",\n \"resolved\": \"https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz\",\n \"integrity\": \"sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==\",\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"color-name\": \"^1.0.0\",\n \"simple-swizzle\": \"^0.2.2\"\n }\n },\n \"node_modules/concat-map\": {\n \"version\": \"0.0.1\",\n \"resolved\": \"https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz\",\n \"integrity\": \"sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/cross-spawn\": {\n \"version\": \"7.0.6\",\n \"resolved\": \"https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz\",\n \"integrity\": \"sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"path-key\": \"^3.1.0\",\n \"shebang-command\": \"^2.0.0\",\n \"which\": \"^2.0.1\"\n },\n \"engines\": {\n \"node\": \">= 8\"\n }\n },\n \"node_modules/csstype\": {\n \"version\": \"3.2.3\",\n \"resolved\": \"https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz\",\n \"integrity\": \"sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/damerau-levenshtein\": {\n \"version\": \"1.0.8\",\n \"resolved\": \"https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz\",\n \"integrity\": \"sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==\",\n \"dev\": true,\n \"license\": \"BSD-2-Clause\"\n },\n \"node_modules/data-view-buffer\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz\",\n \"integrity\": \"sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"es-errors\": \"^1.3.0\",\n \"is-data-view\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/data-view-byte-length\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz\",\n \"integrity\": \"sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"es-errors\": \"^1.3.0\",\n \"is-data-view\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/inspect-js\"\n }\n },\n \"node_modules/data-view-byte-offset\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz\",\n \"integrity\": \"sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"es-errors\": \"^1.3.0\",\n \"is-data-view\": \"^1.0.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/debug\": {\n \"version\": \"4.4.3\",\n \"resolved\": \"https://registry.npmjs.org/debug/-/debug-4.4.3.tgz\",\n \"integrity\": \"sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"ms\": \"^2.1.3\"\n },\n \"engines\": {\n \"node\": \">=6.0\"\n },\n \"peerDependenciesMeta\": {\n \"supports-color\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/deep-is\": {\n \"version\": \"0.1.4\",\n \"resolved\": \"https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz\",\n \"integrity\": \"sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/define-data-property\": {\n \"version\": \"1.1.4\",\n \"resolved\": \"https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz\",\n \"integrity\": \"sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-define-property\": \"^1.0.0\",\n \"es-errors\": \"^1.3.0\",\n \"gopd\": \"^1.0.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/define-properties\": {\n \"version\": \"1.2.1\",\n \"resolved\": \"https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz\",\n \"integrity\": \"sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"define-data-property\": \"^1.0.1\",\n \"has-property-descriptors\": \"^1.0.0\",\n \"object-keys\": \"^1.1.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/detect-libc\": {\n \"version\": \"2.1.2\",\n \"resolved\": \"https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz\",\n \"integrity\": \"sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==\",\n \"devOptional\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/doctrine\": {\n \"version\": \"2.1.0\",\n \"resolved\": \"https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz\",\n \"integrity\": \"sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"dependencies\": {\n \"esutils\": \"^2.0.2\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/dunder-proto\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz\",\n \"integrity\": \"sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind-apply-helpers\": \"^1.0.1\",\n \"es-errors\": \"^1.3.0\",\n \"gopd\": \"^1.2.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/emoji-regex\": {\n \"version\": \"9.2.2\",\n \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz\",\n \"integrity\": \"sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/enhanced-resolve\": {\n \"version\": \"5.20.1\",\n \"resolved\": \"https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz\",\n \"integrity\": \"sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"graceful-fs\": \"^4.2.4\",\n \"tapable\": \"^2.3.0\"\n },\n \"engines\": {\n \"node\": \">=10.13.0\"\n }\n },\n \"node_modules/es-abstract\": {\n \"version\": \"1.24.1\",\n \"resolved\": \"https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz\",\n \"integrity\": \"sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"array-buffer-byte-length\": \"^1.0.2\",\n \"arraybuffer.prototype.slice\": \"^1.0.4\",\n \"available-typed-arrays\": \"^1.0.7\",\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.4\",\n \"data-view-buffer\": \"^1.0.2\",\n \"data-view-byte-length\": \"^1.0.2\",\n \"data-view-byte-offset\": \"^1.0.1\",\n \"es-define-property\": \"^1.0.1\",\n \"es-errors\": \"^1.3.0\",\n \"es-object-atoms\": \"^1.1.1\",\n \"es-set-tostringtag\": \"^2.1.0\",\n \"es-to-primitive\": \"^1.3.0\",\n \"function.prototype.name\": \"^1.1.8\",\n \"get-intrinsic\": \"^1.3.0\",\n \"get-proto\": \"^1.0.1\",\n \"get-symbol-description\": \"^1.1.0\",\n \"globalthis\": \"^1.0.4\",\n \"gopd\": \"^1.2.0\",\n \"has-property-descriptors\": \"^1.0.2\",\n \"has-proto\": \"^1.2.0\",\n \"has-symbols\": \"^1.1.0\",\n \"hasown\": \"^2.0.2\",\n \"internal-slot\": \"^1.1.0\",\n \"is-array-buffer\": \"^3.0.5\",\n \"is-callable\": \"^1.2.7\",\n \"is-data-view\": \"^1.0.2\",\n \"is-negative-zero\": \"^2.0.3\",\n \"is-regex\": \"^1.2.1\",\n \"is-set\": \"^2.0.3\",\n \"is-shared-array-buffer\": \"^1.0.4\",\n \"is-string\": \"^1.1.1\",\n \"is-typed-array\": \"^1.1.15\",\n \"is-weakref\": \"^1.1.1\",\n \"math-intrinsics\": \"^1.1.0\",\n \"object-inspect\": \"^1.13.4\",\n \"object-keys\": \"^1.1.1\",\n \"object.assign\": \"^4.1.7\",\n \"own-keys\": \"^1.0.1\",\n \"regexp.prototype.flags\": \"^1.5.4\",\n \"safe-array-concat\": \"^1.1.3\",\n \"safe-push-apply\": \"^1.0.0\",\n \"safe-regex-test\": \"^1.1.0\",\n \"set-proto\": \"^1.0.0\",\n \"stop-iteration-iterator\": \"^1.1.0\",\n \"string.prototype.trim\": \"^1.2.10\",\n \"string.prototype.trimend\": \"^1.0.9\",\n \"string.prototype.trimstart\": \"^1.0.8\",\n \"typed-array-buffer\": \"^1.0.3\",\n \"typed-array-byte-length\": \"^1.0.3\",\n \"typed-array-byte-offset\": \"^1.0.4\",\n \"typed-array-length\": \"^1.0.7\",\n \"unbox-primitive\": \"^1.1.0\",\n \"which-typed-array\": \"^1.1.19\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/es-define-property\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz\",\n \"integrity\": \"sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/es-errors\": {\n \"version\": \"1.3.0\",\n \"resolved\": \"https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz\",\n \"integrity\": \"sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/es-iterator-helpers\": {\n \"version\": \"1.3.1\",\n \"resolved\": \"https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz\",\n \"integrity\": \"sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.4\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.24.1\",\n \"es-errors\": \"^1.3.0\",\n \"es-set-tostringtag\": \"^2.1.0\",\n \"function-bind\": \"^1.1.2\",\n \"get-intrinsic\": \"^1.3.0\",\n \"globalthis\": \"^1.0.4\",\n \"gopd\": \"^1.2.0\",\n \"has-property-descriptors\": \"^1.0.2\",\n \"has-proto\": \"^1.2.0\",\n \"has-symbols\": \"^1.1.0\",\n \"internal-slot\": \"^1.1.0\",\n \"iterator.prototype\": \"^1.1.5\",\n \"math-intrinsics\": \"^1.1.0\",\n \"safe-array-concat\": \"^1.1.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/es-object-atoms\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz\",\n \"integrity\": \"sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/es-set-tostringtag\": {\n \"version\": \"2.1.0\",\n \"resolved\": \"https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz\",\n \"integrity\": \"sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\",\n \"get-intrinsic\": \"^1.2.6\",\n \"has-tostringtag\": \"^1.0.2\",\n \"hasown\": \"^2.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/es-shim-unscopables\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz\",\n \"integrity\": \"sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"hasown\": \"^2.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/es-to-primitive\": {\n \"version\": \"1.3.0\",\n \"resolved\": \"https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz\",\n \"integrity\": \"sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"is-callable\": \"^1.2.7\",\n \"is-date-object\": \"^1.0.5\",\n \"is-symbol\": \"^1.0.4\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/escape-string-regexp\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz\",\n \"integrity\": \"sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=10\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/eslint\": {\n \"version\": \"9.39.4\",\n \"resolved\": \"https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz\",\n \"integrity\": \"sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@eslint-community/eslint-utils\": \"^4.8.0\",\n \"@eslint-community/regexpp\": \"^4.12.1\",\n \"@eslint/config-array\": \"^0.21.2\",\n \"@eslint/config-helpers\": \"^0.4.2\",\n \"@eslint/core\": \"^0.17.0\",\n \"@eslint/eslintrc\": \"^3.3.5\",\n \"@eslint/js\": \"9.39.4\",\n \"@eslint/plugin-kit\": \"^0.4.1\",\n \"@humanfs/node\": \"^0.16.6\",\n \"@humanwhocodes/module-importer\": \"^1.0.1\",\n \"@humanwhocodes/retry\": \"^0.4.2\",\n \"@types/estree\": \"^1.0.6\",\n \"ajv\": \"^6.14.0\",\n \"chalk\": \"^4.0.0\",\n \"cross-spawn\": \"^7.0.6\",\n \"debug\": \"^4.3.2\",\n \"escape-string-regexp\": \"^4.0.0\",\n \"eslint-scope\": \"^8.4.0\",\n \"eslint-visitor-keys\": \"^4.2.1\",\n \"espree\": \"^10.4.0\",\n \"esquery\": \"^1.5.0\",\n \"esutils\": \"^2.0.2\",\n \"fast-deep-equal\": \"^3.1.3\",\n \"file-entry-cache\": \"^8.0.0\",\n \"find-up\": \"^5.0.0\",\n \"glob-parent\": \"^6.0.2\",\n \"ignore\": \"^5.2.0\",\n \"imurmurhash\": \"^0.1.4\",\n \"is-glob\": \"^4.0.0\",\n \"json-stable-stringify-without-jsonify\": \"^1.0.1\",\n \"lodash.merge\": \"^4.6.2\",\n \"minimatch\": \"^3.1.5\",\n \"natural-compare\": \"^1.4.0\",\n \"optionator\": \"^0.9.3\"\n },\n \"bin\": {\n \"eslint\": \"bin/eslint.js\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"url\": \"https://eslint.org/donate\"\n },\n \"peerDependencies\": {\n \"jiti\": \"*\"\n },\n \"peerDependenciesMeta\": {\n \"jiti\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/eslint-config-next\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.4.tgz\",\n \"integrity\": \"sha512-v4gYjd4eYIme8qzaJItpR5MMBXJ0/YV07u7eb50kEnlEmX7yhOjdUdzz70v4fiINYRjLf8X8TbogF0k7wlz6sA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@next/eslint-plugin-next\": \"15.2.4\",\n \"@rushstack/eslint-patch\": \"^1.10.3\",\n \"@typescript-eslint/eslint-plugin\": \"^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0\",\n \"@typescript-eslint/parser\": \"^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0\",\n \"eslint-import-resolver-node\": \"^0.3.6\",\n \"eslint-import-resolver-typescript\": \"^3.5.2\",\n \"eslint-plugin-import\": \"^2.31.0\",\n \"eslint-plugin-jsx-a11y\": \"^6.10.0\",\n \"eslint-plugin-react\": \"^7.37.0\",\n \"eslint-plugin-react-hooks\": \"^5.0.0\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^7.23.0 || ^8.0.0 || ^9.0.0\",\n \"typescript\": \">=3.3.1\"\n },\n \"peerDependenciesMeta\": {\n \"typescript\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/eslint-import-resolver-node\": {\n \"version\": \"0.3.9\",\n \"resolved\": \"https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz\",\n \"integrity\": \"sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"debug\": \"^3.2.7\",\n \"is-core-module\": \"^2.13.0\",\n \"resolve\": \"^1.22.4\"\n }\n },\n \"node_modules/eslint-import-resolver-node/node_modules/debug\": {\n \"version\": \"3.2.7\",\n \"resolved\": \"https://registry.npmjs.org/debug/-/debug-3.2.7.tgz\",\n \"integrity\": \"sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"ms\": \"^2.1.1\"\n }\n },\n \"node_modules/eslint-import-resolver-typescript\": {\n \"version\": \"3.10.1\",\n \"resolved\": \"https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz\",\n \"integrity\": \"sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"dependencies\": {\n \"@nolyfill/is-core-module\": \"1.0.39\",\n \"debug\": \"^4.4.0\",\n \"get-tsconfig\": \"^4.10.0\",\n \"is-bun-module\": \"^2.0.0\",\n \"stable-hash\": \"^0.0.5\",\n \"tinyglobby\": \"^0.2.13\",\n \"unrs-resolver\": \"^1.6.2\"\n },\n \"engines\": {\n \"node\": \"^14.18.0 || >=16.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/eslint-import-resolver-typescript\"\n },\n \"peerDependencies\": {\n \"eslint\": \"*\",\n \"eslint-plugin-import\": \"*\",\n \"eslint-plugin-import-x\": \"*\"\n },\n \"peerDependenciesMeta\": {\n \"eslint-plugin-import\": {\n \"optional\": true\n },\n \"eslint-plugin-import-x\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/eslint-module-utils\": {\n \"version\": \"2.12.1\",\n \"resolved\": \"https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz\",\n \"integrity\": \"sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"debug\": \"^3.2.7\"\n },\n \"engines\": {\n \"node\": \">=4\"\n },\n \"peerDependenciesMeta\": {\n \"eslint\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/eslint-module-utils/node_modules/debug\": {\n \"version\": \"3.2.7\",\n \"resolved\": \"https://registry.npmjs.org/debug/-/debug-3.2.7.tgz\",\n \"integrity\": \"sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"ms\": \"^2.1.1\"\n }\n },\n \"node_modules/eslint-plugin-import\": {\n \"version\": \"2.32.0\",\n \"resolved\": \"https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz\",\n \"integrity\": \"sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@rtsao/scc\": \"^1.1.0\",\n \"array-includes\": \"^3.1.9\",\n \"array.prototype.findlastindex\": \"^1.2.6\",\n \"array.prototype.flat\": \"^1.3.3\",\n \"array.prototype.flatmap\": \"^1.3.3\",\n \"debug\": \"^3.2.7\",\n \"doctrine\": \"^2.1.0\",\n \"eslint-import-resolver-node\": \"^0.3.9\",\n \"eslint-module-utils\": \"^2.12.1\",\n \"hasown\": \"^2.0.2\",\n \"is-core-module\": \"^2.16.1\",\n \"is-glob\": \"^4.0.3\",\n \"minimatch\": \"^3.1.2\",\n \"object.fromentries\": \"^2.0.8\",\n \"object.groupby\": \"^1.0.3\",\n \"object.values\": \"^1.2.1\",\n \"semver\": \"^6.3.1\",\n \"string.prototype.trimend\": \"^1.0.9\",\n \"tsconfig-paths\": \"^3.15.0\"\n },\n \"engines\": {\n \"node\": \">=4\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9\"\n }\n },\n \"node_modules/eslint-plugin-import/node_modules/debug\": {\n \"version\": \"3.2.7\",\n \"resolved\": \"https://registry.npmjs.org/debug/-/debug-3.2.7.tgz\",\n \"integrity\": \"sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"ms\": \"^2.1.1\"\n }\n },\n \"node_modules/eslint-plugin-import/node_modules/semver\": {\n \"version\": \"6.3.1\",\n \"resolved\": \"https://registry.npmjs.org/semver/-/semver-6.3.1.tgz\",\n \"integrity\": \"sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"bin\": {\n \"semver\": \"bin/semver.js\"\n }\n },\n \"node_modules/eslint-plugin-jsx-a11y\": {\n \"version\": \"6.10.2\",\n \"resolved\": \"https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz\",\n \"integrity\": \"sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"aria-query\": \"^5.3.2\",\n \"array-includes\": \"^3.1.8\",\n \"array.prototype.flatmap\": \"^1.3.2\",\n \"ast-types-flow\": \"^0.0.8\",\n \"axe-core\": \"^4.10.0\",\n \"axobject-query\": \"^4.1.0\",\n \"damerau-levenshtein\": \"^1.0.8\",\n \"emoji-regex\": \"^9.2.2\",\n \"hasown\": \"^2.0.2\",\n \"jsx-ast-utils\": \"^3.3.5\",\n \"language-tags\": \"^1.0.9\",\n \"minimatch\": \"^3.1.2\",\n \"object.fromentries\": \"^2.0.8\",\n \"safe-regex-test\": \"^1.0.3\",\n \"string.prototype.includes\": \"^2.0.1\"\n },\n \"engines\": {\n \"node\": \">=4.0\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9\"\n }\n },\n \"node_modules/eslint-plugin-react\": {\n \"version\": \"7.37.5\",\n \"resolved\": \"https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz\",\n \"integrity\": \"sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"array-includes\": \"^3.1.8\",\n \"array.prototype.findlast\": \"^1.2.5\",\n \"array.prototype.flatmap\": \"^1.3.3\",\n \"array.prototype.tosorted\": \"^1.1.4\",\n \"doctrine\": \"^2.1.0\",\n \"es-iterator-helpers\": \"^1.2.1\",\n \"estraverse\": \"^5.3.0\",\n \"hasown\": \"^2.0.2\",\n \"jsx-ast-utils\": \"^2.4.1 || ^3.0.0\",\n \"minimatch\": \"^3.1.2\",\n \"object.entries\": \"^1.1.9\",\n \"object.fromentries\": \"^2.0.8\",\n \"object.values\": \"^1.2.1\",\n \"prop-types\": \"^15.8.1\",\n \"resolve\": \"^2.0.0-next.5\",\n \"semver\": \"^6.3.1\",\n \"string.prototype.matchall\": \"^4.0.12\",\n \"string.prototype.repeat\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">=4\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7\"\n }\n },\n \"node_modules/eslint-plugin-react-hooks\": {\n \"version\": \"5.2.0\",\n \"resolved\": \"https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz\",\n \"integrity\": \"sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=10\"\n },\n \"peerDependencies\": {\n \"eslint\": \"^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0\"\n }\n },\n \"node_modules/eslint-plugin-react/node_modules/resolve\": {\n \"version\": \"2.0.0-next.6\",\n \"resolved\": \"https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz\",\n \"integrity\": \"sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\",\n \"is-core-module\": \"^2.16.1\",\n \"node-exports-info\": \"^1.6.0\",\n \"object-keys\": \"^1.1.1\",\n \"path-parse\": \"^1.0.7\",\n \"supports-preserve-symlinks-flag\": \"^1.0.0\"\n },\n \"bin\": {\n \"resolve\": \"bin/resolve\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/eslint-plugin-react/node_modules/semver\": {\n \"version\": \"6.3.1\",\n \"resolved\": \"https://registry.npmjs.org/semver/-/semver-6.3.1.tgz\",\n \"integrity\": \"sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"bin\": {\n \"semver\": \"bin/semver.js\"\n }\n },\n \"node_modules/eslint-scope\": {\n \"version\": \"8.4.0\",\n \"resolved\": \"https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz\",\n \"integrity\": \"sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==\",\n \"dev\": true,\n \"license\": \"BSD-2-Clause\",\n \"dependencies\": {\n \"esrecurse\": \"^4.3.0\",\n \"estraverse\": \"^5.2.0\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/eslint\"\n }\n },\n \"node_modules/eslint-visitor-keys\": {\n \"version\": \"4.2.1\",\n \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz\",\n \"integrity\": \"sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/eslint\"\n }\n },\n \"node_modules/espree\": {\n \"version\": \"10.4.0\",\n \"resolved\": \"https://registry.npmjs.org/espree/-/espree-10.4.0.tgz\",\n \"integrity\": \"sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==\",\n \"dev\": true,\n \"license\": \"BSD-2-Clause\",\n \"dependencies\": {\n \"acorn\": \"^8.15.0\",\n \"acorn-jsx\": \"^5.3.2\",\n \"eslint-visitor-keys\": \"^4.2.1\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/eslint\"\n }\n },\n \"node_modules/esquery\": {\n \"version\": \"1.7.0\",\n \"resolved\": \"https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz\",\n \"integrity\": \"sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==\",\n \"dev\": true,\n \"license\": \"BSD-3-Clause\",\n \"dependencies\": {\n \"estraverse\": \"^5.1.0\"\n },\n \"engines\": {\n \"node\": \">=0.10\"\n }\n },\n \"node_modules/esrecurse\": {\n \"version\": \"4.3.0\",\n \"resolved\": \"https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz\",\n \"integrity\": \"sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==\",\n \"dev\": true,\n \"license\": \"BSD-2-Clause\",\n \"dependencies\": {\n \"estraverse\": \"^5.2.0\"\n },\n \"engines\": {\n \"node\": \">=4.0\"\n }\n },\n \"node_modules/estraverse\": {\n \"version\": \"5.3.0\",\n \"resolved\": \"https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz\",\n \"integrity\": \"sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==\",\n \"dev\": true,\n \"license\": \"BSD-2-Clause\",\n \"engines\": {\n \"node\": \">=4.0\"\n }\n },\n \"node_modules/esutils\": {\n \"version\": \"2.0.3\",\n \"resolved\": \"https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz\",\n \"integrity\": \"sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==\",\n \"dev\": true,\n \"license\": \"BSD-2-Clause\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/fast-deep-equal\": {\n \"version\": \"3.1.3\",\n \"resolved\": \"https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz\",\n \"integrity\": \"sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/fast-glob\": {\n \"version\": \"3.3.1\",\n \"resolved\": \"https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz\",\n \"integrity\": \"sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@nodelib/fs.stat\": \"^2.0.2\",\n \"@nodelib/fs.walk\": \"^1.2.3\",\n \"glob-parent\": \"^5.1.2\",\n \"merge2\": \"^1.3.0\",\n \"micromatch\": \"^4.0.4\"\n },\n \"engines\": {\n \"node\": \">=8.6.0\"\n }\n },\n \"node_modules/fast-glob/node_modules/glob-parent\": {\n \"version\": \"5.1.2\",\n \"resolved\": \"https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz\",\n \"integrity\": \"sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"dependencies\": {\n \"is-glob\": \"^4.0.1\"\n },\n \"engines\": {\n \"node\": \">= 6\"\n }\n },\n \"node_modules/fast-json-stable-stringify\": {\n \"version\": \"2.1.0\",\n \"resolved\": \"https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz\",\n \"integrity\": \"sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/fast-levenshtein\": {\n \"version\": \"2.0.6\",\n \"resolved\": \"https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz\",\n \"integrity\": \"sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/fastq\": {\n \"version\": \"1.20.1\",\n \"resolved\": \"https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz\",\n \"integrity\": \"sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"dependencies\": {\n \"reusify\": \"^1.0.4\"\n }\n },\n \"node_modules/file-entry-cache\": {\n \"version\": \"8.0.0\",\n \"resolved\": \"https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz\",\n \"integrity\": \"sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"flat-cache\": \"^4.0.0\"\n },\n \"engines\": {\n \"node\": \">=16.0.0\"\n }\n },\n \"node_modules/fill-range\": {\n \"version\": \"7.1.1\",\n \"resolved\": \"https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz\",\n \"integrity\": \"sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"to-regex-range\": \"^5.0.1\"\n },\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/find-up\": {\n \"version\": \"5.0.0\",\n \"resolved\": \"https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz\",\n \"integrity\": \"sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"locate-path\": \"^6.0.0\",\n \"path-exists\": \"^4.0.0\"\n },\n \"engines\": {\n \"node\": \">=10\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/flat-cache\": {\n \"version\": \"4.0.1\",\n \"resolved\": \"https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz\",\n \"integrity\": \"sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"flatted\": \"^3.2.9\",\n \"keyv\": \"^4.5.4\"\n },\n \"engines\": {\n \"node\": \">=16\"\n }\n },\n \"node_modules/flatted\": {\n \"version\": \"3.4.2\",\n \"resolved\": \"https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz\",\n \"integrity\": \"sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==\",\n \"dev\": true,\n \"license\": \"ISC\"\n },\n \"node_modules/for-each\": {\n \"version\": \"0.3.5\",\n \"resolved\": \"https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz\",\n \"integrity\": \"sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"is-callable\": \"^1.2.7\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/function-bind\": {\n \"version\": \"1.1.2\",\n \"resolved\": \"https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz\",\n \"integrity\": \"sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/function.prototype.name\": {\n \"version\": \"1.1.8\",\n \"resolved\": \"https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz\",\n \"integrity\": \"sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.3\",\n \"define-properties\": \"^1.2.1\",\n \"functions-have-names\": \"^1.2.3\",\n \"hasown\": \"^2.0.2\",\n \"is-callable\": \"^1.2.7\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/functions-have-names\": {\n \"version\": \"1.2.3\",\n \"resolved\": \"https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz\",\n \"integrity\": \"sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/generator-function\": {\n \"version\": \"2.0.1\",\n \"resolved\": \"https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz\",\n \"integrity\": \"sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/get-intrinsic\": {\n \"version\": \"1.3.0\",\n \"resolved\": \"https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz\",\n \"integrity\": \"sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind-apply-helpers\": \"^1.0.2\",\n \"es-define-property\": \"^1.0.1\",\n \"es-errors\": \"^1.3.0\",\n \"es-object-atoms\": \"^1.1.1\",\n \"function-bind\": \"^1.1.2\",\n \"get-proto\": \"^1.0.1\",\n \"gopd\": \"^1.2.0\",\n \"has-symbols\": \"^1.1.0\",\n \"hasown\": \"^2.0.2\",\n \"math-intrinsics\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/get-proto\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz\",\n \"integrity\": \"sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"dunder-proto\": \"^1.0.1\",\n \"es-object-atoms\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/get-symbol-description\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz\",\n \"integrity\": \"sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"es-errors\": \"^1.3.0\",\n \"get-intrinsic\": \"^1.2.6\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/get-tsconfig\": {\n \"version\": \"4.13.7\",\n \"resolved\": \"https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz\",\n \"integrity\": \"sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"resolve-pkg-maps\": \"^1.0.0\"\n },\n \"funding\": {\n \"url\": \"https://github.com/privatenumber/get-tsconfig?sponsor=1\"\n }\n },\n \"node_modules/glob-parent\": {\n \"version\": \"6.0.2\",\n \"resolved\": \"https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz\",\n \"integrity\": \"sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"dependencies\": {\n \"is-glob\": \"^4.0.3\"\n },\n \"engines\": {\n \"node\": \">=10.13.0\"\n }\n },\n \"node_modules/globals\": {\n \"version\": \"14.0.0\",\n \"resolved\": \"https://registry.npmjs.org/globals/-/globals-14.0.0.tgz\",\n \"integrity\": \"sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=18\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/globalthis\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz\",\n \"integrity\": \"sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"define-properties\": \"^1.2.1\",\n \"gopd\": \"^1.0.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/gopd\": {\n \"version\": \"1.2.0\",\n \"resolved\": \"https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz\",\n \"integrity\": \"sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/graceful-fs\": {\n \"version\": \"4.2.11\",\n \"resolved\": \"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz\",\n \"integrity\": \"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\",\n \"dev\": true,\n \"license\": \"ISC\"\n },\n \"node_modules/has-bigints\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz\",\n \"integrity\": \"sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/has-flag\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz\",\n \"integrity\": \"sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/has-property-descriptors\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz\",\n \"integrity\": \"sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-define-property\": \"^1.0.0\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/has-proto\": {\n \"version\": \"1.2.0\",\n \"resolved\": \"https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz\",\n \"integrity\": \"sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"dunder-proto\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/has-symbols\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz\",\n \"integrity\": \"sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/has-tostringtag\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz\",\n \"integrity\": \"sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"has-symbols\": \"^1.0.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/hasown\": {\n \"version\": \"2.0.2\",\n \"resolved\": \"https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz\",\n \"integrity\": \"sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"function-bind\": \"^1.1.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/ignore\": {\n \"version\": \"5.3.2\",\n \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz\",\n \"integrity\": \"sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 4\"\n }\n },\n \"node_modules/import-fresh\": {\n \"version\": \"3.3.1\",\n \"resolved\": \"https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz\",\n \"integrity\": \"sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"parent-module\": \"^1.0.0\",\n \"resolve-from\": \"^4.0.0\"\n },\n \"engines\": {\n \"node\": \">=6\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/imurmurhash\": {\n \"version\": \"0.1.4\",\n \"resolved\": \"https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz\",\n \"integrity\": \"sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=0.8.19\"\n }\n },\n \"node_modules/internal-slot\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz\",\n \"integrity\": \"sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\",\n \"hasown\": \"^2.0.2\",\n \"side-channel\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/is-array-buffer\": {\n \"version\": \"3.0.5\",\n \"resolved\": \"https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz\",\n \"integrity\": \"sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.3\",\n \"get-intrinsic\": \"^1.2.6\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-arrayish\": {\n \"version\": \"0.3.4\",\n \"resolved\": \"https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz\",\n \"integrity\": \"sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==\",\n \"license\": \"MIT\",\n \"optional\": true\n },\n \"node_modules/is-async-function\": {\n \"version\": \"2.1.1\",\n \"resolved\": \"https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz\",\n \"integrity\": \"sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"async-function\": \"^1.0.0\",\n \"call-bound\": \"^1.0.3\",\n \"get-proto\": \"^1.0.1\",\n \"has-tostringtag\": \"^1.0.2\",\n \"safe-regex-test\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-bigint\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz\",\n \"integrity\": \"sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"has-bigints\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-boolean-object\": {\n \"version\": \"1.2.2\",\n \"resolved\": \"https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz\",\n \"integrity\": \"sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"has-tostringtag\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-bun-module\": {\n \"version\": \"2.0.0\",\n \"resolved\": \"https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz\",\n \"integrity\": \"sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"semver\": \"^7.7.1\"\n }\n },\n \"node_modules/is-callable\": {\n \"version\": \"1.2.7\",\n \"resolved\": \"https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz\",\n \"integrity\": \"sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-core-module\": {\n \"version\": \"2.16.1\",\n \"resolved\": \"https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz\",\n \"integrity\": \"sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"hasown\": \"^2.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-data-view\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz\",\n \"integrity\": \"sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"get-intrinsic\": \"^1.2.6\",\n \"is-typed-array\": \"^1.1.13\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-date-object\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz\",\n \"integrity\": \"sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"has-tostringtag\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-extglob\": {\n \"version\": \"2.1.1\",\n \"resolved\": \"https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz\",\n \"integrity\": \"sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/is-finalizationregistry\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz\",\n \"integrity\": \"sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-generator-function\": {\n \"version\": \"1.1.2\",\n \"resolved\": \"https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz\",\n \"integrity\": \"sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.4\",\n \"generator-function\": \"^2.0.0\",\n \"get-proto\": \"^1.0.1\",\n \"has-tostringtag\": \"^1.0.2\",\n \"safe-regex-test\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-glob\": {\n \"version\": \"4.0.3\",\n \"resolved\": \"https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz\",\n \"integrity\": \"sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"is-extglob\": \"^2.1.1\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/is-map\": {\n \"version\": \"2.0.3\",\n \"resolved\": \"https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz\",\n \"integrity\": \"sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-negative-zero\": {\n \"version\": \"2.0.3\",\n \"resolved\": \"https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz\",\n \"integrity\": \"sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-number\": {\n \"version\": \"7.0.0\",\n \"resolved\": \"https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz\",\n \"integrity\": \"sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=0.12.0\"\n }\n },\n \"node_modules/is-number-object\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz\",\n \"integrity\": \"sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"has-tostringtag\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-regex\": {\n \"version\": \"1.2.1\",\n \"resolved\": \"https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz\",\n \"integrity\": \"sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"gopd\": \"^1.2.0\",\n \"has-tostringtag\": \"^1.0.2\",\n \"hasown\": \"^2.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-set\": {\n \"version\": \"2.0.3\",\n \"resolved\": \"https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz\",\n \"integrity\": \"sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-shared-array-buffer\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz\",\n \"integrity\": \"sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-string\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz\",\n \"integrity\": \"sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"has-tostringtag\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-symbol\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz\",\n \"integrity\": \"sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"has-symbols\": \"^1.1.0\",\n \"safe-regex-test\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-typed-array\": {\n \"version\": \"1.1.15\",\n \"resolved\": \"https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz\",\n \"integrity\": \"sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"which-typed-array\": \"^1.1.16\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-weakmap\": {\n \"version\": \"2.0.2\",\n \"resolved\": \"https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz\",\n \"integrity\": \"sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-weakref\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz\",\n \"integrity\": \"sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/is-weakset\": {\n \"version\": \"2.0.4\",\n \"resolved\": \"https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz\",\n \"integrity\": \"sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"get-intrinsic\": \"^1.2.6\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/isarray\": {\n \"version\": \"2.0.5\",\n \"resolved\": \"https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz\",\n \"integrity\": \"sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/isexe\": {\n \"version\": \"2.0.0\",\n \"resolved\": \"https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz\",\n \"integrity\": \"sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==\",\n \"dev\": true,\n \"license\": \"ISC\"\n },\n \"node_modules/iterator.prototype\": {\n \"version\": \"1.1.5\",\n \"resolved\": \"https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz\",\n \"integrity\": \"sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"define-data-property\": \"^1.1.4\",\n \"es-object-atoms\": \"^1.0.0\",\n \"get-intrinsic\": \"^1.2.6\",\n \"get-proto\": \"^1.0.0\",\n \"has-symbols\": \"^1.1.0\",\n \"set-function-name\": \"^2.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/jiti\": {\n \"version\": \"2.6.1\",\n \"resolved\": \"https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz\",\n \"integrity\": \"sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"bin\": {\n \"jiti\": \"lib/jiti-cli.mjs\"\n }\n },\n \"node_modules/js-tokens\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz\",\n \"integrity\": \"sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/js-yaml\": {\n \"version\": \"4.1.1\",\n \"resolved\": \"https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz\",\n \"integrity\": \"sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"argparse\": \"^2.0.1\"\n },\n \"bin\": {\n \"js-yaml\": \"bin/js-yaml.js\"\n }\n },\n \"node_modules/json-buffer\": {\n \"version\": \"3.0.1\",\n \"resolved\": \"https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz\",\n \"integrity\": \"sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/json-schema-traverse\": {\n \"version\": \"0.4.1\",\n \"resolved\": \"https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz\",\n \"integrity\": \"sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/json-stable-stringify-without-jsonify\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz\",\n \"integrity\": \"sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/json5\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/json5/-/json5-1.0.2.tgz\",\n \"integrity\": \"sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"minimist\": \"^1.2.0\"\n },\n \"bin\": {\n \"json5\": \"lib/cli.js\"\n }\n },\n \"node_modules/jsx-ast-utils\": {\n \"version\": \"3.3.5\",\n \"resolved\": \"https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz\",\n \"integrity\": \"sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"array-includes\": \"^3.1.6\",\n \"array.prototype.flat\": \"^1.3.1\",\n \"object.assign\": \"^4.1.4\",\n \"object.values\": \"^1.1.6\"\n },\n \"engines\": {\n \"node\": \">=4.0\"\n }\n },\n \"node_modules/keyv\": {\n \"version\": \"4.5.4\",\n \"resolved\": \"https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz\",\n \"integrity\": \"sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"json-buffer\": \"3.0.1\"\n }\n },\n \"node_modules/language-subtag-registry\": {\n \"version\": \"0.3.23\",\n \"resolved\": \"https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz\",\n \"integrity\": \"sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==\",\n \"dev\": true,\n \"license\": \"CC0-1.0\"\n },\n \"node_modules/language-tags\": {\n \"version\": \"1.0.9\",\n \"resolved\": \"https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz\",\n \"integrity\": \"sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"language-subtag-registry\": \"^0.3.20\"\n },\n \"engines\": {\n \"node\": \">=0.10\"\n }\n },\n \"node_modules/levn\": {\n \"version\": \"0.4.1\",\n \"resolved\": \"https://registry.npmjs.org/levn/-/levn-0.4.1.tgz\",\n \"integrity\": \"sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"prelude-ls\": \"^1.2.1\",\n \"type-check\": \"~0.4.0\"\n },\n \"engines\": {\n \"node\": \">= 0.8.0\"\n }\n },\n \"node_modules/lightningcss\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz\",\n \"integrity\": \"sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==\",\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"dependencies\": {\n \"detect-libc\": \"^2.0.3\"\n },\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n },\n \"optionalDependencies\": {\n \"lightningcss-android-arm64\": \"1.32.0\",\n \"lightningcss-darwin-arm64\": \"1.32.0\",\n \"lightningcss-darwin-x64\": \"1.32.0\",\n \"lightningcss-freebsd-x64\": \"1.32.0\",\n \"lightningcss-linux-arm-gnueabihf\": \"1.32.0\",\n \"lightningcss-linux-arm64-gnu\": \"1.32.0\",\n \"lightningcss-linux-arm64-musl\": \"1.32.0\",\n \"lightningcss-linux-x64-gnu\": \"1.32.0\",\n \"lightningcss-linux-x64-musl\": \"1.32.0\",\n \"lightningcss-win32-arm64-msvc\": \"1.32.0\",\n \"lightningcss-win32-x64-msvc\": \"1.32.0\"\n }\n },\n \"node_modules/lightningcss-android-arm64\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz\",\n \"integrity\": \"sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"android\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-darwin-arm64\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz\",\n \"integrity\": \"sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-darwin-x64\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz\",\n \"integrity\": \"sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"darwin\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-freebsd-x64\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz\",\n \"integrity\": \"sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"freebsd\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-linux-arm-gnueabihf\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz\",\n \"integrity\": \"sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==\",\n \"cpu\": [\n \"arm\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-linux-arm64-gnu\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz\",\n \"integrity\": \"sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-linux-arm64-musl\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz\",\n \"integrity\": \"sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-linux-x64-gnu\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz\",\n \"integrity\": \"sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-linux-x64-musl\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz\",\n \"integrity\": \"sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"linux\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-win32-arm64-msvc\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz\",\n \"integrity\": \"sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==\",\n \"cpu\": [\n \"arm64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/lightningcss-win32-x64-msvc\": {\n \"version\": \"1.32.0\",\n \"resolved\": \"https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz\",\n \"integrity\": \"sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==\",\n \"cpu\": [\n \"x64\"\n ],\n \"dev\": true,\n \"license\": \"MPL-2.0\",\n \"optional\": true,\n \"os\": [\n \"win32\"\n ],\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/parcel\"\n }\n },\n \"node_modules/locate-path\": {\n \"version\": \"6.0.0\",\n \"resolved\": \"https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz\",\n \"integrity\": \"sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"p-locate\": \"^5.0.0\"\n },\n \"engines\": {\n \"node\": \">=10\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/lodash.merge\": {\n \"version\": \"4.6.2\",\n \"resolved\": \"https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz\",\n \"integrity\": \"sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/loose-envify\": {\n \"version\": \"1.4.0\",\n \"resolved\": \"https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz\",\n \"integrity\": \"sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"js-tokens\": \"^3.0.0 || ^4.0.0\"\n },\n \"bin\": {\n \"loose-envify\": \"cli.js\"\n }\n },\n \"node_modules/lucide-react\": {\n \"version\": \"0.563.0\",\n \"resolved\": \"https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz\",\n \"integrity\": \"sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==\",\n \"license\": \"ISC\",\n \"peerDependencies\": {\n \"react\": \"^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0\"\n }\n },\n \"node_modules/magic-string\": {\n \"version\": \"0.30.21\",\n \"resolved\": \"https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz\",\n \"integrity\": \"sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@jridgewell/sourcemap-codec\": \"^1.5.5\"\n }\n },\n \"node_modules/math-intrinsics\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz\",\n \"integrity\": \"sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/merge2\": {\n \"version\": \"1.4.1\",\n \"resolved\": \"https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz\",\n \"integrity\": \"sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 8\"\n }\n },\n \"node_modules/micromatch\": {\n \"version\": \"4.0.8\",\n \"resolved\": \"https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz\",\n \"integrity\": \"sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"braces\": \"^3.0.3\",\n \"picomatch\": \"^2.3.1\"\n },\n \"engines\": {\n \"node\": \">=8.6\"\n }\n },\n \"node_modules/minimatch\": {\n \"version\": \"3.1.5\",\n \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz\",\n \"integrity\": \"sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"dependencies\": {\n \"brace-expansion\": \"^1.1.7\"\n },\n \"engines\": {\n \"node\": \"*\"\n }\n },\n \"node_modules/minimist\": {\n \"version\": \"1.2.8\",\n \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz\",\n \"integrity\": \"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/ms\": {\n \"version\": \"2.1.3\",\n \"resolved\": \"https://registry.npmjs.org/ms/-/ms-2.1.3.tgz\",\n \"integrity\": \"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/nanoid\": {\n \"version\": \"3.3.11\",\n \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz\",\n \"integrity\": \"sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==\",\n \"funding\": [\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ],\n \"license\": \"MIT\",\n \"bin\": {\n \"nanoid\": \"bin/nanoid.cjs\"\n },\n \"engines\": {\n \"node\": \"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\"\n }\n },\n \"node_modules/napi-postinstall\": {\n \"version\": \"0.3.4\",\n \"resolved\": \"https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz\",\n \"integrity\": \"sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"bin\": {\n \"napi-postinstall\": \"lib/cli.js\"\n },\n \"engines\": {\n \"node\": \"^12.20.0 || ^14.18.0 || >=16.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/napi-postinstall\"\n }\n },\n \"node_modules/natural-compare\": {\n \"version\": \"1.4.0\",\n \"resolved\": \"https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz\",\n \"integrity\": \"sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/next\": {\n \"version\": \"15.2.4\",\n \"resolved\": \"https://registry.npmjs.org/next/-/next-15.2.4.tgz\",\n \"integrity\": \"sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==\",\n \"deprecated\": \"This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@next/env\": \"15.2.4\",\n \"@swc/counter\": \"0.1.3\",\n \"@swc/helpers\": \"0.5.15\",\n \"busboy\": \"1.6.0\",\n \"caniuse-lite\": \"^1.0.30001579\",\n \"postcss\": \"8.4.31\",\n \"styled-jsx\": \"5.1.6\"\n },\n \"bin\": {\n \"next\": \"dist/bin/next\"\n },\n \"engines\": {\n \"node\": \"^18.18.0 || ^19.8.0 || >= 20.0.0\"\n },\n \"optionalDependencies\": {\n \"@next/swc-darwin-arm64\": \"15.2.4\",\n \"@next/swc-darwin-x64\": \"15.2.4\",\n \"@next/swc-linux-arm64-gnu\": \"15.2.4\",\n \"@next/swc-linux-arm64-musl\": \"15.2.4\",\n \"@next/swc-linux-x64-gnu\": \"15.2.4\",\n \"@next/swc-linux-x64-musl\": \"15.2.4\",\n \"@next/swc-win32-arm64-msvc\": \"15.2.4\",\n \"@next/swc-win32-x64-msvc\": \"15.2.4\",\n \"sharp\": \"^0.33.5\"\n },\n \"peerDependencies\": {\n \"@opentelemetry/api\": \"^1.1.0\",\n \"@playwright/test\": \"^1.41.2\",\n \"babel-plugin-react-compiler\": \"*\",\n \"react\": \"^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0\",\n \"react-dom\": \"^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0\",\n \"sass\": \"^1.3.0\"\n },\n \"peerDependenciesMeta\": {\n \"@opentelemetry/api\": {\n \"optional\": true\n },\n \"@playwright/test\": {\n \"optional\": true\n },\n \"babel-plugin-react-compiler\": {\n \"optional\": true\n },\n \"sass\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/next/node_modules/postcss\": {\n \"version\": \"8.4.31\",\n \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz\",\n \"integrity\": \"sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==\",\n \"funding\": [\n {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/postcss/\"\n },\n {\n \"type\": \"tidelift\",\n \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n },\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ],\n \"license\": \"MIT\",\n \"dependencies\": {\n \"nanoid\": \"^3.3.6\",\n \"picocolors\": \"^1.0.0\",\n \"source-map-js\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \"^10 || ^12 || >=14\"\n }\n },\n \"node_modules/node-exports-info\": {\n \"version\": \"1.6.0\",\n \"resolved\": \"https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz\",\n \"integrity\": \"sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"array.prototype.flatmap\": \"^1.3.3\",\n \"es-errors\": \"^1.3.0\",\n \"object.entries\": \"^1.1.9\",\n \"semver\": \"^6.3.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/node-exports-info/node_modules/semver\": {\n \"version\": \"6.3.1\",\n \"resolved\": \"https://registry.npmjs.org/semver/-/semver-6.3.1.tgz\",\n \"integrity\": \"sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"bin\": {\n \"semver\": \"bin/semver.js\"\n }\n },\n \"node_modules/object-assign\": {\n \"version\": \"4.1.1\",\n \"resolved\": \"https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz\",\n \"integrity\": \"sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/object-inspect\": {\n \"version\": \"1.13.4\",\n \"resolved\": \"https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz\",\n \"integrity\": \"sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/object-keys\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz\",\n \"integrity\": \"sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/object.assign\": {\n \"version\": \"4.1.7\",\n \"resolved\": \"https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz\",\n \"integrity\": \"sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.3\",\n \"define-properties\": \"^1.2.1\",\n \"es-object-atoms\": \"^1.0.0\",\n \"has-symbols\": \"^1.1.0\",\n \"object-keys\": \"^1.1.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/object.entries\": {\n \"version\": \"1.1.9\",\n \"resolved\": \"https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz\",\n \"integrity\": \"sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.4\",\n \"define-properties\": \"^1.2.1\",\n \"es-object-atoms\": \"^1.1.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/object.fromentries\": {\n \"version\": \"2.0.8\",\n \"resolved\": \"https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz\",\n \"integrity\": \"sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.7\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.2\",\n \"es-object-atoms\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/object.groupby\": {\n \"version\": \"1.0.3\",\n \"resolved\": \"https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz\",\n \"integrity\": \"sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.7\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/object.values\": {\n \"version\": \"1.2.1\",\n \"resolved\": \"https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz\",\n \"integrity\": \"sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.3\",\n \"define-properties\": \"^1.2.1\",\n \"es-object-atoms\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/optionator\": {\n \"version\": \"0.9.4\",\n \"resolved\": \"https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz\",\n \"integrity\": \"sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"deep-is\": \"^0.1.3\",\n \"fast-levenshtein\": \"^2.0.6\",\n \"levn\": \"^0.4.1\",\n \"prelude-ls\": \"^1.2.1\",\n \"type-check\": \"^0.4.0\",\n \"word-wrap\": \"^1.2.5\"\n },\n \"engines\": {\n \"node\": \">= 0.8.0\"\n }\n },\n \"node_modules/own-keys\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz\",\n \"integrity\": \"sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"get-intrinsic\": \"^1.2.6\",\n \"object-keys\": \"^1.1.1\",\n \"safe-push-apply\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/p-limit\": {\n \"version\": \"3.1.0\",\n \"resolved\": \"https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz\",\n \"integrity\": \"sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"yocto-queue\": \"^0.1.0\"\n },\n \"engines\": {\n \"node\": \">=10\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/p-locate\": {\n \"version\": \"5.0.0\",\n \"resolved\": \"https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz\",\n \"integrity\": \"sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"p-limit\": \"^3.0.2\"\n },\n \"engines\": {\n \"node\": \">=10\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/parent-module\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz\",\n \"integrity\": \"sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"callsites\": \"^3.0.0\"\n },\n \"engines\": {\n \"node\": \">=6\"\n }\n },\n \"node_modules/path-exists\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz\",\n \"integrity\": \"sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/path-key\": {\n \"version\": \"3.1.1\",\n \"resolved\": \"https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz\",\n \"integrity\": \"sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/path-parse\": {\n \"version\": \"1.0.7\",\n \"resolved\": \"https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz\",\n \"integrity\": \"sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/picocolors\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz\",\n \"integrity\": \"sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==\",\n \"license\": \"ISC\"\n },\n \"node_modules/picomatch\": {\n \"version\": \"2.3.2\",\n \"resolved\": \"https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz\",\n \"integrity\": \"sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=8.6\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/jonschlinkert\"\n }\n },\n \"node_modules/possible-typed-array-names\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz\",\n \"integrity\": \"sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/postcss\": {\n \"version\": \"8.5.8\",\n \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz\",\n \"integrity\": \"sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==\",\n \"dev\": true,\n \"funding\": [\n {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/postcss/\"\n },\n {\n \"type\": \"tidelift\",\n \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n },\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/ai\"\n }\n ],\n \"license\": \"MIT\",\n \"dependencies\": {\n \"nanoid\": \"^3.3.11\",\n \"picocolors\": \"^1.1.1\",\n \"source-map-js\": \"^1.2.1\"\n },\n \"engines\": {\n \"node\": \"^10 || ^12 || >=14\"\n }\n },\n \"node_modules/prelude-ls\": {\n \"version\": \"1.2.1\",\n \"resolved\": \"https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz\",\n \"integrity\": \"sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.8.0\"\n }\n },\n \"node_modules/prop-types\": {\n \"version\": \"15.8.1\",\n \"resolved\": \"https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz\",\n \"integrity\": \"sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"loose-envify\": \"^1.4.0\",\n \"object-assign\": \"^4.1.1\",\n \"react-is\": \"^16.13.1\"\n }\n },\n \"node_modules/punycode\": {\n \"version\": \"2.3.1\",\n \"resolved\": \"https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz\",\n \"integrity\": \"sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=6\"\n }\n },\n \"node_modules/queue-microtask\": {\n \"version\": \"1.2.3\",\n \"resolved\": \"https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz\",\n \"integrity\": \"sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==\",\n \"dev\": true,\n \"funding\": [\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/feross\"\n },\n {\n \"type\": \"patreon\",\n \"url\": \"https://www.patreon.com/feross\"\n },\n {\n \"type\": \"consulting\",\n \"url\": \"https://feross.org/support\"\n }\n ],\n \"license\": \"MIT\"\n },\n \"node_modules/react\": {\n \"version\": \"19.2.3\",\n \"resolved\": \"https://registry.npmjs.org/react/-/react-19.2.3.tgz\",\n \"integrity\": \"sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==\",\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/react-dom\": {\n \"version\": \"19.2.3\",\n \"resolved\": \"https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz\",\n \"integrity\": \"sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"scheduler\": \"^0.27.0\"\n },\n \"peerDependencies\": {\n \"react\": \"^19.2.3\"\n }\n },\n \"node_modules/react-is\": {\n \"version\": \"16.13.1\",\n \"resolved\": \"https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz\",\n \"integrity\": \"sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/reflect.getprototypeof\": {\n \"version\": \"1.0.10\",\n \"resolved\": \"https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz\",\n \"integrity\": \"sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.9\",\n \"es-errors\": \"^1.3.0\",\n \"es-object-atoms\": \"^1.0.0\",\n \"get-intrinsic\": \"^1.2.7\",\n \"get-proto\": \"^1.0.1\",\n \"which-builtin-type\": \"^1.2.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/regexp.prototype.flags\": {\n \"version\": \"1.5.4\",\n \"resolved\": \"https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz\",\n \"integrity\": \"sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"define-properties\": \"^1.2.1\",\n \"es-errors\": \"^1.3.0\",\n \"get-proto\": \"^1.0.1\",\n \"gopd\": \"^1.2.0\",\n \"set-function-name\": \"^2.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/resolve\": {\n \"version\": \"1.22.11\",\n \"resolved\": \"https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz\",\n \"integrity\": \"sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"is-core-module\": \"^2.16.1\",\n \"path-parse\": \"^1.0.7\",\n \"supports-preserve-symlinks-flag\": \"^1.0.0\"\n },\n \"bin\": {\n \"resolve\": \"bin/resolve\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/resolve-from\": {\n \"version\": \"4.0.0\",\n \"resolved\": \"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz\",\n \"integrity\": \"sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=4\"\n }\n },\n \"node_modules/resolve-pkg-maps\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz\",\n \"integrity\": \"sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"funding\": {\n \"url\": \"https://github.com/privatenumber/resolve-pkg-maps?sponsor=1\"\n }\n },\n \"node_modules/reusify\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz\",\n \"integrity\": \"sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"iojs\": \">=1.0.0\",\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/run-parallel\": {\n \"version\": \"1.2.0\",\n \"resolved\": \"https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz\",\n \"integrity\": \"sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==\",\n \"dev\": true,\n \"funding\": [\n {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/feross\"\n },\n {\n \"type\": \"patreon\",\n \"url\": \"https://www.patreon.com/feross\"\n },\n {\n \"type\": \"consulting\",\n \"url\": \"https://feross.org/support\"\n }\n ],\n \"license\": \"MIT\",\n \"dependencies\": {\n \"queue-microtask\": \"^1.2.2\"\n }\n },\n \"node_modules/safe-array-concat\": {\n \"version\": \"1.1.3\",\n \"resolved\": \"https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz\",\n \"integrity\": \"sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.2\",\n \"get-intrinsic\": \"^1.2.6\",\n \"has-symbols\": \"^1.1.0\",\n \"isarray\": \"^2.0.5\"\n },\n \"engines\": {\n \"node\": \">=0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/safe-push-apply\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz\",\n \"integrity\": \"sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\",\n \"isarray\": \"^2.0.5\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/safe-regex-test\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz\",\n \"integrity\": \"sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"es-errors\": \"^1.3.0\",\n \"is-regex\": \"^1.2.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/scheduler\": {\n \"version\": \"0.27.0\",\n \"resolved\": \"https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz\",\n \"integrity\": \"sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==\",\n \"license\": \"MIT\"\n },\n \"node_modules/semver\": {\n \"version\": \"7.7.4\",\n \"resolved\": \"https://registry.npmjs.org/semver/-/semver-7.7.4.tgz\",\n \"integrity\": \"sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==\",\n \"devOptional\": true,\n \"license\": \"ISC\",\n \"bin\": {\n \"semver\": \"bin/semver.js\"\n },\n \"engines\": {\n \"node\": \">=10\"\n }\n },\n \"node_modules/set-function-length\": {\n \"version\": \"1.2.2\",\n \"resolved\": \"https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz\",\n \"integrity\": \"sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"define-data-property\": \"^1.1.4\",\n \"es-errors\": \"^1.3.0\",\n \"function-bind\": \"^1.1.2\",\n \"get-intrinsic\": \"^1.2.4\",\n \"gopd\": \"^1.0.1\",\n \"has-property-descriptors\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/set-function-name\": {\n \"version\": \"2.0.2\",\n \"resolved\": \"https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz\",\n \"integrity\": \"sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"define-data-property\": \"^1.1.4\",\n \"es-errors\": \"^1.3.0\",\n \"functions-have-names\": \"^1.2.3\",\n \"has-property-descriptors\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/set-proto\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz\",\n \"integrity\": \"sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"dunder-proto\": \"^1.0.1\",\n \"es-errors\": \"^1.3.0\",\n \"es-object-atoms\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/sharp\": {\n \"version\": \"0.33.5\",\n \"resolved\": \"https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz\",\n \"integrity\": \"sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==\",\n \"hasInstallScript\": true,\n \"license\": \"Apache-2.0\",\n \"optional\": true,\n \"dependencies\": {\n \"color\": \"^4.2.3\",\n \"detect-libc\": \"^2.0.3\",\n \"semver\": \"^7.6.3\"\n },\n \"engines\": {\n \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/libvips\"\n },\n \"optionalDependencies\": {\n \"@img/sharp-darwin-arm64\": \"0.33.5\",\n \"@img/sharp-darwin-x64\": \"0.33.5\",\n \"@img/sharp-libvips-darwin-arm64\": \"1.0.4\",\n \"@img/sharp-libvips-darwin-x64\": \"1.0.4\",\n \"@img/sharp-libvips-linux-arm\": \"1.0.5\",\n \"@img/sharp-libvips-linux-arm64\": \"1.0.4\",\n \"@img/sharp-libvips-linux-s390x\": \"1.0.4\",\n \"@img/sharp-libvips-linux-x64\": \"1.0.4\",\n \"@img/sharp-libvips-linuxmusl-arm64\": \"1.0.4\",\n \"@img/sharp-libvips-linuxmusl-x64\": \"1.0.4\",\n \"@img/sharp-linux-arm\": \"0.33.5\",\n \"@img/sharp-linux-arm64\": \"0.33.5\",\n \"@img/sharp-linux-s390x\": \"0.33.5\",\n \"@img/sharp-linux-x64\": \"0.33.5\",\n \"@img/sharp-linuxmusl-arm64\": \"0.33.5\",\n \"@img/sharp-linuxmusl-x64\": \"0.33.5\",\n \"@img/sharp-wasm32\": \"0.33.5\",\n \"@img/sharp-win32-ia32\": \"0.33.5\",\n \"@img/sharp-win32-x64\": \"0.33.5\"\n }\n },\n \"node_modules/shebang-command\": {\n \"version\": \"2.0.0\",\n \"resolved\": \"https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz\",\n \"integrity\": \"sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"shebang-regex\": \"^3.0.0\"\n },\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/shebang-regex\": {\n \"version\": \"3.0.0\",\n \"resolved\": \"https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz\",\n \"integrity\": \"sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/side-channel\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz\",\n \"integrity\": \"sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\",\n \"object-inspect\": \"^1.13.3\",\n \"side-channel-list\": \"^1.0.0\",\n \"side-channel-map\": \"^1.0.1\",\n \"side-channel-weakmap\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/side-channel-list\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz\",\n \"integrity\": \"sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\",\n \"object-inspect\": \"^1.13.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/side-channel-map\": {\n \"version\": \"1.0.1\",\n \"resolved\": \"https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz\",\n \"integrity\": \"sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"es-errors\": \"^1.3.0\",\n \"get-intrinsic\": \"^1.2.5\",\n \"object-inspect\": \"^1.13.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/side-channel-weakmap\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz\",\n \"integrity\": \"sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"es-errors\": \"^1.3.0\",\n \"get-intrinsic\": \"^1.2.5\",\n \"object-inspect\": \"^1.13.3\",\n \"side-channel-map\": \"^1.0.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/simple-swizzle\": {\n \"version\": \"0.2.4\",\n \"resolved\": \"https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz\",\n \"integrity\": \"sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==\",\n \"license\": \"MIT\",\n \"optional\": true,\n \"dependencies\": {\n \"is-arrayish\": \"^0.3.1\"\n }\n },\n \"node_modules/source-map-js\": {\n \"version\": \"1.2.1\",\n \"resolved\": \"https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz\",\n \"integrity\": \"sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==\",\n \"license\": \"BSD-3-Clause\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/stable-hash\": {\n \"version\": \"0.0.5\",\n \"resolved\": \"https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz\",\n \"integrity\": \"sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/stop-iteration-iterator\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz\",\n \"integrity\": \"sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"es-errors\": \"^1.3.0\",\n \"internal-slot\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/streamsearch\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz\",\n \"integrity\": \"sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==\",\n \"engines\": {\n \"node\": \">=10.0.0\"\n }\n },\n \"node_modules/string.prototype.includes\": {\n \"version\": \"2.0.1\",\n \"resolved\": \"https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz\",\n \"integrity\": \"sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.7\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/string.prototype.matchall\": {\n \"version\": \"4.0.12\",\n \"resolved\": \"https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz\",\n \"integrity\": \"sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.3\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.6\",\n \"es-errors\": \"^1.3.0\",\n \"es-object-atoms\": \"^1.0.0\",\n \"get-intrinsic\": \"^1.2.6\",\n \"gopd\": \"^1.2.0\",\n \"has-symbols\": \"^1.1.0\",\n \"internal-slot\": \"^1.1.0\",\n \"regexp.prototype.flags\": \"^1.5.3\",\n \"set-function-name\": \"^2.0.2\",\n \"side-channel\": \"^1.1.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/string.prototype.repeat\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz\",\n \"integrity\": \"sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"define-properties\": \"^1.1.3\",\n \"es-abstract\": \"^1.17.5\"\n }\n },\n \"node_modules/string.prototype.trim\": {\n \"version\": \"1.2.10\",\n \"resolved\": \"https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz\",\n \"integrity\": \"sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.2\",\n \"define-data-property\": \"^1.1.4\",\n \"define-properties\": \"^1.2.1\",\n \"es-abstract\": \"^1.23.5\",\n \"es-object-atoms\": \"^1.0.0\",\n \"has-property-descriptors\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/string.prototype.trimend\": {\n \"version\": \"1.0.9\",\n \"resolved\": \"https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz\",\n \"integrity\": \"sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.2\",\n \"define-properties\": \"^1.2.1\",\n \"es-object-atoms\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/string.prototype.trimstart\": {\n \"version\": \"1.0.8\",\n \"resolved\": \"https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz\",\n \"integrity\": \"sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.7\",\n \"define-properties\": \"^1.2.1\",\n \"es-object-atoms\": \"^1.0.0\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/strip-bom\": {\n \"version\": \"3.0.0\",\n \"resolved\": \"https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz\",\n \"integrity\": \"sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=4\"\n }\n },\n \"node_modules/strip-json-comments\": {\n \"version\": \"3.1.1\",\n \"resolved\": \"https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz\",\n \"integrity\": \"sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=8\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n },\n \"node_modules/styled-jsx\": {\n \"version\": \"5.1.6\",\n \"resolved\": \"https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz\",\n \"integrity\": \"sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"client-only\": \"0.0.1\"\n },\n \"engines\": {\n \"node\": \">= 12.0.0\"\n },\n \"peerDependencies\": {\n \"react\": \">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0\"\n },\n \"peerDependenciesMeta\": {\n \"@babel/core\": {\n \"optional\": true\n },\n \"babel-plugin-macros\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/supports-color\": {\n \"version\": \"7.2.0\",\n \"resolved\": \"https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz\",\n \"integrity\": \"sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"has-flag\": \"^4.0.0\"\n },\n \"engines\": {\n \"node\": \">=8\"\n }\n },\n \"node_modules/supports-preserve-symlinks-flag\": {\n \"version\": \"1.0.0\",\n \"resolved\": \"https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz\",\n \"integrity\": \"sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/tailwind-merge\": {\n \"version\": \"3.5.0\",\n \"resolved\": \"https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz\",\n \"integrity\": \"sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==\",\n \"license\": \"MIT\",\n \"funding\": {\n \"type\": \"github\",\n \"url\": \"https://github.com/sponsors/dcastil\"\n }\n },\n \"node_modules/tailwindcss\": {\n \"version\": \"4.2.2\",\n \"resolved\": \"https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz\",\n \"integrity\": \"sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/tapable\": {\n \"version\": \"2.3.2\",\n \"resolved\": \"https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz\",\n \"integrity\": \"sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=6\"\n },\n \"funding\": {\n \"type\": \"opencollective\",\n \"url\": \"https://opencollective.com/webpack\"\n }\n },\n \"node_modules/tinyglobby\": {\n \"version\": \"0.2.15\",\n \"resolved\": \"https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz\",\n \"integrity\": \"sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"fdir\": \"^6.5.0\",\n \"picomatch\": \"^4.0.3\"\n },\n \"engines\": {\n \"node\": \">=12.0.0\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/SuperchupuDev\"\n }\n },\n \"node_modules/tinyglobby/node_modules/fdir\": {\n \"version\": \"6.5.0\",\n \"resolved\": \"https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz\",\n \"integrity\": \"sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=12.0.0\"\n },\n \"peerDependencies\": {\n \"picomatch\": \"^3 || ^4\"\n },\n \"peerDependenciesMeta\": {\n \"picomatch\": {\n \"optional\": true\n }\n }\n },\n \"node_modules/tinyglobby/node_modules/picomatch\": {\n \"version\": \"4.0.4\",\n \"resolved\": \"https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz\",\n \"integrity\": \"sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=12\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/jonschlinkert\"\n }\n },\n \"node_modules/to-regex-range\": {\n \"version\": \"5.0.1\",\n \"resolved\": \"https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz\",\n \"integrity\": \"sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"is-number\": \"^7.0.0\"\n },\n \"engines\": {\n \"node\": \">=8.0\"\n }\n },\n \"node_modules/ts-api-utils\": {\n \"version\": \"2.5.0\",\n \"resolved\": \"https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz\",\n \"integrity\": \"sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=18.12\"\n },\n \"peerDependencies\": {\n \"typescript\": \">=4.8.4\"\n }\n },\n \"node_modules/tsconfig-paths\": {\n \"version\": \"3.15.0\",\n \"resolved\": \"https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz\",\n \"integrity\": \"sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@types/json5\": \"^0.0.29\",\n \"json5\": \"^1.0.2\",\n \"minimist\": \"^1.2.6\",\n \"strip-bom\": \"^3.0.0\"\n }\n },\n \"node_modules/tslib\": {\n \"version\": \"2.8.1\",\n \"resolved\": \"https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz\",\n \"integrity\": \"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==\",\n \"license\": \"0BSD\"\n },\n \"node_modules/type-check\": {\n \"version\": \"0.4.0\",\n \"resolved\": \"https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz\",\n \"integrity\": \"sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"prelude-ls\": \"^1.2.1\"\n },\n \"engines\": {\n \"node\": \">= 0.8.0\"\n }\n },\n \"node_modules/typed-array-buffer\": {\n \"version\": \"1.0.3\",\n \"resolved\": \"https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz\",\n \"integrity\": \"sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"es-errors\": \"^1.3.0\",\n \"is-typed-array\": \"^1.1.14\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n }\n },\n \"node_modules/typed-array-byte-length\": {\n \"version\": \"1.0.3\",\n \"resolved\": \"https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz\",\n \"integrity\": \"sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.8\",\n \"for-each\": \"^0.3.3\",\n \"gopd\": \"^1.2.0\",\n \"has-proto\": \"^1.2.0\",\n \"is-typed-array\": \"^1.1.14\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/typed-array-byte-offset\": {\n \"version\": \"1.0.4\",\n \"resolved\": \"https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz\",\n \"integrity\": \"sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"available-typed-arrays\": \"^1.0.7\",\n \"call-bind\": \"^1.0.8\",\n \"for-each\": \"^0.3.3\",\n \"gopd\": \"^1.2.0\",\n \"has-proto\": \"^1.2.0\",\n \"is-typed-array\": \"^1.1.15\",\n \"reflect.getprototypeof\": \"^1.0.9\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/typed-array-length\": {\n \"version\": \"1.0.7\",\n \"resolved\": \"https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz\",\n \"integrity\": \"sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bind\": \"^1.0.7\",\n \"for-each\": \"^0.3.3\",\n \"gopd\": \"^1.0.1\",\n \"is-typed-array\": \"^1.1.13\",\n \"possible-typed-array-names\": \"^1.0.0\",\n \"reflect.getprototypeof\": \"^1.0.6\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/typescript\": {\n \"version\": \"5.9.3\",\n \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz\",\n \"integrity\": \"sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==\",\n \"dev\": true,\n \"license\": \"Apache-2.0\",\n \"bin\": {\n \"tsc\": \"bin/tsc\",\n \"tsserver\": \"bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=14.17\"\n }\n },\n \"node_modules/unbox-primitive\": {\n \"version\": \"1.1.0\",\n \"resolved\": \"https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz\",\n \"integrity\": \"sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.3\",\n \"has-bigints\": \"^1.0.2\",\n \"has-symbols\": \"^1.1.0\",\n \"which-boxed-primitive\": \"^1.1.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/undici-types\": {\n \"version\": \"6.21.0\",\n \"resolved\": \"https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz\",\n \"integrity\": \"sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==\",\n \"dev\": true,\n \"license\": \"MIT\"\n },\n \"node_modules/unrs-resolver\": {\n \"version\": \"1.11.1\",\n \"resolved\": \"https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz\",\n \"integrity\": \"sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==\",\n \"dev\": true,\n \"hasInstallScript\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"napi-postinstall\": \"^0.3.0\"\n },\n \"funding\": {\n \"url\": \"https://opencollective.com/unrs-resolver\"\n },\n \"optionalDependencies\": {\n \"@unrs/resolver-binding-android-arm-eabi\": \"1.11.1\",\n \"@unrs/resolver-binding-android-arm64\": \"1.11.1\",\n \"@unrs/resolver-binding-darwin-arm64\": \"1.11.1\",\n \"@unrs/resolver-binding-darwin-x64\": \"1.11.1\",\n \"@unrs/resolver-binding-freebsd-x64\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-arm-gnueabihf\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-arm-musleabihf\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-arm64-gnu\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-arm64-musl\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-ppc64-gnu\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-riscv64-gnu\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-riscv64-musl\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-s390x-gnu\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-x64-gnu\": \"1.11.1\",\n \"@unrs/resolver-binding-linux-x64-musl\": \"1.11.1\",\n \"@unrs/resolver-binding-wasm32-wasi\": \"1.11.1\",\n \"@unrs/resolver-binding-win32-arm64-msvc\": \"1.11.1\",\n \"@unrs/resolver-binding-win32-ia32-msvc\": \"1.11.1\",\n \"@unrs/resolver-binding-win32-x64-msvc\": \"1.11.1\"\n }\n },\n \"node_modules/uri-js\": {\n \"version\": \"4.4.1\",\n \"resolved\": \"https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz\",\n \"integrity\": \"sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==\",\n \"dev\": true,\n \"license\": \"BSD-2-Clause\",\n \"dependencies\": {\n \"punycode\": \"^2.1.0\"\n }\n },\n \"node_modules/which\": {\n \"version\": \"2.0.2\",\n \"resolved\": \"https://registry.npmjs.org/which/-/which-2.0.2.tgz\",\n \"integrity\": \"sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==\",\n \"dev\": true,\n \"license\": \"ISC\",\n \"dependencies\": {\n \"isexe\": \"^2.0.0\"\n },\n \"bin\": {\n \"node-which\": \"bin/node-which\"\n },\n \"engines\": {\n \"node\": \">= 8\"\n }\n },\n \"node_modules/which-boxed-primitive\": {\n \"version\": \"1.1.1\",\n \"resolved\": \"https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz\",\n \"integrity\": \"sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"is-bigint\": \"^1.1.0\",\n \"is-boolean-object\": \"^1.2.1\",\n \"is-number-object\": \"^1.1.1\",\n \"is-string\": \"^1.1.1\",\n \"is-symbol\": \"^1.1.1\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/which-builtin-type\": {\n \"version\": \"1.2.1\",\n \"resolved\": \"https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz\",\n \"integrity\": \"sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"call-bound\": \"^1.0.2\",\n \"function.prototype.name\": \"^1.1.6\",\n \"has-tostringtag\": \"^1.0.2\",\n \"is-async-function\": \"^2.0.0\",\n \"is-date-object\": \"^1.1.0\",\n \"is-finalizationregistry\": \"^1.1.0\",\n \"is-generator-function\": \"^1.0.10\",\n \"is-regex\": \"^1.2.1\",\n \"is-weakref\": \"^1.0.2\",\n \"isarray\": \"^2.0.5\",\n \"which-boxed-primitive\": \"^1.1.0\",\n \"which-collection\": \"^1.0.2\",\n \"which-typed-array\": \"^1.1.16\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/which-collection\": {\n \"version\": \"1.0.2\",\n \"resolved\": \"https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz\",\n \"integrity\": \"sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"is-map\": \"^2.0.3\",\n \"is-set\": \"^2.0.3\",\n \"is-weakmap\": \"^2.0.2\",\n \"is-weakset\": \"^2.0.3\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/which-typed-array\": {\n \"version\": \"1.1.20\",\n \"resolved\": \"https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz\",\n \"integrity\": \"sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"dependencies\": {\n \"available-typed-arrays\": \"^1.0.7\",\n \"call-bind\": \"^1.0.8\",\n \"call-bound\": \"^1.0.4\",\n \"for-each\": \"^0.3.5\",\n \"get-proto\": \"^1.0.1\",\n \"gopd\": \"^1.2.0\",\n \"has-tostringtag\": \"^1.0.2\"\n },\n \"engines\": {\n \"node\": \">= 0.4\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/ljharb\"\n }\n },\n \"node_modules/word-wrap\": {\n \"version\": \"1.2.5\",\n \"resolved\": \"https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz\",\n \"integrity\": \"sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=0.10.0\"\n }\n },\n \"node_modules/yocto-queue\": {\n \"version\": \"0.1.0\",\n \"resolved\": \"https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz\",\n \"integrity\": \"sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==\",\n \"dev\": true,\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=10\"\n },\n \"funding\": {\n \"url\": \"https://github.com/sponsors/sindresorhus\"\n }\n }\n }\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":211989,"content_sha256":"748ce339d1482e9f1ec03ac07c4c7f25b0540eab1b0590c737f8c52f2b252d58"},{"filename":"aigc-claw/frontend/package.json","content":"{\n \"name\": \"aigc-claw\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \"next build --no-lint\",\n \"start\": \"next start\",\n \"lint\": \"eslint\"\n },\n \"dependencies\": {\n \"clsx\": \"^2.1.1\",\n \"lucide-react\": \"^0.563.0\",\n \"next\": \"15.2.4\",\n \"react\": \"19.2.3\",\n \"react-dom\": \"19.2.3\",\n \"tailwind-merge\": \"^3.4.0\"\n },\n \"devDependencies\": {\n \"@tailwindcss/postcss\": \"^4\",\n \"@types/node\": \"^20\",\n \"@types/react\": \"^19\",\n \"@types/react-dom\": \"^19\",\n \"eslint\": \"^9\",\n \"eslint-config-next\": \"15.2.4\",\n \"tailwindcss\": \"^4\",\n \"typescript\": \"^5\"\n }\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":627,"content_sha256":"285ed3a553892949095a4a288e0d56e86a03d188b1deba9274214f3b4346f5d3"},{"filename":"aigc-claw/frontend/postcss.config.mjs","content":"const config = {\n plugins: {\n \"@tailwindcss/postcss\": {},\n },\n};\n\nexport default config;\n","content_type":"text/javascript","language":"javascript","size":94,"content_sha256":"dfac7ac2d86d326a0e5adb024e7943c181393ed17a5fcb8f0315b24c7da6ddde"},{"filename":"aigc-claw/frontend/README.md","content":"This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1450,"content_sha256":"60b55ff7df79af72590f9524208e46642bc32bdc175cdad41349681c0e2f958f"},{"filename":"aigc-claw/frontend/tsconfig.json","content":"{\n \"compilerOptions\": {\n \"target\": \"ES2017\",\n \"lib\": [\n \"dom\",\n \"dom.iterable\",\n \"esnext\"\n ],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n \"strict\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"preserve\",\n \"incremental\": true,\n \"plugins\": [\n {\n \"name\": \"next\"\n }\n ],\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ]\n }\n },\n \"include\": [\n \"next-env.d.ts\",\n \"**/*.ts\",\n \"**/*.tsx\",\n \".next/types/**/*.ts\",\n \".next/dev/types/**/*.ts\",\n \"**/*.mts\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":713,"content_sha256":"81f0aa4d1b85baaf7d65f8c6949d7f55a6c234a4bce00a1ce349362dea87ca8f"},{"filename":"CLAUDE.md","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nAIGC-Claw is an AI video generation system that transforms user ideas into complete videos through 6 stages: Script → Character/Scene Design → Storyboard → Reference Images → Video Generation → Post-production.\n\nThis repository (`aigc-director/`) is an **OpenClaw Agent Skill** that wraps the actual code project:\n- **aigc-claw/**: The actual code project containing the backend (Python FastAPI) and frontend (Next.js)\n- **SKILL.md**: Workflow rules for the OpenClaw agent\n- **references/**: API documentation\n\nBoth run locally: backend at `http://localhost:8000`, frontend at `http://localhost:3000`.\n\n## Commands\n\n### Backend\n```bash\ncd aigc-claw/backend\nsource venv/bin/activate\npython api_server.py\n```\n\n### Frontend\n```bash\ncd aigc-claw/frontend\nnpm install # first time only\nnpm run build\nnpm start\n```\n\n### Health Check\n```bash\ncurl http://localhost:8000/api/health\n```\n\n## Architecture\n\n### Backend Core\n- **[orchestrator.py](aigc-claw/backend/core/orchestrator.py)**: Workflow engine managing the 6-stage state machine. Controls session state (idle/running/waiting_in_stage/stage_completed/session_completed), persists to `aigc-claw/backend/code/data/sessions/`, and coordinates agent execution.\n\n- **[base_agent.py](aigc-claw/backend/core/agents/base_agent.py)**: Abstract base class for all stage agents. Defines `process(input_data, intervention)` interface that returns `{\"payload\": ..., \"requires_intervention\": bool, \"stage_completed\": bool}`.\n\n### 6 Stage Agents\nEach agent handles one workflow stage:\n| Agent | File | Stage |\n|-------|------|-------|\n| ScriptWriterAgent | script_agent.py | script_generation |\n| CharacterDesignerAgent | character_agent.py | character_design |\n| StoryboardAgent | storyboard_agent.py | storyboard |\n| ReferenceGeneratorAgent | reference_agent.py | reference_generation |\n| VideoDirectorAgent | video_agent.py | video_generation |\n| VideoEditorAgent | editor_agent.py | post_production |\n\n### Tool Clients\nExternal API integrations in `aigc-claw/backend/tool/`:\n- **LLM clients**: llm_dashscope.py, llm_deepseek.py, llm_gpt.py, llm_gemini.py\n- **Image clients**: image_dashscope.py, image_client.py (Seedream, Jimeng, Wan)\n- **Video clients**: video_wan.py, video_kling.py (Wan, Kling)\n- **VLM clients**: vlm_dashscope.py, vlm_gemini.py\n\n### Data Storage\n- Results: `aigc-claw/backend/code/result/` (image/, video/, script/)\n- Session state: `aigc-claw/backend/code/data/sessions/{session_id}.json`\n\n## Workflow (from SKILL.md)\n\nThe system uses **9 stop points** where the agent MUST pause and wait for user confirmation before proceeding:\n\n1. Project config confirmation\n2. Script suggest_expand (optional)\n3. Script logline selection\n4. Script mode selection (movie/micro-film)\n5. Script generation confirmation\n6. Character/scene design confirmation\n7. Storyboard confirmation\n8. Reference image confirmation\n9. Video clip confirmation\n\nAfter each stage completes, the agent must:\n1. Get artifact via `GET /api/project/{session_id}/artifact/{stage}`\n2. Present results to user\n3. Wait for user confirmation\n4. Call `POST /api/project/{session_id}/continue` to proceed\n\n## Key References\n\nThe `references/` folder contains detailed API documentation:\n- `run_project/` - Service startup instructions\n- `workflow/` - 6-stage workflow API docs\n- `sandbox/` - Single-shot tools (image generation, video generation)\n- `send_message/` - Feishu/WeChat integration\n\nImportant files:\n- [SKILL.md](SKILL.md) - Contains the complete workflow rules for OpenClaw agent execution\n- [README.md](README.md) - Full project documentation including model configuration","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3750,"content_sha256":"210722330a7c8188f54fb45a51f847515dfe7f312d36a0c1984b0466cca1253b"},{"filename":"README.md","content":"# 🎬 AIGC-Claw\n\nAI 视频生成全流程系统,通过 6 个阶段将用户想法转化为完整视频。\n\n## 功能特性\n\n- **剧本生成**:输入创意自动生成结构化剧本\n- **角色设计**:AI 生成角色设定图(四视图)\n- **场景设计**:自动生成场景背景图\n- **分镜设计**:智能拆分镜头脚本\n- **参考图生成**:为每个镜头生成高精度参考图\n- **视频生成**:文生视频 / 图生视频\n- **后期剪辑**:自动拼接视频片段,添加转场\n\n## 环境要求\n\n- **Python**: 3.9+\n- **Node.js**: 18+\n- **npm**: 9+\n\n## 技术栈\n\n- **前端**:Next.js 14 + TypeScript + Tailwind CSS\n- **后端**:Python FastAPI\n- **AI 模型**:阿里云 DashScope (Qwen)、字节跳动 Seedream、即梦 Jimeng、快手可灵 Kling、DeepSeek、OpenAI、Google Gemini\n\n## 快速开始\n\n### 方式一:手动安装\n\n#### 1. 克隆项目\n\n```bash\ngit clone https://github.com/hit-cxf/AIGC-Claw.git\ncd AIGC-Claw # 完整项目根目录(包括FilmAgent和aigc-director)\n```\n\n#### 2. 配置并启动后端\n\n先确保进入完整项目目录 `AIGC-Claw`\n此时目录下应当有 `aigc-director` 和 `FilmAgent` 两个子目录\n\n##### 配置后端\n\n```bash\ncd aigc-director # skill目录\ncd aigc-claw # 项目目录\ncd backend # 后端目录\n\n# 创建虚拟环境\npython -m venv venv\n\n# 根据操作系统选择启动虚拟环境的命令\nsource venv/bin/activate # Linux/Mac\n.\\venv\\Scripts\\activate # Windows\n\n# 安装依赖\npip install -r requirements.txt\n\n# 配置环境变量\ncp .env.example .env\n# 编辑 .env 填入 API Key\n# 支持的模型见“配置说明”部分\n```\n\n##### 启动后端\n\n```bash\n# 根据操作系统选择启动虚拟环境的命令\nsource venv/bin/activate # Linux/Mac\n.\\venv\\Scripts\\activate # Windows\n\npython api_server.py\n# 服务运行在 http://localhost:8000\n```\n\n终端显示\n\n```\nINFO: Application startup complete.\nINFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n```\n\n说明启动成功。保持当前终端运行,新建终端以启动前端\n\n#### 3. 配置并启动前端\n\n在新的终端下完成配置\n先确保进入完整项目目录 `AIGC-Claw`\n此时目录下应当有 `aigc-director` 和 `FilmAgent` 两个子目录\n\n##### 配置前端\n\n```bash\ncd aigc-director # skill目录\ncd aigc-claw # 项目目录\ncd frontend\nnpm install\n# 首次启动或代码变更后需要 build\nnpm run build\n# 启动生产服务(开销小,只需 build 一次)\n```\n\n##### 启动前端\n\n```bash\nnpm start\n# 访问 http://localhost:3000\n```\n\n---\n\n### 方式二:OpenClaw 自动配置\n\n向openclaw发送消息:\n\n```\n帮我克隆git仓库:https://github.com/hit-cxf/AIGC-Claw.git\n然后把AIGC-Claw中的aigc-director文件夹递归复制到workspace/skills中,用作AIGC相关的skill\n复制完成后,检查aigc-director是否加载到了技能列表中\n```\n\n之后使用时,建议在向openclaw发送指令的同时,指明“使用aigc-director”,如:\n\n```\n你用aigc-director来帮我生成一个视频,内容是“一条狗的使命”\n```\n\n## 项目结构\n\n```\naigc-director/ # OpenClaw Agent Skill(供 OpenClaw 调用的 AI 视频制作助手)\n├── SKILL.md # Agent 工作流规则定义\n├── CLAUDE.md # Claude Code 开发指引\n├── README.md # 项目说明\n├── references/ # API 参考文档\n│ ├── run_project/ # 服务启动指南\n│ ├── workflow/ # 六阶段工作流 API 文档\n│ ├── sandbox/ # 临时工作台 API 文档\n│ └── send_message/ # 消息推送集成\n└── aigc-claw/ # 实际代码项目\n ├── backend/ # Python FastAPI 后端\n │ ├── api_server.py # API 入口\n │ ├── config.py # 配置管理\n │ ├── core/\n │ │ ├── orchestrator.py # 工作流引擎\n │ │ └── agents/ # 6 个阶段 Agent\n │ │ ├── script_agent.py # 剧本生成\n │ │ ├── character_agent.py # 角色设计\n │ │ ├── storyboard_agent.py # 分镜设计\n │ │ ├── reference_agent.py # 参考图生成\n │ │ ├── video_agent.py # 视频生成\n │ │ └── editor_agent.py # 后期剪辑\n │ └── tool/ # 外部 API 客户端\n └── frontend/ # Next.js 前端\n ├── app/ # App Router 页面\n ├── components/ # React 组件\n └── config/ # 配置文件\n```\n\n> **注意**:整个 AI 视频生成系统代码在 `aigc-claw/` 子目录中,`aigc-director/` 目录是提供给 OpenClaw 平台调用的 Skill 包装。\n\n## 工作流阶段\n\n| 阶段 | Agent | 说明 |\n|------|-------|------|\n| 1 | 剧本生成 | 将灵感转化为结构化剧本 |\n| 2 | 角色设计 | 生成角色设计图和场景背景 |\n| 3 | 分镜设计 | 设计镜头语言和分镜脚本 |\n| 4 | 参考图生成 | 生成高精度参考图 |\n| 5 | 视频生成 | 将参考图转化为视频 |\n| 6 | 后期剪辑 | 拼接视频片段为最终成片 |\n\n## 数据存储\n\n### 产物存储位置\n\n所有生成的资产存储在 `aigc-claw/backend/code/result/` 目录下:\n\n| 目录 | 说明 |\n|------|------|\n| `code/result/image/{session_id}/` | 角色/场景/参考图 |\n| `code/result/video/{session_id}/` | 视频片段 |\n| `code/result/sandbox/` | 临时工作台生成的文件 |\n| `code/result/script/` | LLM 生成的剧本初始数据 |\n\n### 会话数据存储\n\n会话状态和产物元数据存储在 `aigc-claw/backend/code/data/sessions/` 目录下:\n\n- `{session_id}.json` - 包含会话状态、已完成阶段、产物信息等\n\n### 数据读取优先级\n\n1. **会话数据** (`sessions/`) - 用户修改和当前状态(权威数据)\n2. **剧本数据** (`result/script/`) - LLM 生成的初始数据\n\nAPI 返回的资产路径使用相对路径格式:`code/result/...`\n\n## API 接口\n\n| 接口 | 方法 | 说明 |\n|------|------|------|\n| `/api/project/start` | POST | 创建新项目 |\n| `/api/project/{session_id}/execute/{stage}` | POST | 执行指定阶段 |\n| `/api/project/{session_id}/status` | GET | 获取项目状态 |\n| `/api/project/{session_id}/artifact/{stage}` | GET | 获取阶段产物 |\n| `/api/project/{session_id}/intervene` | POST | 干预阶段 |\n| `/api/project/{session_id}/continue` | POST | 确认并继续 |\n| `/api/project/{session_id}/stop` | POST | 停止执行 |\n| `/api/sessions` | GET | 获取会话列表 |\n| `/api/stages` | GET | 获取阶段列表 |\n\n## 配置说明\n\n### 后端环境变量\n\n主要配置项(详见 `aigc-claw/backend/.env`):\n\n```bash\n# LLM 配置(剧本生成)\nLLM_MODEL=qwen3.5-plus\n\n# VLM 配置(图像评估)\nVLM_MODEL=qwen-vl-plus\n\n# 图像生成(默认:doubao-seedream-5-0-260128,支持高并发)\nIMAGE_T2I_MODEL=doubao-seedream-5-0-260128\nIMAGE_IT2I_MODEL=doubao-seedream-5-0-260128\n\n# 视频生成\nVIDEO_MODEL=wan2.6-i2v-flash\nVIDEO_RATIO=16:9\n```\n\n### API Keys 配置\n\n在 `aigc-claw/backend/.env` 中配置各平台 API Key:\n\n| API Key | 提供商 | 可用模型 |\n|---------|------|---------|\n| `DASHSCOPE_API_KEY` | 阿里云DashScope | qwen3.5-plus, qwen-vl-plus, wan2.6-t2i, wan2.6-i2v-flash |\n| `ARK_API_KEY` | 字节跳动Seedream | doubao-seedream-5-0-260128 (500次/分钟,高并发) |\n| `VOLC_ACCESS_KEY/SECRET` | 火山引擎即梦 | jimeng_t2i_v40, jimeng_ti2v_v30_pro |\n| `KLING_ACCESS_KEY/SECRET` | 快手可灵 | kling-v3, kling-v2-6 |\n| `DEEPSEEK_API_KEY` | DeepSeek | deepseek-chat, deepseek-reasoner |\n| `OPENAI_API_KEY` | OpenAI | gpt-4o, gpt-5, o3 |\n| `GEMINI_API_KEY` | Google Gemini | gemini-2.5-flash, gemini-2.5-flash-image |\n\n### 可用模型\n\n- **LLM 模型**: deepseek-chat, deepseek-reasoner, gpt-4o, gpt-4, gpt-5, o3, gemini-3-flash-preview, qwen3.5-plus, qwen3.5-max\n- **VLM 评估模型**: qwen3.5-plus, qwen-vl-plus, qwen3.5-max, gemini-2.5-flash-image (性价比最高), gemini-2.0-flash\n- **文生图模型**: doubao-seedream-5-0-260128, jimeng_t2i_v40, wan2.6-t2i, sora_image, gpt-image-1.5\n- **图生图模型**: doubao-seedream-5-0-260128, jimeng_t2i_v40, wan2.6-image\n- **视频生成模型**: wan2.6-i2v-flash, kling-v3, kling-v2-6, kling-v2-5-turbo\n- **视频比例**: 16:9, 9:16, 1:1, 4:3, 3:4, 21:9\n\n### 并发配置\n\n`aigc-claw/backend/config_model.json` 定义了每个模型的并发限制:\n\n```json\n{\n \"models\": {\n \"doubao-seedream-5-0-260128\": {\n \"concurrency\": 10, // 高并发\n \"provider\": \"seedream\"\n },\n \"wan2.6-i2v-flash\": {\n \"concurrency\": 5,\n \"provider\": \"dashscope\"\n }\n }\n}\n```\n\n修改此文件可调整模型的最大并发数。\n\n#### 图像生成\n\n| 模型 | 调用限制 | 并发数上限 | 备注 |\n|------|---------|-----------|------|\n| **wan2.6-t2i** | 1次/秒 | 5个 | 文生图 |\n| **wan2.6-image** | 5次/秒 | 5个 | 图像生成 |\n| **jimeng_t2i_v40** | - | 2-5个 | 即梦系列 |\n| **doubao-seedream-*** | 500次/分钟 | 高并发 | 字节跳动Seedream |\n| **qwen-image** | 2次/秒 | 同步无限制 | 需开通 |\n\n#### 视频生成\n\n| 模型 | 调用限制 | 并发数上限 | 备注 |\n|------|------|------|------|\n| **wan2.6-i2v-flash** | 5次/秒 | 5个 | 首帧生视频 |\n| **wan2.6-i2v** | 5次/秒 | 5个 | 首帧生视频 |\n| **jimeng_ti2v_v30_pro** | 即梦视频,需实测限流 | | |\n| **kling-v3/v2-6** | 快手可灵,需查阅官方文档 | | |\n\n#### LLM / VLM\n\n| 模型 | RPM | TPM |\n|------|-----|-----|\n| **qwen3.5-plus** | 30,000 | 5,000,000 |\n| **qwen-plus** | 30,000 | 5,000,000 |\n| **qwen-vl-plus** | 1,200 | 1,000,000 |\n| **deepseek-chat** | 15,000 | 1,200,000 |\n\n> **RPM**: 每分钟请求数 | **TPM**: 每分钟Token数\n\n## 文档\n\n- [API 文档](./docs/)\n- [SKILL.md](./SKILL.md) - OpenClaw Agent 工作流规则\n- [CLAUDE.md](./CLAUDE.md) - Claude Code 开发指引\n\n## 许可证\n\nMIT License\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10167,"content_sha256":"4fbe4d7122dea0aa9282b252d683a99bd56a6964d7e32c6be75fd0168577e364"},{"filename":"references/init_project/init_all.md","content":"# 项目初始化\n\n首次下载项目后,需要配置环境并启动前后端服务。\n\n## 使用场景\n\n当用户给出以下指令时使用:\n- \"初始化项目\"\n- \"配置项目\"\n- \"部署项目\"\n- \"安装项目\"\n- \"开始项目\"\n- \"setup project\"\n- \"deploy\"\n\n## 前置检查\n\n```bash\n# 检查 Python 版本(需要 3.9+)\npython3 --version\n\n# 检查 Node.js 版本(需要 18+)\nnode --version\n\n# 检查 npm 版本\nnpm --version\n```\n\n## 初始化步骤\n\n### 步骤1:初始化后端\n\n参考 [init_backend.md](init_backend.md)\n\n### 步骤2:初始化前端\n\n参考 [init_frontend.md](init_frontend.md)\n\n### 步骤3:验证服务\n\n```bash\n# 检查后端运行\ncurl http://localhost:8000/api/health\n\n# 检查前端运行\ncurl http://localhost:3000\n```\n\n## 常见问题\n\n| 问题 | 解决方法 |\n|------|----------|\n| 后端启动失败 | 检查 Python 版本,确保 3.9+,检查 .env 配置 |\n| 前端 build 失败 | 删除 node_modules 和 .next,重新 `npm install` |\n| 端口被占用 | `lsof -ti :8000 | xargs kill` 或 `lsof -ti :3000 | xargs kill` |\n| 依赖安装慢 | 使用国内镜像源 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1115,"content_sha256":"afb1f8b89abe838d0cc9f829441544b8827c6f951bc5013f1827c10a8315cfb2"},{"filename":"references/init_project/init_backend.md","content":"# 初始化后端\n\n配置并启动 FastAPI 后端服务。\n\n## 步骤1:进入后端目录\n\n```bash\ncd aigc-claw/backend\n```\n\n## 步骤2:创建虚拟环境(首次)\n\n```bash\n# 创建虚拟环境\npython3 -m venv venv\n\n# 激活虚拟环境\nsource venv/bin/activate\n```\n\n## 步骤3:安装依赖\n\n```bash\n# 激活虚拟环境后执行\npip install -r requirements.txt\n```\n\n> **注意**:如果安装慢,可使用国内镜像:\n> ```bash\n> pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt\n> ```\n\n## 步骤4:配置环境变量\n\n```bash\n# 复制配置示例文件\ncp .env.example .env\n```\n\n然后编辑 `.env` 文件,填入必要的 API Key:\n\n| 变量 | 说明 | 获取方式 |\n|------|------|----------|\n| `DASHSCOPE_API_KEY` | 阿里云 Dashscope API Key(文生图、文生视频) | [阿里云百炼](https://dashscope.console.aliyun.com/) |\n| `DEEPSEEK_API_KEY` | DeepSeek API Key(LLM) | [DeepSeek](https://platform.deepseek.com/) |\n| `OPENAI_API_KEY` | OpenAI 兼容 API Key | 根据实际部署情况 |\n\n> ⚠️ **重要**:至少需要配置一个 LLM 和一个图片/视频生成 API,否则无法正常使用。\n\n## 步骤5:启动后端\n\n```bash\nsource venv/bin/activate\npython api_server.py\n```\n\n## 步骤6:验证\n\n```bash\ncurl http://localhost:8000/api/health\n```\n\n返回 `{\"status\":\"ok\"}` 表示成功。\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `python: command not found` | Python 未安装 | 安装 Python 3.9+ |\n| `No module named venv` | python3-venv 未安装 | `brew install python3-venv` (macOS) |\n| `ModuleNotFoundError` | 依赖未安装 | `pip install -r requirements.txt` |\n| `KeyError: 'DASHSCOPE_API_KEY'` | .env 未配置 | 编辑 .env 填入 API Key |\n| `Address already in use` | 端口 8000 被占用 | `lsof -ti :8000 | xargs kill` |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1856,"content_sha256":"ff5ea5c7dcbada64e51b5070175521bda122a5ae2d6687f04bafd135ea5a8315"},{"filename":"references/init_project/init_frontend.md","content":"# 初始化前端\n\n配置并启动 Next.js 前端服务。\n\n## 步骤1:进入前端目录\n\n```bash\ncd aigc-claw/frontend\n```\n\n## 步骤2:安装依赖(首次)\n\n```bash\nnpm install\n```\n\n> **注意**:如果安装慢,可使用国内镜像:\n> ```bash\n> npm config set registry https://registry.npmmirror.com\n> npm install\n> ```\n\n## 步骤3:配置环境变量(可选)\n\n```bash\n# 复制配置示例文件\ncp .env.local.example .env.local\n```\n\n通常无需修改默认配置。\n\n## 步骤4:Build 并启动\n\n```bash\n# 首次 build(必须)\nnpm run build\n\n# 启动服务\nnpm start\n```\n\n## 步骤5:验证\n\n```bash\ncurl http://localhost:3000\n```\n\n返回 HTML 表示成功。\n\n## 后续启动\n\n首次初始化后,后续启动只需:\n\n```bash\ncd aigc-claw/frontend\nnpm start\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `npm: command not found` | Node.js 未安装 | 安装 Node.js 18+ |\n| `Error: Could not find or load config file` | .next 目录损坏 | `rm -rf .next && npm run build` |\n| 白屏/空白页面 | build 缓存问题 | `rm -rf .next && npm run build` |\n| `Address already in use` | 端口 3000 被占用 | `lsof -ti :3000 | xargs kill` |\n| 依赖安装失败 | 网络问题 | 使用镜像或科学上网 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1280,"content_sha256":"eabcb32492a205c9bcd13f977f40e86519faa704bf3b2c2d11f3ce88569c402b"},{"filename":"references/run_project/start_backend.md","content":"# 启动后端\n\n启动 FastAPI 后端服务。\n\n## 检查是否已运行\n\n```bash\n# 推荐(检查服务响应)\ncurl -s http://localhost:8000/api/health && echo \"后端运行中\" || echo \"后端未运行\"\n```\n\n## 启动命令\n\n```bash\ncd aigc-claw/backend\nsource venv/bin/activate\npython api_server.py\n```\n\n## 验证\n\n```bash\ncurl http://localhost:8000/api/health\n```\n\n## 启动后等待\n\n⚠️ **重要**:后端启动需要时间,启动后必须等待 **3 秒** 再调用 API,否则可能收到 404 错误。\n\n```bash\nsleep 3\n# 然后再调用 API\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `command not found: lsof` | lsof 命令不存在 | 使用备用检查方式 `curl http://localhost:8000/api/health` |\n| `后端未运行` | 服务未启动 | 执行启动命令 |\n| `Address already in use` | 端口被占用 | `lsof -ti :8000 \\| xargs kill` 或 `pkill -f api_server.py` |\n| `ModuleNotFoundError` | 虚拟环境未激活 | 先执行 `source venv/bin/activate` |\n| `Connection refused` | 服务未启动或崩溃 | 检查日志 `/tmp/movie-backend.log` |\n\n## 注意事项\n\n- 后端端口:`8000`\n- 日志位置:`/tmp/movie-backend.log`\n- **必须确保后端运行后才能调用 API**\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1248,"content_sha256":"5d9d893d9e06e27acd45df7b48767a1ff0858129efdf02ba20c200611b57da34"},{"filename":"references/run_project/start_frontend.md","content":"# 启动前端\n\n启动 Next.js 前端服务。\n\n## 检查是否已运行\n\n```bash\n# 推荐(检查服务响应)\ncurl -s http://localhost:3000 > /dev/null 2>&1 && echo \"前端运行中\" || echo \"前端未运行\"\n```\n\n## 首次启动(需要 build)\n\n```bash\ncd aigc-claw/frontend\nnpm run build\nnpm start\n```\n\n## 后续启动\n\n```bash\ncd aigc-claw/frontend\nnpm start\n```\n\n## 验证\n\n```bash\ncurl http://localhost:3000\n```\n\n## 启动后等待\n\n⚠️ **重要**:前端启动需要时间,启动后必须等待 **5 秒** 再访问页面,否则可能出现空白页面。\n\n```bash\nsleep 5\n# 然后再访问 http://localhost:3000\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `command not found: lsof` | lsof 命令不存在 | 使用备用检查方式 `curl http://localhost:3000` |\n| `前端未运行` | 服务未启动 | 执行启动命令 |\n| `Address already in use` | 端口被占用 | `lsof -ti :3000 \\| xargs kill` |\n| `npm: command not found` | Node.js 未安装 | 安装 Node.js |\n| `Error: Could not find or load config file` | .next 目录损坏 | 删除 .next 目录后重新 `npm run build` |\n| 白屏/空白页面 | build 缓存问题 | `rm -rf .next && npm run build` |\n\n## 注意事项\n\n- 前端端口:`3000`\n- **必须确保前端运行后才能给用户 Web 界面链接**\n- 建议使用生产模式 `npm start`,开发模式可用 `npm run dev`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1411,"content_sha256":"23a9d8c37ab2169dec47c492d8f7a16bbfbb36675123b189c4f6ebe10f2b5db2"},{"filename":"references/sandbox/generate_image_it2i.md","content":"# 图生图 (I2I)\n\n使用图片生成图片(风格转换)。\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/sandbox/i2i\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"doubao-seedream-5-0\",\n \"prompt\": \"转换为动漫风格\",\n \"image\": \"code/result/image/user_upload/photo.png\"\n }'\n```\n\n## 参数说明\n\n| 参数 | 必填 | 说明 |\n|------|------|------|\n| model | | 模型,默认 doubao-seedream-5-0 |\n| prompt | ✅ | 目标风格描述 |\n| image | ✅ | 源图片路径 |\n\n## 可用模型\n\n| 模型 | 说明 |\n|------|------|\n| doubao-seedream-5-0 | 默认 |\n| wan2.6-image | |\n\n## ⚠️ 路径格式\n\n- `image` 必须使用 `code/result/...` 格式的**相对路径**\n- 禁止使用完整 URL 或本地路径\n\n## 响应\n\n```json\n{\n \"success\": true,\n \"image_url\": \"code/result/sandbox/images/xxx.png\"\n}\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `\"error\": \"image not found\"` | 图片路径错误 | 确认 image 路径格式为 `code/result/...` |\n| `\"success\": false` | API Key 额度用完或无效 | 检查对应平台的 API Key |\n| 风格转换效果差 | 提示词不明确 | 使用更具体的风格描述 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1279,"content_sha256":"81c5587d6c2ff1137cc0c25f34cb92676064678cf9ee6b4bf517f19363e4c11e"},{"filename":"references/sandbox/generate_image_t2i.md","content":"# 文生图 (T2I)\n\n使用文字生成图片。\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/sandbox/t2i\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"doubao-seedream-5-0\",\n \"prompt\": \"A cute cat sitting on a couch, realistic style\"\n }'\n```\n\n## 可用模型\n\n| 模型 | 说明 |\n|------|------|\n| doubao-seedream-5-0 | 默认 |\n| wan2.6-t2i | |\n| sora_image | |\n\n## 响应\n\n```json\n{\n \"success\": true,\n \"image_url\": \"code/result/sandbox/images/xxx.png\"\n}\n```\n\n## 注意事项\n\n1. **提示词建议使用英文**,效果更好\n2. **图片路径格式**:返回的是 `code/result/...` 格式,需要转换为实际文件路径\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `\"success\": false` | API Key 额度用完或无效 | 检查对应平台的 API Key |\n| `\"error\": \"rate limit\"` | 触发限流 | 等待后重试 |\n| 图片生成质量差 | 提示词不够具体 | 使用更具体的英文描述 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1051,"content_sha256":"89b243d09468f5b6c52ce078cb79e772acd7ae484a9c9fd86f6a78f81097e6cd"},{"filename":"references/sandbox/generate_video.md","content":"# 视频生成\n\n使用图片或文字生成视频片段(15秒以内)。\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/sandbox/video\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"wan2.6-i2v-flash\",\n \"prompt\": \"一只猫在草地上奔跑\",\n \"image\": \"code/result/image/user_upload/cat.png\"\n }'\n```\n\n## 参数说明\n\n| 参数 | 必填 | 说明 |\n|------|------|------|\n| model | | 模型,默认 wan2.6-i2v-flash |\n| prompt | ✅ | 视频描述 |\n| image | ✅ | 参考图片路径 |\n\n## 可用模型\n\n| 模型 | 说明 |\n|------|------|\n| wan2.6-i2v-flash | 默认,最快 |\n| wan2.6-i2v | |\n| kling-v3 | |\n| kling-v2-6 | |\n| jimeng_ti2v_v30_pro | |\n\n## ⚠️ 路径格式\n\n- `image` 必须使用 `code/result/...` 格式的**相对路径**\n- 禁止使用完整 URL 或本地路径\n\n## 响应\n\n```json\n{\n \"success\": true,\n \"video_path\": \"code/result/sandbox/videos/xxx.mp4\",\n \"record_id\": \"xxx\"\n}\n```\n\n## 获取视频文件\n\n```python\n# 直接从后端目录复制\nbackend_path = \"/code/result/sandbox/videos/{record_id}.mp4\"\nlocal_path = \"~/.openclaw/workspace/temp_imgs/{record_id}.mp4\"\nshutil.copy2(backend_path, local_path)\n```\n\n## 注意事项\n\n1. 生成的视频较短(15秒以内)\n2. 如果需要生成长视频,请使用完整工作流\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `404 Not Found` | 路径格式错误 | 使用 `code/result/...` 格式 |\n| `\"error\": \"image not found\"` | 图片文件不存在 | 检查图片路径是否正确 |\n| `\"success\": false` | API Key 额度用完或无效 | 检查对应平台的 API Key |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1703,"content_sha256":"2d393979f3591550d20cc3278b9785418c47a2375888f14b7bc11e9cdf6b6999"},{"filename":"references/send_message/feishu.md","content":"# 飞书消息发送\n\n向用户发送图片、视频等媒体文件。\n\n## 发送消息\n\n```python\nmessage(action=\"send\", message=\"消息内容\", target=\"user_open_id\")\n```\n\n## 发送图片\n\n```python\nmessage(action=\"send\", filePath=\"~/.openclaw/workspace/temp_imgs/xxx.png\", message=\"图片描述\", target=\"user_open_id\")\n```\n\n## 发送视频\n\n```python\nmessage(action=\"send\", filePath=\"~/.openclaw/workspace/temp_imgs/xxx.mp4\", message=\"视频描述\", target=\"user_open_id\")\n```\n\n## 参数说明\n\n| 参数 | 说明 |\n|------|------|\n| action | 固定为 \"send\" |\n| message | 消息文本内容 |\n| filePath | 本地文件路径(可选) |\n| target | 用户 Open ID |\n\n## ⚠️ 强制要求\n\n1. **必须直接发送文件**:使用 `filePath` 参数直接发送,禁止只发送路径或 URL\n2. **下载到本地**:生成媒体后必须先下载到 `~/.openclaw/workspace/temp_imgs/` 目录\n\n## 下载图片示例\n\n```python\nimport requests\nimport os\n\ntemp_dir = os.path.expanduser(\"~/.openclaw/workspace/temp_imgs\")\nos.makedirs(temp_dir, exist_ok=True)\n\n# 从后端下载图片\nurl = \"http://localhost:8000/code/result/image/xxx.png\"\nlocal_path = os.path.join(temp_dir, \"xxx.png\")\n\nresp = requests.get(url)\nwith open(local_path, 'wb') as f:\n f.write(resp.content)\n\n# 发送给用户\nmessage(action=\"send\", filePath=local_path, message=\"图片描述\", target=\"user_open_id\")\n```\n\n## ⚠️ 违规警告\n\n- 禁止只告诉用户\"图片已保存到 xxx 路径\"而不发送\n- 禁止只发送 URL 而不发送文件\n- 违规后果:用户必须主动要求\"把图片发给我\",这是严重失误!\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| 文件下载失败 | URL 错误或后端未运行 | 检查 URL 是否正确,确认后端已启动 |\n| 文件太小/无效 | 下载的可能是错误页面 | 验证文件大小 > 1KB |\n| 找不到文件 | 路径不存在 | 确认 `~/.openclaw/workspace/temp_imgs/` 目录下有文件 |\n| message 发送失败 | user_open_id 错误 | 确认 target 参数正确 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2054,"content_sha256":"0891d7e0ace1273873b2c7c6bf3efc485a1d75988150e6ca5e888abc5415bc03"},{"filename":"references/send_message/wechat.md","content":"# 微信发送消息\n\n## 注意事项\n\n微信不支持 Markdown 语法,**不要使用 Markdown 表格**!\n\n如果需要发送表格内容,改用**带缩进的编号列表**格式:\n\n```markdown\n1. 选项1:说明内容\n - 详情1\n - 详情2\n2. 选项2:说明内容\n - 详情1\n - 详情2\n3. 选项3:说明内容\n - 详情1\n - 详情2\n```\n\n## 示例\n\n❌ 错误(Markdown 表格,微信不显示):\n```\n| 选项 | 说明 |\n|------|------|\n| A | xxx |\n| B | yyy |\n```\n\n✅ 正确(缩进编号列表):\n```\n1. 选项A:xxx\n - 详情1\n - 详情2\n2. 选项B:yyy\n - 详情1\n - 详情2\n```","content_type":"text/markdown; charset=utf-8","language":"markdown","size":625,"content_sha256":"d1db68ad0ad078534d964685602da6701f0c128a420dce67713961b34c6e81d8"},{"filename":"references/workflow/create_character.md","content":"# 生成角色与场景\n\n执行第二阶段:角色和场景设计。\n\n---\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/character_design\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n---\n\n## 停点说明\n\n此阶段有 1 个停点:角色/场景设计完成后需要用户确认。\n\n## 产物结构\n\n```json\n{\n \"characters\": [\n {\n \"id\": \"char_1\",\n \"name\": \"角色名\",\n \"description\": \"角色描述\",\n \"visual_prompt\": \"视觉提示词\",\n \"selected\": \"code/result/image/xxx/character_001.png\",\n \"versions\": [\"code/result/xxx.png\"]\n }\n ],\n \"settings\": [\n {\n \"id\": \"set_1\",\n \"name\": \"场景名\",\n \"description\": \"场景描述\",\n \"visual_prompt\": \"视觉提示词\",\n \"selected\": \"code/result/image/xxx/setting_001.png\",\n \"versions\": [...]\n }\n ]\n}\n```\n\n## 实时反馈\n\n在生成过程中,SSE 会发送 `asset_complete` 事件:\n\n```json\n{\n \"type\": \"progress\",\n \"data\": {\n \"asset_complete\": {\n \"type\": \"characters|settings\",\n \"id\": \"char_1\",\n \"status\": \"done\",\n \"selected\": \"code/result/image/xxx.png\",\n \"versions\": [...]\n }\n }\n}\n```\n\n收到此事件后,必须立即下载图片并发送给用户。\n\n## 停点6:角色/场景设计完成,等待用户确认后继续下一阶段\n\n**必须向用户发送消息**,展示完整的角色和场景设计:\n\n1. **人物图片**:从 `artifact.characters[].selected` 获取每个人物的图片路径\n2. **场景图片**:从 `artifact.settings[].selected` 获取每个场景的图片路径\n3. **人物列表**:包含角色名、描述、视觉提示词\n4. **场景列表**:包含场景名、描述、视觉提示词\n\n**发送消息时必须**:\n- 根据消息渠道参考 [send_message/feishu.md](../send_message/feishu.md) 或 [send_message/wechat.md](../send_message/wechat.md) 发送图片\n- 每张图片需附带简短说明(角色名/场景名)\n- **发送前端 URL**(获取本地 IPv4 地址,构造 `http://{local_ip}:3000/?session={session_id}&stage=character_design`)\n- 发送完整列表后,询问用户确认\n\n询问内容示例:\n> \"角色和场景设计已完成,共生成 X 个人物和 Y 个场景。请确认是否继续进行分镜设计?\"\n\n## 继续下一阶段\n\n用户确认后调用:\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/continue\"\n```\n\n---\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `asset_complete` 状态为 failed | 图片生成失败 | 检查 API Key 配置,记录失败原因 |\n| 图片下载失败 | URL 路径错误 | 确认 path 格式为 `code/result/...` |\n| SSE 连接断开 | 网络超时 | 使用轮询 `/api/project/{session_id}/status` 继续 |\n| 用户不确认 | 用户想修改角色 | 调用 modify_character 重新生成 |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2972,"content_sha256":"ba31a3d7b606fa653078c011283e7d096ebc71acd2dbc1b7c35119dd1c29dd0d"},{"filename":"references/workflow/create_post.md","content":"# 后期剪辑\n\n执行第六阶段:将所有视频片段拼接成一个完整视频。\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/post_production\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n## 停点说明\n\n此阶段**无停点**,直接执行完成。\n\n## 产物结构\n\n```json\n{\n \"final_video\": \"code/result/video/xxx/final.mp4\"\n}\n```\n\n## 完成提示\n\n全部阶段完成后,告知用户:\n- 完整视频已生成\n- **发送前端 URL**(获取本地 IPv4 地址,构造 `http://{local_ip}:3000/?session={session_id}&stage=post_production`)\n- 提供 Web 界面链接供用户查看和下载\n\n```bash\ncurl \"http://localhost:8000/api/project/{session_id}/artifact/post_production\"\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| 拼接失败 | 部分视频片段缺失或损坏 | 检查 video_generation 阶段的产物 |\n| final_video 为空 | 所有视频片段生成失败 | 回退到 video_generation 阶段重新生成 |\n| 视频时长为 0 | FFmpeg 处理失败 | 检查后端日志 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1191,"content_sha256":"d01199563de0fcefc60b667d2b3f004d09de3936acbba4735d4b3953b0da1081"},{"filename":"references/workflow/create_project.md","content":"# 创建项目\n\n创建一个新的视频生成项目。\n\n## 请求与响应\n\n### 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/start\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"idea\": \"故事内容\",\n \"style\": \"anime\",\n \"video_ratio\": \"16:9\",\n \"llm_model\": \"qwen3.5-plus\",\n \"vlm_model\": \"qwen-vl-plus\",\n \"image_t2i_model\": \"doubao-seedream-5-0\",\n \"image_it2i_model\": \"doubao-seedream-5-0\",\n \"video_model\": \"wan2.6-i2v-flash\",\n \"enable_concurrency\": true,\n \"web_search\": false\n }'\n```\n\n### 响应\n\n```json\n{\n \"session_id\": \"xxx\",\n \"status\": \"idle\",\n \"current_stage\": \"init\"\n}\n```\n\n---\n\n## 参数说明\n\n| 参数 | 必填 | 说明 | 默认值 |\n|------|------|------|--------|\n| idea | ✅ | 故事创意/灵感 | - |\n| style | ✅ | 视频风格 | realistic |\n| video_ratio | | 视频比例 | 16:9 |\n| llm_model | | 剧本生成模型 | qwen3.5-plus |\n| vlm_model | | VLM 评估模型 | qwen-vl-plus |\n| image_t2i_model | | 文生图模型 | doubao-seedream-5-0 |\n| image_it2i_model | | 图生图模型 | doubao-seedream-5-0 |\n| video_model | | 视频生成模型 | wan2.6-i2v-flash |\n| enable_concurrency | | 开启并发生成 | true |\n| web_search | | 联网搜索 | false |\n\n### 可用风格\n\n`anime`, `realistic`, `comic-book`, `3d-disney`, `watercolor`, `oil-painting`, `cyberpunk`, `chinese-ink`\n\n### 可选视频比例\n\n6:9, 9:16, 1:1, 4:3, 3:4\n\n### 可用模型\n\n| 模块 | 模型 |\n|------|------|\n| LLM | qwen3.5-plus, deepseek-chat, gpt-4o, gemini-2.5-flash |\n| VLM | qwen-vl-plus, gemini-2.5-flash-image |\n| T2I | doubao-seedream-5-0, wan2.6-t2i, jimeng_t2i_v40 |\n| I2I | doubao-seedream-5-0, wan2.6-image |\n| Video | wan2.6-i2v-flash, kling-v3, jimeng_ti2v_v30_pro |\n\n---\n\n## 询问用户示例\n\n在创建项目前,请向用户展示以下选项并让用户选择:\n\n表格形式:\n| 配置项 | 选项 | 默认值(推荐) |\n|--------|------|---------------|\n| **视频风格 (style)** | anime, realistic, comic-book, 3d-disney, watercolor, oil-painting, cyberpunk, chinese-ink | realistic |\n| **视频比例 (video_ratio)** | 16:9(横屏), 9:16(竖屏), 1:1(方形), 4:3, 3:4 | 16:9 |\n| **LLM 模型** | qwen3.5-plus, deepseek-chat, gpt-4o, gemini-2.5-flash | qwen3.5-plus |\n| **VLM 模型** | qwen-vl-plus, gemini-2.5-flash-image | qwen-vl-plus |\n| **T2I 模型** | doubao-seedream-5-0, wan2.6-t2i, jimeng_t2i_v40 | doubao-seedream-5-0 |\n| **I2I 模型** | doubao-seedream-5-0, wan2.6-image | doubao-seedream-5-0 |\n| **Video 模型** | wan2.6-i2v-flash, kling-v3, jimeng_ti2v_v30_pro | wan2.6-i2v-flash |\n| **联网搜索** | true, false | false |\n| **并发生成** | true, false | true |\n\n编号列表形式:\n1. 故事创意 (idea): [用户的创意内容]\n2. 视频风格 (style): realistic(默认值)\n - 可选:anime, realistic, comic-book, 3d-disney, watercolor, oil-painting, cyberpunk, chinese-ink\n3. 视频比例 (video_ratio): 16:9(默认值)\n - 可选:16:9, 9:16, 1:1, 4:3, 3:4\n4. LLM 模型: qwen3.5-plus(默认值)\n - 可选:qwen3.5-plus, deepseek-chat, gpt-4o, gemini-2.5-flash\n5. VLM 模型: qwen-vl-plus(默认值)\n - 可选:qwen-vl-plus, gemini-2.5-flash-image\n6. T2I 模型: doubao-seedream-5-0(默认值)\n - 可选:doubao-seedream-5-0, wan2.6-t2i, jimeng_t2i_v40\n7. I2I 模型: doubao-seedream-5-0(默认值)\n - 可选:doubao-seedream-5-0, wan2.6-image\n8. Video 模型: wan2.6-i2v-flash(默认值)\n - 可选:wan2.6-i2v-flash, kling-v3, jimeng_ti2v_v30_pro\n9. 联网搜索: false(默认值)\n - 可选:true, false\n10. 并发生成: true(默认值)\n - 可选:true, false\n\n> **注意**:\n> - **所有参数必须都展示给用户**\n> - 根据用户消息渠道选择格式:\n> - 飞书:使用 Markdown 表格\n> - 微信:使用编号列表(微信不支持 Markdown 表格)\n\n---\n\n## 停点1:项目配置确认\n\n在调用 API 创建项目之前,必须展示当前配置并询问用户:\n\n### 展示当前配置\n\n根据用户提供的idea和选择(用户未提及的选项使用默认值),生成配置确认表格:\n\n| 配置项 | 当前值 |\n|--------|--------|\n| 故事创意 (idea) | [用户的创意内容] |\n| 视频风格 (style) | realistic(默认值)或其他用户选择 |\n| 视频比例 (video_ratio) | 16:9(默认值)或其他用户选择 |\n| LLM 模型 | qwen3.5-plus(默认值)或其他用户选择 |\n| VLM 模型 | qwen-vl-plus(默认值)或其他用户选择 |\n| T2I 模型 | doubao-seedream-5-0(默认值)或其他用户选择 |\n| I2I 模型 | doubao-seedream-5-0(默认值)或其他用户选择 |\n| Video 模型 | wan2.6-i2v-flash(默认值)或其他用户选择 |\n| 联网搜索 | false(默认值)|\n| 并发生成 | true(默认值)|\n\n### 询问用户\n\n> 当前配置如上,请问是否有需要修改的?\n> - 如需修改,请告知具体要修改的项目和新值\n> - 如无需修改,请回复\"确认\"或\"确定\"\n\n### 循环确认\n\n- 如果用户提出修改 → 记录修改项 → 重新展示更新后的配置 → 再次询问确认\n- 直到用户确认无需修改 → 才能调用 API 创建项目\n\n---\n\n## 注意事项\n\n1. **必须询问用户**:在创建项目前,一定要询问用户项目的配置,用户没有提及的选项则使用默认值\n2. **检查 API Key**:在创建项目前,必须检查用户选择的模型对应的 API Key 是否已配置\n\n### API Key 检查步骤\n\n```bash\n# 1. 读取 .env 文件\ncat aigc-claw/backend/.env\n\n# 2. 根据用户选择的模型检查对应 API Key\n# - LLM 模型:检查 DASHSCOPE_API_KEY / DEEPSEEK_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY\n# - 图片模型:检查 ARK_API_KEY / DASHSCOPE_API_KEY / VOLC_ACCESS_KEY/VOLC_SECRET_KEY\n# - 视频模型:检查 DASHSCOPE_API_KEY / VOLC_ACCESS_KEY/VOLC_SECRET_KEY / KLING_ACCESS_KEY/KLING_SECRET_KEY\n\n# 3. 如果缺少 API Key,提醒用户配置\n```\n\n### 缺少 API Key 时的处理\n\n如果检测到缺少必要的 API Key,需要告知用户:\n1. 缺少哪个平台的 API Key\n2. 如何获取(官方链接)\n3. 配置位置(`aigc-claw/backend/.env` 文件)\n4. 等待用户配置完成后才能继续创建项目\n\n| 平台 | API Key 变量 | 获取链接 |\n|------|--------------|----------|\n| DeepSeek | `DEEPSEEK_API_KEY` | https://platform.deepseek.com/api_keys |\n| 阿里云 DashScope | `DASHSCOPE_API_KEY` | https://bailian.console.aliyun.com/cn-beijing/?tab=home#/home |\n| 字节火山方舟 | `ARK_API_KEY` 或 `VOLC_ACCESS_KEY`/`VOLC_SECRET_KEY` | https://www.volcengine.com/product/ark |\n| 快手可灵 Kling | `KLING_ACCESS_KEY`/`KLING_SECRET_KEY` | https://klingai.com/cn/dev |\n\n---\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `500 Internal Server Error` | API Key 缺失或配置错误 | 检查 `backend/.env` 文件 |\n| `404 Not Found` | API 路径错误 | 确认 URL 为 `http://localhost:8000/api/project/start` |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7043,"content_sha256":"43f6adce87e874057831c7afcd3df793d8f925b08a1f4967f6e5e825565198fa"},{"filename":"references/workflow/create_reference.md","content":"# 生成参考图\n\n执行第四阶段:为每个分镜生成参考图。\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/reference_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n## 停点说明\n\n此阶段有 1 个停点:参考图生成完成后需要用户确认。\n\n## 产物结构\n\n```json\n{\n \"shots\": [\n {\n \"scene_id\": \"1\",\n \"shot_id\": \"1\",\n \"selected\": \"code/result/image/xxx/shot_001.png\",\n \"versions\": [...],\n \"visual_prompt\": \"优化后的视觉提示词\"\n }\n ]\n}\n```\n\n## 实时反馈\n\n在生成过程中,SSE 会发送 `asset_complete` 事件:\n\n```json\n{\n \"type\": \"progress\",\n \"data\": {\n \"asset_complete\": {\n \"type\": \"images\",\n \"id\": \"1-1\",\n \"status\": \"done\",\n \"selected\": \"code/result/image/xxx.png\",\n \"versions\": [...]\n }\n }\n}\n```\n\n收到此事件后,必须立即下载图片并发送给用户。\n\n## 停点8:参考图生成完成,等待用户确认后继续下一阶段\n\n**必须向用户发送消息**,展示每个分镜的参考图:\n\n从 `artifact.shots[].selected` 获取每个分镜的参考图路径。\n\n**发送消息时必须**:\n- 参考 [send_message/feishu.md](../send_message/feishu.md) 发送图片给用户\n- 每张参考图需附带分镜编号和简短描述(如\"场景1-分镜1:角色A在咖啡馆\")\n- 按场景顺序依次发送\n- **发送前端 URL**(获取本地 IPv4 地址,构造 `http://{local_ip}:3000/?session={session_id}&stage=reference_generation`)\n- 发送完整列表后,询问用户确认\n\n询问内容示例:\n> \"参考图生成已完成,共 X 张参考图。请确认是否继续生成视频片段?\"\n\n## 继续下一阶段\n\n用户确认后调用:\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/continue\"\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `asset_complete` 状态为 failed | 图片生成失败(超时/限流) | 可在请求中设置 `\"enable_concurrency\": false` 降低并发重试 |\n| 图片下载失败 | URL 路径错误 | 确认 path 格式为 `code/result/...` |\n| SSE 连接断开 | 网络超时 | 使用轮询 `/api/project/{session_id}/status` 继续 |\n| 用户不确认 | 用户想修改参考图 | 调用 modify_reference 重新生成 |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2428,"content_sha256":"58129e66879b217b6edbc66d10f84658de6ae1289dfcf9ddb2239ccab532cddd"},{"filename":"references/workflow/create_script.md","content":"# 生成剧本\n\n执行第一阶段:剧本生成。\n\n---\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/script_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\", \"style\": \"anime\"}'\n```\n\n---\n\n## 停点流程\n\n剧本生成阶段有 4 个停点:\n\n| 停点 | phase 值 | 操作 |\n|------|----------|------|\n| 1 | suggest_expand | 询问用户是否需要对情节进行扩写 |\n| 2 | logline_selection | 让用户从 3 个情节候选中选择 |\n| 3 | mode_selection | 让用户选择电影(4幕)或微电影(1幕) |\n| 4 | script_generation | 等待剧本生成完成 |\n\n> **注意**:如果用户输入足够清晰完整丰富,`suggest_expand` 和 `logline_selection` 可能会直接跳过,直接进入 `mode_selection`。\n\n## 处理各停点\n\n### 停点2:建议扩写(suggest_expand)\n\n此停点表示当前输入的情节过于简短,难以生成高质量剧情,系统建议进行创意扩写。\n\n```bash\n# 获取 artifact 查看扩写建议\ncurl \"http://localhost:8000/api/project/{session_id}/artifact/script_generation\"\n```\n\n从 `artifact.expand_suggestion` 或 `artifact.suggestion` 获取扩写建议,询问用户选择:\n\n- **选择扩写**:调用 intervene,让系统进行创意扩写\n- **跳过扩写**:直接进入下一停点(logline_selection 或 mode_selection)\n\n```bash\n# 选择扩写\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/intervene\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"stage\": \"script_generation\", \"modifications\": {\"expand_idea\": true}}'\n\n# 跳过扩写\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/intervene\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"stage\": \"script_generation\", \"modifications\": {\"expand_idea\": false}}'\n```\n\n### 停点3:选择情节\n\n```bash\n# 获取 artifact 查看候选\ncurl \"http://localhost:8000/api/project/{session_id}/artifact/script_generation\"\n```\n\n从 `artifact.logline_options` 获取候选列表,询问用户选择,然后调用 intervene:\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/intervene\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"stage\": \"script_generation\", \"modifications\": {\"selected_logline\": 0}}'\n```\n\n> **注意**:参数名是 `selected_logline`,不是 `selected_logline_index`!\n\n### 停点4:选择模式\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/intervene\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"stage\": \"script_generation\", \"modifications\": {\"selected_mode\": \"expand\"}}'\n```\n\n- `expand`: 电影模式(4幕,15-20分钟)\n- `micro`: 微电影模式(1幕,3-5分钟)\n\n### 停点5:剧本生成完成,等待用户确认后继续下一阶段\n\n**必须向用户发送消息**,展示完整的剧本内容:\n\n- **标题**:`artifact.title`\n- **故事线**:`artifact.logline`\n- **人物列表**:`artifact.characters`(包含人物名称、描述、性格特点)\n- **背景列表**:`artifact.settings`(包含背景名称、描述、氛围)\n- **场景列表**:`artifact.scenes`(包含场景编号、类型、描述、人物、地点)\n\n- **发送前端 URL**(获取本地 IPv4 地址,构造 `http://{local_ip}:3000/?session={session_id}&stage=script_generation`)\n\n询问用户确认后调用:\n\n```bash\n# 确认剧本,继续下一阶段\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/continue\"\n```\n\n---\n\n## SSE 事件监听\n\n- `progress`: 实时进度,可能包含 `asset_complete`\n- `stage_complete`: 阶段完成,检查 `data.phase` 确认是否还有下一阶段\n- `error`: 执行出错\n\n---\n\n## 响应示例\n\n```json\n{\n \"title\": \"标题\",\n \"logline\": \"核心故事线\",\n \"characters\": [...],\n \"settings\": [...],\n \"scenes\": [...],\n \"phase\": \"script_generation\"\n}\n```\n\n---\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `404 Not Found` | session_id 错误或 API 路径错误 | 确认 session_id 正确 |\n| SSE 无响应 | 后端任务卡住 | 检查日志 `/tmp/movie-backend.log` |\n| 用户不选择 | 用户在停点未回复 | 等待用户选择,不要自行决定 |\n| `\"phase\": \"suggest_expand\"` | 系统建议启用创意扩写 | 可自动调用 intervene 启用 |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4354,"content_sha256":"e4521ad6433e67ced8245a010116df658013020781b95423d84b77f711485192"},{"filename":"references/workflow/create_storyboard.md","content":"# 生成分镜\n\n执行第三阶段:分镜设计。\n\n---\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/storyboard\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n---\n\n## 停点说明\n\n此阶段有 1 个停点:分镜设计完成后需要用户确认。\n\n## 产物结构\n\n```json\n{\n \"shots\": [\n {\n \"scene_id\": \"1\",\n \"shot_id\": \"1\",\n \"duration\": \"5\",\n \"characters\": [\"角色A\"],\n \"location\": \"场景名\",\n \"description\": \"分镜描述\",\n \"visual_prompt\": \"视觉提示词\"\n }\n ]\n}\n```\n\n## 停点7:分镜设计完成,等待用户确认后继续下一阶段\n\n**必须向用户发送消息**,展示完整的分镜列表:\n\n从 `artifact.shots` 获取所有分镜数据,用表格形式展示:\n\n| 场景-分镜 | 时长 | 人物 | 地点 | 情节描述 |\n|-----------|------|------|------|----------|\n| 1-1 | 5s | 角色A, 角色B | 咖啡馆 | 两人在咖啡馆交谈 |\n| 1-2 | 3s | 角色A | 咖啡馆 | 角色A望向窗外 |\n\n**发送消息时必须**:\n- 使用文字形式发送表格(参考 [send_message/feishu.md](../send_message/feishu.md))\n- 包含总时长统计\n- **发送前端 URL**(获取本地 IPv4 地址,构造 `http://{local_ip}:3000/?session={session_id}&stage=storyboard`)\n- 发送完整列表后,询问用户确认\n\n询问内容示例:\n> \"分镜设计已完成,共 X 个分镜,总时长约 Y 秒。请确认是否继续生成参考图?\"\n\n## 继续下一阶段\n\n用户确认后调用:\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/continue\"\n```\n\n---\n\n## 智能续写(可选)\n\n用户可以要求续写分镜:\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/intervene\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"stage\": \"storyboard\", \"modifications\": {\"continue_story\": true}}'\n```\n\n---\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| 分镜数量太少 | LLM 生成不完整 | 询问用户是否需要续写 |\n| 用户不确认 | 用户想修改分镜 | 调用 modify_storyboard 修改 |\n| 续写失败 | 剧本内容不足 | 检查剧本阶段产物是否完整 |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2296,"content_sha256":"d1d0031f8ee383fb77fb56ef325e6e4c52a90a326629ba423339951d139dba02"},{"filename":"references/workflow/create_video.md","content":"# 生成视频片段\n\n执行第五阶段:视频生成。\n\n## 请求\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/video_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n## 停点说明\n\n此阶段有 1 个停点:视频生成完成后需要用户确认。\n\n## 产物结构\n\n```json\n{\n \"clips\": [\n {\n \"scene_id\": \"1\",\n \"shot_id\": \"1\",\n \"description\": \"视频描述\",\n \"selected\": \"code/result/video/xxx/clip_001.mp4\",\n \"versions\": [...],\n \"status\": \"done|failed\"\n }\n ]\n}\n```\n\n## 实时反馈\n\n在生成过程中,SSE 会发送 `asset_complete` 事件:\n\n```json\n{\n \"type\": \"progress\",\n \"data\": {\n \"asset_complete\": {\n \"type\": \"clips\",\n \"id\": \"1-1\",\n \"status\": \"done\",\n \"selected\": \"code/result/video/xxx.mp4\",\n \"versions\": [...]\n }\n }\n}\n```\n\n收到此事件后,必须立即下载视频并发送给用户。\n\n## 停点9:视频片段生成完成,等待用户确认后继续下一阶段\n\n**必须向用户发送消息**,展示每个视频片段:\n\n从 `artifact.clips[].selected` 获取每个视频片段的路径。\n\n**发送消息时必须**:\n- 参考 [send_message/feishu.md](../send_message/feishu.md) 发送视频给用户\n- 每个视频片段需附带分镜编号和描述(如\"场景1-分镜1:角色A走进咖啡馆\")\n- 标注视频时长和状态(done/failed)\n- 按场景顺序依次发送\n- **发送前端 URL**(获取本地 IPv4 地址,构造 `http://{local_ip}:3000/?session={session_id}&stage=video_generation`)\n- 发送完整列表后,询问用户确认\n\n询问内容示例:\n> \"视频片段生成完成,共 X 个片段,Y 个成功。请确认是否继续进行后期剪辑?\"\n\n## 继续下一阶段\n\n用户确认后调用:\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/continue\"\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `asset_complete` 状态为 failed | 视频生成失败(超时/限流/不支持的内容) | 可降低并发数重试,或检查视频模型是否支持该内容 |\n| 视频下载失败 | URL 路径错误 | 确认 path 格式为 `code/result/...` |\n| 视频文件太小 | 生成可能失败 | 检查文件大小 > 1KB |\n| SSE 连接断开 | 视频生成时间长 | 使用轮询 `/api/project/{session_id}/status` 继续 |\n| 用户不确认 | 用户想修改视频 | 调用 modify_video 重新生成 |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2558,"content_sha256":"9927db357b66e05a99c17992eafff19bb4f3c0e3df497554781e5505add06c6b"},{"filename":"references/workflow/modify_character.md","content":"# 修改角色/场景提示词\n\n在第二阶段完成后,用户可以修改角色或场景的视觉提示词并重新生成。\n\n## 修改角色提示词\n\n```bash\ncurl -X PATCH \"http://localhost:8000/api/project/{session_id}/artifact/character_design\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"characters\": [\n {\n \"id\": \"char_1\",\n \"visual_prompt\": \"新的视觉提示词\"\n }\n ]\n }'\n```\n\n## 重新执行角色/场景设计\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/character_design\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n## 参数说明\n\n| 参数 | 说明 |\n|------|------|\n| characters[].id | 角色 ID |\n| characters[].visual_prompt | 新的视觉提示词 |\n\n> 修改后需要重新执行阶段才能生效。\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| `404 Not Found` | session_id 错误 | 确认 session_id 正确 |\n| PATCH 成功但无变化 | 需要重新执行阶段 | 调用 execute/character_design 重新生成 |\n| 角色 ID 不存在 | ID 错误 | 从 artifact 中获取正确的角色 ID |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1228,"content_sha256":"3212c567e65dfc9161e39524fa9723c27b4aa3198a3f76eb5b98dbdeeb6c607f"},{"filename":"references/workflow/modify_reference.md","content":"# 修改参考图提示词\n\n在第四阶段完成后,用户可以修改某个分镜的视觉提示词并重新生成参考图。\n\n## 修改分镜提示词\n\n```bash\ncurl -X PATCH \"http://localhost:8000/api/project/{session_id}/artifact/reference_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"shots\": [\n {\n \"scene_id\": \"1\",\n \"shot_id\": \"1\",\n \"visual_prompt\": \"新的视觉提示词\"\n }\n ]\n }'\n```\n\n## 重新执行参考图生成\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/reference_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| PATCH 成功但无变化 | 需要重新执行阶段 | 调用 execute/reference_generation 重新生成 |\n| scene_id/shot_id 不存在 | ID 错误 | 从 artifact 中获取正确的 ID |\n| 图片生成失败 | 提示词包含不支持的内容 | 修改提示词后重试 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1085,"content_sha256":"6b84d0e05d083d2c310410c908793fd09f3cbfe0767b0e1d0fc5f13f6d00a78e"},{"filename":"references/workflow/modify_storyboard.md","content":"# 修改分镜\n\n在第三阶段完成后,用户可以修改或续写分镜。\n\n## 修改分镜\n\n```bash\ncurl -X PATCH \"http://localhost:8000/api/project/{session_id}/artifact/storyboard\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"shots\": [\n {\n \"scene_id\": \"1\",\n \"shot_id\": \"1\",\n \"duration\": \"8\",\n \"description\": \"新的分镜描述\",\n \"visual_prompt\": \"新的视觉提示词\"\n }\n ]\n }'\n```\n\n## 智能续写\n\n根据已有剧情自动生成新场景:\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/intervene\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"stage\": \"storyboard\", \"modifications\": {\"continue_story\": true}}'\n```\n\n## 重新执行分镜设计\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/storyboard\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| PATCH 成功但无变化 | 需要重新执行阶段 | 调用 execute/storyboard 重新生成 |\n| scene_id/shot_id 不存在 | ID 错误 | 从 artifact 中获取正确的 ID |\n| 续写失败 | 剧本内容不足 | 检查剧本阶段产物是否完整 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1315,"content_sha256":"779ac706fe2e6f2ff3563b78a5ea30bc26bf8defe7abfc11f58b17e7e53ae673"},{"filename":"references/workflow/modify_video.md","content":"# 修改视频生成提示词\n\n在第五阶段完成后,用户可以修改某个分镜的视频生成提示词并重新生成视频。\n\n## 修改分镜提示词\n\n```bash\ncurl -X PATCH \"http://localhost:8000/api/project/{session_id}/artifact/video_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"clips\": [\n {\n \"scene_id\": \"1\",\n \"shot_id\": \"1\",\n \"description\": \"新的视频描述\"\n }\n ]\n }'\n```\n\n## 重新执行视频生成\n\n```bash\ncurl -X POST \"http://localhost:8000/api/project/{session_id}/execute/video_generation\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"session_id\": \"xxx\"}'\n```\n\n## 常见问题\n\n| 错误 | 原因 | 解决方法 |\n|------|------|----------|\n| `curl: (7) Failed to connect` | 后端未运行 | 启动后端服务 |\n| PATCH 成功但无变化 | 需要重新执行阶段 | 调用 execute/video_generation 重新生成 |\n| scene_id/shot_id 不存在 | ID 错误 | 从 artifact 中获取正确的 ID |\n| 视频生成失败 | 提示词不支持或超时 | 修改提示词或降低并发后重试 |","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1080,"content_sha256":"41974f2e006f49905c09b344fad86f4f0352c96d93998566e3baea155ba86040"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"AIGC-Director Agent Skill","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"本地运行","type":"text","marks":[{"type":"strong"}]},{"text":":这是一个","type":"text"},{"text":"本地部署","type":"text","marks":[{"type":"strong"}]},{"text":"的视频生成项目,","type":"text"},{"text":"前后端都运行在本机","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"后端:","type":"text"},{"text":"http://localhost:8000","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"前端:","type":"text"},{"text":"http://localhost:3000","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"所有 API 调用都请求本地服务器,不要请求其他地址!","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"确保在调用任何 API 之前,后端和前端服务都已经启动并运行正常!","type":"text"}]}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"核心理念","type":"text","marks":[{"type":"strong"}]},{"text":":Agent 应该像\"持续陪伴的智能视频制作助理\",每完成一个用户可感知的重要任务,都应立即给用户一条简报,并等待用户确认。","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"核心原则","type":"text","marks":[{"type":"strong"}]},{"text":":每个阶段的产物都必须展示给用户,必须停下来等待用户确认后才能继续下一阶段。","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"防止遗忘","type":"text","marks":[{"type":"strong"}]},{"text":":在整个流程中,Agent 可能会忘记之前的用户输入或之前阶段的产物内容。","type":"text"},{"text":"每当进入一个新的阶段时,Agent 都必须重新加载这篇SKILL文档,确保不会忘记任何细节","type":"text","marks":[{"type":"strong"}]},{"text":"。","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"项目结构","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"aigc-director/ ← OpenClaw 调用的 skill 根目录\n├── aigc-claw/ ← 前后端项目代码\n│ ├── backend/ ← FastAPI 后端(端口 8000)\n│ │ └── code/result/ ← 模型生成产物存放目录\n│ │ ├── script/ ← 剧本产物\n│ │ ├── image/ ← 图片产物(角色、场景、参考图)\n│ │ └── video/ ← 视频产物\n│ └── frontend/ ← Next.js 前端(端口 3000)\n├── references/ ← OpenClaw 调用时的参考文档\n│ ├── init_project/ ← 项目初始化\n│ ├── run_project/ ← 服务启动\n│ ├── workflow/ ← 六阶段工作流 API\n│ ├── sandbox/ ← 临时工作台 API\n│ └── send_message/ ← 消息发送\n└── SKILL.md ← skill 正文","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"产物存放目录","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"},{"text":"aigc-claw/backend/code/result/","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"script/","type":"text","marks":[{"type":"code_inline"}]},{"text":" - 剧本产物","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"image/","type":"text","marks":[{"type":"code_inline"}]},{"text":" - 图片产物(角色、场景、参考图)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"video/","type":"text","marks":[{"type":"code_inline"}]},{"text":" - 视频产物","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"阶段与停点(共9个)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"停点","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"阶段","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phase 值","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"描述","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"操作","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"项目配置","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认配置选项","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"展示配置 → 用户确认","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"剧本生成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"suggest_expand","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"建议扩写","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"等待用户确认","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"剧本生成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"logline_selection","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"选择情节","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"从3个候选中选择","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"剧本生成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mode_selection","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"选择模式","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"电影(4幕) / 微电影(1幕)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"剧本生成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"script_generation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认剧本","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认后继续","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"角色/场景设计","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认角色/场景图片","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认后继续","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"分镜设计","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认分镜列表","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认后继续","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"参考图生成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认参考图","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认后继续","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"9","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频生成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认视频片段","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"确认后继续","type":"text"}]}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"注意","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"},{"text":"suggest_expand","type":"text","marks":[{"type":"code_inline"}]},{"text":" 和 ","type":"text"},{"text":"logline_selection","type":"text","marks":[{"type":"code_inline"}]},{"text":" 可能根据输入质量被跳过。","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"工作流程","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. 本地部署(仅初始化时执行)","type":"text"}]},{"type":"paragraph","content":[{"text":"当用户要求\"初始化项目\"、\"配置项目\"、\"部署项目\"时,需要先进行项目初始化:参考 ","type":"text"},{"text":"init_all.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/init_project/init_all.md","title":null}}]},{"text":" 执行完整初始化流程。","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"注意","type":"text","marks":[{"type":"strong"}]},{"text":":仅在用户首次下载项目或需要重新配置环境时使用。项目已初始化过则跳过此步骤,直接检查服务运行状态。","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. 检查本地服务","type":"text"}]},{"type":"paragraph","content":[{"text":"参考 ","type":"text"},{"text":"start_backend.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/run_project/start_backend.md","title":null}}]},{"text":" 和 ","type":"text"},{"text":"start_frontend.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/run_project/start_frontend.md","title":null}}]},{"text":" 检查服务是否运行。","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ 强制要求","type":"text","marks":[{"type":"strong"}]},{"text":":如果服务未运行,必须先启动服务再继续!","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. 路由判断","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户说","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"处理","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"生成图片\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"临时工作台 (sandbox)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"生成视频\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"必须先询问:长视频(工作流) 还是 短视频(工作台)?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"分析图片\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"临时工作台 (sandbox)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"问 LLM 问题\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"临时工作台 (sandbox)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"照片转动漫\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"临时工作台 (sandbox)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. 执行流程","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"1. 检查后端运行状态 → 未运行则参考 start_backend.md 启动 → 等待3秒 → 再次检查\n2. 检查前端运行状态 → 未运行则参考 start_frontend.md 启动 → 等待5秒 → 再次检查\n3. 检查 API Key 配置 → 读取 .env 文件,确认所需 API Key 已配置\n4. 参考 create_project.md 询问用户项目配置 → 停点1(配置确认)→ 创建项目\n5. 参考 create_script.md 执行剧本生成 → 停点2-5\n6. 参考 create_character.md 执行角色设计 → 停点6\n7. 参考 create_storyboard.md 执行分镜设计 → 停点7\n8. 参考 create_reference.md 执行参考图生成 → 停点8\n9. 参考 create_video.md 执行视频生成 → 停点9\n10. 参考 create_post.md 执行后期剪辑\n11. 完成 → 发送最终视频给用户","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"注意","type":"text","marks":[{"type":"strong"}]},{"text":":一定要参考 ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" 目录下的具体文档执行每一步操作,不要凭记忆或想当然去调用 API!","type":"text"}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"检查 API Key 配置","type":"text"}]},{"type":"paragraph","content":[{"text":"在创建项目前,必须检查用户选择的模型对应的 API Key 是否已配置:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 读取 .env 文件检查配置\ncat aigc-claw/backend/.env | grep -E \"API_KEY|KEY\"\n\n# 必需的配置(根据选择的模型)\n# LLM: DASHSCOPE_API_KEY / DEEPSEEK_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY\n# 图片: ARK_API_KEY / DASHSCOPE_API_KEY\n# 视频: DASHSCOPE_API_KEY / VOLC_ACCESS_KEY / KLING_ACCESS_KEY","type":"text"}]},{"type":"paragraph","content":[{"text":"如果 API Key 未配置,需要提醒用户:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"告知缺少哪个平台的 API Key","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"提供获取方式","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"配置位置(","type":"text"},{"text":"aigc-claw/backend/.env","type":"text","marks":[{"type":"code_inline"}]},{"text":" 文件)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"等待用户配置完成后才能继续","type":"text"}]}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"平台","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API Key 变量","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"获取链接","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DeepSeek","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DEEPSEEK_API_KEY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://platform.deepseek.com/api_keys","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"阿里云 DashScope","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DASHSCOPE_API_KEY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://bailian.console.aliyun.com/cn-beijing/?tab=home#/home","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"字节火山方舟","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ARK_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" 或 ","type":"text"},{"text":"VOLC_ACCESS_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"VOLC_SECRET_KEY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://www.volcengine.com/product/ark","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"快手可灵 Kling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"KLING_ACCESS_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"KLING_SECRET_KEY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://klingai.com/cn/dev","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"🚨 停点处理(强制规则)","type":"text"}]},{"type":"paragraph","content":[{"text":"当查询状态为 ","type":"text","marks":[{"type":"strong"}]},{"text":"stage_completed","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 或 ","type":"text","marks":[{"type":"strong"}]},{"text":"waiting_in_stage","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 时,必须按以下步骤执行:","type":"text","marks":[{"type":"strong"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"步骤1:获取产物","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl \"http://localhost:8000/api/project/{session_id}/artifact/{stage}\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"步骤2:展示给用户","type":"text"}]},{"type":"paragraph","content":[{"text":"将 artifact 中的内容(选项列表、建议、产物摘要)","type":"text"},{"text":"完整展示","type":"text","marks":[{"type":"strong"}]},{"text":"给用户","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"步骤3:询问决策","type":"text"}]},{"type":"paragraph","content":[{"text":"明确告诉用户:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"选项有哪些","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"每个选项的含义","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"需要用户选择什么","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"步骤4:等待用户回复","type":"text"}]},{"type":"paragraph","content":[{"text":"禁止","type":"text","marks":[{"type":"strong"}]},{"text":"在用户回复前自行调用 ","type":"text"},{"text":"intervene","type":"text","marks":[{"type":"code_inline"}]},{"text":" 或 ","type":"text"},{"text":"continue","type":"text","marks":[{"type":"code_inline"}]},{"text":"!","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"步骤5:用户确认后执行","type":"text"}]},{"type":"paragraph","content":[{"text":"根据用户的选择,调用相应的 API","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"❌ 错误示例(我刚才犯的错)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"收到 suggest_expand 停点 → 直接调用 intervene → 跳过用户确认","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"✅ 正确示例","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"1. 阶段内部停点触发(如 suggest_expand)\n收到 suggest_expand 停点 \n→ 获取 artifact 查看内容\n→ 展示给用户:\"系统建议启用创意扩写模式...\"\n→ 询问:\"是否同意?\"\n→ 用户回复\"同意\" → 调用 intervene\n\n2. 阶段完成停点触发\n收到 stage_completed 停点\n→ 获取 artifact 查看产物内容\n→ 展示给用户:\"第一阶段已完成,生成了剧本内容...\"\n→ 询问:\"是否继续下一阶段?\"\n→ 用户回复\"继续\" → 调用 continue","type":"text"}]},{"type":"paragraph","content":[{"text":"每个停点必须","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"展示产物或选项给用户","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"询问确认","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"用户确认后才能继续","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"状态判断","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"status","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"含义","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"操作","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"idle","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"新建会话","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"启动项目","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"running","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"执行中","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"轮询等待","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"waiting_in_stage","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"等待用户介入","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"调用 ","type":"text"},{"text":"intervene","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"stage_completed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"阶段完成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"调用 ","type":"text"},{"text":"continue","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"session_completed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"全部完成","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"结束","type":"text"}]}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"注意","type":"text","marks":[{"type":"strong"}]},{"text":":只有 status 变化时才需要干预,不要反复调用 artifact API 去\"确认\"!","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"消息发送渠道","type":"text"}]},{"type":"paragraph","content":[{"text":"根据向用户发送消息的渠道(飞书/微信),读取 ","type":"text"},{"text":"references/send_message/","type":"text","marks":[{"type":"code_inline"}]},{"text":" 下的对应参考文档,获取注意事项和发送方法:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"feishu.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/send_message/feishu.md","title":null}}]},{"text":" - 飞书发送消息","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"wechat.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/send_message/wechat.md","title":null}}]},{"text":" - 微信发送消息","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"任务简报格式","type":"text"}]},{"type":"paragraph","content":[{"text":"每个阶段完成后,发送简报必须包含:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"刚完成什么","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"下一步做什么","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"需要用户决策的内容","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Web 界面链接","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"},{"text":"http://[本地IP]:3000/?session={session_id}&stage={stage}","type":"text","marks":[{"type":"code_inline"}]},{"text":"(注意,这里使用本地 IPv4 地址,不要用 localhost!)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"产物图片/视频(直接发送文件,禁止只发路径)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Web 界面链接格式","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# 获取本地 IPv4 地址\nimport socket\nlocal_ip = socket.gethostbyname(socket.gethostname())\n\n# 构造前端 URL\nfrontend_url = f\"http://{local_ip}:3000/?session={session_id}&stage={stage}\"\n\n# 发送给用户\nsend_to_user(f\"📊 查看详情:{frontend_url}\")","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"重要","type":"text","marks":[{"type":"strong"}]},{"text":":必须使用本地 IPv4 地址(如 ","type":"text"},{"text":"192.168.1.x","type":"text","marks":[{"type":"code_inline"}]},{"text":"),不要使用 ","type":"text"},{"text":"localhost","type":"text","marks":[{"type":"code_inline"}]},{"text":" 或 ","type":"text"},{"text":"127.0.0.1","type":"text","marks":[{"type":"code_inline"}]},{"text":",否则用户无法从其他设备访问!","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"详细参考","type":"text"}]},{"type":"paragraph","content":[{"text":"根据用户的需求和当前阶段,参考 ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" 目录下的具体文档执行相应操作:","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"references 目录","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"文件","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用途","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"查看时机","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"init_project/","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"项目初始化","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户首次下载或要求\"初始化项目\"时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"init_all.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/init_project/init_all.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"完整初始化流程","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户要求初始化部署时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"init_backend.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/init_project/init_backend.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"后端初始化","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"首次配置后端环境时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"init_frontend.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/init_project/init_frontend.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"前端初始化","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"首次配置前端环境时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"run_project/","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"项目启动","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"start_backend.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/run_project/start_backend.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"启动后端服务","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"服务未运行时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"start_frontend.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/run_project/start_frontend.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"启动前端服务","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"服务未运行时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"workflow/","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"六阶段工作流","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_project.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/create_project.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"创建新项目 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"开始新视频项目时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_script.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/create_script.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"剧本生成 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"执行第一阶段时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_character.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/create_character.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"角色/场景设计 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"执行第二阶段时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_storyboard.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/create_storyboard.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"分镜设计 API/剧情续写 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"执行第三阶段时/用户提出续写剧情时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_reference.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/create_reference.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"参考图生成 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"执行第四阶段时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_video.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/create_video.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频生成 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"执行第五阶段时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_post.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/create_post.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"后期剪辑 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"执行第六阶段时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"modify_character.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/modify_character.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"修改角色提示词","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户要求修改角色时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"modify_storyboard.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/modify_storyboard.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"修改/续写分镜","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户要求修改/续写分镜时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"modify_reference.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/modify_reference.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"修改参考图提示词","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户要求修改参考图时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"modify_video.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflow/modify_video.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"修改视频提示词","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户要求修改视频时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sandbox/","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"临时工作台","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"generate_image_t2i.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/sandbox/generate_image_t2i.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"文生图 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户要求生成图片时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"generate_image_it2i.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/sandbox/generate_image_it2i.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"图生图/风格转换 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户要求转换图片风格时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"generate_video.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/sandbox/generate_video.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"短视频生成 API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户要求生成15秒内视频时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"send_message/","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"消息发送","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"feishu.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/send_message/feishu.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"飞书发送媒体文件","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户通过飞书渠道发起对话,并且需要向用户发送图片/视频给用户时","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"wechat.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/send_message/wechat.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"微信发送媒体文件","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户通过微信渠道发起对话,并且需要向用户发送图片/视频给用户时","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"aigc-director","author":"@skillopedia","source":{"stars":65,"repo_name":"claude-code-skills","origin_url":"https://github.com/aaaaqwq/claude-code-skills/blob/HEAD/skills/aigc-director/SKILL.md","repo_owner":"aaaaqwq","body_sha256":"7a5963276ff901efd6b5ead7a0b2d249bb5dde9a38eb9c21456c164887cab78b","cluster_key":"83ca391e89d1feab612e26a64135a80d5bd5996a21d34724bc1aebc72be1bc32","clean_bundle":{"format":"clean-skill-bundle-v1","source":"aaaaqwq/claude-code-skills/skills/aigc-director/SKILL.md","attachments":[{"id":"22e98233-fe16-5b5c-98cb-a1f6dc33af38","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/22e98233-fe16-5b5c-98cb-a1f6dc33af38/attachment","path":".gitignore","size":295,"sha256":"39b4c7ef1b80bfd00dfa21106abb26f3183317f3f6dbd82c139cd687836de9a3","contentType":"text/plain; charset=utf-8"},{"id":"3dce4f33-4b5b-5a0b-b437-936416751f2d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3dce4f33-4b5b-5a0b-b437-936416751f2d/attachment.md","path":"CLAUDE.md","size":3750,"sha256":"210722330a7c8188f54fb45a51f847515dfe7f312d36a0c1984b0466cca1253b","contentType":"text/markdown; charset=utf-8"},{"id":"554ec89c-93d8-5a38-80d0-db988e17550f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/554ec89c-93d8-5a38-80d0-db988e17550f/attachment.md","path":"README.md","size":10167,"sha256":"4fbe4d7122dea0aa9282b252d683a99bd56a6964d7e32c6be75fd0168577e364","contentType":"text/markdown; charset=utf-8"},{"id":"addfa516-956a-5c10-9149-ca1748f41109","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/addfa516-956a-5c10-9149-ca1748f41109/attachment","path":"aigc-claw/backend/.gitignore","size":71,"sha256":"06bf6b558f1ed8885f13d76e1ca1b6762fe1549cc0b3aca70293597a6f51f98b","contentType":"text/plain; charset=utf-8"},{"id":"411ab6cb-e578-5425-83ab-9ab3c195c15d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/411ab6cb-e578-5425-83ab-9ab3c195c15d/attachment.py","path":"aigc-claw/backend/__init__.py","size":69,"sha256":"9f9effa19b724d900e404a00987e68baae0fcde2d532e21bf563862d8a0e3a15","contentType":"text/x-python; charset=utf-8"},{"id":"c032d50e-7c8b-5813-a2cd-80708131ef6b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c032d50e-7c8b-5813-a2cd-80708131ef6b/attachment.py","path":"aigc-claw/backend/api_server.py","size":44704,"sha256":"feb90214d4376b8560b422472e8649b0bed82d8b8cba1d52c99ce2e8c9d4bb9d","contentType":"text/x-python; charset=utf-8"},{"id":"93e6a5c1-a6cd-5ff6-8e13-811934b3a067","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/93e6a5c1-a6cd-5ff6-8e13-811934b3a067/attachment.py","path":"aigc-claw/backend/config.py","size":3928,"sha256":"2df8772d9a7b788a5e70bb43dfccb50540f874a2dd0361387aa670652095ca7f","contentType":"text/x-python; charset=utf-8"},{"id":"5141c3c7-75df-57e2-b4c6-727ac92af008","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5141c3c7-75df-57e2-b4c6-727ac92af008/attachment.json","path":"aigc-claw/backend/config_model.json","size":7962,"sha256":"2e3b531bb033f392a88e1a63579eae2c1d448aa9a0a012529d0f78697153f6e5","contentType":"application/json; charset=utf-8"},{"id":"894e2a35-aafc-553b-ae91-49e8cf35391f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/894e2a35-aafc-553b-ae91-49e8cf35391f/attachment.py","path":"aigc-claw/backend/config_model.py","size":1624,"sha256":"de64d120c5140d4dd8f589fb7b307cf224d7adacd6b621f642ed67d8c766fecb","contentType":"text/x-python; charset=utf-8"},{"id":"501d6a4d-09d6-5169-9be3-bb30d8cc3774","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/501d6a4d-09d6-5169-9be3-bb30d8cc3774/attachment.py","path":"aigc-claw/backend/core/__init__.py","size":127,"sha256":"dcaac51b78f7de2a43387ed60b29555d7870806c9ce44a996ce4cff534d741c8","contentType":"text/x-python; charset=utf-8"},{"id":"ac888319-9f7c-5123-98e9-1f980249c36d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ac888319-9f7c-5123-98e9-1f980249c36d/attachment.py","path":"aigc-claw/backend/core/agents/__init__.py","size":541,"sha256":"ada24b633bad81101b70212884e234a0bf0dcc64ba0fc2004589d8de1b744206","contentType":"text/x-python; charset=utf-8"},{"id":"ea8f7e82-e945-5f3e-9a99-6a878de92b33","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ea8f7e82-e945-5f3e-9a99-6a878de92b33/attachment.py","path":"aigc-claw/backend/core/agents/base_agent.py","size":1696,"sha256":"d2d1e0c0e55862c60c2f56d35eba7acf9ed2015b06eca49acf95486d5c373d63","contentType":"text/x-python; charset=utf-8"},{"id":"99a2c174-cd71-54bd-8786-a3d6da62ca3c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/99a2c174-cd71-54bd-8786-a3d6da62ca3c/attachment.py","path":"aigc-claw/backend/core/agents/character_agent.py","size":27577,"sha256":"9e6f81b18e3d7b2eed71025437b3d2f4b48c09e58eed0fa3741bd02a31c74b3e","contentType":"text/x-python; charset=utf-8"},{"id":"30ae41a5-7aa4-5738-ae49-f9fd1359ff32","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/30ae41a5-7aa4-5738-ae49-f9fd1359ff32/attachment.py","path":"aigc-claw/backend/core/agents/editor_agent.py","size":2730,"sha256":"9416dc1cf70d98590a0fe48094507045e52294696a075d0076092ce331001bd4","contentType":"text/x-python; charset=utf-8"},{"id":"14675ae5-3a60-57c9-8b3a-1bcd2d809928","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/14675ae5-3a60-57c9-8b3a-1bcd2d809928/attachment.py","path":"aigc-claw/backend/core/agents/reference_agent.py","size":31623,"sha256":"919bef65f0c206339a43f91f0d7575b4dae6add6a346fd885077e451726d85c8","contentType":"text/x-python; charset=utf-8"},{"id":"f51d069a-2f22-5649-b195-c331ebbc21cc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f51d069a-2f22-5649-b195-c331ebbc21cc/attachment.py","path":"aigc-claw/backend/core/agents/script_agent.py","size":69336,"sha256":"ec400233a0ff15c8286bb47a74414e7b494404e07672e841d1266a4312fb5589","contentType":"text/x-python; charset=utf-8"},{"id":"e726e3b9-6b72-50b4-a130-fc371047b325","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e726e3b9-6b72-50b4-a130-fc371047b325/attachment.py","path":"aigc-claw/backend/core/agents/storyboard_agent.py","size":17796,"sha256":"ae37e5155e7399028422b8b4d7e608f2aa64220b16dbaa339d3e1b216aa9821e","contentType":"text/x-python; charset=utf-8"},{"id":"908b8ab5-f335-5218-8bf9-00483556a4a0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/908b8ab5-f335-5218-8bf9-00483556a4a0/attachment.py","path":"aigc-claw/backend/core/agents/video_agent.py","size":27370,"sha256":"846a8a83b71a76bf1f1d2024d6b8dc219c27df22a2b0c391989728d5cc08a026","contentType":"text/x-python; charset=utf-8"},{"id":"47e9a712-b5f3-5637-b07d-a26d4c2d5d45","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/47e9a712-b5f3-5637-b07d-a26d4c2d5d45/attachment.py","path":"aigc-claw/backend/core/orchestrator.py","size":23177,"sha256":"3fa272f58dddd5be3d9846c0c681b596fe78102cfe0a579c5ab1e0fa75a1d247","contentType":"text/x-python; charset=utf-8"},{"id":"a34d3882-b999-5920-baae-2e0886d03197","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a34d3882-b999-5920-baae-2e0886d03197/attachment.md","path":"aigc-claw/backend/docs/api.md","size":10146,"sha256":"2339a356873a6c5b8d0c9ccccd079f05a68034f4c4af6f25babbaeafa9e489ff","contentType":"text/markdown; charset=utf-8"},{"id":"230b1b96-d0d9-5d54-80aa-f257d12857d4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/230b1b96-d0d9-5d54-80aa-f257d12857d4/attachment.md","path":"aigc-claw/backend/docs/session_format.md","size":6416,"sha256":"73ce580d997ff83a77909d5f8184ce1029441a54e9bdcf83441c02b76ef6a622","contentType":"text/markdown; charset=utf-8"},{"id":"cacbab8a-8220-541f-b131-617d45f058b1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cacbab8a-8220-541f-b131-617d45f058b1/attachment.txt","path":"aigc-claw/backend/prompts/character/character_styles.txt","size":9693,"sha256":"f1d1e5ba7243a36e9ec16e940207be7fd27d4d274bf5667282c5d59bd0882563","contentType":"text/plain; charset=utf-8"},{"id":"7d49380e-0437-5a16-b619-8ba7b378da8e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7d49380e-0437-5a16-b619-8ba7b378da8e/attachment.txt","path":"aigc-claw/backend/prompts/character/character_zh.txt","size":647,"sha256":"45b7b956d1b040ac9dd7b92fdd877a34d0d6ddcde3266169b50bdaa671c2669f","contentType":"text/plain; charset=utf-8"},{"id":"70caa215-5559-5325-8082-1e93e6ff450c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/70caa215-5559-5325-8082-1e93e6ff450c/attachment.txt","path":"aigc-claw/backend/prompts/character/eval_character_zh.txt","size":1807,"sha256":"202940cd46e23a5f6e1a6de2a524e41497a6852ea47bf5228be87f4d04390a10","contentType":"text/plain; charset=utf-8"},{"id":"82385287-ced0-58ff-8783-fa672c3c19bc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/82385287-ced0-58ff-8783-fa672c3c19bc/attachment.txt","path":"aigc-claw/backend/prompts/character/eval_select_best_zh.txt","size":1489,"sha256":"b047c8273e3383744aec788ed9ac379a4a75da24c69bfeb432b6cd7e525e81f1","contentType":"text/plain; charset=utf-8"},{"id":"465727a5-b8e7-5b61-ace8-a7716829b676","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/465727a5-b8e7-5b61-ace8-a7716829b676/attachment.py","path":"aigc-claw/backend/prompts/loader.py","size":4960,"sha256":"ed9ccbb9508ea2eaa755aa765c8055929e4279493f80707e19d6f7b407263257","contentType":"text/x-python; charset=utf-8"},{"id":"4fd20f17-0bca-5062-b9fd-594cdaf56f41","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4fd20f17-0bca-5062-b9fd-594cdaf56f41/attachment.txt","path":"aigc-claw/backend/prompts/logline/check_en.txt","size":190,"sha256":"18b77f71fda51317f82d53fbdf1e0e1097d4ced50ea7d3d759aaa93e674ecb07","contentType":"text/plain; charset=utf-8"},{"id":"1828b932-03ce-57a8-8a73-eaa339b455ca","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1828b932-03ce-57a8-8a73-eaa339b455ca/attachment.txt","path":"aigc-claw/backend/prompts/logline/check_zh.txt","size":268,"sha256":"cd8beb0a4d84e4eaad9be6926da2ba171e1287228f603066a533ba646ad9afac","contentType":"text/plain; charset=utf-8"},{"id":"9fa76d2d-add2-5343-97c0-9a3a6dcc1e28","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9fa76d2d-add2-5343-97c0-9a3a6dcc1e28/attachment.txt","path":"aigc-claw/backend/prompts/logline/extract_zh.txt","size":413,"sha256":"c1f035356cbb66d673489eecd75b9e252a604b21484d71b0f4fa11f4683dc8b3","contentType":"text/plain; charset=utf-8"},{"id":"dc89ab36-0ff6-57d4-b96d-575985c68d27","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dc89ab36-0ff6-57d4-b96d-575985c68d27/attachment.txt","path":"aigc-claw/backend/prompts/logline/generate_en.txt","size":508,"sha256":"d004b7243b4ef856dda2b8a27b15512cfa77fab31bdfb4bde34fd42f0579ad80","contentType":"text/plain; charset=utf-8"},{"id":"28a086e9-6853-5c52-bb84-c15581e77fcb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28a086e9-6853-5c52-bb84-c15581e77fcb/attachment.txt","path":"aigc-claw/backend/prompts/logline/generate_zh.txt","size":671,"sha256":"8c6a0f660caed7084aac88b5a7fab0a0a7e3a73a45ae09aa4622082a449d25eb","contentType":"text/plain; charset=utf-8"},{"id":"c4fed40d-371e-5e92-ab92-0e19e725b001","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c4fed40d-371e-5e92-ab92-0e19e725b001/attachment.txt","path":"aigc-claw/backend/prompts/reference/eval_first_frame_zh.txt","size":2432,"sha256":"ed7bf082fde412d24df0186d737a3d8b2fc3e1f524bd784f183493eab65a482e","contentType":"text/plain; charset=utf-8"},{"id":"82ba45c2-82ea-5b9e-974a-a18e749c095e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/82ba45c2-82ea-5b9e-974a-a18e749c095e/attachment.txt","path":"aigc-claw/backend/prompts/reference/eval_select_best_zh.txt","size":1749,"sha256":"8ebf7a4be5dce08507816ead4ab6c8ab94df9ba720c61c13caa7d39b32b23563","contentType":"text/plain; charset=utf-8"},{"id":"67f1b917-5be4-54a1-8f20-890515d65ca5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/67f1b917-5be4-54a1-8f20-890515d65ca5/attachment.txt","path":"aigc-claw/backend/prompts/reference/first_frame_en.txt","size":624,"sha256":"94a7ae14278f6a27f25a0557bf9e4f9ee1d7a3a5f23912b121624f3ccc85247d","contentType":"text/plain; charset=utf-8"},{"id":"767c7930-a422-5965-830c-617fa4f6c118","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/767c7930-a422-5965-830c-617fa4f6c118/attachment.txt","path":"aigc-claw/backend/prompts/reference/first_frame_zh.txt","size":600,"sha256":"04f132957f9f22dff6d2af84fefd4d64c78422c663908dcd8d9a66321e99bddd","contentType":"text/plain; charset=utf-8"},{"id":"6d13b663-9b0c-5ec6-8ab2-f258a379c4ee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6d13b663-9b0c-5ec6-8ab2-f258a379c4ee/attachment.txt","path":"aigc-claw/backend/prompts/script/beat_sheet_en.txt","size":817,"sha256":"cc571db8cfd48bb6bcd5de762230f561985bfbe670298e5265aca714f8e98c8c","contentType":"text/plain; charset=utf-8"},{"id":"0ec60aaa-7315-5d1f-b6a0-3b807064f42f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0ec60aaa-7315-5d1f-b6a0-3b807064f42f/attachment.txt","path":"aigc-claw/backend/prompts/script/beat_sheet_zh.txt","size":863,"sha256":"4e2302c349a88306f6545081079f730b2c33991a19c884ae25ef0c7a36108948","contentType":"text/plain; charset=utf-8"},{"id":"7171d06e-09ad-585b-9ced-c41ee70f32f3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7171d06e-09ad-585b-9ced-c41ee70f32f3/attachment.txt","path":"aigc-claw/backend/prompts/script/micro_beat_sheet_en.txt","size":500,"sha256":"564f7f2d005176fbe5c1f06eea8d1ba5048a9c34e72f61e0e14f98e967097f09","contentType":"text/plain; charset=utf-8"},{"id":"e56ec1f0-14b5-5fdd-88aa-7e2849d0b6d6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e56ec1f0-14b5-5fdd-88aa-7e2849d0b6d6/attachment.txt","path":"aigc-claw/backend/prompts/script/micro_beat_sheet_zh.txt","size":490,"sha256":"d7280a4c9c33329144558700a3d9e55f297cd21dbc7e29cddab17f3080322bb9","contentType":"text/plain; charset=utf-8"},{"id":"0b446ab7-3814-513b-a054-9234be320380","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0b446ab7-3814-513b-a054-9234be320380/attachment.txt","path":"aigc-claw/backend/prompts/script/micro_step_outline_zh.txt","size":1198,"sha256":"9ee4d110b0db2874386b611b26e79830ff8f2e4dfbdb0e880b4a67dbc81df420","contentType":"text/plain; charset=utf-8"},{"id":"4b4f6266-2b35-5275-9326-639bb234ef53","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4b4f6266-2b35-5275-9326-639bb234ef53/attachment.txt","path":"aigc-claw/backend/prompts/script/step_outline_en.txt","size":1242,"sha256":"3da66dad192fbc81c2959df33c152d8eaca3417009865b32a0218dc06239f918","contentType":"text/plain; charset=utf-8"},{"id":"633daf39-df1b-500d-a53b-d6a1d9036be8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/633daf39-df1b-500d-a53b-d6a1d9036be8/attachment.txt","path":"aigc-claw/backend/prompts/script/step_outline_zh.txt","size":1235,"sha256":"d10920e8b1cc4fa7ed82068e080cc85e5034973563af11a65d52befacf85c616","contentType":"text/plain; charset=utf-8"},{"id":"43a5633a-45fd-5ad1-aed1-b3dd9d10a79d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/43a5633a-45fd-5ad1-aed1-b3dd9d10a79d/attachment.txt","path":"aigc-claw/backend/prompts/script/validate_characters_zh.txt","size":1121,"sha256":"b4857d8c026fe122d9b0e95efc322498a9982e9dcd9204142b8b83f5144e0890","contentType":"text/plain; charset=utf-8"},{"id":"f7951dae-0a6a-5bef-8f8d-d33ad763337a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f7951dae-0a6a-5bef-8f8d-d33ad763337a/attachment.txt","path":"aigc-claw/backend/prompts/script/validate_settings_zh.txt","size":859,"sha256":"9ababe6167a2377244289ab20e5678886fc6fc9067b6bb28307bf9ed0c89bc43","contentType":"text/plain; charset=utf-8"},{"id":"6f37992c-3c6f-5ab9-b89f-6bf0bd4d62a6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6f37992c-3c6f-5ab9-b89f-6bf0bd4d62a6/attachment.txt","path":"aigc-claw/backend/prompts/setting/eval_select_best_zh.txt","size":1484,"sha256":"afb89d4ea2ae8aa81a600165bf040e3434724391472a546367c9fd0547a7aabf","contentType":"text/plain; charset=utf-8"},{"id":"e3f899e6-a179-58a3-b5ea-3b946885dfb6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e3f899e6-a179-58a3-b5ea-3b946885dfb6/attachment.txt","path":"aigc-claw/backend/prompts/setting/eval_setting_zh.txt","size":1639,"sha256":"ed0790b865421929957add179fc7d9f4a201b711e2b79bbb9992266248ec7402","contentType":"text/plain; charset=utf-8"},{"id":"83003a8c-3589-5920-99ff-29c245adf8c4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/83003a8c-3589-5920-99ff-29c245adf8c4/attachment.txt","path":"aigc-claw/backend/prompts/setting/setting_styles.txt","size":5294,"sha256":"954f38c7eaf285f712bd5b6c49c6bdcc6fc0c03b2bcc362cec79e0c0812a142e","contentType":"text/plain; charset=utf-8"},{"id":"4e557404-452c-5ff0-80ff-5a3e3f640775","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4e557404-452c-5ff0-80ff-5a3e3f640775/attachment.txt","path":"aigc-claw/backend/prompts/setting/setting_zh.txt","size":212,"sha256":"142b49db6e75418f7c7e4f52611de601c0bb7a9895b517dbb5e6bd5a3901573d","contentType":"text/plain; charset=utf-8"},{"id":"f876813a-515b-5a76-b305-3a97136df9bf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f876813a-515b-5a76-b305-3a97136df9bf/attachment.txt","path":"aigc-claw/backend/prompts/storyboard/continue_zh.txt","size":1681,"sha256":"855894f1ff06207c6a81e831a56036ba936b9b4f853060be99e092a69bc8dadd","contentType":"text/plain; charset=utf-8"},{"id":"a4249903-6e23-5113-b600-6a045674bccd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a4249903-6e23-5113-b600-6a045674bccd/attachment.txt","path":"aigc-claw/backend/prompts/storyboard/shot_zh.txt","size":1216,"sha256":"9a08c8d364adaca0cffe5674fe76581869c6f5442ef008a59a3e04a74b28db5b","contentType":"text/plain; charset=utf-8"},{"id":"81ecc4b2-07da-5de8-b553-a3d794c8d0e2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/81ecc4b2-07da-5de8-b553-a3d794c8d0e2/attachment.txt","path":"aigc-claw/backend/prompts/video/enhance.txt","size":1567,"sha256":"60cad69a13e42653510b0b6aa8a3b8e1a56dfd765b9a17e6fe4e5d3f8db77a7e","contentType":"text/plain; charset=utf-8"},{"id":"57a0b245-c627-592f-b5a3-9bbb9d917202","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/57a0b245-c627-592f-b5a3-9bbb9d917202/attachment.txt","path":"aigc-claw/backend/requirements.txt","size":361,"sha256":"b053d11b8c78465e45e7b48676493c02efd5ee6cd11e0c02bff76aa6ddd842c8","contentType":"text/plain; charset=utf-8"},{"id":"e0818c44-7da5-517b-b387-a6632edc040c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e0818c44-7da5-517b-b387-a6632edc040c/attachment.py","path":"aigc-claw/backend/session.py","size":2209,"sha256":"0b66a502e3cbb24521bea489435db49d16a1f8fb64d3cfeabe3b48fbd255e8fa","contentType":"text/x-python; charset=utf-8"},{"id":"a979141b-9ac7-5015-83e6-309dd42c6891","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a979141b-9ac7-5015-83e6-309dd42c6891/attachment.py","path":"aigc-claw/backend/tool/image_client.py","size":9279,"sha256":"24a5a4c5251b4754e9561ebf6bf6ecd80a87acd80c1bf4cb23bbfc29e995e396","contentType":"text/x-python; charset=utf-8"},{"id":"ee01948c-2788-5338-946c-9cf6ffe97a0d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ee01948c-2788-5338-946c-9cf6ffe97a0d/attachment.py","path":"aigc-claw/backend/tool/image_dashscope.py","size":7163,"sha256":"51d9492c0a2b757ea58255ee3e1d94022926a0426df92b37f69c1fa7da8ac387","contentType":"text/x-python; charset=utf-8"},{"id":"97ef710b-cd84-5850-96aa-66f7acdcbfae","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/97ef710b-cd84-5850-96aa-66f7acdcbfae/attachment.py","path":"aigc-claw/backend/tool/image_gpt.py","size":10434,"sha256":"a6889c8d7d79e71c180a2bd05b9c9e400c3e1e8a9a61a970fe19a6932fed3bcf","contentType":"text/x-python; charset=utf-8"},{"id":"e835bb8d-6623-5bcf-9ce1-f184bf18811c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e835bb8d-6623-5bcf-9ce1-f184bf18811c/attachment.py","path":"aigc-claw/backend/tool/image_jimeng.py","size":16196,"sha256":"95ca225908192ce49a07064f9fd123ff3ca581c424f407c2410b949f8670e286","contentType":"text/x-python; charset=utf-8"},{"id":"620da890-dd2e-5b9c-897d-690af44e06eb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/620da890-dd2e-5b9c-897d-690af44e06eb/attachment.py","path":"aigc-claw/backend/tool/image_processor.py","size":15888,"sha256":"06c9a2ea679bed97c21f2fa8e36365ec616e220937673f57f5e87743d1dee8c3","contentType":"text/x-python; charset=utf-8"},{"id":"8a0f4a04-7347-52e8-b804-ceb86a1fef00","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8a0f4a04-7347-52e8-b804-ceb86a1fef00/attachment.py","path":"aigc-claw/backend/tool/image_seedream.py","size":10131,"sha256":"7cf9d50a25d4ef0235d1f9f505768cb681c24e58b9a6564e5deb209007c5a373","contentType":"text/x-python; charset=utf-8"},{"id":"20057f12-374f-5ffc-b674-5a96a518fe66","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/20057f12-374f-5ffc-b674-5a96a518fe66/attachment.py","path":"aigc-claw/backend/tool/llm_client.py","size":5886,"sha256":"ec43bbd2b56d17fc5668917648c0dd0fafd705707097ebbc751427e683d8538d","contentType":"text/x-python; charset=utf-8"},{"id":"0d4fab6e-a070-51cc-9de1-7c1492fb16fe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0d4fab6e-a070-51cc-9de1-7c1492fb16fe/attachment.py","path":"aigc-claw/backend/tool/llm_dashscope.py","size":4567,"sha256":"44375211ed432f915eaabcac2b3dd90069b63e5fc99aebaec7d35b4c98689e99","contentType":"text/x-python; charset=utf-8"},{"id":"1098a942-1ba5-5479-816d-e5ec6eba7db7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1098a942-1ba5-5479-816d-e5ec6eba7db7/attachment.py","path":"aigc-claw/backend/tool/llm_deepseek.py","size":3373,"sha256":"49e7503eb15e25511b9082e62cec8fdc53fb422fd1c7e5097c7340ad453aa03e","contentType":"text/x-python; charset=utf-8"},{"id":"8ba8725f-5f93-51e5-a9d5-986f2415fde5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8ba8725f-5f93-51e5-a9d5-986f2415fde5/attachment.py","path":"aigc-claw/backend/tool/llm_gemini.py","size":4322,"sha256":"1aaff74f3dfd700a7e123a512413205e4507a77b59e474cddb670f9cbd61ee6e","contentType":"text/x-python; charset=utf-8"},{"id":"2cdd92a7-d055-5d57-bfb9-72ebc5a70718","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2cdd92a7-d055-5d57-bfb9-72ebc5a70718/attachment.py","path":"aigc-claw/backend/tool/llm_gpt.py","size":3507,"sha256":"969f2c4bed463c8cc395200372cc5863173b1d41b6451a62c77b3b45cd30bbf7","contentType":"text/x-python; charset=utf-8"},{"id":"d5be95c5-1403-5454-b9c3-ef59f99aa621","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5be95c5-1403-5454-b9c3-ef59f99aa621/attachment.py","path":"aigc-claw/backend/tool/relay_client.py","size":20633,"sha256":"ce9ed2c78a6d6a7c44344f12cd2d746a5c9b378426b0c59798ff1556a100f4f8","contentType":"text/x-python; charset=utf-8"},{"id":"d3492480-07a5-5228-98b0-85fe3ed73382","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d3492480-07a5-5228-98b0-85fe3ed73382/attachment.py","path":"aigc-claw/backend/tool/video_client.py","size":5794,"sha256":"f929dc5ddea3774f11dfed0eae66ba1f8fbd874b07f78ec52602846564b805a8","contentType":"text/x-python; charset=utf-8"},{"id":"b9be0213-d08a-5dbd-9bc4-a9546918475b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b9be0213-d08a-5dbd-9bc4-a9546918475b/attachment.py","path":"aigc-claw/backend/tool/video_kling.py","size":15004,"sha256":"35e71711039d64cedee20e0690687e8a0f3f3ebb6a048c8363c171e04952bbc6","contentType":"text/x-python; charset=utf-8"},{"id":"6724a0ce-2529-5bfd-9c98-c9e14c4fe4a2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6724a0ce-2529-5bfd-9c98-c9e14c4fe4a2/attachment.py","path":"aigc-claw/backend/tool/video_wan.py","size":4112,"sha256":"e475c3ad1ce79f9a040ac58279c517f9019def59121099b6420db18c2882368b","contentType":"text/x-python; charset=utf-8"},{"id":"dbc2ea56-9af6-536e-8aec-eeb787ee3e74","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dbc2ea56-9af6-536e-8aec-eeb787ee3e74/attachment.py","path":"aigc-claw/backend/tool/vlm_client.py","size":5522,"sha256":"b51e6ac01762fcb28f2cc448c6b0535e70ad41be9f75e2b4890d9e8180b40a97","contentType":"text/x-python; charset=utf-8"},{"id":"2cdba597-cd57-5a11-bc53-02210ef76cfe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2cdba597-cd57-5a11-bc53-02210ef76cfe/attachment.py","path":"aigc-claw/backend/tool/vlm_dashscope.py","size":4149,"sha256":"beaf54484a54ffd90fb4cfc91da0571d8d3101ee5877294e05b770ae4450111f","contentType":"text/x-python; charset=utf-8"},{"id":"da4290a4-4d8e-5cc2-a25a-9a2330f9d081","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/da4290a4-4d8e-5cc2-a25a-9a2330f9d081/attachment.py","path":"aigc-claw/backend/tool/vlm_gemini.py","size":5887,"sha256":"2040cb140a5d521c33564f5295b354b44bd0ebcdb5957938b6fb14766d94253c","contentType":"text/x-python; charset=utf-8"},{"id":"aae7ecc2-fdf9-5da1-9979-c83e0a9c01d2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aae7ecc2-fdf9-5da1-9979-c83e0a9c01d2/attachment","path":"aigc-claw/frontend/.gitignore","size":480,"sha256":"207e265ff4901f9ad9f96d8ce08530e04f9fc600815472a66d0a446096d654cd","contentType":"text/plain; charset=utf-8"},{"id":"bcae83b1-7e8f-5742-ad85-22b6f82b1c88","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bcae83b1-7e8f-5742-ad85-22b6f82b1c88/attachment.md","path":"aigc-claw/frontend/README.md","size":1450,"sha256":"60b55ff7df79af72590f9524208e46642bc32bdc175cdad41349681c0e2f958f","contentType":"text/markdown; charset=utf-8"},{"id":"00d57949-5114-5e82-abef-e91f8b00bb4a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/00d57949-5114-5e82-abef-e91f8b00bb4a/attachment.ico","path":"aigc-claw/frontend/app/favicon.ico","size":25931,"sha256":"2b8ad2d33455a8f736fc3a8ebf8f0bdea8848ad4c0db48a2833bd0f9cd775932","contentType":"image/vnd.microsoft.icon"},{"id":"f03d23b1-7ff0-50fc-84b1-c15763973d22","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f03d23b1-7ff0-50fc-84b1-c15763973d22/attachment.css","path":"aigc-claw/frontend/app/globals.css","size":671,"sha256":"162fed8deaa1e41b90f35645771a16e74c2478c5237608f1f5b223a5e4033d40","contentType":"text/css; charset=utf-8"},{"id":"111c1786-d015-5fc7-bf0c-c36fa87023ac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/111c1786-d015-5fc7-bf0c-c36fa87023ac/attachment.tsx","path":"aigc-claw/frontend/app/layout.tsx","size":409,"sha256":"ce793ca9abbd90175e6f3e1e9a70c43498103a6584a71e0222b17062eaa5d43c","contentType":"text/typescript; charset=utf-8"},{"id":"d8f8b9b8-9fa8-59ce-a7b5-a303845c3b2b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d8f8b9b8-9fa8-59ce-a7b5-a303845c3b2b/attachment.tsx","path":"aigc-claw/frontend/app/page.tsx","size":573,"sha256":"9cfe292674ebf67de8bf02f2fbafaa87f17a5b67fc5b5f12d868dda2a5e92d6f","contentType":"text/typescript; charset=utf-8"},{"id":"c84b5f12-59cf-5b8b-a418-24fe93e8fd5a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c84b5f12-59cf-5b8b-a418-24fe93e8fd5a/attachment.tsx","path":"aigc-claw/frontend/app/sandbox/page.tsx","size":245,"sha256":"a18cfbb371458dff3488aafa590918bb765bdb526fa26b91f3e43ef6cda6ca17","contentType":"text/typescript; charset=utf-8"},{"id":"06d6404f-76b7-5672-9a90-a9057e7060f5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/06d6404f-76b7-5672-9a90-a9057e7060f5/attachment.tsx","path":"aigc-claw/frontend/components/HomePage.tsx","size":21305,"sha256":"f74d73b1f58a657b6ed736ac1f86e62344a863004dc702ba6855c7ecf82e0dd9","contentType":"text/typescript; charset=utf-8"},{"id":"14bc9e9a-53b9-52e4-957a-7b1ddb7cf3a0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/14bc9e9a-53b9-52e4-957a-7b1ddb7cf3a0/attachment.tsx","path":"aigc-claw/frontend/components/Sandbox/Sandbox.tsx","size":28550,"sha256":"309d3d4c117d9a88e3b74c25ac2f55de18b094eb8763d6c5404d2c786b3961e8","contentType":"text/typescript; charset=utf-8"},{"id":"a86007d7-a730-55c7-b83d-e6733276d3a5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a86007d7-a730-55c7-b83d-e6733276d3a5/attachment.tsx","path":"aigc-claw/frontend/components/TopBar.tsx","size":14126,"sha256":"d00d3b1d51f36cf90d5c2bc643fc748a7192574bfb8fb7578bc40a2dbcc0a48b","contentType":"text/typescript; charset=utf-8"},{"id":"fe6dcc85-0dac-53f7-9db9-ce4c3f7829e1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fe6dcc85-0dac-53f7-9db9-ce4c3f7829e1/attachment.tsx","path":"aigc-claw/frontend/components/WorkflowPanel.tsx","size":46416,"sha256":"343ee7c677fbdb556cc7adf5af034077a8b5c4b2c567f32f9561d75c4019c18c","contentType":"text/typescript; charset=utf-8"},{"id":"21c6eda3-ac4c-5f60-bd91-c478ded6c4f1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/21c6eda3-ac4c-5f60-bd91-c478ded6c4f1/attachment.tsx","path":"aigc-claw/frontend/components/stages/CharacterStage.tsx","size":17239,"sha256":"ef0137437d6ae952a97264e5376911bf975ef7f20c83b87ba3d041961ecfab80","contentType":"text/typescript; charset=utf-8"},{"id":"6319f4cb-c0f3-5525-a9bf-82adc0737412","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6319f4cb-c0f3-5525-a9bf-82adc0737412/attachment.tsx","path":"aigc-claw/frontend/components/stages/ImageLightbox.tsx","size":7208,"sha256":"19a551d775e610f5e3964c9f463f43f6604405b70cd5bf98ee7f2b27654cb83e","contentType":"text/typescript; charset=utf-8"},{"id":"8f1ca5af-139e-5922-b380-254d7b8540d8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8f1ca5af-139e-5922-b380-254d7b8540d8/attachment.tsx","path":"aigc-claw/frontend/components/stages/PostProductionStage.tsx","size":2811,"sha256":"4674a72c022389cf47cff607e0505da0b88e3e52e97c0fa7e5eb4874bf893fd9","contentType":"text/typescript; charset=utf-8"},{"id":"2f59747b-1d49-5f68-9378-8b13d5d5d281","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2f59747b-1d49-5f68-9378-8b13d5d5d281/attachment.tsx","path":"aigc-claw/frontend/components/stages/ReferenceStage.tsx","size":18923,"sha256":"26578e6759babe15af75c41339b5e805cfeb7b925d01373371fca6bc77d8b4ae","contentType":"text/typescript; charset=utf-8"},{"id":"fc478d9e-c6de-5418-89e3-a3e477563267","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fc478d9e-c6de-5418-89e3-a3e477563267/attachment.tsx","path":"aigc-claw/frontend/components/stages/ScriptStage.tsx","size":34398,"sha256":"7862619259f7e6b4f7d2fe72fa0ebfae6ff97b2070b09183e978caa533b60ee6","contentType":"text/typescript; charset=utf-8"},{"id":"7887da71-7b92-56a1-9de9-33396681fd3c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7887da71-7b92-56a1-9de9-33396681fd3c/attachment.tsx","path":"aigc-claw/frontend/components/stages/StageActions.tsx","size":6088,"sha256":"e6cc2dc6f2a6971364347de43a63901d8f08d1e953b1a34f8f4f71c02d09398c","contentType":"text/typescript; charset=utf-8"},{"id":"1d24ee88-3ad2-5406-8faa-c38603a2f566","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1d24ee88-3ad2-5406-8faa-c38603a2f566/attachment.tsx","path":"aigc-claw/frontend/components/stages/StageProgress.tsx","size":2466,"sha256":"f7760d7b9d57531a196b204f0779ea08fd3347ac05d884a680f81d57d5cb1795","contentType":"text/typescript; charset=utf-8"},{"id":"ff3f9ae2-4175-5fcb-9f34-6372f88ca173","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ff3f9ae2-4175-5fcb-9f34-6372f88ca173/attachment.tsx","path":"aigc-claw/frontend/components/stages/StoryboardStage.tsx","size":29554,"sha256":"2b9cfd596de38ad62ba76f15bd5cafa927d1c2dc0cb354443ca9c357cf95119f","contentType":"text/typescript; charset=utf-8"},{"id":"00216aa2-c75d-545b-9ee8-b1e93ba6d292","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/00216aa2-c75d-545b-9ee8-b1e93ba6d292/attachment.tsx","path":"aigc-claw/frontend/components/stages/VideoStage.tsx","size":21347,"sha256":"de7c23684feb734eb14d265d60238798eaae855a95402f562322aed96c369bbc","contentType":"text/typescript; charset=utf-8"},{"id":"a85ed42f-d162-53eb-83d3-af056f77e0e6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a85ed42f-d162-53eb-83d3-af056f77e0e6/attachment.ts","path":"aigc-claw/frontend/components/stages/index.ts","size":442,"sha256":"99ca5d2ed4483904ca224141b77c0919b0e563da1d963aa1fe261d7714d3c186","contentType":"text/typescript; charset=utf-8"},{"id":"7d067062-5409-5ec9-a516-391e8753bffb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7d067062-5409-5ec9-a516-391e8753bffb/attachment.ts","path":"aigc-claw/frontend/components/stages/types.ts","size":1178,"sha256":"56f0bbfedf35fd7a06902a10f56687cd5104d2b87d88e0f3dd5ae370908721b6","contentType":"text/typescript; charset=utf-8"},{"id":"e24a9b41-b5dc-534f-879e-41cb179e0996","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e24a9b41-b5dc-534f-879e-41cb179e0996/attachment.ts","path":"aigc-claw/frontend/components/stages/utils.ts","size":4812,"sha256":"084858116454ce6168bafd78faa9e7a7ff7e5257a4b6eeb32270092163d686c8","contentType":"text/typescript; charset=utf-8"},{"id":"f56b4612-9285-59b1-af7a-d3f0e215c303","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f56b4612-9285-59b1-af7a-d3f0e215c303/attachment.ts","path":"aigc-claw/frontend/config/examples.ts","size":1121,"sha256":"8b53e9a14afb8b09bb8db279f020df0eb3ac293cc3f8cb5e2c8ff1fc3f4907b8","contentType":"text/typescript; charset=utf-8"},{"id":"4fcf8b98-1979-5f39-bfce-54e8d2ce99eb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4fcf8b98-1979-5f39-bfce-54e8d2ce99eb/attachment.ts","path":"aigc-claw/frontend/config/models.ts","size":5712,"sha256":"e947d3abb43dde64aa1777caba6ea03193136b8b496bfb17cc8c8e54aad037b2","contentType":"text/typescript; charset=utf-8"},{"id":"089ca382-5ae9-5b67-a932-de3cf4aa02be","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/089ca382-5ae9-5b67-a932-de3cf4aa02be/attachment.mjs","path":"aigc-claw/frontend/eslint.config.mjs","size":299,"sha256":"25c9dcf6df454830d8138f043dc81a52c036d18aa0812dabca234d109b143653","contentType":"text/javascript"},{"id":"aef742d3-3314-5f7e-a21e-0e6fc135d1c5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aef742d3-3314-5f7e-a21e-0e6fc135d1c5/attachment.ts","path":"aigc-claw/frontend/next.config.ts","size":889,"sha256":"9833972db63952a2b984f18c04f5b91c216c627b908262dd461db848497fcb34","contentType":"text/typescript; charset=utf-8"},{"id":"6a218b49-6ea3-528d-9d7e-d3e17e518396","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6a218b49-6ea3-528d-9d7e-d3e17e518396/attachment.json","path":"aigc-claw/frontend/package-lock.json","size":211989,"sha256":"748ce339d1482e9f1ec03ac07c4c7f25b0540eab1b0590c737f8c52f2b252d58","contentType":"application/json; charset=utf-8"},{"id":"5ed362c5-1656-5f7a-b5d7-7ae6b3717bbe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5ed362c5-1656-5f7a-b5d7-7ae6b3717bbe/attachment.json","path":"aigc-claw/frontend/package.json","size":627,"sha256":"285ed3a553892949095a4a288e0d56e86a03d188b1deba9274214f3b4346f5d3","contentType":"application/json; charset=utf-8"},{"id":"bcc81cbb-6034-532f-9705-d97ced3ec9f2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bcc81cbb-6034-532f-9705-d97ced3ec9f2/attachment.mjs","path":"aigc-claw/frontend/postcss.config.mjs","size":94,"sha256":"dfac7ac2d86d326a0e5adb024e7943c181393ed17a5fcb8f0315b24c7da6ddde","contentType":"text/javascript"},{"id":"fde2f469-7448-514d-89a2-176c17fcafdf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fde2f469-7448-514d-89a2-176c17fcafdf/attachment.svg","path":"aigc-claw/frontend/public/file.svg","size":391,"sha256":"2b67812c325c199a02536cdbeea0c593a72f707d323b72ee3e08dbab06753bd4","contentType":"image/svg+xml"},{"id":"99725ed4-a096-5e66-b5f4-278d92150963","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/99725ed4-a096-5e66-b5f4-278d92150963/attachment.svg","path":"aigc-claw/frontend/public/globe.svg","size":1035,"sha256":"b614b9bf183925957661ac851498fe1d8029fd43a62fbfed86f9e2624a57e7cf","contentType":"image/svg+xml"},{"id":"ac16f03d-206c-5ca6-ab08-0eb620a8c152","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ac16f03d-206c-5ca6-ab08-0eb620a8c152/attachment.jpg","path":"aigc-claw/frontend/public/logo.jpg","size":9804,"sha256":"30a32c301f6e49e49e0511f0481373057dec6bd6c225be59ec05245b028007e3","contentType":"image/jpeg"},{"id":"db9ee8ee-04b8-5fd9-8848-5b031a35a6e9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/db9ee8ee-04b8-5fd9-8848-5b031a35a6e9/attachment.svg","path":"aigc-claw/frontend/public/next.svg","size":1375,"sha256":"55995dfad6ecb4945a1e856ddca03c5e16aa5bf13fd21b4df6a74ae79357bcfc","contentType":"image/svg+xml"},{"id":"1f677ba0-8e86-579c-848d-2d43d11affd4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1f677ba0-8e86-579c-848d-2d43d11affd4/attachment.svg","path":"aigc-claw/frontend/public/vercel.svg","size":128,"sha256":"f081337b2fee635b455b63275406a3e7f39d6a014e25ad90dab5a67e62a12ac4","contentType":"image/svg+xml"},{"id":"e27be9ff-62d2-53c2-b94b-cf1949cff494","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e27be9ff-62d2-53c2-b94b-cf1949cff494/attachment.svg","path":"aigc-claw/frontend/public/window.svg","size":385,"sha256":"644768c4aaeb4767bce293344eeb0c125fb804a94d801440424072202d85e3a1","contentType":"image/svg+xml"},{"id":"a5ef8b8f-03d0-5842-9b82-b9319f2effeb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a5ef8b8f-03d0-5842-9b82-b9319f2effeb/attachment.json","path":"aigc-claw/frontend/tsconfig.json","size":713,"sha256":"81f0aa4d1b85baaf7d65f8c6949d7f55a6c234a4bce00a1ce349362dea87ca8f","contentType":"application/json; charset=utf-8"},{"id":"1ef91088-0933-5696-8088-c7ff8ea944dd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1ef91088-0933-5696-8088-c7ff8ea944dd/attachment.md","path":"references/init_project/init_all.md","size":1115,"sha256":"afb1f8b89abe838d0cc9f829441544b8827c6f951bc5013f1827c10a8315cfb2","contentType":"text/markdown; charset=utf-8"},{"id":"6e4fb5da-7bc3-51b2-a8bf-97bb93d1fb44","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6e4fb5da-7bc3-51b2-a8bf-97bb93d1fb44/attachment.md","path":"references/init_project/init_backend.md","size":1856,"sha256":"ff5ea5c7dcbada64e51b5070175521bda122a5ae2d6687f04bafd135ea5a8315","contentType":"text/markdown; charset=utf-8"},{"id":"8d6f58e7-028c-57d3-9dce-b28c3a32ceff","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8d6f58e7-028c-57d3-9dce-b28c3a32ceff/attachment.md","path":"references/init_project/init_frontend.md","size":1280,"sha256":"eabcb32492a205c9bcd13f977f40e86519faa704bf3b2c2d11f3ce88569c402b","contentType":"text/markdown; charset=utf-8"},{"id":"f23b3924-3ea6-5ea0-9e7e-e84633bdb915","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f23b3924-3ea6-5ea0-9e7e-e84633bdb915/attachment.md","path":"references/run_project/start_backend.md","size":1248,"sha256":"5d9d893d9e06e27acd45df7b48767a1ff0858129efdf02ba20c200611b57da34","contentType":"text/markdown; charset=utf-8"},{"id":"aad14333-a79a-51e1-8bb1-9150c27b41b6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aad14333-a79a-51e1-8bb1-9150c27b41b6/attachment.md","path":"references/run_project/start_frontend.md","size":1411,"sha256":"23a9d8c37ab2169dec47c492d8f7a16bbfbb36675123b189c4f6ebe10f2b5db2","contentType":"text/markdown; charset=utf-8"},{"id":"ee99cc92-d729-541d-a2c0-6f0d15acff58","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ee99cc92-d729-541d-a2c0-6f0d15acff58/attachment.md","path":"references/sandbox/generate_image_it2i.md","size":1279,"sha256":"81c5587d6c2ff1137cc0c25f34cb92676064678cf9ee6b4bf517f19363e4c11e","contentType":"text/markdown; charset=utf-8"},{"id":"989bd0a8-d471-5579-85ee-28966efdf681","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/989bd0a8-d471-5579-85ee-28966efdf681/attachment.md","path":"references/sandbox/generate_image_t2i.md","size":1051,"sha256":"89b243d09468f5b6c52ce078cb79e772acd7ae484a9c9fd86f6a78f81097e6cd","contentType":"text/markdown; charset=utf-8"},{"id":"91d948f2-9271-5939-90eb-e302a672a1bc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/91d948f2-9271-5939-90eb-e302a672a1bc/attachment.md","path":"references/sandbox/generate_video.md","size":1703,"sha256":"2d393979f3591550d20cc3278b9785418c47a2375888f14b7bc11e9cdf6b6999","contentType":"text/markdown; charset=utf-8"},{"id":"eb4daa19-44b8-55b9-98c8-8edf5f67c35e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eb4daa19-44b8-55b9-98c8-8edf5f67c35e/attachment.md","path":"references/send_message/feishu.md","size":2054,"sha256":"0891d7e0ace1273873b2c7c6bf3efc485a1d75988150e6ca5e888abc5415bc03","contentType":"text/markdown; charset=utf-8"},{"id":"e818f17d-909e-57e7-beb1-a1f4b978ca90","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e818f17d-909e-57e7-beb1-a1f4b978ca90/attachment.md","path":"references/send_message/wechat.md","size":625,"sha256":"d1db68ad0ad078534d964685602da6701f0c128a420dce67713961b34c6e81d8","contentType":"text/markdown; charset=utf-8"},{"id":"4cc7d2f5-3891-5598-937b-6753c7f8a8d5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4cc7d2f5-3891-5598-937b-6753c7f8a8d5/attachment.md","path":"references/workflow/create_character.md","size":2972,"sha256":"ba31a3d7b606fa653078c011283e7d096ebc71acd2dbc1b7c35119dd1c29dd0d","contentType":"text/markdown; charset=utf-8"},{"id":"045059b5-8981-5763-a5ed-519a11181b4b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/045059b5-8981-5763-a5ed-519a11181b4b/attachment.md","path":"references/workflow/create_post.md","size":1191,"sha256":"d01199563de0fcefc60b667d2b3f004d09de3936acbba4735d4b3953b0da1081","contentType":"text/markdown; charset=utf-8"},{"id":"c34bc499-f6c5-50d8-b682-8c15ed04803f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c34bc499-f6c5-50d8-b682-8c15ed04803f/attachment.md","path":"references/workflow/create_project.md","size":7043,"sha256":"43f6adce87e874057831c7afcd3df793d8f925b08a1f4967f6e5e825565198fa","contentType":"text/markdown; charset=utf-8"},{"id":"21575b25-d507-5fa7-a038-74bebea05dbf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/21575b25-d507-5fa7-a038-74bebea05dbf/attachment.md","path":"references/workflow/create_reference.md","size":2428,"sha256":"58129e66879b217b6edbc66d10f84658de6ae1289dfcf9ddb2239ccab532cddd","contentType":"text/markdown; charset=utf-8"},{"id":"4692ebb5-3b1c-5da4-bb26-b6b71389267d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4692ebb5-3b1c-5da4-bb26-b6b71389267d/attachment.md","path":"references/workflow/create_script.md","size":4354,"sha256":"e4521ad6433e67ced8245a010116df658013020781b95423d84b77f711485192","contentType":"text/markdown; charset=utf-8"},{"id":"4e307c7e-2026-57d9-bb3f-bc31f14cc03e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4e307c7e-2026-57d9-bb3f-bc31f14cc03e/attachment.md","path":"references/workflow/create_storyboard.md","size":2296,"sha256":"d1d0031f8ee383fb77fb56ef325e6e4c52a90a326629ba423339951d139dba02","contentType":"text/markdown; charset=utf-8"},{"id":"d9c155f9-0ada-570f-bc85-3e0a217bd504","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d9c155f9-0ada-570f-bc85-3e0a217bd504/attachment.md","path":"references/workflow/create_video.md","size":2558,"sha256":"9927db357b66e05a99c17992eafff19bb4f3c0e3df497554781e5505add06c6b","contentType":"text/markdown; charset=utf-8"},{"id":"ab084e5f-746f-521f-89ef-e6b1c95f73f6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ab084e5f-746f-521f-89ef-e6b1c95f73f6/attachment.md","path":"references/workflow/modify_character.md","size":1228,"sha256":"3212c567e65dfc9161e39524fa9723c27b4aa3198a3f76eb5b98dbdeeb6c607f","contentType":"text/markdown; charset=utf-8"},{"id":"51bcd2d3-55ea-5e5a-a484-5a06223cbf34","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/51bcd2d3-55ea-5e5a-a484-5a06223cbf34/attachment.md","path":"references/workflow/modify_reference.md","size":1085,"sha256":"6b84d0e05d083d2c310410c908793fd09f3cbfe0767b0e1d0fc5f13f6d00a78e","contentType":"text/markdown; charset=utf-8"},{"id":"b7398c07-d089-56d5-b8ad-ae9deac67ba3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b7398c07-d089-56d5-b8ad-ae9deac67ba3/attachment.md","path":"references/workflow/modify_storyboard.md","size":1315,"sha256":"779ac706fe2e6f2ff3563b78a5ea30bc26bf8defe7abfc11f58b17e7e53ae673","contentType":"text/markdown; charset=utf-8"},{"id":"a854d313-db3e-52a9-aecd-dcca5afea3ca","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a854d313-db3e-52a9-aecd-dcca5afea3ca/attachment.md","path":"references/workflow/modify_video.md","size":1080,"sha256":"41974f2e006f49905c09b344fad86f4f0352c96d93998566e3baea155ba86040","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"e4547669024e741a56836aa95c66003f5c8b204082e56285c056ca2de88b7b4f","attachment_count":129,"text_attachments":119,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":10,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/aigc-director/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"ai-agent-development","category_label":"AI"},"exact_dupes_collapsed_into_this":0},"license":"MIT License","version":"v1","category":"ai-agent-development","metadata":{"author":"Lychee","version":"1.0"},"import_tag":"clean-skills-v1","description":"AI 视频生成全流程:通过 6 个阶段(剧本→角色/场景设计→分镜→参考图→视频生成→后期剪辑)将用户想法转化为完整视频。支持临时工作台(单独调用 LLM、VLM、文生图、图生图、视频生成)。触发词:视频生成、AI视频、AIGC、创作视频、制作视频、AI画图。"}},"renderedAt":1782981075418}

AIGC-Director Agent Skill 本地运行 :这是一个 本地部署 的视频生成项目, 前后端都运行在本机 : - 后端: - 前端: - 所有 API 调用都请求本地服务器,不要请求其他地址! - 确保在调用任何 API 之前,后端和前端服务都已经启动并运行正常! 核心理念 :Agent 应该像"持续陪伴的智能视频制作助理",每完成一个用户可感知的重要任务,都应立即给用户一条简报,并等待用户确认。 核心原则 :每个阶段的产物都必须展示给用户,必须停下来等待用户确认后才能继续下一阶段。 防止遗忘 :在整个流程中,Agent 可能会忘记之前的用户输入或之前阶段的产物内容。 每当进入一个新的阶段时,Agent 都必须重新加载这篇SKILL文档,确保不会忘记任何细节 。 --- 项目结构 产物存放目录 : - - 剧本产物 - - 图片产物(角色、场景、参考图) - - 视频产物 --- 阶段与停点(共9个) | 停点 | 阶段 | phase 值 | 描述 | 操作 | |------|------|----------|------|------| | 1 | 项目配置 | - | 确认配置选项 | 展示配置 → 用户确认 | | 2 | 剧本生成 | suggest expand | 建议扩写 | 等待用户确认 | | 3 | 剧本生成 | logline sele…