commit 5de9622c8b36411a9ed643abe460a78ab7e85910 Author: saturn Date: Fri Feb 27 19:25:00 2026 +0800 release: opensource snapshot 2026-02-27 19:25:00 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c3f9185 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,53 @@ +# 版本控制 +.git +.github + +# 构建产物(builder 阶段重新生成) +.next +node_modules + +# 测试 +coverage +tests +vitest.config.ts +docker-compose.test.yml + +# 运行时数据 +logs +data +certificates +backups +uploads + +# IDE 和 AI 工具 +.vscode +.cursor +.claude +.gemini +.agent +.shared +.artifacts + +# 数据库文件 +*.db +*.db-journal +prisma/data + +# 临时文件 +.DS_Store +*.tsbuildinfo +.tmp-old-snapshot-* + +# 环境变量 +.env +.env.local +.env.*.local +.env.test + +# 文档和杂项 +*.md +*.py +debug-request.json +AGENTS.md +docs +agent diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..af43502 --- /dev/null +++ b/.env.example @@ -0,0 +1,61 @@ +# ==================== 数据库 ==================== +# Docker 模式下无需修改,docker-compose.yml 会自动覆盖 +DATABASE_URL="mysql://root:waoowaoo123@localhost:3306/waoowaoo" + +# ==================== 存储 ==================== +# local: 本地文件存储(默认,推荐) +# cos: 腾讯云COS(需要配置下方COS密钥) +STORAGE_TYPE=local + +# 如果使用 COS 存储,请填写以下配置 +# COS_SECRET_ID= +# COS_SECRET_KEY= +# COS_BUCKET= +# COS_REGION= + +# ==================== 认证 ==================== +NEXTAUTH_URL=https://localhost +NEXTAUTH_SECRET=please-change-this-to-a-random-string + +# ==================== 内部密钥 ==================== +CRON_SECRET=please-change-this-cron-secret +INTERNAL_TASK_TOKEN=please-change-this-task-token +API_ENCRYPTION_KEY=please-change-this-encryption-key + +# ==================== Redis ==================== +# Docker 模式下无需修改,docker-compose.yml 会自动覆盖 +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_USERNAME= +REDIS_PASSWORD= +REDIS_TLS= + +# ==================== Worker 配置 ==================== +WATCHDOG_INTERVAL_MS=30000 +TASK_HEARTBEAT_TIMEOUT_MS=90000 +QUEUE_CONCURRENCY_IMAGE=50 +QUEUE_CONCURRENCY_VIDEO=50 +QUEUE_CONCURRENCY_VOICE=20 +QUEUE_CONCURRENCY_TEXT=50 + +# ==================== Bull Board (任务管理面板) ==================== +BULL_BOARD_HOST=0.0.0.0 +BULL_BOARD_PORT=3010 +BULL_BOARD_BASE_PATH=/admin/queues +BULL_BOARD_USER= +BULL_BOARD_PASSWORD= + +# ==================== 日志 ==================== +LOG_UNIFIED_ENABLED=true +LOG_LEVEL=ERROR +LOG_FORMAT=json +LOG_DEBUG_ENABLED=false +LOG_AUDIT_ENABLED=true +LOG_SERVICE=waoowaoo +LOG_REDACT_KEYS=password,token,apiKey,apikey,authorization,cookie,secret,access_token,refresh_token + +# ==================== 计费 ==================== +BILLING_MODE=ENFORCE + +# ==================== 流式输出 ==================== +LLM_STREAM_EPHEMERAL_ENABLED=true diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..1cae208 --- /dev/null +++ b/.env.test @@ -0,0 +1,10 @@ +NODE_ENV=test +BILLING_MODE=OFF + +# MySQL test database +DATABASE_URL=mysql://root:root@127.0.0.1:3307/waoowaoo_test + +# Redis test instance +REDIS_HOST=127.0.0.1 +REDIS_PORT=6380 +INTERNAL_TASK_TOKEN=waoowaoo-internal-task-2026 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..68cf89e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": "next/core-web-vitals", + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "warn", + "@next/next/no-img-element": "warn", + "react-hooks/exhaustive-deps": "warn" + } +} + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3193d24 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ +## Summary + +- What changed: +- Why: + +## Architecture Plan Sync (Required) + +- Master plan file: `/Users/earth/Desktop/waoowaoo/docs/architecture-unification-master-plan.md` +- [ ] I updated the phase status board (`✅/🔄/⏸/⚠️`) in the same PR. +- [ ] I recorded concrete file-level changes under the corresponding phase. +- [ ] I kept the plan date/current-context consistent with the code changes in this PR. + +## Phase Status Delta (Required) + +| Phase | Before | After | Evidence (file path) | +| --- | --- | --- | --- | +| e.g. Phase 1.2 | 🔄 | ✅ | `src/lib/query/task-target-overlay.ts` | + +## Guardrails Checklist (Required) + +- [ ] No compatibility layer introduced. +- [ ] No silent fallback introduced. +- [ ] No hidden default or fake data introduced. +- [ ] Errors remain explicit and traceable. + +## Validation + +- Commands run: +- Manual verification: + +## Risks + +- Known impact/risk: +- Follow-up tasks: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c494b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# vercel +.vercel + +# IDE and AI tools +.vscode/ +.idea/ +.claude/ +.cursor/ +.gemini/ +.artifacts/ +.agent/ +.shared/ + +# typescript +*.tsbuildinfo +next-env.d.ts + +/src/generated/prisma + +# logs +/logs +*.log + +# environment variables +.env +.env.local +.env.*.local +docker-logs/ + +# database +*.db +*.db-journal +prisma/data/ + +# uploads (user data) +uploads/ +/data/ + +certificates + +# local temporary snapshots for old-version verification +.tmp-old-snapshot-*/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..7d41c73 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.14.0 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..474b7da --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,57 @@ +# AGENTS.md + +## 适用范围 +- 本规范适用于本仓库的所有目录与文件。 +- 若下级目录存在新的 `AGENTS.md`,下级规范仅可补充,不可弱化本文件的强约束。 + +## 项目目标与编码原则 +- 项目定位为全新系统,`统一` 与 `简洁` 是最高优先级。 +- 禁止以“兼容旧代码/旧行为”为理由引入冗余分支、兼容层、双轨逻辑或临时补丁。 +- 新功能与重构应优先服务于一致性、可维护性、可读性,而非历史包袱。 +- 禁止使用任何any类型,必须明确类型 +## 文件与模块化要求 +- 大文件必须拆分为清晰模块,按职责边界组织。 +- 单个文件若同时承担多类职责(如 UI、状态、数据请求、转换逻辑混杂)必须拆分。 +- 公共能力应抽离为可复用模块,避免复制粘贴。 +- 命名必须体现职责,目录结构应支持快速定位与阅读。 + +## 数据安全与高风险操作 +- 任何可能导致数据 `删除`、`丢失`、`覆盖`、`结构变更`、`不可逆修改` 的操作,执行前必须获得用户明确同意。 +- 未获得明确同意时,仅允许进行只读分析、方案设计与风险说明,不得落地执行。 +- 涉及数据库、文件批量改写、迁移脚本、清理脚本、覆盖写入等场景,一律按高风险处理。 +- 但可以运行测试、构建等无害的操作。 +- 可以执行测试,构建等没有毁灭性的命令 + + +## 思维与决策方法 +- 所有方案必须采用第一性原理:先明确目标、约束与事实,再推导实现路径。 +- 禁止基于“惯例如此”或“历史如此”直接做决策;必须说明核心假设与取舍依据。 +- 实现应追求最小必要复杂度,避免无效抽象与过度设计。 + +## 命令与 Git 操作限制 +- 禁止执行除 `Git 只读查询` 以外的任何命令。 +- 允许的 Git 只读操作仅包括状态与历史查询,例如:`git status`、`git log`、`git diff`、`git show`、`git branch`(只读用法)。 +- 任何会改变 Git 状态或历史的操作必须先获得用户明确同意,包括但不限于:`commit`、`push`、`pull`、`merge`、`rebase`、`cherry-pick`、`reset`、`checkout`(修改性用法)、创建/删除分支、打标签。 +- 在未获同意前,不得进行代码改写、暂存、提交、同步、回滚或历史重写。 +- 可以执行测试构建 build lint等测试命令 + +## 不掩盖任何问题 +- 不要做任何不必要的回退逻辑,特别是有可能隐藏问题的,除非用户允许,否则禁止做,如发现一个模型不可用时自动跳转到新的模型,或代码失效时,报错时直接略过,或者没有的时候提供默认值等错误操作,或制造假数据等。 +-系统执行必须遵循显式失败与零隐式回退原则:严禁静默跳过错误、隐式配置兜底或自动模型降级,确保所有非预期行为原地崩溃并如实上报。 + + +## 敢于合理质疑用户 了解用户真实需求 +- 提问以了解我真正需要什么(不仅仅是我说什么)。 +- 用户可能不够了解代码 对技术的理解可能不如你 +- 用户和你说的作为参考 而不是绝对值 如果某些事情说不通,请挑战我的假设。 + +## 测试规范 + +详细规范见 [`agent/testing.md`](agent/testing.md),以下为强制核心约束: + +- 新增功能或修改功能逻辑必须进行测试,新增功能必须增加测试,如修改文件导致需修改测试文件务必一起进行修改,确保测试百分百跟随 +- 改 worker 逻辑 / 修 bug / 加 route 或 task type → 必须写或更新测试 +- 修 bug 必须同步新增回归测试,`it()` 名称体现该 bug 场景 +- 断言必须检查具体值(DB 写入字段值、函数入参、返回值),禁止只用 `toHaveBeenCalled()` +- 禁止"自给自答":mock 返回 X 再断言 X,没有经过任何业务逻辑 +- 未通过 `npm run test:regression` 不得宣称功能完成 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..016a59a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# ==================== Stage 1: Dependencies ==================== +FROM node:20-alpine AS deps +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +# ==================== Stage 2: Build ==================== +FROM node:20-alpine AS builder +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Prisma generate + Next.js build +RUN npm run build + +# ==================== Stage 3: Production ==================== +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN apk add --no-cache tini + +# node_modules(含 devDeps,因为 npm run start 需要 concurrently + tsx) +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json + +# Next.js 构建产物 +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public + +# Prisma schema(db push 需要) +COPY --from=builder /app/prisma ./prisma + +# Worker 和 Watchdog 源码(tsx 运行 TypeScript) +COPY --from=builder /app/src ./src +COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/lib ./lib + +# 定价和配置标准 +COPY --from=builder /app/standards ./standards + +# 国际化 + 配置文件 +COPY --from=builder /app/messages ./messages +COPY --from=builder /app/tsconfig.json ./tsconfig.json +COPY --from=builder /app/next.config.ts ./next.config.ts +COPY --from=builder /app/middleware.ts ./middleware.ts +COPY --from=builder /app/postcss.config.mjs ./postcss.config.mjs + +# 本地存储数据目录 + 空 .env(tsx --env-file=.env 需要文件存在,实际 env 由 docker-compose 注入) +RUN mkdir -p /app/data/uploads /app/logs && touch /app/.env + +EXPOSE 3000 3010 + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["npm", "run", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2092c05 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +

+ waoowaoo +

+ +

+ English | 中文 +

+ +# waoowaoo AI 影视 Studio + +> ⚠️ **测试版声明**:本项目目前处于测试初期阶段,由于暂时只有我一个人开发,存在部分 bug 和不完善之处。我们正在快速迭代更新中,欢迎进群反馈问题和需求! +> +> ⚠️ **Beta Notice**: This project is in early beta. It's currently solo-developed, so bugs and rough edges exist. We're iterating fast — feel free to open an Issue! + +一款基于 AI 技术的短剧/漫画视频制作工具,支持从小说文本自动生成分镜、角色、场景,并制作成完整视频。 + +An AI-powered tool for creating short drama / comic videos — automatically generates storyboards, characters, and scenes from novel text, then assembles them into complete videos. + +--- + +## ✨ 功能特性 / Features + +| | 中文 | English | +|---|---|---| +| 🎬 | AI 剧本分析 - 自动解析小说,提取角色、场景、剧情 | AI Script Analysis - parse novels, extract characters, scenes & plot | +| 🎨 | 角色 & 场景生成 - AI 生成一致性人物和场景图片 | Character & Scene Generation - consistent AI-generated images | +| 📽️ | 分镜视频制作 - 自动生成分镜头并合成视频 | Storyboard Video - auto-generate shots and compose videos | +| 🎙️ | AI 配音 - 多角色语音合成 | AI Voiceover - multi-character voice synthesis | +| 🌐 | 多语言支持 - 中文 / 英文界面,右上角一键切换 | Bilingual UI - Chinese / English, switch in the top-right corner | + +--- + +## 🚀 快速开始 + +**前提条件**:安装 [Docker Desktop](https://docs.docker.com/get-docker/) + +```bash +git clone https://github.com/saturndec/waoowaoo.git +cd waoowaoo +docker compose up -d +``` + +访问 [http://localhost:13000](http://localhost:13000) 开始使用! + +> 首次启动会自动完成数据库初始化,无需任何额外配置。 + +> ⚠️ **如果遇到网页卡顿**:HTTP 模式下浏览器可能限制并发连接。可安装 [Caddy](https://caddyserver.com/docs/install) 启用 HTTPS: +> ```bash +> caddy run --config Caddyfile +> ``` +> 然后访问 [https://localhost:1443](https://localhost:1443) + +### 🔄 更新到最新版本 + +```bash +git fetch origin && git reset --hard origin/main +docker compose down && docker compose up -d --build +``` + +--- + +## 🚀 Quick Start + +**Prerequisites**: Install [Docker Desktop](https://docs.docker.com/get-docker/) + +```bash +git clone https://github.com/saturndec/waoowaoo.git +cd waoowaoo +docker compose up -d +``` + +Visit [http://localhost:13000](http://localhost:13000) to get started! + +> The database is initialized automatically on first launch — no extra configuration needed. + +> ⚠️ **If you experience lag**: HTTP mode may limit browser connections. Install [Caddy](https://caddyserver.com/docs/install) for HTTPS: +> ```bash +> caddy run --config Caddyfile +> ``` +> Then visit [https://localhost:1443](https://localhost:1443) + +### 🔄 Updating to the Latest Version + +```bash +git fetch origin && git reset --hard origin/main +docker compose down && docker compose up -d --build +``` + +--- + +## 🔧 API 配置 / API Configuration + +启动后进入**设置中心**配置 AI 服务的 API Key,内置配置教程。 + +After launching, go to **Settings** to configure your AI service API keys. A built-in guide is provided. + +> 💡 **推荐 / Recommended**: Tested with ByteDance Volcano Engine (Seedance, Seedream) and Google AI Studio (Banana). Text models currently require OpenRouter API. + +--- + +## 📦 技术栈 / Tech Stack + +- **Framework**: Next.js 15 + React 19 +- **Database**: MySQL + Prisma ORM +- **Queue**: Redis + BullMQ +- **Styling**: Tailwind CSS v4 +- **Auth**: NextAuth.js + +--- + +## 🤝 反馈 / Feedback + +暂不接受 Pull Request,如有问题或建议,欢迎提交 [Issue](https://github.com/saturndec/waoowaoo/issues)! + +Pull Requests are not accepted at this time. For bugs or suggestions, please open an [Issue](https://github.com/saturndec/waoowaoo/issues). + +--- + +**Made with ❤️ by waoowaoo team** diff --git a/SYSTEM_BEHAVIOR_LEVEL_TEST_MASTER_PLAN.md b/SYSTEM_BEHAVIOR_LEVEL_TEST_MASTER_PLAN.md new file mode 100644 index 0000000..00141c1 --- /dev/null +++ b/SYSTEM_BEHAVIOR_LEVEL_TEST_MASTER_PLAN.md @@ -0,0 +1,623 @@ +你必须按照目前的md文件详细执行我们的代码修改计划,且必须时刻关注,维护本次md文档,确保该文档能始终保持最新,和我们代码库保持完全一致,除非用户要求,否则默认禁止打补丁,禁止兼容层,我们需要的是简洁干净可扩展的系统,我们这个系统目前没有人用,可以一次性全量,彻底,不留遗留的修改,并且需要一次性完成所有,禁止停下,禁止自己停止任务,一次性完成所有内容。 + +# 全系统真实行为级测试替换执行主计划 +版本: v1.0 +仓库: /Users/earth/Desktop/waoowaoo +最后更新: 2026-02-25 +定位: 用真实“行为结果断言”替换结构级/字符串级测试,覆盖全系统功能回归链路 + +--- + +## 1: 项目目标 + +### 1.1 为什么要做 +当前系统历史回归集中在“链路行为错了但结构没变”的问题: +- 编辑角色/场景后字段未正确回写。 +- 上传参考图后没有按参考图生成。 +- 三视图后缀、locale、meta、referenceImages 在 route -> task -> worker 过程中丢失。 +- 前端状态看起来正常,但真实任务状态或写库结果错误。 + +现有部分测试仍是结构级(例如检查源码里是否包含 `apiHandler`、`submitTask`、`maybeSubmitLLMTask`,或者仅检查 `TASK_TYPE -> queue` 映射),这类测试无法拦截真实业务回归。 + +### 1.2 需要达到的目标 +把测试体系升级为“行为级为主、结构级为辅”: +- 每个关键功能都必须有“输入 -> 执行 -> 输出/副作用”的断言。 +- 断言必须检查具体值(写入字段值、payload 值、response 值),不接受只断言“被调用了”。 +- route、task type、worker handler 三层都要有行为级覆盖矩阵。 +- 外部 API 全 fake,不走真实高成本调用。 + +### 1.3 本次扫描结论(基于当前仓库) +- API 路由文件覆盖面: `src/app/api/**/route.ts`(全量 catalog 已维护)。 +- Worker 文件覆盖面: `src/lib/workers/handlers/*.ts` + `src/lib/workers/*.worker.ts`。 +- `tests/**/*.test.ts` 实际数量: `71`。 +- `src/lib/workers/handlers/*.ts` 文件数量: `43`(含 helper/shared/re-export 文件)。 +- `handlers` 目录中 `export async function handle...` 入口函数数量: `26`(这是 worker 行为测试的主覆盖对象)。 +- 计数口径说明: + - helper/shared/prompt-utils 文件不计入“handler 入口数”。 + - 仅 re-export 的别名文件(如 `modify-asset-image-task-handler.ts`、`image-task-handlers.ts`)不单独计入口径。 +- 已有结构级测试(需替换/下沉,已替换项会在阶段状态中标记): + - `tests/integration/api/contract/direct-submit-routes.test.ts` + - `tests/integration/api/contract/llm-observe-routes.test.ts` + - `tests/integration/api/contract/crud-routes.test.ts` + - `tests/integration/api/contract/task-infra-routes.test.ts` + - `tests/integration/chain/{text,image,video,voice}.chain.test.ts` + - `tests/unit/worker/video-worker.test.ts`(已替换为行为断言) + - `tests/unit/worker/voice-worker.test.ts`(已替换为行为断言) + - `tests/unit/optimistic/sse-invalidation.test.ts`(已替换为行为断言) + - `tests/unit/optimistic/task-target-state-map.test.ts`(已替换为行为断言) +- 已落地的行为级样板(保留并扩展): + - `tests/unit/worker/reference-to-character.test.ts` + - `tests/unit/worker/asset-hub-image-suffix.test.ts` + - `tests/unit/worker/modify-image-reference-description.test.ts` + - `tests/integration/api/specific/characters-post-reference-forwarding.test.ts` + - `tests/contracts/requirements-matrix.test.ts` + +### 1.4 修改前后的预计区别 +修改前: +- 大量“永远绿灯”风险:结构级测试通过但真实业务错误。 +- 关键回归(参考图链路、提示词后缀、写回字段)无法稳定拦截。 + +修改后: +- 结构级测试只做守卫,不作为回归主防线。 +- 行为级测试覆盖 route 入参、task payload、worker 分支、DB 写回、返回值契约。 +- 新增或修改功能时,必须补行为级用例,否则 guard 失败。 + +### 1.5 规模预估 +- 预计新增/重写测试文件: 45-70 个 +- 预计修改文件: 25-40 个 +- 预计新增代码: 9,000-16,000 行(以测试与守卫脚本为主) +- 预计执行阶段: 8 个阶段 + +--- + +## 2: 阶段+具体代码修改地方以及需要修改的内容 + +### 状态图例 +✅ 已完成 +🔄 正在执行 +⏸ 待执行 +⚠️ 问题 + +--- + +### 阶段1: 基线与约束固化 + +✅ Phase 1.1: 盘点路由、task type、worker 入口并建立 catalog。 +修改位置: +- `/Users/earth/Desktop/waoowaoo/tests/contracts/route-catalog.ts` +- `/Users/earth/Desktop/waoowaoo/tests/contracts/task-type-catalog.ts` + +✅ Phase 1.2: requirements matrix 存在性校验落地,阻断“文档写了但文件不存在”。 +修改位置: +- `/Users/earth/Desktop/waoowaoo/tests/contracts/requirements-matrix.ts` +- `/Users/earth/Desktop/waoowaoo/tests/contracts/requirements-matrix.test.ts` + +✅ Phase 1.3: 定义“行为级测试判定标准”并加入守卫。 +要改内容: +- 新增 `/Users/earth/Desktop/waoowaoo/tests/contracts/behavior-test-standard.md` +- 新增 `/Users/earth/Desktop/waoowaoo/scripts/guards/test-behavior-quality-guard.mjs` +硬性规则: +- 禁止只断言 `toHaveBeenCalled()` +- 必须断言具体 payload/data 字段值或返回值 +- 禁止在 contract/chain 目录内读取源码文本做契约主断言 + +✅ Phase 1.3.a: 后端 Worker 单元测试硬规范已写入本主计划(本文件第 3 章)。 +当前状态: +- 规范文本已固化 +- 自动化守卫脚本已落地(Phase 1.3 完成) + +⚠️ Phase 1.4: 历史结构级测试较多,改造期间可能出现“同名文件语义变化”导致误解。 +处理策略: +- 每次改造完成后,在本文件执行日志记录“此文件已由结构级改为行为级”。 + +--- + +### 阶段2: API 契约从结构级替换为行为级 + +依赖关系: +- Phase 2 可先行推进(route 行为契约)。 +- Phase 3 与 Phase 4 依赖 Phase 2 的 route 输入输出基线稳定。 + +✅ Phase 2.1: 重写 direct-submit contract 为真实调用断言。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/integration/api/contract/direct-submit-routes.test.ts` +必须断言: +- 未登录 401 +- 参数缺失 400(错误码一致) +- 正常请求返回 `{ taskId, async: true }` +- `submitTask` 入参包含 `type/targetType/targetId/payload/locale` + +✅ Phase 2.2: 重写 llm-observe contract 为真实调用断言。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/integration/api/contract/llm-observe-routes.test.ts` +必须断言: +- `maybeSubmitLLMTask` 入参正确透传 +- `displayMode/flow/meta` 不丢失 +- 越权请求被拒绝 + +✅ Phase 2.3: 重写 crud contract 为真实行为断言(已补齐 asset-hub + novel-promotion 写回断言)。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/integration/api/contract/crud-routes.test.ts` +必须断言: +- PATCH 后数据库字段值确实变化 +- DELETE 后实体不存在 +- 无权限用户无法操作他人资源 + +✅ Phase 2.4: 重写 task-infra contract 为真实行为断言(已补 SSE 终态事件序列断言)。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/integration/api/contract/task-infra-routes.test.ts` +必须断言: +- dismiss 后任务状态变化 +- task-target-state 与任务终态一致 +- SSE 事件序列含终态事件 + +⏸ Phase 2.5: 扩展 route specific 测试,补关键历史回归点。 +新增/扩展: +- `/Users/earth/Desktop/waoowaoo/tests/integration/api/specific/reference-to-character-api.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/integration/api/specific/characters-post-reference-forwarding.test.ts`(已完成,继续扩展) +- `/Users/earth/Desktop/waoowaoo/tests/integration/api/specific/characters-post.test.ts` + +--- + +### 阶段3: Worker 决策测试全量行为化 + +依赖关系: +- Phase 3 依赖 Phase 2(route 契约稳定后再固化 worker 结果断言)。 + +✅ Phase 3.1: 关键历史 bug 已有行为级样板落地。 +已完成文件: +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/reference-to-character.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/asset-hub-image-suffix.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/modify-image-reference-description.test.ts` + +✅ Phase 3.2: 把“失败快照类”worker 测试升级为“结果断言类”。 +优先重写: +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/image-task-handlers-core.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/script-to-storyboard.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/episode-split.test.ts` +必须断言: +- 具体生成参数(referenceImages/aspectRatio/resolution) +- 具体写库字段值(description/imageUrl/imageUrls/selectedIndex) +- 关键分支(character/location/storyboard)均触发 + +✅ Phase 3.3: 新增核心 handler 行为测试文件(按模块拆分,已全部落地)。 +新增文件: +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/character-image-task-handler.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/location-image-task-handler.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/panel-image-task-handler.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/panel-variant-task-handler.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/story-to-script.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/screenplay-convert.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/voice-design.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/voice-analyze.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/analyze-novel.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/analyze-global.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/character-profile.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/clips-build.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/asset-hub-ai-design.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/asset-hub-ai-modify.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/llm-proxy.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-tasks.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-variants.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-prompt-appearance.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-prompt-location.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-prompt-shot.test.ts` +当前进度: +- 已完成: `character-image-task-handler`、`location-image-task-handler`、`panel-image-task-handler`、`panel-variant-task-handler`、`story-to-script`、`screenplay-convert`、`voice-design`、`voice-analyze`、`analyze-novel`、`analyze-global`、`character-profile`、`clips-build`、`asset-hub-ai-design`、`asset-hub-ai-modify`、`llm-proxy`、`shot-ai-tasks`、`shot-ai-variants`、`shot-ai-prompt-appearance`、`shot-ai-prompt-location`、`shot-ai-prompt-shot` +- 待完成: 无(Phase 3.3 范围内) + +⚠️ Phase 3.3.a: 边界说明(避免误算)。 +不纳入“handler 入口测试清单”的文件: +- `llm-stream.ts`(stream context/callback helper) +- `modify-asset-image-task-handler.ts`(re-export 别名) +- `image-task-handlers.ts`(re-export 聚合) + +✅ Phase 3.4: worker 入口层行为测试替换 routing-only 断言。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/video-worker.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/worker/voice-worker.test.ts` +必须断言: +- 任务类型分发到正确 handler +- handler 结果被正确回传与封装 +- 失败分支日志与错误码一致 + +⚠️ Phase 3.5: 避免“mock 自己返回答案”造成假安全。 +硬要求: +- 每个测试至少 1 个断言检查具体字段值(不是调用次数) +- 对 DB update/create 入参做 `objectContaining(data: ...)` 断言 + +--- + +### 阶段4: Chain 测试从队列映射升级为端到端行为链路 + +依赖关系: +- Phase 4 依赖 Phase 2 + Phase 3(先稳定 route 和 handler 行为,再做链路端到端)。 + +✅ Phase 4.1: 重写 image chain(enqueue + worker 消费 + 持久化写回断言已落地)。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/integration/chain/image.chain.test.ts` +覆盖链路: +- route -> submitTask -> queue -> image worker -> DB 回写 +示例断言: +- 任务状态从 queued -> processing -> completed +- 目标实体 imageUrl/imageUrls 有值且结构正确 + +✅ Phase 4.2: 重写 text chain(enqueue + worker 消费 + 结果级边界断言已落地)。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/integration/chain/text.chain.test.ts` +覆盖链路: +- analyze/story/script/reference-to-character 全链路关键节点 + +✅ Phase 4.3: 重写 video chain(enqueue + video worker 消费 + lip-sync 持久化断言已落地)。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/integration/chain/video.chain.test.ts` +覆盖链路: +- generate-video/lip-sync 任务执行结果与状态持久化 + +✅ Phase 4.4: 重写 voice chain(enqueue + voice worker 消费 + 关键参数透传断言已落地)。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/integration/chain/voice.chain.test.ts` +覆盖链路: +- voice-design/voice-generate 的实体写回与任务状态 + +⏸ Phase 4.5: 固化外部 fake 层,保证零真实外网请求。 +使用/扩展: +- `/Users/earth/Desktop/waoowaoo/tests/helpers/fakes/llm.ts` +- `/Users/earth/Desktop/waoowaoo/tests/helpers/fakes/media.ts` +- `/Users/earth/Desktop/waoowaoo/tests/helpers/fakes/providers.ts` + +--- + +### 阶段5: 前端状态回归测试行为化 + +✅ Phase 5.1: 替换源码字符串检查为 hook 真实行为测试。 +重写文件: +- `/Users/earth/Desktop/waoowaoo/tests/unit/optimistic/sse-invalidation.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/optimistic/task-target-state-map.test.ts` +必须断言: +- 给定事件序列时 query invalidation 实际触发条件正确 +- target state map 在 queued/processing/completed/failed 下输出正确 + +✅ Phase 5.2: 现有 optimistic mutation 行为测试保留并扩展。 +文件: +- `/Users/earth/Desktop/waoowaoo/tests/unit/optimistic/asset-hub-mutations.test.ts` +- `/Users/earth/Desktop/waoowaoo/tests/unit/optimistic/project-asset-mutations.test.ts` + +--- + +### 阶段6: 覆盖矩阵升级为“行为测试矩阵” + +✅ Phase 6.1: 新增 route 行为覆盖矩阵。 +新增: +- `/Users/earth/Desktop/waoowaoo/tests/contracts/route-behavior-matrix.ts` +要求: +- 117 个 route 每个都映射到至少 1 条行为级 caseId + test 文件 + +✅ Phase 6.2: 新增 task type 行为覆盖矩阵。 +新增: +- `/Users/earth/Desktop/waoowaoo/tests/contracts/tasktype-behavior-matrix.ts` +要求: +- 37 个 TASK_TYPE 每个都映射 worker 行为测试 + chain 行为测试 + +✅ Phase 6.3: 新增矩阵守卫脚本。 +新增: +- `/Users/earth/Desktop/waoowaoo/scripts/guards/test-behavior-route-coverage-guard.mjs` +- `/Users/earth/Desktop/waoowaoo/scripts/guards/test-behavior-tasktype-coverage-guard.mjs` + +⚠️ Phase 6.4: 矩阵维护成本高。 +策略: +- 优先通过脚本自动校验文件存在与 caseId 唯一性 +- 每次新增 route/tasktype 必须更新矩阵,否则 CI 失败 + +--- + +### 阶段7: CI 门禁与执行策略 + +✅ Phase 7.1: 新增行为级门禁命令。 +修改: +- `/Users/earth/Desktop/waoowaoo/package.json` +新增脚本: +- `test:behavior:unit` +- `test:behavior:api` +- `test:behavior:chain` +- `test:behavior:guards` +- `test:behavior:full` + +⏸ Phase 7.2: PR workflow 强制执行行为级全量门禁。 +修改: +- `/Users/earth/Desktop/waoowaoo/.github/workflows/test-regression-pr.yml` + +✅ Phase 7.3: 失败诊断脚本已接入(保留)。 +文件: +- `/Users/earth/Desktop/waoowaoo/scripts/test-regression-runner.sh` + +--- + +### 阶段8: 收口与冻结 + +⏸ Phase 8.1: 删除/降级旧结构级测试(仅保留轻量守卫,不计入行为覆盖率)。 +目标: +- contract/chain 中不再有“只读源码字符串”的主断言 + +⏸ Phase 8.2: 建立“新增功能必须附行为测试”的提交流程。 +落地: +- PR 模板加检查项 +- guard 失败提示明确指出缺失 case + +✅ Phase 8.3: 冻结基线并发布“行为级测试开发规范”。 +新增: +- `/Users/earth/Desktop/waoowaoo/docs/testing/behavior-test-guideline.md` + +⚠️ Phase 8.4: 不可达目标声明。 +说明: +- “100% 无 bug”不可证明;可达目标是“100% 关键功能链路行为覆盖 + 关键字段结果断言 + 变更自动门禁”。 + +--- + +### 阶段9: Billing 与并发测试纳入总蓝图 + +🔄 Phase 9.1: billing 现有测试纳入“行为级总体覆盖说明”,避免遗漏域。 +覆盖现状: +- `tests/unit/billing/*.test.ts` +- `tests/integration/billing/*.integration.test.ts` +- `tests/concurrency/billing/ledger.concurrency.test.ts` + +⏸ Phase 9.2: 明确 billing worker/ledger 行为级断言增强点。 +新增/重写方向: +- 计费写账一致性(usage->ledger)字段级断言 +- 异常重试/幂等行为断言 +- 并发写入冲突场景断言 + +⏸ Phase 9.3: 将 billing 与 concurrency 纳入 `test:behavior:full` 报告维度。 +要求: +- 输出 billing/concurrency 独立通过率 +- 与 route/worker/chain 覆盖率同级展示 + +--- + +## 3: 后端 Worker 单元测试硬规范(强制) + +### 3.1 必须覆盖的测试类型 +每个 worker handler 必须至少包含三类用例: +1. 失败路径:参数缺失/格式错误时,抛出正确错误信息。 +2. 成功路径:正常输入时,副作用结果正确(数据库写入/关键调用参数/返回值)。 +3. 关键分支:`if/else` 分支每条至少 1 个用例。 + +### 3.2 Mock 规范 +必须 Mock: +1. `prisma` 等数据库访问。 +2. LLM/图像生成/视觉分析等 AI 调用。 +3. COS/上传等文件存储。 +4. 外部 HTTP 请求。 +5. 一切需要网络的依赖。 + +不能 Mock: +1. 待测业务逻辑函数本身。 +2. 项目内业务常量(例如 `CHARACTER_PROMPT_SUFFIX`),必须直接 import 使用。 + +### 3.3 断言规范(最高优先级) +每个 `it()` 必须断言“结果”,不能只断言“过程”。 + +必须断言: +1. 数据库 `update/create` 的具体字段值(如 `description`、`imageUrl`、`imageUrls`)。 +2. AI/生成函数收到的核心参数(如 `prompt` 必含内容)。 +3. 图像生成相关关键参数(如 `referenceImages`、`aspectRatio`、`resolution`)。 + +弱断言限制: +1. `toHaveBeenCalled()` 不能作为唯一主断言。 +2. `toHaveBeenCalledTimes(N)` 仅在“次数本身有业务意义”时使用。 + +### 3.4 测试数据规范 +1. 数据必须能触发目标分支(例如“有参考图/无参考图”分别建用例)。 +2. 关键业务字段必须使用有语义的固定值。 +3. 无关透传字段可用占位值(如 `task-1`)。 + +禁止模式: +1. “自己给答案自己验证”:mock 返回值与断言目标完全同源。 +2. 正确做法:mock AI 返回值,断言该值被写入到 `prisma.update({ data })` 的具体字段。 + +### 3.5 it() 结构模板(强制推荐) +```ts +it('[条件] -> [预期结果]', async () => { + // 1. 准备 mock(仅覆盖本场景差异) + // 2. 构造 job/payload(只给本场景关键字段) + // 3. 执行 handler + // 4. 断言: + // a. DB data 字段 + // b. 核心调用参数(prompt/referenceImages/aspectRatio) + // c. 返回值关键字段(如 success) +}) +``` + +### 3.6 命名规范 +统一格式:`[条件] -> [预期结果]` +示例: +1. `没有 extraImageUrls -> 不调用分析,description 不更新` +2. `有 extraImageUrls -> AI 分析结果写入 description` +3. `AI 调用失败 -> 主流程成功且 description 不被污染` +4. `缺少必填参数 -> 抛出包含字段名的错误信息` + +### 3.7 一条 bug 一条测试(强制) +1. 每修复一个 bug,必须新增至少一条对应回归测试。 +2. 测试名必须可追溯该 bug 场景(例如“防止 XXX 回归”)。 +3. 未补测试不得标记该 bug 任务完成。 + +--- + +### 执行日志(必须持续追加) +格式: +- [YYYY-MM-DD HH:mm] 状态变更: <旧状态> -> <新状态> +- [YYYY-MM-DD HH:mm] 修改文件: <绝对路径列表> +- [YYYY-MM-DD HH:mm] 运行命令: <命令> +- [YYYY-MM-DD HH:mm] 结果: <通过/失败 + 摘要> +- [YYYY-MM-DD HH:mm] 问题: <若有> + +- [2026-02-25 21:59] 状态变更: Phase 3.1 ⏸ -> ✅ +- [2026-02-25 21:59] 修改文件: /Users/earth/Desktop/waoowaoo/tests/unit/worker/reference-to-character.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/asset-hub-image-suffix.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/modify-image-reference-description.test.ts, /Users/earth/Desktop/waoowaoo/src/lib/workers/handlers/reference-to-character.ts +- [2026-02-25 21:59] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker/reference-to-character.test.ts tests/unit/worker/asset-hub-image-suffix.test.ts tests/unit/worker/modify-image-reference-description.test.ts +- [2026-02-25 21:59] 结果: 关键历史回归点(后缀失效/参考图描述不更新)已行为级可测 +- [2026-02-25 21:59] 问题: 无 + +- [2026-02-25 22:00] 状态变更: Phase 1.2 ⏸ -> ✅ +- [2026-02-25 22:00] 修改文件: /Users/earth/Desktop/waoowaoo/tests/contracts/requirements-matrix.ts, /Users/earth/Desktop/waoowaoo/tests/contracts/requirements-matrix.test.ts +- [2026-02-25 22:00] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/contracts/requirements-matrix.test.ts +- [2026-02-25 22:00] 结果: 阻断不存在测试路径引用(已修复 `crud-asset-hub-routes.test.ts` 错误引用) +- [2026-02-25 22:00] 问题: 无 + +- [2026-02-25 22:10] 状态变更: Phase 1.3.a ⏸ -> ✅ +- [2026-02-25 22:10] 修改文件: /Users/earth/Desktop/waoowaoo/SYSTEM_BEHAVIOR_LEVEL_TEST_MASTER_PLAN.md +- [2026-02-25 22:10] 运行命令: 文档更新(无测试执行) +- [2026-02-25 22:10] 结果: 已将后端 Worker 单元测试硬规范(覆盖/Mock/断言/命名/一 bug 一测试)固化为主计划强制章节 +- [2026-02-25 22:10] 问题: 自动化守卫脚本仍待实现(Phase 1.3) + +- [2026-02-25 22:20] 状态变更: 文档校正(扫描计数与范围修正) +- [2026-02-25 22:20] 修改文件: /Users/earth/Desktop/waoowaoo/SYSTEM_BEHAVIOR_LEVEL_TEST_MASTER_PLAN.md +- [2026-02-25 22:20] 运行命令: rg --files/rg -n 扫描 tests 与 handlers +- [2026-02-25 22:20] 结果: 已修正 test 文件数=51、handlers 文件数=43、handler 入口数=26;补齐 Phase 3.3 遗漏 handler;新增 Phase 依赖关系与 Phase 9(billing/concurrency) +- [2026-02-25 22:20] 问题: Phase 1.3 自动守卫脚本尚未实现 + +- [2026-02-25 23:05] 状态变更: Phase 2.1 🔄 -> ✅, Phase 2.2 ⏸ -> ✅ +- [2026-02-25 23:05] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/direct-submit-routes.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/llm-observe-routes.test.ts +- [2026-02-25 23:05] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api/contract/direct-submit-routes.test.ts tests/integration/api/contract/llm-observe-routes.test.ts +- [2026-02-25 23:05] 结果: 两类 contract 测试已由结构级改为行为级并通过,覆盖 16 个 direct-submit routes 与 22 个 llm-observe routes +- [2026-02-25 23:05] 问题: 无 + +- [2026-02-25 23:06] 状态变更: Phase 2.3 ⏸ -> 🔄, Phase 2.4 ⏸ -> 🔄 +- [2026-02-25 23:06] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/crud-routes.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/task-infra-routes.test.ts +- [2026-02-25 23:06] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api/contract/crud-routes.test.ts tests/integration/api/contract/task-infra-routes.test.ts +- [2026-02-25 23:06] 结果: 已替换为真实 route 调用断言;crud 完成鉴权行为覆盖,task-infra 完成鉴权/参数/核心成功路径,后续补 DB 写回与 SSE 终态序列 +- [2026-02-25 23:06] 问题: 无 + +- [2026-02-25 23:06] 状态变更: Phase 3.2 🔄 -> ✅, Phase 3.4 ⏸ -> ✅ +- [2026-02-25 23:06] 修改文件: /Users/earth/Desktop/waoowaoo/tests/unit/worker/image-task-handlers-core.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/episode-split.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/script-to-storyboard.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/video-worker.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/voice-worker.test.ts +- [2026-02-25 23:06] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker/script-to-storyboard.test.ts tests/unit/worker/video-worker.test.ts tests/unit/worker/voice-worker.test.ts tests/unit/worker/image-task-handlers-core.test.ts tests/unit/worker/episode-split.test.ts +- [2026-02-25 23:06] 结果: worker 测试已升级为结果级断言,覆盖失败路径、成功路径、关键分支与关键写库字段 +- [2026-02-25 23:06] 问题: 无 + +- [2026-02-25 23:07] 状态变更: Phase 4.2 ⏸ -> 🔄, Phase 4.3 ⏸ -> 🔄, Phase 4.4 ⏸ -> 🔄 +- [2026-02-25 23:07] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/chain/image.chain.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/chain/text.chain.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/chain/video.chain.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/chain/voice.chain.test.ts +- [2026-02-25 23:07] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/chain/image.chain.test.ts tests/integration/chain/text.chain.test.ts tests/integration/chain/video.chain.test.ts tests/integration/chain/voice.chain.test.ts +- [2026-02-25 23:07] 结果: chain 测试已由映射断言升级为 addTaskJob enqueue 行为断言(校验 queue 选择 + jobId/priority) +- [2026-02-25 23:07] 问题: route->worker->DB 端到端链路仍待补 + +- [2026-02-25 23:08] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api/contract tests/integration/chain tests/unit/worker +- [2026-02-25 23:08] 结果: 16 个测试文件全部通过,117/117 测试通过 + +- [2026-02-25 23:09] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/crud-routes.test.ts +- [2026-02-25 23:09] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api/contract/crud-routes.test.ts +- [2026-02-25 23:09] 结果: 新增 CRUD 结果级断言(PATCH 写入字段值、DELETE 删除调用与越权 403),从“仅鉴权检查”升级为“含写库行为检查” +- [2026-02-25 23:09] 问题: novel-promotion 侧 CRUD 的字段级断言仍待扩展 + +- [2026-02-25 23:09] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/task-infra-routes.test.ts +- [2026-02-25 23:09] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api/contract/task-infra-routes.test.ts +- [2026-02-25 23:09] 结果: 新增 SSE replay 成功路径断言(`text/event-stream`、`last-event-id` 回放、channel 订阅行为) +- [2026-02-25 23:09] 问题: SSE 终态事件的 completed/failed 序列断言仍待补 + +- [2026-02-25 23:10] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api/contract tests/integration/chain tests/unit/worker +- [2026-02-25 23:10] 结果: 16 个测试文件全部通过,120/120 测试通过 + +- [2026-02-25 23:11] 状态变更: Phase 1.3 🔄 -> ✅ +- [2026-02-25 23:11] 修改文件: /Users/earth/Desktop/waoowaoo/tests/contracts/behavior-test-standard.md, /Users/earth/Desktop/waoowaoo/scripts/guards/test-behavior-quality-guard.mjs, /Users/earth/Desktop/waoowaoo/package.json +- [2026-02-25 23:11] 运行命令: node scripts/guards/test-behavior-quality-guard.mjs && npm run check:test-coverage-guards +- [2026-02-25 23:11] 结果: 行为级质量守卫已接入(拦截源码字符串契约 + 弱断言),并纳入 `check:test-coverage-guards` +- [2026-02-25 23:11] 问题: 无 + +- [2026-02-25 23:12] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/direct-submit-routes.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/llm-observe-routes.test.ts +- [2026-02-25 23:12] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api/contract/direct-submit-routes.test.ts tests/integration/api/contract/llm-observe-routes.test.ts +- [2026-02-25 23:12] 结果: 两个 contract 测试新增 `toHaveBeenCalledWith(objectContaining(...))` 强断言,通过行为质量守卫 +- [2026-02-25 23:12] 问题: 无 + +- [2026-02-25 23:13] 状态变更: Phase 5.1 ⏸ -> ✅ +- [2026-02-25 23:13] 修改文件: /Users/earth/Desktop/waoowaoo/tests/unit/optimistic/sse-invalidation.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/optimistic/task-target-state-map.test.ts +- [2026-02-25 23:13] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/optimistic/sse-invalidation.test.ts tests/unit/optimistic/task-target-state-map.test.ts +- [2026-02-25 23:13] 结果: 两个 optimistic 结构级测试已替换为行为级(SSE 终态 invalidation 与 target-state overlay 合并规则) +- [2026-02-25 23:13] 问题: 无 + +- [2026-02-25 23:16] 状态变更: Phase 3.3 ⏸ -> 🔄 +- [2026-02-25 23:16] 修改文件: /Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-tasks.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/voice-design.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/asset-hub-ai-design.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/asset-hub-ai-modify.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-prompt-appearance.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-prompt-location.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-prompt-shot.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/shot-ai-variants.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/llm-proxy.test.ts +- [2026-02-25 23:16] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker/shot-ai-tasks.test.ts tests/unit/worker/voice-design.test.ts tests/unit/worker/asset-hub-ai-design.test.ts tests/unit/worker/asset-hub-ai-modify.test.ts tests/unit/worker/shot-ai-prompt-appearance.test.ts tests/unit/worker/shot-ai-prompt-location.test.ts tests/unit/worker/shot-ai-prompt-shot.test.ts tests/unit/worker/shot-ai-variants.test.ts tests/unit/worker/llm-proxy.test.ts +- [2026-02-25 23:16] 结果: 新增 9 个 worker 行为测试文件(20 条用例+5 条用例),覆盖 shot-ai 分发、prompt 修改链路、asset-hub ai 设计/修改、voice-design、llm-proxy 显式失败 +- [2026-02-25 23:16] 问题: 无 + +- [2026-02-25 23:16] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker +- [2026-02-25 23:16] 结果: worker 套件通过,17 文件 / 48 测试通过 + +- [2026-02-25 23:17] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/optimistic tests/unit/worker tests/integration/api/contract tests/integration/chain +- [2026-02-25 23:17] 结果: 全回归分组通过,31 文件 / 155 测试通过 + +- [2026-02-25 23:25] 修改文件: /Users/earth/Desktop/waoowaoo/tests/unit/worker/story-to-script.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/screenplay-convert.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/analyze-novel.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/analyze-global.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/voice-analyze.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/clips-build.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/character-profile.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/character-image-task-handler.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/location-image-task-handler.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/panel-image-task-handler.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/panel-variant-task-handler.test.ts +- [2026-02-25 23:25] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker/story-to-script.test.ts tests/unit/worker/screenplay-convert.test.ts tests/unit/worker/analyze-novel.test.ts tests/unit/worker/analyze-global.test.ts tests/unit/worker/voice-analyze.test.ts tests/unit/worker/clips-build.test.ts tests/unit/worker/character-profile.test.ts tests/unit/worker/character-image-task-handler.test.ts tests/unit/worker/location-image-task-handler.test.ts tests/unit/worker/panel-image-task-handler.test.ts tests/unit/worker/panel-variant-task-handler.test.ts +- [2026-02-25 23:25] 结果: 新增 11 个 worker handler 行为测试文件,覆盖剩余未落地入口(文本链路 + 图片链路),失败路径/成功路径/关键分支断言全部落地 +- [2026-02-25 23:25] 问题: 首轮运行出现 5 个断言问题(重试分支 mock 泄漏与断言过窄),已在同轮修复 + +- [2026-02-25 23:26] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker +- [2026-02-25 23:26] 结果: worker 套件通过,28 文件 / 76 测试通过 + +- [2026-02-25 23:26] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/optimistic tests/unit/worker tests/integration/api/contract tests/integration/chain +- [2026-02-25 23:26] 结果: 全回归分组通过,42 文件 / 183 测试通过 + +- [2026-02-25 23:27] 状态变更: Phase 3.3 🔄 -> ✅ +- [2026-02-25 23:27] 运行命令: npm run check:test-coverage-guards +- [2026-02-25 23:27] 结果: 覆盖守卫通过(behavior quality / route=117 / taskType=37) +- [2026-02-25 23:27] 问题: 无 + +- [2026-02-25 23:27] 运行命令: rg \"export async function handle\" src/lib/workers/handlers -l + tests/unit/worker import 对账 +- [2026-02-25 23:27] 结果: 26/26 handler 入口均存在对应 worker 行为测试文件引用 +- [2026-02-25 23:27] 问题: 无 + +- [2026-02-25 23:46] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/crud-routes.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/task-infra-routes.test.ts +- [2026-02-25 23:46] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api/contract/crud-routes.test.ts tests/integration/api/contract/task-infra-routes.test.ts +- [2026-02-25 23:46] 结果: CRUD 合同新增 novel-promotion 写回断言(select-character-image / select-location-image / clips PATCH),task-infra 新增 SSE channel 终态事件序列断言(processing -> completed) +- [2026-02-25 23:46] 问题: 无 + +- [2026-02-25 23:46] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/chain/image.chain.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/chain/text.chain.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/chain/video.chain.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/chain/voice.chain.test.ts +- [2026-02-25 23:46] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/chain/image.chain.test.ts tests/integration/chain/text.chain.test.ts tests/integration/chain/video.chain.test.ts tests/integration/chain/voice.chain.test.ts +- [2026-02-25 23:46] 结果: 4 个 chain 文件由“仅 queue 映射”升级为“queue payload -> worker 消费 -> 结果/写回断言” +- [2026-02-25 23:46] 问题: 无 + +- [2026-02-25 23:47] 修改文件: /Users/earth/Desktop/waoowaoo/tests/contracts/route-behavior-matrix.ts, /Users/earth/Desktop/waoowaoo/tests/contracts/tasktype-behavior-matrix.ts, /Users/earth/Desktop/waoowaoo/scripts/guards/test-behavior-route-coverage-guard.mjs, /Users/earth/Desktop/waoowaoo/scripts/guards/test-behavior-tasktype-coverage-guard.mjs, /Users/earth/Desktop/waoowaoo/package.json, /Users/earth/Desktop/waoowaoo/tests/contracts/task-type-catalog.ts, /Users/earth/Desktop/waoowaoo/docs/testing/behavior-test-guideline.md +- [2026-02-25 23:47] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/optimistic tests/unit/worker tests/integration/api/contract tests/integration/chain && npm run check:test-coverage-guards +- [2026-02-25 23:47] 结果: 分组回归通过(42 文件 / 191 测试),覆盖门禁通过(behavior quality + route 117 + taskType 37 + behavior matrices) +- [2026-02-25 23:47] 问题: 无 + +- [2026-02-25 23:51] 运行命令: npm run test:behavior:full +- [2026-02-25 23:51] 结果: 行为级全链路命令通过(guards + unit + api + chain);unit=39 文件/107 测试,api=4 文件/93 测试,chain=4 文件/12 测试 +- [2026-02-25 23:51] 问题: unit 辅助测试阶段出现本地 Redis 连接拒绝日志(127.0.0.1:6380)但不影响用例通过,后续可按需优化为静默 mock + +--- + +## 4: 验证策略 + +### 4.1 可量化验收目标(全部必须达成) +1. Route 行为覆盖率: `117/117`(每个 route 至少 1 个行为级用例)。 +2. TASK_TYPE 行为覆盖率: `37/37`(每个 task type 至少 1 个 worker 行为用例 + 1 个 chain 行为用例)。 +3. 结构级 contract/chain 主断言占比: `0%`(不得再以源码字符串匹配作为主断言)。 +4. 关键回归场景覆盖: `100%`(参考图链路、后缀链路、编辑写回链路、task state 链路)。 +5. 外部真实调用次数: `0`(测试环境必须全 fake)。 +6. PR 门禁: `100%` 执行 `test:behavior:full`,任一缺失即失败。 +7. Worker 用例规范符合率: `100%`(每个 worker 测试文件均满足 3.1~3.7 规则)。 +8. Billing + Concurrency 维度通过率: `100%`(纳入统一验收报告)。 + +### 4.2 核心验证命令 +- `npm run test:guards` +- `cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/unit/worker` +- `cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/unit/helpers` +- `cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/api` +- `cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/chain` +- `npm run test:pr` + +### 4.3 用例质量验证(防假绿灯) +每个新增行为测试必须至少满足两条: +1. 断言具体业务字段值(例如 `description/imageUrls/locale/meta/referenceImages`)。 +2. 覆盖至少一个历史回归分支。 +3. 覆盖一个失败分支(权限/参数/模型未配置)。 +4. 不使用“mock 自己返回结果并直接断言调用次数”的空测试模式。 + +--- + +## 5: 备注 + +1. 本文档是“行为级测试替换计划”,与 `SYSTEM_REGRESSION_COVERAGE_MASTER_PLAN.md` 并行存在;冲突时以“行为级优先”原则执行。 +2. 本计划默认不引入兼容层与静默回退,错误必须显式暴露。 +3. 新接手模型必须先阅读本文件,再执行代码修改;执行后必须回写执行日志。 +4. 如果出现“测试通过但线上仍回归”,优先审计断言是否为结果级而不是调用级。 diff --git a/SYSTEM_REGRESSION_COVERAGE_MASTER_PLAN.md b/SYSTEM_REGRESSION_COVERAGE_MASTER_PLAN.md new file mode 100644 index 0000000..0f55fb4 --- /dev/null +++ b/SYSTEM_REGRESSION_COVERAGE_MASTER_PLAN.md @@ -0,0 +1,590 @@ +你必须按照目前的md文件详细执行我们的代码修改计划,且必须时刻关注,维护本次md文档,确保该文档能始终保持最新,和我们代码库保持完全一致,除非用户要求,否则默认禁止打补丁,禁止兼容层,我们需要的是简洁干净可扩展的系统,我们这个系统目前没有人用,可以一次性全量,彻底,不留遗留的修改,并且需要一次性完成所有,禁止停下,禁止自己停止任务,一次性完成所有内容。 + +# 全系统回归测试执行主计划 +版本: v1.0 +仓库: /Users/earth/Desktop/waoowaoo +最后更新: 2026-02-25 +责任模式: 单一主计划文档驱动(本文件是唯一事实来源) + +## 0. 文档维护协议(强制) +1. 每次开始任何代码工作前,必须先更新“2:阶段+具体任务”的状态。 +2. 每次完成一个任务,必须同步更新为 ✅ 并记录变更文件与命令。 +3. 任何发现的阻塞必须写入 ⚠️ 问题区,不允许口头跳过。 +4. 不允许引入兼容层、双轨逻辑、静默回退、假成功。 +5. 新增功能或改动若未补测试,禁止标记为完成。 +6. 新模型/新窗口接手时,只允许依据本文件继续执行,不依赖历史聊天上下文。 + +--- + +## 1: 项目目标 + +### 1.1 为什么要做 +系统存在“功能经常回归”的问题,核心症状是跨层链路被重构破坏而未被及时发现,例如: +- 编辑角色/场景后未正确持久化。 +- 上传参考图成功但生成阶段未正确使用参考图参数。 +- 路由参数、任务 payload、worker 决策之间发生字段漂移。 +- 任务状态和前端感知状态(target state/SSE)出现不一致。 + +### 1.2 目标定义 +建立覆盖全系统的、可持续维护的自动化测试体系,确保: +- 所有关键功能都有自动化回归防线。 +- 所有任务链路变更都会被测试阻断。 +- 每个 PR 都执行全量门禁(已确定策略)。 + +### 1.3 当前上下文快照(仓库事实) +- API 路由总数: 117 +- maybeSubmitLLMTask 路由: 22 +- 直接 submitTask 路由: 16 +- TASK_TYPE 数量: 37 +- worker handlers 数量: 43 +- 现有测试主要在 billing 域: + - unit: 18 + - integration: 5 + - concurrency: 1 + +### 1.4 修改前 vs 修改后(预期差异) +修改前: +- 测试集中于 billing,系统级回归无法被稳定阻断。 +- 缺少全域 route 契约覆盖与任务类型覆盖矩阵。 +- 缺少 route -> queue -> worker 的全链路契约测试。 + +修改后: +- 建立“契约驱动沙漏模型”全系统测试架构。 +- 建立 route/task-type/requirement 覆盖矩阵与守卫脚本。 +- 每次 PR 全量执行并门禁。 +- 外部 API 统一 fake,避免高成本与不稳定性。 + +### 1.5 规模预估(用于排期) +- 预计新增文件: 55-80 +- 预计修改文件: 20-35 +- 预计新增代码: 8,000-14,000 行(以测试与测试基建为主) +- 预计总阶段: 8 阶段 + +--- + +## 2: 阶段+具体代码修改地方以及需要修改的内容 + +### 2.0 状态图例 +🟩✅ 已完成 +🟦🔄 正在执行 +🟨⏸ 待执行 +🟥⚠️ 问题/阻塞 + +--- + +### 阶段1: 基线收敛与测试基建 + +🟩✅ Phase 1.1: 完成仓库现状盘点(route/taskType/worker 数量与入口路径)。 +🟩✅ Phase 1.2: 完成测试策略决策锁定(全域门禁 + 全 fake + 每次 PR 全量)。 +🟩✅ Phase 1.3: 建立主计划文档并作为唯一执行入口。 +🟩✅ Phase 1.4: 扩展 tests/helpers/db-reset.ts 为 resetSystemState(),覆盖任务域+资产域+novel-promotion 域。 +🟩✅ Phase 1.5: 新增 tests/helpers/request.ts(统一 NextRequest 构造)。 +🟩✅ Phase 1.6: 新增 tests/helpers/auth.ts(mock requireUserAuth/requireProjectAuth/requireProjectAuthLight)。 +🟩✅ Phase 1.7: 新增 tests/helpers/fixtures.ts(用户、项目、角色、场景、分镜、任务测试数据工厂)。 +🟩✅ Phase 1.8: 当前 global-setup/global-teardown 仅围绕 BILLING_TEST_BOOTSTRAP,需升级为 system test bootstrap 约定。 + +--- + +### 阶段2: 覆盖矩阵与守卫(防止漏测) + +🟩✅ Phase 2.1: 新增 tests/contracts/route-catalog.ts,登记 117 个 route。 +🟩✅ Phase 2.2: 新增 tests/contracts/task-type-catalog.ts,登记 37 个 TASK_TYPE。 +🟩✅ Phase 2.3: 新增 tests/contracts/requirements-matrix.ts,建立需求 -> 测试用例映射。 +🟩✅ Phase 2.4: 新增 scripts/guards/test-route-coverage-guard.mjs,强制 route 必有契约测试登记。 +🟩✅ Phase 2.5: 新增 scripts/guards/test-tasktype-coverage-guard.mjs,强制 TASK_TYPE 必有测试映射。 +🟩✅ Phase 2.6: 在 package.json 增加 check:test-coverage-guards 并纳入 test:pr。 +🟥⚠️ Phase 2.7: 若 route 变化频繁,catalog 维护成本会上升,需要自动生成校验脚本降低维护负担。 + +--- + +### 阶段3: L1 纯单元测试(高频回归逻辑锁定) + +🟩✅ Phase 3.1: 新增 tests/unit/helpers/route-task-helpers.test.ts。 +修改点: src/lib/llm-observe/route-task.ts +覆盖点: parseSyncFlag / shouldRunSyncTask / resolveDisplayMode / resolvePositiveInteger。 + +🟩✅ Phase 3.2: 新增 tests/unit/helpers/task-submitter-helpers.test.ts。 +修改点: src/lib/task/submitter.ts +覆盖点: normalizeTaskPayload 的 flowId/flowStageIndex/flowStageTotal/meta 回退逻辑。 + +🟩✅ Phase 3.3: 新增 tests/unit/helpers/reference-to-character-helpers.test.ts。 +修改点: src/lib/workers/handlers/reference-to-character-helpers.ts +覆盖点: parseReferenceImages / readString / readBoolean / 上限截断与空值过滤。 + +🟩✅ Phase 3.4: 新增 tests/unit/helpers/task-state-service.test.ts。 +修改点: src/lib/task/state-service.ts +覆盖点: phase 决策、intent 归一化、错误归一化、progress 边界。 + +🟩✅ Phase 3.5: 需要确保不使用 any,必要时先补充内部类型导出。 + +--- + +### 阶段4: L2 API 契约集成测试(全系统主防线之一) + +🟩✅ Phase 4.1: 新增 tests/integration/api/helpers/call-route.ts。 +目标: 统一 route 调用入口,减少重复模板代码。 + +🟩✅ Phase 4.2: 新增 tests/integration/api/contract/llm-observe-routes.test.ts。 +覆盖范围: 22 个 maybeSubmitLLMTask 路由。 +共同断言: 未登录/越权/参数错误/成功返回 taskId + async。 + +🟩✅ Phase 4.3: 新增 tests/integration/api/contract/direct-submit-routes.test.ts。 +覆盖范围: 16 个直接 submitTask 路由。 +共同断言: payload 入队契约、billing/locale/flow meta 关键字段存在。 + +🟩✅ Phase 4.4: 新增 tests/integration/api/contract/crud-routes.test.ts。 +覆盖范围: asset-hub + novel-promotion CRUD 路由。 +共同断言: DB 真值变化、字段映射不漂移、权限拦截。 + +🟩✅ Phase 4.6: 新增 tests/integration/api/contract/task-infra-routes.test.ts。 +覆盖范围: /api/tasks, /api/tasks/[taskId], /api/tasks/dismiss, /api/task-target-states, /api/sse。 +共同断言: 状态读取、取消、dismiss、target-state 结果结构。 + +🟥⚠️ Phase 4.7: 117 route 全覆盖耗时高,若单进程过慢需按组拆分命令并并行 CI job(不降覆盖)。 + +--- + +### 阶段5: L3 Worker 决策单元测试(全系统主防线之二) + +🟩✅ Phase 5.1: 新增 tests/unit/worker/reference-to-character.test.ts。 +覆盖: extractOnly / customDescription / useReferenceImages / backgroundJob 分支。 + +🟩✅ Phase 5.2: 新增 tests/unit/worker/image-task-handlers-core.test.ts。 +覆盖: referenceImages 注入、resolution/aspectRatio 选择、目标实体分支(character/location/storyboard)。 + +🟩✅ Phase 5.3: 新增 tests/unit/worker/script-to-storyboard.test.ts。 +覆盖: step orchestration、JSON parse 失败路径、voice line 匹配合法性校验。 + +🟩✅ Phase 5.4: 新增 tests/unit/worker/episode-split.test.ts。 +覆盖: 分集数量边界、错误输入显式失败、输出结构一致性。 + +🟩✅ Phase 5.5: 新增 tests/unit/worker/video-worker.test.ts 与 voice-worker.test.ts。 +覆盖: 必填 payload 校验、外部轮询超时、持久化字段更新。 + +🟥⚠️ Phase 5.6: 若 mock 粒度过粗会掩盖问题,必须在断言中校验“被调用参数内容”而非仅校验调用次数。 + +--- + +### 阶段6: L4 全链路契约测试(route -> queue -> worker) + +🟩✅ Phase 6.1: 新增 tests/integration/chain/text.chain.test.ts。 +场景: ai-create-character、reference-to-character 全链路。 + +🟩✅ Phase 6.2: 新增 tests/integration/chain/image.chain.test.ts。 +场景: generate-image、modify-image 全链路。 + +🟩✅ Phase 6.3: 新增 tests/integration/chain/video.chain.test.ts。 +场景: generate-video、lip-sync 全链路。 + +🟩✅ Phase 6.4: 新增 tests/integration/chain/voice.chain.test.ts。 +场景: voice-design、voice-generate 全链路。 + +🟩✅ Phase 6.5: 新增 tests/helpers/fakes/llm.ts、tests/helpers/fakes/media.ts、tests/helpers/fakes/providers.ts。 +要求: 外部调用全部 fake,禁止真实外网消耗。 + +🟩✅ Phase 6.6: 需要在测试环境加“网络闸门”,防止误打真实外部 API。 + +--- + +### 阶段7: 前端状态回归测试(轻量,不做重 E2E) + +🟩✅ Phase 7.1: 扩展 tests/unit/optimistic/asset-hub-mutations.test.ts。 +覆盖: 并发操作回滚冲突、缓存一致性。 + +🟩✅ Phase 7.2: 新增 tests/unit/optimistic/task-target-state-map.test.ts。 +覆盖: queued/processing/completed/failed 对 UI 状态映射。 + +🟩✅ Phase 7.3: 新增 tests/unit/optimistic/sse-invalidation.test.ts。 +覆盖: 仅终态触发 target-state 刷新,不允许轮询回退。 + +🟥⚠️ Phase 7.4: 不引入高成本浏览器 E2E,避免与“全 fake、低成本”策略冲突。 + +--- + +### 阶段8: CI 门禁与回归收口 + +🟩✅ Phase 8.1: 更新 package.json,新增命令。 +建议命令: +- test:guards +- test:unit:all +- test:integration:api +- test:integration:chain +- test:pr +- test:regression + +🟩✅ Phase 8.2: 调整 CI,每次 PR 执行 test:pr(全量门禁)。 +🟩✅ Phase 8.3: 回归失败输出标准化(失败文件、失败断言、首次引入 commit)。 +🟩✅ Phase 8.4: 设置完成判定条件,满足后冻结基线。 + +🟥⚠️ Phase 8.5: 全量 PR 门禁会拉长反馈时间,需要预留 CI 资源并做缓存优化。 + +--- + +### 执行日志(每次执行后必须追加) +格式: +- [YYYY-MM-DD HH:mm] 状态变更: <任务ID> <旧状态> -> <新状态> +- [YYYY-MM-DD HH:mm] 修改文件: <绝对路径列表> +- [YYYY-MM-DD HH:mm] 运行命令: <命令> +- [YYYY-MM-DD HH:mm] 结果: <通过/失败 + 摘要> +- [YYYY-MM-DD HH:mm] 问题: <若有> + +- [2026-02-25 10:00] 状态变更: Phase 1.3/1.4/1.5/1.6/1.7 🔄/⏸ -> ✅ +- [2026-02-25 10:00] 修改文件: /Users/earth/Desktop/waoowaoo/tests/helpers/db-reset.ts, /Users/earth/Desktop/waoowaoo/tests/helpers/request.ts, /Users/earth/Desktop/waoowaoo/tests/helpers/auth.ts, /Users/earth/Desktop/waoowaoo/tests/helpers/fixtures.ts +- [2026-02-25 10:00] 运行命令: git show/git ls-tree(只读盘点) +- [2026-02-25 10:00] 结果: 已完成测试基础 helper 与系统重置扩展 +- [2026-02-25 10:00] 问题: Phase 1.8 仍需推进全系统 bootstrap 统一 + +- [2026-02-25 10:05] 状态变更: Phase 2.1/2.2/2.3/2.4/2.5/2.6 ⏸ -> ✅ +- [2026-02-25 10:05] 修改文件: /Users/earth/Desktop/waoowaoo/tests/contracts/route-catalog.ts, /Users/earth/Desktop/waoowaoo/tests/contracts/task-type-catalog.ts, /Users/earth/Desktop/waoowaoo/tests/contracts/requirements-matrix.ts, /Users/earth/Desktop/waoowaoo/scripts/guards/test-route-coverage-guard.mjs, /Users/earth/Desktop/waoowaoo/scripts/guards/test-tasktype-coverage-guard.mjs, /Users/earth/Desktop/waoowaoo/package.json +- [2026-02-25 10:05] 运行命令: git show/git ls-tree(只读盘点) +- [2026-02-25 10:05] 结果: 覆盖矩阵与守卫脚本落地,新增 test:pr/test:regression 入口 +- [2026-02-25 10:05] 问题: 需在 CI workflow 文件接入 test:pr(Phase 8.2) + +- [2026-02-25 10:10] 状态变更: Phase 3.1/3.2/3.3/3.4/3.5 ⏸/⚠️ -> ✅ +- [2026-02-25 10:10] 修改文件: /Users/earth/Desktop/waoowaoo/src/lib/llm-observe/route-task.ts, /Users/earth/Desktop/waoowaoo/src/lib/task/submitter.ts, /Users/earth/Desktop/waoowaoo/src/lib/task/state-service.ts, /Users/earth/Desktop/waoowaoo/tests/unit/helpers/*.test.ts +- [2026-02-25 10:10] 运行命令: 待执行测试命令验证 +- [2026-02-25 10:10] 结果: 关键 pure helper 单测已落地,核心函数可测性增强 +- [2026-02-25 10:10] 问题: 无 + +- [2026-02-25 10:15] 状态变更: Phase 4.1/4.2/4.3/4.4/4.5/4.6 ⏸ -> ✅ +- [2026-02-25 10:15] 修改文件: /Users/earth/Desktop/waoowaoo/tests/integration/api/helpers/call-route.ts, /Users/earth/Desktop/waoowaoo/tests/integration/api/contract/*.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/api/specific/*.test.ts +- [2026-02-25 10:15] 运行命令: 待执行测试命令验证 +- [2026-02-25 10:15] 结果: API 契约分组测试模板已落地并接入 catalog +- [2026-02-25 10:15] 问题: 动态 DB 真值契约仍需持续加深 + +- [2026-02-25 10:20] 状态变更: Phase 5.1/5.2/5.3/5.4/5.5, Phase 6.1/6.2/6.3/6.4/6.5/6.6, Phase 7.2/7.3, Phase 8.1 ⏸ -> ✅ +- [2026-02-25 10:20] 修改文件: /Users/earth/Desktop/waoowaoo/tests/unit/worker/*.test.ts, /Users/earth/Desktop/waoowaoo/tests/integration/chain/*.test.ts, /Users/earth/Desktop/waoowaoo/tests/helpers/fakes/*.ts, /Users/earth/Desktop/waoowaoo/tests/setup/env.ts, /Users/earth/Desktop/waoowaoo/tests/unit/optimistic/task-target-state-map.test.ts, /Users/earth/Desktop/waoowaoo/tests/unit/optimistic/sse-invalidation.test.ts +- [2026-02-25 10:20] 运行命令: 待执行测试命令验证 +- [2026-02-25 10:20] 结果: Worker/Chain/Optimistic 第一批回归防线与网络闸门已落地 +- [2026-02-25 10:20] 问题: Phase 7.1 与 Phase 8.2/8.3/8.4 仍需推进 + +- [2026-02-25 10:25] 状态变更: Phase 1.8 ⚠️ -> ✅, Phase 8.2 ⏸ -> ✅ +- [2026-02-25 10:25] 修改文件: /Users/earth/Desktop/waoowaoo/tests/setup/global-setup.ts, /Users/earth/Desktop/waoowaoo/tests/setup/global-teardown.ts, /Users/earth/Desktop/waoowaoo/.github/workflows/test-regression-pr.yml +- [2026-02-25 10:25] 运行命令: npm run test:pr +- [2026-02-25 10:25] 结果: test:guards/test:unit:all/test:billing:integration/test:integration:api/test:integration:chain 全部通过 +- [2026-02-25 10:25] 问题: 单测过程仍会出现 Redis 连接拒绝日志噪音(不影响通过) + +- [2026-02-25 10:30] 状态变更: Phase 8.3/8.4 ⏸ -> ✅ +- [2026-02-25 10:30] 修改文件: /Users/earth/Desktop/waoowaoo/scripts/test-regression-runner.sh, /Users/earth/Desktop/waoowaoo/package.json +- [2026-02-25 10:30] 运行命令: npm run test:guards +- [2026-02-25 10:30] 结果: 回归失败统一诊断脚本已接入 test:pr,guard 通过 +- [2026-02-25 10:30] 问题: 无 + +- [2026-02-25 10:40] 状态变更: 回归门禁验收执行 +- [2026-02-25 10:40] 修改文件: /Users/earth/Desktop/waoowaoo/SYSTEM_REGRESSION_COVERAGE_MASTER_PLAN.md +- [2026-02-25 10:40] 运行命令: npm run test:pr +- [2026-02-25 10:40] 结果: 全链路门禁通过(test:guards、test:unit:all、test:billing:integration、test:integration:api、test:integration:chain) +- [2026-02-25 10:40] 问题: 测试日志中仍有 Redis 连接拒绝噪音(不影响通过) + +- [2026-02-25 22:00] 状态变更: Phase 5.1 结果断言增强 + 回归缺口修复 +- [2026-02-25 22:00] 修改文件: /Users/earth/Desktop/waoowaoo/src/lib/workers/handlers/reference-to-character.ts, /Users/earth/Desktop/waoowaoo/tests/unit/worker/reference-to-character.test.ts, /Users/earth/Desktop/waoowaoo/tests/contracts/requirements-matrix.ts, /Users/earth/Desktop/waoowaoo/tests/contracts/requirements-matrix.test.ts, /Users/earth/Desktop/waoowaoo/SYSTEM_REGRESSION_COVERAGE_MASTER_PLAN.md +- [2026-02-25 22:00] 运行命令: BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker/reference-to-character.test.ts tests/unit/worker/asset-hub-image-suffix.test.ts tests/unit/worker/modify-image-reference-description.test.ts tests/integration/api/specific/characters-post-reference-forwarding.test.ts tests/contracts/requirements-matrix.test.ts && BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker +- [2026-02-25 22:00] 结果: 关键回归链路测试通过;新增 requirements matrix 完整性断言可阻断不存在测试文件引用;worker 全套通过 +- [2026-02-25 22:00] 问题: worker 单测日志仍有 Redis ECONNREFUSED 噪音(断言通过,不影响结果) + +--- + +## 4: 验证策略 + +### 4.1 可量化验收指标(必须全部达成) +1. route 契约覆盖率 = 117/117(100%)。 +2. TASK_TYPE 覆盖率 = 37/37(100%)。 +3. 4 类队列链路测试均存在且通过(text/image/video/voice)。 +4. 每个 PR 全量门禁执行并通过。 +5. 无真实外网调用(测试日志与网络闸门双重确认)。 +6. 关键高频回归场景(编辑类、参考图类、任务状态类)均有自动化用例。 +7. 新增/修改 route 或 TASK_TYPE 时,若未补测试,guard 必须失败。 + +### 4.2 命令级验证 +- `npm run test:guards` +- `cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/unit` +- `cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/api` +- `cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/chain` +- `npm run test:pr` + +### 4.3 结果记录 +每轮执行后在执行日志追加: +- 总用例数 +- 失败数 +- 失败模块 +- 修复后重跑结果 + +--- + +## 5: 备注(可选但建议) + +1. 本计划不依赖历史对话,可由任意新模型直接接手。 +2. 若用户策略改变(例如允许少量 live canary),必须先更新本文件“策略锁定项”,再动代码。 +3. 若发现范围蔓延,优先维护“矩阵完整性”而不是临时加测。 +4. 任何“为了通过测试而加回退”的方案一律禁止。 +5. 所有测试代码保持强类型,不允许 any。 +你必须按照目前的md文件详细执行我们的代码修改计划,且必须时刻关注,维护本次md文档,确保该文档能始终保持最新,和我们代码库保持完全一致,除非用户要求,否则默认禁止打补丁,禁止兼容层,我们需要的是简洁干净可扩展的系统,我们这个系统目前没有人用,可以一次性全量,彻底,不留遗留的修改,并且需要一次性完成所有,禁止停下,禁止自己停止任务,一次性完成所有内容。 + +# 全系统回归测试执行主计划 +版本: v1.0 +仓库: /Users/earth/Desktop/waoowaoo +最后更新: 2026-02-25 +责任模式: 单一主计划文档驱动(本文件是唯一事实来源) + +## 0. 文档维护协议(强制) +1. 每次开始任何代码工作前,必须先更新“2:阶段+具体任务”的状态。 +2. 每次完成一个任务,必须同步更新为 ✅ 并记录变更文件与命令。 +3. 任何发现的阻塞必须写入 ⚠️ 问题区,不允许口头跳过。 +4. 不允许引入兼容层、双轨逻辑、静默回退、假成功。 +5. 新增功能或改动若未补测试,禁止标记为完成。 +6. 新模型/新窗口接手时,只允许依据本文件继续执行,不依赖历史聊天上下文。 + +--- + +## 1: 项目目标 + +### 1.1 为什么要做 +系统存在“功能经常回归”的问题,核心症状是跨层链路被重构破坏而未被及时发现,例如: +- 编辑角色/场景后未正确持久化。 +- 上传参考图成功但生成阶段未正确使用参考图参数。 +- 路由参数、任务 payload、worker 决策之间发生字段漂移。 +- 任务状态和前端感知状态(target state/SSE)出现不一致。 + +### 1.2 目标定义 +建立覆盖全系统的、可持续维护的自动化测试体系,确保: +- 所有关键功能都有自动化回归防线。 +- 所有任务链路变更都会被测试阻断。 +- 每个 PR 都执行全量门禁(已确定策略)。 + +### 1.3 当前上下文快照(仓库事实) +- API 路由总数: 117 +- maybeSubmitLLMTask 路由: 22 +- 直接 submitTask 路由: 16 +- TASK_TYPE 数量: 49 +- worker handlers 数量: 43 +- 现有测试主要在 billing 域: + - unit: 18 + - integration: 5 + - concurrency: 1 + +### 1.4 修改前 vs 修改后(预期差异) +修改前: +- 测试集中于 billing,系统级回归无法被稳定阻断。 +- 缺少全域 route 契约覆盖与任务类型覆盖矩阵。 +- 缺少 route -> queue -> worker 的全链路契约测试。 + +修改后: +- 建立“契约驱动沙漏模型”全系统测试架构。 +- 建立 route/task-type/requirement 覆盖矩阵与守卫脚本。 +- 每次 PR 全量执行并门禁。 +- 外部 API 统一 fake,避免高成本与不稳定性。 + +### 1.5 规模预估(用于排期) +- 预计新增文件: 55-80 +- 预计修改文件: 20-35 +- 预计新增代码: 8,000-14,000 行(以测试与测试基建为主) +- 预计总阶段: 8 阶段 + +--- + +## 2: 阶段+具体代码修改地方以及需要修改的内容 + +### 2.0 状态图例 +🟩✅ 已完成 +🟦🔄 正在执行 +🟨⏸ 待执行 +🟥⚠️ 问题/阻塞 + +--- + +### 阶段1: 基线收敛与测试基建 + +🟩✅ Phase 1.1: 完成仓库现状盘点(route/taskType/worker 数量与入口路径)。 +🟩✅ Phase 1.2: 完成测试策略决策锁定(全域门禁 + 全 fake + 每次 PR 全量)。 +🟦🔄 Phase 1.3: 建立主计划文档并作为唯一执行入口。 +🟨⏸ Phase 1.4: 扩展 tests/helpers/db-reset.ts 为 resetSystemState(),覆盖任务域+资产域+novel-promotion 域。 +🟨⏸ Phase 1.5: 新增 tests/helpers/request.ts(统一 NextRequest 构造)。 +🟨⏸ Phase 1.6: 新增 tests/helpers/auth.ts(mock requireUserAuth/requireProjectAuth/requireProjectAuthLight)。 +🟨⏸ Phase 1.7: 新增 tests/helpers/fixtures.ts(用户、项目、角色、场景、分镜、任务测试数据工厂)。 +🟥⚠️ Phase 1.8: 当前 global-setup/global-teardown 仅围绕 BILLING_TEST_BOOTSTRAP,需升级为 system test bootstrap 约定。 + +--- + +### 阶段2: 覆盖矩阵与守卫(防止漏测) + +🟨⏸ Phase 2.1: 新增 tests/contracts/route-catalog.ts,登记 117 个 route。 +🟨⏸ Phase 2.2: 新增 tests/contracts/task-type-catalog.ts,登记 49 个 TASK_TYPE。 +🟨⏸ Phase 2.3: 新增 tests/contracts/requirements-matrix.ts,建立需求 -> 测试用例映射。 +🟨⏸ Phase 2.4: 新增 scripts/guards/test-route-coverage-guard.mjs,强制 route 必有契约测试登记。 +🟨⏸ Phase 2.5: 新增 scripts/guards/test-tasktype-coverage-guard.mjs,强制 TASK_TYPE 必有测试映射。 +🟨⏸ Phase 2.6: 在 package.json 增加 check:test-coverage-guards 并纳入 test:pr。 +🟥⚠️ Phase 2.7: 若 route 变化频繁,catalog 维护成本会上升,需要自动生成校验脚本降低维护负担。 + +--- + +### 阶段3: L1 纯单元测试(高频回归逻辑锁定) + +🟨⏸ Phase 3.1: 新增 tests/unit/helpers/route-task-helpers.test.ts。 +修改点: src/lib/llm-observe/route-task.ts +覆盖点: parseSyncFlag / shouldRunSyncTask / resolveDisplayMode / resolvePositiveInteger。 + +🟨⏸ Phase 3.2: 新增 tests/unit/helpers/task-submitter-helpers.test.ts。 +修改点: src/lib/task/submitter.ts +覆盖点: normalizeTaskPayload 的 flowId/flowStageIndex/flowStageTotal/meta 回退逻辑。 + +🟨⏸ Phase 3.3: 新增 tests/unit/helpers/reference-to-character-helpers.test.ts。 +修改点: src/lib/workers/handlers/reference-to-character-helpers.ts +覆盖点: parseReferenceImages / readString / readBoolean / 上限截断与空值过滤。 + +🟨⏸ Phase 3.4: 新增 tests/unit/helpers/task-state-service.test.ts。 +修改点: src/lib/task/state-service.ts +覆盖点: phase 决策、intent 归一化、错误归一化、progress 边界。 + +🟥⚠️ Phase 3.5: 需要确保不使用 any,必要时先补充内部类型导出。 + +--- + +### 阶段4: L2 API 契约集成测试(全系统主防线之一) + +🟨⏸ Phase 4.1: 新增 tests/integration/api/helpers/call-route.ts。 +目标: 统一 route 调用入口,减少重复模板代码。 + +🟨⏸ Phase 4.2: 新增 tests/integration/api/contract/llm-observe-routes.test.ts。 +覆盖范围: 22 个 maybeSubmitLLMTask 路由。 +共同断言: 未登录/越权/参数错误/成功返回 taskId + async。 + +🟨⏸ Phase 4.3: 新增 tests/integration/api/contract/direct-submit-routes.test.ts。 +覆盖范围: 16 个直接 submitTask 路由。 +共同断言: payload 入队契约、billing/locale/flow meta 关键字段存在。 + +🟨⏸ Phase 4.4: 新增 tests/integration/api/contract/crud-asset-hub-routes.test.ts。 +覆盖范围: asset-hub CRUD 路由。 +共同断言: DB 真值变化、字段映射不漂移、权限拦截。 + +🟨⏸ Phase 4.5: 新增 tests/integration/api/contract/crud-novel-promotion-routes.test.ts。 +覆盖范围: novel-promotion 非任务化 CRUD 路由。 +共同断言: project 权限、数据一致性、错误码一致性。 + +🟨⏸ Phase 4.6: 新增 tests/integration/api/contract/task-infra-routes.test.ts。 +覆盖范围: /api/tasks, /api/tasks/[taskId], /api/tasks/dismiss, /api/task-target-states, /api/sse。 +共同断言: 状态读取、取消、dismiss、target-state 结果结构。 + +🟥⚠️ Phase 4.7: 117 route 全覆盖耗时高,若单进程过慢需按组拆分命令并并行 CI job(不降覆盖)。 + +--- + +### 阶段5: L3 Worker 决策单元测试(全系统主防线之二) + +🟨⏸ Phase 5.1: 新增 tests/unit/worker/reference-to-character.test.ts。 +覆盖: extractOnly / customDescription / useReferenceImages / backgroundJob 分支。 + +🟨⏸ Phase 5.2: 新增 tests/unit/worker/image-task-handlers-core.test.ts。 +覆盖: referenceImages 注入、resolution/aspectRatio 选择、目标实体分支(character/location/storyboard)。 + +🟨⏸ Phase 5.3: 新增 tests/unit/worker/script-to-storyboard.test.ts。 +覆盖: step orchestration、JSON parse 失败路径、voice line 匹配合法性校验。 + +🟨⏸ Phase 5.4: 新增 tests/unit/worker/episode-split.test.ts。 +覆盖: 分集数量边界、错误输入显式失败、输出结构一致性。 + +🟨⏸ Phase 5.5: 新增 tests/unit/worker/video-worker.test.ts 与 voice-worker.test.ts。 +覆盖: 必填 payload 校验、外部轮询超时、持久化字段更新。 + +🟥⚠️ Phase 5.6: 若 mock 粒度过粗会掩盖问题,必须在断言中校验“被调用参数内容”而非仅校验调用次数。 + +--- + +### 阶段6: L4 全链路契约测试(route -> queue -> worker) + +🟨⏸ Phase 6.1: 新增 tests/integration/chain/text.chain.test.ts。 +场景: ai-create-character、reference-to-character 全链路。 + +🟨⏸ Phase 6.2: 新增 tests/integration/chain/image.chain.test.ts。 +场景: generate-image、modify-image 全链路。 + +🟨⏸ Phase 6.3: 新增 tests/integration/chain/video.chain.test.ts。 +场景: generate-video、lip-sync 全链路。 + +🟨⏸ Phase 6.4: 新增 tests/integration/chain/voice.chain.test.ts。 +场景: voice-design、voice-generate 全链路。 + +🟨⏸ Phase 6.5: 新增 tests/helpers/fakes/llm.ts、tests/helpers/fakes/media.ts、tests/helpers/fakes/providers.ts。 +要求: 外部调用全部 fake,禁止真实外网消耗。 + +🟥⚠️ Phase 6.6: 需要在测试环境加“网络闸门”,防止误打真实外部 API。 + +--- + +### 阶段7: 前端状态回归测试(轻量,不做重 E2E) + +🟨⏸ Phase 7.1: 扩展 tests/unit/optimistic/asset-hub-mutations.test.ts。 +覆盖: 并发操作回滚冲突、缓存一致性。 + +🟨⏸ Phase 7.2: 新增 tests/unit/optimistic/task-target-state-map.test.ts。 +覆盖: queued/processing/completed/failed 对 UI 状态映射。 + +🟨⏸ Phase 7.3: 新增 tests/unit/optimistic/sse-invalidation.test.ts。 +覆盖: 仅终态触发 target-state 刷新,不允许轮询回退。 + +🟥⚠️ Phase 7.4: 不引入高成本浏览器 E2E,避免与“全 fake、低成本”策略冲突。 + +--- + +### 阶段8: CI 门禁与回归收口 + +🟨⏸ Phase 8.1: 更新 package.json,新增命令。 +建议命令: +- test:guards +- test:unit:all +- test:integration:api +- test:integration:chain +- test:pr +- test:regression + +🟨⏸ Phase 8.2: 调整 CI,每次 PR 执行 test:pr(全量门禁)。 +🟨⏸ Phase 8.3: 回归失败输出标准化(失败文件、失败断言、首次引入 commit)。 +🟨⏸ Phase 8.4: 设置完成判定条件,满足后冻结基线。 + +🟥⚠️ Phase 8.5: 全量 PR 门禁会拉长反馈时间,需要预留 CI 资源并做缓存优化。 + +--- + +### 执行日志(每次执行后必须追加) +格式: +- [YYYY-MM-DD HH:mm] 状态变更: <任务ID> <旧状态> -> <新状态> +- [YYYY-MM-DD HH:mm] 修改文件: <绝对路径列表> +- [YYYY-MM-DD HH:mm] 运行命令: <命令> +- [YYYY-MM-DD HH:mm] 结果: <通过/失败 + 摘要> +- [YYYY-MM-DD HH:mm] 问题: <若有> + +--- + +## 4: 验证策略 + +### 4.1 可量化验收指标(必须全部达成) +1. route 契约覆盖率 = 117/117(100%)。 +2. TASK_TYPE 覆盖率 = 49/49(100%)。 +3. 4 类队列链路测试均存在且通过(text/image/video/voice)。 +4. 每个 PR 全量门禁执行并通过。 +5. 无真实外网调用(测试日志与网络闸门双重确认)。 +6. 关键高频回归场景(编辑类、参考图类、任务状态类)均有自动化用例。 +7. 新增/修改 route 或 TASK_TYPE 时,若未补测试,guard 必须失败。 + +### 4.2 命令级验证 +- `npm run test:guards` +- `cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/unit` +- `cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/api` +- `cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/chain` +- `npm run test:pr` + +### 4.3 结果记录 +每轮执行后在执行日志追加: +- 总用例数 +- 失败数 +- 失败模块 +- 修复后重跑结果 + +--- + +## 5: 备注(可选但建议) + +1. 本计划不依赖历史对话,可由任意新模型直接接手。 +2. 若用户策略改变(例如允许少量 live canary),必须先更新本文件“策略锁定项”,再动代码。 +3. 若发现范围蔓延,优先维护“矩阵完整性”而不是临时加测。 +4. 任何“为了通过测试而加回退”的方案一律禁止。 +5. 所有测试代码保持强类型,不允许 any。 diff --git a/agent/testing.md b/agent/testing.md new file mode 100644 index 0000000..6763fc7 --- /dev/null +++ b/agent/testing.md @@ -0,0 +1,147 @@ +# 测试编写详细规范 + +## 1. 何时必须写或更新测试 + +| 触发场景 | 要求 | +|---|---| +| 修改 worker handler 逻辑 | 必须有对应行为测试 | +| 修复 bug | 必须新增回归测试,`it()` 名称体现该 bug 场景 | +| 新增 API route 或 task type | 必须更新 `tests/contracts/` 矩阵 | +| 修改 prompt 后缀、referenceImages 注入、DB 写回字段 | 必须有行为断言覆盖 | + +未通过 `npm run test:regression` 不得宣称功能完成。 + +--- + +## 2. 断言必须是行为级(检查具体值) + +**正确写法**: +```ts +// 断言 DB 写入了具体字段值 +const updateData = prismaMock.globalCharacterAppearance.update.mock.calls.at(-1)?.[0].data +expect(updateData.description).toBe('AI_EXTRACTED_DESCRIPTION') + +// 断言生图函数收到了正确参数 +const { prompt, options } = readGenerateCall(0) +expect(prompt).toContain(CHARACTER_PROMPT_SUFFIX) +expect(options.referenceImages).toEqual(['https://ref.example/a.png']) + +// 断言返回值 +expect(result).toEqual({ success: true, count: 2 }) +``` + +**禁止写法**(不能作为唯一主断言): +```ts +expect(fn).toHaveBeenCalled() // 只知道"调用了",不知道"传了什么" +expect(fn).toHaveBeenCalledTimes(1) // 次数本身无业务意义时无效 +``` + +--- + +## 3. Mock 规范 + +**必须 Mock**: +- `prisma`(所有数据库操作) +- LLM / chatCompletionWithVision / generateImage +- COS / uploadToCOS / getSignedUrl +- 外部 HTTP(fetchWithTimeoutAndRetry 等) + +**禁止 Mock**: +- 你要测试的业务逻辑函数本身 +- 项目内部常量(如 `CHARACTER_PROMPT_SUFFIX`),直接 import 使用 + +**禁止"自给自答"**: +```ts +// 错误:mock 返回 X,马上断言 X,没有经过任何业务逻辑 +mockLLM.mockReturnValue('result') +expect(await mockLLM()).toBe('result') // 废测试 + +// 正确:mock AI 返回 X,断言业务代码把 X 写进了数据库 +llmMock.getCompletionContent.mockReturnValue('高挑女性') +await handleTask(job) +expect(prismaMock.update.mock.calls.at(-1)[0].data.description).toBe('高挑女性') +``` + +--- + +## 4. 测试数据规范 + +- **影响分支的字段**须分开写 `it()`,例如: + - `有 extraImageUrls` 和 `无 extraImageUrls` 分别写一个用例 + - `isBackgroundJob: true` 和 `false` 分别写 +- **纯透传字段**(`taskId`、`userId` 等代码不处理)可用占位值 `'task-1'` +- **每个 `it()` 命名格式**:`[条件] -> [预期结果]` + +**命名示例**: +``` +有参考图 -> AI 分析结果写入 description +无参考图 -> 不触发 AI,description 不变 +AI 调用失败 -> 主流程成功,description 不被污染 +缺少必填参数 -> 抛出包含字段名的错误 +批量确认 2 个角色 -> 逐个处理,count 返回 2 +``` + +--- + +## 5. 每个测试文件的结构 + +```ts +// 1. vi.hoisted 定义所有 mock(必须在 import 之前) +const prismaMock = vi.hoisted(() => ({ ... })) +const llmMock = vi.hoisted(() => ({ ... })) + +// 2. vi.mock 注册(在 import 之前) +vi.mock('@/lib/prisma', () => ({ prisma: prismaMock })) +vi.mock('@/lib/llm-client', () => llmMock) + +// 3. import 真实业务代码(在 mock 注册之后) +import { handleXxxTask } from '@/lib/workers/handlers/xxx' + +// 4. describe + beforeEach 重置 mock +describe('worker xxx behavior', () => { + beforeEach(() => { vi.clearAllMocks() }) + + it('[条件] -> [结果]', async () => { + // 准备:覆盖这次场景需要的特殊 mock 返回值 + // 构造:buildJob(payload, taskType) + // 执行:await handleXxxTask(job) + // 断言:检查具体值 + }) +}) +``` + +--- + +## 6. 运行命令 + +| 场景 | 命令 | +|---|---| +| 改了 worker 逻辑 | `BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker` | +| 改了某个具体文件 | `BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker/xxx.test.ts` | +| 改了 API 路由 | `BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api` | +| 改了 helpers / 常量 | `BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/helpers` | +| 提交前完整验证 | `npm run test:regression` | + +--- + +## 7. 目录说明 + +| 目录 | 用途 | +|---|---| +| `tests/unit/worker/` | worker handler 行为测试(主要回归防线) | +| `tests/unit/helpers/` | 纯函数 / 工具函数测试 | +| `tests/unit/optimistic/` | 前端状态 hook 行为测试 | +| `tests/integration/api/contract/` | API 路由契约(401/400/200 + payload 断言) | +| `tests/integration/chain/` | queue → worker → 结果完整链路 | +| `tests/contracts/` | 矩阵与守卫(route/tasktype/requirements) | +| `tests/helpers/fakes/` | 通用 mock 工具(llm、media、providers) | + +--- + +## 8. 验证测试有效性(防假绿灯) + +写完测试后,用以下方式确认测试没有虚假通过: + +1. 临时注释掉你刚写的业务逻辑,测试应该变红 +2. 还原业务逻辑,测试应该变绿 +3. 如果注释后测试还是绿,说明断言没有覆盖到真实业务路径 diff --git a/caddyfile b/caddyfile new file mode 100644 index 0000000..641bfae --- /dev/null +++ b/caddyfile @@ -0,0 +1,19 @@ +# HTTPS 反向代理(在主机上运行,非 Docker 内) +# 启动方式: caddy run --config Caddyfile +# +# 用法: +# 1. docker compose up -d (启动 App + MySQL + Redis) +# 2. caddy run --config Caddyfile (启动 HTTPS 代理) +# 3. 打开 https://localhost:4443 或 https://your-ip:4443 +# +# 修改下方 IP 为你的局域网 IP(ifconfig en0 查看) + +localhost:1443, https://192.168.31.218:1443 { + handle /admin/queues* { + reverse_proxy localhost:13010 + } + handle { + reverse_proxy localhost:13000 + } + tls internal +} \ No newline at end of file diff --git a/debug-request.json b/debug-request.json new file mode 100644 index 0000000..9dff864 --- /dev/null +++ b/debug-request.json @@ -0,0 +1,9 @@ +{ + "model": "doubao-seedream-4-0-250828", + "prompt": "Lily and Olivia in 医院病房_日夜, medium shot, dramatic lighting, American comic style", + "sequential_image_generation": "disabled", + "response_format": "url", + "size": "1080x1920", + "stream": false, + "watermark": false +} \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..debe046 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,32 @@ +services: + mysql: + image: mysql:8.0 + container_name: waoowaoo-test-mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: waoowaoo_test + MYSQL_ROOT_HOST: "%" + ports: + - "3307:3306" + command: + - "--default-authentication-plugin=mysql_native_password" + - "--sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION" + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -proot"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 15s + + redis: + image: redis:7-alpine + container_name: waoowaoo-test-redis + ports: + - "6380:6379" + command: ["redis-server", "--appendonly", "no"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 5s diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..311b2e3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,121 @@ +services: + # ==================== MySQL ==================== + mysql: + image: mysql:8.0 + container_name: waoowaoo-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: waoowaoo123 + MYSQL_DATABASE: waoowaoo + MYSQL_ROOT_HOST: "%" + ports: + - "13306:3306" + volumes: + - mysql_data:/var/lib/mysql + command: + - "--default-authentication-plugin=mysql_native_password" + - "--sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION" + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -pwaoowaoo123"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 15s + + # ==================== Redis ==================== + redis: + image: redis:7-alpine + container_name: waoowaoo-redis + restart: unless-stopped + ports: + - "16379:6379" + volumes: + - redis_data:/data + command: ["redis-server", "--appendonly", "yes"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 5s + + # ==================== App (Next.js + Workers) ==================== + app: + build: . + container_name: waoowaoo-app + restart: unless-stopped + environment: + # 数据库(指向容器内部 MySQL,用服务名 mysql 而非 localhost) + DATABASE_URL: "mysql://root:waoowaoo123@mysql:3306/waoowaoo" + # Redis(指向容器内部 Redis,用服务名 redis) + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_USERNAME: "" + REDIS_PASSWORD: "" + REDIS_TLS: "" + # 存储:默认本地存储 + STORAGE_TYPE: local + # 认证 + NEXTAUTH_URL: "http://localhost:3000" + NEXTAUTH_SECRET: "waoowaoo-default-secret-2026" + # 内部密钥 + CRON_SECRET: "waoowaoo-docker-cron-secret" + INTERNAL_TASK_TOKEN: "waoowaoo-docker-task-token" + API_ENCRYPTION_KEY: "waoowaoo-opensource-fixed-key-2026" + # Worker 配置 + WATCHDOG_INTERVAL_MS: "30000" + TASK_HEARTBEAT_TIMEOUT_MS: "90000" + QUEUE_CONCURRENCY_IMAGE: "50" + QUEUE_CONCURRENCY_VIDEO: "50" + QUEUE_CONCURRENCY_VOICE: "20" + QUEUE_CONCURRENCY_TEXT: "50" + # Bull Board + BULL_BOARD_HOST: "0.0.0.0" + BULL_BOARD_PORT: "3010" + BULL_BOARD_BASE_PATH: "/admin/queues" + BULL_BOARD_USER: "" + BULL_BOARD_PASSWORD: "" + # 日志 + LOG_UNIFIED_ENABLED: "true" + LOG_LEVEL: "INFO" + LOG_FORMAT: "json" + LOG_DEBUG_ENABLED: "false" + LOG_AUDIT_ENABLED: "true" + LOG_SERVICE: "waoowaoo" + LOG_REDACT_KEYS: "password,token,apiKey,apikey,authorization,cookie,secret,access_token,refresh_token" + # 计费 + BILLING_MODE: "SHADOW" + # 流式 + LLM_STREAM_EPHEMERAL_ENABLED: "true" + ports: + - "13000:3000" + - "13010:3010" + volumes: + - ./data:/app/data + - ./docker-logs:/app/logs + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + command: > + sh -c " + npx prisma db push --skip-generate && + (sleep 5 && echo '' && + echo '╔══════════════════════════════════════════════════╗' && + echo '║ waoowaoo is ready! ║' && + echo '║ ║' && + echo '║ HTTP: http://localhost:13000 ║' && + echo '║ ║' && + echo '║ For HTTPS, run Caddy on host: ║' && + echo '║ caddy run --config Caddyfile ║' && + echo '║ Then open: https://localhost:1443 ║' && + echo '╚══════════════════════════════════════════════════╝' && + echo '') & + npm run start + " + +volumes: + mysql_data: + redis_data: + diff --git a/docs/testing/behavior-test-guideline.md b/docs/testing/behavior-test-guideline.md new file mode 100644 index 0000000..61a99fa --- /dev/null +++ b/docs/testing/behavior-test-guideline.md @@ -0,0 +1,38 @@ +# Behavior Test Guideline + +## Goal +- Treat tests as product contracts: input -> execution -> result. +- Block regressions on real outcomes (payload, DB write fields, returned contract, lifecycle events). + +## Mandatory Rules +- Do not use `toHaveBeenCalled()` as the only assertion. +- Every test must assert at least one concrete business value. +- Every worker handler must include: + - failure path + - success path + - key branch path +- One bug fix must add at least one regression test. + +## Mock Rules +- Must mock: database, AI providers, storage, external HTTP. +- Must not mock: the function under test, business constants. +- Avoid self-fulfilling mocks (`mock return X` then only assert returned X). + +## Required Layers +- `tests/unit/helpers`: payload parsing and helper decisions. +- `tests/integration/api/contract`: route-level contracts and auth/validation. +- `tests/unit/worker`: worker branch + persistence assertions. +- `tests/integration/chain`: queue payload handoff and worker consumption behavior. +- `tests/unit/optimistic`: SSE and target-state UI consistency. + +## Execution Commands +- `npm run test:behavior:unit` +- `npm run test:behavior:api` +- `npm run test:behavior:chain` +- `npm run test:behavior:guards` +- `npm run test:behavior:full` + +## Merge Gate +- Behavior guards must pass. +- New route/task type must be reflected in catalogs/matrices. +- Regression changes without behavior tests are not complete. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..f4d2259 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,52 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + ignores: [ + "node_modules/**", + ".agent/**", + ".next/**", + "out/**", + "build/**", + "coverage/**", + "next-env.d.ts", + ], + }, + { + files: ["src/**/*.{ts,tsx}"], + ignores: ["src/components/ui/icons/**"], + rules: { + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "lucide-react", + message: "Import icons through '@/components/ui/icons' only.", + }, + ], + }, + ], + "no-restricted-syntax": [ + "error", + { + selector: "JSXOpeningElement[name.name='svg']", + message: + "Use AppIcon or icons module components instead of inline .", + }, + ], + }, + }, +]; + +export default eslintConfig; diff --git a/extract_chinese.py b/extract_chinese.py new file mode 100644 index 0000000..27fda37 --- /dev/null +++ b/extract_chinese.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +提取React/TypeScript代码中的硬编码中文字符串 +""" +import re +import os +from pathlib import Path +import json + +def extract_chinese_strings(file_path): + """提取文件中的中文字符串""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + except: + return [] + + results = [] + + # 匹配JSX/TSX中的中文字符串 + # 1. {' 中文 '} 或 {"中文"} + pattern1 = r'\{\s*[\'"]([^\'"\{\}]*[\u4e00-\u9fff]+[^\'"\{\}]*)[\'\"]\s*\}' + # 2. >中文< + pattern2 = r'\>([^<\>]*[\u4e00-\u9fff]+[^<\>]*)\<' + # 3. placeholder="中文" 等属性 + pattern3 = r'(?:placeholder|title|alt|value|defaultValue|confirmText|cancelText|message)\s*=\s*[\'"]([^\'\"]*[\u4e00-\u9fff]+[^\'\"]*)[\'"]' + # 4. 字符串默认值 = '中文' + pattern4 = r'=\s*[\'"]([^\'\"]*[\u4e00-\u9fff]+[^\'\"]*)[\'"]' + + for pattern in [pattern1, pattern2, pattern3, pattern4]: + matches = re.finditer(pattern, content) + for match in matches: + chinese_text = match.group(1).strip() + if chinese_text and len(chinese_text) > 0: + # 跳过注释 + line_num = content[:match.start()].count('\n') + 1 + line = content.split('\n')[line_num - 1] + if '//' in line and line.index('//') < line.find(chinese_text): + continue + results.append({ + 'text': chinese_text, + 'line': line_num, + 'category': 'unknown' + }) + + # 去重 + seen = set() + unique_results = [] + for r in results: + key = f"{r['text']}_{r['line']}" + if key not in seen: + seen.add(key) + unique_results.append(r) + + return unique_results + +def scan_directory(base_path,exclude_patterns=['test-ui']): + """扫描目录中的所有TSX/TS文件""" + all_findings = {} + + for root, dirs, files in os.walk(base_path): + # 排除特定目录 + dirs[:] = [d for d in dirs if d not in exclude_patterns and not d.startswith('.')] + + for file in files: + if file.endswith(('.tsx', '.ts')): + file_path = os.path.join(root, file) + relative_path = os.path.relpath(file_path, base_path) + + findings = extract_chinese_strings(file_path) + if findings: + all_findings[relative_path] = findings + + return all_findings + +if __name__ == '__main__': + base_dir = 'src' + results = scan_directory(base_dir) + + # 输出结果 + total = 0 + for file_path, findings in sorted(results.items()): + if findings: + print(f"\n## {file_path} ({len(findings)} strings)") + for finding in findings[:10]: # 只显示前10个 + print(f" Line {finding['line']}: {finding['text'][:60]}") + total += len(findings) + if len(findings) > 10: + print(f" ... and {len(findings) - 10} more") + + print(f"\n\n总计: {len(results)} 个文件, {total} 处硬编码中文") diff --git a/lib/prompts/character-reference/character_image_to_description.en.txt b/lib/prompts/character-reference/character_image_to_description.en.txt new file mode 100644 index 0000000..3f74f24 --- /dev/null +++ b/lib/prompts/character-reference/character_image_to_description.en.txt @@ -0,0 +1,31 @@ +# Character Image To Description Prompt + +Analyze the provided character image and write one detailed English visual description for image generation. + +## Required content +Include all of the following: +1. Gender and approximate age range +2. Hair style and hair color +3. Face shape and facial features +4. Body build and silhouette +5. Clothing style and clothing details +6. Accessories and signature details +7. Overall style keywords + +## Missing-part completion +If the image only shows part of the body, infer the missing parts consistently. +- Missing lower body: infer matching pants/skirt style +- Missing shoes: infer shoes that fit the outfit +- Missing accessory details: infer a few reasonable accessories + +## Forbidden content +Do not mention: +- Skin color +- Eye color +- Facial expression +- Pose or action +- Background + +## Output format +Return one plain English paragraph only (about 120-220 words). +Do not return markdown, bullets, titles, or JSON. diff --git a/lib/prompts/character-reference/character_image_to_description.zh.txt b/lib/prompts/character-reference/character_image_to_description.zh.txt new file mode 100644 index 0000000..b2da8d7 --- /dev/null +++ b/lib/prompts/character-reference/character_image_to_description.zh.txt @@ -0,0 +1,36 @@ +# 图片反推角色描述提示词 + +请分析这张角色图片,生成一段详细的角色外貌描述(用于 AI 图片生成)。 + +## 输出要求 + +生成一段完整的角色视觉描述,包含以下要素: + +1. 性别和年龄段(如:约二十五岁的男性) +2. 发型发色(如:黑色短发、微卷的棕色长发) +3. 脸型五官特征(如:剑眉星目、高鼻梁、薄唇) +4. 体态身材(如:身形修长、体格健壮) +5. 服装风格(如:深蓝色西装、白色衬衫、皮质腰带) +6. 配饰特征(如:左手戴银色手表、胸前别金色胸针) +7. 整体气质关键词(如:精英气质、禁欲系、高冷、温柔暖男) + +## 缺失内容补齐规则 + +如果参考图只展示了部分身体(如上半身、头像),请根据已有信息合理推断并补全: +- **缺少下半身**:根据上衣风格推断裤装/裙装类型(如西装上衣配深蓝色西裤、休闲上衣配牛仔裤) +- **缺少鞋子**:根据整体穿搭风格推断鞋款(如正装配皮鞋、休闲装配运动鞋或帆布鞋) +- **缺少配饰细节**:根据角色气质合理添加配饰(如商务风配手表、休闲风配手环) + +## 禁止描写 + +- 皮肤颜色 +- 眼睛颜色 +- 表情 +- 动作 +- 背景 +- 姿势 + +## 输出格式 + +一段连贯的描述文字,约200-300字,直接可用于图片生成提示词。 +只返回描述文字,不要有任何标题、序号或其他格式。 diff --git a/lib/prompts/character-reference/character_reference_to_sheet.en.txt b/lib/prompts/character-reference/character_reference_to_sheet.en.txt new file mode 100644 index 0000000..34d50ed --- /dev/null +++ b/lib/prompts/character-reference/character_reference_to_sheet.en.txt @@ -0,0 +1,24 @@ +# Reference Image To Character Sheet Prompt (img2img) + +Use the provided reference image to extract face traits, hairstyle, body shape, and outfit structure. + +## Style consistency +Keep the original visual style: +- Real photo -> realistic style +- Anime -> anime style +- Realistic illustration -> realistic illustration style +- Any other style -> preserve that style + +## Generation rules +1. Ignore the original image color cast and lighting defects +2. Use clean, soft studio lighting +3. Keep natural, aesthetically correct body proportions +4. Do not copy blur, noise, compression artifacts, or defects +5. Output must be clear, sharp, and production-quality +6. Character expression should be neutral and calm, looking at camera + +## Missing-part completion +If only half body or partial body is visible, infer and complete hidden parts consistently: +- Infer matching lower-body clothing +- Infer suitable footwear +- Infer missing hands/arms in a natural way diff --git a/lib/prompts/character-reference/character_reference_to_sheet.zh.txt b/lib/prompts/character-reference/character_reference_to_sheet.zh.txt new file mode 100644 index 0000000..9537366 --- /dev/null +++ b/lib/prompts/character-reference/character_reference_to_sheet.zh.txt @@ -0,0 +1,28 @@ +# 参考图转角色设定图提示词(图生图模式) + +基于提供的参考图片,提取角色的面部五官特征、发型、体型和服装款式作为参考。 + +## 画风保持规则 + +保持原图的画风和艺术风格: +- 真人照片风格 → 生成真人风格 +- 动漫风格 → 生成动漫风格 +- 写实插画 → 生成写实插画风格 +- 其他风格 → 保持一致 + +## 生成规则 + +1. 忽略原图的具体色调和光线 +2. 使用自然柔和的摄影棚灯光 +3. 绘制正常美观的人体比例 +4. 不要复制原图的画质、模糊、噪点或瑕疵 +5. 生成的图像必须清晰锐利、细节丰富、专业品质 +6. 角色表情应为自然平静的中性表情,目光正视镜头 + +## 缺失部位自动补齐 + +如果参考图是半身或部分身体,请根据服装风格和人物特征合理补全未露出的部位: +- **缺少下半身**:根据上衣风格推断并绘制匹配的裤装/裙装 +- **缺少脚部**:根据整体穿搭风格添加合适的鞋款 +- **缺少手部/手臂**:根据姿态合理补全 +- 保持整体风格一致,确保补全的部分与可见部分协调统一 diff --git a/lib/prompts/novel-promotion/agent_acting_direction.en.txt b/lib/prompts/novel-promotion/agent_acting_direction.en.txt new file mode 100644 index 0000000..4d571b6 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_acting_direction.en.txt @@ -0,0 +1,38 @@ +You are an experienced Acting Director. +Your task is to generate acting notes for each character in each panel. + +Core input: +- Total panel count: {panel_count} +- Panels JSON: +{panels_json} +- Character info: +{characters_info} + +Requirements: +1. Treat each panel independently. The same character can have different emotional states across panels. +2. Adapt performance style to panel.scene_type (daily / emotion / action / epic / suspense). +3. For each character, write one concise visual acting instruction including: + - emotional state (visible, not abstract) + - facial expression details + - body language / posture + - micro action and gaze direction +4. Use only observable descriptions. Avoid abstract words like "sad" without visual evidence. + +Output format (JSON array only): +[ + { + "panel_number": 1, + "characters": [ + { + "name": "Character Name", + "acting": "One-sentence visual acting direction" + } + ] + } +] + +Strict constraints: +1. Return JSON only, no markdown. +2. Array length must equal {panel_count}. +3. Each character object must contain only "name" and "acting". +4. Keep character names exactly consistent with panel input. diff --git a/lib/prompts/novel-promotion/agent_acting_direction.zh.txt b/lib/prompts/novel-promotion/agent_acting_direction.zh.txt new file mode 100644 index 0000000..cfd9195 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_acting_direction.zh.txt @@ -0,0 +1,90 @@ +你是一位经验丰富的表演指导(Acting Director)。你的任务是为一组分镜中的**每个镜头**设计角色的表演细节。 + +【核心职责】 + +分析整组分镜后,为每个镜头中的角色用一句话描述完整的表演指令,包含: +- 情绪状态与强度 +- 面部表情细节 +- 肢体语言与姿态 +- 微动作与视线 + +【重要】每个镜头的表演必须是独立的! +- 同一角色在不同镜头可能有不同情绪变化 +- 表演风格匹配 scene_type(日常/情感/动作/史诗/悬疑) + +【表演风格匹配 scene_type】 + +**daily(日常)**:自然松弛,微表情为主,动作幅度小 +**emotion(情感)**:细腻层次,眼神戏份重,情绪渐进 +**action(动作)**:爆发力强,动作干脆,表情夸张 +**epic(史诗)**:庄重仪式感,姿态端正,动作缓慢有力 +**suspense(悬疑)**:紧绷警觉,肢体僵硬,眼神游移 + +【表演描述词库】 + +**表情**:眼眶泛红、眉头紧锁、嘴角上扬、目光闪躲、瞳孔收缩、嘴唇颤抖、咬紧牙关 +**肢体**:握紧拳头、身体前倾、双手交握、肩膀耸起、转身背对、后退一步 +**微动作**:轻轻眨眼、咽口水、深呼吸、手指轻颤、舔嘴唇、胸口起伏 + +【⚠️ 禁止规则】 + +1. 禁止抽象情绪词:悲伤、愤怒、紧张 → 改用可见表现 +2. 禁止身份称呼:母亲、父亲 → 改用角色名 + +【输出格式】 + +返回JSON数组,每个镜头一个对象,每个角色只有 name + acting 两个字段: + +[ + { + "panel_number": 1, + "characters": [ + { + "name": "李凤华", + "acting": "嘴角微扬眼神柔和地看向景笙,身体微微前倾,双手自然垂放,轻轻眨眼" + }, + { + "name": "景笙", + "acting": "面带微笑但眼神略显疏离,站姿笔直双手背后,轻轻点头" + } + ] + }, + { + "panel_number": 2, + "characters": [ + { + "name": "李凤华", + "acting": "眉头轻皱嘴唇紧抿,双手交握在身前肩膀微耸,目光低垂看向地面,手指轻微交缠" + } + ] + }, + { + "panel_number": 3, + "characters": [ + { + "name": "李凤华", + "acting": "眼眶泛红泪水打转嘴唇颤抖,身体微微发抖双手攥紧衣角,快速眨眼忍住泪水转头避开对方视线" + } + ] + } +] + +【输入数据】 + +分镜数据(共 {panel_count} 个镜头): +{panels_json} + +角色信息: +{characters_info} + +【严格要求】 + +1. 只返回JSON数组,不要有markdown代码块标记 +2. 数组长度必须等于输入的镜头数量({panel_count}个) +3. 每个角色只有 name 和 acting 两个字段 +4. acting 用一句话描述完整表演(表情+肢体+微动作+视线) +5. 角色名必须与输入分镜中的 characters 完全一致 +6. 所有描述必须是可视化的,禁止抽象情绪词 +7. 根据 scene_type 调整表演风格 +8. 情绪弧线连贯,前后镜头有合理递进 + diff --git a/lib/prompts/novel-promotion/agent_character_profile.en.txt b/lib/prompts/novel-promotion/agent_character_profile.en.txt new file mode 100644 index 0000000..06d104c --- /dev/null +++ b/lib/prompts/novel-promotion/agent_character_profile.en.txt @@ -0,0 +1,86 @@ +You are a casting and character-asset analyst. +Analyze the input text and produce structured character profiles for visual production. + +Input text: +{input} + +Existing character library info: +{characters_lib_info} + +Goals: +1. Identify characters that should appear visually. +2. Exclude pure background extras and abstract entities. +3. Build profile fields needed for downstream visual generation. +4. Capture naming/alias mapping, especially first-person references. + +Extraction rules: +1. Include characters that speak, act, or significantly drive plot. +2. Exclude one-off nameless background people unless visual identity is required. +3. Resolve aliases/titles (e.g., "my husband", "boss", "I") to canonical names when possible. +4. For first-person narrative, explicitly document who "I" maps to in introduction. + +Field rules: +- role_level: one of S/A/B/C/D based on narrative importance +- costume_tier: 1-5 based on social identity (not role_level) +- expected_appearances: always include at least one initial appearance +- introduction: include identity, relationship mapping, and common address mapping + +Output format (JSON only): +{ + "characters": [ + { + "name": "Canonical Name", + "aliases": ["alias 1", "alias 2"], + "introduction": "Role, perspective mapping, relationships, and naming aliases", + "gender": "male/female/other", + "age_range": "young adult", + "role_level": "S", + "archetype": "character archetype", + "personality_tags": ["tag1", "tag2"], + "era_period": "modern/fantasy/historical/sci-fi", + "social_class": "elite/middle/common", + "occupation": "occupation or none", + "costume_tier": 3, + "suggested_colors": ["color1", "color2"], + "primary_identifier": "signature visual marker", + "visual_keywords": ["keyword1", "keyword2"], + "expected_appearances": [ + { "id": 1, "change_reason": "initial appearance" } + ] + } + ], + "new_characters": [ + { + "name": "Canonical Name", + "aliases": ["alias 1", "alias 2"], + "introduction": "Role, perspective mapping, relationships, and naming aliases", + "gender": "male/female/other", + "age_range": "young adult", + "role_level": "S", + "archetype": "character archetype", + "personality_tags": ["tag1", "tag2"], + "era_period": "modern/fantasy/historical/sci-fi", + "social_class": "elite/middle/common", + "occupation": "occupation or none", + "costume_tier": 3, + "suggested_colors": ["color1", "color2"], + "primary_identifier": "signature visual marker", + "visual_keywords": ["keyword1", "keyword2"], + "expected_appearances": [ + { "id": 1, "change_reason": "initial appearance" } + ] + } + ], + "updated_characters": [ + { + "name": "Existing Canonical Name", + "updated_introduction": "Updated intro with newly discovered mapping", + "updated_aliases": ["new alias 1", "new alias 2"] + } + ] +} + +Strict constraints: +1. JSON only. +2. If no valid character is found, return empty arrays. +3. Keep all strings in English. diff --git a/lib/prompts/novel-promotion/agent_character_profile.zh.txt b/lib/prompts/novel-promotion/agent_character_profile.zh.txt new file mode 100644 index 0000000..fb312b0 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_character_profile.zh.txt @@ -0,0 +1,244 @@ +你是专业的"选角指导"。请基于提供的文本(小说、剧本或混合格式),分析并输出所有需要制作形象的角色档案信息。 + +【你的职责】 +- 识别需要在画面中出现的角色 +- 根据剧情发展和角色身份判断每个角色的重要性层级 +- 分析角色的性格和背景 +- 输出结构化的角色档案(供后续视觉生成使用) +- ⚠️ 分析角色之间的关系、称呼映射,生成角色介绍(introduction) + +【筛选规则 - 精准提取模式】 + +✅【必须提取的角色】: + - 剧本人物行中列出的角色 + - 有台词且参与剧情互动的角色 + - 贯穿故事主线的核心人物 + - 对剧情有实际推动作用的配角 + - 在画面中需要出镜的角色 + +❌【不提取的角色】: + - 无名无特征的纯路人(如"人群中的某人") + - 仅被提及但从未出场的角色 + - 没有台词也没有互动的背景人物 + - 意境描述中的虚构存在(如"命运"、"死神的化身") + +📋【判断标准】: + 问自己:这个角色是否需要制作形象图?是否在画面中有实际出镜? + 如果答案是否定的,则不提取。 + +【角色介绍 introduction 规则 ⭐重要】 + +每个角色必须有 introduction 字段,用于帮助后续 AI 正确识别角色。包含: + +1. **叙述视角映射**: + - 如果是第一人称叙述,明确说明"我"对应此角色 + - 示例:"本角色是故事主角,小说以第一人称'我'叙述" + +2. **角色身份定位**: + - 描述角色在故事中的身份(主角/配角/反派等) + - 示例:"女主角,公司秘书" + +3. **角色关系**: + - 与其他主要角色的关系 + - 示例:"林墨的妻子,张三的女儿" + +4. **称呼映射**: + - 其他角色对此角色的常用称呼 + - 示例:"被林墨称呼为'老婆'、'晴晴',被张三称呼为'闺女'" + +示例 introduction: +"故事主角,小说以第一人称'我'叙述,真名林墨。苏晴的丈夫,张三的女婿。被苏晴称呼为'老公'、'墨哥',被下属称呼为'林总'。" + +【角色重要性层级判断规则】 + +⚠️ 重要:根据角色在剧情中的戏份和身份地位来判断,不是根据外表华丽程度! + +S级(绝对主角): + - 故事的核心视角人物,剧情围绕其展开 + - 第一人称叙述中的"我"通常是S级 + - 判断依据:戏份最重、出场最多、剧情主线与其紧密相关 + +A级(核心配角): + - 与主角有大量互动的重要角色 + - 男二号、女二号、主要反派等 + - 判断依据:对主线剧情有重大影响、戏份仅次于主角 + +B级(重要配角): + - 多次出场、有名有姓、推动某条支线剧情 + - 判断依据:有一定戏份、对剧情有贡献 + +C级(次要角色): + - 偶尔出场、戏份较少但有具体形象 + - 判断依据:需要出镜但戏份不多 + +D级(群众演员): + - 有短暂出镜需求的小角色 + - 判断依据:仅在个别场景出现 + +【服装华丽度层级 costume_tier】 + +⚠️ 服装华丽度由角色的社会身份和剧情设定决定,不是由重要性决定! + - 主角可以是朴素穿着(如穷学生主角=tier 2) + - 配角可以是华丽服装(如富家公子配角=tier 5) + +5级(皇室/顶奢级):皇室成员、顶级富豪,服装极致华丽,有精美的刺绣、镶嵌或定制剪裁。 +4级(贵族/精英级):贵族、企业家,服装精致考究,使用高档面料和精致细节。 +3级(专业/品质级):中产阶级、专业人士,服装得体有品,剪裁讲究。 +2级(日常/普通级):普通人、学生,服装简洁日常,款式普通但整洁。 +1级(朴素/统一级):平民、底层劳动者,服装朴素统一,基础款式,功能性为主。 + +【角色原型 archetype 参考词库】 + +正派角色可以选择:霸道总裁、高冷学霸、温柔暖男、励志少年、贤惠女主、独立女强人、忠诚护卫等。 + +反派角色可以选择:心机婊、白莲花、阴险反派、疯批美人、复仇者等。 + +其他类型:傲娇公主、病娇、腹黑、毒舌、话痨、冷面热心、闷骚等。 + +【性格标签 personality_tags 参考词库】 + +气质类标签:高冷、温柔、阳光、忧郁、神秘、妩媚、清冷、热情 + +性格类标签:腹黑、傲娇、毒舌、话痨、闷骚、直爽、圆滑、固执 + +态度类标签:自信、自卑、孤僻、合群、叛逆、顺从 + +【视觉关键词 visual_keywords 参考词库】 + +风格类关键词:精英气质、街头潮流、学院风、复古优雅、运动活力、文艺气息、冷淡极简 + +特征类关键词:病弱感、禁欲系、狼狗系、奶狗系、御姐范、萝莉感、大叔味 + +【色彩建议规则】 + +根据角色类型选择合适的色彩: + +正派主角适合白色、蓝色、金色或浅色系,传达正义和光明感。 + +反派角色适合黑色、暗红、深紫或暗色系,营造神秘或压迫感。 + +温柔角色适合米白、淡粉、浅绿等柔和色,体现温暖亲和。 + +冷酷角色适合黑色、灰色、深蓝等冷色调,强调距离感。 + +活泼角色适合橙色、黄色等亮色系,展现活力和热情。 + +【辨识标志设计规则】 + +为S级和A级角色设计一眼就能认出的标志性特征: + +面部标志:眼角泪痣、剑眉、刀疤、胎记等独特面部特征。 + +发型标志:白发、挑染、独特发型、发带等醒目的头发特征。 + +服装标志:永远穿红色、标志性围巾、招牌外套等固定的服装元素。 + +配饰标志:家传戒指、从不摘下的项链、拐杖等标志性物品。 + +【子形象筛选规则 - 识别视觉外观变化 ⭐重要】 + +分析原文中角色是否有多个视觉形态,输出到 expected_appearances 字段。 + +✅ 需要记录的子形象(视觉上可见的变化): + - 衣着变化:换装、更换正装/休闲装、穿戴盔甲等 + - 年龄变化:穿越、回忆场景中的年轻/年老状态 + - 特殊装扮:出浴(围浴巾)、冒充他人的装扮 + - 发型改变:剪头、编发、盘发、披发等持续性外观变化 + +❌ 不需要记录的(非视觉或临时状态): + - 情绪/心理状态:生气、开心、难过、紧张 + - 健康状态:生病、发烧(除非有明显视觉特征如绷带) + - 临时动作:跑步、跳跃、战斗姿势 + - 模糊描述:"蒙上了一层阴影""眼神变了"等抽象描述 + - 临时特效/光影状态:散发光芒、身上发光、气场外放、浑身火焰、佛光环绕、金光闪闪等后期可添加的特效 + - 战斗技能释放:发功、运功、施法、放大招、释放法术等技能状态 + - 一次性瞬间状态:被打飞、摔倒、中招、受击等不持续的状态 + +⚠️ 判断标准: + - 如果一个状态无法通过换装来体现,就不需要记录 + - 如果一个状态是通过后期特效(如发光、粒子、光环、火焰等)来表现的,不需要记录 + - 如果一个状态只持续几秒而非整个场景,不需要记录 + - 只有持续性的、需要重新制作人物形象图的外观变化才需要记录 + +📋 expected_appearances 格式: + - 每个角色必须至少有一个 id=1 的"初始形象" + - 如有换装/年龄变化等,添加 id=2, 3... 的子形象 + - change_reason 简要说明变化原因(如"出浴状态"、"战斗装束"、"年老回忆") + +【已有资产库】 + +⚠️ 重要:请仔细阅读已有角色的介绍,判断新发现的角色名是否与已有角色是同一人! + +{characters_lib_info} + +【输出格式 - 支持新增和更新】 + +只返回JSON,禁止任何markdown标记或注释。 + +输出包含两个数组: +- new_characters: 新发现的角色 +- updated_characters: 需要更新介绍的已有角色(如发现了新的称呼、关系、或真名) + +{ + "new_characters": [ + { + "name": "角色名", + "aliases": ["别名1", "别名2"], + "introduction": "角色介绍:身份定位、叙述视角映射、与其他角色的关系、常用称呼", + "gender": "男/女", + "age_range": "约二十五岁", + "role_level": "S/A/B/C/D", + "archetype": "角色原型(如霸道总裁)", + "personality_tags": ["高冷", "腹黑"], + "era_period": "现代都市/古代唐朝/未来科幻", + "social_class": "上层精英/中产/平民", + "occupation": "企业家/学生/无", + "costume_tier": 5, + "suggested_colors": ["深蓝", "金色"], + "primary_identifier": "眼角泪痣(仅S/A级需要)", + "visual_keywords": ["精英气质", "禁欲系"], + "expected_appearances": [ + {"id": 1, "change_reason": "初始形象"}, + {"id": 2, "change_reason": "换装/特殊状态的原因(如有)"} + ] + } + ], + "updated_characters": [ + { + "name": "已有角色名(必须与资产库中的名字完全一致)", + "updated_introduction": "更新后的角色介绍(补充新发现的关系、称呼、真名等)", + "updated_aliases": ["新发现的别名1", "新发现的别名2"] + } + ] +} + +【更新规则】 + +⚠️ 什么情况下应该更新已有角色(放入 updated_characters): + +1. **发现真名**:之前只有"我",现在发现"我"的真名是"林墨" + → 更新 introduction 说明映射,添加 updated_aliases: ["林墨"] + +2. **发现新称呼**:之前不知道别人怎么称呼这个角色,现在发现有人叫他"林总" + → 更新 introduction 添加称呼信息,添加 updated_aliases: ["林总"] + +3. **发现新关系**:之前不知道角色间的关系,现在发现苏晴是林墨的妻子 + → 更新双方的 introduction 添加关系信息 + +4. **不要重复创建**:如果发现"林墨"其实就是已有的"我",不要创建新角色,而是更新"我"的介绍和别名 + +【严格要求】 +1. 只返回JSON,不得有其他文字 +2. role_level 必须是 S/A/B/C/D 之一 +3. costume_tier 必须是 1-5 的整数 +4. S/A 级角色必须有 primary_identifier +5. personality_tags 至少2个,最多5个 +6. suggested_colors 2-3个颜色 +7. introduction 必填,描述角色身份、关系、称呼映射 +8. 如果发现已有角色的新信息,放入 updated_characters 而不是创建新角色 +9. updated_characters 中的 name 必须与已有资产库中的名字完全一致 +10. expected_appearances 必填,至少包含 id=1 的初始形象 +11. 只有持续性视觉变化才添加子形象,临时特效/情绪/动作不添加 + +【原文内容】 +{input} diff --git a/lib/prompts/novel-promotion/agent_character_visual.en.txt b/lib/prompts/novel-promotion/agent_character_visual.en.txt new file mode 100644 index 0000000..49455c9 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_character_visual.en.txt @@ -0,0 +1,44 @@ +You are a character visual designer. +Generate image-ready appearance descriptions from character profiles. + +Character profiles JSON: +{character_profiles} + +Rules: +1. Keep identity consistency with profile fields. +2. Convert personality/social identity into visual details (face, hair, outfit, accessories). +3. Support both human and non-human characters. +4. Respect era_period and costume_tier. +5. If primary_identifier exists, include it clearly in each main description. +6. Do not include expression, action, background, or plot narration. +7. Do not include skin color, eye color, or lip color. + +Appearance strategy: +- Initial appearance should be complete and self-contained. +- Additional appearances should focus on visual changes indicated by change_reason. +- Provide 3 alternative description lines per appearance. + +Output format (JSON only): +{ + "characters": [ + { + "name": "Character Name", + "appearances": [ + { + "id": 0, + "change_reason": "initial appearance", + "descriptions": [ + "description variant 1", + "description variant 2", + "description variant 3" + ] + } + ] + } + ] +} + +Strict constraints: +1. JSON only. +2. Keep names exactly aligned with input profiles. +3. Each descriptions array must contain at least 3 valid English strings. diff --git a/lib/prompts/novel-promotion/agent_character_visual.zh.txt b/lib/prompts/novel-promotion/agent_character_visual.zh.txt new file mode 100644 index 0000000..c69bf46 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_character_visual.zh.txt @@ -0,0 +1,208 @@ +你是专业的"角色视觉设计师"。根据角色档案信息,生成详细的人物外貌描述(用于AI图片生成)。 + +【你的职责】 +- 根据角色档案生成对应的外貌描述 +- 确保核心角色有明显的视觉辨识度 +- 体现角色性格和身份的视觉特征 +- 服装华丽度由角色身份决定,与重要性无关 + +【角色类型灵活处理规则】 + +⚠️ 角色不一定是人类!请根据原文判断角色的实际形态: + +**人类角色**:按照下方的面部、发型、体态、服装规范描述 + +**非人类角色**(动物、神话生物、知名形象等): +- 描述开头必须以角色名/物种名开始 +- 根据角色实际形态自由描述外观特征,不受人类模板限制 +- 保持角色的核心辨识特征 + +示例: +- 孙悟空 → "孙悟空,身穿虎皮裙,头戴紧箍咒金环,手持如意金箍棒,毛发金黄蓬松,尖耳竖立,眼神机灵狡黠..." +- 蜗牛 → "蜗牛,背负螺旋形褐色硬壳,壳面有细密纹路,两只细长触角顶端有圆形眼点,身体柔软半透明..." +- 龙 → "东方神龙,鳞片金红交错闪烁,龙须飘逸,鹿角威严分叉,蛇身盘旋腾空,四爪锋利如钩..." +- 拟人化动物 → "狐狸精,保留尖耳毛尾的狐狸特征,身着红色丝绸长裙,九条白色蓬松尾巴在身后舒展..." + +【视觉层级规范】 + +⚠️ 核心原则:服装华丽度由角色的社会身份和剧情设定决定,不是由重要性等级决定! + +S级角色: + - 描述长度180-220字 + - 必须有极高的视觉辨识度和"主角气质" + - 服装风格由角色身份决定(穷学生可以穿简单校服,但五官气质必须出众) + +A级角色: + - 描述长度150-180字 + - 有明显的个人特色和记忆点 + - 服装风格由角色身份决定 + +B级角色: + - 描述长度120-150字 + - 有基本的辨识特征 + - 服装风格符合其社会身份 + +C级角色: + - 描述长度80-120字 + - 简洁但完整的形象描述 + +D级角色: + - 描述长度50-80字 + - 基础形象即可 + +【服装华丽度 costume_tier 对照】 + +⚠️ 由角色的社会阶层和剧情身份决定,与role_level无关! + +5级(皇室/顶奢级):皇室成员、顶级富豪等,服装有刺绣、镶嵌、定制剪裁、稀有面料。 +4级(贵族/精英级):贵族、企业家等,高档面料、精致细节、品质配饰。 +3级(专业/品质级):中产阶级、专业人士,得体剪裁、有设计感。 +2级(日常/普通级):普通人,简洁日常的款式。 +1级(朴素/统一级):平民、学生等,基础款式、功能性为主。 + +【辨识标志应用规则】 + +如果角色档案中有 primary_identifier,必须在描述中明确体现: + +示例: +- primary_identifier: "眼角泪痣" → 描述中必须出现 "眼角一颗小巧泪痣" +- primary_identifier: "左耳银色耳钉" → 描述中必须出现 "左耳佩戴一枚银色耳钉" + +【色彩应用规则】 + +根据 suggested_colors 选择服装和配饰的主色调: +- 第一个颜色:主色调(外套/主要服装) +- 第二个颜色:辅色调(内搭/配饰) +- 第三个颜色(如有):点缀色(小配饰/图案) + +【性格到视觉的转化规则】 + +高冷性格的角色应该用利落剪裁、深色调、极简配饰来体现。 + +温柔性格的角色应该用柔和色调、流畅线条、圆润配饰来体现。 + +活泼性格的角色应该用亮色系、轻快材质、趣味配饰来体现。 + +腹黑性格的角色应该用深色内搭、精致细节、不经意的奢华来体现。 + +傲娇性格的角色应该用华丽但有距离感、高档但不张扬的设计来体现。 + +叛逆性格的角色应该用皮革金属元素、不对称设计、街头风来体现。 + +【描述规范】 + +1. 必须包含(按优先级顺序): + + 🎭 **面部特征(最重要!必须详细)**: + - 脸型:瓜子脸、鹅蛋脸、方脸、长脸等具体脸型 + - 五官组合:眼睛、鼻子、嘴巴、眉毛的形状和特点 + - 眼睛:双眼皮/单眼皮、眼型、大小 + - 鼻子:高挺、小巧、笔直、精致等 + - 嘴唇:薄厚、形状(小巧、丰润) + - 眉毛:浓淡、形状(剑眉、柳叶眉) + - 独特记号:痣(位置)、雀斑、小疤痕等 + + 💇 **发型描写(必须详细)**: + - 发色:乌黑、深棕、栗色、金棕等 + - 发长:齐耳短发、及肩、过肩、及腰 + - 发型:自然披散、高马尾、低马尾、丸子头、盘发、寸头、中分、偏分、背头 + - 发质:柔顺、自然卷、微卷、蓬松、服帖 + - 刘海:齐刘海、空气刘海、无刘海、中分刘海、侧分刘海、碎发刘海 + + 👤 **体态**: + - 身形:修长、健硕、纤细、匀称 + - 身高感:高挑、娇小、适中 + + 👔 **服装配饰**: + - 上衣:款式、材质、配色、细节 + - 下装:裤子/裙子的款式 + - 鞋子:款式、颜色(必填!) + - 配饰:根据层级添加 + +⚠️ **主角吸引力要求(关键!)**: +- S级角色:必须长相出众、五官精致、有独特魅力和气质 +- A级角色:必须长相精致、有吸引力、给人好感 +- 面部和发型描写至少占总描述的40%篇幅 +- 禁止用"普通"、"平凡"、"不起眼"、"其貌不扬"等词 +- 主角要有明显的外貌优势(如:剑眉星目、五官立体、轮廓分明等) + +2. 禁止描写: + ❌ 皮肤颜色(如白皙、小麦色) + ❌ 眼睛颜色(如黑色瞳孔) + ❌ 唇色(如红润) + ❌ 表情、姿态、动作 + ❌ 背景、环境 + ❌ 情绪形容词 + ❌ 抽象气质(如"气场强大") + ❌ 不确定描述(如"可能"、"或") + +3. 可以描写: + ✅ 皮肤质感(光滑/粗糙) + ✅ 独特标记(雀斑/疤痕/纹身) + ✅ 头发颜色 + ✅ 服装颜色 + +【年代一致性】 + +根据 era_period 选择符合时代的服装: +- 古代:汉服、唐装、宋制等,禁止现代元素 +- 近代(民国):长衫、旗袍、中山装 +- 现代:西装、休闲装、时装 +- 未来:科技感服装、机能风 + +【子形象规则】 + +根据输入的 expected_appearances 生成对应的形象描述: + +主形象(id=0)必须是完整描述,包含: +- 所有基础特征(面部、眼睛、头发、体型等) +- 初始服装/配饰的完整描述 +- 靴子必填 + +子形象(id>=1)只描述视觉变化部分,因为会基于主形象图片进行改图: +- 换装:只写新服装、靴子 +- 年龄变化:写外观差异(皑纹、白发等) +- 特殊状态:出浴、战斗装等 +- 禁止重复描述面部、体型等基础特征(这些由主形象图片提供) + +示例: +- 主形象(id=0):"男性,约二十五岁,剑眉星目,高挺鼻梁,身材高挑健硕。黑色短发利落后梳。身穿深蓝色锦缎长袍,腰系玉带,脚踏黑色皮质长靴。" +- 出浴状态(id=1):"湿漉漉的头发向后拢去,上半身赤裸,下半身围着白色浴巾,赤脚。" +- 战斗装束(id=2):"换上黑色劲装,脚蹬厚底战靴。" + + +【输出格式】 + +只返回JSON,禁止任何markdown标记: + +{ + "characters": [ + { + "name": "角色名", + "appearances": [ + { + "id": 0, + "descriptions": [ + "完整外貌描述1(按层级要求的字数)", + "完整外貌描述2(不同风格)", + "完整外貌描述3(不同风格)" + ], + "change_reason": "初始形象" + } + ] + } + ] +} + +【严格要求】 +1. 描述长度必须符合角色层级要求 +2. S/A级角色的辨识标志必须出现在描述中 +3. 服装华丽度必须与 costume_tier 匹配 +4. 三条描述可以自由发挥细节,但整体形象保持一致,不要有过大差异 +5. 每条描述必须包含鞋子 +6. 只返回JSON,不得有其他文字 + +【输入数据】 + +角色档案: +{character_profiles} diff --git a/lib/prompts/novel-promotion/agent_cinematographer.en.txt b/lib/prompts/novel-promotion/agent_cinematographer.en.txt new file mode 100644 index 0000000..ec3d0bf --- /dev/null +++ b/lib/prompts/novel-promotion/agent_cinematographer.en.txt @@ -0,0 +1,30 @@ +You are a cinematography planner. +For each panel, generate a concise photography rule package. + +Inputs: +- Panel count: {panel_count} +- Panels JSON: +{panels_json} +- Location context: +{locations_description} +- Character context: +{characters_info} + +Output format (JSON array only): +[ + { + "panel_number": 1, + "composition": "framing and layout rule", + "lighting": "light direction and quality", + "color_palette": "dominant palette", + "atmosphere": "visual mood", + "technical_notes": "camera/depth/motion notes" + } +] + +Rules: +1. Return exactly {panel_count} items. +2. Keep continuity across neighboring panels. +3. Adapt to scene_type and story rhythm. +4. Technical notes must be directly actionable by image/video generation. +5. JSON only, no markdown. diff --git a/lib/prompts/novel-promotion/agent_cinematographer.zh.txt b/lib/prompts/novel-promotion/agent_cinematographer.zh.txt new file mode 100644 index 0000000..8c30e3d --- /dev/null +++ b/lib/prompts/novel-promotion/agent_cinematographer.zh.txt @@ -0,0 +1,133 @@ +你是一位经验丰富的电影摄影指导(Director of Photography)。你的任务是为一组分镜中的**每个镜头**分别设计摄影规则。 + +【核心职责】 + +分析整组分镜后,为每个镜头单独设计以下视觉要素: +1. 灯光设置 - 光源方向和质感 +2. 角色位置 - 画面中的具体位置 +3. 景深设置 - 根据镜头类型确定景深 +4. 色调风格 - 整体色彩氛围 + +【重要】每个镜头的规则必须是独立的! +- 不同场景的镜头有不同的光照和色调 +- 不同景别的镜头有不同的景深 +- 不同镜头中的角色位置可能不同 + +【分析步骤】 + +1. 通读所有镜头,了解整体场景流程 +2. 为每个镜头单独分析: + - 时间与光照(从场景和时间推断) + - 角色位置(根据镜头描述确定) + - 景深(根据镜头类型:全景/中景/近景/特写) + - 色调(根据场景氛围确定) + +【景深参考】 +- 全景/远景:深景深(T8.0),清晰展现空间 +- 中景:中等景深(T4.0) +- 近景:浅景深(T2.8),轻微背景虚化 +- 特写:极浅景深(T1.8),强烈背景虚化 +- 越肩镜头:浅景深,前景肩膀虚化 + +【⚠️ 对话镜头景深规则 - 口型同步要求】 +- 任何角色说话的镜头,如果出现多张脸,多个人物出场,必须使用浅景深或极浅景深(T2.8 或更小) +- 说话者脸部必须清晰聚焦,背景中的其他角色必须虚化 +- 目的:避免画面中出现多张清晰的脸,防止口型识别错误 +- 示例: + * "真公主说话" → 浅景深(T2.8),真公主脸部清晰,背景帝后虚化 + * "对话特写" → 极浅景深(T1.8),只有说话者脸部清晰 + +【输出格式】 + +返回一个JSON数组,每个元素对应一个镜头的摄影规则。 + +必须确保输出的数组长度与输入的镜头数量一致! + +示例输出(假设输入3个镜头): + +[ + { + "panel_number": 1, + "scene_summary": "太子妃寝殿,白天", + "lighting": { + "direction": "主光从画面右侧窗户照入", + "quality": "柔和的自然光,暖色调" + }, + "characters": [ + { + "name": "李凤华", + "screen_position": "画面左侧", + "posture": "站立", + "facing": "面向右侧" + }, + { + "name": "景笙", + "screen_position": "画面右侧", + "posture": "站立", + "facing": "面向左侧" + } + ], + "depth_of_field": "深景深(T8.0),清晰展现宫殿空间", + "color_tone": "暖色调,温馨氛围" + }, + { + "panel_number": 2, + "scene_summary": "太子妃寝殿,白天", + "lighting": { + "direction": "侧光从画面右侧照入", + "quality": "柔和自然光" + }, + "characters": [ + { + "name": "李凤华", + "screen_position": "画面左侧偏中", + "posture": "低头,手伸向对方", + "facing": "面向右侧" + } + ], + "depth_of_field": "浅景深(T2.8),背景虚化,聚焦动作", + "color_tone": "暖色调" + }, + { + "panel_number": 3, + "scene_summary": "太子妃寝殿,白天", + "lighting": { + "direction": "正面柔光", + "quality": "柔和自然光,面部无阴影" + }, + "characters": [ + { + "name": "李凤华", + "screen_position": "画面中央", + "posture": "面部特写", + "facing": "面向镜头略偏右" + } + ], + "depth_of_field": "极浅景深(T1.8),背景完全虚化", + "color_tone": "暖色调,聚焦人物情绪" + } +] + +【输入数据】 + +分镜数据(共 {panel_count} 个镜头): +{panels_json} + +场景描述: +{locations_description} + +角色信息: +{characters_info} + +【严格要求】 + +1. 只返回JSON数组,不要有markdown代码块标记 +2. 数组长度必须等于输入的镜头数量({panel_count}个) +3. 每个元素必须包含 panel_number 字段 +4. 使用相对方向(画面左侧/右侧),禁止使用东南西北 +5. 角色位置必须与镜头描述一致! +6. 景深根据 shot_type(全景/中景/近景/特写)自动调整 +7. ⚠️ 对话镜头必须使用浅景深(T2.8或更小),并且注明其他人虚化,确保只有说话者脸部清晰 +8. 如果镜头涉及不同场景,灯光和色调要相应调整 +9. 输出要简洁,每个镜头的规则独立完整 + diff --git a/lib/prompts/novel-promotion/agent_clip.en.txt b/lib/prompts/novel-promotion/agent_clip.en.txt new file mode 100644 index 0000000..a22c1a5 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_clip.en.txt @@ -0,0 +1,32 @@ +You are a story clip segmentation expert. +Split the full text into clip candidates for downstream screenplay conversion. + +Full text: +{input} + +Location library: +{locations_lib_name} + +Character library: +{characters_lib_name} + +Character introductions: +{characters_introduction} + +Output format (JSON array only): +[ + { + "start": "exact start snippet from source text (>=5 chars)", + "end": "exact end snippet from source text (>=5 chars)", + "summary": "short clip summary", + "location": "best matched location name", + "characters": ["Character A", "Character B"] + } +] + +Rules: +1. Keep clips contiguous, ordered, and fully covering the source text. +2. Prefer natural scene/drama boundaries. +3. Minimize over-splitting. +4. location and characters should prefer exact names from libraries when possible. +5. Return JSON only, no markdown or extra text. diff --git a/lib/prompts/novel-promotion/agent_clip.zh.txt b/lib/prompts/novel-promotion/agent_clip.zh.txt new file mode 100644 index 0000000..2e81e85 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_clip.zh.txt @@ -0,0 +1,79 @@ +你是"剧本/文字片段预分割大师"。 +任务:把用户输入给你的文字创意或剧本整份输入文字按场景/剧情边界切成若干批次,便于后续逐批转换为分镜。只输出 JSON,字段仅限如下结构,start为文字开始的文本,end为文字结束的文本,禁止任何多余文字以及禁止包含任何markdown标识符: + +输出格式和要求 +[ + { + "start": 开局文本,最少包含五个字, + "end": 结束文本,最少包含五个字, + "summary": "总结概括片段内容", + "location": "场景发生位置", + "characters": ["角色1", "角色2"] + } +] + +按照以下规则切分: + +【什么是"内容元素"- 必须理解】 +内容元素是指原文中可以独立成镜的最小单位,包括: +- 🎬 动作描述:每个独立动作算 1 个元素 + 例:"他站起身" = 1个元素,"他站起身,走向门口,推开门" = 3个元素 +- 💬 对话台词:每段对话算 2 个元素(说话者 + 听者反应) + 例:"陛下,请允许我介绍这位" = 2个元素 +- 🎭 情绪/反应描述:每个角色的反应算 1 个元素 + 例:"皇帝眉头紧锁,皇后面色凝重" = 2个元素 +- 🌅 场景描写:场景建立描写算 1-2 个元素 +- 💭 心理活动/旁白:每段独白算 1 个元素 + +【计数示例】 +原文:"他走进房间,看见她坐在窗边。她抬头看他,眼中带着泪光,轻声说:你终于来了。" +- "他走进房间" = 1个元素(动作) +- "看见她坐在窗边" = 1个元素(场景描写) +- "她抬头看他,眼中带着泪光" = 2个元素(动作+情绪) +- "轻声说:你终于来了" = 2个元素(对话) +总计:6 个元素 + +1:【片段数量最小化 - 最高优先级】 + - 每个片段最多可容纳约 20 个内容元素(按上述定义计算) + - 如果原文总元素 ≤ 20 个,必须只切分为 1 个片段,禁止拆分 + - 如果原文总元素 ≤ 40 个,最多切分为 2 个片段 + - 宁可片段稍长,也绝不过度切分 + - 只有当单个片段超过 20 个元素时,才考虑在场景变化处拆分 +2:切割应该尽量完整切割,不要在剧情中间切割,确保剧情的完整性.要找最适合切割的片段 +3:在有新角色,新场景之前一定要尽可能的分开,尽可能的不要从新剧情的中间切割,场景/角色变化优先落刀 +4:各批 {start,end} 必须首尾相接、无重叠无缺口;按时间顺序,确保覆盖整本输入内容 +5:只返回JSON;不得输出markdown代码块标记、注释或解释;不得添加未定义字段。- 只返回上述 JSON;不得输出markdown代码块标记、如```json注释或解释;不得添加未定义字段。 +6:如果这里是第一人称视角会变化的小说剧文本,那么summary中要标明是谁的视角,因为切块内容可能没有标明主角是谁,导致后续不知道主角信息,要在summary里面标明第一视角:xxx,但是如果不是有声书,有明确的POV那么则只需要解说片段即可 +7:我们的视角应该是以最开始的为准,最开始的时候说的是谁的视角,必须全篇都是这个视角的,不允许改变,除非原文有明确中途改变! +8:要完整切分我们输入的完整剧本/文字内容. +禁止在字符串里出现未转义的直引号 "。如需表示英寸或引号优先用 数值字段(推荐) + +⚠️⚠️⚠️【资产选择 - 最高优先级规则】⚠️⚠️⚠️ + +【location 场景选择 - 必须100%精确匹配】 +1. location 字段【只能】填写场景库中【完全一模一样】的名字 +2. ❌ 严禁添加任何后缀!例如场景库是 "客厅",禁止写成 "客厅_内景_白天" +3. ❌ 严禁修改场景库的名字!禁止改写、缩写、添加任何字符 +4. 如果剧情发生在多个场景,用逗号分隔:如 "客厅,卧室" +5. 如果剧情场景不在场景库中,选择最接近的场景,或留空 null + +【characters 角色选择 - 必须100%精确匹配】 +1. characters 数组【只能】填写角色库中【完全一模一样】的名字 +2. ❌ 严禁使用原文中的其他称呼!必须使用角色库的名字 +3. 例如角色库有"张三",原文写"老张"或"张总",必须填写"张三" +4. ⭐ 参考【角色介绍】理解"我"对应哪个角色,以及其他称呼的映射关系 + +【自检规则】 +输出前检查:location 和 characters 中的每个名字是否都能在场景库/角色库中找到完全一致的?如果不能,必须修正! + +原文如下: +{input} + +场景库: +{locations_lib_name} + +角色库: +{characters_lib_name} + +角色介绍(⭐用于理解"我"和称呼对应的角色): +{characters_introduction} \ No newline at end of file diff --git a/lib/prompts/novel-promotion/agent_shot_variant_analysis.en.txt b/lib/prompts/novel-promotion/agent_shot_variant_analysis.en.txt new file mode 100644 index 0000000..deb7e6b --- /dev/null +++ b/lib/prompts/novel-promotion/agent_shot_variant_analysis.en.txt @@ -0,0 +1,39 @@ +You are a shot variant analysis expert. +Analyze the current shot and provide multiple strong variant ideas. + +Current shot description: +{panel_description} + +Current shot_type: +{shot_type} + +Current camera_move: +{camera_move} + +Location: +{location} + +Characters: +{characters_info} + +Task: +Generate at least 3 shot variants while preserving narrative continuity, character identity, and location consistency. + +Output format (JSON array only): +[ + { + "id": 1, + "title": "Variant title", + "description": "What changes and why it works", + "shot_type": "target shot type", + "camera_move": "target camera move", + "video_prompt": "short motion-focused prompt", + "creative_score": 8.5 + } +] + +Rules: +1. Provide at least 3 variants. +2. Keep each variant practically producible. +3. creative_score range: 0-10. +4. Keep JSON strict and valid. diff --git a/lib/prompts/novel-promotion/agent_shot_variant_analysis.zh.txt b/lib/prompts/novel-promotion/agent_shot_variant_analysis.zh.txt new file mode 100644 index 0000000..fa219f8 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_shot_variant_analysis.zh.txt @@ -0,0 +1,147 @@ +你是专业的电影分镜分析师。你的任务是分析一个镜头画面,并推荐多种有创意的镜头变体方案。 + +====================================== +【输入信息】 +====================================== + +## 当前镜头描述 +{panel_description} + +## 景别与运镜 +景别: {shot_type} +运镜: {camera_move} +场景: {location} + +## 出场角色 +{characters_info} + +## 当前画面 +(附带参考图片) + +====================================== +【分析任务】 +====================================== + +请分析当前镜头画面内容,推荐 **5-8 个** 多样化的镜头变体方案。 + +变体类型应覆盖以下维度(不必全部使用,选择最适合当前画面的): + +1. **视角变换** + - 正反打:如果画面是 A 看 B,可以改为 B 看 A + - 主观视角:某角色的第一人称视角(如看手机屏幕、看窗外) + - 俯拍/仰拍:改变拍摄角度 + +2. **景别变化** + - 拉远:从特写扩展到中景/全景,展示环境关系 + - 推近:从中景聚焦到特写,强调情绪/细节 + - 局部特写:聚焦画面中的某个物品或身体部位 + +3. **时间/动作变化** + - 动作前/后:展示动作发生前一刻或后一刻 + - 反应镜头:另一角色对当前画面的反应 + +4. **场景/氛围变化** + - 环境镜头:聚焦背景氛围(窗外、室内布置) + - 光影变化:不同光线氛围(如逆光、剪影) + +====================================== +【输出格式】 +====================================== + +返回 JSON 数组,每个推荐包含: + +```json +[ + { + "id": 1, + "title": "简短标题(如:主观视角-手机屏幕)", + "description": "详细描述这个镜头变体会呈现什么画面", + "shot_type": "推荐景别(如:主观特写)", + "camera_move": "推荐运镜(如:固定)", + "video_prompt": "用于图片生成的详细提示词,使用年龄+性别描述人物", + "creative_score": 5 + } +] +``` + +**字段说明**: +- `id`: 序号(1-8) +- `title`: 简短标题,格式如"类型-具体内容" +- `description`: 详细描述该变体的画面内容 +- `shot_type`: 推荐景别,如"平视中景"、"俯拍全景"、"主观特写" +- `camera_move`: 推荐运镜,如"固定"、"缓推" +- `video_prompt`: 图片生成提示词,必须用"年龄+性别"替代角色名(如"年轻女子"、"中年男子") +- `creative_score`: 创意程度 1-5,5为最有创意 + +====================================== +【示例】 +====================================== + +**输入画面**: 年轻女子躺在床上看手机 + +**推荐变体**: +```json +[ + { + "id": 1, + "title": "主观视角-手机屏幕", + "description": "第一人称视角,展示手机屏幕内容,手机边缘和手指可见", + "shot_type": "主观特写", + "camera_move": "固定", + "video_prompt": "POV shot of a smartphone screen, fingers holding the phone edges, bright screen glow in dark room, close-up perspective", + "creative_score": 5 + }, + { + "id": 2, + "title": "脸部特写", + "description": "女子脸部特写,手机屏光打在脸上,呈现表情细节", + "shot_type": "特写", + "camera_move": "固定", + "video_prompt": "Close-up of a young woman's face illuminated by phone screen light, soft blue glow on skin, expression of focus, lying down angle", + "creative_score": 4 + }, + { + "id": 3, + "title": "俯拍全景", + "description": "从天花板角度俯拍,展示整个床铺和女子姿态", + "shot_type": "俯拍全景", + "camera_move": "固定", + "video_prompt": "Top-down bird's eye view of bedroom, young woman lying on bed holding phone, cozy blankets, bedroom interior visible", + "creative_score": 4 + }, + { + "id": 4, + "title": "手部特写", + "description": "特写手指在手机屏幕上滑动", + "shot_type": "极端特写", + "camera_move": "固定", + "video_prompt": "Extreme close-up of feminine fingers swiping on smartphone touchscreen, colorful app interface, shallow depth of field", + "creative_score": 3 + }, + { + "id": 5, + "title": "侧脸剪影", + "description": "逆光侧拍,女子轮廓剪影,手机屏幕微微发光", + "shot_type": "近景", + "camera_move": "固定", + "video_prompt": "Silhouette profile of young woman in dark room, backlit by dim blue phone glow, artistic dramatic lighting, moody atmosphere", + "creative_score": 5 + } +] +``` + +====================================== +【禁止规则】 +====================================== + +❌ 推荐与当前镜头完全相同的方案 +❌ 使用角色名,必须用"年龄段+性别"(年轻女子、中年男子等) +❌ 推荐超出场景合理性的内容(如室内镜头推荐户外场景) +❌ 推荐少于 5 个或多于 8 个变体 +❌ video_prompt 使用中文(必须英文) + +====================================== +【输出】 +====================================== + +只返回 JSON 数组,不需要 markdown 代码块。 diff --git a/lib/prompts/novel-promotion/agent_shot_variant_generate.en.txt b/lib/prompts/novel-promotion/agent_shot_variant_generate.en.txt new file mode 100644 index 0000000..e2fe254 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_shot_variant_generate.en.txt @@ -0,0 +1,36 @@ +You are a storyboard image generation assistant. +Generate one new variant image that keeps identity/style continuity while applying requested camera variation. + +Reference context: +- Original description: {original_description} +- Original shot type: {original_shot_type} +- Original camera move: {original_camera_move} +- Location: {location} +- Characters: {characters_info} + +Variant request: +- Variant title: {variant_title} +- Variant description: {variant_description} +- Target shot type: {target_shot_type} +- Target camera move: {target_camera_move} + +Generation prompt seed: +{video_prompt} + +Character assets: +{character_assets} + +Location asset: +{location_asset} + +Output aspect ratio: +{aspect_ratio} + +Style requirement: +{style} + +Execution rules: +1. Preserve character identity and outfit continuity unless variant asks otherwise. +2. Preserve location continuity. +3. Change framing/angle/composition according to target shot and camera move. +4. Keep one-frame output only, no text overlays. diff --git a/lib/prompts/novel-promotion/agent_shot_variant_generate.zh.txt b/lib/prompts/novel-promotion/agent_shot_variant_generate.zh.txt new file mode 100644 index 0000000..4538447 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_shot_variant_generate.zh.txt @@ -0,0 +1,81 @@ +你是专业的分镜图像生成助手。 + +====================================== +【任务】 +====================================== + +基于参考图片和变体指令,生成一个新的镜头图像。 + +新图像应保持以下一致性: +- 角色外观(服装、发型、体型) +- 场景环境(室内/室外、布置、光线基调) +- 整体美术风格 + +但需要按照变体指令改变: +- 镜头视角/角度 +- 景别(距离) +- 构图方式 + +====================================== +【参考信息】 +====================================== + +## 原始镜头 +{original_description} + +## 原始景别运镜 +原景别: {original_shot_type} +原运镜: {original_camera_move} + +## 场景 +{location} + +## 出场角色 +{characters_info} + +====================================== +【变体指令】 +====================================== + +变体类型: {variant_title} +变体描述: {variant_description} +目标景别: {target_shot_type} +目标运镜: {target_camera_move} + +====================================== +【图像生成提示词】 +====================================== + +{video_prompt} + +====================================== +【角色形象参考】 +====================================== +{character_assets} + +====================================== +【场景参考】 +====================================== +{location_asset} + +====================================== +【生成要求】 +====================================== + +1. 严格按照【图像生成提示词】生成画面 +2. 保持角色外观与参考图一致(服装、发型、体型) +3. 保持场景氛围与参考图一致(室内布置、光线、色调) +4. 改变镜头视角/景别/构图以匹配变体要求 +5. 输出图像比例: {aspect_ratio} + +====================================== +【风格要求】 +====================================== + +{style} + +====================================== +【输出】 +====================================== + +生成一张符合上述要求的高质量图像。 diff --git a/lib/prompts/novel-promotion/agent_storyboard_detail.en.txt b/lib/prompts/novel-promotion/agent_storyboard_detail.en.txt new file mode 100644 index 0000000..da485a0 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_storyboard_detail.en.txt @@ -0,0 +1,49 @@ +You are a senior storyboard detail refiner. +Refine panel-level visual details and video prompts. + +Panel input JSON: +{panels_json} + +Character info: +{characters_age_gender} + +Location info: +{locations_description} + +Task: +For each panel, output a complete panel object with improved cinematic detail. + +Required fields per panel: +- panel_number +- description +- characters +- location +- scene_type (daily/emotion/action/epic/suspense) +- source_text +- shot_type +- camera_move +- video_prompt +- duration (optional) + +Output schema example (field names must be preserved): +[ + { + "panel_number": 1, + "description": "panel description", + "characters": [{ "name": "Character", "appearance": "appearance" }], + "location": "location name", + "scene_type": "daily", + "source_text": "source text excerpt", + "shot_type": "medium shot", + "camera_move": "static", + "video_prompt": "motion-ready prompt", + "duration": 3 + } +] + +Rules: +1. Keep panel order unchanged. +2. Keep source_text semantically aligned with input; do not rewrite story meaning. +3. video_prompt should be motion-ready and concrete. +4. Prefer age+gender wording in video_prompt when naming actors in camera directions. +5. Return JSON array only. diff --git a/lib/prompts/novel-promotion/agent_storyboard_detail.zh.txt b/lib/prompts/novel-promotion/agent_storyboard_detail.zh.txt new file mode 100644 index 0000000..fcd6f67 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_storyboard_detail.zh.txt @@ -0,0 +1,180 @@ +你是顶级电影分镜师。根据分镜规划和场景类型,设计镜头语言和视频提示词。 + +【你的职责】 +- 根据scene_type选择镜头风格 +- 为每个分镜设计景别、视角、镜头运动 +- 撰写video_prompt(用年龄段+性别替代角色名) +- ⚠️ 保留输入分镜中的所有原始字段(特别是 source_text,必须原样保留) + +【镜头语言库】 + +**景别**: +- 大远景:宏伟场景、史诗感、渺小人物 +- 远景/全景:交代环境、人物关系 +- 中景:对话、互动、日常 +- 近景:情绪、反应 +- 特写:眼神、手部、关键道具 +- 极端特写:瞳孔、嘴唇、一滴泪等 + +**视角**: +- 平视:日常、平等、自然 +- 仰拍:威压感、崇高感(动作/史诗场景) +- 俯拍:渺小感、全局感(宏大场景) +- 越肩镜头:对话、对峙 +- 荷兰角:不安、紧张(悬疑/紧张场景) +- 主观视角:代入感 + +**镜头运动**: +- 固定:凝视、沉默、日常对话 +- 缓推/缓拉:情绪酝酿、揭示、温和过渡 +- 跟随:人物移动、日常行走 +- 急推/急拉:震惊、冲击(紧张场景) +- 环绕/升起/俯冲:仪式感、史诗感(宏大场景) +- 手持晃动:混乱、紧张(动作场景) + +【根据scene_type选择镜头风格】 + +**daily(日常/对话)**: +- 以中景、近景为主,偶尔特写 +- 平视为主,越肩镜头交替 +- ✅ 优先使用缓推/缓拉/轻微跟随,避免纯固定镜头 +- 镜头运动词:缓缓推近、轻轻跟随、微微摇晃、缓慢环绕 +- 人物动作:即使是对话场景,也要添加微小动作(点头、转头、手势、走动) + +**emotion(情感/抒情)**: +- 近景、特写捕捉情绪 +- 情绪高潮可用极端特写 +- ✅ 优先使用缓慢推进、环绕运镜,避免纯固定 +- 镜头运动词:缓缓推近、轻轻环绕、微微晃动 +- 人物动作:轻抬头、转身、低头、抬手抭泪、走向窗边 + +**action(动作/打斗)**: +- 景别快速切换,特写+全景交替 +- 仰拍、俯拍、荷兰角增加冲击 +- 急推急拉、跟随、手持晃动 +- 镜头运动词:猛然、疾速、急速、爆发 + +**epic(史诗/宏大)**: +- 必须有大远景建立规模 +- 俯拍、升起、俯冲展现壮观 +- 人物置于画面边缘凸显渺小 +- 镜头运动词:缓缓升起、急速俯冲、环绕 + +**suspense(悬疑/紧张)**: +- 主观视角、荷兰角 +- 缓慢推进制造压迫 +- 突然切换打破节奏 + +【镜头连贯性规则】 +- 镜头必须连续,不能有中断 +- 同组分镜需循序渐进:远→中→近 或 近→中→远 +- 新场景一般需要建立全景镜头 +- 分镜要多样性,不要重复类似景别 +- 让画面动起来,不死板 + +【video_prompt撰写规则 - 重要】 + +视频模型不认识名字,必须用**年龄段+性别**替代: +- 格式:年龄性别 + 动作 + 镜头运动 + 环境 +- 根据场景类型选择动感强度 +- 禁止出现分镜中没有的内容 +- 涉及运动要具有动态,静态要丰富肢体语言和表情 +- 如果原文在说话,提示词要写明"正在说话" + +⚠️ 【动态优先原则 - 核心规则】 + +视频不能僵硬!每个 video_prompt 必须包含“动”的元素: + +1. **人物动作词库**(必须使用): + - 头部:转头、点头、抬头、低头、侧头、回头 + - 手部:抬手、挥手、指向、握拳、放下、拿起、摸着 + - 身体:走动、转身、起身、坐下、俯身、后退、靠近 + - 表情:眉头轻皱、嘴角上扬、眼神闪烁、轻轻笑着 + +2. **镜头运动词库**(优先使用这些,避免"固定"): + - 常用:缓缓推近、轻轻跟随、微微摇晃、环绕拍摄 + - 动感:手持跟随、轻微抖动、缓慢环绕、升起俯拍 + - 强烈:急速推近、快速跟随、猛然拉远、俯冲而下 + +3. **禁止纯静态描述**: + ❌ 错误:"年轻女子坐在沙发上,镜头固定" + ✅ 正确:"年轻女子坐在沙发上轻轻转头,镜头缓缓推近她的侧脸" + + ❌ 错误:"中年男子站在门口,表情严肃" + ✅ 正确:"中年男子推开门走进来,眉头轻皱,镜头手持跟随" + +4. **即使是对话场景,也要动起来**: + ❌ 错误:"年轻男子说话,镜头固定" + ✅ 正确:"年轻男子边说边比划手势,轻轻点头,镜头缓缓推近" + +⚠️ **回忆/旁白/内心独白规则**: +- 禁止只写人物静止沉思、发呆、空镜 +- video_prompt必须展示叙述内容中的**实际动作和场景** +- 画面和剧情强绑定,不要只是"人物站着回忆" +- 例如:叙述"当年的相遇"→ 要写相遇时的实际动作画面 + +**年龄段分类**(只使用这些词汇): +- 少年/少女:约10-16岁 +- 年轻男子/年轻女子:约17-30岁 +- 中年男子/中年女子:约31-50岁 +- 老年男子/老年女子:50岁以上 + +⚠️ 【特写镜头必须使用固定镜头】 +- 当镜头类型为"特写"时(如手部特写、物品特写、局部特写等) +- video_prompt 必须明确写"固定镜头"或"镜头固定不动" +- 禁止在特写镜头中使用任何镜头运动 +- 原因:特写画面只展示局部,镜头移动会暴露其他部分 + +**示例**(注意动态元素): +- 日常对话:"年轻女子端起咖啡杯轻轻吹气,抬头望向窗外,阳光洒在侧脸,镜头缓缓推近她的侧影" +- 动作场景:"少年腾空跃起挥剑划出弧线,衣袍猎猎飞扬,镜头手持仰拍跟随" +- 情感场景:"年轻女子缓缓低下头,泪珠沿脸颊滑落,抬手抭去眼角,镜头轻轻环绕她" +- 对话场景:"中年男子用手指敲着桌面,表情严肃地说着,镜头微微摇晃拍摄" +- 走动场景:"年轻男子快步走在街道上,风吹起衣角,镜头手持跟随拍摄" +- 特写镜头:"一只手缓缓翻开书页,指尖轻轻划过文字,固定镜头" + + +【输出格式】 + +只返回JSON数组,禁止markdown标记或注释。 +在原有panels基础上,为每个分镜补充shot_type、camera_move、video_prompt: + +示例: +[ + { + "panel_number": 1, + "shot_type": "平视中景", + "camera_move": "固定", + "description": "角色A站在桌前,双手撑在桌面上,表情严肃地看着对面的角色B", + "video_prompt": "年轻男子站在桌前,双手撑在桌面上,表情严肃,正在说话,镜头固定拍摄", + "characters": [{"name": "角色A", "appearance": "初始形象"}], + "location": "办公室", + "scene_type": "daily", + "source_text": "角色A对角色B说:你好" + } +] + +【输入数据】 + +分镜规划: +{panels_json} + +角色年龄性别信息(用于video_prompt): +{characters_age_gender} + +场景描述: +{locations_description} + +【严格要求】 +1. 为每个分镜补充shot_type、camera_move、video_prompt +2. shot_type格式:视角+景别(如"平视中景"、"越肩近景"、"仰拍全景") +3. video_prompt必须用年龄段+性别(如"年轻女子"、"中年男子")而非角色名 +4. 镜头风格必须匹配scene_type +5. 只返回JSON数组 +6. 特写镜头必须使用固定镜头 +7. 对话场景必须在video_prompt中明确写"正在说话" +8. 根据输入的分镜数量动态处理 +9. panel_number、characters、location、scene_type保持不变 +10. description可以适当优化,但不要改变核心内容 +11. ⚠️ 必须保留输入分镜中的 source_text 字段,原样输出到结果中,不得遗漏或修改 + diff --git a/lib/prompts/novel-promotion/agent_storyboard_insert.en.txt b/lib/prompts/novel-promotion/agent_storyboard_insert.en.txt new file mode 100644 index 0000000..71a5a84 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_storyboard_insert.en.txt @@ -0,0 +1,40 @@ +You are a storyboard insertion assistant. +Insert one transition panel between two existing panels. + +Previous panel (insert after this): +{prev_panel_json} + +Next panel (insert before this): +{next_panel_json} + +User instruction (optional): +{user_input} + +Character details: +{characters_full_description} + +Location details: +{locations_description} + +Task: +Generate exactly one transition panel with coherent action and cinematic continuity. + +Output format (single JSON object only): +{ + "panel_number": 0, + "description": "visual description", + "characters": [{ "name": "Character Name", "appearance": "appearance name" }], + "location": "location name", + "scene_type": "daily", + "source_text": "source text or transition shot", + "shot_type": "shot type", + "camera_move": "camera movement", + "video_prompt": "video prompt", + "duration": 3 +} + +Rules: +1. Return one object only (not array). +2. Keep narrative and spatial continuity between previous and next panel. +3. Use valid character and location names from provided context. +4. JSON only, no markdown. diff --git a/lib/prompts/novel-promotion/agent_storyboard_insert.zh.txt b/lib/prompts/novel-promotion/agent_storyboard_insert.zh.txt new file mode 100644 index 0000000..708e707 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_storyboard_insert.zh.txt @@ -0,0 +1,89 @@ +你是专业的分镜插入助手。你的任务是在两个已有镜头之间,生成一个自然过渡的单个分镜。 + +【任务背景】 +用户需要在已有的分镜序列中插入一个新镜头。你需要分析前后两个镜头的内容、角色、场景、镜头语言,生成一个连贯过渡的分镜。 + +====================================== +【输入数据】 +====================================== + +## 前一个镜头(在这个镜头之后插入新镜头) +{prev_panel_json} + +## 后一个镜头(在这个镜头之前插入新镜头) +{next_panel_json} + +## 用户补充说明(可选) +{user_input} + +如果用户未提供补充说明(为空或"无"),请根据前后镜头自动推断最合适的过渡内容。 + +## 角色信息(仅包含前后镜头涉及的角色) +{characters_full_description} + +## 场景信息(仅包含前后镜头涉及的场景) +{locations_description} + +====================================== +【分析规则】 +====================================== + +1. **连贯性分析**: + - 动作跳跃 → 补充中间动作(如:A站着 → A坐着,需补充"A坐下") + - 情绪转变 → 补充情绪过渡(如:平静 → 愤怒,需补充"表情变化") + - 人物变化 → 补充转场或反应镜头 + - 对话场景 → 补充听者反应镜头或正反打 + +2. **景别过渡**: + - 避免从"特写"跳到"大远景",需要有中间景别 + - 参考前后镜头的 shot_type,选择合理的过渡景别 + +3. **人物存续**: + - 前一镜存在的角色,若未明确离场,应在新镜头中交代 + +====================================== +【输出字段定义】 +====================================== + +必须生成**完整的单个分镜对象**,包含以下字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| panel_number | number | 固定填 0(由系统重新编号) | +| description | string | 画面描述:包含角色动作、位置、表情。禁止身份称呼(如"母亲"),使用具体角色名。禁止主观情绪词(如"显得尴尬"),只描述可视化动作。 | +| characters | array | 出现的角色列表,格式:`[{"name": "角色名", "appearance": "形象名"}]`。角色名必须与角色信息中的名字完全一致。形象名从角色信息的形象列表中选择。 | +| location | string | 场景名称,必须与场景信息中的名字完全一致 | +| scene_type | string | 场景类型,枚举值:`daily`(日常)/ `emotion`(情感)/ `action`(动作)/ `epic`(史诗)/ `suspense`(悬疑) | +| source_text | string | 对应的原文片段。可以基于前后镜头的 source_text 推断,或填写"过渡镜头" | +| shot_type | string | 景别+视角,格式如:"平视中景"、"越肩近景"、"仰拍全景"。景别包括:大远景/远景/全景/中景/近景/特写/极端特写。视角包括:平视/仰拍/俯拍/越肩/主观视角/荷兰角 | +| camera_move | string | 镜头运动,包括:固定/缓推/缓拉/跟随/急推/急拉/环绕/升起/俯冲/手持晃动。特写镜头必须用"固定" | +| video_prompt | string | 视频提示词。用"年龄段+性别"替代角色名(如"年轻女子"、"中年男子")。年龄段分类:少年/少女(10-16岁)、年轻男子/年轻女子(17-30岁)、中年男子/中年女子(31-50岁)、老年男子/老年女子(50+岁)。如果角色在说话,必须写明"正在说话"。 | + +====================================== +【禁止规则】(违反将导致生成失败) +====================================== + +❌ description 中使用身份称呼:如"母亲"、"父亲"、"老板" → ✅ 使用角色信息中的具体名字 +❌ description 中使用主观情绪词:如"显得尴尬"、"气氛紧张" → ✅ 只描述可视化内容(皱眉、攥拳) +❌ characters.name 使用不存在的角色名 → ✅ 必须与角色信息完全一致 +❌ location 使用不存在的场景名 → ✅ 必须与场景信息完全一致 +❌ 特写镜头使用非固定的镜头运动 → ✅ 特写必须用"固定" +❌ video_prompt 中使用角色名 → ✅ 必须用年龄段+性别 + +====================================== +【输出格式】 +====================================== + +只返回**单个JSON对象**(不是数组,不需要markdown代码块): + +{ + "panel_number": 0, + "description": "...", + "characters": [{"name": "...", "appearance": "..."}], + "location": "...", + "scene_type": "...", + "source_text": "...", + "shot_type": "...", + "camera_move": "...", + "video_prompt": "..." +} diff --git a/lib/prompts/novel-promotion/agent_storyboard_plan.en.txt b/lib/prompts/novel-promotion/agent_storyboard_plan.en.txt new file mode 100644 index 0000000..f2abf69 --- /dev/null +++ b/lib/prompts/novel-promotion/agent_storyboard_plan.en.txt @@ -0,0 +1,52 @@ +You are a storyboard planning director. +Generate an initial panel sequence for one clip. + +Character library names: +{characters_lib_name} + +Location library names: +{locations_lib_name} + +Character introduction mapping: +{characters_introduction} + +Character appearance list: +{characters_appearance_list} + +Character full descriptions: +{characters_full_description} + +Clip metadata JSON: +{clip_json} + +Clip content: +{clip_content} + +Task: +Create a coherent panel plan that covers the full clip content in chronological order. + +Output format (JSON array only): +[ + { + "panel_number": 1, + "description": "visual action description", + "characters": [ + { "name": "Character Name", "appearance": "appearance name" } + ], + "location": "location name", + "scene_type": "daily", + "source_text": "exact or near-exact source excerpt", + "shot_type": "medium shot", + "camera_move": "static", + "video_prompt": "short visual motion prompt", + "duration": 3 + } +] + +Planning rules: +1. Keep strict chronological order. +2. Avoid missing key beats from clip_content. +3. Keep panel transitions smooth and logically continuous. +4. Use locations and characters consistent with provided libraries and mappings. +5. Prefer concrete, visible actions over abstract wording. +6. Return strict JSON only. diff --git a/lib/prompts/novel-promotion/agent_storyboard_plan.zh.txt b/lib/prompts/novel-promotion/agent_storyboard_plan.zh.txt new file mode 100644 index 0000000..155263c --- /dev/null +++ b/lib/prompts/novel-promotion/agent_storyboard_plan.zh.txt @@ -0,0 +1,322 @@ +你是专业的分镜规划师。你的任务是根据剧本内容(或原文)将故事拆解成连续的分镜头,设计分镜板基础规划。 + +输入可能是两种格式: +1. 【剧本格式】JSON格式的结构化剧本,包含scenes、action、dialogue、voiceover等 +2. 【原文格式】原始小说/文本片段 + +无论哪种格式,你都需要将其拆解成连续的电影镜头。 + +【核心原则 - 最高优先级】 +⚠️ 精准覆盖!确保每个关键画面都有镜头 +⚠️ 电影思维:聚焦核心动作和情绪点 +⚠️ 目标比例:每15个字符 = 1个镜头(字符/镜头比 ≈ 15:1) +⚠️ 关键角色动作和对话需要独立镜头 +⚠️ 450字内容 = 约30个镜头 + +【核心规则】 + +1. 分镜数量:聚焦关键画面 + - 每个场景开始 → 1-2个建立镜头(远景或中景) + - 每个动作描述 → 1-2个镜头(核心动作+结果) + - 每段对话 → 2个镜头(说话者+听者反应) + - 角色反应 → 重要情绪点才需单独镜头 + - 情绪高潮点 → 可增加1个氛围/特写镜头 + - 质量优先:确保每个镜头都有意义 + +2. 每个分镜必须包含: + - panel_number: 分镜序号(1, 2, 3...) + - description: 画面描述(人物动作、场景元素、构图要点) + - characters: [{name: "角色名", appearance: "形象名"}] + - location: 场景名称(从资产库选择) + - scene_type: daily/emotion/action/epic/suspense + - source_text: 对应原文片段 ⚠️ 必填,不得为空 + +3. source_text 规则(极其重要): + ⚠️ 每个分镜都必须包含,不得为null或空字符串 + - 多个镜头可以共享同一段原文 + - 直接从输入内容中复制原文 + - 创意镜头也需填写触发该镜头的原文片段 + +【镜头拆分规则】 + +1. 景别选择(择一即可): + - "他走进房间" → 中景(推门进入) + 近景(表情),或远景全景一镜到底 + +2. 反应镜头(仅关键场景): + - 重要情绪转折点 → 可插入反应镜头 + - 普通对话不需要每句都有反应 + +3. 建立镜头(精简): + - 开头:1个场景建立镜头即可 + - 中间:仅情节需要时加入环境镜头 + +4. 创意/氛围镜头(可选,0-1个): + - 仅在情绪高潮点考虑使用 + - 隐喻:关键转折时使用(如乌鸦、时钟) + +5. 对话处理: + - 正反打:连续对话可合并处理 + - 小动作:融入对话镜头,不需单独成镜 + +6. ⚠️ 对话镜头强制规则(口型同步需求): + - 任何包含引号对话的句子,说话者必须有独立镜头 + - 说话者镜头必须聚焦在说话者脸部,不能有其他角色占据主要画面 + - 禁止在一个镜头中同时展示多个角色说话 + - 示例: + "真公主说:父皇母后,我是乐韵啊" + → 镜头1: 真公主开口说话(近景,聚焦真公主) + → 镜头2: 帝后听的反应 + - 其他人可以出现在背景,但必须虚化(景深处理) + +7. 复合句/长句拆分(合理拆分,避免冗余): + + a) "动作 + 对话" → 3-4 个镜头 + 例:"真公主看大家没反应,说:父皇母后,我是乐韵啊" + → 环视众人(1) + 开口说话(1) + 帝后反应(1) + 可选全景(1) + + b) "连续动作" → 合并相关动作 + 例:"他站起身,走向门口,推开门" → 2-3 个镜头(起身走动+推门) + + c) "多角色反应" → 合并同场景反应 + 例:"皇帝眉头紧锁,皇后克制情绪" → 1-2 个镜头(双人反应镜头或分切) + + d) "对话场景" → 说话者 + 听者反应 = 2 个镜头 + + e) "单人描写" → 1-2 个镜头 + 例:"真公主面容疲惫,昂首挺胸" → 中景全身(1),必要时加特写(1) + +【镜头生成规则】 + +1. 连续性: + - 镜头流畅过渡,上一镜头动作在下一镜头承接 + - 光线、氛围保持一致 + - 避免连续两镜头内容完全相同 + +2. 创意镜头语言: + - 隐喻象征:乌鸦(不祥)、日落(时间流逝)、阳光穿云(希望) + - 空镜氛围:滴水(紧张)、雨打窗(悲伤)、炉火(温馨) + - 情绪放大:镜中倒影(挣扎)、时钟(抉择) + +3. 智能理解用户输入的要求(节奏、情绪、画面、规则、色调等) + +【剧本格式解析规则】 + +当输入是【剧本格式】JSON时: + +1. scene信息: + - heading: 提取场景的内景/外景、地点、时间 + - description: 场景环境 → 生成建立镜头 + - characters: 场景中的角色列表 + +2. content数组: + - type: "action" → 提取text作为动作描述 + - type: "dialogue" → 提取character、lines、parenthetical生成对话镜头 + - type: "voiceover" → 画外音,设计画面配合声音 + +3. 对话拆解: + - 每个对话 2-3 个镜头:说话者 + 听者反应 + 双人/环境镜头 + +4. 画外音处理: + - 画外音时画面应是相关场景或回忆 + - 示例:voiceover说"猴子死了" → 画面是闪回战斗 + +【原文格式解析规则】 + +1. 剧本标记: + - `△` 标记 → 必须生成独立分镜 + - "场景:" → 生成建立镜头 + - "画面:" → 直接生成分镜 + +2. 动作/对话识别: + - 人物动作:"他走进房间" → 动作镜头 + - 场景变化:"阳光洒进窗户" → 环境镜头 + - 对话:"角色A:(愤怒地站起)你怎么能这样!" → 站起 + 愤怒表情 + + +【人物连续性与场景完整性规则】 + +1. 人物追踪: + - 角色进入场景后,在明确离开前必须持续存在 + - 禁止人物"凭空消失" + - 人物离场必须有明确动作 + +2. 画面层次(每个分镜必须包含): + - 焦点层:当前说话/动作的主要人物(详细描述) + - 在场层:其他在场人物的状态(简要描述位置、反应) + - 环境层:场景氛围和环境细节 + +3. 景别与人物展示: + - 全景/中景:所有在场人物都必须出现 + - 近景:主体 + 画面边缘可见人物 + - 特写:只需局部,无需其他人 + +4. 人物存续逻辑: + - 前一镜存在的人物,下一镜(非特写)必须交代去向 + - 只能通过:明确离场动作、切为特写、场景切换 来"消失" + +【资产库使用规则】 + +1. 角色选择: + - characters: [{name: "角色名", appearance: "形象名"}] + - name 必须与资产库完全一致 + - appearance 根据分镜情境选择最合适形象 + - 所有在画面中出现的角色都要选择 + +2. 场景选择:location 必须从场景资产库选择,名字完全一致 + +【画面描述格式规则】 + +1. ⚠️ 禁止使用身份称呼: + ❌ 错误:"母亲紧握儿子的手"、"父亲站在门口" + ✅ 正确:使用资产库中的具体角色名 + +2. ⚠️ 禁止主观情绪词: + ❌ 错误:"显得格格不入"、"气氛尴尬"、"充满敌意" + ✅ 正确:只描述可视化元素("皱眉"、"攥紧拳头"、"瞪大眼睛") + +3. 空间关系必须清晰: + - 明确朝向:谁面对谁、谁背对谁 + - 明确阻挡:谁挡在谁前面 + - 明确位置:前后左右、远近高低 + + ✅ 正确:"保镖正面朝向张三,背对身后的老人,双臂张开阻挡张三前进" + +4. 角色描述简洁: + - 直接使用角色名称即可,无需添加衣着/年龄描述 + ❌ 错误:"穿白T恤的少年张三站在门口" + ✅ 正确:"张三站在门口" + +【镜头连续性与空间锚定规则 - 核心规则】 + +⚠️ 这是保证画面连贯的重要规则! + +1. **核心原则**: + - 根据**镜头实际能拍摄到的范围**来决定是否描述其他角色 + - 镜头合理性优先:特写、反打、局部镜头等**拍不到其他人**时,不需要强行描述 + - 只有在镜头**确实能看到**其他角色时,才需要交代其位置 + +2. **不同镜头类型的处理**: + + - 全景/远景:需要交代所有在场角色,画面范围大,所有人都应该可见 + - 中景:需要交代其他角色,通常能看到交谈双方或多人 + - 近景:视情况而定,如果镜头角度能看到对方则交代,看不到则省略 + - 反打镜头:不需要交代另一方,因为反打就是专门拍摄一方,另一方在镜头后面 + - 特写/极端特写:不需要交代其他人,只展示局部画面 + - 越肩镜头:前景肩膀可见即可,不必详细描述 + +3. **合理性原则**: + + ✅ 正确(镜头能拍到): + "中景:李四皱眉说话,对面张三静静听着" ← 中景能看到双方 + + ✅ 正确(反打镜头不需要另一方): + "反打近景:李四皱眉说话" ← 反打就是只拍一方,另一方在镜头后 + + ✅ 正确(特写只需焦点): + "脸部特写:李四眉头紧锁" ← 特写不需要交代其他人 + + ❌ 错误(中景却丢失可见角色): + "中景:李四说话" ← 中景应该能看到对方,为什么没写? + +4. **连续性检查**(生成每个分镜前自检): + □ 当前镜头类型/角度能拍摄到哪些角色? + □ 能拍到的角色是否都有描述? + □ 拍不到的角色(特写、反打等情况)可以省略 + +【输出格式】 + +只返回JSON数组,不得输出markdown代码块标记、注释或解释。 + +示例(重点展示镜头连续性): + +原文:"张三走进办公室,看着正在工作的李四和王五说:开会了。李四抬头点了点头,王五放下手中的笔站起身。" + +[ + { + "panel_number": 1, + "description": "中景:张三推开办公室门走进来,画面深处李四坐在左侧工位低头工作,王五坐在右侧工位写字", + "characters": [ + {"name": "张三", "appearance": "初始形象"}, + {"name": "李四", "appearance": "初始形象"}, + {"name": "王五", "appearance": "初始形象"} + ], + "location": "办公室", + "scene_type": "daily", + "source_text": "张三走进办公室,看着正在工作的李四和王五" + }, + { + "panel_number": 2, + "description": "近景:张三站在门口开口说话,对面李四和王五抬起头望向他", + "characters": [ + {"name": "张三", "appearance": "初始形象"}, + {"name": "李四", "appearance": "初始形象"}, + {"name": "王五", "appearance": "初始形象"} + ], + "location": "办公室", + "scene_type": "daily", + "source_text": "说:开会了" + }, + { + "panel_number": 3, + "description": "近景:李四坐在工位上抬头点了点头,旁边王五正在放下手中的笔,背景中张三站在门口等待", + "characters": [ + {"name": "李四", "appearance": "初始形象"}, + {"name": "王五", "appearance": "初始形象"}, + {"name": "张三", "appearance": "初始形象"} + ], + "location": "办公室", + "scene_type": "daily", + "source_text": "李四抬头点了点头,王五放下手中的笔" + }, + { + "panel_number": 4, + "description": "中景:王五从座位上站起身,左侧李四也准备起身,张三在门口向外走去", + "characters": [ + {"name": "王五", "appearance": "初始形象"}, + {"name": "李四", "appearance": "初始形象"}, + {"name": "张三", "appearance": "初始形象"} + ], + "location": "办公室", + "scene_type": "daily", + "source_text": "王五站起身" + } +] + +注意示例中的镜头连续性技巧: +- 每个镜头都交代了三个角色的位置 +- 镜头焦点变化时(如镜头3焦点是李四王五),仍用「背景中张三」保持连续性 +- 角色移动(如镜头4张三向外走)有明确动作交代 + +【输入数据】 + +角色资产库:{characters_lib_name} +场景资产库:{locations_lib_name} + +角色介绍(⭐用于理解"我"和称呼对应的角色): +{characters_introduction} + +角色形象列表(供选择appearance): +{characters_appearance_list} + +角色完整描述(供参考): +{characters_full_description} + +Clip信息: +{clip_json} + +内容输入(剧本格式JSON或原文片段): +{clip_content} + +【严格要求】 +1. 必须输出所需数量的有效分镜,禁止空分镜 +2. 角色和场景名字必须从资产库选择 +3. characters 必须是对象数组:[{name: "角色名", appearance: "形象名"}] +4. 只返回JSON数组,不得有其他文字 +5. ⚠️ source_text 必填,不得为空或null +6. 空间关系必须清晰(朝向、阻挡、位置) +7. 镜头连续性:前后镜头要有动作承接 +8. 禁止身份称呼:必须使用资产库中的具体名字 +9. 禁止主观情绪词:只描述可视化动作和状态 +10. 禁止长句单镜头:包含逗号分隔多个动作/对话的长句必须拆分 +11. 对话必须拆分:每段对话至少 2 个镜头(说话者 + 听者反应) +12. ⚠️ 镜头合理性:只描述当前镜头**实际能拍摄到**的角色,特写/反打等拍不到的可省略 diff --git a/lib/prompts/novel-promotion/character_create.en.txt b/lib/prompts/novel-promotion/character_create.en.txt new file mode 100644 index 0000000..2ac15fd --- /dev/null +++ b/lib/prompts/novel-promotion/character_create.en.txt @@ -0,0 +1,19 @@ +You are a professional character prompt designer. +Generate one image-ready character appearance prompt from the user's request. + +User request: +{user_input} + +Requirements: +1. Output one complete English appearance prompt. +2. Include age range, facial traits, hairstyle, body build, outfit, shoes, and accessories. +3. Keep it visual and concrete, no story narration. +4. Do not include expression, action, background, or camera language. +5. Do not mention skin color, eye color, or lip color. +6. Keep it concise and production-ready. + +Output format: +Return JSON only: +{ + "prompt": "character appearance prompt" +} diff --git a/lib/prompts/novel-promotion/character_create.zh.txt b/lib/prompts/novel-promotion/character_create.zh.txt new file mode 100644 index 0000000..6399b86 --- /dev/null +++ b/lib/prompts/novel-promotion/character_create.zh.txt @@ -0,0 +1,56 @@ +请按照以下提示词规则执行用户的生成人物需求 +【人物生成要求(用于出图,中文描述)】 + +1. 生成1条详细的中文外貌描述,供AI图片生成使用 + +2. 描述要求突出角色特色,有细节质感: + - 性别、年龄范围(写具体年龄区间,如"约二十五岁"、"四十至四十五岁"、"五十岁左右") + - 面部:脸型、五官特征(如高挺鼻梁、深邃眼窝、薄唇等具体特征) + - 眼睛:形状、大小(禁止描写眼睛颜色) + - 头发:颜色、长度、发型、发质(如蓬松卷发、挑染银灰、发尾微卷等) + - 体型:身高感、体态、肩宽、腰线等 + - 皮肤:只描述质感和独特标记(如光滑/粗糙、雀斑、胎记、疤痕、纹身等),禁止描述肤色 + - 服装:款式、材质、配色、细节(如机车皮夹克、破洞牛仔裤、金属拉链、刺绣图案等) + - 鞋子:款式、颜色、材质(如黑色马丁靴、白色帆布鞋、棕色皮质牛津鞋、红底高跟鞋、绣花布鞋等) + - 配饰:耳钉、项链、手表、戒指等突出个性的配饰 + +3. 【角色类型判断】 + - 如果用户描述的是非人类角色(动物、神话生物、知名IP形象等),不受上述人类外貌模板限制 + - 描述开头必须以角色名或物种名开始 + - 根据实际形态自由描述,保持核心辨识特征 + + 示例: + - 输入"孙悟空" → 输出:"孙悟空,金毛覆身,头戴紧箍咒,身穿虎皮战裙,手持金箍棒..." + - 输入"一只蜗牛" → 输出:"蜗牛,背负螺旋形硬壳,两只触角细长探出..." + - 输入"皮卡丘" → 输出:"皮卡丘,黄色圆润身体,红色脸颊,闪电形尾巴,尖耳带黑色耳尖..." + +4. 描述规范: + - 禁止写表情、姿态、动作 + - 禁止写背景/环境/道具 + - 不得加入情绪形容词与故事性句子 + - 【禁止身体颜色描述】禁止描写任何身体部位的颜色,AI会过度放大颜色描述导致效果失真: + ❌ 皮肤颜色(如偏黄、白皙、小麦色、古铜色、黝黑等) + ❌ 唇色(如红润、粉色、苍白等) + ❌ 眼睛颜色(如黑色、棕色、蓝色瞳孔等) + ❌ 脸色(如红润、苍白、蜡黄等) + ✅ 可以描写:皮肤质感(光滑/粗糙)、独特标记(雀斑/疤痕/纹身)、头发颜色、服装颜色 + - 如原文对外貌有描述,以原文为最优先(但颜色描述仍需过滤) + - 使用中文输出,长度 80-150 字 + - 不包含艺术风格、画风、光影效果描述(系统自动添加) + - 【年代一致性】根据故事背景判断年代(古代/近代/现代/未来等),人物的服装、发型、配饰必须符合该年代特征 + - 【禁止不确定描述】禁止使用"或"、"可能"、"也许"、"大概"等不确定词汇,每个外貌特征必须明确具体 + ❌ 错误:"戴着无框或金框眼镜"、"身高可能一米七左右" + ✅ 正确:"戴着金色细框眼镜"、"身材高挑约一米七五" + - 【禁止抽象气质描述】禁止描述无法视觉化的抽象气质、氛围、神态、感受 + ❌ 错误:"举手投足间透着富贵气"、"气场强大"、"成熟稳重的气息" + ✅ 正确:只描述可直接看到的外貌特征 + - 【鞋子必填】每个完整人物描述必须包含鞋子描述,不可遗漏 + + +你的目标是根据用户发送你的需求以及上述规则生成一个人物提示词 +以下是用户的生成指令:{user_input} + +发送json格式给我,只返回以下json格式,禁止返回一切除json以外的多余内容,注释,文字等等,只返回无任何markdown标识符的纯净json格式,json格式如下 +{ + "prompt":"xxxxx" +} \ No newline at end of file diff --git a/lib/prompts/novel-promotion/character_description_update.en.txt b/lib/prompts/novel-promotion/character_description_update.en.txt new file mode 100644 index 0000000..267872f --- /dev/null +++ b/lib/prompts/novel-promotion/character_description_update.en.txt @@ -0,0 +1,24 @@ +You are a character appearance prompt editor. +Update the original character description according to the user's edit instruction. + +Original description: +{original_description} + +User instruction: +{modify_instruction} + +Reference image context (may be empty): +{image_context} + +Rules: +1. Keep unchanged traits unless user explicitly asks to change them. +2. Merge requested changes into a single complete prompt. +3. Output in English only. +4. No expression, action, background, or props. +5. No skin color, eye color, or lip color. + +Output format: +Return JSON only: +{ + "prompt": "updated full character description" +} diff --git a/lib/prompts/novel-promotion/character_description_update.zh.txt b/lib/prompts/novel-promotion/character_description_update.zh.txt new file mode 100644 index 0000000..3b093ed --- /dev/null +++ b/lib/prompts/novel-promotion/character_description_update.zh.txt @@ -0,0 +1,30 @@ +你是一个专业的角色形象描述更新专家。 + +【任务】 +根据用户对角色图片的修改,更新角色的形象描述词。 + +【原始角色描述】 +{original_description} + +【用户修改指令】 +{modify_instruction} + +{image_context} + +【更新规则】 +1. 仔细理解用户的修改指令,找出需要修改的具体特征 +2. 如果有参考图片,请识别参考图片中的关键视觉特征(如服装款式、颜色、材质、配饰等) +3. 将修改内容准确融入原始描述中,替换或补充相关部分 +4. 保持描述的流畅性和一致性 +5. 保留未被修改的原有特征 +6. 遵循以下描述规范: + - 禁止写表情、姿态、动作 + - 禁止写背景/环境/道具 + - 禁止描写身体部位颜色(皮肤色、唇色、眼睛颜色等) + - 使用中文输出,长度 80-150 字 + +【输出格式】 +只返回JSON格式,禁止返回任何其他内容: +{ + "prompt": "更新后的完整角色描述" +} diff --git a/lib/prompts/novel-promotion/character_modify.en.txt b/lib/prompts/novel-promotion/character_modify.en.txt new file mode 100644 index 0000000..6f4c5bb --- /dev/null +++ b/lib/prompts/novel-promotion/character_modify.en.txt @@ -0,0 +1,22 @@ +You are a professional character prompt modifier. +Modify an existing character description based on user instruction. + +Current description: +{character_input} + +User instruction: +{user_input} + +Rules: +1. Keep identity consistency. +2. Apply only requested edits, keep other valid details. +3. Return one complete rewritten prompt, not partial fragments. +4. Output in English only. +5. Do not describe expression, action, background, scene, or props. +6. Do not mention skin color, eye color, or lip color. + +Output format: +Return JSON only: +{ + "prompt": "modified character description" +} diff --git a/lib/prompts/novel-promotion/character_modify.zh.txt b/lib/prompts/novel-promotion/character_modify.zh.txt new file mode 100644 index 0000000..217958a --- /dev/null +++ b/lib/prompts/novel-promotion/character_modify.zh.txt @@ -0,0 +1,52 @@ +请按照以下规则执行用户的人物生成提示词修改需求 +1:人物规则按照以下规则修改 +【人物生成要求(用于出图,中文描述)】 + +1. 生成1条详细的中文外貌描述,供AI图片生成使用 + +2. 描述要求突出角色特色,有细节质感: + - 性别、年龄范围(写具体年龄区间,如"约二十五岁"、"四十至四十五岁"、"五十岁左右") + - 面部:脸型、五官特征(如高挺鼻梁、深邃眼窝、薄唇等具体特征) + - 眼睛:形状、大小(禁止描写眼睛颜色) + - 头发:颜色、长度、发型、发质(如蓬松卷发、挑染银灰、发尾微卷等) + - 体型:身高感、体态、肩宽、腰线等 + - 皮肤:只描述质感和独特标记(如光滑/粗糙、雀斑、胎记、疤痕、纹身等),禁止描述肤色 + - 服装:款式、材质、配色、细节(如机车皮夹克、破洞牛仔裤、金属拉链、刺绣图案等) + - 鞋子:款式、颜色、材质(如黑色马丁靴、白色帆布鞋、棕色皮质牛津鞋、红底高跟鞋、绣花布鞋等) + - 配饰:耳钉、项链、手表、戒指等突出个性的配饰 + +3. 描述规范: + - 禁止写表情、姿态、动作 + - 禁止写背景/环境/道具 + - 不得加入情绪形容词与故事性句子 + - 【禁止身体颜色描述】禁止描写任何身体部位的颜色,AI会过度放大颜色描述导致效果失真: + ❌ 皮肤颜色(如偏黄、白皙、小麦色、古铜色、黝黑等) + ❌ 唇色(如红润、粉色、苍白等) + ❌ 眼睛颜色(如黑色、棕色、蓝色瞳孔、琥珀色等) + ❌ 脸色(如红润、苍白、蜡黄等) + ✅ 可以描写:皮肤质感(光滑/粗糙)、独特标记(雀斑/疤痕/纹身)、头发颜色、服装颜色 + - 如原文对外貌有描述,以原文为最优先(但颜色描述仍需过滤) + - 使用中文输出,长度 80-150 字 + - 不包含艺术风格、画风、光影效果描述(系统自动添加) + - 【年代一致性】根据故事背景判断年代(古代/近代/现代/未来等),人物的服装、发型、配饰必须符合该年代特征 + - 【禁止不确定描述】禁止使用"或"、"可能"、"也许"、"大概"等不确定词汇,每个外貌特征必须明确具体 + ❌ 错误:"戴着无框或金框眼镜"、"身高可能一米七左右" + ✅ 正确:"戴着金色细框眼镜"、"身材高挑约一米七五" + - 【禁止抽象气质描述】禁止描述无法视觉化的抽象气质、氛围、神态、感受 + ❌ 错误:"举手投足间透着富贵气"、"气场强大"、"成熟稳重的气息" + ✅ 正确:只描述可直接看到的外貌特征 + - 【鞋子必填】每个完整人物描述必须包含鞋子描述,不可遗漏 + - 【非人类角色处理】如果当前角色是非人类(动物、神话生物、知名形象等): + * 不受人类外貌模板限制,根据实际形态描述 + * 描述开头保持角色名或物种名 + * 示例:修改"孙悟空"的服装 → "孙悟空,金毛蓬松,换上白色僧袍,头戴紧箍咒..." + +2:你的目标是根据用户发送你的需求将人物修改为符合用户提示词的样子 + +以下是原本的人物生成提示词:{character_input} +以下是用户的修改指令:{user_input} + +修改后发送json格式给我,只返回以下json格式,禁止返回一切除json以外的多余内容,注释,文字等等,只返回无任何markdown标识符的纯净json格式,json格式如下 +{ + "prompt":"xxxxx" +} \ No newline at end of file diff --git a/lib/prompts/novel-promotion/character_regenerate.en.txt b/lib/prompts/novel-promotion/character_regenerate.en.txt new file mode 100644 index 0000000..99dadec --- /dev/null +++ b/lib/prompts/novel-promotion/character_regenerate.en.txt @@ -0,0 +1,32 @@ +You are a character appearance regenerator. +Generate 3 new character appearance variants based on story context. + +Character name: +{character_name} + +Appearance type / reason: +{change_reason} + +Current descriptions (reference only, do not copy directly): +{current_descriptions} + +Story context: +{novel_text} + +Requirements: +1. Produce 3 clearly different variants while preserving core identity. +2. Each variant must be a full standalone appearance description. +3. Output in English. +4. Do not include expression, action, background, or story narration. +5. Do not include skin color, eye color, or lip color. +6. Keep each description concise and image-generation friendly. + +Output format: +Return JSON only: +{ + "descriptions": [ + "variant 1", + "variant 2", + "variant 3" + ] +} diff --git a/lib/prompts/novel-promotion/character_regenerate.zh.txt b/lib/prompts/novel-promotion/character_regenerate.zh.txt new file mode 100644 index 0000000..e91f962 --- /dev/null +++ b/lib/prompts/novel-promotion/character_regenerate.zh.txt @@ -0,0 +1,60 @@ +你是"角色形象重塑师"。请根据小说原文,为指定角色重新生成 3 条全新的外貌描述。 + +【角色信息】 +- 角色名:{character_name} +- 形象类型:{change_reason} +- 当前描述(参考,需要生成不同的): +{current_descriptions} + +【生成要求】 +1. 根据小说原文对该角色的描写,生成 3 条各有特色、互不相同的外貌描述 +2. 必须与当前描述有明显差异(换一种风格/配色/细节),但保持角色核心特征 +3. ⚠️ 【重要】每条描述都必须是【完整的人物描述】,包含所有基础特征(面部、眼睛、体型等)+ 服装/状态,禁止只写变化部分 +4. 【非人类角色处理】 + - 如果角色是非人类(动物、神话生物、知名形象等),不受人类模板限制 + - 每条描述开头必须以角色名或物种名开始 + - 根据实际形态自由描述外观特征 +5. 描述内容: + - 性别、年龄段(不写具体数字) + - 面部:脸型、五官特征(如高挺鼻梁、深邃眼窝、薄唇等) + - 眼睛:形状、大小(禁止描写眼睛颜色) + - 头发:颜色、长度、发型、发质(如蓬松卷发、挑染银灰等) + - 体型:身高感、体态、肩宽、腰线等 + - 皮肤:只描述质感和独特标记(如光滑/粗糙、雀斑、疤痕、纹身等),禁止描述肤色 + - 服装:款式、材质、配色、细节(如机车皮夹克、金属拉链等) + - 鞋子:款式、颜色、材质(如黑色马丁靴、白色帆布鞋、棕色皮质牛津鞋、红底高跟鞋、绣花布鞋等) + - 配饰:耳钉、项链、手表、戒指等突出个性的配饰 + +4. 描述规范: + - 禁止写表情、姿态、动作 + - 禁止写背景/环境/道具 + - 不得加入情绪形容词与故事性句子 + - 【禁止身体颜色描述】禁止描写任何身体部位的颜色,AI会过度放大颜色描述导致效果失真: + ❌ 皮肤颜色(如偏黄、白皙、小麦色、古铜色、黝黑等) + ❌ 唇色(如红润、粉色、苍白等) + ❌ 眼睛颜色(如黑色、棕色、蓝色瞳孔、琥珀色等) + ❌ 脸色(如红润、苍白、蜡黄等) + ✅ 可以描写:皮肤质感(光滑/粗糙)、独特标记(雀斑/疤痕/纹身)、头发颜色、服装颜色 + - 如原文对外貌有描述,以原文为最优先(但颜色描述仍需过滤) + - 使用中文输出,长度 80-150 字 + - 不包含艺术风格、画风、光影效果描述(系统自动添加) + - 【年代一致性】根据故事背景判断年代,服装、发型、配饰必须符合该年代特征 + +【输出格式】只返回以下 JSON,不要任何其他内容 +{ + "descriptions": [ + "新描述1(80-150字)", + "新描述2(80-150字)", + "新描述3(80-150字)" + ] +} + +【小说原文】 +{novel_text} + + + + + + + diff --git a/lib/prompts/novel-promotion/episode_split.en.txt b/lib/prompts/novel-promotion/episode_split.en.txt new file mode 100644 index 0000000..3249430 --- /dev/null +++ b/lib/prompts/novel-promotion/episode_split.en.txt @@ -0,0 +1,40 @@ +You are a long-text episode splitter. +Analyze the full text and split it into balanced episodes. + +Input text: +{CONTENT} + +Core rules: +1. Keep episode lengths as balanced as possible. +2. Prefer natural breakpoints (chapter boundary, scene shift, time jump). +3. Keep chronology and full coverage (no overlap, no missing text). +4. Provide reliable startMarker and endMarker copied from source text. +5. Return JSON only. + +Output format: +{ + "analysis": { + "totalWords": 0, + "episodeCount": 0, + "targetWordsPerEpisode": 0, + "allowedRange": "0-0" + }, + "episodes": [ + { + "number": 1, + "title": "Episode title", + "summary": "Short summary", + "estimatedWords": 0, + "startMarker": "exact start marker", + "endMarker": "exact end marker", + "startIndex": 0, + "endIndex": 0 + } + ], + "validation": { + "maxWords": 0, + "minWords": 0, + "variance": 0, + "isBalanced": true + } +} diff --git a/lib/prompts/novel-promotion/episode_split.zh.txt b/lib/prompts/novel-promotion/episode_split.zh.txt new file mode 100644 index 0000000..c7a68ae --- /dev/null +++ b/lib/prompts/novel-promotion/episode_split.zh.txt @@ -0,0 +1,94 @@ +你是一个专业的内容分析助手。请分析以下文本,将其智能分割为多个剧集。 + +## ⚠️ 核心规则:字数必须均衡(最重要) + +**所有剧集的字数必须尽可能均衡!偏差不得超过 ±20%** + +### 📊 第一步:精确计算(必须执行) + +1. 统计总字数:count_total_characters(文本) +2. 计算目标集数:total ÷ 650 = N 集(四舍五入) +3. 计算每集目标字数:target = total ÷ N +4. 确定允许范围:[target × 0.8, target × 1.2] + +**示例**: +- 总字数 6500 字 → 10 集 → 每集目标 650 字 → 范围 520-780 字 +- 总字数 13000 字 → 20 集 → 每集目标 650 字 → 范围 520-780 字 + +### 📐 第二步:均衡分割(必须执行) + +❌ **绝对禁止**: +- 任何一集超过 target × 1.3(如目标650字,禁止超过845字) +- 任何一集少于 target × 0.6(如目标650字,禁止少于390字) +- 前几集很长、后几集很短(或反过来) + +✅ **必须保证**: +- 所有集的字数在目标值 ±20% 范围内 +- 最长集与最短集的差距不超过 300 字 +- 字数分布均匀,不能头重脚轻或头轻脚重 + +### 🎬 第三步:寻找分割点 + +1. **优先识别自然断点**: + - 章节标记:「第X集」「Chapter X」「Episode X」 + - 场景编号:`X-Y【场景】` 中的 X 变化(1-x → 2-x 是新集) + - 时间跳跃:「第二天」「三个月后」 + +2. **在自然断点附近微调**: + - 如果自然断点导致字数不均,可在附近段落边界调整 + - 优先在对话结束、场景转换处分割 + - 宁可牺牲一点叙事连贯性,也要保证字数均衡 + +## 输入文本 + +{{CONTENT}} + +## 📝 输出格式 + +```json +{ + "analysis": { + "totalWords": 6500, + "episodeCount": 10, + "targetWordsPerEpisode": 650, + "allowedRange": "520-780" + }, + "episodes": [ + { + "number": 1, + "title": "剧集标题(4-8字)", + "summary": "50字以内的剧情简介", + "estimatedWords": 650, + "startMarker": "该集开头的前20个字符(精确复制原文)", + "endMarker": "该集结尾的后20个字符(精确复制原文)" + } + ], + "validation": { + "maxWords": 720, + "minWords": 590, + "variance": 130, + "isBalanced": true + } +} +``` + +## ⚠️ 最终验证清单(输出前必须检查) + +在输出之前,你必须验证以下条件: + +1. ☐ episodes.length ≈ totalWords ÷ 650(误差 ±1 集) +2. ☐ 所有 estimatedWords 都在 allowedRange 范围内 +3. ☐ maxWords - minWords ≤ 300 字 +4. ☐ 没有任何一集超过 850 字 +5. ☐ 没有任何一集少于 400 字 +6. ☐ 上一集 endMarker 紧邻下一集 startMarker,无内容遗漏 +7. ☐ endMarker 不包含下一集的任何内容 + +**如果验证失败,必须重新调整分割点直到通过!** + +## 🔧 场景编号说明 + +- `X-Y【场景描述】` 格式中,X = 集数,Y = 场景序号 +- 1-1, 1-2, 1-3 都属于第 1 集 +- 2-1 开始第 2 集 +- 分集在 X 变化时进行 diff --git a/lib/prompts/novel-promotion/image_prompt_modify.en.txt b/lib/prompts/novel-promotion/image_prompt_modify.en.txt new file mode 100644 index 0000000..603c3f2 --- /dev/null +++ b/lib/prompts/novel-promotion/image_prompt_modify.en.txt @@ -0,0 +1,24 @@ +You are a prompt refinement expert for storyboard image/video generation. + +Current image prompt: +{prompt_input} + +Current video prompt: +{video_prompt_input} + +User instruction: +{user_input} + +Requirements: +1. Return an updated image prompt and video prompt. +2. Keep subject identity and scene continuity unless user asks to change. +3. Write in concise English. +4. Image prompt should focus on visual composition. +5. Video prompt should focus on motion/performance/camera behavior. + +Output format: +Return JSON only: +{ + "image_prompt": "updated image prompt", + "video_prompt": "updated video prompt" +} diff --git a/lib/prompts/novel-promotion/image_prompt_modify.zh.txt b/lib/prompts/novel-promotion/image_prompt_modify.zh.txt new file mode 100644 index 0000000..c1a73ab --- /dev/null +++ b/lib/prompts/novel-promotion/image_prompt_modify.zh.txt @@ -0,0 +1,38 @@ +请按照以下提示词规则执行用户的ai生图以及视频运动提示词场景需求 +1. 详细描述镜头角度和地点、动作、人物、环境,也就是谁、和谁、在哪里、干了什么,如果是空镜标题等,那么也要描述出原文想要表达的东西,例如作者名字,或者和原文有关的内容,提示词务必详细完整 + +2. **场景描述使用规则(重要):** + - locations字段:只写场景名字(如"病房_白天") + - image_prompt字段:直接使用场景名字(如"病房_白天"),可以结合具体位置描述(如"病房_白天的窗边") + - **禁止添加光线、色调、天气描述**:场景资产已包含完整的光线和色调信息,提示词中不要添加"阳光"、"光线"、"明亮"、"昏暗"、"温暖色调"、"冷色调"、"天气"等描述 + - **人物所在的位置和互动的地方要和场景提示词中已有的物品有关系,不要出现人物和场景没有的关系进行互动** + +3. **人物描述使用规则:** + - characters字段:只写人物名字(如"Victor") + - image_prompt字段:直接使用人物名字(如"Victor"),可以结合动作和情绪描述(如"Victor站在门口,表情严肃") + - 写图片生成提示词的时候务必要写明地点+人物名字+情绪+动作 + - 我们这个是有声书第一视角,第一视角默认就是主角的视角,也就是在pov中的人物决策,如无其他意外那么这个就是主角,按照这个名字来进行主视角生成 + +4. 我们的场景限制只对有效目前人物处在实际发生地点的有效!例如有回忆、旁白等情况,我们不应该完全限制在固定场景之内,而是可以插入多样性的画面 + +5. 使用中文输出提示词,提示词编写规范按照用简洁连贯的自然语言写明:主体 + 行为 + 环境 + 空间关系 + 例如:Dr.Smith站在病床边,看着躺在床上的Mary,病房_白天 + +6.涉及到文字内容的全部使用原文srt的语言作为输出,如原文srt是英文,就代表着我们这个面向英文观众,那么如果会画面内容里面出现文字的话(例如出现了医院名字的特写)就要输出英文的具体文字提示词(注意,主要提示词还是中文),如:医院的镜头特写,上面写着hospital + +7.使用主体+行为+环境的提示词,确保不要错过主体,除非无主体的空镜,否则一定要交代主体是谁 + +按照用户的修改指令把提示词修改为用户需要的样子,用户可能会发送一些额外内容让你修改,例如让你把人物修改为另外一个人物,这个时候你会看到人物或场景的具体描述词 +例如用户输入:把张三(黑色长发,穿着西装...)修改为小明(蓝色头发,红色眼睛...)然后就可以利用这些具体描述词来按照以上的生成提示词规则修改。 + +当前的图片提示词:{prompt_input} + +当前的视频提示词:{video_prompt_input} + +用户的修改指令:{user_input} + +结果发送json格式给我,只返回以下json格式,禁止返回一切除json以外的多余内容,注释,文字等等,只返回无任何markdown标识符的纯净json格式,json格式如下 +{ + "image_prompt": "修改后的图片提示词(静态画面描述)", + "video_prompt": "" +} diff --git a/lib/prompts/novel-promotion/location_create.en.txt b/lib/prompts/novel-promotion/location_create.en.txt new file mode 100644 index 0000000..a524575 --- /dev/null +++ b/lib/prompts/novel-promotion/location_create.en.txt @@ -0,0 +1,19 @@ +You are a professional environment prompt designer. +Generate one scene prompt for image generation. + +User request: +{user_input} + +Rules: +1. Output in English only. +2. Start with scene name in this format: "[Scene Name] ..." +3. Describe a wide, clear environment with spatial layout and key objects. +4. Mention lighting direction and atmosphere. +5. No protagonist actions or dialogue. +6. If crowd is implied by context, use generic crowd terms only (guests, pedestrians, audience). + +Output format: +Return JSON only: +{ + "prompt": "[Scene Name] environment description" +} diff --git a/lib/prompts/novel-promotion/location_create.zh.txt b/lib/prompts/novel-promotion/location_create.zh.txt new file mode 100644 index 0000000..1be3825 --- /dev/null +++ b/lib/prompts/novel-promotion/location_create.zh.txt @@ -0,0 +1,30 @@ +请按照以下提示词规则执行用户的生成场景需求 + +【场景生成要求(用于出图,中文描述)】 + +1. 生成1条中文环境描述(60-120字),像真实摄影场景一样描述 + +2. **开头必须明确写明场景名称**: + - 描述开头必须以"【场景名称】"的形式标注空间属性 + - 示例:「皇宫」殿内铺设着... / 「客厅」窗外阳光透过... / 「卧室」床边放着... + - 这样AI在生成图片时能明确理解这是什么类型的空间 + +3. 核心原则: + - 写真实存在的物体,不要写抽象感受 + - 材质要具体(深棕色实木地板、青灰色石砖墙、做旧铁艺栏杆) + - 物品要有使用痕迹和生活气息(桌上散落的书籍、墙角堆放的杂物、窗台晒干的植物) + - 光线要写清楚来源和效果(午后阳光斜照进来在地板上拉出长影、暖黄色壁灯打在墙面上) + +4. 禁止:不写主角人物具体动作、不写画风、不写"温馨""优雅"等抽象词 + +5. 【人群处理规则】 + - 如果用户描述的场景暗示有人群(如宴会厅、集市、教室上课等),在描述中加入模糊人群元素 + - 人群描述示例:"大厅中宾客三两成群"、"街道上行人往来"、"座位上零散坐着几位观众" + - 如果是私密空间或用户明确要求空镜,则不添加人群 + +以下是用户的生成指令:{user_input} + +只返回以下json格式,禁止返回一切除json以外的多余内容 +{ + "prompt":"「场景名称」场景描述内容" +} diff --git a/lib/prompts/novel-promotion/location_description_update.en.txt b/lib/prompts/novel-promotion/location_description_update.en.txt new file mode 100644 index 0000000..7fda437 --- /dev/null +++ b/lib/prompts/novel-promotion/location_description_update.en.txt @@ -0,0 +1,26 @@ +You are a scene description editor. +Update the original location description based on user instruction. + +Location name: +{location_name} + +Original description: +{original_description} + +User instruction: +{modify_instruction} + +Reference image context (may be empty): +{image_context} + +Rules: +1. Keep unchanged scene elements unless explicitly modified. +2. Return one complete updated description in English. +3. Keep scene name at the beginning: "[{location_name}] ..." +4. No protagonist actions or story narration. + +Output format: +Return JSON only: +{ + "prompt": "updated location description" +} diff --git a/lib/prompts/novel-promotion/location_description_update.zh.txt b/lib/prompts/novel-promotion/location_description_update.zh.txt new file mode 100644 index 0000000..3a75dbb --- /dev/null +++ b/lib/prompts/novel-promotion/location_description_update.zh.txt @@ -0,0 +1,36 @@ +你是一个专业的场景描述更新专家。 + +【任务】 +根据用户对场景图片的修改,更新场景的描述词。 + +【场景名称】 +{location_name} + +【原始场景描述】 +{original_description} + +【用户修改指令】 +{modify_instruction} + +{image_context} + +【更新规则】 +1. **开头必须明确写明场景名称**: + - 描述开头必须以「{location_name}」的形式标注空间属性 + - 示例:「皇宫」殿内铺设着... / 「客厅」窗外阳光透过... + - 这样AI在生成图片时能明确理解这是什么类型的空间 + +2. 仔细理解用户的修改指令,找出需要修改的具体特征 +3. 如果有参考图片,请识别参考图片中的关键视觉特征(如建筑风格、装饰元素、光线氛围、色调等) +4. 将修改内容准确融入原始描述中,替换或补充相关部分 +5. 保持描述的流畅性和一致性 +6. 保留未被修改的原有特征 +7. 遵循以下描述规范: + - 只描述场景本身,禁止描述人物 + - 使用中文输出,长度 50-100 字 + +【输出格式】 +只返回JSON格式,禁止返回任何其他内容: +{ + "prompt": "「场景名」更新后的完整场景描述" +} diff --git a/lib/prompts/novel-promotion/location_modify.en.txt b/lib/prompts/novel-promotion/location_modify.en.txt new file mode 100644 index 0000000..f03e2c7 --- /dev/null +++ b/lib/prompts/novel-promotion/location_modify.en.txt @@ -0,0 +1,24 @@ +You are a professional scene prompt modifier. +Modify an existing scene description while preserving scene identity. + +Location name: +{location_name} + +Current description: +{location_input} + +User instruction: +{user_input} + +Rules: +1. Keep core scene identity and function. +2. Apply requested changes with concrete visual details. +3. Output in English only. +4. Start with scene name: "[{location_name}] ..." +5. No protagonist actions, dialogue, or narrative plot. + +Output format: +Return JSON only: +{ + "prompt": "modified location description" +} diff --git a/lib/prompts/novel-promotion/location_modify.zh.txt b/lib/prompts/novel-promotion/location_modify.zh.txt new file mode 100644 index 0000000..63920f4 --- /dev/null +++ b/lib/prompts/novel-promotion/location_modify.zh.txt @@ -0,0 +1,64 @@ +请按照以下提示词规则执行用户的修改场景需求 + +【场景名称】 +{location_name} + +【场景生成要求(用于出图,中文描述)】 + +1. **开头必须明确写明场景名称**: + - 描述开头必须以「{location_name}」的形式标注空间属性 + - 示例:「皇宫」殿内铺设着... / 「客厅」窗外阳光透过... + - 这样AI在生成图片时能明确理解这是什么类型的空间 + +2. 每个场景生成 1 条中文环境描述,用于AI图片生成 + +3. 必须包含具体的视觉元素,按以下结构描述: + + **空间定位**:明确场景类型 + - 室内:客厅/卧室/厨房/办公室/医院病房/教室等 + - 室外:街道/巷子/公园/山谷/海边/广场等 + + **主要结构**:描述可见的建筑元素 + - 墙面/地面/天花板的材质和颜色(如"白色墙面"/"木质地板"/"水泥地面"/"瓷砖地面") + - 门窗位置和类型(如"左侧有大窗户"/"右侧木门"/"落地窗"/"玻璃门") + - 建筑特征(如"高天花板"/"拱形门"/"砖墙"/"玻璃幕墙") + + **家具/道具**:列出场景中的主要物体 + - 家具摆放(如"中央有沙发"/"墙边书架"/"床靠窗"/"办公桌") + - 特征道具(如"桌上有台灯"/"墙上挂画"/"地上地毯"/"书架上有书") + - 植物装饰(如"窗台有盆栽"/"角落有绿植") + + **光线环境**:描述光源和照明效果 + - 自然光:时间段+光线特征 + * 白天:窗外阳光透过窗帘/明亮日光从窗户照入/柔和晨光 + * 夜晚:月光从窗外照入/窗外夜色/窗外灯光 + - 人工光:光源类型+位置 + * 天花板吊灯照明/墙壁壁灯柔和光线/台灯局部照明/落地灯/射灯 + + **氛围细节**:补充环境特征 + - 整体色调:暖色调/冷色调/中性色/明亮/昏暗 + - 空间感:宽敞/狭窄/纵深感强/开阔/封闭 + - 状态特征:整洁/凌乱/陈旧/现代/简约/复古 + +4. 描述规范: + - 使用具体名词,避免抽象形容词 + * ✅ 好:"木质书桌"/"灰色布艺沙发"/"白色窗帘" + * ❌ 差:"优雅的家具"/"舒适的环境"/"温馨的氛围" + - 描述固定元素,不写主角人物具体动作、情绪 + - 【人群处理规则】如果用户要求添加人群,或场景本身暗示有人群(如宴会、集市等): + * 可以加入模糊人群描述:\"人群\"、\"宾客\"、\"路人\"等 + * 示例:\"大厅远处三两宾客交谈\"、\"街角有行人匆匆走过\" + - 长度控制在 60-100 字 + - 不包含艺术风格描述(如"美式漫画风"/"水彩风"),风格由系统自动添加 + - 不包含光影效果描述(如"dramatic lighting"/"柔和渐变"),由风格控制 + +你的目标是根据用户的修改指令,在原有场景描述的基础上进行修改 + +当前场景描述:{location_input} + +用户的修改指令:{user_input} + +发送json格式给我,只返回以下json格式,禁止返回一切除json以外的多余内容,注释,文字等等,只返回无任何markdown标识符的纯净json格式,json格式如下 +{ + "prompt":"「场景名」xxxxx" +} diff --git a/lib/prompts/novel-promotion/location_regenerate.en.txt b/lib/prompts/novel-promotion/location_regenerate.en.txt new file mode 100644 index 0000000..1264115 --- /dev/null +++ b/lib/prompts/novel-promotion/location_regenerate.en.txt @@ -0,0 +1,25 @@ +You are a scene variant regenerator. +Generate 3 new scene description variants for the same location. + +Location name: +{location_name} + +Current descriptions (reference): +{current_descriptions} + +Requirements: +1. Generate 3 clearly different but same-location variants. +2. Keep the scene name prefix in each line: "[{location_name}] ..." +3. Output in English only. +4. Keep environment-only description (no protagonist actions). +5. Keep each variant concise and image-generation friendly. + +Output format: +Return JSON only: +{ + "descriptions": [ + "[{location_name}] variant 1", + "[{location_name}] variant 2", + "[{location_name}] variant 3" + ] +} diff --git a/lib/prompts/novel-promotion/location_regenerate.zh.txt b/lib/prompts/novel-promotion/location_regenerate.zh.txt new file mode 100644 index 0000000..5951236 --- /dev/null +++ b/lib/prompts/novel-promotion/location_regenerate.zh.txt @@ -0,0 +1,41 @@ +你是"场景重塑师"。请根据当前的场景描述,为指定场景重新生成 3 条全新的场景描述变体。 + +【场景信息】 +- 场景名:{location_name} +- 当前描述(作为参考,需要生成不同的变体): +{current_descriptions} + +【生成要求】 +1. **开头必须明确写明场景名称**: + - 每条描述开头必须以「{location_name}」的形式标注空间属性 + - 示例:「皇宫」殿内铺设着... / 「客厅」窗外阳光透过... + - 这样AI在生成图片时能明确理解这是什么类型的空间 + +2. 根据当前描述的核心元素,生成 3 条各有特色、互不相同的场景描述变体 +3. 必须与当前描述有明显差异(换一种氛围/布局/细节),但保持场景核心特征 +4. 描述内容应包含: + - 空间结构:整体布局、空间大小、层次感 + - 建筑/地形:建筑风格、地形特征、主要构造物 + - 光影氛围:光源类型、明暗对比、色调倾向 + - 材质细节:地面、墙面、物体的材质质感 + - 环境元素:植物、天气、装饰物等 + - 独特标识:该场景的标志性元素或特殊物件 + +5. 描述规范: + - 禁止写主角人物具体动作、剧情 + - 【人群处理规则】如果当前描述中包含人群元素,新描述也应保持人群元素 + * 可以调整人群的位置、密度、状态,但保持有人群存在 + * 人群描述使用模糊词汇:"人群"、"宾客"、"路人"等 + - 使用中文输出,长度 80-150 字 + - 不包含艺术风格、画风描述(系统自动添加) + - 【年代一致性】根据场景特征判断年代,建筑、装饰、物品必须符合该年代特征 + - 【时间一致性】如场景名包含"白天/黑夜/黄昏"等,描述中的光影必须匹配 + +【输出格式】只返回以下 JSON,不要任何其他内容 +{ + "descriptions": [ + "「场景名」新描述1(80-150字)", + "「场景名」新描述2(80-150字)", + "「场景名」新描述3(80-150字)" + ] +} diff --git a/lib/prompts/novel-promotion/screenplay_conversion.en.txt b/lib/prompts/novel-promotion/screenplay_conversion.en.txt new file mode 100644 index 0000000..c192379 --- /dev/null +++ b/lib/prompts/novel-promotion/screenplay_conversion.en.txt @@ -0,0 +1,59 @@ +You are a screenplay conversion specialist. +Convert the clip text into structured screenplay JSON without adding new story facts. + +Clip ID: +{clip_id} + +Clip content: +{clip_content} + +Location library: +{locations_lib_name} + +Character library: +{characters_lib_name} + +Character introductions: +{characters_introduction} + +Output format (JSON object only): +{ + "clip_id": "{clip_id}", + "original_text": "original clip text", + "scenes": [ + { + "scene_number": 1, + "heading": { + "int_ext": "INT or EXT", + "location": "location name", + "time": "morning/day/evening/night" + }, + "description": "scene setup", + "characters": ["Character A", "Character B"], + "content": [ + { + "type": "action", + "text": "action description" + }, + { + "type": "dialogue", + "character": "Character A", + "parenthetical": "optional performance cue", + "lines": "spoken line" + }, + { + "type": "voiceover", + "character": "Narrator or Character", + "text": "voiceover content" + } + ] + } + ] +} + +Rules: +1. Preserve original story facts; do not invent new events. +2. Keep scene/content order aligned with source text. +3. Resolve character aliases to canonical names when possible. +4. Use the best matching location name from library; if none fits, use source location text. +5. Return strict JSON only. diff --git a/lib/prompts/novel-promotion/screenplay_conversion.zh.txt b/lib/prompts/novel-promotion/screenplay_conversion.zh.txt new file mode 100644 index 0000000..f0fdc78 --- /dev/null +++ b/lib/prompts/novel-promotion/screenplay_conversion.zh.txt @@ -0,0 +1,247 @@ +你是专业的编剧和剧本改编师。你的任务是将小说/文学文本转换为标准的影视剧本格式。 + +⚠️⚠️⚠️【最高优先级原则 - 100%忠实原文】⚠️⚠️⚠️ + +你的工作是**格式转换**,不是**创作**!你必须100%忠实于原文,严禁任何形式的"创造性发挥": + +🚫 绝对禁止: +- 添加原文中没有的对话、动作、场景描述 +- 扩展或改编原文内容(即使你觉得"更合理"或"更生动") +- 添加原文没有提及的角色反应、表情、心理活动 +- 脑补原文没有描写的环境细节、道具、氛围 +- 添加过渡性描述来"填充空白" +- 用你的理解"补全"原文的留白 + +✅ 你只能做: +- 将原文已有的内容转换为剧本格式 +- 识别原文明确描述的场景、角色、对话 +- 提取原文已有的动作和环境描述 + +如果原文内容简短或留白,你的剧本也应该简短,不要试图"丰富"它! + +【核心职责】 + +1. 把小说文字转换为剧本格式(场景、对话、动作描述) +2. 提取场景的时间、地点、环境信息 +3. 识别场景中出现的角色(匹配资产库) +4. 区分动作、对话、画外音等不同类型 +5. ⭐100%保持原文信息的完整性,不增不减 + +【剧本格式规范】 + +标准剧本包含以下元素: + +1. **场景头(Scene Heading)**: + - 格式: 内景/外景 + 地点 + 时间 + - 示例: "内景 客厅 清晨" / "外景 取经路 白天" + +2. **场景描述(Scene Description)**: + - 简洁描述场景环境、布局、关键道具 + - 不需要过度细节,只需要建立基本视觉印象 + +3. **动作描述(Action)**: + - 描述角色的动作、表情、行为 + - 连续的段落形式,不要拆分成碎片 + +4. **对话(Dialogue)**: + - 角色名字 + - 副文本(parenthetical): 括号内的表演指导 + - 台词内容 + +5. **画外音(Voiceover)**: + - 旁白、独白、回忆、读信等不在画面中的声音 + - 标记角色(如果是特定角色的独白) + +【转换规则】 + +## 1. 场景识别规则 + +- 分析原文,识别场景边界(地点变化、时间跨越) +- 每个场景必须包含: + * 内景(INT)或外景(EXT) + * 地点名称: + - 优先从场景资产库选择完全一致的名称 + - ⚠️【重要】如果资产库中没有匹配的场景,直接使用原文中的场景名称,不要强行匹配错误的资产库场景 + - 宁可输出资产库中不存在的场景名,也不要用错误的场景名(后续会自动创建缺失的资产) + * 时间段(清晨/上午/正午/下午/黄昏/夜晚/深夜) + +## 2. 内容类型识别 + +必须准确区分以下类型: + +**action** - 动作描述 +- 角色的动作、表情、行为 +- 场景变化、环境细节 +- 示例: "孙悟空举起金箍棒,朝六耳猕猴砸去。" + +**dialogue** - 对话 +- 画面中角色的说话 +- 必须包含:character(角色名)、lines(台词) +- 可选包含:parenthetical(副文本,如"愤怒地""小声") +- 示例: + 角色: 孙悟空 + 副文本: 愤怒地 + 台词: "一个冒牌货,也敢拦你孙爷爷的路!" + +**voiceover** - 画外音 +- 旁白、独白、回忆中的声音、心理活动 +- 不在画面中出现的声音 +- 示例: "原来孙悟空真的死在了取经路上。" (旁白) +- 示例: 二郎神独白:"猴子死了,我却没有出手..." (特定角色的画外音) + +## 3. 角色识别规则 + +- 优先从角色资产库中选择完全一致的名字 +- ⚠️【重要】如果资产库中没有匹配的角色,直接使用原文中的角色名称,不要强行匹配错误的资产库角色 +- 宁可输出资产库中不存在的角色名,也不要用错误的角色名(后续会自动创建缺失的资产) +- 不要使用简称:用"六耳猕猴"而非"六耳" +- 不要使用代词:用具体名字替代"他""她" +- characters数组只包含画面中出现的角色,不包括画外音角色 + +## 4. 副文本(Parenthetical)提取规则 + +从原文中识别并提取表演指导: +- "XX愤怒地说" → parenthetical: "愤怒地" +- "XX小声嘀咕" → parenthetical: "小声" +- "XX(转身)说" → parenthetical: "转身" +- "XX边走边说" → parenthetical: "边走边" + +## 5. 动作连续性规则 + +- 动作描述应该是连续的段落,不要过度拆分 +- 多个连续动作可以合并在一个action中 +- 示例: ❌ 不好: "孙悟空站起来。" + "孙悟空走向门口。" + "孙悟空打开门。" +- 示例: ✅ 好: "孙悟空站起来,走向门口,打开门。" + +【输出格式】 + +只返回JSON对象,不得有markdown代码块标记、注释或解释。 + +{ + "clip_id": "{clip_id}", + "original_text": "原文内容", + + "scenes": [ + { + "scene_number": 1, + "heading": { + "int_ext": "INT或EXT", + "location": "场景名称(必须从资产库选择)", + "time": "时间段" + }, + "description": "场景环境描述", + "characters": ["角色1", "角色2"], + + "content": [ + { + "type": "action", + "text": "动作描述文本" + }, + { + "type": "dialogue", + "character": "角色名", + "parenthetical": "副文本", + "lines": "台词内容" + }, + { + "type": "voiceover", + "character": "角色名或旁白", + "text": "画外音内容" + } + ] + } + ] +} + +【场景描述撰写规则】 + +description字段应该简洁但信息丰富,包含: +1. 环境类型(室内/室外/特殊环境) +2. 主要布局(如"狭长的走廊""开阔的广场") +3. 关键道具或环境元素(如"右侧落地窗""远处山脉") +4. 氛围提示(如"荒凉""温馨""阴森") + +示例: +- "简约客厅。米白色墙面,木质地板。右侧落地窗,左侧入口门。" +- "荒凉的取经路。黄沙漫天,远处是连绵的山脉。" +- "阴暗的洞穴。石壁潮湿,只有微弱的火光。" + +【特殊情况处理】 + +1. **第一人称叙述**: + - 如果原文是"我走进房间",需要替换为具体角色名 + - ⭐ 参考【角色介绍】中的说明,找到"我"对应的角色 + - 如果角色介绍中说明"我"对应某角色,则使用该角色名 + - 如果资产库有"我"这个角色,则使用"我" + +2. **称呼映射**: + - ⭐ 参考【角色介绍】中的称呼说明 + - 如"老公"在介绍中说明对应"林墨",则dialogue的character填"林墨" + - 不要被原文的称呼误导,以资产库的名字为准 + +3. **回忆/闪回场景**: + - 回忆中的对话/动作 → 正常处理(因为画面中会演) + - 回忆的旁白叙述 → 使用voiceover类型 + +4. **多个小场景**: + - 如果原文包含多个地点变化,拆分成多个scene + - 每个scene有独立的scene_number + +5. **心理活动**: + - 角色的内心想法 → voiceover类型 + - 标记character为对应角色 + +【严格要求 - 必须遵守】 + +⭐⭐⭐ 忠实原文的强制要求 ⭐⭐⭐ + +1. 🚨【最重要】禁止编造!所有动作、对话、描述必须来自原文,不能添加任何原文中没有的内容 +2. 🚨 如果原文只有一句话,剧本也只能有一句话对应的内容,禁止"扩写" +3. 🚨 如果原文没有描述角色的表情/动作,禁止添加"XX露出微笑"、"XX点了点头"等内容 +4. 🚨 如果原文没有环境描写,description字段只写能从原文推断的最基本信息 +5. 🚨 禁止添加过渡性动作,如"XX走了过来"、"XX转身离开"(除非原文明确写了) + +格式要求: + +1. 只返回JSON对象,不得有markdown标记或注释 +2. location优先从场景资产库选择;如果资产库没有匹配的,使用原文场景名称(宁可缺失也不用错误的) +3. characters优先从角色资产库选择;如果资产库没有匹配的,使用原文角色名称(宁可缺失也不用错误的) +4. ⭐ 根据角色介绍中的称呼映射,将原文中的"我"、"老公"等替换为正确的角色名 +5. ❌ 严禁添加原文中没有的内容(这是最常见的错误!) +6. content数组保持时间顺序 +7. type只能是: action, dialogue, voiceover +8. dialogue必须包含character和lines +9. voiceover如果是特定角色必须包含character +10. parenthetical是可选的,只在原文有明确表演指导时添加 +11. 输出必须是**严格合法的JSON**:字符串中不能出现原始换行/回车/制表符,必须使用转义字符(\\n、\\r、\\t) +12. 字符串内的双引号必须转义为 \" +13. clip_id 必须与输入的 Clip ID 完全一致,严禁输出 "{clip_id}" 这种占位符 +14. 建议输出为单行JSON对象(不包含多余空行/解释) + +⚠️ 自检清单(输出前必须确认): +- [ ] 我的每一句动作描述都能在原文中找到对应吗? +- [ ] 我的每一句对话都是原文的原话吗? +- [ ] 我有没有添加原文没有的"走过来"、"点头"、"微笑"等动作? +- [ ] 我有没有"丰富"原文简短的描写? + +【输入数据】 + +Clip原文: +{clip_content} + +场景资产库(优先匹配,无匹配时可使用原文名称): +{locations_lib_name} + +角色资产库(优先匹配,无匹配时可使用原文名称): +{characters_lib_name} + +角色介绍(⭐重要:用于理解"我"和称呼对应的角色以及角色关系): +{characters_introduction} + +Clip ID: +{clip_id} + +【输出要求】 + +请将上述原文转换为标准剧本格式,只返回JSON对象。 +再次强调:输出必须是**严格合法的JSON**,不得包含任何额外文本。 diff --git a/lib/prompts/novel-promotion/select_location.en.txt b/lib/prompts/novel-promotion/select_location.en.txt new file mode 100644 index 0000000..3934c86 --- /dev/null +++ b/lib/prompts/novel-promotion/select_location.en.txt @@ -0,0 +1,43 @@ +You are a location asset extraction specialist. +Extract locations that need dedicated background assets. + +Input text: +{input} + +Existing location library: +{locations_lib_name} + +Selection rules: +1. Include locations where meaningful story actions happen. +2. Exclude abstract/metaphorical spaces and one-off passing mentions. +3. Deduplicate aliases of the same location. +4. Prefer exact library names when a location already exists. + +For each selected location, generate 3 wide-angle environment descriptions. +Each description should: +- start with location name in brackets: "[Location Name] ..." +- describe spatial layout, depth layers, major objects, and lighting direction +- remain environment-only (no named protagonist actions) +- use concise, production-ready English + +Output format (JSON only): +{ + "locations": [ + { + "name": "location_name", + "summary": "short usage summary", + "has_crowd": false, + "crowd_description": "", + "descriptions": [ + "[location_name] description 1", + "[location_name] description 2", + "[location_name] description 3" + ] + } + ] +} + +Strict constraints: +1. JSON only. +2. If no valid location exists, return: {"locations":[]}. + \ No newline at end of file diff --git a/lib/prompts/novel-promotion/select_location.zh.txt b/lib/prompts/novel-promotion/select_location.zh.txt new file mode 100644 index 0000000..b9175b6 --- /dev/null +++ b/lib/prompts/novel-promotion/select_location.zh.txt @@ -0,0 +1,135 @@ +你是"场景资产建立师"。请基于我提供的文本(可能是小说、剧本、或混合格式),筛选【需要制作画面的场景】,生成用于出图与后续生产的资产 JSON。 + +【筛选规则 - 精准提取模式】 + +✅【必须提取的场景】: + - 剧本场景头部中出现的地点(如"内景 客厅 白天") + - 角色实际身处、产生互动的具体场所 + - 剧情主线发生的核心地点 + - 多次出现或戏份较重的场景 + - 有明确空间描写、需要制作背景画面的地点 + +❌【不提取的场景】(严格执行!): + - 一次性路过、仅提及但无剧情发生的地点 + - 意境类、比喻类、修辞类描述(如"从天堂打到地狱"、"从天上打到地下"、"心灵深处"、"记忆长河"等) + - 抽象空间或无法具象化的概念(如"命运交汇点"、"时空裂缝") + - 仅作为对话背景提及、没有实际画面需求的地点 + - 纯过渡性场景(如"穿过走廊"、"路过门口"等一笔带过的移动描述) + - 回忆/幻想中一闪而过、没有具体剧情的场景 + - 战斗过程中一笔带过的地点(如"打遍三界"、"从山上打到山下"、"从天宫打到凡间"等表示战斗范围的修辞) + +📋【判断标准】: + 问自己:这个场景是否需要单独制作一张背景图?角色是否在此场景有实际戏份? + 如果只是一句话带过的地点,则不提取。 + 如果是表示"打斗范围"的修辞(如从天堂到地狱),则不提取。 + +🔄【去重规则】: + - 若场景在库中已存在则跳过,场景库如下:{locations_lib_name} + - 同一场景不同称呼合并为一个(如"书房"和"张先生的书房"视为同一场景) + - 返回的场景名必须与资产库中已有名称完全一致 + +【场景生成要求 - A: 全景空间版】 +侧重点:宽广完整的空间全貌、整体布局、画面层次 + +⚠️ 【核心要求】必须生成【宽广的空间全景】,展示场景的完整面貌,而非局部特写! +- 镜头应该是【广角/远景】视角,能看到整个空间的全貌 +- 展示空间的完整边界(墙壁、地面、天花板/天空) +- 让观众能够清晰理解这是一个什么样的完整空间 +- 严格按照原文的场景描述来描写,原文描述的场景是最优先级,其他才可以自由发挥 + +1. **开头必须明确写明场景名称**: + - 每条描述开头必须以「场景名」的形式标注空间属性 + - 示例:「皇宫」殿内铺设着... / 「客厅」窗外阳光透过... / 「卧室」床边放着... + - 这样AI在生成图片时能明确理解这是什么类型的空间 + +2. 每个场景生成 3 条中文环境描述(用于AI图片生成),供用户选择 + +3. 3条描述要求: + - 全部符合原文描述的场景特征 + - 可以自由发挥细节,但整体风格保持一致,不要有过大差异 + - 全部使用广角/远景视角,展示完整空间全貌 + - 每条描述开头都必须以「场景名」标注 + +4. 每条描述都必须包含: + + **宽广空间感**(最重要): + - 必须是【广角镜头】或【远景视角】,能看到空间的大部分区域 + - 室内场景:能看到2-3面墙壁、地板、部分天花板 + - 室外场景:能看到开阔的视野、远处的地平线或建筑群 + - 强调空间的【开阔感】和【完整性】 + + **空间定位与规模**: + - 场景类型(室内/室外/幻想空间) + - 空间大小感:描述实际的空间尺度(如"约30平米的客厅"/"一眼望不到边的草原") + - 层高/纵深感:能看到的最远距离 + + **空间层次**(创造画面深度): + - 前景:靠近镜头的元素(桌角/门框边缘/植物叶片/栏杆等,部分可见) + - 中景:主要场景区域(核心物体的完整呈现) + - 背景:远处可见的元素(窗外景色/远处墙面/天际线/门廊深处) + + **物体布局**: + - 使用明确的位置词:左侧/右侧/中央/角落/靠窗/远处 + - 描述物体之间的空间关系和前后层次 + - 5-8件物体,每件都有位置说明 + + **光线方向**:光从哪个方向照入,照亮哪些区域 + +5. 描述规范: + - 强调位置关系词:前方、远处、左侧、角落、靠近、深处 + - 长度 100-150 字 + + ⚠️【场景图不能出现任何角色 - 核心规则】: + + 场景图的用途:场景图是纯粹的"背景板",主角和重要角色会在后期通过 AI 合成到背景上。 + 因此,场景描述中**绝对不能出现任何有名有姓的角色**。 + + ❌ 错误示例(包含了角色): + - "两只猴王持棒对峙" → 错!猴王是角色,不能出现 + - "张三站在门口迎接" → 错!张三是角色,不能出现 + - "孙悟空和六耳猕猴在街上打斗" → 错!主角不能出现 + + ✅ 正确示例(纯背景): + - "「古道」广角镜头展现蜿蜒在险峻石林间的黄土古道,前景几株枯松,中景道路宽阔平坦,尘土飞扬,背景是连绵群山。" + - "「宴会厅」大厅远处三两宾客交谈" → 可以!这是无名背景群众 + - "「集市」街道上行人往来" → 可以!这是模糊的路人群众 + + 📋 什么情况可以写人群? + - 只有无名的、模糊的背景群众可以出现(如"宾客"、"路人"、"行人"、"围观群众") + - 这些群众不能有具体描述,只能用模糊词汇 + - 如果场景是私密空间或无人场景,保持空镜即可 + + - 不包含艺术风格描述,风格由系统自动添加 + +6. 场景命名规则:中文 "地点_时间/状态" + - 示例:"客厅_白天"/"空间站_夜间"/"仙宫_黄昏"/"森林_迷雾中" + +7. 剧情中出现的关键元素必须在场景中体现(如椅子、桌子等) + +8.如无特殊要求,使用用户输入的语言来进行场景生成,例如输入英文输出偏西方场景,中文则输出偏中国场景,但是原则要按照文字剧本里实际发生的地点为准, + +【输出规范(只允许以下 JSON 结构;字段名中文;不得输出任何多余文字)】 +{ + "locations": [ + { + "name": "场景_时间", + "summary": "场景简要说明(用途/人物关联,如:张三居住的主卧室、公司高层会议室等)", + "has_crowd": true/false, + "crowd_description": "人群类型描述(仅当has_crowd为true时填写,如:宴会宾客、集市人群、学生们等)", + "descriptions": [ + "「场景名」场景环境描述1(如has_crowd为true则包含人群元素)", + "「场景名」场景环境描述2", + "「场景名」场景环境描述3" + ] + } + ] +} + +【严格性】 +- 若无符合条件的场景,locations数组返回 []。 +- 只返回上述 JSON;不得输出markdown代码块标记、如```json注释或解释;不得添加未定义字段。 +- 每条描述必须遵守长度限制(100-150字);发现超长请自行截断。 +- 禁止在字符串里出现未转义的直引号 "。如需表示英寸或引号优先用数值字段(推荐),若必须用直引号,必须转义为 \ + +【原文内容如下】 +{input} diff --git a/lib/prompts/novel-promotion/single_panel_image.en.txt b/lib/prompts/novel-promotion/single_panel_image.en.txt new file mode 100644 index 0000000..d753336 --- /dev/null +++ b/lib/prompts/novel-promotion/single_panel_image.en.txt @@ -0,0 +1,27 @@ +You are a professional storyboard image artist. +Generate exactly one high-quality image for one panel. + +Absolute constraints: +1. No text in the image. +2. No subtitles, labels, numbers, watermarks, or symbols. +3. Do not create collage or multi-frame output. +4. Output exactly one frame. + +Aspect ratio (must be exact): +{aspect_ratio} + +Storyboard panel data: +{storyboard_text_json_input} + +Source text: +{source_text} + +Style requirement: +{style} + +Execution rules: +1. Respect panel composition, character placement, and action logic. +2. Use reference images for style/identity consistency only. +3. Repaint the background according to shot type and angle. +4. If storyboard conflicts with source text, keep narrative logic from source text. +5. Keep final visual style consistent with provided references. diff --git a/lib/prompts/novel-promotion/single_panel_image.zh.txt b/lib/prompts/novel-promotion/single_panel_image.zh.txt new file mode 100644 index 0000000..390cf3e --- /dev/null +++ b/lib/prompts/novel-promotion/single_panel_image.zh.txt @@ -0,0 +1,65 @@ +你是一位专业的分镜画师。请根据以下分镜数据生成单张高质量的镜头图片。 + +【绝对禁止 - 图像中不得出现任何文字 - 最高优先级】 +生成的图像中绝对禁止出现任何文字: +- 禁止出现镜头类型标签(特写、中景、全景等) +- 禁止出现镜头运动文字(推、拉、摇等) +- 禁止出现数字或画面编号(1、2、3等) +- 禁止出现中文或英文文字 +- 禁止出现水印、注释或符号 +- 参考图上的文字标签仅供你识别使用,禁止画入图中 +- 所有输入信息都是给你的指令,不是要画进图像的内容 +纯视觉内容!纯视觉内容!纯视觉内容! +- 每个图片只能有一张镜头,禁止一张图片多张镜头,禁止拼图,禁止生成多张图,禁止生成一张图片里面三张图 + +【⚠️ 画面比例 - 必须严格遵守】 +本次生成的画面比例为:{aspect_ratio} +- 必须严格按照此比例生成画面 +- 禁止输出与指定比例不符的图像 +- 禁止因参考图比例影响输出比例 +- 每张输出的图片里面只能有一张图,禁止一张图片多张图! + +【参考图使用规则】 +- 角色参考:用于参考角色外貌、服装、面部特征、体型 +- 场景参考:仅用于参考环境的构图布局风格和氛围,需根据画面重新绘制,不要直接贴在背景上使用 +- 画面的背景必须根据镜头角度和景别重新绘制 +- 特写/细节镜头应使用虚化或局部背景 +- 参考图上方的文字标签标注了角色/场景名称,请与分镜要求对应 + +【⚠️ 摄影规则 - 关键】 +如果分镜数据中包含 photography_rules,必须严格遵守: +- **光照方向**:按照 lighting.direction 的描述绘制光源方向 +- **角色位置**:按照 characters 数组中的 screen_position 确定角色在画面中的位置(左/右/中央) +- **角色姿势**:按照 characters 数组中的 posture 确定角色姿态(站立/坐着等) +- **景深**:按照 depth_of_field 的描述控制前后景虚实 +- **色调**:按照 color_tone 的描述确定整体色彩氛围 + +【分镜内容要求】 +- 根据分镜数据设计画面的视觉内容 +- 确保镜头方向一致(不跳轴),角色位置正确 +- 严格按照文字分镜要求绘制镜头 + +【⚠️ 原文优先原则 - 重要】 +分镜描述可能存在空间/位置错误。务必与原文交叉验证: +- 当分镜与原文冲突时:按原文的空间关系、角色位置、动作顺序 +- 参考分镜的:镜头类型、构图、摄影角度 +- 参考原文的:剧情逻辑、空间关系、角色互动、动作顺序 +- 智能结合两者生成最准确的视觉效果 + +【绝对规则 - 严格遵守】 +- 严格按照分镜要求绘制画面 +- 禁止添加、删除或重排任何镜头 +- 镜头必须与输入完全匹配 + +【分镜数据】 +{storyboard_text_json_input} + +【镜头原文】 +{source_text} + +【⚠️ 风格要求 - 必须严格遵守】 +画面风格:{style} +- 必须严格遵循上传的角色和场景参考图的美术风格 +- 角色绘制风格、线条、色彩必须与角色参考图匹配 +- 环境风格、氛围、色调必须与场景参考图匹配 +- 禁止出现与参考图风格不一致的情况! diff --git a/lib/prompts/novel-promotion/storyboard_edit.en.txt b/lib/prompts/novel-promotion/storyboard_edit.en.txt new file mode 100644 index 0000000..4e34e7e --- /dev/null +++ b/lib/prompts/novel-promotion/storyboard_edit.en.txt @@ -0,0 +1,12 @@ +You are an expert storyboard image editor. +Edit a single panel image or a panel set according to user instruction. + +Rules: +1. Do not add any text overlay, subtitle, or technical labels. +2. If user uploaded reference images, use them as primary visual guidance. +3. Keep identity and scene continuity unless user requests a change. + +User instruction: +{user_input} + +Return only the edited image result. diff --git a/lib/prompts/novel-promotion/storyboard_edit.zh.txt b/lib/prompts/novel-promotion/storyboard_edit.zh.txt new file mode 100644 index 0000000..cda53d2 --- /dev/null +++ b/lib/prompts/novel-promotion/storyboard_edit.zh.txt @@ -0,0 +1,12 @@ +你是一个修改编辑图片的大师,你需要根据用户的指令来编辑单个镜头或整组分镜图片,编辑时需要遵守以下规则: +1:不要添加任何多余标识符文字,如字幕,景别信息等 +2:如果用户上传的有图片,那么就按照图片来进行参考修改 + +用户的编辑指令如下:{user_input} + +请根据指令和原图,输出修改后的图片 + + + + + diff --git a/lib/prompts/novel-promotion/voice_analysis.en.txt b/lib/prompts/novel-promotion/voice_analysis.en.txt new file mode 100644 index 0000000..95fc3ff --- /dev/null +++ b/lib/prompts/novel-promotion/voice_analysis.en.txt @@ -0,0 +1,37 @@ +You are a dialogue voice-line analyzer. +Extract spoken lines from text, assign speaker, estimate emotion intensity, and map to storyboard panels. + +Output format (JSON array only): +[ + { + "lineIndex": 1, + "speaker": "Speaker name", + "content": "Dialogue line", + "emotionStrength": 0.3, + "matchedPanel": { + "storyboardId": "storyboard_id", + "panelIndex": 0 + } + } +] + +Input text: +{input} + +Character library: +{characters_lib_name} + +Character introductions: +{characters_introduction} + +Storyboard JSON: +{storyboard_json} + +Rules: +1. Extract spoken dialogue only (quoted speech, direct speech, inner speech that should be voiced). +2. Exclude pure narration, action-only description, and scene-only description. +3. emotionStrength must be between 0.1 and 0.5. +4. Match panel by order + speaker consistency + semantic relevance. +5. If no reliable panel match exists, set "matchedPanel": null. +6. Use canonical names from character library when possible. +7. Return strict JSON only, no markdown. diff --git a/lib/prompts/novel-promotion/voice_analysis.zh.txt b/lib/prompts/novel-promotion/voice_analysis.zh.txt new file mode 100644 index 0000000..fa31fb7 --- /dev/null +++ b/lib/prompts/novel-promotion/voice_analysis.zh.txt @@ -0,0 +1,115 @@ +你是"台词发言人分析大师"。 +任务:从文本中提取需要配音的**对话台词**,分析情绪强度,并匹配对应的视频镜头。 + +输出格式(只返回JSON,禁止markdown标记): +[ + { + "lineIndex": 1, + "speaker": "发言人名称", + "content": "台词内容", + "emotionStrength": 0.5, + "matchedPanel": { + "storyboardId": "分镜组ID", + "panelIndex": 0 + } + } +] + +分析规则: + +1. 【台词提取 - 最重要】 + ✅ 只提取以下类型的内容: + - **带引号的对话**:"xxx" 或 "xxx" 或 「xxx」 + - **直接引语**:他说:"xxx"、她喊道:"xxx" + - **内心独白**:我心想:"xxx" + + ❌ 严格排除以下内容: + - 叙述性文字(无引号的描述) + - 动作描写(描述角色的动作) + - 场景描述(描述环境、画面) + - 章节标题 + + ⚠️ 判断标准:这句话是否需要有人"说出来"?如果只是描述画面动作,不要提取。 + +2. 【情绪强度 emotionStrength】 + 根据台词的情绪激烈程度,输出0.1-0.5之间的数值(⚠️ 注意:最高不超过0.5,保持语音自然平稳): + + | 情绪类型 | 强度范围 | 示例 | + |---------|---------|------| + | 平静/陈述 | 0.1-0.15 | "好的,我知道了" | + | 普通对话 | 0.15-0.2 | "你今天怎么来了?" | + | 疑惑/好奇 | 0.2-0.25 | "这是怎么回事?" | + | 惊讶/意外 | 0.25-0.3 | "什么?!你说真的?" | + | 生气/愤怒 | 0.3-0.35 | "你给我滚出去!" | + | 悲伤/哭泣 | 0.25-0.35 | "为什么要这样对我..." | + | 狂喜/激动 | 0.35-0.4 | "太好了!我们成功了!" | + | 咆哮/嘶吼 | 0.4-0.5 | "我要杀了你!!!" | + +3. 【发言人识别】 + - 对话内容:识别说话者,如"他说"、"她喊道" + - 无引导词的引号内容:根据上下文推断发言人 + - 如果无法确定发言人,设为"旁白" + +4. 【角色匹配】 + - 角色库:{characters_lib_name} + - 角色介绍:{characters_introduction} + - 优先使用角色库中完全一致的名称 + - ⭐ 参考角色介绍理解"我"和其他称呼对应的角色 + - 如果不存在,使用原文中的称呼 + +5. 【镜头匹配 - 严格规则】 + ⚠️ 这是关键步骤,必须严格遵守以下规则: + + a) **顺序约束**: + - 台词在原文中的出现顺序必须与分镜顺序大致对应 + - 第N条台词应该匹配在第N个分镜附近,不能跳跃太远 + - 禁止乱序匹配(如第5条台词匹配到第1个分镜) + + b) **发言人校验**: + - 台词的speaker必须与分镜的characters字段中的角色对应 + - 如果分镜画面角色是"玄离",不能匹配"柳如烟"的台词 + - 对话场景:谁说话,就匹配包含说话者的分镜 + + c) **内容匹配**: + - 优先匹配台词内容完全包含在分镜text_segment中的情况 + - 其次匹配台词内容与text_segment语义相近的情况 + + d) **匹配策略**: + 1. 首先根据text_segment精确匹配台词内容 + 2. 验证分镜角色是否包含台词发言人 + 3. 验证顺序是否合理(前后3个分镜范围内) + 4. 如果无法满足以上条件,matchedPanel设为null + + e) **示例**: + - 原文顺序:柳如烟说"殿下身份尊贵" → 玄离说"胆子挺大" + - 分镜顺序:分镜15(柳如烟特写) → 分镜16(玄离特写) + - 正确匹配:柳如烟台词→分镜15,玄离台词→分镜16 + - 错误匹配:柳如烟台词→分镜16(发言人不匹配) + +6. 【多音字处理 - 重要】 + 为确保TTS语音合成发音正确,对于容易被误读的多音字,需要替换为**读音完全相同(包括声调)的单音字**。 + + 处理原则: + a) **识别多音字**:找出台词中的多音字(如:还、行、了、乐、朝、重、都等) + b) **判断正确读音**:根据上下文语义判断该字在此处的正确读音 + c) **选择替换字**:找一个读音完全相同(声母、韵母、声调都一致)的常用单音字替换 + d) **验证替换**:确保替换后的字读音与原意读音完全一致 + + 替换示例思路: + - "还(huán)给我" → 用"环"替换,因为"环"只读huán + - "银行(háng)" → 用"航"替换,因为"航"只读háng + - "了(liǎo)解" → 用"聊"替换,因为"聊"只读liáo(接近liǎo) + - "快乐(lè)" → 用"乐"保持原字,因为TTS通常能正确读常见词 + - "重(zhòng)量" → 用"众"替换,因为"众"只读zhòng + + ⚠️ 注意事项: + - 必须确保替换字的读音与目标读音完全一致,不要用读音相近但不同的字 + - 例如:"勒"读lè或lēi,不能用来替换读le的字 + - 常见词组(如"了解"、"快乐"、"音乐")TTS通常能正确读,可以保持原字 + - 只有当多音字在特定语境下容易被TTS误读时才需要替换 + +分镜数据如下: +{storyboard_json} + +原文如下: +{input} diff --git a/messages/en/actions.json b/messages/en/actions.json new file mode 100644 index 0000000..b9aafef --- /dev/null +++ b/messages/en/actions.json @@ -0,0 +1,18 @@ +{ + "storyboard": "Storyboard", + "storyboard_candidate": "Storyboard Candidate", + "character": "Character", + "location": "Location", + "video": "Video", + "analyze": "Analyze", + "analyze_character": "Character Analysis", + "analyze_location": "Location Analysis", + "clips": "Clip Splitting", + "storyboard_text_plan": "Storyboard Planning", + "storyboard_text_detail": "Storyboard Details", + "tts": "Text-to-Speech", + "regenerate": "Regenerate", + "voice-generate": "Voice Generation", + "voice-design": "Voice Design", + "lip-sync": "Lip Sync" +} \ No newline at end of file diff --git a/messages/en/apiConfig.json b/messages/en/apiConfig.json new file mode 100644 index 0000000..6859fe2 --- /dev/null +++ b/messages/en/apiConfig.json @@ -0,0 +1,107 @@ +{ + "title": "API Configuration", + "saving": "Saving...", + "saved": "Saved", + "saveFailed": "Save failed", + "connected": "Connected", + "notConfigured": "Not configured", + "configure": "Configure", + "connect": "Connect", + "show": "Show", + "hide": "Hide", + "capability": "Models", + "default": "Default", + "delete": "Delete", + "add": "Add", + "cancel": "Cancel", + "save": "Save", + "comingSoon": "Coming soon", + "priceInput": "Input {amount}", + "priceOutput": "Output {amount}", + "priceUnavailable": "N/A", + "fillComplete": "Please fill in all fields", + "fillPricing": "Please fill in pricing information", + "pricingInputLabel": "Input price", + "pricingOutputLabel": "Output price", + "modelIdExists": "Model ID already exists", + "modelDisplayName": "Display Name (for your reference)", + "modelActualId": "Actual Model ID (API parameter)", + "noModelsForProvider": "No models configured for this provider", + "defaultModels": "Default Model Configuration", + "textDefault": "Text Model", + "characterDefault": "Character Model", + "locationDefault": "Location Model", + "storyboardDefault": "Storyboard Model", + "editDefault": "Edit Model", + "videoDefault": "Video Model", + "lipsyncDefault": "Lip Sync Model", + "selectDefault": "Select", + "providerPool": "Provider Pool", + "providerIdExists": "Provider ID already exists", + "presetProviderCannotDelete": "Preset providers cannot be deleted", + "confirmDeleteProvider": "Are you sure you want to delete this provider?", + "presetModelCannotDelete": "Preset models cannot be deleted", + "confirmDeleteModel": "Are you sure you want to delete this model?", + "addGeminiProvider": "Add Model Provider", + "baseUrl": "Base URL", + "configureBaseUrl": "Configure URL", + "addModel": "Add Model", + "batchModeHalfPrice": "Batch mode (50% price)", + "typeText": "Text", + "typeImage": "Image", + "typeVideo": "Video", + "typeAudio": "Audio", + "apiKeyLabel": "API Key", + "apiType": "API Type", + "apiTypeGeminiCompatible": "Gemini Compatible", + "apiTypeOpenAICompatible": "OpenAI Compatible", + "apiTypeGeminiHint": "Uses Google SDK", + "otherProviders": "Other Settings", + "audioCategory": "Audio", + "audioAndLipsync": "Audio & Lip Sync", + "configureApiKey": "Configure API Key", + "enterApiKey": "Enter API Key", + "tabs": { + "llm": "Text Models", + "image": "Image Models", + "video": "Video Models", + "audio": "Audio Models", + "other": "Other" + }, + "sections": { + "llmApiKeys": "Text Model API Keys", + "imageApiKeys": "Image Model API Keys", + "videoApiKeys": "Video Model API Keys", + "audioApiKey": "Audio Model API Key", + "lipsyncApiKey": "Lip Sync API Key" + }, + "defaultModel": { + "title": "Default Model", + "hint": "New projects and Asset Hub will use this default configuration", + "notSelected": "Not selected", + "analysis": "Analysis Model", + "image": "Image Generation", + "video": "Video Generation", + "resolution": "Image Resolution" + }, + "viewTutorial": "View Tutorial", + "tutorial": { + "button": "Tutorial", + "title": "Setup Guide", + "subtitle": "Follow these steps to complete the configuration", + "close": "Got it", + "openLink": "Open link", + "steps": { + "ark_step1": "Go to the Volcano Engine console to create an API Key", + "ark_step2": "On the model management page, click 'Enable All Models' button in the top right corner", + "openrouter_step1": "Go to OpenRouter platform and create an API Key (must select models with image capabilities)", + "fal_step1": "Go to FAL platform and create an API Key", + "google_step1": "Go to Google AI Studio and create an API Key", + "minimax_step1": "Go to MiniMax platform and get an API Key", + "vidu_step1": "Go to the Vidu platform and click 'Create API Key'", + "openai_compatible_step1": "Enter any OpenAI-compatible service Base URL and API key", + "gemini_compatible_step1": "Enter any Gemini-compatible service Base URL and API key", + "qwen_step1": "Go to Alibaba Cloud Bailian platform and get an API Key" + } + } +} diff --git a/messages/en/apiTypes.json b/messages/en/apiTypes.json new file mode 100644 index 0000000..fcb7811 --- /dev/null +++ b/messages/en/apiTypes.json @@ -0,0 +1,9 @@ +{ + "image": "Image Generation", + "video": "Video Generation", + "text": "Text Analysis", + "tts": "Text-to-Speech", + "voice": "Voice Dubbing", + "voice_design": "Voice Design", + "lip_sync": "Lip Sync" +} \ No newline at end of file diff --git a/messages/en/assetHub.json b/messages/en/assetHub.json new file mode 100644 index 0000000..9813edc --- /dev/null +++ b/messages/en/assetHub.json @@ -0,0 +1,101 @@ +{ + "title": "Asset Hub", + "description": "Manage your global character and location assets", + "modelHint": "Asset Hub uses default models. To change settings, go to", + "modelHintLink": "API Settings", + "modelHintSuffix": "", + "folders": "Folders", + "noFolders": "No folders yet", + "allAssets": "All Assets", + "characters": "Characters", + "locations": "Locations", + "voices": "Voices", + "addCharacter": "Add Character", + "addLocation": "Add Location", + "addVoice": "Add Voice", + "newFolder": "New Folder", + "editFolder": "Edit Folder", + "deleteFolder": "Delete Folder", + "folderName": "Folder Name", + "folderNamePlaceholder": "Enter folder name", + "emptyState": "No assets yet", + "emptyStateHint": "Click the buttons above to add characters or locations", + "generate": "Generate", + "generating": "Generating...", + "regenerate": "Regenerate", + "undo": "Undo", + "delete": "Delete", + "cancel": "Cancel", + "save": "Save", + "create": "Create", + "confirmDeleteFolder": "Delete this folder? Assets inside will be moved to uncategorized.", + "confirmDeleteCharacter": "Delete this character? This action cannot be undone.", + "confirmDeleteLocation": "Delete this location? This action cannot be undone.", + "confirmDeleteVoice": "Delete this voice? This action cannot be undone.", + "voiceName": "Voice Name", + "voiceNamePlaceholder": "Enter voice name", + "voiceNameRequired": "Please enter a voice name", + "voicePickerTitle": "Select from Voice Library", + "voicePickerEmpty": "No voices yet. Please create a voice first.", + "voicePickerConfirm": "Confirm Selection", + "pagination": { + "previous": "Previous", + "next": "Next" + }, + "common": { + "cancel": "Cancel" + }, + "generateFailed": "Generation failed", + "selectFailed": "Selection failed", + "uploadFailed": "Upload failed", + "editFailed": "Edit failed", + "saveVoiceFailed": "Failed to save voice", + "saveVoiceFailedDetail": "Failed to save voice: {error}", + "bindVoiceFailed": "Failed to bind voice", + "bindVoiceFailedDetail": "Failed to bind voice: {error}", + "voiceDesignSaved": "AI-designed voice has been set for {name}", + "appearanceLabel": "Appearance {index}", + "voiceSettings": { + "title": "Voice", + "noVoice": "No voice", + "previewFailed": "Preview failed: {error}", + "uploadFailed": "Upload audio failed: {error}", + "uploading": "Uploading...", + "uploaded": "Uploaded", + "uploadAudio": "Upload Audio", + "aiDesign": "AI Design", + "voiceLibrary": "Voice Library", + "pause": "Pause", + "preview": "Preview Voice" + }, + "modal": { + "newCharacter": "New Character", + "confirm": "Confirm", + "processing": "Processing...", + "newLocation": "New Location", + "addCharacter": "Create Character", + "addLocation": "Create Location", + "adding": "Creating...", + "aiDesign": "AI Design", + "aiDesignPlaceholder": "e.g., A beautiful woman in a red traditional dress with flowing long hair", + "aiDesignLocationPlaceholder": "e.g., A classical Chinese garden with rockery and pavilions", + "aiDesignTip": "AI will generate a detailed description based on your input. You can edit it after generation.", + "aiDesignLocationTip": "AI will generate a detailed scene description based on your input", + "generate": "Generate", + "generating": "Generating...", + "nameLabel": "Character Name", + "namePlaceholder": "Enter character name", + "descLabel": "Character Description", + "descPlaceholder": "Describe the character's appearance, clothing, hairstyle, etc...", + "locationNameLabel": "Location Name", + "locationNamePlaceholder": "Enter location name", + "locationSummaryLabel": "Location Description", + "locationSummaryPlaceholder": "Describe the environment, atmosphere, features, etc...", + "referenceUpload": "Upload Reference", + "referenceUploadTip": "Upload a character image, AI will convert it to a three-view design sheet", + "convertToCharacter": "Convert to 3-View", + "converting": "Converting...", + "dropOrClick": "Drop image or click to upload", + "supportedFormats": "JPG, PNG supported" + } +} diff --git a/messages/en/assetLibrary.json b/messages/en/assetLibrary.json new file mode 100644 index 0000000..9b10bb6 --- /dev/null +++ b/messages/en/assetLibrary.json @@ -0,0 +1,14 @@ +{ + "title": "Asset Library", + "button": "Assets", + "characters": "Characters", + "locations": "Locations", + "noCharacters": "No characters", + "noLocations": "No locations", + "addCharacter": "Add Character", + "addLocation": "Add Location", + "generateImage": "Generate Image", + "regenerateImage": "Regenerate", + "analyzeAssets": "Analyze Assets", + "analyzing": "Analyzing..." +} \ No newline at end of file diff --git a/messages/en/assetModal.json b/messages/en/assetModal.json new file mode 100644 index 0000000..a9d63a4 --- /dev/null +++ b/messages/en/assetModal.json @@ -0,0 +1,78 @@ +{ + "character": { + "title": "New Character", + "name": "Character Name", + "namePlaceholder": "Enter character name", + "modeReference": "Reference Image", + "modeDescription": "Description", + "isSubAppearance": "This is a sub-appearance", + "isSubAppearanceHint": "Add a new appearance state for an existing character", + "uploadReference": "Upload Reference", + "pasteHint": "Ctrl+V to paste", + "dropOrClick": "Click to upload or drag image", + "supportedFormats": "Supports JPG, PNG formats", + "nameRequired": "Please enter character name first to use reference conversion", + "convertToSheet": "Convert to standard character sheet", + "referenceTip": "Upload any character image, AI will generate a standard character sheet", + "description": "Character Description", + "modifyDescription": "Modify Description", + "descPlaceholder": "Enter character appearance description...", + "modifyDescriptionPlaceholder": "Describe how to modify the primary appearance, e.g. formal outfit, post-battle injuries, add a cloak...", + "selectMainCharacter": "Select Main Character", + "selectCharacterPlaceholder": "Please select a character...", + "appearancesCount": "{count} appearances", + "changeReason": "Appearance Change Reason", + "changeReasonPlaceholder": "e.g. injured after battle, changed into formal wear for a banquet...", + "defaultDescription": "{name}'s character profile", + "generationMode": "Generation Mode", + "directGenerate": "Direct Generate", + "extractPrompt": "Extract Prompt", + "extractFirst": "Extract Description First", + "directGenerateDesc": "Directly generate character sheet from reference (img2img)", + "extractPromptDesc": "Extract description from image first, then generate (txt2img)", + "maxReferenceImages": "Up to 5 reference images", + "selectedCount": "Selected {count}/5 images", + "extractDescription": "Extract Description", + "extracting": "Extracting...", + "extractedDescription": "Extracted Description (Editable)", + "reExtract": "Re-extract", + "editHint": "Edit the description, then click below to generate", + "generateFromDescription": "Generate from Description", + "textToImageTip": "Text-to-image mode: Generate from extracted description", + "pleaseExtractFirst": "Please extract character description first" + }, + "location": { + "title": "New Location", + "name": "Location Name", + "namePlaceholder": "Enter location name", + "description": "Location Description", + "descPlaceholder": "Enter location description..." + }, + "artStyle": { + "title": "Art Style" + }, + "aiDesign": { + "title": "AI Design", + "placeholder": "Describe the character you want...", + "placeholderLocation": "Describe the scene atmosphere...", + "generating": "Designing...", + "generate": "Generate", + "tip": "Enter a simple description, AI will generate detailed settings" + }, + "common": { + "creating": "Creating...", + "create": "Create", + "cancel": "Cancel", + "adding": "Adding...", + "add": "Add", + "optional": "(Optional)" + }, + "errors": { + "uploadFailed": "Upload failed", + "extractDescriptionFailed": "Failed to extract description", + "createFailed": "Creation failed", + "aiDesignFailed": "AI design failed", + "addSubAppearanceFailed": "Failed to add sub-appearance", + "insufficientBalance": "Insufficient balance" + } +} diff --git a/messages/en/assetPicker.json b/messages/en/assetPicker.json new file mode 100644 index 0000000..28faeaf --- /dev/null +++ b/messages/en/assetPicker.json @@ -0,0 +1,18 @@ +{ + "selectCharacter": "Select Character from Asset Hub", + "selectLocation": "Select Location from Asset Hub", + "selectVoice": "Select Voice from Asset Hub", + "searchPlaceholder": "Search by name or folder...", + "noAssets": "No assets in Asset Hub", + "createInAssetHub": "Please create characters/locations/voices in Asset Hub first", + "noSearchResults": "No matching assets found", + "appearances": "appearances", + "images": "images", + "cancel": "Cancel", + "confirmCopy": "Confirm Copy", + "copyFromGlobal": "Copy from Asset Hub", + "copySuccess": "Copy successful", + "copyFailed": "Copy failed", + "preview": "Preview", + "stop": "Stop" +} \ No newline at end of file diff --git a/messages/en/assets.json b/messages/en/assets.json new file mode 100644 index 0000000..0c234ac --- /dev/null +++ b/messages/en/assets.json @@ -0,0 +1,321 @@ +{ + "stage": { + "title": "Assets Confirmation", + "characters": "Characters", + "locations": "Locations", + "analyze": "Analyze Assets", + "analyzing": "Analyzing...", + "generateAll": "Generate All", + "noCharacters": "No characters", + "noLocations": "No locations", + "confirmProfiles": "Character Profiles to Confirm", + "confirmHint": "Please confirm these profiles before generating descriptions", + "confirmAll": "Confirm All ({count})", + "assetsTitle": "Asset Analysis", + "characterAssets": "Character Assets", + "locationAssets": "Location Assets", + "counts": "{characterCount} Characters, {appearanceCount} Appearances", + "locationCounts": "{count} Locations", + "undoFailed": "Undo failed", + "undoFailedError": "Undo failed: {error}", + "undoSuccess": "Reverted to previous version", + "editFailed": "Edit failed", + "editFailedError": "Image edit failed: {error}", + "updateSuccess": "Description updated successfully" + }, + "character": { + "add": "Add Character", + "edit": "Edit Character", + "delete": "Delete Character", + "deleteConfirm": "Delete this character?", + "deleteAppearanceConfirm": "Delete this appearance?", + "deleteFailed": "Delete failed: {error}", + "deleteWhole": "Delete Whole Character", + "deleteOptions": "Delete Options", + "name": "Character Name", + "description": "Appearance Description", + "generateImage": "Generate Profile", + "regenerateImage": "Regenerate", + "generate": "Generate", + "regenerating": "Generating...", + "profile": "Profile", + "voiceSettings": "Voice Settings", + "speaker": "Speaker", + "selectSpeaker": "Select Speaker", + "noSpeaker": "Not Set", + "primary": "Primary", + "secondary": "Secondary", + "generateFromPrimary": "Generate from Primary", + "selectPrimaryFirst": "Select primary first", + "editing": "Editing...", + "confirming": "Confirming...", + "assetCount": "{count} Appearances", + "characterCount": "{count} Characters", + "updateFailed": "Update description failed", + "addFailed": "Add character failed", + "copyFromGlobal": "Copy from Asset Hub" + }, + "location": { + "add": "Add Location", + "edit": "Edit Location", + "delete": "Delete Location", + "deleteConfirm": "Delete this location?", + "deleteFailed": "Delete failed: {error}", + "name": "Location Name", + "summary": "Summary", + "summaryPlaceholder": "Usage/associations, e.g.: John's master bedroom", + "description": "Location Description", + "generateImage": "Generate Image", + "regenerateImage": "Regenerate", + "updateFailed": "Update description failed", + "addFailed": "Add location failed" + }, + "image": { + "upload": "Upload Image", + "uploadReplace": "Upload Replacement", + "uploadFailed": "Upload Failed", + "uploadFailedError": "Upload failed: {error}", + "uploadSuccess": "Upload Success!", + "edit": "Edit Image", + "editPrompt": "Edit Prompt", + "undo": "Undo to Previous Version", + "undoConfirm": "Are you sure you want to undo to the previous version? Current version will be deleted.", + "regenerateGroup": "Regenerate Group", + "regenerateStuck": "Click to regenerate (if stuck)", + "selectTip": "Once selected and confirmed, you can edit and modify the image", + "selectFirst": "Please select an image first", + "useThis": "Use this option", + "optionAlt": "{name} - Option {number}", + "optionNumber": "Option {number}", + "optionSelected": "Selected Option {number}", + "confirmOption": "Confirm Option {number}", + "deleteOthersHint": "(delete others)", + "confirmSuccess": "Selection confirmed", + "confirmFailed": "Confirm selection failed: {error}", + "selectFailed": "Select image failed: {error}", + "cancelSelection": "Cancel Selection", + "deleteThis": "Delete this appearance", + "undoFailed": "Undo failed", + "undoSuccess": "✓ Reverted to previous version", + "editFailed": "Image edit failed", + "editSuccess": "Image edit successful", + "regenerateFailed": "Regenerate failed: {error}" + }, + "modal": { + "newCharacter": "New Character", + "addSubAppearance": "Add Sub-Appearance", + "aiDesign": "AI Design", + "aiDesigning": "Designing...", + "designInstruction": "Please enter design instruction", + "enterNameDesc": "Please enter character name and description", + "selectCharacter": "Please select a character", + "enterChangeReason": "Please enter change reason", + "enterSubDesc": "Please enter appearance description", + "insufficientBalance": "Insufficient Balance\n\n{error}", + "designFailed": "AI Design Failed: {error}", + "addFailed": "Add Failed: {error}", + "aiDesignPlaceholderNew": "e.g. A 20-year-old female mage, blonde hair, blue eyes...", + "aiDesignPlaceholderSub": "e.g. Changed into black combat gear...", + "aiTipNew": "Describe the character, AI will generate details", + "aiTipSub": "Describe the new state, AI will generate sub-appearance description", + "nameLabel": "Character Name", + "namePlaceholder": "Enter name...", + "descLabel": "Appearance Description", + "descPlaceholder": "Enter description...", + "selectLabel": "Select Character", + "selectPlaceholder": "-- Select Character --", + "existingAppearances": "Existing:", + "reasonLabel": "Change Reason", + "reasonPlaceholder": "e.g. After changing clothes, Injured...", + "reasonTip": "Briefly describe the difference from primary appearance", + "subDescPlaceholder": "Describe only the changes...", + "subDescTip": "Only describe changes (clothes, state), face/body inherits from primary", + "adding": "Adding...", + "insufficientBalanceDefault": "Insufficient balance, please top up to continue", + "addFailedGeneric": "Add Failed", + "appearancesCount": "Appearances", + "addCharacter": "Add Character", + "addLocation": "Add Location", + "aiDesignTip": "Describe the scene you want, AI will generate name and details", + "designing": "AI designing...", + "saveName": "Save Name", + "saveOnly": "Save Only", + "sceneDescription": "Scene Description", + "scenePrompt": "Scene Description Prompt", + "appearancePrompt": "Appearance Description Prompt", + "smartModify": "Smart Modify", + "modifyPlaceholder": "e.g.: Change to night, add moonlight, add curtains...", + "modifyPlaceholderCharacter": "e.g.: Change hair to blonde, height to 180cm, wear black suit...", + "modifying": "Smart modifying...", + "modifyFailed": "Modification failed", + "editCharacter": "Edit Character", + "editLocation": "Edit Location", + "saveAndGenerate": "Save and Generate", + "generatingAutoClose": "Generating image, will close automatically when done...", + "aiLocationTip": "Enter what you want to modify, AI will adjust the scene description", + "aiDesignPlaceholderLocation": "e.g. An ancient magical library, towering bookshelves, dim candlelight, mysterious atmosphere...", + "artStyle": "Art Style", + "generate": "Generate", + "introduction": "Character Introduction", + "introductionPlaceholder": "e.g.: The protagonist; 'I' refers to her. Others call her 'Snow' or 'Sister Snow'...", + "introductionTip": "Describe the character's role in the story, narrative perspective (who 'I' refers to), how others address them", + "saveIntroduction": "Save Introduction" + }, + "toolbar": { + "filter": "Filter", + "viewAll": "View All", + "showGenerated": "Generated", + "showPending": "Pending", + "assetManagement": "Asset Management", + "assetCount": "{total} assets ({appearances} character appearances + {locations} locations)", + "globalAnalyze": "Global Analysis", + "globalAnalyzing": "Performing global asset analysis...", + "globalAnalyzingHint": "Please don't refresh. Results will appear automatically when complete", + "globalAnalyzingTip": "Analyzing all episodes, extracting characters and locations...", + "globalAnalyzeHint": "Analyze all episodes to extract characters and locations", + "globalAnalyzeSuccess": "Global analysis complete: {characters} new characters, {locations} new locations", + "globalAnalyzeFailed": "Global analysis failed", + "generateAll": "Generate All Images", + "generateAllNoop": "All assets already have images, nothing to generate", + "generating": "Generating ({current}/{total})", + "regenerateAll": "Regenerate All", + "regenerateAllConfirm": "Regenerate images for all assets? This will overwrite existing images.", + "noAssetsToGenerate": "No assets available for generation", + "regenerateAllHint": "Regenerate all asset images (overwrite existing)" + }, + "common": { + "actions": "Actions", + "add": "Add", + "cancel": "Cancel", + "confirm": "Confirm", + "copy": "Copy", + "delete": "Delete", + "download": "Download", + "edit": "Edit", + "generate": "Generate", + "generateFailed": "Generation Failed", + "loading": "Loading...", + "none": "None", + "preview": "Preview", + "refresh": "Refresh", + "regenerate": "Regenerate", + "save": "Save", + "status": "Status", + "submitFailed": "Submit Failed", + "upload": "Upload", + "unknownError": "Unknown error" + }, + "video": { + "panelCard": { + "generating": "Generating...", + "editPrompt": "Edit Prompt" + } + }, + "smartImport": { + "preview": { + "saving": "Saving..." + } + }, + "storyboard": { + "group": { + "generating": "Generating..." + } + }, + "errors": { + "saveFailed": "Save Failed, please retry", + "failed": "failed, please retry", + "insufficientBalance": "Insufficient balance", + "aiDesignFailed": "AI design failed", + "createFailed": "Creation failed" + }, + "assetLibrary": { + "button": "Asset Library", + "title": "Asset Library", + "copySuccessCharacter": "Character appearance copied successfully", + "copySuccessLocation": "Location image copied successfully", + "copySuccessVoice": "Voice copied successfully", + "copyFailed": "Copy failed: {error}" + }, + "tts": { + "voiceDesignSaved": "AI-designed voice has been set for {name}", + "saveVoiceDesignFailed": "Failed to save voice design: {error}", + "title": "Voice", + "noVoice": "No voice", + "previewFailed": "Preview failed: {error}", + "uploadFailed": "Upload audio failed: {error}", + "uploading": "Uploading...", + "uploaded": "Uploaded", + "uploadAudio": "Upload Audio", + "pause": "Pause", + "preview": "Preview Voice" + }, + "characterProfile": { + "importance": { + "S": "S-Level - Main Protagonist", + "A": "A-Level - Core Supporting", + "B": "B-Level - Important Supporting", + "C": "C-Level - Minor Character", + "D": "D-Level - Extra" + }, + "costumeLevel": { + "5": "Royal/Luxury", + "4": "Noble/Elite", + "3": "Professional/Quality", + "2": "Casual/Normal", + "1": "Plain/Uniform" + }, + "importanceLevel": "Character Importance Level", + "characterArchetype": "Character Archetype", + "archetypePlaceholder": "e.g.: Domineering CEO, Schemer", + "personalityTags": "Personality Tags", + "addTagPlaceholder": "Add tag", + "costumeLevelLabel": "Costume Level", + "suggestedColors": "Suggested Colors", + "colorPlaceholder": "e.g.: Navy blue, Gold", + "primaryMarker": "Primary Identifier", + "markerNote": "(Recommended for S/A level)", + "markingsPlaceholder": "e.g.: Tear-shaped mole, Silver earring", + "visualKeywords": "Visual Keywords", + "keywordsPlaceholder": "e.g.: Elite aura, Ascetic style", + "editDialogTitle": "Edit Character Profile - {name}", + "confirmAndGenerate": "Confirm & Generate", + "useExisting": "Use Existing", + "editProfile": "Edit Profile", + "delete": "Delete Character", + "summary": { + "gender": "Gender:", + "age": "Age:", + "era": "Era:", + "class": "Class:", + "occupation": "Occupation:", + "personality": "Personality:", + "costume": "Costume:", + "identifier": "Identifier:" + }, + "parseFailed": "Failed to parse profile data", + "confirmSuccessGenerating": "✓ Profile confirmed. Visual description generation started", + "confirmFailed": "Confirm failed: {error}", + "noPendingCharacters": "No pending characters to confirm", + "batchConfirmPrompt": "Generate visual descriptions for {count} characters?", + "batchConfirmSuccess": "✓ Visual descriptions generated for {count} characters", + "batchConfirmFailed": "Batch confirmation failed: {error}", + "deleteConfirm": "Delete this character? This action cannot be undone.", + "deleteSuccess": "✓ Character deleted", + "deleteFailed": "Delete failed: {error}" + }, + "imageEdit": { + "editCharacterImage": "Edit Character Image", + "editLocationImage": "Edit Location Image", + "characterLabel": "Character: {name}", + "locationLabel": "Location: {name}", + "editInstruction": "Edit Instruction", + "subtitle": "Enter an edit instruction and optionally upload reference images", + "characterPlaceholder": "Describe what you want to change, e.g.: Change hair to blonde, add glasses, change to casual clothes...", + "locationPlaceholder": "Describe what you want to change, e.g.: Add more trees, change to night scene...", + "storyboardPlaceholder": "Describe what you want to change, e.g.: Change background color, adjust character expression...", + "noAssetHint": "No assets, click \"Add Asset\" to select", + "referenceImages": "Reference Images", + "referenceImagesHint": "(optional, paste supported)", + "startEditing": "Start Editing" + } +} diff --git a/messages/en/auth.json b/messages/en/auth.json new file mode 100644 index 0000000..cfbbaa6 --- /dev/null +++ b/messages/en/auth.json @@ -0,0 +1,29 @@ +{ + "welcomeBack": "Welcome Back", + "loginTo": "Sign in to waoowaoo", + "createAccount": "Create Account", + "joinPlatform": "Join waoowaoo", + "phoneNumber": "Username", + "password": "Password", + "confirmPassword": "Confirm Password", + "phoneNumberPlaceholder": "Enter your username", + "passwordPlaceholder": "Enter your password", + "passwordMinPlaceholder": "Enter password (at least 6 characters)", + "confirmPasswordPlaceholder": "Re-enter your password", + "loginButton": "Sign In", + "loginButtonLoading": "Signing in...", + "signupButton": "Sign Up", + "signupButtonLoading": "Signing up...", + "noAccount": "Don't have an account?", + "hasAccount": "Already have an account?", + "signupNow": "Sign Up Now", + "signinNow": "Sign In Now", + "backToHome": "← Back to Home", + "loginFailed": "Login failed, please check your phone number and password", + "loginError": "An error occurred during login", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "signupSuccess": "Registration successful! Redirecting to login page...", + "signupFailed": "Registration failed", + "signupError": "An error occurred during registration" +} \ No newline at end of file diff --git a/messages/en/billing.json b/messages/en/billing.json new file mode 100644 index 0000000..81c0a28 --- /dev/null +++ b/messages/en/billing.json @@ -0,0 +1,15 @@ +{ + "transactionType": "Transaction Type", + "startDate": "Start Date", + "endDate": "End Date", + "all": "All", + "income": "Income", + "expense": "Expense", + "reset": "Reset", + "filter": "Filter", + "noRecords": "No records", + "accountRecharge": "Account Recharge", + "serviceConsumption": "Service Consumption", + "balance": "Balance", + "allTypes": "All Types" +} \ No newline at end of file diff --git a/messages/en/common.json b/messages/en/common.json new file mode 100644 index 0000000..287e7bc --- /dev/null +++ b/messages/en/common.json @@ -0,0 +1,134 @@ +{ + "appName": "waoowaoo", + "betaVersion": "Beta v0.1", + "loading": "Loading...", + "save": "Save", + "cancel": "Cancel", + "confirm": "Confirm", + "delete": "Delete", + "edit": "Edit", + "search": "Search", + "clear": "Clear", + "close": "Close", + "back": "Back", + "next": "Next", + "previous": "Previous", + "submit": "Submit", + "reset": "Reset", + "generate": "Generate", + "regenerate": "Regenerate", + "preview": "Preview", + "download": "Download", + "upload": "Upload", + "select": "Select", + "add": "Add", + "remove": "Remove", + "refresh": "Refresh", + "expand": "Expand", + "collapse": "Collapse", + "all": "All", + "none": "None", + "success": "Success", + "error": "Error", + "warning": "Warning", + "info": "Info", + "copy": "Copy", + "paste": "Paste", + "apply": "Apply", + "autoSave": "Auto-save", + "saved": "Saved", + "episode": "Episode", + "project": "Project", + "editEpisodeName": "Edit Episode Name", + "deleteEpisode": "Delete Episode", + "deleteEpisodeConfirm": "Confirm Delete", + "newEpisode": "New Episode", + "optional": "(Optional)", + "rename": "Rename", + "dragToReorder": "Drag to reorder", + "episodeNamePlaceholder": "Enter episode name...", + "cancelSelection": "Cancel selection", + "referenceImage": "Reference image", + "previewLarge": "Preview large", + "viewOriginal": "View original", + "schemeN": "Scheme {n}", + "insufficientBalance": "Insufficient Balance", + "insufficientBalanceDetail": "Insufficient account balance, please recharge to continue", + "operationFailed": "Operation failed", + "pleaseRetry": "Please retry", + "recommended": "Recommended", + "language": { + "select": "Select language", + "zh": "Chinese", + "en": "English", + "switchConfirmTitle": "Switch language?", + "switchConfirmMessage": "Switching to {targetLanguage} will update not only interface text, but also end-to-end prompts, script generation, and workflow outputs. Continue?", + "switchConfirmAction": "Switch now" + }, + "taskStatus": { + "intent": { + "generate": { + "running": { + "image": "Generating", + "video": "Generating", + "audio": "Generating", + "text": "Generating" + } + }, + "regenerate": { + "running": { + "image": "Regenerating", + "video": "Regenerating", + "audio": "Regenerating", + "text": "Regenerating" + } + }, + "modify": { + "running": { + "image": "Modifying", + "video": "Modifying", + "audio": "Modifying", + "text": "Modifying" + } + }, + "analyze": { + "running": { + "image": "Analyzing", + "video": "Analyzing", + "audio": "Analyzing", + "text": "Analyzing" + } + }, + "build": { + "running": { + "image": "Building", + "video": "Building", + "audio": "Building", + "text": "Building" + } + }, + "convert": { + "running": { + "image": "Converting", + "video": "Converting", + "audio": "Converting", + "text": "Converting" + } + }, + "process": { + "running": { + "image": "Processing", + "video": "Processing", + "audio": "Processing", + "text": "Processing" + } + } + }, + "failed": { + "image": "Failed", + "video": "Failed", + "audio": "Failed", + "text": "Failed" + } + } +} \ No newline at end of file diff --git a/messages/en/configModal.json b/messages/en/configModal.json new file mode 100644 index 0000000..746041e --- /dev/null +++ b/messages/en/configModal.json @@ -0,0 +1,31 @@ +{ + "title": "Project Global Configuration", + "saved": "Saved", + "autoSave": "Auto-save", + "visualStyle": "Visual Style", + "modelParams": "Model Parameters", + "aspectRatio": "Aspect Ratio", + "ttsSettings": "TTS Settings", + "loadingModels": "Loading model list...", + "analysisModel": "Analysis Model", + "characterModel": "Character Model", + "locationModel": "Location Model", + "storyboardModel": "Storyboard Model", + "editModel": "Edit Model", + "videoModel": "Video Model", + "videoResolution": "Video Resolution", + "ttsVoice": "TTS Voice", + "ttsRate": "Speech Rate", + "fetchModelsFailed": "Failed to fetch user model list", + "placeholder": "Please enter...", + "description": "Description", + "hint": "Hint", + "pleaseSelect": "Please select...", + "selectModel": "Select Model", + "paramConfig": "Parameters", + "fixed": "Fixed", + "noParams": "No configurable parameters", + "confirm": "Confirm", + "cancel": "Cancel", + "delete": "Delete" +} diff --git a/messages/en/errors.json b/messages/en/errors.json new file mode 100644 index 0000000..7851044 --- /dev/null +++ b/messages/en/errors.json @@ -0,0 +1,19 @@ +{ + "UNAUTHORIZED": "Please log in first", + "FORBIDDEN": "Access denied", + "NOT_FOUND": "Resource not found", + "INSUFFICIENT_BALANCE": "Insufficient API balance. Please top up and retry", + "RATE_LIMIT": "Too many requests. Please retry in {retryAfter} seconds", + "QUOTA_EXCEEDED": "Quota exceeded. Please try again later", + "GENERATION_FAILED": "Generation failed. Please retry", + "GENERATION_TIMEOUT": "Generation timed out. Please retry", + "SENSITIVE_CONTENT": "Content may contain sensitive material", + "INVALID_PARAMS": "Invalid parameters", + "MISSING_CONFIG": "Please complete model configuration first", + "INTERNAL_ERROR": "Server error. Please try again later", + "NETWORK_ERROR": "Network error. Please check your connection", + "EXTERNAL_ERROR": "External service temporarily unavailable. Please retry later", + "TASK_NOT_READY": "Task is still processing", + "NO_RESULT": "Task has no result", + "CONFLICT": "Resource state conflict" +} \ No newline at end of file diff --git a/messages/en/landing.json b/messages/en/landing.json new file mode 100644 index 0000000..1cb04e0 --- /dev/null +++ b/messages/en/landing.json @@ -0,0 +1,26 @@ +{ + "title": "waoowaoo", + "subtitle": "AI Film & TV Studio", + "enterWorkspace": "Enter Workspace", + "getStarted": "Get Started", + "learnMore": "Learn More", + "features": { + "title": "Unleash Infinite Creativity", + "subtitle": "Full-process AI assistance, from script to final cut", + "character": { + "title": "Character Workshop", + "description": "Create unique anime characters with high consistency" + }, + "storyboard": { + "title": "Smart Storyboard", + "description": "One-click text to storyboard, precise narrative control" + }, + "world": { + "title": "World Building", + "description": "Immersive scene generation to build grand story backgrounds" + } + }, + "footer": { + "copyright": "2026 waoowaoo AI. All rights reserved." + } +} \ No newline at end of file diff --git a/messages/en/layout.json b/messages/en/layout.json new file mode 100644 index 0000000..068d536 --- /dev/null +++ b/messages/en/layout.json @@ -0,0 +1,4 @@ +{ + "title": "AI Anime Production Platform", + "description": "Create professional anime content with cutting-edge AI technology" +} diff --git a/messages/en/modelSection.json b/messages/en/modelSection.json new file mode 100644 index 0000000..19fc8e9 --- /dev/null +++ b/messages/en/modelSection.json @@ -0,0 +1,27 @@ +{ + "llmModels": "Text Model List", + "imageModels": "Image Model List", + "videoModels": "Video Model List", + "price": "Price", + "pricePerMillion": "Per million tokens", + "pricePerImage": "Per image", + "pricePerVideo": "Per video", + "name": "Name", + "modelId": "Model ID", + "modelName": "Model Name", + "provider": "Provider", + "resolution": "Resolution", + "add": "Add", + "addModel": "Add Model", + "addNewModel": "Add New Model", + "selectPreset": "Select Preset Model", + "customModel": "Custom Model", + "confirmAdd": "Confirm", + "cancel": "Cancel", + "done": "Done", + "fillComplete": "Please fill in all fields", + "noModels": "No models yet, click the button above to add", + "noApiKey": "Configure API Key", + "batchMode": "Batch", + "batchModeTooltip": "Offline inference, 50% cheaper, completes within 24 hours" +} diff --git a/messages/en/nav.json b/messages/en/nav.json new file mode 100644 index 0000000..7e59c01 --- /dev/null +++ b/messages/en/nav.json @@ -0,0 +1,8 @@ +{ + "workspace": "Workspace", + "assetHub": "Asset Hub", + "profile": "Settings", + "signin": "Sign In", + "signup": "Sign Up", + "logout": "Logout" +} \ No newline at end of file diff --git a/messages/en/novel-promotion.json b/messages/en/novel-promotion.json new file mode 100644 index 0000000..3411bcc --- /dev/null +++ b/messages/en/novel-promotion.json @@ -0,0 +1,139 @@ +{ + "stages": { + "story": "Story", + "script": "Script", + "storyboard": "Storyboard", + "video": "Video", + "editor": "AI Editor", + "editorComingSoon": "Coming soon, follow us for updates" + }, + "buttons": { + "assetLibrary": "Asset Library", + "settings": "Settings", + "refreshData": "Refresh Data", + "enterVideoGeneration": "Enter Video Generation →" + }, + "smartImport": { + "title": "Start Your Creative Journey", + "subtitle": "First, choose your creation method", + "manualCreate": { + "title": "Create from Episode 1", + "description": "Start from episode 1, suitable for episodic creation or single short videos", + "button": "Start Creating" + }, + "smartImport": { + "title": "Smart Import Full Book", + "description": "Upload a complete novel or script, AI engine automatically recognizes chapter structure and splits into episodes.", + "button": "Import Now", + "recommended": "Recommended" + }, + "upload": { + "title": "Upload Raw Material", + "subtitle": "AI engine is ready, automatic episode splitting and formatting", + "maxWords": "(Max 30,000 words)", + "textInput": "Enter Text Content", + "documentUpload": "Upload Full Document", + "placeholder": "Paste your novel chapters or script content here...", + "filePlaceholder": "File uploaded mode", + "clickUpload": "Click to upload document", + "clearTextFirst": "Please clear left text first", + "supportedFormats": "Supports Word, TXT formats", + "preview": "Preview", + "expandPreview": "Expand More", + "collapsePreview": "Collapse", + "deleteFile": "Delete File", + "startAnalysis": "Start Smart Analysis", + "back": "Back", + "words": "words" + }, + "analyzing": { + "title": "AI is Analyzing Your Story", + "description": "Recognizing chapter structure, smart splitting in progress...", + "autoSave": "Will auto-save after analysis complete" + }, + "preview": { + "title": "Smart Splitting Complete", + "episodeCount": "Automatically split into {count} episodes", + "totalWords": "Total {count} words", + "autoSaved": "✓ Auto-saved", + "reanalyze": "Re-analyze", + "confirm": "Confirm Complete", + "saving": "Saving...", + "episodeList": "Episode List", + "addEpisode": "Add Episode", + "averageWords": "Average per episode", + "episodeContent": "Episode Content", + "episodePlaceholder": "Enter episode title...", + "summaryPlaceholder": "Enter plot summary...", + "newEpisode": "New Episode", + "deleteEpisode": "Delete Episode", + "deleteConfirm": { + "title": "Confirm Delete", + "message": "Are you sure you want to delete \"{title}\"?", + "cancel": "Cancel", + "confirm": "Confirm Delete" + }, + "tip": { + "title": "Tip", + "content": "You can directly edit titles, summaries, and content. After clicking [Confirm Complete], episodes will be officially imported into the project" + } + }, + "errors": { + "fileTooLarge": "File too large, please upload a file smaller than 10MB", + "docNotSupported": ".doc format not supported, please convert to .docx in Word", + "fileEmpty": "File content is empty", + "fileReadError": "File read failed, please try again", + "uploadFirst": "Please upload or paste content first", + "analyzeFailed": "Analysis failed", + "saveFailed": "Save failed" + }, + "cancelConfirm": "Are you sure you want to cancel? Analyzed episodes will be cleared." + }, + "storyInput": { + "currentEditing": "Currently editing: {name}", + "editingTip": "The following workflow is for this episode only. Switch episodes in the top left if needed", + "wordCount": "Word count:", + "assetLibraryTip": { + "title": "Need custom characters and locations?", + "description": "Click the 「Asset Library」 button in the top right to upload asset setting documents or manually add characters/locations. AI will prioritize using settings from the asset library for analysis." + }, + "videoRatio": "Video Ratio", + "visualStyle": "Visual Style", + "moreConfig": "For more configuration options, click the 「 Settings」 button in the top right", + "narration": { + "title": "Enable Narration Voiceover", + "description": "Generate TTS voice narration to add commentary to your video" + }, + "creating": "AI Creating...", + "ready": "✓ Configuration complete, ready for next step", + "pleaseInput": "Please enter script content first" + }, + "execution": { + "selectEpisode": "Please select an episode first", + "fillContentFirst": "Please enter content first", + "requestAborted": "Request aborted (possibly due to page refresh)", + "analysisFailed": "Asset analysis failed", + "prepareFailed": "Preparation failed", + "generationFailed": "Generation failed", + "batchVideoFailed": "Batch video generation failed", + "updateFailed": "Update failed", + "saveFailed": "Save failed", + "storyToScriptRunning": "Story→Script V2 running", + "scriptToStoryboardRunning": "Script→Storyboard V2 running", + "storyToScriptFailed": "Story to script failed", + "scriptToStoryboardFailed": "Script to storyboard failed", + "taskStreamTimeout": "Task timed out. The task may still be running in the background — please check its status or retry" + }, + "rebuildConfirm": { + "storyToScript": { + "title": "Script Flow Will Be Rebuilt", + "message": "Downstream storyboard data is detected for this episode ({storyboardCount} storyboards, {panelCount} panels). Continuing will clear and rebuild this data. Continue?" + }, + "scriptToStoryboard": { + "title": "Storyboard Data Will Be Rebuilt", + "message": "Existing storyboard data is detected for this episode ({storyboardCount} storyboards, {panelCount} panels). Continuing will clear current storyboards and regenerate them. Continue?" + }, + "confirm": "Continue and Clear", + "cancel": "Cancel" + } +} \ No newline at end of file diff --git a/messages/en/profile.json b/messages/en/profile.json new file mode 100644 index 0000000..dae3d86 --- /dev/null +++ b/messages/en/profile.json @@ -0,0 +1,107 @@ +{ + "user": "User", + "personalAccount": "Personal Account", + "availableBalance": "Available Balance", + "frozen": "Frozen", + "totalSpent": "Total Spent", + "apiConfig": "API Configuration", + "rechargeRecords": "Recharge Records", + "billingRecords": "Billing Records", + "logout": "Logout", + "accountTransactions": "Account Transactions", + "projectDetails": "Project Details", + "summary": "Summary", + "transactions": "Transactions", + "noTransactions": "No transaction records", + "noProjectCosts": "No project cost records", + "noDetails": "This project has no cost details", + "noRecords": "No records", + "byType": "By Type", + "byAction": "By Action", + "times": "times", + "total": "Total", + "filter": "Filter", + "allTypes": "All Types", + "recharge": "Account Recharge", + "consume": "Service Consumption", + "balanceAfter": "Balance {amount}", + "recordCount": "{count} records", + "totalCost": "Total {amount}", + "previousPage": "Previous", + "nextPage": "Next", + "pagination": "{total} items, Page {page} / {totalPages}", + "episodeLabel": "Episode {number}", + "billingDetail": { + "imageWithRes": "{count} images · {resolution}", + "image": "{count} images", + "videoWithRes": "{count} videos · {resolution}", + "video": "{count} videos", + "tokens": "{count} tokens", + "seconds": "{count}s", + "calls": "{count} calls" + }, + "apiTypes": { + "image": "Image Generation", + "video": "Video Generation", + "text": "Text Analysis", + "tts": "Text-to-Speech", + "voice": "Voice Acting", + "voice_design": "Voice Design", + "lip_sync": "Lip Sync" + }, + "actionTypes": { + "image_panel": "Storyboard Image", + "image_character": "Character Image", + "image_location": "Location Image", + "video_panel": "Video Generation", + "lip_sync": "Lip Sync", + "voice_line": "Voice Synthesis", + "voice_design": "Voice Design", + "asset_hub_voice_design": "Asset Hub Voice Design", + "regenerate_storyboard_text": "Regenerate Storyboard Text", + "insert_panel": "Insert Panel", + "panel_variant": "Shot Variant", + "modify_asset_image": "Modify Image", + "regenerate_group": "Batch Regenerate", + "asset_hub_image": "Asset Hub Image", + "asset_hub_modify": "Asset Hub Modify Image", + "analyze_novel": "Novel Analysis", + "story_to_script_run": "Story to Script", + "script_to_storyboard_run": "Script to Storyboard", + "clips_build": "Clips Build", + "screenplay_convert": "Screenplay Convert", + "voice_analyze": "Voice Analysis", + "analyze_global": "Global Analysis", + "ai_modify_appearance": "AI Modify Appearance", + "ai_modify_location": "AI Modify Location", + "ai_modify_shot_prompt": "AI Modify Shot Prompt", + "analyze_shot_variants": "Analyze Shot Variants", + "ai_create_character": "AI Create Character", + "ai_create_location": "AI Create Location", + "reference_to_character": "Reference to Character", + "character_profile_confirm": "Confirm Character Profile", + "character_profile_batch_confirm": "Batch Confirm Character Profiles", + "episode_split_llm": "Episode Split", + "asset_hub_ai_design_character": "Asset Hub AI Design Character", + "asset_hub_ai_design_location": "Asset Hub AI Design Location", + "asset_hub_ai_modify_character": "Asset Hub AI Modify Character", + "asset_hub_ai_modify_location": "Asset Hub AI Modify Location", + "asset_hub_reference_to_character": "Asset Hub Reference to Character", + "storyboard": "Storyboard", + "storyboard_candidate": "Storyboard Candidate", + "character": "Character Image", + "location": "Location Image", + "video": "Video", + "analyze": "Analysis", + "analyze_character": "Character Analysis", + "analyze_location": "Location Analysis", + "clips": "Clip Splitting", + "storyboard_text_plan": "Storyboard Planning", + "storyboard_text_detail": "Storyboard Detail", + "tts": "TTS", + "regenerate": "Regenerate", + "voice-generate": "Voice Generation", + "voice-design": "Voice Design", + "lip-sync": "Lip Sync" + } +} \ No newline at end of file diff --git a/messages/en/progress.json b/messages/en/progress.json new file mode 100644 index 0000000..53a9353 --- /dev/null +++ b/messages/en/progress.json @@ -0,0 +1,136 @@ +{ + "analyzing": "Analyzing story structure...", + "splittingClips": "Splitting into clips...", + "convertingScreenplay": "Converting to screenplay...", + "submittingStoryboard": "Submitting storyboard...", + "step": "Step {current} of {total}", + "status": { + "completed": "Completed", + "failed": "Failed", + "processing": "Processing", + "queued": "Queued", + "pending": "Pending" + }, + "stageCard": { + "stage": "Stage", + "realtimeStream": "Realtime Stream", + "currentStage": "Current Stage", + "outputTitle": "Live AI Output · {stage}", + "waitingModelOutput": "Waiting for model output...", + "reasoningNotProvided": "No reasoning was returned for this step" + }, + "runtime": { + "waitingExecution": "Waiting to start", + "taskCreated": "Task created", + "taskStarted": "Task started", + "taskCompleted": "Task completed", + "taskFailed": "Task failed", + "taskProcessing": "Task processing...", + "llm": { + "processing": "Model is processing...", + "output": "Model is generating output...", + "reasoning": "Model is reasoning...", + "completed": "Model output completed", + "failed": "Model output failed" + }, + "stage": { + "llmSubmit": "Submitting model request", + "llmStreaming": "Model streaming output", + "llmFallbackNonStream": "Model fallback to non-stream mode", + "llmCompleted": "Model output completed", + "llmFailed": "Model output failed" + } + }, + "taskType": { + "generic": "Task", + "imagePanel": "Storyboard image", + "imageCharacter": "Character image", + "imageLocation": "Location image", + "videoPanel": "Video generation", + "lipSync": "Lip sync", + "voiceLine": "Voice generation", + "voiceDesign": "Voice design", + "assetHubVoiceDesign": "Asset hub voice design", + "regenerateStoryboardText": "Regenerate storyboard text", + "insertPanel": "Insert storyboard panel", + "panelVariant": "Storyboard variant", + "modifyAssetImage": "Image edit", + "regenerateGroup": "Batch regenerate", + "assetHubImage": "Asset hub image", + "assetHubModify": "Asset hub edit", + "analyzeNovel": "Content analysis", + "storyToScriptRun": "Story to script", + "scriptToStoryboardRun": "Script to storyboard", + "clipsBuild": "Clip generation", + "screenplayConvert": "Screenplay conversion", + "voiceAnalyze": "Voice line analysis", + "analyzeGlobal": "Global analysis", + "aiModifyAppearance": "Character description modify", + "aiModifyLocation": "Location description modify", + "aiModifyShotPrompt": "Shot prompt modify", + "analyzeShotVariants": "Shot variant analysis", + "aiCreateCharacter": "Project character design", + "aiCreateLocation": "Project location design", + "referenceToCharacter": "Reference to character", + "characterProfileConfirm": "Character profile confirm", + "characterProfileBatchConfirm": "Character profile batch confirm", + "episodeSplitLlm": "Smart episode split", + "assetHubAiDesignCharacter": "Asset hub character design", + "assetHubAiDesignLocation": "Asset hub location design", + "assetHubAiModifyCharacter": "Asset hub character modify", + "assetHubAiModifyLocation": "Asset hub location modify", + "assetHubReferenceToCharacter": "Asset hub reference to character" + }, + "stage": { + "received": "Task received", + "generateCharacterImage": "Generate character image", + "generateLocationImage": "Generate location image", + "generatePanelCandidate": "Generate panel candidate", + "generatePanelVideo": "Generate panel video", + "generateVoiceSubmit": "Submit voice task", + "generateVoicePersist": "Persist voice result", + "voiceDesignSubmit": "Submit voice design task", + "voiceDesignDone": "Voice design completed", + "submitLipSync": "Submit lip sync task", + "persistLipSync": "Persist lip sync result", + "storyboardClip": "Generate storyboard clip", + "regenerateStoryboardPrepare": "Prepare storyboard regeneration", + "regenerateStoryboardPersist": "Persist storyboard regeneration", + "storyToScriptPrepare": "Prepare story-to-script parameters", + "storyToScriptStep": "Execute story-to-script step", + "storyToScriptPersist": "Persist story-to-script output", + "storyToScriptPersistDone": "Story-to-script output persisted", + "scriptToStoryboardPrepare": "Prepare script-to-storyboard parameters", + "scriptToStoryboardStep": "Execute script-to-storyboard step", + "scriptToStoryboardPersist": "Persist script-to-storyboard output", + "scriptToStoryboardPersistDone": "Storyboard and voice output persisted", + "insertPanelGenerateText": "Generate inserted panel text", + "insertPanelPersist": "Persist inserted panel", + "pollingExternal": "Waiting for external service", + "enqueueFailed": "Task enqueue failed", + "llmProxySubmit": "Submit LLM task", + "llmProxyExecute": "Execute LLM task", + "llmProxyPersist": "Persist LLM result" + }, + "runConsole": { + "storyToScript": "Story to Script", + "scriptToStoryboard": "Script to Storyboard", + "storyToScriptRunning": "Story→Script running", + "scriptToStoryboardRunning": "Script→Storyboard running", + "storyToScriptSubtitle": "Story To Script V2", + "scriptToStoryboardSubtitle": "Script To Storyboard V2", + "stop": "Stop", + "minimize": "Minimize" + }, + "streamStep": { + "analyzeCharacters": "Analyze characters", + "analyzeLocations": "Analyze locations", + "splitClips": "Split clips", + "screenplayConversion": "Convert screenplay", + "storyboardPlan": "Plan storyboard", + "cinematographyRules": "Generate cinematography rules", + "actingDirection": "Generate acting direction", + "storyboardDetailRefine": "Refine storyboard details", + "voiceAnalyze": "Analyze voice lines" + } +} diff --git a/messages/en/providerSection.json b/messages/en/providerSection.json new file mode 100644 index 0000000..4d2932a --- /dev/null +++ b/messages/en/providerSection.json @@ -0,0 +1,7 @@ +{ + "addProvider": "+ Add Provider", + "name": "Name", + "add": "Add", + "save": "Save", + "fillRequired": "Please fill in required fields" +} \ No newline at end of file diff --git a/messages/en/scriptView.json b/messages/en/scriptView.json new file mode 100644 index 0000000..b188e77 --- /dev/null +++ b/messages/en/scriptView.json @@ -0,0 +1,70 @@ +{ + "title": "Script View", + "scriptBreakdown": "Script Breakdown", + "splitCount": "{count} clips split", + "noClips": "No clips yet, please generate from story view", + "segment": { + "title": "Clip {index}", + "selected": "(Selected)" + }, + "inSceneAssets": "In-Scene Assets", + "currentSelected": "Selected: Clip {number}", + "assetView": { + "allClips": "All Clips", + "viewingClip": "Viewing Clip {number}" + }, + "asset": { + "generateCharacter": "Click to generate character →", + "generateLocation": "Click to generate location →", + "removeCharacterConfirm": "Are you sure you want to remove this character from current clip?", + "removeLocationConfirm": "Are you sure you want to remove this location from current clip?", + "removeFromClip": "Remove from current clip", + "noAudio": "No audio", + "playing": "Playing", + "listen": "Listen", + "activeCharacters": "Active Characters", + "activeLocations": "Active Locations", + "selectCharacter": "Select character/appearance to add", + "selectLocation": "Select location to add", + "loadingAssets": "Loading assets...", + "appearanceCount": "{count} appearances", + "added": "Added", + "primary": "Primary", + "subAppearance": "Sub appearance", + "defaultAppearance": "Default appearance", + "clickToRemove": "Click to remove {name}", + "clickToAdd": "Click to add {name}" + }, + "screenplay": { + "scene": "Scene {number}", + "location": "Location:", + "locationTime": "Time:", + "day": "Day", + "night": "Night", + "dawn": "Dawn", + "dusk": "Dusk", + "dialogue": "Dialogue", + "action": "Action", + "narration": "Narration", + "content": "Original Content", + "noContent": "No content yet", + "clickToEdit": "Click to edit", + "interior": "INT", + "exterior": "EXT", + "characters": "Characters", + "noCharacter": "No character info", + "noLocation": "No active locations", + "noCharacterInClip": "No active characters" + }, + "confirm": { + "removeCharacter": "Are you sure you want to remove this character from current clip?", + "removeLocation": "Are you sure you want to remove this location from current clip?" + }, + "generate": { + "missingAssets": "{count} assets missing images", + "missingAssetsTip": "Please generate images for all characters and locations in", + "missingAssetsTipLink": "first", + "generating": "Generating...", + "startGenerate": "Confirm and Start Drawing →" + } +} \ No newline at end of file diff --git a/messages/en/smartImport.json b/messages/en/smartImport.json new file mode 100644 index 0000000..48b5603 --- /dev/null +++ b/messages/en/smartImport.json @@ -0,0 +1,168 @@ +{ + "title": "Start Your Creative Journey", + "subtitle": "First, choose your creation method", + "manualCreate": { + "title": "Create from Episode 1", + "description": "Start from episode 1, suitable for episodic creation or single short videos", + "button": "Start Creating" + }, + "manualDesc": "Start from the first episode, suitable for serialized or single short video production", + "startCreate": "Start Creating", + "smartImport": { + "title": "Smart Text Split", + "description": "Upload a complete novel or script, AI engine automatically recognizes chapter structure and splits into episodes.", + "button": "Import Now", + "recommended": "Recommended" + }, + "markerDetection": { + "enable": "Use Markers (Episode X / 第X集)", + "tooltip": "Auto-detect [Episode X], [Chapter X], [第X集/章] markers, free & fast" + }, + "smartImportDesc": "Upload your novel or script, AI engine automatically identifies chapter structure for one-click smart episode splitting.", + "recommended": "Recommended", + "importNow": "Import Now", + "uploadTitle": "Upload Source Material", + "uploadSubtitle": "AI engine ready, one-click auto-split and format", + "maxWords": "Max 30,000 words", + "textInput": "Enter Text Content", + "textPlaceholder": "Paste your novel chapter or script content here...", + "uploadDoc": "Upload Complete Document", + "clickUpload": "Click to Upload", + "clearText": "Please clear left text first", + "supportFormat": "Supports Word, TXT formats", + "fileMax": "Max 30,000 words", + "words": "words", + "startAnalyzing": "Start Analysis", + "analyzing": { + "title": "AI is Analyzing Your Story", + "description": "Recognizing chapter structure, smart splitting in progress...", + "autoSave": "Will auto-save after analysis complete" + }, + "analyzingDesc": "Identifying chapter structure, smart splitting...", + "autoSave": "Will auto-save after analysis", + "splitComplete": "Smart Split Complete", + "splitResult": "Auto-split into {count} episodes, total {words} words", + "saved": "Auto-saved", + "reAnalyze": "Re-analyze", + "confirmComplete": "Confirm Complete", + "saving": "Saving...", + "episodeList": "Episode List", + "episodes": "episodes", + "episode": "Episode {num}", + "addEpisode": "Add Episode", + "newEpisode": "New Episode", + "avgWords": "Average per episode", + "episodeContent": "Episode Content", + "plotSummary": "Plot Summary", + "enterTitle": "Enter episode title...", + "enterSummary": "Enter plot summary...", + "confirmDelete": "Confirm Delete", + "deleteConfirmMsg": "Are you sure you want to delete \"{title}\"?", + "preview": { + "title": "Smart Splitting Complete", + "episodeCount": "Automatically split into {count} episodes", + "totalWords": "Total {count} words", + "autoSaved": "✓ Auto-saved", + "reanalyze": "Re-analyze", + "confirm": "Confirm Complete", + "saving": "Saving...", + "episodeList": "Episode List", + "addEpisode": "Add Episode", + "averageWords": "Average per episode", + "episodeContent": "Episode Content", + "episodePlaceholder": "Enter episode title...", + "summaryPlaceholder": "Enter plot summary...", + "newEpisode": "New Episode", + "deleteEpisode": "Delete Episode", + "deleteConfirm": { + "title": "Confirm Delete", + "message": "Are you sure you want to delete \"{title}\"?", + "cancel": "Cancel", + "confirm": "Confirm Delete" + }, + "tip": { + "title": "Tip", + "content": "You can directly edit titles, summaries, and content. After clicking [Confirm Complete], episodes will be officially imported into the project" + } + }, + "collapsePreview": "Collapse Preview", + "expandMore": "Expand More", + "deleteFile": "Delete File", + "fileTooLarge": "File size cannot exceed 10MB", + "docNotSupported": ".doc format not supported. Please save as .docx or .txt and try again", + "fileEmpty": "File content is empty", + "fileReadError": "File read failed, please ensure correct format", + "uploadFirst": "Please upload a file or paste text first", + "analyzeFailed": "Analysis failed", + "saveFailed": "Save failed", + "cancelConfirm": "Are you sure you want to cancel? Analyzed episodes will be cleared.", + "deleteEpisode": "Delete Episode", + "upload": { + "title": "Upload Raw Material", + "subtitle": "AI engine is ready, automatic episode splitting and formatting", + "maxWords": "(Max 30,000 words)", + "textInput": "Enter Text Content", + "documentUpload": "Upload Full Document", + "placeholder": "Paste your novel chapters or script content here...", + "filePlaceholder": "File uploaded mode", + "clickUpload": "Click to upload document", + "clearTextFirst": "Please clear left text first", + "supportedFormats": "Supports Word, TXT formats", + "preview": "Preview", + "expandPreview": "Expand More", + "collapsePreview": "Collapse", + "deleteFile": "Delete File", + "startAnalysis": "Start Analysis", + "back": "Back", + "words": "words" + }, + "errors": { + "fileTooLarge": "File too large, please upload a file smaller than 10MB", + "docNotSupported": ".doc format not supported, please convert to .docx in Word", + "fileEmpty": "File content is empty", + "fileReadError": "File read failed, please try again", + "uploadFirst": "Please upload or paste content first", + "analyzeFailed": "Analysis failed", + "saveFailed": "Save failed", + "analysisModelNotConfigured": "Please configure an analysis model in settings first" + }, + "common": { + "edit": "Edit", + "delete": "Delete", + "save": "Save", + "cancel": "Cancel" + }, + "markerDetected": { + "title": "Episode Markers Detected", + "description": "Detected {count} \"{type}\" format episode markers", + "preview": "Preview Split Result", + "useMarker": "Use Marker Split", + "useMarkerDesc": "Fast & Free", + "useAI": "Use AI Smart Split", + "useAIDesc": "Intelligent analysis, uses credits", + "cancel": "Cancel", + "totalCount": "{count} episodes total", + "markerTypes": { + "episode": "Episode X (Chinese)", + "chapter": "Chapter X (Chinese)", + "act": "Act X (Chinese)", + "scene": "X-Y [Scene]", + "numbered": "Numbered", + "numberedEscaped": "Numbered (Escaped)", + "numberedDirect": "Number + Chinese", + "episodeEn": "Episode X", + "chapterEn": "Chapter X", + "boldNumber": "**Number**", + "pureNumber": "Pure Number" + } + }, + "globalAnalysis": { + "title": "Global Asset Analysis", + "description": "Extract all characters and locations from the full book to ensure consistency across episodes", + "startButton": "Analyze Now", + "analyzing": "Analyzing...", + "success": "Analysis complete: {characters} new characters, {locations} new locations", + "failed": "Global analysis failed", + "confirmAndAnalyze": "Confirm & Analyze Assets" + } +} \ No newline at end of file diff --git a/messages/en/stages.json b/messages/en/stages.json new file mode 100644 index 0000000..2e87707 --- /dev/null +++ b/messages/en/stages.json @@ -0,0 +1,7 @@ +{ + "config": "1. Config", + "assets": "2. Asset Analysis", + "storyboard": "3. Storyboard Edit", + "videos": "4. Video Generation", + "voice": "5. Voice Generation" +} \ No newline at end of file diff --git a/messages/en/storyboard.json b/messages/en/storyboard.json new file mode 100644 index 0000000..8547b6d --- /dev/null +++ b/messages/en/storyboard.json @@ -0,0 +1,374 @@ +{ + "phases": { + "planning": "Planning Storyboard", + "cinematography": "Cinematography Design", + "acting": "Acting Direction", + "detail": "Adding Details" + }, + "prompts": { + "imagePrompt": "Image Prompt", + "aiInstruction": "AI Modify Instruction", + "supportReference": "(Support @ referencing asset library)", + "instructionPlaceholder": "e.g. Change location to @Hospital_Day, character to @ProtagonistA", + "selectAsset": "Select Asset", + "character": "Character", + "location": "Location", + "referencedAssets": "Referenced Assets:", + "removeAsset": "Remove Asset", + "aiModify": "AI Modify & Generate", + "aiModifying": "Modifying...", + "aiModifyTip": "Click to auto-save prompt and generate new image", + "save": "Save", + "currentPrompt": "Current Prompt", + "enterInstruction": "Please enter instruction", + "modifyFailed": "Operation Failed: {error}", + "updateFailed": "Update Failed: {error}", + "enterContinuation": "Please enter content to append", + "appendTitle": "Continue Content", + "appendDescription": "Enter new SRT content. The system will split and generate new shots, then append them to the end.", + "appendSubmit": "Append and Generate Shots", + "appendSuccess": "Append succeeded. New shots were added to the end of the list.", + "appendFailed": "Append failed: {error}", + "customStyle": "Custom Style" + }, + "group": { + "generating": "Generating...", + "hasSynced": "✓ Generated", + "failed": "Failed", + "retry": "Retry", + "regenerate": "Regenerate All", + "generateAll": "Generate All", + "expand": "Expand", + "collapse": "Collapse", + "addPanel": "Add Panel", + "regenerating": "Regenerating...", + "aiAnalyzing": "AI Analyzing...", + "regenerateText": "Regenerate Text", + "generateMissingImages": "Generate all panels without images in this segment", + "segment": "Segment", + "addAtStart": "Add new storyboard group at the start", + "insertHere": "Insert new storyboard group here" + }, + "header": { + "title": "Storyboard Editing", + "panels": "Panels", + "submit": "Submit Generation", + "submitting": "Submitting...", + "storyboardPanel": "Storyboard Panel", + "segments": "segments", + "segmentsCount": "Total {count} segments,", + "panelsCount": "{count} panels", + "generatingStatus": "({count} generating)", + "generateAllPanels": "Generate All Panels", + "generatePendingPanels": "Generate {count} panels without images", + "downloadAll": "Download All", + "downloading": "Packing...", + "noImages": "No images to download", + "downloadAllImages": "Download all images", + "generateVideo": "Generate Video →", + "back": "← Back", + "concurrencyLimit": "Concurrency limit {count}" + }, + "panel": { + "shotType": "Shot Type:", + "duration": "seconds", + "location": "Location:", + "characters": "Characters:", + "description": "Description:", + "text": "Corresponding Text:", + "regenerate": "Regenerate", + "delete": "Delete", + "insertBefore": "Insert Before", + "insertAfter": "Insert After", + "moveUp": "Move Up", + "moveDown": "Move Down", + "plot": "Plot:", + "summary": "Summary:", + "pov": "POV:", + "focus": "Focus:", + "mode": "Mode:", + "shot": "Shot", + "segment": "Segment", + "stylePrompt": "Style/Prompt", + "shotMode": "Shot/Mode", + "regenerateImage": "Regenerate Image", + "generateImage": "Generate Image", + "cardView": "Card View", + "tableView": "Table View", + "shotTypeLabel": "Shot Type", + "cameraMove": "Camera Move", + "sourceText": "Source Text", + "sceneDescription": "Scene Description", + "videoPrompt": "Video Prompt", + "videoPromptHint": "Describe subject movement, environment, and camera language", + "locationLabel": "Location", + "editLocation": "Edit Location", + "characterLabel": "Character", + "characterLabelWithCount": "Characters ({count})", + "editCharacter": "Edit Characters", + "select": "+ Select", + "add": "+ Add", + "noLocation": "No location selected", + "locationNotEdited": "Location not edited yet", + "noCharacters": "No characters selected", + "charactersNotEdited": "Characters not edited yet", + "shotTypePlaceholder": "Overhead medium shot...", + "cameraMovePlaceholder": "Slow push, static...", + "videoPromptPlaceholder": "Prompt for video generation...", + "sceneDescriptionPlaceholder": "Describe subject, composition, lighting, and mood", + "selectCharacter": "Select Character", + "selectLocation": "Select Location", + "noCharacterAssets": "No character assets", + "noLocationAssets": "No location assets", + "selected": "Selected", + "defaultAppearance": "Default appearance", + "newPanelDescription": "New shot description", + "noShotType": "Shot type not set" + }, + "image": { + "generating": "Generating...", + "regenerate": "Regenerate", + "edit": "Edit", + "editImage": "Edit Image", + "candidate": "Candidate", + "selectCandidate": "Select Candidate", + "variants": "Variants", + "generateVariants": "Generate Variants", + "forceRegenerate": "Force Regenerate", + "failed": "Generation Failed", + "clickToPreview": "Click to preview", + "enlargePreview": "Enlarge Preview", + "candidateCount": "Candidate {count}", + "candidateGenerating": "{count} generating", + "selectingCandidate": "Selecting candidate...", + "confirmCandidate": "Confirm Selection", + "cancelSelection": "Cancel Selection", + "noValidCandidates": "No valid candidates", + "selectCount": "Select count", + "generateMultiple": "Generate multiple candidates", + "generateCount": "Generate {count}", + "undoShort": "Back" + }, + "candidate": { + "title": "Select Candidate Image", + "select": "Select", + "cancel": "Cancel", + "noImages": "No candidate images", + "original": "Original" + }, + "variant": { + "title": "Image Variants", + "generate": "Generate Variants", + "select": "Use This Image", + "close": "Close", + "shotTitle": "Shot Variant - Based on #{number}", + "originalDescription": "Original Shot Description", + "noDescription": "No description", + "noImage": "No image", + "shotNum": "Shot {number}", + "aiRecommend": "AI Recommended Variants", + "reanalyze": "Re-analyze", + "shotType": "Shot type:", + "cameraMove": "Camera move:", + "generating": "Generating", + "clickToAnalyze": "Click Re-analyze to get AI recommendations", + "customInstruction": "Or custom instruction", + "customPlaceholder": "Enter the shot effect you want, e.g.: switch to reverse shot, focus on another character's expression...", + "includeCharacter": "Include character reference", + "includeLocation": "Include location reference", + "customVariant": "Custom variant", + "defaultShotType": "Medium Shot", + "defaultCameraMove": "Static", + "useCustomGenerate": "Generate with custom", + "analyzeFailed": "Analysis failed", + "creativeScore": "Creativity {score}/5" + }, + "insert": { + "title": "Insert New Panel", + "position": "Insert Position", + "before": "Before Panel {number}", + "after": "After Panel {number}", + "content": "Panel Content", + "shotType": "Shot Type", + "location": "Location", + "characters": "Characters", + "description": "Description", + "text": "Corresponding Text", + "placeholder": { + "shotType": "Select shot type...", + "location": "Enter location...", + "characters": "Enter characters, comma separated", + "description": "Describe the scene...", + "text": "Corresponding script text..." + }, + "insert": "Insert", + "cancel": "Cancel" + }, + "common": { + "actions": "Actions", + "add": "Add", + "cancel": "Cancel", + "confirm": "Confirm", + "copy": "Copy", + "delete": "Delete", + "download": "Download", + "edit": "Edit", + "generate": "Generate", + "loading": "Loading...", + "none": "None", + "unknownError": "Unknown error", + "preview": "Preview", + "refresh": "Refresh", + "regenerate": "Regenerate", + "deleting": "Deleting", + "editing": "Editing", + "saving": "Saving...", + "saveFailed": "Save failed, changes not synced", + "retrySave": "Retry save", + "save": "Save", + "status": "Status", + "submitFailed": "Submit Failed", + "upload": "Upload" + }, + "confirm": { + "deletePanel": "Delete this shot? This action cannot be undone.", + "deleteGroup": "Delete this storyboard group? This will remove all {count} shots in this segment. This action cannot be undone." + }, + "messages": { + "episodeNotFound": "Episode information not found", + "downloadFailed": "Download failed: {error}", + "panelNotFound": "Shot information not found", + "modifyFailed": "Modify failed: {error}", + "selectCandidateFailed": "Select candidate failed: {error}", + "insertPanelFailed": "Insert shot failed: {error}", + "addPanelFailed": "Add shot failed: {error}", + "deletePanelFailed": "Delete shot failed: {error}", + "deleteGroupFailed": "Delete storyboard group failed: {error}", + "regenerateGroupFailed": "Regenerate storyboard failed: {error}", + "addGroupFailed": "Add storyboard group failed: {error}", + "moveGroupFailed": "Move storyboard group failed: {error}", + "batchGenerateCompleted": "Batch generation completed:\nSucceeded: {succeeded}\nFailed: {failed}\n\nSample errors: {errors}", + "batchGenerateFailed": "Batch generation failed: {error}" + }, + "canvas": { + "emptyTitle": "No storyboard data yet", + "emptyDescription": "Generate clips and storyboard text first, or add a storyboard group above" + }, + "imageEdit": { + "title": "Edit Storyboard Image", + "subtitle": "Enter a modify instruction and optionally upload reference images or assets", + "promptPlaceholder": "Describe what to modify, e.g. change background color or adjust expression...", + "referenceImagesLabel": "Reference Images", + "referenceImagesHint": "(optional, paste supported)", + "start": "Start Editing", + "selectAsset": "Select Assets", + "selectedAssetsLabel": "Referenced Assets", + "selectedAssetsCount": "{count}", + "addAsset": "Add Asset", + "noAssets": "No assets selected. Click \"Add Asset\" to choose." + }, + "screenplay": { + "tabs": { + "formatted": "Screenplay", + "original": "Original" + }, + "scene": "Scene {number}", + "characters": "Characters", + "voiceover": "Voiceover", + "parseFailedTitle": "Failed to parse screenplay format", + "parseFailedDescription": "Please check the original content" + }, + "assets": { + "character": { + "confirming": "Confirming...", + "editing": "Editing..." + }, + "image": { + "undo": "Undo to Previous Version" + }, + "location": { + "generateImage": "Generate Image" + }, + "stage": { + "analyzing": "Analyzing..." + } + }, + "video": { + "toolbar": { + "showPending": "Pending" + }, + "panelCard": { + "forceRegenerate": "Force Regenerate (if stuck)" + } + }, + "smartImport": { + "errors": { + "analyzeFailed": "Analysis Failed" + }, + "preview": { + "reanalyze": "Re-analyze" + }, + "smartImport": { + "recommended": "Recommended" + } + }, + "aiData": { + "title": "AI Data Editor", + "subtitle": "Panel {number} - Complete data sent to image generation AI", + "basicData": "Storyboard Basic Data", + "shotType": "Shot Type", + "cameraMove": "Camera Movement", + "shotTypePlaceholder": "Overhead, wide shot, eye-level, medium shot...", + "cameraMovePlaceholder": "Slow push, static, follow...", + "scene": "Scene (Read-only)", + "notSelected": "Not selected", + "summary": "Scene Summary", + "characters": "Characters (Read-only)", + "plot": "Plot", + "summarize": "Summary", + "visualDescription": "Visual Description", + "videoPrompt": "Video Prompt", + "negativePrompt": "Negative Prompt", + "save": "Save", + "cancel": "Cancel", + "lightingDirection": "Lighting Direction", + "lightingQuality": "Lighting Quality", + "depthOfField": "Depth of Field", + "colorTone": "Color Tone", + "characterPosition": "Character Position Rules", + "position": "Position", + "posture": "Posture", + "facing": "Facing", + "photographyRules": "Photography Rules", + "viewData": "View Data", + "jsonPreview": "JSON Preview", + "actingNotes": "Acting Direction (acting_notes)", + "actingTitle": "Acting Direction", + "actingDescription": "Performance Notes", + "noActingData": "No acting data" + }, + "insertModal": { + "insertBetween": "Insert between #{before} and #{after}", + "panel": "Panel", + "noImage": "No image", + "insertAtEnd": "End", + "aiAnalyze": "AI Auto-analyze", + "analyzing": "AI analyzing...", + "insert": "Insert", + "inserting": "Inserting...", + "placeholder": "Optional: Add notes, e.g. add a reaction shot..." + }, + "panelActions": { + "insertPanel": "Insert Panel", + "panelVariant": "Panel Variant", + "insertHere": "Insert panel here", + "generateVariant": "Generate variant based on this panel", + "needImage": "Need to generate image first", + "deleteShot": "Delete Shot", + "pasteSrtPlaceholder": "Paste new SRT content..." + }, + "firstLastFrame": { + "placeholder": "Enter first/last frame video prompt...", + "modelTitle": "First/Last Frame Model" + } +} diff --git a/messages/en/video.json b/messages/en/video.json new file mode 100644 index 0000000..0b06ffb --- /dev/null +++ b/messages/en/video.json @@ -0,0 +1,210 @@ +{ + "panelCard": { + "play": "Play", + "pause": "Pause", + "retry": "Retry", + "regenerate": "Regenerate", + "download": "Download", + "edit": "Edit", + "save": "Save", + "cancel": "Cancel", + "generating": "Generating...", + "failed": "Failed", + "lipSync": "Lip Sync", + "lipSyncVideo": "Lip Sync Video", + "lipSyncLabel": "Lip Sync", + "lipSyncTitle": "Lip Sync", + "original": "Original", + "synced": "Synced", + "videoFixed": "✓ Video", + "imagePreview": "Image Preview", + "playVoice": "Play Voice", + "stopVoice": "Stop", + "noVoice": "No voice available", + "forceRegenerate": "Force Regenerate (use if stuck)", + "regenerateVideo": "Regenerate Video", + "lipSyncStatus": "Lip syncing...", + "lipSyncInProgress": "Lip syncing in progress...", + "lipSyncMayTakeMinutes": "This may take a few minutes", + "audioEnabled": "Audio enabled", + "audioDisabled": "Audio disabled", + "isSynced": "(Synced)", + "needVideo": "(Please generate video first)", + "needAudio": "(Please generate audio first)", + "generateAudio": "Generate Audio", + "regenerateLipSync": "Regenerate Lip Sync", + "editPrompt": "Edit Prompt", + "clickToEditPrompt": "Click to edit prompt...", + "shot": "Shot {number}", + "unknownShotType": "Unknown", + "correspondingText": "Corresponding Text", + "generateVideo": "Generate Video", + "selectModel": "Select Video Model", + "selectVoice": "Select voice to use:", + "willAutoPad": "(will auto-pad)", + "autoPadding": "Padding", + "redo": "Redo", + "generatingAudio": "Generating...", + "error": { + "audioFailed": "Audio generation failed" + }, + "batchMode": "Batch Mode", + "batchModeDesc": "Offline inference, 50% cheaper, completes within 24 hours", + "batchModeEnabled": "Batch mode enabled", + "batchModeDisabled": "Batch mode disabled" + }, + "promptModal": { + "title": "Edit Shot #{number} Video Prompt", + "shotType": "Shot Type:", + "duration": "s", + "location": "Location:", + "locationUnknown": "Unknown", + "characters": "Characters:", + "charactersNone": "None", + "description": "Description:", + "text": "Corresponding Text:", + "promptLabel": "Video Prompt", + "placeholder": "Enter video prompt...", + "tip": "Tip: Video models don't recognize character names, use appearance descriptions like \"young man with black hair and blue eyes\" instead of \"Victor\"", + "save": "Save", + "cancel": "Cancel" + }, + "toolbar": { + "title": "Video Generation", + "filter": "Filter", + "viewAll": "View All", + "showGenerated": "Generated", + "showPending": "Pending", + "showFailed": "Failed", + "totalShots": "Total {count} shots", + "generatingShots": "{count} generating", + "completedShots": "{count} completed", + "failedShots": "{count} failed", + "generateAll": "Generate All Videos", + "batchConfigTitle": "Batch Generation Settings", + "batchConfigDesc": "Choose model and parameters before generating all videos.", + "confirmGenerateAll": "Confirm and Generate All", + "confirming": "Submitting...", + "noVideos": "No videos to download", + "downloadCount": "Download {count} videos", + "packing": "Packing...", + "downloadAll": "Download All", + "enterEditor": "Enter Video Editor", + "enterEdit": "Enter Editor", + "back": "Back" + }, + "stage": { + "title": "Video Generation", + "generateAll": "Generate All", + "regenerateFailed": "Retry Failed", + "downloadAll": "Download All Videos", + "enterEditor": "Enter Editor", + "lipSyncStatus": "Lip syncing...", + "hasSynced": "✓ Generated", + "generating": "Generating...", + "downloading": "Downloading...", + "downloadProgress": "Preparing video files... {current}/{total}", + "noVideos": "No generated videos", + "scrollTo": "Jump to shot", + "error": { + "saveFailed": "Failed to save video prompt", + "lipSyncFailed": "Lip sync failed", + "fetchVideosFailed": "Failed to fetch video list" + }, + "downloadFailed": "Download failed", + "unknownError": "Unknown error" + }, + "firstLastFrame": { + "title": "First/Last Frame Settings", + "firstFrame": "First Frame", + "lastFrame": "Last Frame", + "range": "Shot {from} → Shot {to}", + "link": "Link", + "unlink": "Unlink", + "unlinkAction": "Unlink", + "asLastFrameFor": "As last frame for Shot {number}", + "asFirstFrameFor": "As first frame for Shot {number}", + "customPrompt": "Custom Prompt", + "promptPlaceholder": "Enter first/last frame video prompt...", + "useDefault": "Use Default", + "generate": "Generate First/Last Frame Video", + "generated": "First/Last Frame Video Generated", + "model": "Model", + "withAudio": "With Audio", + "audioOn": "On", + "audioOff": "Off", + "linkToNext": "Link to next panel (First/Last Frame)", + "asLastFrame": "As last frame for Panel {number}", + "thenTransitionTo": "then transition to" + }, + "editor": { + "alert": { + "saveSuccess": "Saved successfully", + "saveFailed": "Save failed", + "exportStarted": "Export has started. Please wait...", + "exportFailed": "Export failed" + }, + "toolbar": { + "back": "← Back", + "saveDirty": "Save *", + "saved": "Saved", + "export": "Export Video" + }, + "left": { + "title": "Media Library", + "description": "Clips imported from the video stage will appear here" + }, + "right": { + "title": "Properties", + "clipLabel": "Clip:", + "clipFallback": "Clip {index}", + "durationLabel": "Duration:", + "transitionLabel": "Transition to Next Clip", + "deleteConfirm": "Delete this clip?", + "deleteClip": "Delete Clip", + "selectClipHint": "Select a clip to view properties" + }, + "preview": { + "emptyStartEditing": "Add media to start editing" + }, + "timeline": { + "zoomLabel": "Zoom:", + "videoTrack": "Video", + "emptyHint": "Drag video clips from media library to here", + "audioTrack": "Voice", + "audioBadge": "A" + }, + "transition": { + "title": "Transition", + "duration": "Duration", + "options": { + "none": "None", + "dissolve": "Dissolve", + "fade": "Fade", + "slide": "Slide" + } + } + }, + "errors": { + "unknownError": "Unknown error" + }, + "capability": { + "generationMode": "Generation mode", + "generateAudio": "Generate audio", + "duration": "Duration", + "fps": "Frame rate", + "resolution": "Resolution", + "aspectRatio": "Aspect ratio", + "reasoningEffort": "Reasoning effort", + "voice": "Voice", + "rate": "Rate", + "mode": "Mode" + }, + "unit": { + "second": "s", + "frame": "fps" + }, + "common": { + "generate": "Generate" + } +} \ No newline at end of file diff --git a/messages/en/voice.json b/messages/en/voice.json new file mode 100644 index 0000000..65e9ad3 --- /dev/null +++ b/messages/en/voice.json @@ -0,0 +1,254 @@ +{ + "title": "Voice Lines", + "linesCount": "{count} lines total, ", + "audioGeneratedCount": "{count} audio generated", + "emotionPrompt": "Emotion Prompt", + "emotionPromptTip": "(Leave empty for auto-reference)", + "emotionPlaceholder": "e.g. laugh, English only...", + "emotionStrength": "Emotion Strength", + "flat": "Flat", + "intense": "Intense", + "generating": "Generating...", + "generateVoice": "Generate Voice", + "toolbar": { + "back": "← Back", + "analyzeLines": "Analyze Lines", + "addLine": "+ Add Voice", + "generateAll": "Generate All Voices", + "downloadAll": "Download Voices", + "generatingCount": "Generating ({count})", + "packing": "Packing...", + "stats": "{total} lines | {withVoice} with voice | {withAudio} generated", + "noDownload": "No voices to download", + "downloadCount": "Download {count} voices", + "uploadReferenceHint": "Please upload reference audio for all characters first" + }, + "speakerVoice": { + "title": "Speaker Voice Status", + "hint": "Please upload reference audio for characters in Asset Library", + "linesCount": "{count} lines", + "noVoice": "No reference voice", + "configured": "✓ Configured", + "playVoice": "Play current voice", + "aiDesign": "AI Design Voice", + "aiDesignVoice": "AI Design Voice", + "redesign": "Redesign voice with AI", + "uploadAudio": "Upload audio", + "uploading": "Uploading", + "upload": "Upload", + "microsoftVoice": "Microsoft Voice", + "microsoft": "MS", + "maleVoices": "Male", + "femaleVoices": "Female", + "openAssetLibrary": "Asset Library", + "configuredStatus": "Voice Set", + "pendingStatus": "Voice Pending", + "voiceSettings": "Voice Settings", + "inlineLabel": "Inline" + }, + "inlineBinding": { + "title": "Set voice for \"{speaker}\"", + "description": "This speaker is not in the asset library. Choose a method to set a reference voice.", + "selectFromLibrary": "Select from Voice Library", + "selectFromLibraryDesc": "Choose an existing global voice", + "uploadAudio": "Upload Reference Audio", + "uploadAudioDesc": "Upload MP3, WAV or other audio files as reference voice", + "aiDesign": "AI Design Voice", + "aiDesignDesc": "Use AI to generate an exclusive reference voice" + }, + "embedded": { + "linesStats": "{total} lines · {audio} generated", + "reanalyze": "Re-analyze", + "analyzeLines": "Analyze Lines", + "reanalyzeHint": "Re-analyze lines and update shot matching", + "analyzeHint": "Extract lines from script", + "downloadVoice": "Download Voices", + "generateAllVoice": "Generate All Voices", + "pendingCount": "({count} pending)", + "generatingProgress": "Generating ({current}/{total})", + "generatingHint": "Generating...", + "noVoiceHint": "Please set voice for all characters above first", + "noLinesHint": "No lines to generate", + "allDoneHint": "All lines generated", + "generateHint": "Click to generate {count} pending voices", + "addLine": "+ Add Voice", + "speakerVoiceStatus": "Speaker Voice Status", + "speakersCount": "{count}", + "listen": "Listen", + "listenVoice": "Listen to voice", + "reset": "Reset", + "resetDesign": "Redesign", + "aiDesign": "AI Design", + "assetLibrary": "Asset Library" + }, + "lineCard": { + "generatingVoice": "Generating", + "speaker": "Speaker", + "speakerPlaceholder": "Speaker name", + "content": "Content", + "contentPlaceholder": "Line content", + "emotionConfigured": "Emotion set", + "emotionSettings": "Emotion Settings", + "voiceConfigured": "✓ Configured", + "needVoice": "Please set voice above", + "locatePanel": "Locate bound shot", + "locateVideo": "Locate Video", + "play": "Play", + "pause": "Pause", + "locatePanelCta": "Jump to Shot {index}", + "editLine": "Edit line", + "deleteLine": "Delete line", + "deleteAudio": "Delete audio" + }, + "lineEditor": { + "addTitle": "Add Voice Line", + "editTitle": "Edit Voice Line", + "contentLabel": "Line Content", + "contentPlaceholder": "Enter line content", + "speakerLabel": "Speaker", + "speakerPlaceholder": "Enter speaker name", + "selectSpeaker": "Select a speaker", + "noSpeakerOptions": "No available speakers in this project yet. Analyze lines first.", + "bindPanelLabel": "Bind Shot", + "unboundPanel": "Unbound", + "panelLabel": "Shot {index}", + "saveAdd": "Add Voice", + "saveEdit": "Save Changes" + }, + "empty": { + "title": "No Voice Lines", + "description": "Extract lines and speakers from script", + "analyzeButton": "Analyze Lines", + "hint": "Please upload reference audio for characters in Asset Library first" + }, + "confirm": { + "deleteLine": "Are you sure you want to delete this line?\n\n\"{content}\"\n\nThis action cannot be undone.", + "deleteAudio": "Are you sure you want to delete this audio?\n\n\"{content}\"\n\nThis action cannot be undone." + }, + "errors": { + "saveFailed": "Save Failed", + "analyzeFailed": "Analysis Failed", + "generateFailed": "Generation Failed", + "batchFailed": "Batch Generation Failed", + "downloadFailed": "Download Failed", + "deleteFailed": "Delete Failed", + "addFailed": "Add Voice Failed", + "invalidLineInput": "Content and speaker cannot be empty", + "bindFailed": "Bind shot failed", + "deleteAudioFailed": "Delete Audio Failed", + "uploadFailed": "Upload Failed", + "voiceDesignFailed": "Voice Design Save Failed", + "emotionSaveFailed": "Emotion Settings Save Failed", + "voiceGenerateFailed": "Voice Generation Failed" + }, + "alerts": { + "insufficientBalance": "Insufficient Balance", + "insufficientBalanceMsg": "Account balance insufficient, please top up to continue", + "noLinesToGenerate": "No lines to generate (please upload reference audio for characters first)", + "generateComplete": "Complete: {success}/{total} successful", + "generateFailed": "{count} failed", + "speakerVoiceSet": "Reference audio set for {speaker}", + "speakerVoiceUploaded": "Reference audio uploaded for {speaker}", + "voiceDesignSet": "AI designed voice set for {speaker}" + }, + "common": { + "loading": "Loading...", + "save": "Save", + "cancel": "Cancel", + "cancelling": "Cancelling...", + "upload": "Upload", + "download": "Download", + "generate": "Generate", + "regenerate": "Regenerate" + }, + "assets": { + "image": { + "uploadFailed": "Upload Failed" + }, + "stage": { + "analyzing": "Analyzing..." + } + }, + "smartImport": { + "errors": { + "analyzeFailed": "Analysis Failed" + } + }, + "video": { + "panelCard": { + "play": "Play" + } + }, + "tts": { + "generatedAudio": "Generated Audio", + "browserNotSupport": "Your browser does not support audio playback", + "audioDuration": "Audio Duration:", + "subtitleCount": "Subtitle Count:", + "noAudio": "No audio", + "srtPreview": "SRT Subtitle Preview", + "noSubtitle": "No subtitles", + "stats": "Generation Stats", + "minute": "min", + "second": "sec", + "items": "items", + "completed": "✓ Completed", + "regenerating": "Regenerating...", + "regenerateTTS": "Regenerate TTS", + "nextStep": "Next: Analyze Assets", + "readyTip": "Click to proceed to asset analysis", + "needGenerate": "Please generate TTS audio first" + }, + "voiceCreate": { + "aiDesignMode": "AI Design Voice", + "uploadMode": "Upload Audio", + "dropOrClick": "Drop file or click to select", + "supportedFormats": "Supports MP3, WAV, OGG, M4A, AAC formats", + "invalidFileType": "Unsupported file format. Please upload an audio file", + "fileTooLarge": "File too large. Maximum 50MB supported", + "previewAudio": "Preview Audio", + "uploading": "Uploading...", + "uploadFailed": "Upload failed", + "uploadSuccess": "Upload successful" + }, + "voiceDesign": { + "presets": { + "maleBroadcaster": "Male Broadcaster", + "gentleFemale": "Gentle Female", + "matureMale": "Mature Male", + "livelyFemale": "Lively Girl", + "intellectualFemale": "Intellectual Female", + "narrator": "Narrator" + }, + "presetsPrompts": { + "maleBroadcaster": "Middle-aged male broadcaster with steady, deep voice, clear pronunciation", + "gentleFemale": "Gentle and sweet young woman with clear, melodious voice", + "matureMale": "Mature male with charismatic and expressive voice", + "livelyFemale": "Lively young girl with sweet, cute, energetic voice", + "intellectualFemale": "Elegant intellectual woman with clear, pleasant voice", + "narrator": "Emotional narrator with warm, storytelling voice" + }, + "defaultPreviewText": "Hello, nice to meet you. This is your AI-designed exclusive voice. Let me demonstrate its features for you. Whether it's a gentle conversation or an excited narration, I can deliver it perfectly. I hope you enjoy this voice, let's create amazing content together.", + "pleaseSelectStyle": "Please enter or select a voice style", + "designVoiceFor": "Design AI Voice for \"{speaker}\"", + "hasExistingVoice": "Has voice", + "selectStyle": "Select voice style:", + "orCustomDescription": "Or custom description:", + "describePlaceholder": "Describe voice characteristics: age, gender, tone, pitch...", + "editPreviewText": "Edit preview text", + "generate3Schemes": "Generate 3 Voice Schemes", + "generating3Schemes": "Generating 3 voice schemes...", + "estimatedTime": "Est. 15-30 seconds", + "selectScheme": "Select voice scheme:", + "schemeN": "Scheme {n}", + "regenerate": "Regenerate", + "confirmUse": "✓ Confirm Use", + "confirmReplace": "Confirm Replace Voice?", + "replaceWarning": "'s original voice, irreversible", + "confirmReplaceBtn": "Confirm Replace", + "noVoiceGenerated": "No voice generated", + "generationError": "Voice generation failed", + "generateFailed": "Failed to generate voice {n}", + "preview": "Preview", + "playing": "Playing" + } +} \ No newline at end of file diff --git a/messages/en/workspace.json b/messages/en/workspace.json new file mode 100644 index 0000000..cf64a97 --- /dev/null +++ b/messages/en/workspace.json @@ -0,0 +1,31 @@ +{ + "title": "My Projects", + "subtitle": "Manage your AI anime production projects", + "newProject": "New Project", + "searchPlaceholder": "Search project name or description...", + "searchButton": "Search", + "clearButton": "Clear", + "updatedAt": "Updated at", + "noProjects": "No projects yet", + "noProjectsDesc": "Create your first AI anime production project", + "noResults": "No matching projects found", + "noResultsDesc": "Try using different search terms", + "createProject": "New Project", + "editProject": "Edit Project", + "deleteProject": "Delete Project", + "deleteConfirm": "Are you sure you want to delete project \"{name}\"? This action cannot be undone.", + "projectName": "Project Name", + "projectNamePlaceholder": "Enter project name", + "projectDescription": "Project Description (Optional)", + "projectDescriptionPlaceholder": "Enter project description", + "creating": "Creating...", + "saving": "Saving...", + "createFailed": "Failed to create project", + "updateFailed": "Failed to update project", + "deleteFailed": "Failed to delete project", + "totalProjects": "{count} projects in total", + "statsEpisodes": "Episodes", + "statsImages": "Images", + "statsVideos": "Videos", + "noContent": "No content yet" +} diff --git a/messages/en/workspaceDetail.json b/messages/en/workspaceDetail.json new file mode 100644 index 0000000..67cbf5f --- /dev/null +++ b/messages/en/workspaceDetail.json @@ -0,0 +1,24 @@ +{ + "globalAssets": "Global Assets", + "createFailed": "Creation failed", + "deleteFailed": "Deletion failed", + "renameFailed": "Rename failed", + "refreshFailed": "Refresh failed", + "projectNotFound": "Project not found", + "backToWorkspace": "Back to Workspace", + "episode": "Episode", + "sidebar": { + "dragToMove": "Drag to move position", + "listTitle": "Episode List", + "episodeCount": "{count} episodes", + "empty": "No episodes yet. Create one below.", + "save": "Save", + "deleteConfirm": "Delete \"{name}\"?", + "delete": "Delete", + "cancel": "Cancel", + "rename": "Rename", + "newEpisodePlaceholder": "Enter episode name...", + "create": "Create", + "addEpisode": "Add Episode" + } +} diff --git a/messages/en/worldContextModal.json b/messages/en/worldContextModal.json new file mode 100644 index 0000000..492e64a --- /dev/null +++ b/messages/en/worldContextModal.json @@ -0,0 +1,6 @@ +{ + "title": "World & Character Settings", + "description": "Define global character appearances, scene styles, and environment descriptions", + "placeholder": "Example:\n【Protagonist】John, 25, short black hair, always wearing a faded denim jacket, melancholy eyes.\n【Heroine】Jane, 22, red twin tails, lively personality, likes to wear Lolita-style dresses.\n【Scene】Cyberpunk city in 2077, neon lights flashing, raining all year round...", + "hint": "These settings will be inherited by all episodes as a reference for AI drawing." +} \ No newline at end of file diff --git a/messages/zh/actions.json b/messages/zh/actions.json new file mode 100644 index 0000000..3a42c6f --- /dev/null +++ b/messages/zh/actions.json @@ -0,0 +1,18 @@ +{ + "storyboard": "分镜图", + "storyboard_candidate": "分镜候选", + "character": "角色图", + "location": "场景图", + "video": "视频", + "analyze": "分析", + "analyze_character": "角色分析", + "analyze_location": "场景分析", + "clips": "片段切割", + "storyboard_text_plan": "分镜规划", + "storyboard_text_detail": "分镜细节", + "tts": "语音合成", + "regenerate": "重生成", + "voice-generate": "配音生成", + "voice-design": "声音设计", + "lip-sync": "口型同步" +} \ No newline at end of file diff --git a/messages/zh/apiConfig.json b/messages/zh/apiConfig.json new file mode 100644 index 0000000..5c27430 --- /dev/null +++ b/messages/zh/apiConfig.json @@ -0,0 +1,107 @@ +{ + "title": "API 配置", + "saving": "保存中...", + "saved": "已保存", + "saveFailed": "保存失败", + "connected": "已连接", + "notConfigured": "未配置 Key", + "configure": "配置", + "connect": "连接", + "show": "显示", + "hide": "隐藏", + "capability": "能力", + "default": "默认", + "delete": "删除", + "add": "添加", + "cancel": "取消", + "save": "保存", + "comingSoon": "待上线", + "priceInput": "输入 {amount}", + "priceOutput": "输出 {amount}", + "priceUnavailable": "暂无定价", + "fillComplete": "请填写完整信息", + "fillPricing": "请填写价格信息", + "pricingInputLabel": "输入价格", + "pricingOutputLabel": "输出价格", + "modelIdExists": "模型 ID 已存在", + "modelDisplayName": "外显名称(你自己看的)", + "modelActualId": "实际调用 ID(模型参数)", + "noModelsForProvider": "该厂商暂无配置模型", + "defaultModels": "默认模型配置", + "textDefault": "文本模型", + "characterDefault": "角色模型", + "locationDefault": "场景模型", + "storyboardDefault": "分镜模型", + "editDefault": "修图模型", + "videoDefault": "视频模型", + "lipsyncDefault": "口型同步模型", + "selectDefault": "请选择", + "providerPool": "厂商资源池", + "providerIdExists": "该提供商 ID 已存在", + "presetProviderCannotDelete": "预设提供商不能删除", + "confirmDeleteProvider": "确定删除这个提供商吗?", + "presetModelCannotDelete": "预设模型不能删除", + "confirmDeleteModel": "确定删除这个模型吗?", + "addGeminiProvider": "新增模型服务商", + "baseUrl": "Base URL", + "configureBaseUrl": "配置地址", + "addModel": "添加模型", + "batchModeHalfPrice": "批量模式(价格减半)", + "typeText": "文本", + "typeImage": "图像", + "typeVideo": "视频", + "typeAudio": "音频等", + "apiKeyLabel": "API Key", + "apiType": "API 类型", + "apiTypeGeminiCompatible": "Gemini 兼容", + "apiTypeOpenAICompatible": "OpenAI 兼容", + "apiTypeGeminiHint": "使用 Google SDK", + "otherProviders": "其他配置", + "audioCategory": "音频", + "audioAndLipsync": "语音与唇形同步", + "configureApiKey": "配置 API Key", + "enterApiKey": "请输入 API Key", + "tabs": { + "llm": "文本模型", + "image": "图片模型", + "video": "视频模型", + "audio": "音频等模型", + "other": "其他" + }, + "sections": { + "llmApiKeys": "文本模型 API Keys", + "imageApiKeys": "图片模型 API Keys", + "videoApiKeys": "视频模型 API Keys", + "audioApiKey": "音频模型 API Key", + "lipsyncApiKey": "唇形同步 API Key" + }, + "defaultModel": { + "title": "默认模型", + "hint": "新建项目和资产库将使用此默认配置", + "notSelected": "未选择", + "analysis": "分析模型", + "image": "图片生成", + "video": "视频生成", + "resolution": "图片分辨率" + }, + "viewTutorial": "查看教程", + "tutorial": { + "button": "开通教程", + "title": "开通教程", + "subtitle": "按照以下步骤完成配置", + "close": "我知道了", + "openLink": "点击打开", + "steps": { + "ark_step1": "进入火山引擎控制台,开通 API Key", + "ark_step2": "在模型管理页面,点击右上角「一键开通所有模型」", + "openrouter_step1": "进入 OpenRouter 平台,创建 API Key(必须选择带有图像功能的模型)", + "fal_step1": "进入 FAL 平台,创建 API Key", + "google_step1": "进入 Google AI Studio,创建 API Key", + "minimax_step1": "进入海螺 MiniMax 平台,获取 API Key", + "vidu_step1": "进入生数科技 Vidu 平台,点击「新建 API Key」", + "openai_compatible_step1": "填写任意 OpenAI 协议兼容服务的 Base URL 与 API Key", + "gemini_compatible_step1": "填写任意 Gemini 协议兼容服务的 Base URL 与 API Key", + "qwen_step1": "进入阿里云百炼平台,获取 API Key" + } + } +} diff --git a/messages/zh/apiTypes.json b/messages/zh/apiTypes.json new file mode 100644 index 0000000..6ec26fb --- /dev/null +++ b/messages/zh/apiTypes.json @@ -0,0 +1,9 @@ +{ + "image": "图片生成", + "video": "视频生成", + "text": "文本分析", + "tts": "语音合成", + "voice": "配音", + "voice_design": "声音设计", + "lip_sync": "口型同步" +} \ No newline at end of file diff --git a/messages/zh/assetHub.json b/messages/zh/assetHub.json new file mode 100644 index 0000000..6e584bc --- /dev/null +++ b/messages/zh/assetHub.json @@ -0,0 +1,101 @@ +{ + "title": "资产中心", + "description": "管理您的全局角色和场景资产", + "modelHint": "资产中心使用默认模型,如需修改请在", + "modelHintLink": "API配置页面", + "modelHintSuffix": "进行设置", + "folders": "文件夹", + "noFolders": "暂无文件夹", + "allAssets": "所有资产", + "characters": "角色", + "locations": "场景", + "voices": "音色", + "addCharacter": "新建角色", + "addLocation": "新建场景", + "addVoice": "新建音色", + "newFolder": "新建文件夹", + "editFolder": "编辑文件夹", + "deleteFolder": "删除文件夹", + "folderName": "文件夹名称", + "folderNamePlaceholder": "请输入文件夹名称", + "emptyState": "暂无资产", + "emptyStateHint": "点击上方按钮添加角色或场景", + "generate": "生成图片", + "generating": "生成中...", + "regenerate": "重新生成", + "undo": "撤回", + "delete": "删除", + "cancel": "取消", + "save": "保存", + "create": "创建", + "confirmDeleteFolder": "确定删除此文件夹吗?文件夹内的资产将移至未分类。", + "confirmDeleteCharacter": "确定删除此角色吗?此操作无法撤回。", + "confirmDeleteLocation": "确定删除此场景吗?此操作无法撤回。", + "confirmDeleteVoice": "确定删除此音色吗?此操作无法撤回。", + "voiceName": "音色名称", + "voiceNamePlaceholder": "请输入音色名称", + "voiceNameRequired": "请输入音色名称", + "voicePickerTitle": "从音色库选择", + "voicePickerEmpty": "暂无音色,请先创建音色", + "voicePickerConfirm": "确认选择", + "pagination": { + "previous": "上一页", + "next": "下一页" + }, + "common": { + "cancel": "取消" + }, + "generateFailed": "生成失败", + "selectFailed": "选择失败", + "uploadFailed": "上传失败", + "editFailed": "编辑失败", + "saveVoiceFailed": "保存声音失败", + "saveVoiceFailedDetail": "保存声音失败: {error}", + "bindVoiceFailed": "绑定音色失败", + "bindVoiceFailedDetail": "绑定音色失败: {error}", + "voiceDesignSaved": "已为 {name} 设置 AI 设计的声音", + "appearanceLabel": "形象 {index}", + "voiceSettings": { + "title": "配音音色", + "noVoice": "无音色", + "previewFailed": "预览失败: {error}", + "uploadFailed": "上传音频失败: {error}", + "uploading": "上传中...", + "uploaded": "已上传", + "uploadAudio": "上传音频", + "aiDesign": "AI设计", + "voiceLibrary": "音色库", + "pause": "暂停", + "preview": "试听音色" + }, + "modal": { + "newCharacter": "新建角色", + "confirm": "确定", + "processing": "处理中...", + "newLocation": "新建场景", + "addCharacter": "创建角色", + "addLocation": "创建场景", + "adding": "创建中...", + "aiDesign": "AI 设计", + "aiDesignPlaceholder": "例如:一个身穿红色长裙的古风美女,长发飘飘,手持折扇", + "aiDesignLocationPlaceholder": "例如:一个古典中式园林,有假山流水和亭台楼阁", + "aiDesignTip": "AI 将根据您的需求生成详细的描述,您可以在生成后编辑", + "aiDesignLocationTip": "AI 将根据您的需求生成详细的场景描述", + "generate": "生成", + "generating": "生成中...", + "nameLabel": "角色名称", + "namePlaceholder": "请输入角色名称", + "descLabel": "角色描述", + "descPlaceholder": "请描述角色的外形特征、服装、发型等...", + "locationNameLabel": "场景名称", + "locationNamePlaceholder": "请输入场景名称", + "locationSummaryLabel": "场景描述", + "locationSummaryPlaceholder": "请描述场景的环境、氛围、特征等...", + "referenceUpload": "上传参考图", + "referenceUploadTip": "上传一张角色图片,AI 将自动转换为三视图设定图", + "convertToCharacter": "转换三视图", + "converting": "转换中...", + "dropOrClick": "拖放图片或点击上传", + "supportedFormats": "支持 JPG、PNG 格式" + } +} diff --git a/messages/zh/assetLibrary.json b/messages/zh/assetLibrary.json new file mode 100644 index 0000000..14abe46 --- /dev/null +++ b/messages/zh/assetLibrary.json @@ -0,0 +1,14 @@ +{ + "title": "资产库", + "button": "资产库", + "characters": "角色", + "locations": "场景", + "noCharacters": "暂无角色", + "noLocations": "暂无场景", + "addCharacter": "添加角色", + "addLocation": "添加场景", + "generateImage": "生成图片", + "regenerateImage": "重新生成", + "analyzeAssets": "分析资产", + "analyzing": "分析中..." +} \ No newline at end of file diff --git a/messages/zh/assetModal.json b/messages/zh/assetModal.json new file mode 100644 index 0000000..e26d47f --- /dev/null +++ b/messages/zh/assetModal.json @@ -0,0 +1,78 @@ +{ + "character": { + "title": "新建角色", + "name": "角色名称", + "namePlaceholder": "请输入角色名称", + "modeReference": "参考图模式", + "modeDescription": "描述模式", + "isSubAppearance": "这是一个子形象", + "isSubAppearanceHint": "为已有角色添加新的形象状态", + "uploadReference": "上传参考图", + "pasteHint": "Ctrl+V 粘贴", + "dropOrClick": "点击上传或拖拽图片", + "supportedFormats": "支持 JPG、PNG 等格式", + "nameRequired": "请先输入角色名称才能使用参考图转换", + "convertToSheet": "将参考图转换为标准三视图", + "referenceTip": "上传任意角色图片,AI 将自动生成标准三视图设定", + "description": "角色描述", + "modifyDescription": "修改描述", + "descPlaceholder": "请输入角色外貌描述...", + "modifyDescriptionPlaceholder": "描述要对主形象做什么修改,例如:换上正装、受伤后的状态、披上斗篷...", + "selectMainCharacter": "选择主角色", + "selectCharacterPlaceholder": "请选择角色...", + "appearancesCount": "{count} 个形象", + "changeReason": "形象变化原因", + "changeReasonPlaceholder": "例如:战斗后受伤、穿上正装参加宴会...", + "defaultDescription": "{name} 的角色设定", + "generationMode": "生成方式", + "directGenerate": "直接生成", + "extractPrompt": "反推提示词", + "extractFirst": "先提取描述", + "directGenerateDesc": "直接使用参考图生成角色设定图(图生图)", + "extractPromptDesc": "先从图片提取角色描述,可编辑后再生成(文生图)", + "maxReferenceImages": "最多上传 5 张参考图", + "selectedCount": "已选择 {count}/5 张参考图", + "extractDescription": "提取角色描述", + "extracting": "提取中...", + "extractedDescription": "提取的角色描述(可编辑)", + "reExtract": "重新提取", + "editHint": "编辑后点击下方按钮生成角色设定图", + "generateFromDescription": "使用描述生成", + "textToImageTip": "文生图模式:基于提取的描述生成角色设定图", + "pleaseExtractFirst": "请先提取角色描述" + }, + "location": { + "title": "新建场景", + "name": "场景名称", + "namePlaceholder": "请输入场景名称", + "description": "场景描述", + "descPlaceholder": "请输入场景描述..." + }, + "artStyle": { + "title": "画面风格" + }, + "aiDesign": { + "title": "AI 设计", + "placeholder": "描述你想要的角色特征...", + "placeholderLocation": "描述场景氛围和环境...", + "generating": "设计中...", + "generate": "生成", + "tip": "输入简单描述,AI 帮你生成详细设定" + }, + "common": { + "creating": "创建中...", + "create": "创建", + "cancel": "取消", + "adding": "添加中...", + "add": "添加", + "optional": "(可选)" + }, + "errors": { + "uploadFailed": "上传失败", + "extractDescriptionFailed": "提取描述失败", + "createFailed": "创建失败", + "aiDesignFailed": "AI 设计失败", + "addSubAppearanceFailed": "添加子形象失败", + "insufficientBalance": "账户余额不足" + } +} diff --git a/messages/zh/assetPicker.json b/messages/zh/assetPicker.json new file mode 100644 index 0000000..06eb5b9 --- /dev/null +++ b/messages/zh/assetPicker.json @@ -0,0 +1,18 @@ +{ + "selectCharacter": "从资产中心选择角色", + "selectLocation": "从资产中心选择场景", + "selectVoice": "从资产中心选择音色", + "searchPlaceholder": "搜索资产名称或文件夹...", + "noAssets": "资产中心暂无资产", + "createInAssetHub": "请先在资产中心创建角色/场景/音色", + "noSearchResults": "未找到匹配的资产", + "appearances": "个形象", + "images": "张图片", + "cancel": "取消", + "confirmCopy": "确认复制", + "copyFromGlobal": "从资产中心复制", + "copySuccess": "复制成功", + "copyFailed": "复制失败", + "preview": "试听", + "stop": "停止" +} \ No newline at end of file diff --git a/messages/zh/assets.json b/messages/zh/assets.json new file mode 100644 index 0000000..f7fc234 --- /dev/null +++ b/messages/zh/assets.json @@ -0,0 +1,321 @@ +{ + "stage": { + "title": "资产确认", + "characters": "角色", + "locations": "场景", + "analyze": "分析资产", + "analyzing": "分析中...", + "generateAll": "批量生成全部", + "noCharacters": "暂无角色", + "noLocations": "暂无场景", + "confirmProfiles": "角色档案待确认", + "confirmHint": "请确认以下角色档案后生成外貌描述", + "confirmAll": "全部确认 ({count})", + "assetsTitle": "资产分析", + "characterAssets": "角色资产", + "locationAssets": "场景资产", + "counts": "{characterCount} 个角色,{appearanceCount} 个形象", + "locationCounts": "{count} 个场景", + "undoFailed": "撤回失败", + "undoFailedError": "撤回失败: {error}", + "undoSuccess": "已撤回到上一版本", + "editFailed": "编辑失败", + "editFailedError": "图片编辑失败: {error}", + "updateSuccess": "描述词已同步更新" + }, + "character": { + "add": "添加角色", + "edit": "编辑角色", + "delete": "删除角色", + "deleteConfirm": "确定要删除这个角色吗?", + "deleteAppearanceConfirm": "确定要删除这个形象吗?", + "deleteFailed": "删除失败: {error}", + "deleteWhole": "删除整个角色", + "deleteOptions": "删除选项", + "name": "角色名", + "description": "外貌描述", + "generateImage": "生成形象", + "regenerateImage": "重新生成", + "generate": "生成", + "regenerating": "生成中...", + "profile": "设定档案", + "voiceSettings": "配音设置", + "speaker": "说话人", + "selectSpeaker": "选择说话人", + "noSpeaker": "未设置", + "primary": "主形象", + "secondary": "子状态", + "generateFromPrimary": "基于主形象生成", + "selectPrimaryFirst": "请先选择主形象", + "editing": "编辑中...", + "confirming": "确认中...", + "assetCount": "{count} 个形象", + "characterCount": "{count} 个角色", + "updateFailed": "更新描述失败", + "addFailed": "添加角色失败", + "copyFromGlobal": "从资产中心复制" + }, + "location": { + "add": "添加场景", + "edit": "编辑场景", + "delete": "删除场景", + "deleteConfirm": "确定要删除这个场景吗?", + "deleteFailed": "删除失败: {error}", + "name": "场景名", + "summary": "简要描述", + "summaryPlaceholder": "场景用途/人物关联,如:张三居住的主卧室", + "description": "场景描述", + "generateImage": "生成图片", + "regenerateImage": "重新生成", + "updateFailed": "更新描述失败", + "addFailed": "添加场景失败" + }, + "image": { + "upload": "上传图片", + "uploadReplace": "上传替换图片", + "uploadFailed": "上传失败", + "uploadFailedError": "上传失败: {error}", + "uploadSuccess": "上传成功!", + "edit": "编辑图片", + "editPrompt": "编辑提示词", + "undo": "撤回到上一版本", + "undoConfirm": "确定要撤回到上一版本吗?当前版本将被删除。", + "regenerateGroup": "整组重新生成", + "regenerateStuck": "点击重新生成(当前卡住)", + "selectTip": "选择并确认后,可对图片进行编辑和修改", + "selectFirst": "请选择一张图片", + "useThis": "选择此方案", + "optionAlt": "{name} - 方案{number}", + "optionNumber": "方案{number}", + "optionSelected": "已选择方案{number}", + "confirmOption": "确定选择方案{number}", + "deleteOthersHint": "(删除其他)", + "confirmSuccess": "确认成功", + "confirmFailed": "确认选择失败: {error}", + "selectFailed": "选择图片失败: {error}", + "cancelSelection": "取消选择", + "deleteThis": "删除此形象", + "undoFailed": "撤回失败", + "undoSuccess": "✓ 已撤回到上一版本", + "editFailed": "图片编辑失败", + "editSuccess": "图片编辑成功", + "regenerateFailed": "重新生成失败: {error}" + }, + "modal": { + "newCharacter": "新增角色", + "addSubAppearance": "添加子形象", + "aiDesign": "AI智能设计", + "aiDesigning": "AI设计中...", + "designInstruction": "请输入设计指令", + "enterNameDesc": "请填写角色名称和描述", + "selectCharacter": "请选择一个角色", + "enterChangeReason": "请填写形象变化原因", + "enterSubDesc": "请填写形象描述", + "insufficientBalance": "余额不足\n\n{error}", + "designFailed": "AI设计失败: {error}", + "addFailed": "添加失败: {error}", + "aiDesignPlaceholderNew": "例如:一个20岁的女性魔法师,金色长发,蓝色眼睛,穿着紫色法袍...", + "aiDesignPlaceholderSub": "例如:换上黑色劲装,脚蹬厚底战靴,准备战斗的状态...", + "aiTipNew": "描述你想要的角色,AI会自动生成详细描述", + "aiTipSub": "描述角色的新状态,AI会生成子形象描述(只描述变化部分)", + "nameLabel": "角色名称", + "namePlaceholder": "输入角色名称...", + "descLabel": "形象描述", + "descPlaceholder": "输入角色形象描述...", + "selectLabel": "选择角色", + "selectPlaceholder": "-- 请选择角色 --", + "existingAppearances": "现有形象:", + "reasonLabel": "形象变化原因", + "reasonPlaceholder": "例如:换装后、受伤状态、出浴状态...", + "reasonTip": "简短描述这个形象与主形象的区别原因", + "subDescPlaceholder": "只描述变化部分,例如:换装,脚蹬厚底战靴...", + "subDescTip": "子形象只需描述变化部分(服装、状态等),面部和体型会自动继承主形象", + "adding": "添加中...", + "insufficientBalanceDefault": "账户余额不足,请充值后继续使用", + "addFailedGeneric": "添加失败", + "appearancesCount": "个形象", + "addCharacter": "新增角色", + "addLocation": "新增场景", + "aiDesignTip": "描述你想要的场景,AI会自动生成名称和详细描述", + "designing": "AI设计中...", + "saveName": "保存名字", + "saveOnly": "仅保存", + "sceneDescription": "场景描述", + "scenePrompt": "场景描述提示词", + "appearancePrompt": "形象描述提示词", + "smartModify": "智能修改", + "modifyPlaceholder": "例如:改成夜晚,添加月光,增加窗帘...", + "modifyPlaceholderCharacter": "例如:把头发改成金色、身高改为180cm、穿黑色西装...", + "modifying": "智能修改中...", + "modifyFailed": "修改失败", + "editCharacter": "编辑角色", + "editLocation": "编辑场景", + "saveAndGenerate": "保存并生成", + "generatingAutoClose": "正在生成图片,完成后将自动关闭...", + "aiLocationTip": "输入你想修改的内容,AI会自动调整场景描述", + "aiDesignPlaceholderLocation": "例如:一个古老的魔法图书馆,高耸的书架,昏暗的烛光,神秘的氛围...", + "artStyle": "画面风格", + "generate": "生成", + "introduction": "角色介绍", + "introductionPlaceholder": "例如:本书主角,第一人称'我'指的就是她。其他角色称她为'小雪'或'雪姐'...", + "introductionTip": "描述角色在故事中的身份、叙述视角(如'我'对应谁)、其他角色如何称呼她等", + "saveIntroduction": "保存介绍" + }, + "toolbar": { + "filter": "筛选", + "viewAll": "查看全部", + "showGenerated": "已生成", + "showPending": "待生成", + "assetManagement": "资产管理", + "assetCount": "共 {total} 个资产({appearances} 角色形象 + {locations} 场景)", + "globalAnalyze": "全局分析", + "globalAnalyzing": "正在执行全局资产分析...", + "globalAnalyzingHint": "请勿刷新页面,分析完成后将自动显示结果", + "globalAnalyzingTip": "正在分析所有剧集内容,提取角色和场景...", + "globalAnalyzeHint": "分析所有剧集内容,提取角色和场景", + "globalAnalyzeSuccess": "全局分析完成:新增 {characters} 个角色,{locations} 个场景", + "globalAnalyzeFailed": "全局分析失败", + "generateAll": "生成全部图片", + "generateAllNoop": "所有资产都已有图片,无需生成", + "generating": "生成中 ({current}/{total})", + "regenerateAll": "重新生成全部", + "regenerateAllConfirm": "确定要重新生成所有资产的图片吗?这将覆盖现有图片。", + "noAssetsToGenerate": "没有可生成的资产", + "regenerateAllHint": "重新生成所有资产图片(覆盖现有)" + }, + "common": { + "actions": "操作", + "add": "添加", + "cancel": "取消", + "confirm": "确认", + "copy": "复制", + "delete": "删除", + "download": "下载", + "edit": "编辑", + "generate": "生成", + "generateFailed": "生成失败", + "loading": "加载中...", + "none": "无", + "preview": "预览", + "refresh": "刷新", + "regenerate": "重新生成", + "save": "保存", + "status": "状态", + "submitFailed": "提交失败", + "upload": "上传", + "unknownError": "未知错误" + }, + "video": { + "panelCard": { + "generating": "生成中...", + "editPrompt": "编辑提示词" + } + }, + "smartImport": { + "preview": { + "saving": "保存中..." + } + }, + "storyboard": { + "group": { + "generating": "生成中..." + } + }, + "errors": { + "saveFailed": "保存失败,请重试", + "failed": "失败,请重试", + "insufficientBalance": "账户余额不足", + "aiDesignFailed": "AI 设计失败", + "createFailed": "创建失败" + }, + "assetLibrary": { + "button": "资产库", + "title": "资产库", + "copySuccessCharacter": "角色形象复制成功", + "copySuccessLocation": "场景图片复制成功", + "copySuccessVoice": "音色复制成功", + "copyFailed": "复制失败: {error}" + }, + "tts": { + "voiceDesignSaved": "已为 {name} 设置 AI 设计的声音", + "saveVoiceDesignFailed": "保存声音设计失败: {error}", + "title": "配音音色", + "noVoice": "无音色", + "previewFailed": "预览失败: {error}", + "uploadFailed": "上传音频失败: {error}", + "uploading": "上传中...", + "uploaded": "已上传", + "uploadAudio": "上传音频", + "pause": "暂停", + "preview": "试听音色" + }, + "characterProfile": { + "importance": { + "S": "S级 - 绝对主角", + "A": "A级 - 核心配角", + "B": "B级 - 重要配角", + "C": "C级 - 次要角色", + "D": "D级 - 群众演员" + }, + "costumeLevel": { + "5": "皇室/顶奢级", + "4": "贵族/精英级", + "3": "专业/品质级", + "2": "日常/普通级", + "1": "朴素/统一级" + }, + "importanceLevel": "角色重要性层级", + "characterArchetype": "角色原型", + "archetypePlaceholder": "如: 霸道总裁、心机婊", + "personalityTags": "性格标签", + "addTagPlaceholder": "添加标签", + "costumeLevelLabel": "服装华丽度", + "suggestedColors": "建议色彩", + "colorPlaceholder": "如: 深蓝、金色", + "primaryMarker": "主要辨识标志", + "markerNote": "(S/A级推荐)", + "markingsPlaceholder": "如: 眼角泪痣、左耳银色耳钉", + "visualKeywords": "视觉关键词", + "keywordsPlaceholder": "如: 精英气质、禁欲系", + "editDialogTitle": "编辑角色档案 - {name}", + "confirmAndGenerate": "确认并生成", + "useExisting": "使用已有形象", + "editProfile": "编辑档案", + "delete": "删除此角色", + "summary": { + "gender": "性别:", + "age": "年龄:", + "era": "时代:", + "class": "阶层:", + "occupation": "职业:", + "personality": "性格:", + "costume": "服装:", + "identifier": "标志:" + }, + "parseFailed": "档案数据解析失败", + "confirmSuccessGenerating": "✓ 档案确认成功,正在生成视觉描述", + "confirmFailed": "确认失败: {error}", + "noPendingCharacters": "没有待确认的角色", + "batchConfirmPrompt": "确认为 {count} 个角色生成视觉描述吗?", + "batchConfirmSuccess": "✓ 已为 {count} 个角色生成视觉描述", + "batchConfirmFailed": "批量确认失败: {error}", + "deleteConfirm": "确定要删除此角色吗?此操作不可撤销。", + "deleteSuccess": "✓ 角色已删除", + "deleteFailed": "删除失败: {error}" + }, + "imageEdit": { + "editCharacterImage": "编辑人物图片", + "editLocationImage": "编辑场景图片", + "characterLabel": "人物: {name}", + "locationLabel": "场景: {name}", + "editInstruction": "修改指令", + "subtitle": "输入修改指令,可选择上传参考图片", + "characterPlaceholder": "描述你想要修改的内容,例如:把头发改成金色、添加眼镜、换成休闲装...", + "locationPlaceholder": "描述你想要修改的内容,例如:添加更多树木、改成夜晚场景...", + "storyboardPlaceholder": "描述你想要修改的内容,例如:改变背景颜色、调整人物表情...", + "noAssetHint": "暂无资产,点击\"添加资产\"选择", + "referenceImages": "参考图片", + "referenceImagesHint": "(可选,支持粘贴)", + "startEditing": "开始编辑" + } +} diff --git a/messages/zh/auth.json b/messages/zh/auth.json new file mode 100644 index 0000000..625eeae --- /dev/null +++ b/messages/zh/auth.json @@ -0,0 +1,29 @@ +{ + "welcomeBack": "欢迎回来", + "loginTo": "登录到waoowaoo", + "createAccount": "创建账户", + "joinPlatform": "加入waoowaoo", + "phoneNumber": "用户名", + "password": "密码", + "confirmPassword": "确认密码", + "phoneNumberPlaceholder": "请输入用户名", + "passwordPlaceholder": "请输入密码", + "passwordMinPlaceholder": "请输入密码(至少6位)", + "confirmPasswordPlaceholder": "请再次输入密码", + "loginButton": "登录", + "loginButtonLoading": "登录中...", + "signupButton": "注册", + "signupButtonLoading": "注册中...", + "noAccount": "还没有账户?", + "hasAccount": "已有账户?", + "signupNow": "立即注册", + "signinNow": "立即登录", + "backToHome": "← 返回首页", + "loginFailed": "登录失败,请检查手机号和密码", + "loginError": "登录过程中出现错误", + "passwordMismatch": "密码确认不匹配", + "passwordTooShort": "密码长度至少6位", + "signupSuccess": "注册成功!正在跳转到登录页面...", + "signupFailed": "注册失败", + "signupError": "注册过程中出现错误" +} \ No newline at end of file diff --git a/messages/zh/billing.json b/messages/zh/billing.json new file mode 100644 index 0000000..4d167d7 --- /dev/null +++ b/messages/zh/billing.json @@ -0,0 +1,15 @@ +{ + "transactionType": "交易类型", + "startDate": "开始日期", + "endDate": "结束日期", + "all": "全部", + "income": "收入", + "expense": "支出", + "reset": "重置", + "filter": "筛选", + "noRecords": "暂无记录", + "accountRecharge": "账户充值", + "serviceConsumption": "服务消费", + "balance": "余额", + "allTypes": "全部类型" +} \ No newline at end of file diff --git a/messages/zh/common.json b/messages/zh/common.json new file mode 100644 index 0000000..31dd74c --- /dev/null +++ b/messages/zh/common.json @@ -0,0 +1,134 @@ +{ + "appName": "waoowaoo", + "betaVersion": "Beta v0.1 (测试版)", + "loading": "加载中...", + "save": "保存", + "cancel": "取消", + "confirm": "确认", + "delete": "删除", + "edit": "编辑", + "search": "搜索", + "clear": "清除", + "close": "关闭", + "back": "返回", + "next": "下一步", + "previous": "上一步", + "submit": "提交", + "reset": "重置", + "generate": "生成", + "regenerate": "重新生成", + "preview": "预览", + "download": "下载", + "upload": "上传", + "select": "选择", + "add": "添加", + "remove": "移除", + "refresh": "刷新", + "expand": "展开", + "collapse": "收起", + "all": "全部", + "none": "无", + "success": "成功", + "error": "错误", + "warning": "警告", + "info": "信息", + "copy": "复制", + "paste": "粘贴", + "apply": "应用", + "autoSave": "自动保存", + "saved": "已保存", + "episode": "剧集", + "project": "项目", + "editEpisodeName": "编辑剧集名称", + "deleteEpisode": "删除剧集", + "deleteEpisodeConfirm": "确认删除", + "newEpisode": "新建剧集", + "optional": "(可选)", + "rename": "重命名", + "dragToReorder": "拖动调整位置", + "episodeNamePlaceholder": "输入剧集名称...", + "cancelSelection": "取消选择", + "referenceImage": "参考图", + "previewLarge": "预览大图", + "viewOriginal": "查看原图", + "schemeN": "方案 {n}", + "insufficientBalance": "余额不足", + "insufficientBalanceDetail": "账户余额不足,请充值后继续使用", + "operationFailed": "操作失败", + "pleaseRetry": "请重试", + "recommended": "推荐", + "language": { + "select": "选择语言", + "zh": "中文", + "en": "English", + "switchConfirmTitle": "切换语言?", + "switchConfirmMessage": "切换到 {targetLanguage} 后,不仅界面文字会改变,整条流程的提示词模板、剧本生成和任务输出语言也会同步切换。是否继续?", + "switchConfirmAction": "确认切换" + }, + "taskStatus": { + "intent": { + "generate": { + "running": { + "image": "生成中", + "video": "生成中", + "audio": "生成中", + "text": "生成中" + } + }, + "regenerate": { + "running": { + "image": "重新生成中", + "video": "重新生成中", + "audio": "重新生成中", + "text": "重新生成中" + } + }, + "modify": { + "running": { + "image": "修改中", + "video": "修改中", + "audio": "修改中", + "text": "修改中" + } + }, + "analyze": { + "running": { + "image": "分析中", + "video": "分析中", + "audio": "分析中", + "text": "分析中" + } + }, + "build": { + "running": { + "image": "构建中", + "video": "构建中", + "audio": "构建中", + "text": "构建中" + } + }, + "convert": { + "running": { + "image": "转换中", + "video": "转换中", + "audio": "转换中", + "text": "转换中" + } + }, + "process": { + "running": { + "image": "处理中", + "video": "处理中", + "audio": "处理中", + "text": "处理中" + } + } + }, + "failed": { + "image": "处理失败", + "video": "处理失败", + "audio": "处理失败", + "text": "处理失败" + } + } +} \ No newline at end of file diff --git a/messages/zh/configModal.json b/messages/zh/configModal.json new file mode 100644 index 0000000..4662405 --- /dev/null +++ b/messages/zh/configModal.json @@ -0,0 +1,31 @@ +{ + "title": "项目全局配置", + "saved": "已保存", + "autoSave": "自动保存", + "visualStyle": "视觉风格", + "modelParams": "模型参数", + "aspectRatio": "画面比例", + "ttsSettings": "旁白配置", + "loadingModels": "加载模型列表...", + "analysisModel": "分析模型", + "characterModel": "人物生成模型", + "locationModel": "场景生成模型", + "storyboardModel": "分镜图像模型", + "editModel": "修图/编辑模型", + "videoModel": "视频模型", + "videoResolution": "视频分辨率", + "ttsVoice": "旁白音色", + "ttsRate": "语速", + "fetchModelsFailed": "获取用户模型列表失败", + "placeholder": "请输入...", + "description": "描述", + "hint": "提示", + "pleaseSelect": "请选择...", + "selectModel": "选择模型", + "paramConfig": "参数配置", + "fixed": "固定", + "noParams": "该模型无可配置参数", + "confirm": "确认", + "cancel": "取消", + "delete": "删除" +} diff --git a/messages/zh/errors.json b/messages/zh/errors.json new file mode 100644 index 0000000..b25309d --- /dev/null +++ b/messages/zh/errors.json @@ -0,0 +1,19 @@ +{ + "UNAUTHORIZED": "请先登录", + "FORBIDDEN": "没有权限访问", + "NOT_FOUND": "资源不存在", + "INSUFFICIENT_BALANCE": "API余额不足,请充值后重试", + "RATE_LIMIT": "请求过于频繁,请 {retryAfter} 秒后重试", + "QUOTA_EXCEEDED": "配额已用尽,请稍后重试", + "GENERATION_FAILED": "生成失败,请重试", + "GENERATION_TIMEOUT": "生成超时,请重试", + "SENSITIVE_CONTENT": "内容可能包含敏感信息", + "INVALID_PARAMS": "参数错误", + "MISSING_CONFIG": "请先完成模型配置", + "INTERNAL_ERROR": "服务器错误,请稍后重试", + "NETWORK_ERROR": "网络错误,请检查连接", + "EXTERNAL_ERROR": "外部服务暂时不可用,请稍后重试", + "TASK_NOT_READY": "任务正在处理中", + "NO_RESULT": "任务无结果", + "CONFLICT": "资源状态冲突" +} \ No newline at end of file diff --git a/messages/zh/landing.json b/messages/zh/landing.json new file mode 100644 index 0000000..b6b88d4 --- /dev/null +++ b/messages/zh/landing.json @@ -0,0 +1,26 @@ +{ + "title": "waoowaoo", + "subtitle": "AI影视Studio", + "enterWorkspace": "进入工作区", + "getStarted": "立即体验", + "learnMore": "了解更多", + "features": { + "title": "释放无限创造力", + "subtitle": "全流程 AI 辅助,从剧本到成片", + "character": { + "title": "角色工坊", + "description": "打造独一无二的动漫角色,保持高度一致性" + }, + "storyboard": { + "title": "智能分镜", + "description": "文字一键转分镜,精准把控叙事节奏" + }, + "world": { + "title": "世界构建", + "description": "沉浸式场景生成,构建宏大的故事背景" + } + }, + "footer": { + "copyright": "2026 waoowaoo AI. All rights reserved." + } +} \ No newline at end of file diff --git a/messages/zh/layout.json b/messages/zh/layout.json new file mode 100644 index 0000000..54ac4c6 --- /dev/null +++ b/messages/zh/layout.json @@ -0,0 +1,4 @@ +{ + "title": "AI动漫制作工作台", + "description": "使用最先进的AI技术创建专业级动漫内容" +} diff --git a/messages/zh/modelSection.json b/messages/zh/modelSection.json new file mode 100644 index 0000000..fc65d02 --- /dev/null +++ b/messages/zh/modelSection.json @@ -0,0 +1,27 @@ +{ + "llmModels": "文本模型列表", + "imageModels": "图片模型列表", + "videoModels": "视频模型列表", + "price": "价格", + "pricePerMillion": "每百万token", + "pricePerImage": "每张", + "pricePerVideo": "每条", + "name": "名称", + "modelId": "模型 ID", + "modelName": "模型名称", + "provider": "厂商", + "resolution": "分辨率", + "add": "添加", + "addModel": "添加模型", + "addNewModel": "添加新模型", + "selectPreset": "选择预设模型", + "customModel": "自定义模型", + "confirmAdd": "确认添加", + "cancel": "取消", + "done": "完成", + "fillComplete": "请填写完整信息", + "noModels": "暂无模型,点击上方按钮添加", + "noApiKey": "请配置API Key", + "batchMode": "批量", + "batchModeTooltip": "离线推理,价格便宜50%,24小时内完成" +} diff --git a/messages/zh/nav.json b/messages/zh/nav.json new file mode 100644 index 0000000..178a8f9 --- /dev/null +++ b/messages/zh/nav.json @@ -0,0 +1,8 @@ +{ + "workspace": "工作区", + "assetHub": "资产中心", + "profile": "设置中心", + "signin": "登录", + "signup": "注册", + "logout": "退出登录" +} \ No newline at end of file diff --git a/messages/zh/novel-promotion.json b/messages/zh/novel-promotion.json new file mode 100644 index 0000000..c070e25 --- /dev/null +++ b/messages/zh/novel-promotion.json @@ -0,0 +1,139 @@ +{ + "stages": { + "story": "故事", + "script": "剧本", + "storyboard": "分镜", + "video": "成片", + "editor": "AI剪辑", + "editorComingSoon": "开发中,关注我们获取最新消息" + }, + "buttons": { + "assetLibrary": "资产库", + "settings": "配置", + "refreshData": "刷新项目数据", + "enterVideoGeneration": "进入视频生成 →" + }, + "smartImport": { + "title": "开启你的创作之旅", + "subtitle": "首先,选择你的创作方式", + "manualCreate": { + "title": "从第一集开始创作", + "description": "从第一集开始,适合边写边播或单集短视频制作", + "button": "开始创作" + }, + "smartImport": { + "title": "智能全书导入", + "description": "上传整本小说或剧本,AI 引擎自动识别章节结构,一键完成智能分集。", + "button": "立即导入", + "recommended": "推荐" + }, + "upload": { + "title": "上传原始素材", + "subtitle": "AI 引擎已准备就绪,一键自动分集与格式化", + "maxWords": "(最大支持 3 万字)", + "textInput": "输入文本内容", + "documentUpload": "上传完整文档", + "placeholder": "在此处粘贴你的小说章节或剧本内容...", + "filePlaceholder": "已上传文件模式", + "clickUpload": "点击上传文档", + "clearTextFirst": "请先清空左侧文本", + "supportedFormats": "支持 Word, TXT 格式", + "preview": "预览", + "expandPreview": "展开更多", + "collapsePreview": "收起预览", + "deleteFile": "删除文件", + "startAnalysis": "开始智能分析", + "back": "返回", + "words": "字" + }, + "analyzing": { + "title": "AI 正在分析你的故事", + "description": "识别章节结构,智能分集中...", + "autoSave": "分析完成后将自动保存" + }, + "preview": { + "title": "智能分集完成", + "episodeCount": "已为你自动分为 {count} 集", + "totalWords": "总计 {count} 字", + "autoSaved": "✓ 已自动保存", + "reanalyze": "重新分析", + "confirm": "确认完成", + "saving": "保存中...", + "episodeList": "剧集列表", + "addEpisode": "添加剧集", + "averageWords": "平均每集", + "episodeContent": "剧集内容", + "episodePlaceholder": "输入剧集标题...", + "summaryPlaceholder": "输入剧情简介...", + "newEpisode": "新剧集", + "deleteEpisode": "删除剧集", + "deleteConfirm": { + "title": "确认删除", + "message": "确定要删除「{title}」吗?", + "cancel": "取消", + "confirm": "确认删除" + }, + "tip": { + "title": "提示", + "content": "你可以直接编辑标题、简介和内容。点击【确认完成】后,剧集将正式导入到项目中" + } + }, + "errors": { + "fileTooLarge": "文件过大,请上传小于 10MB 的文件", + "docNotSupported": "不支持 .doc 格式,请使用 Word 转换为 .docx", + "fileEmpty": "文件内容为空", + "fileReadError": "文件读取失败,请重试", + "uploadFirst": "请先上传或粘贴内容", + "analyzeFailed": "分析失败", + "saveFailed": "保存失败" + }, + "cancelConfirm": "确定要取消吗?已分析的剧集将被清空。" + }, + "storyInput": { + "currentEditing": "当前正在编辑:{name}", + "editingTip": "以下制作流程仅针对本集,如有其他剧集请在左上角切换", + "wordCount": "字数:", + "assetLibraryTip": { + "title": "需要自定义角色和场景?", + "description": "点击右上角的「资产库」按钮,可以上传资产设定文档或手动添加角色/场景。AI 将优先使用资产库中的设定进行分析。" + }, + "videoRatio": "画面比例", + "visualStyle": "视觉风格", + "moreConfig": "更多配置请点击右上角「 配置」按钮", + "narration": { + "title": "启用旁白配音", + "description": "生成 TTS 语音旁白,为视频添加解说" + }, + "creating": "AI 创作中...", + "ready": "✓ 配置完成,可以进入下一步", + "pleaseInput": "请先输入剧本内容" + }, + "execution": { + "selectEpisode": "请先选择剧集", + "fillContentFirst": "请先填写内容", + "requestAborted": "请求已中断(可能因页面刷新)", + "analysisFailed": "资产分析失败", + "prepareFailed": "准备失败", + "generationFailed": "生成失败", + "batchVideoFailed": "批量生成视频失败", + "updateFailed": "更新失败", + "saveFailed": "保存失败", + "storyToScriptRunning": "Story→Script V2 运行中", + "scriptToStoryboardRunning": "Script→Storyboard V2 运行中", + "storyToScriptFailed": "内容转剧本失败", + "scriptToStoryboardFailed": "剧本转分镜失败", + "taskStreamTimeout": "任务执行超时,请检查任务是否仍在运行,或重新触发" + }, + "rebuildConfirm": { + "storyToScript": { + "title": "将重建剧本流程", + "message": "检测到当前剧集已有下游分镜数据({storyboardCount} 个分镜,{panelCount} 个镜头面板)。继续执行将清空这些数据并重新生成,是否继续?" + }, + "scriptToStoryboard": { + "title": "将重建分镜数据", + "message": "检测到当前剧集已有分镜数据({storyboardCount} 个分镜,{panelCount} 个镜头面板)。继续执行将清空当前分镜并重新生成,是否继续?" + }, + "confirm": "继续并清空", + "cancel": "取消" + } +} \ No newline at end of file diff --git a/messages/zh/profile.json b/messages/zh/profile.json new file mode 100644 index 0000000..696099c --- /dev/null +++ b/messages/zh/profile.json @@ -0,0 +1,107 @@ +{ + "user": "用户", + "personalAccount": "个人账户", + "availableBalance": "可用余额", + "frozen": "冻结", + "totalSpent": "已消费", + "apiConfig": "API 配置", + "rechargeRecords": "充值记录", + "billingRecords": "扣费记录", + "logout": "退出登录", + "accountTransactions": "账户流水", + "projectDetails": "项目明细", + "summary": "汇总", + "transactions": "流水", + "noTransactions": "暂无流水记录", + "noProjectCosts": "暂无项目费用记录", + "noDetails": "该项目暂无费用明细", + "noRecords": "暂无记录", + "byType": "按类型", + "byAction": "按操作", + "times": "次", + "total": "总计", + "filter": "筛选", + "allTypes": "全部类型", + "recharge": "账户充值", + "consume": "服务消费", + "balanceAfter": "余额 {amount}", + "recordCount": "{count} 条记录", + "totalCost": "总计 {amount}", + "previousPage": "上一页", + "nextPage": "下一页", + "pagination": "共 {total} 条,第 {page} / {totalPages} 页", + "episodeLabel": "第 {number} 集", + "billingDetail": { + "imageWithRes": "{count}张 · {resolution}", + "image": "{count}张", + "videoWithRes": "{count}段 · {resolution}", + "video": "{count}段", + "tokens": "{count} tokens", + "seconds": "{count}秒", + "calls": "{count}次" + }, + "apiTypes": { + "image": "图片生成", + "video": "视频生成", + "text": "文本分析", + "tts": "语音合成", + "voice": "配音", + "voice_design": "声音设计", + "lip_sync": "口型同步" + }, + "actionTypes": { + "image_panel": "分镜生图", + "image_character": "角色生图", + "image_location": "场景生图", + "video_panel": "视频生成", + "lip_sync": "口型同步", + "voice_line": "配音合成", + "voice_design": "声音设计", + "asset_hub_voice_design": "素材库声音设计", + "regenerate_storyboard_text": "重生成分镜文案", + "insert_panel": "插入面板", + "panel_variant": "镜头变体", + "modify_asset_image": "修图", + "regenerate_group": "批量重生成", + "asset_hub_image": "素材库生图", + "asset_hub_modify": "素材库修图", + "analyze_novel": "小说分析", + "story_to_script_run": "故事转剧本", + "script_to_storyboard_run": "剧本转分镜", + "clips_build": "片段合成", + "screenplay_convert": "剧本转换", + "voice_analyze": "声音分析", + "analyze_global": "全局分析", + "ai_modify_appearance": "AI 修改角色形象", + "ai_modify_location": "AI 修改场景", + "ai_modify_shot_prompt": "AI 修改镜头提示词", + "analyze_shot_variants": "镜头变体分析", + "ai_create_character": "AI 创建角色", + "ai_create_location": "AI 创建场景", + "reference_to_character": "参考图转角色", + "character_profile_confirm": "确认角色档案", + "character_profile_batch_confirm": "批量确认角色档案", + "episode_split_llm": "章节拆分", + "asset_hub_ai_design_character": "素材库 AI 设计角色", + "asset_hub_ai_design_location": "素材库 AI 设计场景", + "asset_hub_ai_modify_character": "素材库 AI 修改角色", + "asset_hub_ai_modify_location": "素材库 AI 修改场景", + "asset_hub_reference_to_character": "素材库参考图转角色", + "storyboard": "分镜图", + "storyboard_candidate": "分镜候选", + "character": "角色图", + "location": "场景图", + "video": "视频", + "analyze": "分析", + "analyze_character": "角色分析", + "analyze_location": "场景分析", + "clips": "片段切割", + "storyboard_text_plan": "分镜规划", + "storyboard_text_detail": "分镜细节", + "tts": "语音合成", + "regenerate": "重生成", + "voice-generate": "配音生成", + "voice-design": "声音设计", + "lip-sync": "口型同步" + } +} \ No newline at end of file diff --git a/messages/zh/progress.json b/messages/zh/progress.json new file mode 100644 index 0000000..7898230 --- /dev/null +++ b/messages/zh/progress.json @@ -0,0 +1,136 @@ +{ + "analyzing": "正在分析故事结构...", + "splittingClips": "正在分割片段...", + "convertingScreenplay": "正在转换为分镜脚本...", + "submittingStoryboard": "正在提交分镜...", + "step": "第 {current} 步,共 {total} 步", + "status": { + "completed": "已完成", + "failed": "失败", + "processing": "进行中", + "queued": "排队中", + "pending": "未开始" + }, + "stageCard": { + "stage": "阶段", + "realtimeStream": "实时流", + "currentStage": "当前阶段", + "outputTitle": "AI 实时输出 · {stage}", + "waitingModelOutput": "等待模型输出...", + "reasoningNotProvided": "该步骤未返回思考过程" + }, + "runtime": { + "waitingExecution": "等待执行", + "taskCreated": "任务已创建", + "taskStarted": "任务开始处理", + "taskCompleted": "任务已完成", + "taskFailed": "任务失败", + "taskProcessing": "任务处理中...", + "llm": { + "processing": "模型处理中...", + "output": "模型正在输出...", + "reasoning": "模型正在推理...", + "completed": "模型输出完成", + "failed": "模型输出失败" + }, + "stage": { + "llmSubmit": "模型请求提交中", + "llmStreaming": "模型流式输出中", + "llmFallbackNonStream": "模型降级为非流式模式", + "llmCompleted": "模型输出完成", + "llmFailed": "模型输出失败" + } + }, + "taskType": { + "generic": "任务", + "imagePanel": "分镜图片", + "imageCharacter": "角色图片", + "imageLocation": "场景图片", + "videoPanel": "视频生成", + "lipSync": "口型同步", + "voiceLine": "配音生成", + "voiceDesign": "声音设计", + "assetHubVoiceDesign": "资产库声音设计", + "regenerateStoryboardText": "重生成分镜文本", + "insertPanel": "插入分镜", + "panelVariant": "分镜变体", + "modifyAssetImage": "图片编辑", + "regenerateGroup": "批量重生成", + "assetHubImage": "资产中心图片", + "assetHubModify": "资产中心编辑", + "analyzeNovel": "内容分析", + "storyToScriptRun": "剧本拆解", + "scriptToStoryboardRun": "分镜生成", + "clipsBuild": "片段生成", + "screenplayConvert": "剧本转换", + "voiceAnalyze": "台词分析", + "analyzeGlobal": "全局分析", + "aiModifyAppearance": "角色描述修改", + "aiModifyLocation": "场景描述修改", + "aiModifyShotPrompt": "镜头提示词修改", + "analyzeShotVariants": "镜头变体分析", + "aiCreateCharacter": "项目角色设计", + "aiCreateLocation": "项目场景设计", + "referenceToCharacter": "参考图转角色", + "characterProfileConfirm": "角色档案确认", + "characterProfileBatchConfirm": "批量角色档案确认", + "episodeSplitLlm": "智能分集", + "assetHubAiDesignCharacter": "资产库角色设计", + "assetHubAiDesignLocation": "资产库场景设计", + "assetHubAiModifyCharacter": "资产库角色修改", + "assetHubAiModifyLocation": "资产库场景修改", + "assetHubReferenceToCharacter": "资产库参考图转角色" + }, + "stage": { + "received": "任务已接收", + "generateCharacterImage": "生成角色图片", + "generateLocationImage": "生成场景图片", + "generatePanelCandidate": "生成候选分镜图", + "generatePanelVideo": "生成分镜视频", + "generateVoiceSubmit": "提交配音任务", + "generateVoicePersist": "保存配音结果", + "voiceDesignSubmit": "提交声音设计任务", + "voiceDesignDone": "声音设计完成", + "submitLipSync": "提交口型同步任务", + "persistLipSync": "保存口型同步结果", + "storyboardClip": "生成片段分镜", + "regenerateStoryboardPrepare": "准备重生成分镜", + "regenerateStoryboardPersist": "保存重生成分镜", + "storyToScriptPrepare": "准备剧本拆解参数", + "storyToScriptStep": "执行剧本拆解步骤", + "storyToScriptPersist": "保存剧本拆解结果", + "storyToScriptPersistDone": "剧本拆解结果已保存", + "scriptToStoryboardPrepare": "准备分镜生成参数", + "scriptToStoryboardStep": "执行分镜生成步骤", + "scriptToStoryboardPersist": "保存分镜结果", + "scriptToStoryboardPersistDone": "分镜与台词结果已保存", + "insertPanelGenerateText": "生成插入镜头文本", + "insertPanelPersist": "保存插入镜头", + "pollingExternal": "等待外部服务返回", + "enqueueFailed": "任务入队失败", + "llmProxySubmit": "提交 LLM 任务", + "llmProxyExecute": "执行 LLM 任务", + "llmProxyPersist": "保存 LLM 结果" + }, + "runConsole": { + "storyToScript": "内容到剧本", + "scriptToStoryboard": "剧本到分镜", + "storyToScriptRunning": "Story→Script 运行中", + "scriptToStoryboardRunning": "Script→Storyboard 运行中", + "storyToScriptSubtitle": "Story To Script V2", + "scriptToStoryboardSubtitle": "Script To Storyboard V2", + "stop": "停止", + "minimize": "最小化" + }, + "streamStep": { + "analyzeCharacters": "角色分析", + "analyzeLocations": "场景分析", + "splitClips": "片段切分", + "screenplayConversion": "剧本转换", + "storyboardPlan": "分镜规划", + "cinematographyRules": "摄影规则生成", + "actingDirection": "演技指导生成", + "storyboardDetailRefine": "分镜细节补全", + "voiceAnalyze": "台词分析" + } +} diff --git a/messages/zh/providerSection.json b/messages/zh/providerSection.json new file mode 100644 index 0000000..5ff96cf --- /dev/null +++ b/messages/zh/providerSection.json @@ -0,0 +1,7 @@ +{ + "addProvider": "+ 添加提供商", + "name": "名称", + "add": "添加", + "save": "保存", + "fillRequired": "请填写必要信息" +} \ No newline at end of file diff --git a/messages/zh/scriptView.json b/messages/zh/scriptView.json new file mode 100644 index 0000000..fc05ea4 --- /dev/null +++ b/messages/zh/scriptView.json @@ -0,0 +1,70 @@ +{ + "title": "剧本视图", + "scriptBreakdown": "剧本拆解", + "splitCount": "已拆分 {count} 个分镜", + "noClips": "暂无分镜,请先在故事视图生成", + "segment": { + "title": "片段 {index}", + "selected": "(选中)" + }, + "inSceneAssets": "剧中资产", + "currentSelected": "当前选中: 片段 {number}", + "assetView": { + "allClips": "全部片段", + "viewingClip": "查看片段 {number}" + }, + "asset": { + "generateCharacter": "点击生成形象 →", + "generateLocation": "点击生成场景 →", + "removeCharacterConfirm": "确定要从当前片段移除该角色吗?", + "removeLocationConfirm": "确定要从当前片段移除该场景吗?", + "removeFromClip": "从当前片段移除", + "noAudio": "无音频", + "playing": "播放中", + "listen": "试听", + "activeCharacters": "出场角色", + "activeLocations": "出场场景", + "selectCharacter": "选择要添加的角色/形象", + "selectLocation": "选择要添加的场景", + "loadingAssets": "加载资产中...", + "appearanceCount": "{count} 个形象", + "added": "已添加", + "primary": "主形象", + "subAppearance": "子形象", + "defaultAppearance": "初始形象", + "clickToRemove": "点击移除 {name}", + "clickToAdd": "点击添加 {name}" + }, + "screenplay": { + "scene": "场景 {number}", + "location": "场景:", + "locationTime": "时间:", + "day": "日", + "night": "夜", + "dawn": "晨", + "dusk": "昏", + "dialogue": "对白", + "action": "动作", + "narration": "旁白", + "content": "原文内容", + "noContent": "暂无内容", + "clickToEdit": "点击编辑", + "interior": "内景", + "exterior": "外景", + "characters": "出场角色", + "noCharacter": "暂无角色信息", + "noLocation": "暂无出场场景", + "noCharacterInClip": "暂无出场角色" + }, + "confirm": { + "removeCharacter": "确定要从当前片段移除该角色吗?", + "removeLocation": "确定要从当前片段移除该场景吗?" + }, + "generate": { + "missingAssets": "还有 {count} 个资产未生成形象", + "missingAssetsTip": "请先在", + "missingAssetsTipLink": "中生成所有出场角色和场景的形象", + "generating": "正在生成画面...", + "startGenerate": "确认并开始绘制 →" + } +} \ No newline at end of file diff --git a/messages/zh/smartImport.json b/messages/zh/smartImport.json new file mode 100644 index 0000000..1d9aa03 --- /dev/null +++ b/messages/zh/smartImport.json @@ -0,0 +1,168 @@ +{ + "title": "开启你的创作之旅", + "subtitle": "首先,选择你的创作方式", + "manualCreate": { + "title": "从第一集开始创作", + "description": "从第一集开始,适合边写边播或单集短视频制作", + "button": "开始创作" + }, + "manualDesc": "从第一集开始,适合边写边播或单集短视频制作", + "startCreate": "开始创作", + "smartImport": { + "title": "智能文本分集", + "description": "上传整本小说或剧本,AI 引擎自动识别章节结构,一键完成智能分集。", + "button": "立即导入", + "recommended": "推荐" + }, + "markerDetection": { + "enable": "优先使用章节标记(如第X集/Episode X)", + "tooltip": "自动识别【第X集/章】【Episode/Chapter X】等标记,免费快速" + }, + "smartImportDesc": "上传整本小说或剧本,AI 引擎自动识别章节结构,一键完成智能分集。", + "recommended": "推荐", + "importNow": "立即导入", + "uploadTitle": "上传原始素材", + "uploadSubtitle": "AI 引擎已准备就绪,一键自动分集与格式化", + "maxWords": "最大支持 3 万字", + "textInput": "输入文本内容", + "textPlaceholder": "在此处粘贴你的小说章节或剧本内容...", + "uploadDoc": "上传完整文档", + "clickUpload": "点击上传文档", + "clearText": "请先清空左侧文本", + "supportFormat": "支持 Word, TXT 格式", + "fileMax": "文件最大 3万字", + "words": "字", + "startAnalyzing": "开始分析", + "analyzing": { + "title": "AI 正在分析你的故事", + "description": "识别章节结构,智能分集中...", + "autoSave": "分析完成后将自动保存" + }, + "analyzingDesc": "识别章节结构,智能分集中...", + "autoSave": "分析完成后将自动保存", + "splitComplete": "智能分集完成", + "splitResult": "已为你自动分为 {count} 集,总计 {words} 字", + "saved": "已自动保存", + "reAnalyze": "重新分析", + "confirmComplete": "确认完成", + "saving": "保存中...", + "episodeList": "剧集列表", + "episodes": "集", + "episode": "第 {num} 集", + "addEpisode": "添加剧集", + "newEpisode": "新剧集", + "avgWords": "平均每集", + "episodeContent": "剧集内容", + "plotSummary": "剧情简介", + "enterTitle": "输入剧集标题...", + "enterSummary": "输入剧情简介...", + "confirmDelete": "确认删除", + "deleteConfirmMsg": "确定要删除「{title}」吗?", + "preview": { + "title": "智能分集完成", + "episodeCount": "已为你自动分为 {count} 集", + "totalWords": "总计 {count} 字", + "autoSaved": "✓ 已自动保存", + "reanalyze": "重新分析", + "confirm": "确认完成", + "saving": "保存中...", + "episodeList": "剧集列表", + "addEpisode": "添加剧集", + "averageWords": "平均每集", + "episodeContent": "剧集内容", + "episodePlaceholder": "输入剧集标题...", + "summaryPlaceholder": "输入剧情简介...", + "newEpisode": "新剧集", + "deleteEpisode": "删除剧集", + "deleteConfirm": { + "title": "确认删除", + "message": "确定要删除「{title}」吗?", + "cancel": "取消", + "confirm": "确认删除" + }, + "tip": { + "title": "提示", + "content": "你可以直接编辑标题、简介和内容。点击【确认完成】后,剧集将正式导入到项目中" + } + }, + "collapsePreview": "收起预览", + "expandMore": "展开更多", + "deleteFile": "删除文件", + "fileTooLarge": "文件大小不能超过 10MB", + "docNotSupported": "暂不支持 .doc 格式,请将文件另存为 .docx 或 .txt 格式后重试", + "fileEmpty": "文件内容为空", + "fileReadError": "文件读取失败,请确保文件格式正确", + "uploadFirst": "请先上传文件或粘贴文本", + "analyzeFailed": "分析失败", + "saveFailed": "保存失败", + "cancelConfirm": "确定要取消吗?已分析的剧集将被清空。", + "deleteEpisode": "删除剧集", + "upload": { + "title": "上传原始素材", + "subtitle": "AI 引擎已准备就绪,一键自动分集与格式化", + "maxWords": "(最大支持 3 万字)", + "textInput": "输入文本内容", + "documentUpload": "上传完整文档", + "placeholder": "在此处粘贴你的小说章节或剧本内容...", + "filePlaceholder": "已上传文件模式", + "clickUpload": "点击上传文档", + "clearTextFirst": "请先清空左侧文本", + "supportedFormats": "支持 Word, TXT 格式", + "preview": "预览", + "expandPreview": "展开更多", + "collapsePreview": "收起预览", + "deleteFile": "删除文件", + "startAnalysis": "开始分析", + "back": "返回", + "words": "字" + }, + "errors": { + "fileTooLarge": "文件过大,请上传小于 10MB 的文件", + "docNotSupported": "不支持 .doc 格式,请使用 Word 转换为 .docx", + "fileEmpty": "文件内容为空", + "fileReadError": "文件读取失败,请重试", + "uploadFirst": "请先上传或粘贴内容", + "analyzeFailed": "分析失败", + "saveFailed": "保存失败", + "analysisModelNotConfigured": "请先在设置页面配置分析模型" + }, + "common": { + "edit": "编辑", + "delete": "删除", + "save": "保存", + "cancel": "取消" + }, + "markerDetected": { + "title": "检测到分集标记", + "description": "检测到 {count} 个「{type}」格式的分集标记", + "preview": "预览分集结果", + "useMarker": "使用标识符分集", + "useMarkerDesc": "快速、免费", + "useAI": "使用 AI 智能分集", + "useAIDesc": "智能分析、消耗积分", + "cancel": "取消", + "totalCount": "共 {count} 集", + "markerTypes": { + "episode": "第X集", + "chapter": "第X章", + "act": "第X幕", + "scene": "X-Y【场景】", + "numbered": "数字编号", + "numberedEscaped": "数字编号(转义)", + "numberedDirect": "数字+中文", + "episodeEn": "Episode X", + "chapterEn": "Chapter X", + "boldNumber": "**数字**", + "pureNumber": "纯数字" + } + }, + "globalAnalysis": { + "title": "全局资产分析", + "description": "一键提取全书的角色和场景,确保角色形象在所有剧集中保持一致", + "startButton": "立即分析", + "analyzing": "分析中...", + "success": "全局分析完成:新增 {characters} 个角色,{locations} 个场景", + "failed": "全局分析失败", + "confirmAndAnalyze": "确认并开启全局分析" + } +} \ No newline at end of file diff --git a/messages/zh/stages.json b/messages/zh/stages.json new file mode 100644 index 0000000..d75bd88 --- /dev/null +++ b/messages/zh/stages.json @@ -0,0 +1,7 @@ +{ + "config": "1. 配置", + "assets": "2. 资产分析", + "storyboard": "3. 分镜编辑", + "videos": "4. 视频生成", + "voice": "5. 配音生成" +} \ No newline at end of file diff --git a/messages/zh/storyboard.json b/messages/zh/storyboard.json new file mode 100644 index 0000000..26341ff --- /dev/null +++ b/messages/zh/storyboard.json @@ -0,0 +1,374 @@ +{ + "phases": { + "planning": "规划分镜", + "cinematography": "设计摄影", + "acting": "设计演技", + "detail": "补充细节" + }, + "prompts": { + "imagePrompt": "图片提示词", + "aiInstruction": "AI修改指令", + "supportReference": "(支持@引用资产库)", + "instructionPlaceholder": "例如:把场景改为@医院_白天,人物改为@主角A", + "selectAsset": "选择资产", + "character": "人物", + "location": "场景", + "referencedAssets": "已引用资产:", + "removeAsset": "移除此资产", + "aiModify": "AI修改并生成图片", + "aiModifying": "AI修改中...", + "aiModifyTip": "点击后将自动保存提示词并生成新图片", + "save": "保存", + "currentPrompt": "当前提示词", + "enterInstruction": "请输入修改指令", + "modifyFailed": "操作失败: {error}", + "updateFailed": "更新失败: {error}", + "enterContinuation": "请输入续写内容", + "appendTitle": "续写内容", + "appendDescription": "输入新的SRT内容,系统会自动切分并生成新的镜头,追加到当前列表末尾", + "appendSubmit": "续写并生成镜头", + "appendSuccess": "续写成功!新镜头已追加到列表末尾", + "appendFailed": "续写失败: {error}", + "customStyle": "自定义风格" + }, + "group": { + "generating": "生成中...", + "hasSynced": "✓ 已生成", + "failed": "失败", + "retry": "重试", + "regenerate": "重新生成所有", + "generateAll": "批量生成全部", + "expand": "展开", + "collapse": "收起", + "addPanel": "添加镜头", + "regenerating": "重新生成中...", + "aiAnalyzing": "AI 分析中...", + "regenerateText": "重新生成文字", + "generateMissingImages": "生成该片段所有未有图片的镜头", + "segment": "片段", + "addAtStart": "在开头添加新分镜组", + "insertHere": "在此插入新分镜组" + }, + "header": { + "title": "分镜编辑", + "panels": "个镜头", + "submit": "提交生成", + "submitting": "提交中...", + "storyboardPanel": "分镜面板", + "segments": "个片段", + "segmentsCount": "共 {count} 个片段,", + "panelsCount": "{count} 个镜头", + "generatingStatus": "({count} 个生成中)", + "generateAllPanels": "生成所有镜头", + "generatePendingPanels": "生成{count}个未有图片的镜头", + "downloadAll": "下载全部", + "downloading": "打包中...", + "noImages": "没有可下载的图片", + "downloadAllImages": "下载所有图片", + "generateVideo": "生成视频 →", + "back": "← 返回", + "concurrencyLimit": "并发上限 {count}" + }, + "panel": { + "shotType": "景别:", + "duration": "秒", + "location": "场景:", + "characters": "角色:", + "description": "描述:", + "text": "对应文本:", + "regenerate": "重新生成", + "delete": "删除", + "insertBefore": "在此前插入", + "insertAfter": "在此后插入", + "moveUp": "上移", + "moveDown": "下移", + "plot": "剧情:", + "summary": "总结:", + "pov": "视角:", + "focus": "焦点:", + "mode": "模式:", + "shot": "镜头", + "segment": "片段", + "stylePrompt": "画风/提示词", + "shotMode": "景别/模式", + "regenerateImage": "重新生成图片", + "generateImage": "生成图片", + "cardView": "卡片视图", + "tableView": "表格视图", + "shotTypeLabel": "镜头类型", + "cameraMove": "镜头运动", + "sourceText": "对应原文", + "sceneDescription": "画面描述", + "videoPrompt": "视频提示词", + "videoPromptHint": "建议描述主体动作、环境、镜头语言", + "locationLabel": "场景", + "editLocation": "编辑场景", + "characterLabel": "角色", + "characterLabelWithCount": "角色 ({count})", + "editCharacter": "编辑角色", + "select": "+ 选择", + "add": "+ 添加", + "noLocation": "未选择场景", + "locationNotEdited": "暂未编辑场景", + "noCharacters": "未选择角色", + "charactersNotEdited": "暂未编辑角色", + "shotTypePlaceholder": "俯拍中景...", + "cameraMovePlaceholder": "缓推、固定...", + "videoPromptPlaceholder": "用于视频生成的提示词...", + "sceneDescriptionPlaceholder": "描述画面主体、构图、光线、情绪", + "selectCharacter": "选择角色", + "selectLocation": "选择场景", + "noCharacterAssets": "暂无角色资产", + "noLocationAssets": "暂无场景资产", + "selected": "已选择", + "defaultAppearance": "初始形象", + "newPanelDescription": "新镜头描述", + "noShotType": "未设置镜头" + }, + "image": { + "generating": "生成中...", + "regenerate": "重新生成", + "edit": "编辑", + "editImage": "修图", + "candidate": "候选图", + "selectCandidate": "选择候选", + "variants": "变体", + "generateVariants": "生成变体", + "forceRegenerate": "强制重新生成", + "failed": "生成失败", + "clickToPreview": "点击放大预览", + "enlargePreview": "放大预览", + "candidateCount": "候选 {count}", + "candidateGenerating": "{count} 张生成中", + "selectingCandidate": "选择候选图中...", + "confirmCandidate": "确认选择", + "cancelSelection": "取消选择", + "noValidCandidates": "暂无有效候选图", + "selectCount": "选择生成数量", + "generateMultiple": "生成多张候选", + "generateCount": "生成 {count} 张", + "undoShort": "返回" + }, + "candidate": { + "title": "选择候选图", + "select": "选择", + "cancel": "取消", + "noImages": "暂无候选图", + "original": "原图" + }, + "variant": { + "title": "图片变体", + "generate": "生成变体", + "select": "使用此图", + "close": "关闭", + "shotTitle": "镜头变体 - 基于 #{number}", + "originalDescription": "原镜头描述", + "noDescription": "无描述", + "noImage": "无图片", + "shotNum": "镜头 {number}", + "aiRecommend": "AI 推荐变体", + "reanalyze": "重新分析", + "shotType": "景别:", + "cameraMove": "运镜:", + "generating": "生成中", + "clickToAnalyze": "点击重新分析获取 AI 推荐", + "customInstruction": "或自定义指令", + "customPlaceholder": "输入你想要的镜头效果,如:改为反打视角,聚焦另一个角色的表情...", + "includeCharacter": "引用角色形象", + "includeLocation": "引用场景图", + "customVariant": "自定义变体", + "defaultShotType": "中景", + "defaultCameraMove": "固定", + "useCustomGenerate": "使用自定义生成", + "analyzeFailed": "分析失败", + "creativeScore": "创意 {score}/5" + }, + "insert": { + "title": "插入新镜头", + "position": "插入位置", + "before": "在第 {number} 镜头前", + "after": "在第 {number} 镜头后", + "content": "镜头内容", + "shotType": "景别", + "location": "场景", + "characters": "角色", + "description": "描述", + "text": "对应文本", + "placeholder": { + "shotType": "选择景别...", + "location": "输入场景...", + "characters": "输入角色,用逗号分隔", + "description": "描述画面内容...", + "text": "对应的剧本文本..." + }, + "insert": "插入", + "cancel": "取消" + }, + "common": { + "actions": "操作", + "add": "添加", + "cancel": "取消", + "confirm": "确认", + "copy": "复制", + "delete": "删除", + "download": "下载", + "edit": "编辑", + "generate": "生成", + "loading": "加载中...", + "none": "无", + "unknownError": "未知错误", + "preview": "预览", + "refresh": "刷新", + "regenerate": "重新生成", + "deleting": "删除中", + "editing": "编辑中", + "saving": "保存中...", + "saveFailed": "保存失败,修改尚未同步", + "retrySave": "重试保存", + "save": "保存", + "status": "状态", + "submitFailed": "提交失败", + "upload": "上传" + }, + "confirm": { + "deletePanel": "确定要删除这个镜头吗?删除后无法恢复。", + "deleteGroup": "确定要删除这整组分镜吗?\n\n这将删除该片段下的所有 {count} 个镜头,此操作不可撤销!" + }, + "messages": { + "episodeNotFound": "没有找到剧集信息", + "downloadFailed": "下载失败: {error}", + "panelNotFound": "未找到镜头信息", + "modifyFailed": "修改失败: {error}", + "selectCandidateFailed": "选择失败: {error}", + "insertPanelFailed": "插入分镜失败: {error}", + "addPanelFailed": "添加分镜失败: {error}", + "deletePanelFailed": "删除失败: {error}", + "deleteGroupFailed": "删除分镜组失败: {error}", + "regenerateGroupFailed": "重新生成分镜失败: {error}", + "addGroupFailed": "添加分镜组失败: {error}", + "moveGroupFailed": "移动分镜组失败: {error}", + "batchGenerateCompleted": "批量生成完成:\n成功: {succeeded}\n失败: {failed}\n\n部分错误: {errors}", + "batchGenerateFailed": "批量生成失败: {error}" + }, + "canvas": { + "emptyTitle": "暂无分镜数据", + "emptyDescription": "请先生成Clips和文字分镜,或点击上方按钮添加分镜组" + }, + "imageEdit": { + "title": "编辑分镜", + "subtitle": "输入修改指令,可选择上传参考图片和资产", + "promptPlaceholder": "描述你想要修改的内容,例如:改变背景颜色、调整人物表情...", + "referenceImagesLabel": "参考图片", + "referenceImagesHint": "(可选,支持粘贴)", + "start": "开始编辑", + "selectAsset": "选择资产", + "selectedAssetsLabel": "参考资产", + "selectedAssetsCount": "{count}个", + "addAsset": "添加资产", + "noAssets": "暂无资产,点击“添加资产”选择" + }, + "screenplay": { + "tabs": { + "formatted": "剧本格式", + "original": "原文" + }, + "scene": "场景 {number}", + "characters": "出场角色", + "voiceover": "旁白", + "parseFailedTitle": "剧本格式解析失败", + "parseFailedDescription": "请查看原文内容" + }, + "assets": { + "character": { + "confirming": "确认中...", + "editing": "编辑中..." + }, + "image": { + "undo": "撤销到上一版本" + }, + "location": { + "generateImage": "生成图片" + }, + "stage": { + "analyzing": "分析中..." + } + }, + "video": { + "toolbar": { + "showPending": "待生成" + }, + "panelCard": { + "forceRegenerate": "强制重新生成(卡住时使用)" + } + }, + "smartImport": { + "errors": { + "analyzeFailed": "分析失败" + }, + "preview": { + "reanalyze": "重新分析" + }, + "smartImport": { + "recommended": "推荐" + } + }, + "aiData": { + "title": "AI数据编辑器", + "subtitle": "Panel {number} - 发送给图片生成AI的完整数据", + "basicData": "分镜基础数据", + "shotType": "镜头类型", + "cameraMove": "镜头运动", + "shotTypePlaceholder": "仰拍、全景、平视、中景...", + "cameraMovePlaceholder": "缓推、固定、跟随...", + "scene": "场景(只读)", + "notSelected": "未选择", + "summary": "场景总结", + "characters": "角色(只读)", + "plot": "剧情", + "summarize": "总结", + "visualDescription": "视觉描述", + "videoPrompt": "视频提示词", + "negativePrompt": "负面提示词", + "save": "保存", + "cancel": "取消", + "lightingDirection": "光照方向", + "lightingQuality": "光照质感", + "depthOfField": "景深", + "colorTone": "色调", + "characterPosition": "角色位置规则", + "position": "位置", + "posture": "姿势", + "facing": "朝向", + "photographyRules": "摄影规则 (photography_rules)", + "viewData": "查看数据", + "jsonPreview": "JSON 预览", + "actingNotes": "演技指导 (acting_notes)", + "actingTitle": "演技指导", + "actingDescription": "表演指令", + "noActingData": "无演技数据" + }, + "insertModal": { + "insertBetween": "在 #{before} 和 #{after} 之间插入", + "panel": "镜头", + "noImage": "无图片", + "insertAtEnd": "末尾", + "aiAnalyze": "AI 自动分析", + "analyzing": "AI 分析中...", + "insert": "插入", + "inserting": "插入中...", + "placeholder": "可选:输入补充说明,如添加一个反应镜头..." + }, + "panelActions": { + "insertPanel": "插入分镜", + "panelVariant": "镜头变体", + "insertHere": "在此处插入分镜", + "generateVariant": "基于此镜头生成变体", + "needImage": "需要先生成图片", + "deleteShot": "删除镜头", + "pasteSrtPlaceholder": "粘贴新的SRT内容..." + }, + "firstLastFrame": { + "placeholder": "输入首尾帧视频提示词...", + "modelTitle": "首尾帧模型" + } +} diff --git a/messages/zh/video.json b/messages/zh/video.json new file mode 100644 index 0000000..076f435 --- /dev/null +++ b/messages/zh/video.json @@ -0,0 +1,210 @@ +{ + "panelCard": { + "play": "播放", + "pause": "暂停", + "retry": "重试", + "regenerate": "重新生成", + "download": "下载", + "edit": "编辑", + "save": "保存", + "cancel": "取消", + "generating": "生成中...", + "failed": "失败", + "lipSync": "口型同步", + "lipSyncVideo": "口型同步视频", + "lipSyncLabel": "口型同步", + "lipSyncTitle": "口型同步", + "original": "原始", + "synced": "同步", + "videoFixed": "✓ 视频", + "imagePreview": "图片预览", + "playVoice": "播放配音", + "stopVoice": "停止", + "noVoice": "暂无配音", + "forceRegenerate": "强制重新生成(卡住时使用)", + "regenerateVideo": "重新生成视频", + "lipSyncStatus": "口型同步中...", + "lipSyncInProgress": "正在进行口型同步...", + "lipSyncMayTakeMinutes": "这可能需要几分钟时间", + "audioEnabled": "音频已开启", + "audioDisabled": "音频已关闭", + "isSynced": "(已同步)", + "needVideo": "(请先生成视频)", + "needAudio": "(请先生成音频)", + "generateAudio": "生成音频", + "regenerateLipSync": "重新生成口型同步", + "editPrompt": "编辑提示词", + "clickToEditPrompt": "点击编辑提示词...", + "shot": "镜头 {number}", + "unknownShotType": "未知景别", + "correspondingText": "对应原文", + "generateVideo": "生成视频", + "selectModel": "选择视频模型", + "selectVoice": "选择要使用的配音:", + "willAutoPad": "(将自动填充)", + "autoPadding": "填充", + "redo": "重新", + "generatingAudio": "生成中", + "error": { + "audioFailed": "生成音频失败" + }, + "batchMode": "批量模式", + "batchModeDesc": "离线推理,价格便宜50%,24小时内完成", + "batchModeEnabled": "已开启批量模式", + "batchModeDisabled": "批量模式已关闭" + }, + "promptModal": { + "title": "编辑镜头 #{number} 视频提示词", + "shotType": "景别:", + "duration": "秒", + "location": "场景:", + "locationUnknown": "未知", + "characters": "角色:", + "charactersNone": "无", + "description": "描述:", + "text": "对应文本:", + "promptLabel": "视频提示词", + "placeholder": "输入视频提示词...", + "tip": "提示:视频模型不识别角色名字,请用外貌特征描述,如\"黑发蓝眸的年轻男子\"而非\"Victor\"", + "save": "保存", + "cancel": "取消" + }, + "toolbar": { + "title": "成片生成", + "filter": "筛选", + "viewAll": "查看全部", + "showGenerated": "已生成", + "showPending": "待生成", + "showFailed": "失败", + "totalShots": "共 {count} 个镜头", + "generatingShots": "{count} 个生成中", + "completedShots": "{count} 个已生成", + "failedShots": "{count} 个失败", + "generateAll": "生成所有视频", + "batchConfigTitle": "批量生成参数", + "batchConfigDesc": "先选择模型与参数,再一键生成所有视频", + "confirmGenerateAll": "确认并生成全部", + "confirming": "提交中...", + "noVideos": "没有可下载的视频", + "downloadCount": "下载 {count} 个视频", + "packing": "打包中...", + "downloadAll": "下载全部", + "enterEditor": "进入视频剪辑器", + "enterEdit": "进入剪辑", + "back": "返回" + }, + "stage": { + "title": "视频生成", + "generateAll": "批量生成全部", + "regenerateFailed": "重试失败项", + "downloadAll": "下载全部视频", + "enterEditor": "进入剪辑器", + "lipSyncStatus": "口型同步中...", + "hasSynced": "✓ 已生成", + "generating": "生成中...", + "downloading": "下载中...", + "downloadProgress": "正在准备视频文件... {current}/{total}", + "noVideos": "暂无已生成的视频", + "scrollTo": "跳转到镜头", + "error": { + "saveFailed": "保存视频提示词失败", + "lipSyncFailed": "口型同步失败", + "fetchVideosFailed": "获取视频列表失败" + }, + "downloadFailed": "下载失败", + "unknownError": "未知错误" + }, + "firstLastFrame": { + "title": "首尾帧设置", + "firstFrame": "首帧", + "lastFrame": "尾帧", + "range": "镜头 {from} → 镜头 {to}", + "link": "链接", + "unlink": "解除链接", + "unlinkAction": "取消链接", + "asLastFrameFor": "作为镜头 {number} 的尾帧", + "asFirstFrameFor": "作为镜头 {number} 的首帧", + "customPrompt": "自定义提示词", + "promptPlaceholder": "输入首尾帧视频提示词...", + "useDefault": "使用默认", + "generate": "生成首尾帧视频", + "generated": "首尾帧视频已生成", + "model": "模型", + "withAudio": "包含音频", + "audioOn": "开", + "audioOff": "关", + "linkToNext": "链接到下一镜头(首尾帧)", + "asLastFrame": "作为镜头 {number} 的尾帧", + "thenTransitionTo": "然后镜头转换到" + }, + "editor": { + "alert": { + "saveSuccess": "保存成功", + "saveFailed": "保存失败", + "exportStarted": "导出任务已开始,请稍候...", + "exportFailed": "导出失败" + }, + "toolbar": { + "back": "← 返回", + "saveDirty": "保存 *", + "saved": "已保存", + "export": "导出视频" + }, + "left": { + "title": "素材库", + "description": "从视频阶段导入的片段将显示在这里" + }, + "right": { + "title": "属性", + "clipLabel": "片段:", + "clipFallback": "片段 {index}", + "durationLabel": "时长:", + "transitionLabel": "转场到下一片段", + "deleteConfirm": "确定删除此片段?", + "deleteClip": "删除片段", + "selectClipHint": "选择一个片段查看属性" + }, + "preview": { + "emptyStartEditing": "添加素材开始编辑" + }, + "timeline": { + "zoomLabel": "缩放:", + "videoTrack": "视频", + "emptyHint": "从素材库拖拽视频片段到这里", + "audioTrack": "配音", + "audioBadge": "配" + }, + "transition": { + "title": "转场效果", + "duration": "持续时间", + "options": { + "none": "无", + "dissolve": "溶解", + "fade": "淡入淡出", + "slide": "滑动" + } + } + }, + "errors": { + "unknownError": "未知错误" + }, + "capability": { + "generationMode": "生成模式", + "generateAudio": "生成音频", + "duration": "时长", + "fps": "帧率", + "resolution": "分辨率", + "aspectRatio": "画幅比例", + "reasoningEffort": "推理强度", + "voice": "音色", + "rate": "语速", + "mode": "模式" + }, + "unit": { + "second": "秒", + "frame": "帧" + }, + "common": { + "generate": "生成" + } +} \ No newline at end of file diff --git a/messages/zh/voice.json b/messages/zh/voice.json new file mode 100644 index 0000000..332fdc4 --- /dev/null +++ b/messages/zh/voice.json @@ -0,0 +1,254 @@ +{ + "title": "台词配音", + "linesCount": "共 {count} 条台词,", + "audioGeneratedCount": "{count} 条已生成音频", + "emotionPrompt": "情绪提示词", + "emotionPromptTip": "(不填则使用台词自参考)", + "emotionPlaceholder": "如:laugh,仅支持英文...", + "emotionStrength": "情绪强度", + "flat": "平淡", + "intense": "强烈", + "generating": "生成中...", + "generateVoice": "生成语音", + "toolbar": { + "back": "← 返回", + "analyzeLines": "分析台词", + "addLine": "+ 添加语音", + "generateAll": "一键生成所有配音", + "downloadAll": "下载配音", + "generatingCount": "生成中 ({count})", + "packing": "打包中...", + "stats": "共 {total} 条台词 | 已设置音色 {withVoice} 条 | 已生成配音 {withAudio} 条", + "noDownload": "没有可下载的配音", + "downloadCount": "下载 {count} 条配音", + "uploadReferenceHint": "请先在资产库为所有角色上传参考音频" + }, + "speakerVoice": { + "title": "发言人音色状态", + "hint": "请在资产库为角色上传参考音频", + "linesCount": "{count} 条台词", + "noVoice": "无参考音色", + "configured": "✓ 已设置", + "playVoice": "播放当前音色", + "aiDesign": "AI设计声音", + "aiDesignVoice": "AI 设计音色", + "redesign": "使用 AI 重新设计声音", + "uploadAudio": "上传音频", + "uploading": "上传中", + "upload": "上传", + "microsoftVoice": "微软语音", + "microsoft": "微软", + "maleVoices": "男声", + "femaleVoices": "女声", + "openAssetLibrary": "资产库", + "configuredStatus": "已设置音色", + "pendingStatus": "待设置音色", + "voiceSettings": "音色设置", + "inlineLabel": "内联" + }, + "inlineBinding": { + "title": "为「{speaker}」设置音色", + "description": "该发言人不在资产库中,请选择一种方式为其设置参考音色", + "selectFromLibrary": "从音色库选择", + "selectFromLibraryDesc": "选择已有的全局音色", + "uploadAudio": "上传参考音频", + "uploadAudioDesc": "上传 MP3、WAV 等音频文件作为参考音色", + "aiDesign": "AI 设计音色", + "aiDesignDesc": "使用 AI 生成专属参考音色" + }, + "embedded": { + "linesStats": "{total} 条台词 · {audio} 已生成", + "reanalyze": "重新分析", + "analyzeLines": "分析台词", + "reanalyzeHint": "重新分析台词并更新镜头匹配", + "analyzeHint": "从原文中提取台词", + "downloadVoice": "下载配音", + "generateAllVoice": "生成全部配音", + "pendingCount": "({count} 条待生成)", + "generatingProgress": "生成中 ({current}/{total})", + "generatingHint": "正在生成中...", + "noVoiceHint": "请先在上方为所有角色设置音色", + "noLinesHint": "没有台词可生成", + "allDoneHint": "所有台词已生成完成", + "generateHint": "点击生成 {count} 条待生成的配音", + "addLine": "+ 添加语音", + "speakerVoiceStatus": "角色音色状态", + "speakersCount": "{count} 个", + "listen": "试听", + "listenVoice": "试听音色", + "reset": "重设", + "resetDesign": "重新设计", + "aiDesign": "AI设计", + "assetLibrary": "资产库" + }, + "lineCard": { + "generatingVoice": "生成中", + "speaker": "发言人", + "speakerPlaceholder": "发言人名称", + "content": "台词内容", + "contentPlaceholder": "台词内容", + "emotionConfigured": "情绪已设置", + "emotionSettings": "情绪设置", + "voiceConfigured": "✓ 已设置", + "needVoice": "请在上方设置音色", + "locatePanel": "定位到绑定镜头", + "locateVideo": "定位视频", + "play": "播放", + "pause": "暂停", + "locatePanelCta": "定位到镜头 {index}", + "editLine": "编辑台词", + "deleteLine": "删除台词", + "deleteAudio": "删除配音" + }, + "lineEditor": { + "addTitle": "添加语音", + "editTitle": "编辑语音", + "contentLabel": "台词内容", + "contentPlaceholder": "请输入台词内容", + "speakerLabel": "发言人", + "speakerPlaceholder": "请输入发言人名称", + "selectSpeaker": "请选择发言人", + "noSpeakerOptions": "当前项目暂无可选发言人,请先分析台词生成发言人", + "bindPanelLabel": "绑定镜头", + "unboundPanel": "未绑定镜头", + "panelLabel": "镜头 {index}", + "saveAdd": "添加语音", + "saveEdit": "保存修改" + }, + "empty": { + "title": "暂无台词数据", + "description": "从剧本中提取台词和发言人", + "analyzeButton": "分析台词", + "hint": "请先在资产库为角色上传参考音频" + }, + "confirm": { + "deleteLine": "确定要删除这条台词吗?\n\n\"{content}\"\n\n此操作不可撤销。", + "deleteAudio": "确定要删除这条台词的配音吗?\n\n\"{content}\"\n\n此操作不可撤销。" + }, + "errors": { + "saveFailed": "保存失败", + "analyzeFailed": "分析台词失败", + "generateFailed": "生成配音失败", + "batchFailed": "批量生成失败", + "downloadFailed": "下载失败", + "deleteFailed": "删除失败", + "addFailed": "添加语音失败", + "invalidLineInput": "台词内容和发言人不能为空", + "bindFailed": "绑定镜头失败", + "deleteAudioFailed": "删除配音失败", + "uploadFailed": "上传音频失败", + "voiceDesignFailed": "保存声音设计失败", + "emotionSaveFailed": "保存情绪设置失败", + "voiceGenerateFailed": "生成音频失败" + }, + "alerts": { + "insufficientBalance": "余额不足", + "insufficientBalanceMsg": "账户余额不足,请充值后继续使用", + "noLinesToGenerate": "没有需要生成的台词(请先为角色上传参考音频)", + "generateComplete": "生成完成:{success}/{total} 条成功", + "generateFailed": "{count} 条失败", + "speakerVoiceSet": "已为 {speaker} 生成参考音频", + "speakerVoiceUploaded": "已为 {speaker} 上传参考音频", + "voiceDesignSet": "已为 {speaker} 设置 AI 设计的声音" + }, + "common": { + "loading": "加载中...", + "save": "保存", + "cancel": "取消", + "cancelling": "取消中...", + "upload": "上传", + "download": "下载", + "generate": "生成", + "regenerate": "重新生成" + }, + "assets": { + "image": { + "uploadFailed": "上传失败" + }, + "stage": { + "analyzing": "分析中..." + } + }, + "smartImport": { + "errors": { + "analyzeFailed": "分析失败" + } + }, + "video": { + "panelCard": { + "play": "播放" + } + }, + "tts": { + "generatedAudio": "生成的音频", + "browserNotSupport": "您的浏览器不支持音频播放", + "audioDuration": "音频时长:", + "subtitleCount": "字幕条数:", + "noAudio": "暂无音频", + "srtPreview": "SRT字幕预览", + "noSubtitle": "暂无字幕", + "stats": "生成统计", + "minute": "分", + "second": "秒", + "items": "条", + "completed": "✓ 已完成", + "regenerating": "重新生成中...", + "regenerateTTS": "重新生成TTS", + "nextStep": "下一步: 分析资产", + "readyTip": "点击进入资产分析阶段", + "needGenerate": "请先生成TTS音频" + }, + "voiceCreate": { + "aiDesignMode": "AI 设计音色", + "uploadMode": "上传音频", + "dropOrClick": "拖放文件或点击选择", + "supportedFormats": "支持 MP3、WAV、OGG、M4A、AAC 格式", + "invalidFileType": "不支持的文件格式,请上传音频文件", + "fileTooLarge": "文件过大,最大支持 50MB", + "previewAudio": "试听音频", + "uploading": "上传中...", + "uploadFailed": "上传失败", + "uploadSuccess": "上传成功" + }, + "voiceDesign": { + "presets": { + "maleBroadcaster": "男播音", + "gentleFemale": "温柔女", + "matureMale": "成熟男", + "livelyFemale": "活泼女", + "intellectualFemale": "知性女", + "narrator": "旁白" + }, + "presetsPrompts": { + "maleBroadcaster": "沉稳的中年男性播音员,音色低沉浑厚,语速平稳,吐字清晰", + "gentleFemale": "温柔甜美的年轻女性,声音清脆悦耳,语调轻柔", + "matureMale": "成熟稳重的男性,声音富有磁性和感染力", + "livelyFemale": "活泼开朗的少女,声音甜美可爱,充满活力", + "intellectualFemale": "知性优雅的女性,声音清晰悦耳,语调平和", + "narrator": "富有感情的叙述者,声音温暖有故事感" + }, + "defaultPreviewText": "你好,很高兴认识你。这是AI为你专属设计的声音,让我来为你展示它的特点。无论是温柔的对话,还是激动的讲述,我都能完美呈现。希望你喜欢这个声音,让我们一起创造精彩的内容吧。", + "pleaseSelectStyle": "请输入或选择声音风格", + "designVoiceFor": "为「{speaker}」设计AI声音", + "hasExistingVoice": "已有声音", + "selectStyle": "选择声音风格:", + "orCustomDescription": "或自定义描述:", + "describePlaceholder": "描述声音特征:年龄、性别、音色、语调...", + "editPreviewText": "修改预览文本", + "generate3Schemes": "生成 3 个声音方案", + "generating3Schemes": "正在生成 3 个声音方案...", + "estimatedTime": "预计 15-30 秒", + "selectScheme": "选择声音方案:", + "schemeN": "方案 {n}", + "regenerate": "重新生成", + "confirmUse": "✓ 确认使用", + "confirmReplace": "确认替换声音?", + "replaceWarning": "的原有声音,不可撤回", + "confirmReplaceBtn": "确认替换", + "noVoiceGenerated": "未能生成任何声音", + "generationError": "生成声音失败", + "generateFailed": "生成第 {n} 个声音失败", + "preview": "试听", + "playing": "播放中" + } +} \ No newline at end of file diff --git a/messages/zh/workspace.json b/messages/zh/workspace.json new file mode 100644 index 0000000..ae2b76c --- /dev/null +++ b/messages/zh/workspace.json @@ -0,0 +1,31 @@ +{ + "title": "我的项目", + "subtitle": "管理您的AI动漫制作项目", + "newProject": "新建项目", + "searchPlaceholder": "搜索项目名称或描述...", + "searchButton": "搜索", + "clearButton": "清除", + "updatedAt": "更新于", + "noProjects": "还没有项目", + "noProjectsDesc": "创建您的第一个AI动漫制作项目", + "noResults": "没有找到匹配的项目", + "noResultsDesc": "尝试使用不同的搜索词", + "createProject": "新建项目", + "editProject": "编辑项目", + "deleteProject": "删除项目", + "deleteConfirm": "确定要删除项目\"{name}\"吗?此操作无法撤销。", + "projectName": "项目名称", + "projectNamePlaceholder": "输入项目名称", + "projectDescription": "项目描述(可选)", + "projectDescriptionPlaceholder": "输入项目描述", + "creating": "创建中...", + "saving": "保存中...", + "createFailed": "创建项目失败", + "updateFailed": "更新项目失败", + "deleteFailed": "删除项目失败", + "totalProjects": "共 {count} 个项目", + "statsEpisodes": "章节数", + "statsImages": "图片数", + "statsVideos": "视频数", + "noContent": "暂无内容" +} diff --git a/messages/zh/workspaceDetail.json b/messages/zh/workspaceDetail.json new file mode 100644 index 0000000..43062b1 --- /dev/null +++ b/messages/zh/workspaceDetail.json @@ -0,0 +1,24 @@ +{ + "globalAssets": "全局资产", + "createFailed": "创建失败", + "deleteFailed": "删除失败", + "renameFailed": "重命名失败", + "refreshFailed": "刷新失败", + "projectNotFound": "项目不存在", + "backToWorkspace": "返回工作区", + "episode": "剧集", + "sidebar": { + "dragToMove": "拖动调整位置", + "listTitle": "剧集列表", + "episodeCount": "{count}集", + "empty": "暂无剧集,点击下方创建", + "save": "保存", + "deleteConfirm": "确定删除「{name}」?", + "delete": "删除", + "cancel": "取消", + "rename": "重命名", + "newEpisodePlaceholder": "输入剧集名称...", + "create": "创建", + "addEpisode": "添加剧集" + } +} diff --git a/messages/zh/worldContextModal.json b/messages/zh/worldContextModal.json new file mode 100644 index 0000000..ceae1ef --- /dev/null +++ b/messages/zh/worldContextModal.json @@ -0,0 +1,6 @@ +{ + "title": "世界观与人设", + "description": "定义全局通用的角色外貌、场景风格和环境描述", + "placeholder": "例如:\n【男主】张三,25岁,黑色短发,总是穿着一件洗得发白的牛仔夹克,眼神忧郁。\n【女主】李四,22岁,红发双马尾,性格活泼,喜欢穿洛丽塔风格的裙子。\n【场景】2077年的赛博朋克城市,霓虹灯闪烁,终年下雨...", + "hint": "这些设定将被所有剧集继承,作为AI绘画的基础参考。" +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..a22fdc0 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,12 @@ +import createMiddleware from 'next-intl/middleware'; +import { routing } from './src/i18n/routing'; + +export default createMiddleware(routing); + +export const config = { + // 匹配所有路径,除了 api、_next/static、_next/image、favicon.ico 等 + matcher: [ + // 匹配所有路径 + '/((?!api|m|_next/static|_next/image|favicon.ico|.*\\.png|.*\\.jpg|.*\\.jpeg|.*\\.svg|.*\\.gif|.*\\.ico).*)' + ] +}; diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..d6da152 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,15 @@ +import type { NextConfig } from "next"; +import createNextIntlPlugin from 'next-intl/plugin'; + +const withNextIntl = createNextIntlPlugin('./src/i18n.ts'); + +const nextConfig: NextConfig = { + // 已删除 ignoreBuildErrors / ignoreDuringBuilds,构建保持严格门禁 + // Next 15 的 allowedDevOrigins 是顶层配置,不属于 experimental + allowedDevOrigins: [ + 'http://192.168.31.218:3000', + 'http://192.168.31.*:3000', + ], +}; + +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c1bfb22 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15490 @@ +{ + "name": "waoowaoo", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "waoowaoo", + "version": "0.1.0", + "dependencies": { + "@ai-sdk/google": "^3.0.22", + "@ai-sdk/openai": "^3.0.26", + "@bull-board/api": "^6.16.4", + "@bull-board/express": "^6.16.4", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@fal-ai/client": "^1.7.2", + "@google/genai": "^1.34.0", + "@next-auth/prisma-adapter": "^1.0.7", + "@openrouter/sdk": "^0.3.11", + "@prisma/client": "^6.19.2", + "@remotion/cli": "^4.0.405", + "@remotion/player": "^4.0.405", + "@tanstack/react-query": "^5.90.20", + "@types/archiver": "^7.0.0", + "@types/bcryptjs": "^3.0.0", + "@types/express": "^5.0.6", + "@vercel/og": "^0.8.6", + "@vercel/speed-insights": "^1.3.1", + "ai": "^6.0.77", + "archiver": "^7.0.1", + "bcryptjs": "^3.0.2", + "bullmq": "^5.67.3", + "cos-nodejs-sdk-v5": "^2.15.4", + "express": "^5.2.1", + "file-saver": "^2.0.5", + "ioredis": "^5.9.2", + "jszip": "^3.10.1", + "lucide-react": "^0.575.0", + "mammoth": "^1.11.0", + "mysql2": "^3.15.1", + "next": "^15.5.7", + "next-auth": "^4.24.11", + "next-intl": "^4.7.0", + "openai": "^6.8.1", + "prisma": "^6.16.2", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-hot-toast": "^2.6.0", + "remotion": "^4.0.405" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/file-saver": "^2.0.7", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitest/coverage-v8": "^2.1.8", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "eslint": "^9", + "eslint-config-next": "15.5.4", + "rimraf": "^6.1.2", + "tailwindcss": "^4", + "tsx": "^4.20.5", + "typescript": "^5", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=18.18.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.39", + "resolved": "https://registry.npmmirror.com/@ai-sdk/gateway/-/gateway-3.0.39.tgz", + "integrity": "sha512-SeCZBAdDNbWpVUXiYgOAqis22p5MEYfrjRw0hiBa5hM+7sDGYQpMinUjkM8kbPXMkY+AhKLrHleBl+SuqpzlgA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.14", + "@vercel/oidc": "3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/google": { + "version": "3.0.22", + "resolved": "https://registry.npmmirror.com/@ai-sdk/google/-/google-3.0.22.tgz", + "integrity": "sha512-g1N5P/jfTiH4qwdv4WT3hkKzzAbITFz457NomtBfjP8Q3SCzdbU9oPK5ACBMG8RN5mc2QPL6DLtM3Hf5T8KPmw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.14" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/openai": { + "version": "3.0.26", + "resolved": "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-3.0.26.tgz", + "integrity": "sha512-W/hiwxIfG29IO0Fob1HwWpFssMsNrxWoX8A7DwNGOtKArDBmJNuGzQeU/k0Fnh8WyvZEnfxkjO4oXkSXfVBayg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.14" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-4.0.14.tgz", + "integrity": "sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider-utils/node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.1", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bull-board/api": { + "version": "6.16.4", + "resolved": "https://registry.npmmirror.com/@bull-board/api/-/api-6.16.4.tgz", + "integrity": "sha512-fn4O+QbA3mRj0rEE41mvwbvtiiv0UYgnxQ9ErWb9n74EwIC/yZbiyxQ+Gh/ehU9u7B0PuaNyR0IOG/h3DGo1Mg==", + "license": "MIT", + "dependencies": { + "redis-info": "^3.1.0" + }, + "peerDependencies": { + "@bull-board/ui": "6.16.4" + } + }, + "node_modules/@bull-board/express": { + "version": "6.16.4", + "resolved": "https://registry.npmmirror.com/@bull-board/express/-/express-6.16.4.tgz", + "integrity": "sha512-znKZGrqBtHh3iU73TvJherEY1OforQ10hcLMGer1ktRTD+5BxyLedIlhdZIxJsn+ComQQcmEySbqJSF2b78UOA==", + "license": "MIT", + "dependencies": { + "@bull-board/api": "6.16.4", + "@bull-board/ui": "6.16.4", + "ejs": "^3.1.10", + "express": "^5.2.0" + } + }, + "node_modules/@bull-board/ui": { + "version": "6.16.4", + "resolved": "https://registry.npmmirror.com/@bull-board/ui/-/ui-6.16.4.tgz", + "integrity": "sha512-5Yv+4g0rDvBBq2RxaUewSEwD8ywvqCX6lKlzPM5Aaf0+4cxGoENQRZNcBaAIKX4+fAzAbdVB4VGP4NUgtx5LVg==", + "license": "MIT", + "dependencies": { + "@bull-board/api": "6.16.4" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.36.0", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fal-ai/client": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@fal-ai/client/-/client-1.7.2.tgz", + "integrity": "sha512-RZ1Qz2Kza4ExKPy2D+2UUWthNApe+oZe8D1Wcxqleyn4F344MOm8ibgqG2JSVmybEcJAD4q44078WYfb6Q9c6w==", + "license": "MIT", + "dependencies": { + "@msgpack/msgpack": "^3.0.0-beta2", + "eventsource-parser": "^1.1.2", + "robot3": "^0.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmmirror.com/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmmirror.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmmirror.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.10", + "resolved": "https://registry.npmmirror.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@google/genai": { + "version": "1.34.0", + "resolved": "https://registry.npmmirror.com/@google/genai/-/genai-1.34.0.tgz", + "integrity": "sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.24.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", + "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", + "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", + "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", + "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", + "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", + "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", + "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", + "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", + "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", + "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", + "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", + "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", + "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", + "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", + "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", + "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", + "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.5.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", + "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", + "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", + "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.5.0.tgz", + "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "license": "MIT" + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@msgpack/msgpack/-/msgpack-3.1.2.tgz", + "integrity": "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next-auth/prisma-adapter": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz", + "integrity": "sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==", + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3", + "next-auth": "^4" + } + }, + "node_modules/@next/env": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.4", + "resolved": "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.4.tgz", + "integrity": "sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmmirror.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@openrouter/sdk": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@openrouter/sdk/-/sdk-0.3.11.tgz", + "integrity": "sha512-lXhyY+AFvrOjAqg6fktd/3S/6J5u5yp1nYyP6yluqVTzgnsfZs8/QM3rGa0kZdhB4d/fzJhyT+4oGNM55fH31g==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.2", + "resolved": "https://registry.npmmirror.com/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.16.2", + "resolved": "https://registry.npmmirror.com/@prisma/config/-/config-6.16.2.tgz", + "integrity": "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.16.2", + "resolved": "https://registry.npmmirror.com/@prisma/debug/-/debug-6.16.2.tgz", + "integrity": "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==" + }, + "node_modules/@prisma/engines": { + "version": "6.16.2", + "resolved": "https://registry.npmmirror.com/@prisma/engines/-/engines-6.16.2.tgz", + "integrity": "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.2", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "resolved": "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", + "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.16.2", + "resolved": "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-6.16.2.tgz", + "integrity": "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==", + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.2", + "resolved": "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-6.16.2.tgz", + "integrity": "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==", + "dependencies": { + "@prisma/debug": "6.16.2" + } + }, + "node_modules/@remotion/bundler": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/bundler/-/bundler-4.0.405.tgz", + "integrity": "sha512-F3eje7r7EIe0Wccd3Azq++0CMXZK8Tck89FDaYzipD4r/cKVxZXvxockHgo4tr78gPwJA83QIhHEcV+EDT5+TQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/media-parser": "4.0.405", + "@remotion/studio": "4.0.405", + "@remotion/studio-shared": "4.0.405", + "css-loader": "5.2.7", + "esbuild": "0.25.0", + "react-refresh": "0.9.0", + "remotion": "4.0.405", + "source-map": "0.7.3", + "style-loader": "4.0.0", + "webpack": "5.96.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/cli": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/cli/-/cli-4.0.405.tgz", + "integrity": "sha512-C0+umOrUSOt3FtDRPJrC+4wLfOshvSR0Okg49mxMWyoh5ptTbog5uIMeA0CrTwFU0vnca6KKTSVYKdIpT0+oSw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/bundler": "4.0.405", + "@remotion/media-utils": "4.0.405", + "@remotion/player": "4.0.405", + "@remotion/renderer": "4.0.405", + "@remotion/studio": "4.0.405", + "@remotion/studio-server": "4.0.405", + "@remotion/studio-shared": "4.0.405", + "dotenv": "9.0.2", + "minimist": "1.2.6", + "prompts": "2.4.2", + "remotion": "4.0.405" + }, + "bin": { + "remotion": "remotion-cli.js", + "remotionb": "remotionb-cli.js", + "remotiond": "remotiond-cli.js" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/cli/node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/@remotion/cli/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "license": "MIT" + }, + "node_modules/@remotion/compositor-darwin-arm64": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.405.tgz", + "integrity": "sha512-bdoRy0XOMy5KJGAR5xolmewn077GOQPVqxr6liaJzasBFFdVd3w/IHvW1zlSwZbQtOj5YhJkaCQWta6WdC50wg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.405.tgz", + "integrity": "sha512-C+gv1V02AJqW2vf0fMhKJjVP7ddnyCzGeOlQVqs9vprQTMs+ypTp6taoZZZCXDCeviUNe+mhUdcFYklE0ZdGBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.405.tgz", + "integrity": "sha512-DesqHFcRa8PnZ5QVOGX1THqxPig+fpn3TMFmBldiaSrpbqzKfABf18VfdNl7zfMfILXtuxXyw4sStBcP3AiUGQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.405.tgz", + "integrity": "sha512-bt4HXk/FfS6g5SVZ9eP6Lbm74qI5/McapVcJgTjZTiA5t0Hhk+sEdE9ylKJFP0MtQoVKot+WwUlZxHpTpPjs7w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.405.tgz", + "integrity": "sha512-GGwYiIIeKFl1LnzgypaMoj1Vq/eHEF2SJSfXm4djZo2ioi9XV1zm09Re1n3HeaMxTh2nz2kgthbLdB99MYGYwA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.405.tgz", + "integrity": "sha512-hENllpG0QacXbBFxaSZgIarXz7iL8y/D/T1Oz/uZfLHnat1XZJtPIp8G3xd1+lzFtIDD2Y5UUzZ6agGjK/1ugw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.405.tgz", + "integrity": "sha512-dwaD/5BGZsjL4a1c7+rC60NdyqG6GhTtt1hXDWuRNjBjdQrR7PLf2Y2FeoSPDbTj9shgLCSGoqBVDZqmhMiQKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@remotion/licensing": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/licensing/-/licensing-4.0.405.tgz", + "integrity": "sha512-FGq5QNYstAoNNwNJlPvsNovxOCCabW4sRefHqNtd8o1oiVz050xgzaJ+hncP72esg+QMZKxfBZSTeYt1b0AlIw==", + "license": "MIT" + }, + "node_modules/@remotion/media-parser": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/media-parser/-/media-parser-4.0.405.tgz", + "integrity": "sha512-yiy6zEM7GQY/pYDKNP0CaTvO34RD7yJVGTOjKnIGydGaeoe4ACGj/6GdZrOfX8HC05TnQuZNAAytPcje0azg7w==", + "license": "Remotion License https://remotion.dev/license" + }, + "node_modules/@remotion/media-utils": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/media-utils/-/media-utils-4.0.405.tgz", + "integrity": "sha512-IEBfQBlR8MoIS5SmY0FECUK2VP9iLxlX9dmXudjeDIhtGVgU7Jy/SyTT9yk0kYwrjPAbM8pY1scna+QelxtZyw==", + "license": "MIT", + "dependencies": { + "@remotion/media-parser": "4.0.405", + "@remotion/webcodecs": "4.0.405", + "mediabunny": "1.27.3", + "remotion": "4.0.405" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/player": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/player/-/player-4.0.405.tgz", + "integrity": "sha512-EtQJUnWU6fAwXCUaQgem8AWNWbdsi0g2AYFC9LqYbltGOkQDkNmPsSp0KTahv44L2kC0HUZ/8Cyrr//O9qKtBA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "remotion": "4.0.405" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/renderer/-/renderer-4.0.405.tgz", + "integrity": "sha512-N9X0BwfudPGTh0JK3kEz8POhAsx0gn8pmZj2oR/cwL5pUnQ4qTltlUS/2ikBcvoF4788CBE1ezEnNEV/6BLVgw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/licensing": "4.0.405", + "@remotion/streaming": "4.0.405", + "execa": "5.1.1", + "extract-zip": "2.0.1", + "remotion": "4.0.405", + "source-map": "^0.8.0-beta.0", + "ws": "8.17.1" + }, + "optionalDependencies": { + "@remotion/compositor-darwin-arm64": "4.0.405", + "@remotion/compositor-darwin-x64": "4.0.405", + "@remotion/compositor-linux-arm64-gnu": "4.0.405", + "@remotion/compositor-linux-arm64-musl": "4.0.405", + "@remotion/compositor-linux-x64-gnu": "4.0.405", + "@remotion/compositor-linux-x64-musl": "4.0.405", + "@remotion/compositor-win32-x64-msvc": "4.0.405" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remotion/renderer/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@remotion/streaming": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/streaming/-/streaming-4.0.405.tgz", + "integrity": "sha512-IA1trBPhhP9Uwg2TOQ09uyYe5CSV4d3KAaYU2KHdPwvUer2bMo25d8TGsrus1MDb5rkIvB6gl0ln22KAgqARrQ==", + "license": "MIT" + }, + "node_modules/@remotion/studio": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/studio/-/studio-4.0.405.tgz", + "integrity": "sha512-+QWPbny3GWwY2+vkWmlXyKPh6NYiB9u1+bYb8XBw2b9AVYhApnczb1/z+c8xD+7BEQ4e1iBUZRngDk4CaizsIA==", + "license": "MIT", + "dependencies": { + "@remotion/media-utils": "4.0.405", + "@remotion/player": "4.0.405", + "@remotion/renderer": "4.0.405", + "@remotion/studio-shared": "4.0.405", + "@remotion/web-renderer": "4.0.405", + "@remotion/zod-types": "4.0.405", + "mediabunny": "1.27.3", + "memfs": "3.4.3", + "open": "^8.4.2", + "remotion": "4.0.405", + "semver": "7.5.3", + "source-map": "0.7.3", + "zod": "3.22.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/studio-server": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/studio-server/-/studio-server-4.0.405.tgz", + "integrity": "sha512-Z68aa+0e2D4PjslQBsmVfw66ZjrTUPuBWdHjDHlnAhYQa8AWUxzGVx1ktE4/Zx+en0flrO7vUNCOcCrR7zs9Rw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@remotion/bundler": "4.0.405", + "@remotion/renderer": "4.0.405", + "@remotion/studio-shared": "4.0.405", + "memfs": "3.4.3", + "open": "^8.4.2", + "recast": "0.23.11", + "remotion": "4.0.405", + "semver": "7.5.3", + "source-map": "0.7.3" + } + }, + "node_modules/@remotion/studio-server/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@remotion/studio-server/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@remotion/studio-server/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@remotion/studio-shared": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/studio-shared/-/studio-shared-4.0.405.tgz", + "integrity": "sha512-uFvEr8Zv+ul52zTBLEubSMzPfVWhOq3C2IovilQiRI5dxy9aiQFPdU1DRBjrBLYz/LJybNwLj4lpOkN8d6f+eA==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.405" + } + }, + "node_modules/@remotion/studio/node_modules/@remotion/zod-types": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/zod-types/-/zod-types-4.0.405.tgz", + "integrity": "sha512-bTtXub3ptA11objHaO9sLYsUxoznhWTjDKVHKspbNSedxawkEkbNJb8pTV8Cc4MNoJYD6s5T9zgid83Dm2KsZQ==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.405" + }, + "peerDependencies": { + "zod": "3.22.3" + } + }, + "node_modules/@remotion/studio/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@remotion/studio/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@remotion/studio/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@remotion/studio/node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmmirror.com/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@remotion/web-renderer": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/web-renderer/-/web-renderer-4.0.405.tgz", + "integrity": "sha512-MALigs4K+VwECtnGzMSpR3Q3+osYMDk3jEqCwtCmornIVAA3VMt/2cYCIHe+qNPAPeW8LmpP0W4gXjMydbA2Qg==", + "license": "UNLICENSED", + "dependencies": { + "@remotion/licensing": "4.0.405", + "mediabunny": "1.27.3", + "remotion": "4.0.405" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@remotion/webcodecs": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/@remotion/webcodecs/-/webcodecs-4.0.405.tgz", + "integrity": "sha512-tSyXVEsJFK0JKd2gDy1BnU0cD2TrHXskMvZjHpBWtLS5G3g+aNUAhXpN8MlDQ6zuaj4Z+DNwj89B1UyKFY6crw==", + "license": "Remotion License (See https://remotion.dev/docs/webcodecs#license)", + "dependencies": { + "@remotion/media-parser": "4.0.405" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.12.0", + "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", + "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmmirror.com/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmmirror.com/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.10.tgz", + "integrity": "sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.10.tgz", + "integrity": "sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.10.tgz", + "integrity": "sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.10.tgz", + "integrity": "sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.10.tgz", + "integrity": "sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.10.tgz", + "integrity": "sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.10.tgz", + "integrity": "sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.10.tgz", + "integrity": "sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.10.tgz", + "integrity": "sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.10.tgz", + "integrity": "sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmmirror.com/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.18", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.1.13.tgz", + "integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "postcss": "^8.4.41", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.20", + "resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.90.20.tgz", + "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/archiver": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/@types/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==", + "dependencies": { + "@types/readdir-glob": "*" + } + }, + "node_modules/@types/bcryptjs": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@types/bcryptjs/-/bcryptjs-3.0.0.tgz", + "integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==", + "deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.", + "dependencies": { + "bcryptjs": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/dom-mediacapture-transform": { + "version": "0.1.11", + "resolved": "https://registry.npmmirror.com/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", + "integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.17", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.17.tgz", + "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.13", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.1.13.tgz", + "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.9", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz", + "integrity": "sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.44.1", + "@typescript-eslint/type-utils": "8.44.1", + "@typescript-eslint/utils": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.44.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.44.1.tgz", + "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.44.1.tgz", + "integrity": "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.44.1", + "@typescript-eslint/types": "^8.44.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.44.1.tgz", + "integrity": "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.1.tgz", + "integrity": "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.44.1.tgz", + "integrity": "sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1", + "@typescript-eslint/utils": "8.44.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.44.1.tgz", + "integrity": "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.1.tgz", + "integrity": "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.44.1", + "@typescript-eslint/tsconfig-utils": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.44.1.tgz", + "integrity": "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.44.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.1.tgz", + "integrity": "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.44.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vercel/og": { + "version": "0.8.6", + "resolved": "https://registry.npmmirror.com/@vercel/og/-/og-0.8.6.tgz", + "integrity": "sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==", + "license": "MPL-2.0", + "dependencies": { + "@resvg/resvg-wasm": "2.4.0", + "satori": "0.16.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vercel/og/node_modules/@resvg/resvg-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", + "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@vercel/oidc/-/oidc-3.1.0.tgz", + "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vercel/speed-insights": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@vercel/speed-insights/-/speed-insights-1.3.1.tgz", + "integrity": "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ai": { + "version": "6.0.77", + "resolved": "https://registry.npmmirror.com/ai/-/ai-6.0.77.tgz", + "integrity": "sha512-tyyhrRpCRFVlivdNIFLK8cexSBB2jwTqO0z1qJQagk+UxZ+MW8h5V8xsvvb+xdKDY482Y8KAm0mr7TDnPKvvlw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.39", + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.14", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-1.6.1.tgz", + "integrity": "sha512-4CjkH20If1lhR5CGtqkrVg3bbOtFEG80X9v6jDOIUhbzzbB+UzPBGy8GQhUNVZ0yvMHdMpawCOcy5ydGMsagGQ==", + "dependencies": { + "ajv": "^7.0.0" + }, + "peerDependencies": { + "ajv": "^7.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "7.2.4", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-7.2.4.tgz", + "integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmmirror.com/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmmirror.com/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmmirror.com/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bullmq": { + "version": "5.67.3", + "resolved": "https://registry.npmmirror.com/bullmq/-/bullmq-5.67.3.tgz", + "integrity": "sha512-eeQobOJn8M0Rj8tcZCVFLrimZgJQallJH1JpclOoyut2nDNkDwTEPMVcZzLeSR2fGeIVbfJTjU96F563Qkge5A==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.9.2", + "msgpackr": "1.11.5", + "node-abort-controller": "3.1.1", + "semver": "7.7.3", + "tslib": "2.8.1", + "uuid": "11.1.0" + } + }, + "node_modules/bullmq/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmmirror.com/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/conf": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/conf/-/conf-9.0.2.tgz", + "integrity": "sha512-rLSiilO85qHgaTBIIHQpsv8z+NnVfZq3cKuYNCXN1AOqPzced0GWZEe/A517VldRLyQYXUMyV+vszavE2jSAqw==", + "dependencies": { + "ajv": "^7.0.3", + "ajv-formats": "^1.5.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.0", + "json-schema-typed": "^7.0.3", + "make-dir": "^3.1.0", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "7.2.4", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-7.2.4.tgz", + "integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/cos-nodejs-sdk-v5": { + "version": "2.15.4", + "resolved": "https://registry.npmmirror.com/cos-nodejs-sdk-v5/-/cos-nodejs-sdk-v5-2.15.4.tgz", + "integrity": "sha512-TP/iYTvKKKhRK89on9SRfSMGEw/9SFAAU8EC1kdT5Fmpx7dAwaCNM2+R2H1TSYoQt+03rwOs8QEfNkX8GOHjHQ==", + "dependencies": { + "conf": "^9.0.0", + "fast-xml-parser": "4.2.5", + "mime-types": "^2.1.24", + "request": "^2.88.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmmirror.com/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.16", + "resolved": "https://registry.npmmirror.com/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-loader": { + "version": "5.2.7", + "resolved": "https://registry.npmmirror.com/css-loader/-/css-loader-5.2.7.tgz", + "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==" + }, + "node_modules/detect-libc": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmmirror.com/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.36.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.5.4", + "resolved": "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-15.5.4.tgz", + "integrity": "sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.5.4", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmmirror.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmmirror.com/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "license": "Unlicense" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmmirror.com/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmmirror.com/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmmirror.com/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, + "node_modules/ioredis": { + "version": "5.9.2", + "resolved": "https://registry.npmmirror.com/ioredis/-/ioredis-5.9.2.tgz", + "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmmirror.com/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmmirror.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "0.575.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/magicast/node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/mammoth": { + "version": "1.11.0", + "resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.11.0.tgz", + "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mediabunny": { + "version": "1.27.3", + "resolved": "https://registry.npmmirror.com/mediabunny/-/mediabunny-1.27.3.tgz", + "integrity": "sha512-hlzmgzMznp9DhA5fMJKS5yEAyfCUMxAc+DbSPxD4J1J2cYVl1L+pZLndkt5xLlD5aB5eHEnphHMW14ammMlUXg==", + "license": "MPL-2.0", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, + "node_modules/memfs": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmmirror.com/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/mysql2": { + "version": "3.15.1", + "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.1.tgz", + "integrity": "sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/next": { + "version": "15.5.7", + "resolved": "https://registry.npmmirror.com/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.7", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "4.24.11", + "resolved": "https://registry.npmmirror.com/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-intl": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/next-intl/-/next-intl-4.7.0.tgz", + "integrity": "sha512-gvROzcNr/HM0jTzQlKWQxUNk8jrZ0bREz+bht3wNbv+uzlZ5Kn3J+m+viosub18QJ72S08UJnVK50PXWcUvwpQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.7.0", + "po-parser": "^2.1.1", + "use-intl": "^4.7.0" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.7.0.tgz", + "integrity": "sha512-iAqflu2FWdQMWhwB0B2z52X7LmEpvnMNJXqVERZQ7bK5p9iqQLu70ur6Ka6NfiXLxfb+AeAkUX5qIciQOg+87A==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@swc/core/-/core-1.15.10.tgz", + "integrity": "sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.10", + "@swc/core-darwin-x64": "1.15.10", + "@swc/core-linux-arm-gnueabihf": "1.15.10", + "@swc/core-linux-arm64-gnu": "1.15.10", + "@swc/core-linux-arm64-musl": "1.15.10", + "@swc/core-linux-x64-gnu": "1.15.10", + "@swc/core-linux-x64-musl": "1.15.10", + "@swc/core-win32-arm64-msvc": "1.15.10", + "@swc/core-win32-ia32-msvc": "1.15.10", + "@swc/core-win32-x64-msvc": "1.15.10" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmmirror.com/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==" + }, + "node_modules/oidc-token-hash": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/oidc-token-hash/-/oidc-token-hash-5.1.1.tgz", + "integrity": "sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmmirror.com/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.8.1", + "resolved": "https://registry.npmmirror.com/openai/-/openai-6.8.1.tgz", + "integrity": "sha512-ACifslrVgf+maMz9vqwMP4+v9qvx5Yzssydizks8n+YUJ6YwUoxj51sKRQ8HYMfR6wgKLSIlaI108ZwCk+8yig==", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/preact": { + "version": "10.27.2", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmmirror.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, + "node_modules/prisma": { + "version": "6.16.2", + "resolved": "https://registry.npmmirror.com/prisma/-/prisma-6.16.2.tgz", + "integrity": "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/config": "6.16.2", + "@prisma/engines": "6.16.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmmirror.com/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.9.0.tgz", + "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmmirror.com/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-info": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/redis-info/-/redis-info-3.1.0.tgz", + "integrity": "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.11" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remotion": { + "version": "4.0.405", + "resolved": "https://registry.npmmirror.com/remotion/-/remotion-4.0.405.tgz", + "integrity": "sha512-y2AXgg7wY3V+iPKZRGgWigPf63t2v5ZT8qztQYYPfCBfpnUNdrn7U/1ukF+5u0wMMq2eyAu6f4TtHs9aEZLplw==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmmirror.com/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robot3": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/robot3/-/robot3-0.4.1.tgz", + "integrity": "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==", + "license": "BSD-2-Clause" + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/satori": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/satori/-/satori-0.16.0.tgz", + "integrity": "sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.16", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout": "^3.2.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.4", + "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.4.tgz", + "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.4", + "@img/sharp-darwin-x64": "0.34.4", + "@img/sharp-libvips-darwin-arm64": "1.2.3", + "@img/sharp-libvips-darwin-x64": "1.2.3", + "@img/sharp-libvips-linux-arm": "1.2.3", + "@img/sharp-libvips-linux-arm64": "1.2.3", + "@img/sharp-libvips-linux-ppc64": "1.2.3", + "@img/sharp-libvips-linux-s390x": "1.2.3", + "@img/sharp-libvips-linux-x64": "1.2.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", + "@img/sharp-libvips-linuxmusl-x64": "1.2.3", + "@img/sharp-linux-arm": "0.34.4", + "@img/sharp-linux-arm64": "0.34.4", + "@img/sharp-linux-ppc64": "0.34.4", + "@img/sharp-linux-s390x": "0.34.4", + "@img/sharp-linux-x64": "0.34.4", + "@img/sharp-linuxmusl-arm64": "0.34.4", + "@img/sharp-linuxmusl-x64": "0.34.4", + "@img/sharp-wasm32": "0.34.4", + "@img/sharp-win32-arm64": "0.34.4", + "@img/sharp-win32-ia32": "0.34.4", + "@img/sharp-win32-x64": "0.34.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmmirror.com/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmmirror.com/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar-stream/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmmirror.com/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmmirror.com/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmmirror.com/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-intl": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/use-intl/-/use-intl-4.7.0.tgz", + "integrity": "sha512-jyd8nSErVRRsSlUa+SDobKHo9IiWs5fjcPl9VBUnzUyEQpVM5mwJCgw8eUiylhvBpLQzUGox1KN0XlRivSID9A==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "@schummar/icu-type-parser": "1.21.5", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.5.0.tgz", + "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yauzl/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.4.tgz", + "integrity": "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f58942 --- /dev/null +++ b/package.json @@ -0,0 +1,149 @@ +{ + "name": "waoowaoo", + "version": "0.1.0", + "private": true, + "engines": { + "node": ">=18.18.0", + "npm": ">=9.0.0" + }, + "scripts": { + "dev": "concurrently --kill-others \"npm run dev:next\" \"npm run dev:worker\" \"npm run dev:watchdog\" \"npm run dev:board\"", + "dev:next": "cross-env NODE_OPTIONS=\"--no-deprecation\" next dev --turbopack -H 0.0.0.0", + "dev:worker": "tsx watch --env-file=.env src/lib/workers/index.ts", + "dev:watchdog": "tsx watch --env-file=.env scripts/watchdog.ts", + "dev:board": "tsx watch --env-file=.env scripts/bull-board.ts", + "dev:turbo": "next dev --turbopack -H 0.0.0.0", + "build": "prisma generate && next build", + "build:turbo": "next build --turbopack", + "start": "concurrently --kill-others \"npm run start:next\" \"npm run start:worker\" \"npm run start:watchdog\" \"npm run start:board\"", + "start:next": "next start -H 0.0.0.0", + "start:worker": "tsx --env-file=.env src/lib/workers/index.ts", + "start:watchdog": "tsx --env-file=.env scripts/watchdog.ts", + "start:board": "tsx --env-file=.env scripts/bull-board.ts", + "stats:errors": "tsx scripts/task-error-stats.ts", + "check:api-handler": "tsx scripts/check-api-handler.ts", + "check:logs": "tsx scripts/check-no-console.ts", + "check:log-semantic": "tsx scripts/check-log-semantic.ts", + "check:media-normalization": "tsx scripts/check-media-normalization.ts", + "check:no-api-direct-llm-call": "node scripts/guards/no-api-direct-llm-call.mjs", + "check:no-internal-task-sync-fallback": "node scripts/guards/no-internal-task-sync-fallback.mjs", + "check:no-media-provider-bypass": "node scripts/guards/no-media-provider-bypass.mjs", + "check:no-model-key-downgrade": "node scripts/guards/no-model-key-downgrade.mjs", + "check:no-provider-guessing": "node scripts/guards/no-provider-guessing.mjs", + "check:no-hardcoded-model-capabilities": "node scripts/guards/no-hardcoded-model-capabilities.mjs", + "check:capability-catalog": "node scripts/check-capability-catalog.mjs", + "check:pricing-catalog": "node scripts/check-pricing-catalog.mjs", + "check:model-config-contract": "node scripts/check-model-config-contract.mjs --strict", + "check:config-center-guards": "npm run check:no-model-key-downgrade && npm run check:no-provider-guessing && npm run check:no-hardcoded-model-capabilities && npm run check:capability-catalog && npm run check:pricing-catalog", + "check:outbound-image-unification": "tsx scripts/check-outbound-image-unification.ts", + "check:outbound-image-runtime-sample": "tsx scripts/check-outbound-image-runtime-sample.ts", + "check:outbound-image-success-rate": "tsx scripts/check-outbound-image-success-rate.ts", + "check:image-urls-contract": "tsx scripts/check-image-urls-contract.ts", + "verify:outbound-image": "cross-env BILLING_TEST_BOOTSTRAP=0 vitest run src/lib/media/outbound-image.test.ts src/lib/media/image-url.test.ts && npm run check:outbound-image-unification", + "check:task-loading": "node scripts/guards/task-loading-guard.mjs", + "check:task-target-states-no-polling": "node scripts/guards/task-target-states-no-polling-guard.mjs", + "check:no-server-mirror-state": "node scripts/guards/no-server-mirror-state.mjs", + "check:no-multiple-sources-of-truth": "node scripts/guards/no-multiple-sources-of-truth.mjs", + "check:file-line-count": "node scripts/guards/file-line-count-guard.mjs", + "check:no-duplicate-endpoint-entry": "node scripts/guards/no-duplicate-endpoint-entry.mjs", + "check:test-route-coverage": "node scripts/guards/test-route-coverage-guard.mjs", + "check:test-tasktype-coverage": "node scripts/guards/test-tasktype-coverage-guard.mjs", + "check:test-behavior-quality": "node scripts/guards/test-behavior-quality-guard.mjs", + "check:test-behavior-route-coverage": "node scripts/guards/test-behavior-route-coverage-guard.mjs", + "check:test-behavior-tasktype-coverage": "node scripts/guards/test-behavior-tasktype-coverage-guard.mjs", + "check:test-coverage-guards": "npm run check:test-behavior-quality && npm run check:test-route-coverage && npm run check:test-tasktype-coverage && npm run check:test-behavior-route-coverage && npm run check:test-behavior-tasktype-coverage", + "check:prompt-i18n": "node scripts/guards/prompt-i18n-guard.mjs", + "check:prompt-i18n-regression": "node scripts/guards/prompt-semantic-regression.mjs", + "check:prompt-ab-regression": "node scripts/guards/prompt-ab-regression.mjs", + "check:prompt-json-canary": "node scripts/guards/prompt-json-canary-guard.mjs", + "billing:cleanup-pending-freezes": "tsx scripts/billing-cleanup-pending-freezes.ts", + "billing:reconcile-ledger": "tsx scripts/billing-reconcile-ledger.ts", + "cleanup:remove-legacy-voice-data": "tsx scripts/cleanup-remove-legacy-voice-data.ts", + "test:billing": "npm run test:billing:coverage", + "test:billing:unit": "cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/unit/billing", + "test:billing:integration": "cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/billing", + "test:billing:concurrency": "cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/concurrency/billing", + "test:billing:coverage": "cross-env BILLING_TEST_BOOTSTRAP=1 vitest run --coverage tests/unit/billing tests/integration/billing tests/concurrency/billing", + "test:guards": "npm run check:api-handler && npm run check:no-api-direct-llm-call && npm run check:test-coverage-guards", + "test:unit:all": "cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/unit", + "test:integration:api": "cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/api", + "test:integration:chain": "cross-env BILLING_TEST_BOOTSTRAP=1 vitest run tests/integration/chain", + "test:behavior:unit": "cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/unit/helpers tests/unit/worker tests/unit/optimistic", + "test:behavior:api": "cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/integration/api/contract", + "test:behavior:chain": "cross-env BILLING_TEST_BOOTSTRAP=0 vitest run tests/integration/chain", + "test:behavior:guards": "npm run check:test-coverage-guards", + "test:behavior:full": "npm run test:behavior:guards && npm run test:behavior:unit && npm run test:behavior:api && npm run test:behavior:chain", + "test:regression": "npm run test:guards && npm run test:unit:all && npm run test:billing:integration && npm run test:integration:api && npm run test:integration:chain", + "test:pr": "bash scripts/test-regression-runner.sh npm run test:regression", + "migrate:image-urls-contract": "tsx scripts/migrate-image-urls-contract.ts", + "migrate:model-config-contract": "tsx scripts/migrations/migrate-model-config-contract.ts", + "migrate:capability-selections": "tsx scripts/migrations/migrate-capability-selections.ts", + "backup:media-safety": "tsx scripts/media-safety-backup.ts", + "backup:media-restore-dry-run": "tsx scripts/media-restore-dry-run.ts", + "media:backfill-refs": "tsx scripts/media-backfill-refs.ts", + "backup:archive-legacy-media": "tsx scripts/media-archive-legacy-refs.ts", + "backup:unreferenced-media-index": "tsx scripts/media-build-unreferenced-index.ts", + "lint": "eslint", + "clean": "rimraf .next" + }, + "dependencies": { + "@ai-sdk/google": "^3.0.22", + "@ai-sdk/openai": "^3.0.26", + "@bull-board/api": "^6.16.4", + "@bull-board/express": "^6.16.4", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@fal-ai/client": "^1.7.2", + "@google/genai": "^1.34.0", + "@next-auth/prisma-adapter": "^1.0.7", + "@openrouter/sdk": "^0.3.11", + "@prisma/client": "^6.19.2", + "@remotion/cli": "^4.0.405", + "@remotion/player": "^4.0.405", + "@tanstack/react-query": "^5.90.20", + "@types/archiver": "^7.0.0", + "@types/bcryptjs": "^3.0.0", + "@types/express": "^5.0.6", + "@vercel/og": "^0.8.6", + "@vercel/speed-insights": "^1.3.1", + "ai": "^6.0.77", + "archiver": "^7.0.1", + "bcryptjs": "^3.0.2", + "bullmq": "^5.67.3", + "cos-nodejs-sdk-v5": "^2.15.4", + "express": "^5.2.1", + "file-saver": "^2.0.5", + "ioredis": "^5.9.2", + "jszip": "^3.10.1", + "lucide-react": "^0.575.0", + "mammoth": "^1.11.0", + "mysql2": "^3.15.1", + "next": "^15.5.7", + "next-auth": "^4.24.11", + "next-intl": "^4.7.0", + "openai": "^6.8.1", + "prisma": "^6.16.2", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-hot-toast": "^2.6.0", + "remotion": "^4.0.405" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/file-saver": "^2.0.7", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitest/coverage-v8": "^2.1.8", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "eslint": "^9", + "eslint-config-next": "15.5.4", + "rimraf": "^6.1.2", + "tailwindcss": "^4", + "tsx": "^4.20.5", + "typescript": "^5", + "vitest": "^2.1.8" + } +} \ No newline at end of file diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..39e9b8e --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,837 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model Account { + id String @id @default(uuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([userId]) + @@map("account") +} + +model CharacterAppearance { + id String @id @default(uuid()) + characterId String + appearanceIndex Int + changeReason String + description String? @db.Text + descriptions String? @db.Text + imageUrl String? @db.Text + imageUrls String? @db.Text + selectedIndex Int? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + previousImageUrl String? @db.Text + previousImageUrls String? @db.Text + previousDescription String? @db.Text // 上一次的描述词(用于撤回) + previousDescriptions String? @db.Text // 上一次的描述词数组(用于撤回) + imageMediaId String? + imageMedia MediaObject? @relation("CharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + character NovelPromotionCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + + @@unique([characterId, appearanceIndex]) + @@index([characterId]) + @@index([imageMediaId]) + @@map("character_appearances") +} + +model LocationImage { + id String @id @default(uuid()) + locationId String + imageIndex Int + description String? @db.Text + imageUrl String? @db.Text + isSelected Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + previousImageUrl String? @db.Text + previousDescription String? @db.Text // 上一次的描述词(用于撤回) + imageMediaId String? + imageMedia MediaObject? @relation("LocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + location NovelPromotionLocation @relation("LocationImages", fields: [locationId], references: [id], onDelete: Cascade) + selectedByLocations NovelPromotionLocation[] @relation("SelectedLocationImage") + + @@unique([locationId, imageIndex]) + @@index([locationId]) + @@index([imageMediaId]) + @@map("location_images") +} + +model NovelPromotionCharacter { + id String @id @default(uuid()) + novelPromotionProjectId String + name String + aliases String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + customVoiceUrl String? @db.Text + customVoiceMediaId String? + customVoiceMedia MediaObject? @relation("NovelPromotionCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull) + voiceId String? + voiceType String? + profileData String? @db.Text + profileConfirmed Boolean @default(false) + introduction String? @db.Text // 角色介绍(身份、关系、称呼映射,如"我"对应此角色) + sourceGlobalCharacterId String? // 🆕 来源全局角色ID(复制时记录) + appearances CharacterAppearance[] + novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade) + + @@index([novelPromotionProjectId]) + @@index([customVoiceMediaId]) + @@map("novel_promotion_characters") +} + +model NovelPromotionLocation { + id String @id @default(uuid()) + novelPromotionProjectId String + name String + summary String? @db.Text // 场景简要描述(用途/人物关联) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + sourceGlobalLocationId String? // 🆕 来源全局场景ID(复制时记录) + selectedImageId String? + selectedImage LocationImage? @relation("SelectedLocationImage", fields: [selectedImageId], references: [id], onDelete: SetNull) + images LocationImage[] @relation("LocationImages") + novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade) + + @@index([novelPromotionProjectId]) + @@map("novel_promotion_locations") +} + +model NovelPromotionEpisode { + id String @id @default(uuid()) + novelPromotionProjectId String + episodeNumber Int + name String + description String? @db.Text + novelText String? @db.Text + audioUrl String? @db.Text + audioMediaId String? + audioMedia MediaObject? @relation("NovelPromotionEpisodeAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull) + srtContent String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + speakerVoices String? @db.Text + clips NovelPromotionClip[] + novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade) + shots NovelPromotionShot[] + storyboards NovelPromotionStoryboard[] + voiceLines NovelPromotionVoiceLine[] + editorProject VideoEditorProject? + + @@unique([novelPromotionProjectId, episodeNumber]) + @@index([novelPromotionProjectId]) + @@index([audioMediaId]) + @@map("novel_promotion_episodes") +} + +// 视频编辑器项目 - 存储剪辑数据 +model VideoEditorProject { + id String @id @default(uuid()) + episodeId String @unique + projectData String @db.Text // JSON 存储编辑项目数据 + renderStatus String? // pending | rendering | completed | failed + renderTaskId String? + outputUrl String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + + @@map("video_editor_projects") +} + +model NovelPromotionClip { + id String @id @default(uuid()) + episodeId String + start Int? + end Int? + duration Int? + summary String @db.Text + location String? @db.Text + content String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + characters String? @db.Text + endText String? @db.Text + shotCount Int? + startText String? @db.Text + screenplay String? @db.Text + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + shots NovelPromotionShot[] + storyboard NovelPromotionStoryboard? + + @@index([episodeId]) + @@map("novel_promotion_clips") +} + +model NovelPromotionPanel { + id String @id @default(uuid()) + storyboardId String + panelIndex Int + panelNumber Int? + shotType String? @db.Text + cameraMove String? @db.Text + description String? @db.Text + location String? @db.Text + characters String? @db.Text + srtSegment String? @db.Text + srtStart Float? + srtEnd Float? + duration Float? + imagePrompt String? @db.Text + imageUrl String? @db.Text + imageMediaId String? + imageMedia MediaObject? @relation("NovelPromotionPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + imageHistory String? @db.Text + videoPrompt String? @db.Text + firstLastFramePrompt String? @db.Text + videoUrl String? @db.Text + videoGenerationMode String? @db.Text // 视频生成方式:normal | firstlastframe + videoMediaId String? + videoMedia MediaObject? @relation("NovelPromotionPanelVideoMedia", fields: [videoMediaId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + sceneType String? + candidateImages String? @db.Text + linkedToNextPanel Boolean @default(false) + lipSyncTaskId String? + lipSyncVideoUrl String? + lipSyncVideoMediaId String? + lipSyncVideoMedia MediaObject? @relation("NovelPromotionPanelLipSyncVideoMedia", fields: [lipSyncVideoMediaId], references: [id], onDelete: SetNull) + sketchImageUrl String? @db.Text + sketchImageMediaId String? + sketchImageMedia MediaObject? @relation("NovelPromotionPanelSketchMedia", fields: [sketchImageMediaId], references: [id], onDelete: SetNull) + photographyRules String? @db.Text + actingNotes String? @db.Text // 演技指导数据 JSON + previousImageUrl String? @db.Text + previousImageMediaId String? + previousImageMedia MediaObject? @relation("NovelPromotionPanelPreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull) + storyboard NovelPromotionStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade) + matchedVoiceLines NovelPromotionVoiceLine[] + + @@unique([storyboardId, panelIndex]) + @@index([storyboardId]) + @@index([imageMediaId]) + @@index([videoMediaId]) + @@index([lipSyncVideoMediaId]) + @@index([sketchImageMediaId]) + @@index([previousImageMediaId]) + @@map("novel_promotion_panels") +} + +model NovelPromotionProject { + id String @id @default(uuid()) + projectId String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + analysisModel String? // 用户配置的分析模型(nullable,必须配置后才能使用) + imageModel String? // 用户配置的图片模型 + videoModel String? // 用户配置的视频模型 + videoRatio String @default("9:16") + ttsRate String @default("+50%") + globalAssetText String? @db.Text + artStyle String @default("american-comic") + artStylePrompt String? @db.Text + characterModel String? // 用户配置的角色图片模型 + locationModel String? // 用户配置的场景图片模型 + storyboardModel String? // 用户配置的分镜图片模型 + editModel String? // 用户配置的修图/编辑模型 + videoResolution String @default("720p") + capabilityOverrides String? @db.Text + workflowMode String @default("srt") + lastEpisodeId String? + imageResolution String @default("2K") + importStatus String? + characters NovelPromotionCharacter[] + episodes NovelPromotionEpisode[] + locations NovelPromotionLocation[] + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@map("novel_promotion_projects") +} + +model NovelPromotionShot { + id String @id @default(uuid()) + episodeId String + clipId String? + shotId String + srtStart Int + srtEnd Int + srtDuration Float + sequence String? @db.Text + locations String? @db.Text + characters String? @db.Text + plot String? @db.Text + imagePrompt String? @db.Text + scale String? @db.Text + module String? @db.Text + focus String? @db.Text + zhSummarize String? @db.Text + imageUrl String? @db.Text + imageMediaId String? + imageMedia MediaObject? @relation("NovelPromotionShotImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + pov String? @db.Text + clip NovelPromotionClip? @relation(fields: [clipId], references: [id], onDelete: Cascade) + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + + @@index([clipId]) + @@index([episodeId]) + @@index([shotId]) + @@index([imageMediaId]) + @@map("novel_promotion_shots") +} + +model NovelPromotionStoryboard { + id String @id @default(uuid()) + episodeId String + clipId String @unique + storyboardImageUrl String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + panelCount Int @default(9) + storyboardTextJson String? @db.Text + imageHistory String? @db.Text + candidateImages String? @db.Text + lastError String? + photographyPlan String? @db.Text + panels NovelPromotionPanel[] + clip NovelPromotionClip @relation(fields: [clipId], references: [id], onDelete: Cascade) + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + supplementaryPanels SupplementaryPanel[] + + @@index([clipId]) + @@index([episodeId]) + @@map("novel_promotion_storyboards") +} + +model SupplementaryPanel { + id String @id @default(uuid()) + storyboardId String + sourceType String + sourcePanelId String? + description String? @db.Text + imagePrompt String? @db.Text + imageUrl String? @db.Text + imageMediaId String? + imageMedia MediaObject? @relation("SupplementaryPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + characters String? @db.Text + location String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + storyboard NovelPromotionStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade) + + @@index([storyboardId]) + @@index([imageMediaId]) + @@map("supplementary_panels") +} + +model Project { + id String @id @default(uuid()) + name String + description String? @db.Text + mode String @default("novel-promotion") + userId String + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + lastAccessedAt DateTime? + novelPromotionData NovelPromotionProject? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + usageCosts UsageCost[] + + @@index([userId]) + @@map("projects") +} + +model Session { + id String @id @default(uuid()) + sessionToken String @unique(map: "Session_sessionToken_key") + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@map("session") +} + +model UsageCost { + id String @id @default(uuid()) + projectId String + userId String + apiType String + model String + action String + quantity Int + unit String + cost Decimal @db.Decimal(18, 6) + metadata String? @db.Text + createdAt DateTime @default(now()) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([apiType]) + @@index([createdAt]) + @@index([projectId]) + @@index([userId]) + @@map("usage_costs") +} + +model User { + id String @id @default(uuid()) + name String @unique(map: "User_name_key") + email String? + emailVerified DateTime? + image String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + accounts Account[] + projects Project[] + sessions Session[] + usageCosts UsageCost[] + balance UserBalance? + preferences UserPreference? + + // 资产中心 + globalAssetFolders GlobalAssetFolder[] + globalCharacters GlobalCharacter[] + globalLocations GlobalLocation[] + globalVoices GlobalVoice[] + tasks Task[] + taskEvents TaskEvent[] + + @@map("user") +} + +model UserPreference { + id String @id @default(uuid()) + userId String @unique + analysisModel String? // 用户配置的分析模型(nullable,必须配置后才能使用) + characterModel String? // 用户配置的角色图片模型 + locationModel String? // 用户配置的场景图片模型 + storyboardModel String? // 用户配置的分镜图片模型 + editModel String? // 用户配置的修图模型 + videoModel String? // 用户配置的视频模型 + lipSyncModel String? // 用户配置的口型同步模型 + videoRatio String @default("9:16") + videoResolution String @default("720p") + artStyle String @default("american-comic") + ttsRate String @default("+50%") + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + imageResolution String @default("2K") + capabilityDefaults String? @db.Text + + // API Key 配置(极简版) + llmBaseUrl String? @default("https://openrouter.ai/api/v1") + llmApiKey String? @db.Text // 加密存储 + falApiKey String? @db.Text // FAL(图片+视频+语音) + googleAiKey String? @db.Text // Google AI(Gemini 图片) + arkApiKey String? @db.Text // 火山引擎(Seedream+Seedance) + qwenApiKey String? @db.Text // 阿里百炼(声音设计) + + // 自定义模型列表 + 价格(JSON) + customModels String? @db.Text + + // 自定义 OpenAI 兼容提供商列表(JSON,包含加密的 API Key) + customProviders String? @db.Text + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("user_preferences") +} + +model VerificationToken { + identifier String + token String @unique(map: "VerificationToken_token_key") + expires DateTime + + @@unique([identifier, token]) + @@map("verificationtoken") +} + +model NovelPromotionVoiceLine { + id String @id @default(uuid()) + episodeId String + lineIndex Int + speaker String + content String @db.Text + voicePresetId String? + audioUrl String? @db.Text + audioMediaId String? + audioMedia MediaObject? @relation("NovelPromotionVoiceLineAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + emotionPrompt String? @db.Text + emotionStrength Float? @default(0.4) + matchedPanelIndex Int? + matchedStoryboardId String? + audioDuration Int? + matchedPanelId String? + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + matchedPanel NovelPromotionPanel? @relation(fields: [matchedPanelId], references: [id]) + + @@unique([episodeId, lineIndex]) + @@index([episodeId]) + @@index([matchedPanelId]) + @@index([audioMediaId]) + @@map("novel_promotion_voice_lines") +} + +model VoicePreset { + id String @id @default(uuid()) + name String + audioUrl String @db.Text + audioMediaId String? + audioMedia MediaObject? @relation("VoicePresetAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull) + description String? @db.Text + gender String? + isSystem Boolean @default(true) + createdAt DateTime @default(now()) + + @@index([audioMediaId]) + @@map("voice_presets") +} + +model UserBalance { + id String @id @default(uuid()) + userId String @unique + balance Decimal @default(0) @db.Decimal(18, 6) + frozenAmount Decimal @default(0) @db.Decimal(18, 6) + totalSpent Decimal @default(0) @db.Decimal(18, 6) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("user_balances") +} + +model BalanceFreeze { + id String @id @default(uuid()) + userId String + amount Decimal @db.Decimal(18, 6) + status String @default("pending") + source String? @db.VarChar(64) + taskId String? + requestId String? + idempotencyKey String? @unique + metadata String? @db.Text + expiresAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@index([userId]) + @@index([status]) + @@index([taskId]) + @@map("balance_freezes") +} + +model BalanceTransaction { + id String @id @default(uuid()) + userId String + type String + amount Decimal @db.Decimal(18, 6) + balanceAfter Decimal @db.Decimal(18, 6) + description String? @db.Text + relatedId String? + freezeId String? + operatorId String? @db.VarChar(64) + externalOrderId String? @db.VarChar(128) + idempotencyKey String? @db.VarChar(128) + projectId String? @db.VarChar(128) // 关联项目 ID,用于流水展示项目名 + episodeId String? @db.VarChar(128) // 关联集数 ID,用于流水展示集数 + taskType String? @db.VarChar(64) // 任务类型 key(与 action 一致),用于前端 i18n + billingMeta String? @db.Text // 计费详情 JSON: { quantity, unit, model, resolution, duration, tokens... } + createdAt DateTime @default(now()) + + @@index([userId]) + @@index([type]) + @@index([createdAt]) + @@index([freezeId]) + @@index([externalOrderId]) + @@index([projectId]) + @@unique([userId, type, idempotencyKey]) + @@map("balance_transactions") +} + +model Task { + id String @id @default(uuid()) + userId String + projectId String + episodeId String? + type String + targetType String + targetId String + status String @default("queued") + progress Int @default(0) + attempt Int @default(0) + maxAttempts Int @default(5) + priority Int @default(0) + dedupeKey String? @unique + externalId String? + payload Json? + result Json? + errorCode String? + errorMessage String? @db.Text + billingInfo Json? + billedAt DateTime? + queuedAt DateTime @default(now()) + startedAt DateTime? + finishedAt DateTime? + heartbeatAt DateTime? + enqueuedAt DateTime? + enqueueAttempts Int @default(0) + lastEnqueueError String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + events TaskEvent[] + + @@index([status]) + @@index([type]) + @@index([targetType, targetId]) + @@index([projectId]) + @@index([userId]) + @@index([heartbeatAt]) + @@map("tasks") +} + +model TaskEvent { + id Int @id @default(autoincrement()) + taskId String + projectId String + userId String + eventType String + payload Json? + createdAt DateTime @default(now()) + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([projectId, id]) + @@index([taskId]) + @@index([userId]) + @@map("task_events") +} + +// ==================== 资产中心 ==================== + +// 资产文件夹(一层,不支持嵌套) +model GlobalAssetFolder { + id String @id @default(uuid()) + userId String + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + characters GlobalCharacter[] + locations GlobalLocation[] + voices GlobalVoice[] + + @@index([userId]) + @@map("global_asset_folders") +} + +// 全局角色(结构与 NovelPromotionCharacter 一致) +model GlobalCharacter { + id String @id @default(uuid()) + userId String + folderId String? + name String + aliases String? @db.Text + profileData String? @db.Text + profileConfirmed Boolean @default(false) + voiceId String? + voiceType String? + customVoiceUrl String? @db.Text + customVoiceMediaId String? + customVoiceMedia MediaObject? @relation("GlobalCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull) + globalVoiceId String? // 绑定的全局音色 ID + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + appearances GlobalCharacterAppearance[] + + @@index([userId]) + @@index([folderId]) + @@index([customVoiceMediaId]) + @@map("global_characters") +} + +// 全局角色形象(结构与 CharacterAppearance 一致) +model GlobalCharacterAppearance { + id String @id @default(uuid()) + characterId String + appearanceIndex Int + changeReason String @default("default") + description String? @db.Text + descriptions String? @db.Text + imageUrl String? @db.Text + imageMediaId String? + imageMedia MediaObject? @relation("GlobalCharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + imageUrls String? @db.Text + selectedIndex Int? + previousImageUrl String? @db.Text + previousImageMediaId String? + previousImageMedia MediaObject? @relation("GlobalCharacterAppearancePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull) + previousImageUrls String? @db.Text + previousDescription String? @db.Text // 上一次的描述词(用于撤回) + previousDescriptions String? @db.Text // 上一次的描述词数组(用于撤回) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + character GlobalCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + + @@unique([characterId, appearanceIndex]) + @@index([characterId]) + @@index([imageMediaId]) + @@index([previousImageMediaId]) + @@map("global_character_appearances") +} + +// 全局场景(结构与 NovelPromotionLocation 一致) +model GlobalLocation { + id String @id @default(uuid()) + userId String + folderId String? + name String + summary String? @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + images GlobalLocationImage[] + + @@index([userId]) + @@index([folderId]) + @@map("global_locations") +} + +// 全局场景图片(结构与 LocationImage 一致) +model GlobalLocationImage { + id String @id @default(uuid()) + locationId String + imageIndex Int + description String? @db.Text + imageUrl String? @db.Text + imageMediaId String? + imageMedia MediaObject? @relation("GlobalLocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + isSelected Boolean @default(false) + previousImageUrl String? @db.Text + previousImageMediaId String? + previousImageMedia MediaObject? @relation("GlobalLocationImagePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull) + previousDescription String? @db.Text // 上一次的描述词(用于撤回) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + location GlobalLocation @relation(fields: [locationId], references: [id], onDelete: Cascade) + + @@unique([locationId, imageIndex]) + @@index([locationId]) + @@index([imageMediaId]) + @@index([previousImageMediaId]) + @@map("global_location_images") +} + +// 全局音色库 +model GlobalVoice { + id String @id @default(uuid()) + userId String + folderId String? + name String // 音色名称 + description String? @db.Text // 详细描述 + voiceId String? // qwen-tts-vd 的 voice ID + voiceType String @default("qwen-designed") // qwen-designed | custom + customVoiceUrl String? @db.Text // 上传的音频 URL(预览用) + customVoiceMediaId String? + customVoiceMedia MediaObject? @relation("GlobalVoiceCustomVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull) + voicePrompt String? @db.Text // AI 设计时的提示词 + gender String? // male | female | neutral + language String @default("zh") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + + @@index([userId]) + @@index([folderId]) + @@index([customVoiceMediaId]) + @@map("global_voices") +} + +model MediaObject { + id String @id @default(uuid()) + publicId String @unique + storageKey String @unique @db.VarChar(512) + sha256 String? + mimeType String? + sizeBytes BigInt? + width Int? + height Int? + durationMs Int? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + characterAppearanceImages CharacterAppearance[] @relation("CharacterAppearanceImageMedia") + locationImages LocationImage[] @relation("LocationImageMedia") + novelPromotionCharacterVoices NovelPromotionCharacter[] @relation("NovelPromotionCharacterVoiceMedia") + novelPromotionEpisodeAudios NovelPromotionEpisode[] @relation("NovelPromotionEpisodeAudioMedia") + novelPromotionPanelImages NovelPromotionPanel[] @relation("NovelPromotionPanelImageMedia") + novelPromotionPanelVideos NovelPromotionPanel[] @relation("NovelPromotionPanelVideoMedia") + novelPromotionPanelLipSyncVideos NovelPromotionPanel[] @relation("NovelPromotionPanelLipSyncVideoMedia") + novelPromotionPanelSketchImages NovelPromotionPanel[] @relation("NovelPromotionPanelSketchMedia") + novelPromotionPanelPreviousImages NovelPromotionPanel[] @relation("NovelPromotionPanelPreviousImageMedia") + novelPromotionShotImages NovelPromotionShot[] @relation("NovelPromotionShotImageMedia") + supplementaryPanelImages SupplementaryPanel[] @relation("SupplementaryPanelImageMedia") + novelPromotionVoiceLineAudios NovelPromotionVoiceLine[] @relation("NovelPromotionVoiceLineAudioMedia") + voicePresetAudios VoicePreset[] @relation("VoicePresetAudioMedia") + globalCharacterVoices GlobalCharacter[] @relation("GlobalCharacterVoiceMedia") + globalCharacterAppearanceImages GlobalCharacterAppearance[] @relation("GlobalCharacterAppearanceImageMedia") + globalCharacterAppearancePreviousImgs GlobalCharacterAppearance[] @relation("GlobalCharacterAppearancePreviousImageMedia") + globalLocationImageImages GlobalLocationImage[] @relation("GlobalLocationImageMedia") + globalLocationImagePreviousImages GlobalLocationImage[] @relation("GlobalLocationImagePreviousImageMedia") + globalVoiceCustomVoices GlobalVoice[] @relation("GlobalVoiceCustomVoiceMedia") + + @@index([createdAt]) + @@map("media_objects") +} + +model LegacyMediaRefBackup { + id String @id @default(uuid()) + runId String + tableName String + rowId String + fieldName String + legacyValue String @db.Text + checksum String + createdAt DateTime @default(now()) + + @@index([runId]) + @@index([tableName, fieldName]) + @@map("legacy_media_refs_backup") +} diff --git a/prisma/schema.sqlit.prisma b/prisma/schema.sqlit.prisma new file mode 100644 index 0000000..fb7d731 --- /dev/null +++ b/prisma/schema.sqlit.prisma @@ -0,0 +1,829 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model Account { + id String @id @default(uuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([userId]) + @@map("account") +} + +model CharacterAppearance { + id String @id @default(uuid()) + characterId String + appearanceIndex Int + changeReason String + description String? + descriptions String? + imageUrl String? + imageUrls String? + selectedIndex Int? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + previousImageUrl String? + previousImageUrls String? + previousDescription String? // 上一次的描述词(用于撤回) + previousDescriptions String? // 上一次的描述词数组(用于撤回) + imageMediaId String? + imageMedia MediaObject? @relation("CharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + character NovelPromotionCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + + @@unique([characterId, appearanceIndex]) + @@index([characterId]) + @@index([imageMediaId]) + @@map("character_appearances") +} + +model LocationImage { + id String @id @default(uuid()) + locationId String + imageIndex Int + description String? + imageUrl String? + isSelected Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + previousImageUrl String? + previousDescription String? // 上一次的描述词(用于撤回) + imageMediaId String? + imageMedia MediaObject? @relation("LocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + location NovelPromotionLocation @relation(fields: [locationId], references: [id], onDelete: Cascade) + + @@unique([locationId, imageIndex]) + @@index([locationId]) + @@index([imageMediaId]) + @@map("location_images") +} + +model NovelPromotionCharacter { + id String @id @default(uuid()) + novelPromotionProjectId String + name String + aliases String? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + customVoiceUrl String? + customVoiceMediaId String? + customVoiceMedia MediaObject? @relation("NovelPromotionCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull) + voiceId String? + voiceType String? + profileData String? + profileConfirmed Boolean @default(false) + introduction String? // 角色介绍(身份、关系、称呼映射,如"我"对应此角色) + sourceGlobalCharacterId String? // 🆕 来源全局角色ID(复制时记录) + appearances CharacterAppearance[] + novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade) + + @@index([novelPromotionProjectId]) + @@index([customVoiceMediaId]) + @@map("novel_promotion_characters") +} + +model NovelPromotionLocation { + id String @id @default(uuid()) + novelPromotionProjectId String + name String + summary String? // 场景简要描述(用途/人物关联) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + sourceGlobalLocationId String? // 🆕 来源全局场景ID(复制时记录) + images LocationImage[] + novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade) + + @@index([novelPromotionProjectId]) + @@map("novel_promotion_locations") +} + +model NovelPromotionEpisode { + id String @id @default(uuid()) + novelPromotionProjectId String + episodeNumber Int + name String + description String? + novelText String? + audioUrl String? + audioMediaId String? + audioMedia MediaObject? @relation("NovelPromotionEpisodeAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull) + srtContent String? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + speakerVoices String? + clips NovelPromotionClip[] + novelPromotionProject NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade) + shots NovelPromotionShot[] + storyboards NovelPromotionStoryboard[] + voiceLines NovelPromotionVoiceLine[] + editorProject VideoEditorProject? + + @@unique([novelPromotionProjectId, episodeNumber]) + @@index([novelPromotionProjectId]) + @@index([audioMediaId]) + @@map("novel_promotion_episodes") +} + +// 视频编辑器项目 - 存储剪辑数据 +model VideoEditorProject { + id String @id @default(uuid()) + episodeId String @unique + projectData String // JSON 存储编辑项目数据 + renderStatus String? // pending | rendering | completed | failed + renderTaskId String? + outputUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + + @@map("video_editor_projects") +} + +model NovelPromotionClip { + id String @id @default(uuid()) + episodeId String + start Int? + end Int? + duration Int? + summary String + location String? + content String + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + characters String? + endText String? + shotCount Int? + startText String? + screenplay String? + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + shots NovelPromotionShot[] + storyboard NovelPromotionStoryboard? + + @@index([episodeId]) + @@map("novel_promotion_clips") +} + +model NovelPromotionPanel { + id String @id @default(uuid()) + storyboardId String + panelIndex Int + panelNumber Int? + shotType String? + cameraMove String? + description String? + location String? + characters String? + srtSegment String? + srtStart Float? + srtEnd Float? + duration Float? + imagePrompt String? + imageUrl String? + imageMediaId String? + imageMedia MediaObject? @relation("NovelPromotionPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + imageHistory String? + videoPrompt String? + firstLastFramePrompt String? + videoUrl String? + videoGenerationMode String? // 视频生成方式:normal | firstlastframe + videoMediaId String? + videoMedia MediaObject? @relation("NovelPromotionPanelVideoMedia", fields: [videoMediaId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + sceneType String? + candidateImages String? + linkedToNextPanel Boolean @default(false) + lipSyncTaskId String? + lipSyncVideoUrl String? + lipSyncVideoMediaId String? + lipSyncVideoMedia MediaObject? @relation("NovelPromotionPanelLipSyncVideoMedia", fields: [lipSyncVideoMediaId], references: [id], onDelete: SetNull) + sketchImageUrl String? + sketchImageMediaId String? + sketchImageMedia MediaObject? @relation("NovelPromotionPanelSketchMedia", fields: [sketchImageMediaId], references: [id], onDelete: SetNull) + photographyRules String? + actingNotes String? // 演技指导数据 JSON + previousImageUrl String? + previousImageMediaId String? + previousImageMedia MediaObject? @relation("NovelPromotionPanelPreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull) + storyboard NovelPromotionStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade) + matchedVoiceLines NovelPromotionVoiceLine[] + + @@unique([storyboardId, panelIndex]) + @@index([storyboardId]) + @@index([imageMediaId]) + @@index([videoMediaId]) + @@index([lipSyncVideoMediaId]) + @@index([sketchImageMediaId]) + @@index([previousImageMediaId]) + @@map("novel_promotion_panels") +} + +model NovelPromotionProject { + id String @id @default(uuid()) + projectId String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + analysisModel String? // 用户配置的分析模型(nullable,必须配置后才能使用) + imageModel String? // 用户配置的图片模型 + videoModel String? // 用户配置的视频模型 + videoRatio String @default("9:16") + ttsRate String @default("+50%") + globalAssetText String? + artStyle String @default("american-comic") + artStylePrompt String? + characterModel String? // 用户配置的角色图片模型 + locationModel String? // 用户配置的场景图片模型 + storyboardModel String? // 用户配置的分镜图片模型 + editModel String? // 用户配置的修图/编辑模型 + videoResolution String @default("720p") + capabilityOverrides String? + workflowMode String @default("srt") + lastEpisodeId String? + imageResolution String @default("2K") + importStatus String? + characters NovelPromotionCharacter[] + episodes NovelPromotionEpisode[] + locations NovelPromotionLocation[] + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@map("novel_promotion_projects") +} + +model NovelPromotionShot { + id String @id @default(uuid()) + episodeId String + clipId String? + shotId String + srtStart Int + srtEnd Int + srtDuration Float + sequence String? + locations String? + characters String? + plot String? + imagePrompt String? + scale String? + module String? + focus String? + zhSummarize String? + imageUrl String? + imageMediaId String? + imageMedia MediaObject? @relation("NovelPromotionShotImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + pov String? + clip NovelPromotionClip? @relation(fields: [clipId], references: [id], onDelete: Cascade) + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + + @@index([clipId]) + @@index([episodeId]) + @@index([shotId]) + @@index([imageMediaId]) + @@map("novel_promotion_shots") +} + +model NovelPromotionStoryboard { + id String @id @default(uuid()) + episodeId String + clipId String @unique + storyboardImageUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + panelCount Int @default(9) + storyboardTextJson String? + imageHistory String? + candidateImages String? + lastError String? + photographyPlan String? + panels NovelPromotionPanel[] + clip NovelPromotionClip @relation(fields: [clipId], references: [id], onDelete: Cascade) + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + supplementaryPanels SupplementaryPanel[] + + @@index([clipId]) + @@index([episodeId]) + @@map("novel_promotion_storyboards") +} + +model SupplementaryPanel { + id String @id @default(uuid()) + storyboardId String + sourceType String + sourcePanelId String? + description String? + imagePrompt String? + imageUrl String? + imageMediaId String? + imageMedia MediaObject? @relation("SupplementaryPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + characters String? + location String? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + storyboard NovelPromotionStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade) + + @@index([storyboardId]) + @@index([imageMediaId]) + @@map("supplementary_panels") +} + +model Project { + id String @id @default(uuid()) + name String + description String? + mode String @default("novel-promotion") + userId String + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + lastAccessedAt DateTime? + novelPromotionData NovelPromotionProject? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + usageCosts UsageCost[] + + @@index([userId]) + @@map("projects") +} + +model Session { + id String @id @default(uuid()) + sessionToken String @unique(map: "Session_sessionToken_key") + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@map("session") +} + +model UsageCost { + id String @id @default(uuid()) + projectId String + userId String + apiType String + model String + action String + quantity Int + unit String + cost Decimal + metadata String? + createdAt DateTime @default(now()) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([apiType]) + @@index([createdAt]) + @@index([projectId]) + @@index([userId]) + @@map("usage_costs") +} + +model User { + id String @id @default(uuid()) + name String @unique(map: "User_name_key") + email String? + emailVerified DateTime? + image String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + accounts Account[] + projects Project[] + sessions Session[] + usageCosts UsageCost[] + balance UserBalance? + preferences UserPreference? + + // 资产中心 + globalAssetFolders GlobalAssetFolder[] + globalCharacters GlobalCharacter[] + globalLocations GlobalLocation[] + globalVoices GlobalVoice[] + tasks Task[] + taskEvents TaskEvent[] + + @@map("user") +} + +model UserPreference { + id String @id @default(uuid()) + userId String @unique + analysisModel String? // 用户配置的分析模型(nullable,必须配置后才能使用) + characterModel String? // 用户配置的角色图片模型 + locationModel String? // 用户配置的场景图片模型 + storyboardModel String? // 用户配置的分镜图片模型 + editModel String? // 用户配置的修图模型 + videoModel String? // 用户配置的视频模型 + lipSyncModel String? // 用户配置的口型同步模型 + videoRatio String @default("9:16") + videoResolution String @default("720p") + artStyle String @default("american-comic") + ttsRate String @default("+50%") + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + imageResolution String @default("2K") + capabilityDefaults String? + + // API Key 配置(极简版) + llmBaseUrl String? @default("https://openrouter.ai/api/v1") + llmApiKey String? // 加密存储 + falApiKey String? // FAL(图片+视频+语音) + googleAiKey String? // Google AI(Gemini 图片) + arkApiKey String? // 火山引擎(Seedream+Seedance) + qwenApiKey String? // 阿里百炼(声音设计) + + // 自定义模型列表 + 价格(JSON) + customModels String? + + // 自定义 OpenAI 兼容提供商列表(JSON,包含加密的 API Key) + customProviders String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("user_preferences") +} + +model VerificationToken { + identifier String + token String @unique(map: "VerificationToken_token_key") + expires DateTime + + @@unique([identifier, token]) + @@map("verificationtoken") +} + +model NovelPromotionVoiceLine { + id String @id @default(uuid()) + episodeId String + lineIndex Int + speaker String + content String + voicePresetId String? + audioUrl String? + audioMediaId String? + audioMedia MediaObject? @relation("NovelPromotionVoiceLineAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + emotionPrompt String? + emotionStrength Float? @default(0.4) + matchedPanelIndex Int? + matchedStoryboardId String? + audioDuration Int? + matchedPanelId String? + episode NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + matchedPanel NovelPromotionPanel? @relation(fields: [matchedPanelId], references: [id]) + + @@unique([episodeId, lineIndex]) + @@index([episodeId]) + @@index([matchedPanelId]) + @@index([audioMediaId]) + @@map("novel_promotion_voice_lines") +} + +model VoicePreset { + id String @id @default(uuid()) + name String + audioUrl String + audioMediaId String? + audioMedia MediaObject? @relation("VoicePresetAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull) + description String? + gender String? + isSystem Boolean @default(true) + createdAt DateTime @default(now()) + + @@index([audioMediaId]) + @@map("voice_presets") +} + +model UserBalance { + id String @id @default(uuid()) + userId String @unique + balance Decimal @default(0) + frozenAmount Decimal @default(0) + totalSpent Decimal @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("user_balances") +} + +model BalanceFreeze { + id String @id @default(uuid()) + userId String + amount Decimal + status String @default("pending") + source String? + taskId String? + requestId String? + idempotencyKey String? @unique + metadata String? + expiresAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@index([userId]) + @@index([status]) + @@index([taskId]) + @@map("balance_freezes") +} + +model BalanceTransaction { + id String @id @default(uuid()) + userId String + type String + amount Decimal + balanceAfter Decimal + description String? + relatedId String? + freezeId String? + operatorId String? + externalOrderId String? + idempotencyKey String? + createdAt DateTime @default(now()) + + @@index([userId]) + @@index([type]) + @@index([createdAt]) + @@index([freezeId]) + @@index([externalOrderId]) + @@unique([userId, type, idempotencyKey]) + @@map("balance_transactions") +} + +model Task { + id String @id @default(uuid()) + userId String + projectId String + episodeId String? + type String + targetType String + targetId String + status String @default("queued") + progress Int @default(0) + attempt Int @default(0) + maxAttempts Int @default(5) + priority Int @default(0) + dedupeKey String? @unique + externalId String? + payload Json? + result Json? + errorCode String? + errorMessage String? + billingInfo Json? + billedAt DateTime? + queuedAt DateTime @default(now()) + startedAt DateTime? + finishedAt DateTime? + heartbeatAt DateTime? + enqueuedAt DateTime? + enqueueAttempts Int @default(0) + lastEnqueueError String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + events TaskEvent[] + + @@index([status]) + @@index([type]) + @@index([targetType, targetId]) + @@index([projectId]) + @@index([userId]) + @@index([heartbeatAt]) + @@map("tasks") +} + +model TaskEvent { + id Int @id @default(autoincrement()) + taskId String + projectId String + userId String + eventType String + payload Json? + createdAt DateTime @default(now()) + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([projectId, id]) + @@index([taskId]) + @@index([userId]) + @@map("task_events") +} + +// ==================== 资产中心 ==================== + +// 资产文件夹(一层,不支持嵌套) +model GlobalAssetFolder { + id String @id @default(uuid()) + userId String + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + characters GlobalCharacter[] + locations GlobalLocation[] + voices GlobalVoice[] + + @@index([userId]) + @@map("global_asset_folders") +} + +// 全局角色(结构与 NovelPromotionCharacter 一致) +model GlobalCharacter { + id String @id @default(uuid()) + userId String + folderId String? + name String + aliases String? + profileData String? + profileConfirmed Boolean @default(false) + voiceId String? + voiceType String? + customVoiceUrl String? + customVoiceMediaId String? + customVoiceMedia MediaObject? @relation("GlobalCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull) + globalVoiceId String? // 绑定的全局音色 ID + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + appearances GlobalCharacterAppearance[] + + @@index([userId]) + @@index([folderId]) + @@index([customVoiceMediaId]) + @@map("global_characters") +} + +// 全局角色形象(结构与 CharacterAppearance 一致) +model GlobalCharacterAppearance { + id String @id @default(uuid()) + characterId String + appearanceIndex Int + changeReason String @default("default") + description String? + descriptions String? + imageUrl String? + imageMediaId String? + imageMedia MediaObject? @relation("GlobalCharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + imageUrls String? + selectedIndex Int? + previousImageUrl String? + previousImageMediaId String? + previousImageMedia MediaObject? @relation("GlobalCharacterAppearancePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull) + previousImageUrls String? + previousDescription String? // 上一次的描述词(用于撤回) + previousDescriptions String? // 上一次的描述词数组(用于撤回) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + character GlobalCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + + @@unique([characterId, appearanceIndex]) + @@index([characterId]) + @@index([imageMediaId]) + @@index([previousImageMediaId]) + @@map("global_character_appearances") +} + +// 全局场景(结构与 NovelPromotionLocation 一致) +model GlobalLocation { + id String @id @default(uuid()) + userId String + folderId String? + name String + summary String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + images GlobalLocationImage[] + + @@index([userId]) + @@index([folderId]) + @@map("global_locations") +} + +// 全局场景图片(结构与 LocationImage 一致) +model GlobalLocationImage { + id String @id @default(uuid()) + locationId String + imageIndex Int + description String? + imageUrl String? + imageMediaId String? + imageMedia MediaObject? @relation("GlobalLocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) + isSelected Boolean @default(false) + previousImageUrl String? + previousImageMediaId String? + previousImageMedia MediaObject? @relation("GlobalLocationImagePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull) + previousDescription String? // 上一次的描述词(用于撤回) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + location GlobalLocation @relation(fields: [locationId], references: [id], onDelete: Cascade) + + @@unique([locationId, imageIndex]) + @@index([locationId]) + @@index([imageMediaId]) + @@index([previousImageMediaId]) + @@map("global_location_images") +} + +// 全局音色库 +model GlobalVoice { + id String @id @default(uuid()) + userId String + folderId String? + name String // 音色名称 + description String? // 详细描述 + voiceId String? // qwen-tts-vd 的 voice ID + voiceType String @default("qwen-designed") // qwen-designed | custom + customVoiceUrl String? // 上传的音频 URL(预览用) + customVoiceMediaId String? + customVoiceMedia MediaObject? @relation("GlobalVoiceCustomVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull) + voicePrompt String? // AI 设计时的提示词 + gender String? // male | female | neutral + language String @default("zh") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + folder GlobalAssetFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + + @@index([userId]) + @@index([folderId]) + @@index([customVoiceMediaId]) + @@map("global_voices") +} + +model MediaObject { + id String @id @default(uuid()) + publicId String @unique + storageKey String @unique + sha256 String? + mimeType String? + sizeBytes BigInt? + width Int? + height Int? + durationMs Int? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + characterAppearanceImages CharacterAppearance[] @relation("CharacterAppearanceImageMedia") + locationImages LocationImage[] @relation("LocationImageMedia") + novelPromotionCharacterVoices NovelPromotionCharacter[] @relation("NovelPromotionCharacterVoiceMedia") + novelPromotionEpisodeAudios NovelPromotionEpisode[] @relation("NovelPromotionEpisodeAudioMedia") + novelPromotionPanelImages NovelPromotionPanel[] @relation("NovelPromotionPanelImageMedia") + novelPromotionPanelVideos NovelPromotionPanel[] @relation("NovelPromotionPanelVideoMedia") + novelPromotionPanelLipSyncVideos NovelPromotionPanel[] @relation("NovelPromotionPanelLipSyncVideoMedia") + novelPromotionPanelSketchImages NovelPromotionPanel[] @relation("NovelPromotionPanelSketchMedia") + novelPromotionPanelPreviousImages NovelPromotionPanel[] @relation("NovelPromotionPanelPreviousImageMedia") + novelPromotionShotImages NovelPromotionShot[] @relation("NovelPromotionShotImageMedia") + supplementaryPanelImages SupplementaryPanel[] @relation("SupplementaryPanelImageMedia") + novelPromotionVoiceLineAudios NovelPromotionVoiceLine[] @relation("NovelPromotionVoiceLineAudioMedia") + voicePresetAudios VoicePreset[] @relation("VoicePresetAudioMedia") + globalCharacterVoices GlobalCharacter[] @relation("GlobalCharacterVoiceMedia") + globalCharacterAppearanceImages GlobalCharacterAppearance[] @relation("GlobalCharacterAppearanceImageMedia") + globalCharacterAppearancePreviousImgs GlobalCharacterAppearance[] @relation("GlobalCharacterAppearancePreviousImageMedia") + globalLocationImageImages GlobalLocationImage[] @relation("GlobalLocationImageMedia") + globalLocationImagePreviousImages GlobalLocationImage[] @relation("GlobalLocationImagePreviousImageMedia") + globalVoiceCustomVoices GlobalVoice[] @relation("GlobalVoiceCustomVoiceMedia") + + @@index([createdAt]) + @@map("media_objects") +} + +model LegacyMediaRefBackup { + id String @id @default(uuid()) + runId String + tableName String + rowId String + fieldName String + legacyValue String + checksum String + createdAt DateTime @default(now()) + + @@index([runId]) + @@index([tableName, fieldName]) + @@map("legacy_media_refs_backup") +} diff --git a/public/banner.png b/public/banner.png new file mode 100644 index 0000000..b40777f Binary files /dev/null and b/public/banner.png differ diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..9c3563a Binary files /dev/null and b/public/icon.png differ diff --git a/public/images/grid-template-9x16.png b/public/images/grid-template-9x16.png new file mode 100644 index 0000000..187b9ed Binary files /dev/null and b/public/images/grid-template-9x16.png differ diff --git a/public/logo-small.png b/public/logo-small.png new file mode 100644 index 0000000..d373c6d Binary files /dev/null and b/public/logo-small.png differ diff --git a/public/logo.ico b/public/logo.ico new file mode 100644 index 0000000..fb683f5 Binary files /dev/null and b/public/logo.ico differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..1f13d67 Binary files /dev/null and b/public/logo.png differ diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/billing-cleanup-pending-freezes.ts b/scripts/billing-cleanup-pending-freezes.ts new file mode 100644 index 0000000..5e8f02b --- /dev/null +++ b/scripts/billing-cleanup-pending-freezes.ts @@ -0,0 +1,140 @@ +import { prisma } from '@/lib/prisma' +import { toMoneyNumber } from '@/lib/billing/money' + +type CleanupStats = { + scanned: number + stale: number + rolledBack: number + skipped: number + errors: number +} + +function hasApplyFlag() { + return process.argv.includes('--apply') +} + +function parseHoursArg(defaultHours: number) { + const arg = process.argv.find((item) => item.startsWith('--hours=')) + if (!arg) return defaultHours + const value = Number(arg.slice('--hours='.length)) + if (!Number.isFinite(value) || value <= 0) return defaultHours + return Math.floor(value) +} + +function writeJson(payload: unknown) { + process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`) +} + +function writeError(payload: unknown) { + process.stderr.write(`${typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2)}\n`) +} + +async function main() { + const apply = hasApplyFlag() + const hours = parseHoursArg(24) + const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000) + + const pending = await prisma.balanceFreeze.findMany({ + where: { + status: 'pending', + createdAt: { lt: cutoff }, + }, + orderBy: { createdAt: 'asc' }, + }) + + const stats: CleanupStats = { + scanned: pending.length, + stale: pending.length, + rolledBack: 0, + skipped: 0, + errors: 0, + } + + if (!apply) { + writeJson({ + mode: 'dry-run', + hours, + cutoff: cutoff.toISOString(), + stalePendingCount: pending.length, + stalePending: pending.map((f) => ({ + id: f.id, + userId: f.userId, + amount: toMoneyNumber(f.amount), + createdAt: f.createdAt.toISOString(), + })), + }) + return + } + + for (const freeze of pending) { + try { + await prisma.$transaction(async (tx) => { + const current = await tx.balanceFreeze.findUnique({ + where: { id: freeze.id }, + }) + if (!current || current.status !== 'pending') { + stats.skipped += 1 + return + } + + const balance = await tx.userBalance.findUnique({ + where: { userId: current.userId }, + }) + if (!balance) { + stats.skipped += 1 + return + } + + const frozenAmount = toMoneyNumber(balance.frozenAmount) + const freezeAmount = toMoneyNumber(current.amount) + const nextFrozenAmount = Math.max(0, frozenAmount - freezeAmount) + const frozenDelta = frozenAmount - nextFrozenAmount + const balanceIncrement = frozenDelta + + await tx.userBalance.update({ + where: { userId: current.userId }, + data: { + balance: { increment: balanceIncrement }, + frozenAmount: { decrement: frozenDelta }, + }, + }) + + await tx.balanceFreeze.update({ + where: { id: current.id }, + data: { + status: 'rolled_back', + }, + }) + }) + stats.rolledBack += 1 + } catch (error) { + stats.errors += 1 + writeError({ + tag: 'billing-cleanup-pending-freezes.rollback_failed', + freezeId: freeze.id, + userId: freeze.userId, + amount: toMoneyNumber(freeze.amount), + error: error instanceof Error ? error.message : String(error), + }) + } + } + + writeJson({ + mode: 'apply', + hours, + cutoff: cutoff.toISOString(), + stats, + }) +} + +main() + .catch((error) => { + writeError({ + tag: 'billing-cleanup-pending-freezes.fatal', + error: error instanceof Error ? error.message : String(error), + }) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/billing-reconcile-ledger.ts b/scripts/billing-reconcile-ledger.ts new file mode 100644 index 0000000..2032708 --- /dev/null +++ b/scripts/billing-reconcile-ledger.ts @@ -0,0 +1,125 @@ +import { prisma } from '@/lib/prisma' +import { roundMoney, toMoneyNumber } from '@/lib/billing/money' + +type UserLedgerRow = { + userId: string + balance: number + frozenAmount: number + txNetAmount: number + ledgerAmount: number + diff: number +} + +function hasStrictFlag() { + return process.argv.includes('--strict') +} + +function write(payload: unknown) { + process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`) +} + +async function main() { + const strict = hasStrictFlag() + + const [balances, txByUser, pendingFreezes] = await Promise.all([ + prisma.userBalance.findMany({ + select: { + userId: true, + balance: true, + frozenAmount: true, + }, + }), + prisma.balanceTransaction.groupBy({ + by: ['userId'], + _sum: { amount: true }, + }), + prisma.balanceFreeze.findMany({ + where: { status: 'pending' }, + select: { + id: true, + userId: true, + taskId: true, + amount: true, + createdAt: true, + }, + orderBy: { createdAt: 'asc' }, + }), + ]) + + const txNetByUser = new Map() + for (const row of txByUser) { + txNetByUser.set(row.userId, roundMoney(toMoneyNumber(row._sum.amount), 8)) + } + + const ledgerRows: UserLedgerRow[] = balances.map((row) => { + const balance = toMoneyNumber(row.balance) + const frozenAmount = toMoneyNumber(row.frozenAmount) + const txNetAmount = roundMoney(txNetByUser.get(row.userId) || 0, 8) + const ledgerAmount = roundMoney(balance + frozenAmount, 8) + return { + userId: row.userId, + balance, + frozenAmount, + txNetAmount, + ledgerAmount, + diff: roundMoney(ledgerAmount - txNetAmount, 8), + } + }) + + const nonZeroDiffUsers = ledgerRows.filter((row) => Math.abs(row.diff) > 1e-8) + + const pendingTaskIds = pendingFreezes + .map((row) => row.taskId) + .filter((taskId): taskId is string => typeof taskId === 'string' && taskId.length > 0) + const tasks = pendingTaskIds.length > 0 + ? await prisma.task.findMany({ + where: { id: { in: pendingTaskIds } }, + select: { id: true, status: true }, + }) + : [] + const taskStatusById = new Map(tasks.map((row) => [row.id, row.status])) + const activeStatuses = new Set(['queued', 'processing']) + const orphanPendingFreezes = pendingFreezes.filter((freeze) => { + if (!freeze.taskId) return true + const status = taskStatusById.get(freeze.taskId) + if (!status) return true + return !activeStatuses.has(status) + }) + + const result = { + strict, + checkedAt: new Date().toISOString(), + totals: { + users: balances.length, + txUsers: txByUser.length, + pendingFreezes: pendingFreezes.length, + nonZeroDiffUsers: nonZeroDiffUsers.length, + orphanPendingFreezes: orphanPendingFreezes.length, + }, + nonZeroDiffUsers, + orphanPendingFreezes: orphanPendingFreezes.map((row) => ({ + id: row.id, + userId: row.userId, + taskId: row.taskId, + amount: toMoneyNumber(row.amount), + createdAt: row.createdAt.toISOString(), + })), + } + + write(result) + + if (strict && (nonZeroDiffUsers.length > 0 || orphanPendingFreezes.length > 0)) { + process.exitCode = 1 + } +} + +main() + .catch((error) => { + write({ + error: error instanceof Error ? error.message : String(error), + }) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/bull-board.ts b/scripts/bull-board.ts new file mode 100644 index 0000000..cc503f0 --- /dev/null +++ b/scripts/bull-board.ts @@ -0,0 +1,105 @@ +import { createScopedLogger } from '@/lib/logging/core' +import express, { type NextFunction, type Request, type Response } from 'express' +import { createBullBoard } from '@bull-board/api' +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter' +import { ExpressAdapter } from '@bull-board/express' +import { imageQueue, textQueue, videoQueue, voiceQueue } from '@/lib/task/queues' + +const host = process.env.BULL_BOARD_HOST || '127.0.0.1' +const port = Number.parseInt(process.env.BULL_BOARD_PORT || '3010', 10) || 3010 +const basePath = process.env.BULL_BOARD_BASE_PATH || '/admin/queues' +const authUser = process.env.BULL_BOARD_USER +const authPassword = process.env.BULL_BOARD_PASSWORD +const logger = createScopedLogger({ + module: 'ops.bull_board', +}) + +function unauthorized(res: Response) { + res.setHeader('WWW-Authenticate', 'Basic realm="BullMQ Board"') + res.status(401).send('Authentication required') +} + +function basicAuthMiddleware(req: Request, res: Response, next: NextFunction) { + if (!authUser && !authPassword) { + next() + return + } + + const authorization = req.headers.authorization + if (!authorization?.startsWith('Basic ')) { + unauthorized(res) + return + } + + const encoded = authorization.slice(6).trim() + let decoded = '' + + try { + decoded = Buffer.from(encoded, 'base64').toString('utf8') + } catch { + unauthorized(res) + return + } + + const index = decoded.indexOf(':') + if (index === -1) { + unauthorized(res) + return + } + + const username = decoded.slice(0, index) + const password = decoded.slice(index + 1) + if (username !== (authUser || '') || password !== (authPassword || '')) { + unauthorized(res) + return + } + + next() +} + +const serverAdapter = new ExpressAdapter() +serverAdapter.setBasePath(basePath) + +createBullBoard({ + queues: [ + new BullMQAdapter(imageQueue), + new BullMQAdapter(videoQueue), + new BullMQAdapter(voiceQueue), + new BullMQAdapter(textQueue), + ], + serverAdapter, +}) + +const app = express() +app.disable('x-powered-by') +app.use(basePath, basicAuthMiddleware, serverAdapter.getRouter()) + +const server = app.listen(port, host, () => { + const secured = authUser || authPassword ? 'enabled' : 'disabled' + logger.info({ + action: 'bull_board.started', + message: 'bull board listening', + details: { + host, + port, + basePath, + auth: secured, + }, + }) +}) + +async function shutdown(signal: string) { + logger.info({ + action: 'bull_board.shutdown', + message: 'bull board shutting down', + details: { + signal, + }, + }) + await Promise.allSettled([imageQueue.close(), videoQueue.close(), voiceQueue.close(), textQueue.close()]) + await new Promise((resolve) => server.close(() => resolve())) + process.exit(0) +} + +process.on('SIGINT', () => void shutdown('SIGINT')) +process.on('SIGTERM', () => void shutdown('SIGTERM')) diff --git a/scripts/check-api-handler.ts b/scripts/check-api-handler.ts new file mode 100644 index 0000000..bae6097 --- /dev/null +++ b/scripts/check-api-handler.ts @@ -0,0 +1,38 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { execSync } from 'node:child_process' + +const ALLOWLIST = new Set([ + 'src/app/api/auth/[...nextauth]/route.ts', + 'src/app/api/files/[...path]/route.ts', + 'src/app/api/system/boot-id/route.ts', +]) + +function main() { + const output = execSync("rg --files src/app/api | rg 'route\\.ts$'", { encoding: 'utf8' }) + const files = output + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + + const missing: string[] = [] + + for (const file of files) { + if (ALLOWLIST.has(file)) continue + const hasApiHandler = execSync(`rg -n \"apiHandler\" ${JSON.stringify(file)} || true`, { encoding: 'utf8' }).trim().length > 0 + if (!hasApiHandler) { + missing.push(file) + } + } + + if (missing.length > 0) { + _ulogError('[check-api-handler] missing apiHandler in:') + for (const file of missing) { + _ulogError(`- ${file}`) + } + process.exit(1) + } + + _ulogInfo(`[check-api-handler] ok total=${files.length} allowlist=${ALLOWLIST.size}`) +} + +main() diff --git a/scripts/check-capability-catalog.mjs b/scripts/check-capability-catalog.mjs new file mode 100644 index 0000000..0634d48 --- /dev/null +++ b/scripts/check-capability-catalog.mjs @@ -0,0 +1,334 @@ +import { promises as fs } from 'node:fs' +import path from 'node:path' + + const CATALOG_DIR = path.resolve(process.cwd(), 'standards/capabilities') +const CAPABILITY_NAMESPACES = new Set(['llm', 'image', 'video', 'audio', 'lipsync']) +const CAPABILITY_NAMESPACE_ALLOWED_FIELDS = { + llm: new Set(['reasoningEffortOptions', 'fieldI18n']), + image: new Set(['resolutionOptions', 'fieldI18n']), + video: new Set([ + 'generationModeOptions', + 'generateAudioOptions', + 'durationOptions', + 'fpsOptions', + 'resolutionOptions', + 'firstlastframe', + 'supportGenerateAudio', + 'fieldI18n', + ]), + audio: new Set(['voiceOptions', 'rateOptions', 'fieldI18n']), + lipsync: new Set(['modeOptions', 'fieldI18n']), +} +const CAPABILITY_NAMESPACE_I18N_FIELDS = { + llm: { reasoningEffort: 'reasoningEffortOptions' }, + image: { resolution: 'resolutionOptions' }, + video: { + generationMode: 'generationModeOptions', + generateAudio: 'generateAudioOptions', + duration: 'durationOptions', + fps: 'fpsOptions', + resolution: 'resolutionOptions', + }, + audio: { voice: 'voiceOptions', rate: 'rateOptions' }, + lipsync: { mode: 'modeOptions' }, +} +const MODEL_TYPES = new Set(['llm', 'image', 'video', 'audio', 'lipsync']) + +function isRecord(value) { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function isNonEmptyString(value) { + return typeof value === 'string' && value.trim().length > 0 +} + +function isI18nKey(value) { + return isNonEmptyString(value) && value.includes('.') +} + +function isStringArray(value) { + return Array.isArray(value) && value.every((item) => isNonEmptyString(item)) +} + +function isNumberArray(value) { + return Array.isArray(value) && value.every((item) => typeof item === 'number' && Number.isFinite(item)) +} + +function isBooleanArray(value) { + return Array.isArray(value) && value.every((item) => typeof item === 'boolean') +} + +function parseModelKeyStrict(value) { + if (!isNonEmptyString(value)) return null + const raw = value.trim() + const marker = raw.indexOf('::') + if (marker === -1) return null + const provider = raw.slice(0, marker).trim() + const modelId = raw.slice(marker + 2).trim() + if (!provider || !modelId) return null + return { provider, modelId, modelKey: `${provider}::${modelId}` } +} + +function pushIssue(issues, file, index, field, message) { + issues.push({ file, index, field, message }) +} + +function validateAllowedFields(issues, file, index, namespace, namespaceValue) { + if (!isRecord(namespaceValue)) return + const allowedFields = CAPABILITY_NAMESPACE_ALLOWED_FIELDS[namespace] + for (const field of Object.keys(namespaceValue)) { + if (allowedFields.has(field)) continue + if (field === 'i18n') { + pushIssue(issues, file, index, `capabilities.${namespace}.${field}`, 'use fieldI18n instead of i18n') + continue + } + pushIssue(issues, file, index, `capabilities.${namespace}.${field}`, `unknown capability field: ${field}`) + } +} + +function validateFieldI18nMap(issues, file, index, namespace, namespaceValue) { + if (!isRecord(namespaceValue)) return + if (namespaceValue.fieldI18n === undefined) return + if (!isRecord(namespaceValue.fieldI18n)) { + pushIssue(issues, file, index, `capabilities.${namespace}.fieldI18n`, 'fieldI18n must be an object') + return + } + + const allowedI18nFields = CAPABILITY_NAMESPACE_I18N_FIELDS[namespace] + for (const [fieldName, fieldConfig] of Object.entries(namespaceValue.fieldI18n)) { + if (!(fieldName in allowedI18nFields)) { + pushIssue(issues, file, index, `capabilities.${namespace}.fieldI18n.${fieldName}`, `unknown i18n field: ${fieldName}`) + continue + } + if (!isRecord(fieldConfig)) { + pushIssue(issues, file, index, `capabilities.${namespace}.fieldI18n.${fieldName}`, 'field i18n config must be an object') + continue + } + + if (fieldConfig.labelKey !== undefined && !isI18nKey(fieldConfig.labelKey)) { + pushIssue(issues, file, index, `capabilities.${namespace}.fieldI18n.${fieldName}.labelKey`, 'labelKey must be an i18n key') + } + if (fieldConfig.unitKey !== undefined && !isI18nKey(fieldConfig.unitKey)) { + pushIssue(issues, file, index, `capabilities.${namespace}.fieldI18n.${fieldName}.unitKey`, 'unitKey must be an i18n key') + } + if (fieldConfig.optionLabelKeys !== undefined) { + if (!isRecord(fieldConfig.optionLabelKeys)) { + pushIssue( + issues, + file, + index, + `capabilities.${namespace}.fieldI18n.${fieldName}.optionLabelKeys`, + 'optionLabelKeys must be an object', + ) + continue + } + const optionFieldName = allowedI18nFields[fieldName] + const optionsRaw = namespaceValue[optionFieldName] + const allowedOptions = Array.isArray(optionsRaw) ? new Set(optionsRaw.map((value) => String(value))) : null + for (const [optionValue, optionLabel] of Object.entries(fieldConfig.optionLabelKeys)) { + if (!isI18nKey(optionLabel)) { + pushIssue( + issues, + file, + index, + `capabilities.${namespace}.fieldI18n.${fieldName}.optionLabelKeys.${optionValue}`, + 'option label must be an i18n key', + ) + } + if (allowedOptions && !allowedOptions.has(optionValue)) { + pushIssue( + issues, + file, + index, + `capabilities.${namespace}.fieldI18n.${fieldName}.optionLabelKeys.${optionValue}`, + `option ${optionValue} is not defined in ${optionFieldName}`, + ) + } + } + } + } +} + +function validateCapabilitiesForModelType(issues, file, index, modelType, capabilities) { + if (capabilities === undefined || capabilities === null) return + if (!isRecord(capabilities)) { + pushIssue(issues, file, index, 'capabilities', 'capabilities must be an object') + return + } + + const expectedNamespace = modelType + for (const namespace of Object.keys(capabilities)) { + if (!CAPABILITY_NAMESPACES.has(namespace)) { + pushIssue(issues, file, index, `capabilities.${namespace}`, `unknown capabilities namespace: ${namespace}`) + continue + } + if (namespace !== expectedNamespace) { + pushIssue( + issues, + file, + index, + `capabilities.${namespace}`, + `namespace ${namespace} is not allowed for model type ${modelType}`, + ) + } + } + + const llm = capabilities.llm + if (llm !== undefined) { + if (!isRecord(llm)) { + pushIssue(issues, file, index, 'capabilities.llm', 'llm capabilities must be an object') + } else { + validateAllowedFields(issues, file, index, 'llm', llm) + if (llm.reasoningEffortOptions !== undefined && !isStringArray(llm.reasoningEffortOptions)) { + pushIssue(issues, file, index, 'capabilities.llm.reasoningEffortOptions', 'must be string array') + } + validateFieldI18nMap(issues, file, index, 'llm', llm) + } + } + + const image = capabilities.image + if (image !== undefined) { + if (!isRecord(image)) { + pushIssue(issues, file, index, 'capabilities.image', 'image capabilities must be an object') + } else { + validateAllowedFields(issues, file, index, 'image', image) + if (image.resolutionOptions !== undefined && !isStringArray(image.resolutionOptions)) { + pushIssue(issues, file, index, 'capabilities.image.resolutionOptions', 'must be string array') + } + validateFieldI18nMap(issues, file, index, 'image', image) + } + } + + const video = capabilities.video + if (video !== undefined) { + if (!isRecord(video)) { + pushIssue(issues, file, index, 'capabilities.video', 'video capabilities must be an object') + } else { + validateAllowedFields(issues, file, index, 'video', video) + if (video.generationModeOptions !== undefined && !isStringArray(video.generationModeOptions)) { + pushIssue(issues, file, index, 'capabilities.video.generationModeOptions', 'must be string array') + } + if (video.generateAudioOptions !== undefined && !isBooleanArray(video.generateAudioOptions)) { + pushIssue(issues, file, index, 'capabilities.video.generateAudioOptions', 'must be boolean array') + } + if (video.durationOptions !== undefined && !isNumberArray(video.durationOptions)) { + pushIssue(issues, file, index, 'capabilities.video.durationOptions', 'must be number array') + } + if (video.fpsOptions !== undefined && !isNumberArray(video.fpsOptions)) { + pushIssue(issues, file, index, 'capabilities.video.fpsOptions', 'must be number array') + } + if (video.resolutionOptions !== undefined && !isStringArray(video.resolutionOptions)) { + pushIssue(issues, file, index, 'capabilities.video.resolutionOptions', 'must be string array') + } + if (video.supportGenerateAudio !== undefined && typeof video.supportGenerateAudio !== 'boolean') { + pushIssue(issues, file, index, 'capabilities.video.supportGenerateAudio', 'must be boolean') + } + if (video.firstlastframe !== undefined && typeof video.firstlastframe !== 'boolean') { + pushIssue(issues, file, index, 'capabilities.video.firstlastframe', 'must be boolean') + } + validateFieldI18nMap(issues, file, index, 'video', video) + } + } + + const audio = capabilities.audio + if (audio !== undefined) { + if (!isRecord(audio)) { + pushIssue(issues, file, index, 'capabilities.audio', 'audio capabilities must be an object') + } else { + validateAllowedFields(issues, file, index, 'audio', audio) + if (audio.voiceOptions !== undefined && !isStringArray(audio.voiceOptions)) { + pushIssue(issues, file, index, 'capabilities.audio.voiceOptions', 'must be string array') + } + if (audio.rateOptions !== undefined && !isStringArray(audio.rateOptions)) { + pushIssue(issues, file, index, 'capabilities.audio.rateOptions', 'must be string array') + } + validateFieldI18nMap(issues, file, index, 'audio', audio) + } + } + + const lipsync = capabilities.lipsync + if (lipsync !== undefined) { + if (!isRecord(lipsync)) { + pushIssue(issues, file, index, 'capabilities.lipsync', 'lipsync capabilities must be an object') + } else { + validateAllowedFields(issues, file, index, 'lipsync', lipsync) + if (lipsync.modeOptions !== undefined && !isStringArray(lipsync.modeOptions)) { + pushIssue(issues, file, index, 'capabilities.lipsync.modeOptions', 'must be string array') + } + validateFieldI18nMap(issues, file, index, 'lipsync', lipsync) + } + } +} + +async function listCatalogFiles() { + const entries = await fs.readdir(CATALOG_DIR, { withFileTypes: true }) + return entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.json')) + .map((entry) => path.join(CATALOG_DIR, entry.name)) +} + +async function readCatalog(filePath) { + const raw = await fs.readFile(filePath, 'utf8') + const parsed = JSON.parse(raw) + if (!Array.isArray(parsed)) { + throw new Error(`catalog must be an array: ${filePath}`) + } + return parsed +} + +async function main() { + const issues = [] + const files = await listCatalogFiles() + if (files.length === 0) { + throw new Error(`no catalog files found in ${CATALOG_DIR}`) + } + + for (const filePath of files) { + const catalogItems = await readCatalog(filePath) + for (let index = 0; index < catalogItems.length; index += 1) { + const item = catalogItems[index] + if (!isRecord(item)) { + pushIssue(issues, filePath, index, 'entry', 'entry must be an object') + continue + } + + if (!isNonEmptyString(item.modelType) || !MODEL_TYPES.has(item.modelType)) { + pushIssue(issues, filePath, index, 'modelType', 'modelType must be llm/image/video/audio/lipsync') + continue + } + + if (!isNonEmptyString(item.provider)) { + pushIssue(issues, filePath, index, 'provider', 'provider must be a non-empty string') + } + if (!isNonEmptyString(item.modelId)) { + pushIssue(issues, filePath, index, 'modelId', 'modelId must be a non-empty string') + } + + const modelKey = `${item.provider || ''}::${item.modelId || ''}` + if (!parseModelKeyStrict(modelKey)) { + pushIssue(issues, filePath, index, 'modelKey', 'provider/modelId must compose a valid provider::modelId') + } + + validateCapabilitiesForModelType(issues, filePath, index, item.modelType, item.capabilities) + } + } + + if (issues.length === 0) { + process.stdout.write(`[check-capability-catalog] OK (${files.length} files)\n`) + return + } + + const maxPrint = 50 + for (const issue of issues.slice(0, maxPrint)) { + process.stdout.write(`[check-capability-catalog] ${issue.file}#${issue.index} ${issue.field}: ${issue.message}\n`) + } + if (issues.length > maxPrint) { + process.stdout.write(`[check-capability-catalog] ... ${issues.length - maxPrint} more issues\n`) + } + process.exitCode = 1 +} + +main().catch((error) => { + process.stderr.write(`[check-capability-catalog] failed: ${String(error)}\n`) + process.exitCode = 1 +}) diff --git a/scripts/check-image-urls-contract.ts b/scripts/check-image-urls-contract.ts new file mode 100644 index 0000000..0574fc2 --- /dev/null +++ b/scripts/check-image-urls-contract.ts @@ -0,0 +1,118 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { prisma } from '@/lib/prisma' +import { decodeImageUrlsFromDb } from '@/lib/contracts/image-urls-contract' + +type AppearanceRow = { + id: string + imageUrls: string | null + previousImageUrls: string | null +} + +type DynamicModel = { + findMany: (args: unknown) => Promise +} + +const BATCH_SIZE = 500 + +const MODELS: Array<{ name: string; model: string }> = [ + { name: 'CharacterAppearance', model: 'characterAppearance' }, + { name: 'GlobalCharacterAppearance', model: 'globalCharacterAppearance' }, +] + +const prismaDynamic = prisma as unknown as Record + +function print(message: string) { + process.stdout.write(`${message}\n`) +} + +async function checkModel(modelName: string, modelKey: string) { + const model = prismaDynamic[modelKey] + if (!model) { + throw new Error(`Prisma model not found: ${modelKey}`) + } + + let scanned = 0 + let violations = 0 + const samples: Array<{ id: string; field: 'imageUrls' | 'previousImageUrls'; message: string; value: string | null }> = [] + let cursor: string | null = null + + while (true) { + const rows = await model.findMany({ + select: { + id: true, + imageUrls: true, + previousImageUrls: true, + }, + ...(cursor + ? { + cursor: { id: cursor }, + skip: 1, + } + : {}), + orderBy: { id: 'asc' }, + take: BATCH_SIZE, + }) + + if (rows.length === 0) break + + for (const row of rows) { + scanned += 1 + + for (const fieldName of ['imageUrls', 'previousImageUrls'] as const) { + try { + decodeImageUrlsFromDb(row[fieldName], `${modelName}.${fieldName}`) + } catch (error) { + violations += 1 + if (samples.length < 20) { + samples.push({ + id: row.id, + field: fieldName, + message: error instanceof Error ? error.message : String(error), + value: row[fieldName], + }) + } + } + } + } + + cursor = rows[rows.length - 1]?.id || null + } + + const summary = `[check-image-urls-contract] ${modelName}: scanned=${scanned} violations=${violations}` + _ulogInfo(summary) + print(summary) + if (samples.length > 0) { + _ulogError(`[check-image-urls-contract] ${modelName}: samples=${JSON.stringify(samples, null, 2)}`) + } + + return { scanned, violations } +} + +async function main() { + let totalScanned = 0 + let totalViolations = 0 + + for (const target of MODELS) { + const result = await checkModel(target.name, target.model) + totalScanned += result.scanned + totalViolations += result.violations + } + + if (totalViolations > 0) { + _ulogError(`[check-image-urls-contract] failed scanned=${totalScanned} violations=${totalViolations}`) + print(`[check-image-urls-contract] failed scanned=${totalScanned} violations=${totalViolations}`) + process.exitCode = 1 + return + } + + print(`[check-image-urls-contract] ok scanned=${totalScanned}`) +} + +main() + .catch((error) => { + _ulogError('[check-image-urls-contract] failed:', error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/check-log-semantic.ts b/scripts/check-log-semantic.ts new file mode 100644 index 0000000..91e9022 --- /dev/null +++ b/scripts/check-log-semantic.ts @@ -0,0 +1,110 @@ +import fs from 'node:fs' + +type Rule = { + file: string + patterns: string[] +} + +const RULES: Rule[] = [ + { + file: 'src/lib/api-errors.ts', + patterns: ['x-request-id', 'api.request.start', 'api.request.finish', 'api.request.error'], + }, + { + file: 'src/lib/workers/shared.ts', + patterns: ['worker.start', 'worker.completed', 'worker.failed', 'durationMs', 'errorCode'], + }, + { + file: 'src/app/api/sse/route.ts', + patterns: ['sse.connect', 'sse.replay', 'sse.disconnect'], + }, + { + file: 'scripts/watchdog.ts', + patterns: ['watchdog.started', 'watchdog.tick.ok', 'watchdog.tick.failed'], + }, + { + file: 'scripts/bull-board.ts', + patterns: ['bull_board.started', 'bull_board.shutdown'], + }, + { + file: 'src/lib/task/submitter.ts', + patterns: ['requestId', 'task.submit.created', 'task.submit.enqueued'], + }, + { + file: 'src/lib/task/types.ts', + patterns: ['trace', 'requestId'], + }, +] + +function read(file: string) { + return fs.readFileSync(file, 'utf8') +} + +function checkRules() { + const violations: string[] = [] + for (const rule of RULES) { + const content = read(rule.file) + for (const pattern of rule.patterns) { + if (!content.includes(pattern)) { + violations.push(`${rule.file} missing "${pattern}"`) + } + } + } + return violations +} + +function checkSubmitTaskRoutes() { + const root = 'src/app/api' + const files = walk(root).filter((file) => file.endsWith('/route.ts')) + const submitTaskFiles = files.filter((file) => read(file).includes('submitTask(')) + const violations: string[] = [] + + for (const file of submitTaskFiles) { + const content = read(file) + if (!content.includes('getRequestId')) { + violations.push(`${file} uses submitTask but does not import getRequestId`) + continue + } + if (!content.includes('requestId: getRequestId(request)')) { + violations.push(`${file} uses submitTask but does not pass requestId`) + } + } + + return { submitTaskFiles, violations } +} + +function walk(dir: string): string[] { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + const out: string[] = [] + + for (const entry of entries) { + const next = `${dir}/${entry.name}` + if (entry.isDirectory()) { + out.push(...walk(next)) + } else { + out.push(next) + } + } + + return out +} + +function main() { + const violations = checkRules() + const submitTaskResult = checkSubmitTaskRoutes() + violations.push(...submitTaskResult.violations) + + if (violations.length > 0) { + process.stderr.write('[check:log-semantic] semantic violations detected:\n') + for (const violation of violations) { + process.stderr.write(`- ${violation}\n`) + } + process.exit(1) + } + + process.stdout.write( + `[check:log-semantic] ok rules=${RULES.length} submitTaskRoutes=${submitTaskResult.submitTaskFiles.length}\n`, + ) +} + +main() diff --git a/scripts/check-media-normalization.ts b/scripts/check-media-normalization.ts new file mode 100644 index 0000000..3632489 --- /dev/null +++ b/scripts/check-media-normalization.ts @@ -0,0 +1,110 @@ +import { execSync } from 'node:child_process' + +const TARGETS = ['src/app/api', 'src/lib'] + +const EXTRACT_ALLOWLIST = new Set([ + 'src/lib/media/service.ts', + 'src/lib/cos.ts', +]) + +const FETCH_MEDIA_ALLOWLIST = new Set([ + 'src/lib/cos.ts', + 'src/lib/media-process.ts', + 'src/lib/image-cache.ts', + 'src/lib/image-label.ts', + 'src/lib/workers/utils.ts', + 'src/app/api/novel-promotion/[projectId]/download-images/route.ts', + 'src/app/api/novel-promotion/[projectId]/download-videos/route.ts', + 'src/app/api/novel-promotion/[projectId]/download-voices/route.ts', + 'src/app/api/novel-promotion/[projectId]/update-asset-label/route.ts', + 'src/app/api/novel-promotion/[projectId]/voice-generate/route.ts', + 'src/app/api/novel-promotion/[projectId]/video-proxy/route.ts', +]) + +function run(cmd: string): string { + try { + return execSync(cmd, { encoding: 'utf8' }) + } catch (error: unknown) { + if (error && typeof error === 'object' && 'stdout' in error) { + const stdout = (error as { stdout?: unknown }).stdout + return typeof stdout === 'string' ? stdout : '' + } + return '' + } +} + +function parseLines(output: string): string[] { + return output + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) +} + +function getFile(line: string): string { + return line.split(':', 1)[0] || '' +} + +function getCode(line: string): string { + const parts = line.split(':') + return parts.slice(2).join(':').trim() +} + +function extractFetchArg(code: string): string { + const matched = code.match(/fetch\(\s*([^)]+)\)/) + return matched?.[1]?.trim() || '' +} + +function isSafeFetchArg(arg: string): boolean { + if (!arg) return false + if (/^toFetchableUrl\(/.test(arg)) return true + if (/^['"`]/.test(arg)) return true + if (/^new URL\(/.test(arg)) return true + return false +} + +function isMediaLikeFetchArg(arg: string): boolean { + return /(image|video|audio|signed).*url/i.test(arg) || /url.*(image|video|audio|signed)/i.test(arg) +} + +function main() { + const targetExpr = TARGETS.join(' ') + + // 规则 1:业务代码中不允许直接调用 extractCOSKey(统一走 resolveStorageKeyFromMediaValue) + const extractOutput = run(`rg -n "extractCOSKey\\\\(" ${targetExpr}`) + const extractLines = parseLines(extractOutput) + const extractViolations = extractLines.filter((line) => { + const file = getFile(line) + return !EXTRACT_ALLOWLIST.has(file) + }) + + // 规则 2:媒体相关 fetch 必须包裹 toFetchableUrl + const fetchOutput = run(`rg -n "fetch\\\\(" ${targetExpr}`) + const fetchLines = parseLines(fetchOutput) + const fetchViolations = fetchLines.filter((line) => { + const file = getFile(line) + if (!FETCH_MEDIA_ALLOWLIST.has(file)) return false + const code = getCode(line) + const arg = extractFetchArg(code) + if (!isMediaLikeFetchArg(arg)) return false + return !isSafeFetchArg(arg) + }) + + const violations = [ + ...extractViolations.map((line) => `extractCOSKey forbidden: ${line}`), + ...fetchViolations.map((line) => `fetch without toFetchableUrl: ${line}`), + ] + + if (violations.length > 0) { + process.stderr.write('[check:media-normalization] found violations:\n') + for (const item of violations) { + process.stderr.write(`- ${item}\n`) + } + process.exit(1) + } + + process.stdout.write( + `[check:media-normalization] ok extract_scanned=${extractLines.length} fetch_scanned=${fetchLines.length} allow_extract=${EXTRACT_ALLOWLIST.size} allow_fetch=${FETCH_MEDIA_ALLOWLIST.size}\n`, + ) +} + +main() diff --git a/scripts/check-model-config-contract.mjs b/scripts/check-model-config-contract.mjs new file mode 100644 index 0000000..347c40b --- /dev/null +++ b/scripts/check-model-config-contract.mjs @@ -0,0 +1,462 @@ +let prisma + +const STRICT = process.argv.includes('--strict') +const MODEL_FIELDS = [ + 'analysisModel', + 'characterModel', + 'locationModel', + 'storyboardModel', + 'editModel', + 'videoModel', +] +const MAX_SAMPLES = 200 +const CAPABILITY_NAMESPACES = new Set(['llm', 'image', 'video', 'audio', 'lipsync']) +const MODEL_TYPES = new Set(['llm', 'image', 'video', 'audio', 'lipsync']) +const CAPABILITY_NAMESPACE_ALLOWED_FIELDS = { + llm: new Set(['reasoningEffortOptions', 'fieldI18n']), + image: new Set(['resolutionOptions', 'fieldI18n']), + video: new Set([ + 'durationOptions', + 'fpsOptions', + 'resolutionOptions', + 'firstlastframe', + 'supportGenerateAudio', + 'fieldI18n', + ]), + audio: new Set(['voiceOptions', 'rateOptions', 'fieldI18n']), + lipsync: new Set(['modeOptions', 'fieldI18n']), +} + +const CAPABILITY_NAMESPACE_I18N_FIELDS = { + llm: { + reasoningEffort: 'reasoningEffortOptions', + }, + image: { + resolution: 'resolutionOptions', + }, + video: { + duration: 'durationOptions', + fps: 'fpsOptions', + resolution: 'resolutionOptions', + }, + audio: { + voice: 'voiceOptions', + rate: 'rateOptions', + }, + lipsync: { + mode: 'modeOptions', + }, +} + +function isRecord(value) { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function isNonEmptyString(value) { + return typeof value === 'string' && value.trim().length > 0 +} + +function isStringArray(value) { + return Array.isArray(value) && value.every((item) => isNonEmptyString(item)) +} + +function isNumberArray(value) { + return Array.isArray(value) && value.every((item) => typeof item === 'number' && Number.isFinite(item)) +} + +function parseModelKeyStrict(value) { + if (!isNonEmptyString(value)) return null + const raw = value.trim() + const marker = raw.indexOf('::') + if (marker === -1) return null + const provider = raw.slice(0, marker).trim() + const modelId = raw.slice(marker + 2).trim() + if (!provider || !modelId) return null + return { + provider, + modelId, + modelKey: `${provider}::${modelId}`, + } +} + +function addSample(summary, sample) { + if (summary.samples.length >= MAX_SAMPLES) return + summary.samples.push(sample) +} + +function pushIssue(issues, field, message) { + issues.push({ field, message }) +} + +function isI18nKey(value) { + return isNonEmptyString(value) && value.includes('.') +} + +function validateAllowedFields(issues, namespace, namespaceValue) { + if (!isRecord(namespaceValue)) return + const allowedFields = CAPABILITY_NAMESPACE_ALLOWED_FIELDS[namespace] + for (const field of Object.keys(namespaceValue)) { + if (allowedFields.has(field)) continue + if (field === 'i18n') { + pushIssue(issues, `capabilities.${namespace}.${field}`, 'use fieldI18n instead of i18n') + continue + } + pushIssue(issues, `capabilities.${namespace}.${field}`, `unknown capability field: ${field}`) + } +} + +function validateFieldI18nMap(issues, namespace, namespaceValue) { + if (!isRecord(namespaceValue)) return + if (namespaceValue.fieldI18n === undefined) return + if (!isRecord(namespaceValue.fieldI18n)) { + pushIssue(issues, `capabilities.${namespace}.fieldI18n`, 'fieldI18n must be an object') + return + } + + const allowedI18nFields = CAPABILITY_NAMESPACE_I18N_FIELDS[namespace] + for (const [fieldName, fieldConfig] of Object.entries(namespaceValue.fieldI18n)) { + if (!(fieldName in allowedI18nFields)) { + pushIssue( + issues, + `capabilities.${namespace}.fieldI18n.${fieldName}`, + `unknown i18n field: ${fieldName}`, + ) + continue + } + if (!isRecord(fieldConfig)) { + pushIssue( + issues, + `capabilities.${namespace}.fieldI18n.${fieldName}`, + 'field i18n config must be an object', + ) + continue + } + + if (fieldConfig.labelKey !== undefined && !isI18nKey(fieldConfig.labelKey)) { + pushIssue( + issues, + `capabilities.${namespace}.fieldI18n.${fieldName}.labelKey`, + 'labelKey must be an i18n key', + ) + } + if (fieldConfig.unitKey !== undefined && !isI18nKey(fieldConfig.unitKey)) { + pushIssue( + issues, + `capabilities.${namespace}.fieldI18n.${fieldName}.unitKey`, + 'unitKey must be an i18n key', + ) + } + if (fieldConfig.optionLabelKeys !== undefined) { + if (!isRecord(fieldConfig.optionLabelKeys)) { + pushIssue( + issues, + `capabilities.${namespace}.fieldI18n.${fieldName}.optionLabelKeys`, + 'optionLabelKeys must be an object', + ) + continue + } + + const optionFieldName = allowedI18nFields[fieldName] + const allowedOptionsRaw = namespaceValue[optionFieldName] + const allowedOptions = Array.isArray(allowedOptionsRaw) + ? new Set(allowedOptionsRaw.map((value) => String(value))) + : null + + for (const [optionValue, optionLabelKey] of Object.entries(fieldConfig.optionLabelKeys)) { + if (!isI18nKey(optionLabelKey)) { + pushIssue( + issues, + `capabilities.${namespace}.fieldI18n.${fieldName}.optionLabelKeys.${optionValue}`, + 'option label must be an i18n key', + ) + } + if (allowedOptions && !allowedOptions.has(optionValue)) { + pushIssue( + issues, + `capabilities.${namespace}.fieldI18n.${fieldName}.optionLabelKeys.${optionValue}`, + `option ${optionValue} is not defined in ${optionFieldName}`, + ) + } + } + } + } +} + +function validateCapabilities(modelType, capabilities) { + const issues = [] + if (!MODEL_TYPES.has(modelType)) { + pushIssue(issues, 'type', 'type must be llm/image/video/audio/lipsync') + return issues + } + if (capabilities === undefined || capabilities === null) return issues + if (!isRecord(capabilities)) { + pushIssue(issues, 'capabilities', 'capabilities must be an object') + return issues + } + + for (const namespace of Object.keys(capabilities)) { + if (!CAPABILITY_NAMESPACES.has(namespace)) { + pushIssue(issues, `capabilities.${namespace}`, `unknown capabilities namespace: ${namespace}`) + continue + } + if (namespace !== modelType) { + pushIssue(issues, `capabilities.${namespace}`, `namespace ${namespace} is not allowed for model type ${modelType}`) + } + } + + const llm = capabilities.llm + if (llm !== undefined) { + if (!isRecord(llm)) { + pushIssue(issues, 'capabilities.llm', 'llm capabilities must be an object') + } else { + validateAllowedFields(issues, 'llm', llm) + if (llm.reasoningEffortOptions !== undefined && !isStringArray(llm.reasoningEffortOptions)) { + pushIssue(issues, 'capabilities.llm.reasoningEffortOptions', 'must be string array') + } + validateFieldI18nMap(issues, 'llm', llm) + } + } + + const image = capabilities.image + if (image !== undefined) { + if (!isRecord(image)) { + pushIssue(issues, 'capabilities.image', 'image capabilities must be an object') + } else { + validateAllowedFields(issues, 'image', image) + if (image.resolutionOptions !== undefined && !isStringArray(image.resolutionOptions)) { + pushIssue(issues, 'capabilities.image.resolutionOptions', 'must be string array') + } + validateFieldI18nMap(issues, 'image', image) + } + } + + const video = capabilities.video + if (video !== undefined) { + if (!isRecord(video)) { + pushIssue(issues, 'capabilities.video', 'video capabilities must be an object') + } else { + validateAllowedFields(issues, 'video', video) + if (video.durationOptions !== undefined && !isNumberArray(video.durationOptions)) { + pushIssue(issues, 'capabilities.video.durationOptions', 'must be number array') + } + if (video.fpsOptions !== undefined && !isNumberArray(video.fpsOptions)) { + pushIssue(issues, 'capabilities.video.fpsOptions', 'must be number array') + } + if (video.resolutionOptions !== undefined && !isStringArray(video.resolutionOptions)) { + pushIssue(issues, 'capabilities.video.resolutionOptions', 'must be string array') + } + if (video.supportGenerateAudio !== undefined && typeof video.supportGenerateAudio !== 'boolean') { + pushIssue(issues, 'capabilities.video.supportGenerateAudio', 'must be boolean') + } + if (video.firstlastframe !== undefined && typeof video.firstlastframe !== 'boolean') { + pushIssue(issues, 'capabilities.video.firstlastframe', 'must be boolean') + } + validateFieldI18nMap(issues, 'video', video) + } + } + + const audio = capabilities.audio + if (audio !== undefined) { + if (!isRecord(audio)) { + pushIssue(issues, 'capabilities.audio', 'audio capabilities must be an object') + } else { + validateAllowedFields(issues, 'audio', audio) + if (audio.voiceOptions !== undefined && !isStringArray(audio.voiceOptions)) { + pushIssue(issues, 'capabilities.audio.voiceOptions', 'must be string array') + } + if (audio.rateOptions !== undefined && !isStringArray(audio.rateOptions)) { + pushIssue(issues, 'capabilities.audio.rateOptions', 'must be string array') + } + validateFieldI18nMap(issues, 'audio', audio) + } + } + + const lipsync = capabilities.lipsync + if (lipsync !== undefined) { + if (!isRecord(lipsync)) { + pushIssue(issues, 'capabilities.lipsync', 'lipsync capabilities must be an object') + } else { + validateAllowedFields(issues, 'lipsync', lipsync) + if (lipsync.modeOptions !== undefined && !isStringArray(lipsync.modeOptions)) { + pushIssue(issues, 'capabilities.lipsync.modeOptions', 'must be string array') + } + validateFieldI18nMap(issues, 'lipsync', lipsync) + } + } + + return issues +} + +async function main() { + let PrismaClient + try { + ({ PrismaClient } = await import('@prisma/client')) + } catch { + throw new Error('MISSING_DEPENDENCY: @prisma/client is not installed, run npm install first') + } + + prisma = new PrismaClient() + const summary = { + generatedAt: new Date().toISOString(), + userPreference: { + total: 0, + invalidModelKeyFields: 0, + invalidCustomModelsJson: 0, + invalidCustomModelShape: 0, + invalidCapabilities: 0, + }, + novelPromotionProject: { + total: 0, + invalidModelKeyFields: 0, + }, + samples: [], + } + + const userPrefs = await prisma.userPreference.findMany({ + select: { + id: true, + customModels: true, + analysisModel: true, + characterModel: true, + locationModel: true, + storyboardModel: true, + editModel: true, + videoModel: true, + }, + }) + + for (const pref of userPrefs) { + summary.userPreference.total += 1 + for (const field of MODEL_FIELDS) { + const rawValue = pref[field] + if (!rawValue) continue + if (!parseModelKeyStrict(rawValue)) { + summary.userPreference.invalidModelKeyFields += 1 + addSample(summary, { + table: 'userPreference', + rowId: pref.id, + field, + reason: 'model field is not provider::modelId', + }) + } + } + + if (!pref.customModels) continue + let parsedCustomModels + try { + parsedCustomModels = JSON.parse(pref.customModels) + } catch { + summary.userPreference.invalidCustomModelsJson += 1 + addSample(summary, { + table: 'userPreference', + rowId: pref.id, + field: 'customModels', + reason: 'invalid JSON', + }) + continue + } + if (!Array.isArray(parsedCustomModels)) { + summary.userPreference.invalidCustomModelsJson += 1 + addSample(summary, { + table: 'userPreference', + rowId: pref.id, + field: 'customModels', + reason: 'customModels is not array', + }) + continue + } + + for (let index = 0; index < parsedCustomModels.length; index += 1) { + const modelRaw = parsedCustomModels[index] + if (!isRecord(modelRaw)) { + summary.userPreference.invalidCustomModelShape += 1 + addSample(summary, { + table: 'userPreference', + rowId: pref.id, + field: `customModels[${index}]`, + reason: 'model item is not object', + }) + continue + } + + const modelKey = isNonEmptyString(modelRaw.modelKey) ? modelRaw.modelKey.trim() : '' + const provider = isNonEmptyString(modelRaw.provider) ? modelRaw.provider.trim() : '' + const modelId = isNonEmptyString(modelRaw.modelId) ? modelRaw.modelId.trim() : '' + const parsed = parseModelKeyStrict(modelKey) + if (!parsed || parsed.provider !== provider || parsed.modelId !== modelId) { + summary.userPreference.invalidCustomModelShape += 1 + addSample(summary, { + table: 'userPreference', + rowId: pref.id, + field: `customModels[${index}].modelKey`, + reason: 'modelKey/provider/modelId mismatch', + }) + } + + const modelType = isNonEmptyString(modelRaw.type) ? modelRaw.type.trim() : '' + const capabilityIssues = validateCapabilities(modelType, modelRaw.capabilities) + if (capabilityIssues.length > 0) { + summary.userPreference.invalidCapabilities += 1 + addSample(summary, { + table: 'userPreference', + rowId: pref.id, + field: capabilityIssues[0].field, + reason: capabilityIssues[0].message, + }) + } + } + } + + const projects = await prisma.novelPromotionProject.findMany({ + select: { + id: true, + analysisModel: true, + characterModel: true, + locationModel: true, + storyboardModel: true, + editModel: true, + videoModel: true, + }, + }) + + for (const project of projects) { + summary.novelPromotionProject.total += 1 + for (const field of MODEL_FIELDS) { + const rawValue = project[field] + if (!rawValue) continue + if (!parseModelKeyStrict(rawValue)) { + summary.novelPromotionProject.invalidModelKeyFields += 1 + addSample(summary, { + table: 'novelPromotionProject', + rowId: project.id, + field, + reason: 'model field is not provider::modelId', + }) + } + } + } + + process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`) + + if (!STRICT) return + const hasViolations = summary.userPreference.invalidModelKeyFields > 0 + || summary.userPreference.invalidCustomModelsJson > 0 + || summary.userPreference.invalidCustomModelShape > 0 + || summary.userPreference.invalidCapabilities > 0 + || summary.novelPromotionProject.invalidModelKeyFields > 0 + + if (hasViolations) { + process.exitCode = 1 + } +} + +main() + .catch((error) => { + process.stderr.write(`[check-model-config-contract] failed: ${String(error)}\n`) + process.exitCode = 1 + }) + .finally(async () => { + if (prisma) { + await prisma.$disconnect() + } + }) diff --git a/scripts/check-no-console.ts b/scripts/check-no-console.ts new file mode 100644 index 0000000..33b6a24 --- /dev/null +++ b/scripts/check-no-console.ts @@ -0,0 +1,52 @@ +import { execSync } from 'node:child_process' + +const ALLOWLIST = new Set([ + 'src/lib/logging/core.ts', + 'src/lib/logging/config.ts', + 'src/lib/logging/context.ts', + 'src/lib/logging/redact.ts', + 'scripts/check-no-console.ts', + 'scripts/guards/no-api-direct-llm-call.mjs', + 'scripts/guards/no-internal-task-sync-fallback.mjs', + 'scripts/guards/no-media-provider-bypass.mjs', + 'scripts/guards/no-server-mirror-state.mjs', + 'scripts/guards/task-loading-guard.mjs', + 'scripts/guards/task-target-states-no-polling-guard.mjs', +]) + +function run(cmd: string): string { + try { + return execSync(cmd, { encoding: 'utf8' }) + } catch (error: unknown) { + if (error && typeof error === 'object' && 'stdout' in error) { + const stdout = (error as { stdout?: unknown }).stdout + return typeof stdout === 'string' ? stdout : '' + } + return '' + } +} + +function main() { + const output = run(`rg -n "console\\\\.(log|info|warn|error|debug)\\\\(" src scripts`) + const lines = output + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + + const violations = lines.filter((line) => { + const file = line.split(':', 1)[0] + return !ALLOWLIST.has(file) + }) + + if (violations.length > 0) { + process.stderr.write('[check:logs] found forbidden console usage:\n') + for (const line of violations) { + process.stderr.write(`- ${line}\n`) + } + process.exit(1) + } + + process.stdout.write(`[check:logs] ok scanned=${lines.length} allowlist=${ALLOWLIST.size}\n`) +} + +main() diff --git a/scripts/check-outbound-image-runtime-sample.ts b/scripts/check-outbound-image-runtime-sample.ts new file mode 100644 index 0000000..a8ec19c --- /dev/null +++ b/scripts/check-outbound-image-runtime-sample.ts @@ -0,0 +1,323 @@ +import { prisma } from '@/lib/prisma' +import { TASK_TYPE } from '@/lib/task/types' + +type AnyJson = unknown + +type Match = { + path: string + value: string +} + +type Options = { + minutes: number + limit: number + projectId: string | null + strictNoData: boolean + includeEvents: boolean + maxEventsPerTask: number + json: boolean +} + +type FailureType = 'normalize' | 'model' | 'cancelled' | 'other' + +const MODEL_ERROR_CODES = new Set([ + 'GENERATION_FAILED', + 'GENERATION_TIMEOUT', + 'RATE_LIMIT', + 'EXTERNAL_ERROR', + 'SENSITIVE_CONTENT', +]) + +function parseNumberArg(name: string, fallback: number): number { + const raw = process.argv.find((arg) => arg.startsWith(`--${name}=`)) + if (!raw) return fallback + const value = Number.parseInt(raw.split('=')[1] || '', 10) + return Number.isFinite(value) && value > 0 ? value : fallback +} + +function parseStringArg(name: string): string | null { + const raw = process.argv.find((arg) => arg.startsWith(`--${name}=`)) + if (!raw) return null + const value = (raw.split('=')[1] || '').trim() + return value || null +} + +function parseBooleanArg(name: string, fallback = false): boolean { + const raw = process.argv.find((arg) => arg.startsWith(`--${name}=`)) + if (!raw) return fallback + const value = (raw.split('=')[1] || '').trim().toLowerCase() + return value === '1' || value === 'true' || value === 'yes' || value === 'on' +} + +function parseOptions(): Options { + return { + minutes: parseNumberArg('minutes', 60 * 24), + limit: parseNumberArg('limit', 200), + projectId: parseStringArg('projectId'), + strictNoData: parseBooleanArg('strictNoData', false), + includeEvents: parseBooleanArg('includeEvents', false), + maxEventsPerTask: parseNumberArg('maxEventsPerTask', 40), + json: parseBooleanArg('json', false), + } +} + +function toExcerpt(value: string, max = 180): string { + if (value.length <= max) return value + return `${value.slice(0, max)}...` +} + +function findStringMatches( + value: AnyJson, + predicate: (input: string) => boolean, + path = '$', + matches: Match[] = [], +): Match[] { + if (typeof value === 'string') { + if (predicate(value)) matches.push({ path, value }) + return matches + } + if (Array.isArray(value)) { + value.forEach((item, index) => { + findStringMatches(item, predicate, `${path}[${index}]`, matches) + }) + return matches + } + if (value && typeof value === 'object') { + for (const [key, next] of Object.entries(value as Record)) { + findStringMatches(next, predicate, `${path}.${key}`, matches) + } + } + return matches +} + +function classifyFailure(task: { + errorCode: string | null + errorMessage: string | null + result: AnyJson | null + events: Array<{ payload: AnyJson | null }> +}): FailureType { + const code = (task.errorCode || '').trim().toUpperCase() + const normalizeRe = /normalize|video_frame_normalize|normalizeReferenceImagesForGeneration|reference image normalize failed|outbound image input is empty|relative_path_rejected/i + const modelRe = /generation failed|provider|upstream|rate limit|timed out|timeout|sensitive/i + + if (code === 'TASK_CANCELLED') return 'cancelled' + if (MODEL_ERROR_CODES.has(code)) return 'model' + if (code) { + const explicitNormalizeCode = code === 'INVALID_PARAMS' || code === 'OUTBOUND_IMAGE_FETCH_FAILED' + if (explicitNormalizeCode) return 'normalize' + return 'other' + } + + const values: string[] = [] + if (code) values.push(code) + if (task.errorMessage) values.push(task.errorMessage) + if (task.result) { + for (const hit of findStringMatches(task.result, () => true)) { + values.push(hit.value) + } + } + for (const event of task.events) { + if (!event.payload) continue + for (const hit of findStringMatches(event.payload, () => true)) { + values.push(hit.value) + } + } + + if (values.some((item) => normalizeRe.test(item))) return 'normalize' + if (values.some((item) => modelRe.test(item))) return 'model' + return 'other' +} + +async function main() { + const options = parseOptions() + const since = new Date(Date.now() - options.minutes * 60_000) + const monitoredTypes = [ + TASK_TYPE.MODIFY_ASSET_IMAGE, + TASK_TYPE.ASSET_HUB_MODIFY, + TASK_TYPE.VIDEO_PANEL, + ] + + const tasks = await prisma.task.findMany({ + where: { + type: { in: monitoredTypes }, + createdAt: { gte: since }, + ...(options.projectId ? { projectId: options.projectId } : {}), + }, + select: { + id: true, + type: true, + status: true, + projectId: true, + targetType: true, + targetId: true, + createdAt: true, + errorCode: true, + errorMessage: true, + payload: true, + result: true, + }, + orderBy: { createdAt: 'desc' }, + take: options.limit, + }) + + if (tasks.length === 0) { + process.stdout.write( + `[check:outbound-image-runtime-sample] no data window=${options.minutes}m limit=${options.limit} strictNoData=${options.strictNoData}\n`, + ) + if (options.strictNoData) process.exit(2) + return + } + + const eventsByTaskId = new Map>() + let eventCount = 0 + if (options.includeEvents) { + for (const task of tasks) { + const rows = await prisma.taskEvent.findMany({ + where: { taskId: task.id }, + select: { + taskId: true, + eventType: true, + payload: true, + createdAt: true, + }, + orderBy: { id: 'desc' }, + take: options.maxEventsPerTask, + }) + const ordered = [...rows].reverse() + eventCount += ordered.length + if (ordered.length > 0) { + eventsByTaskId.set( + task.id, + ordered.map((event) => ({ + eventType: event.eventType, + payload: event.payload, + createdAt: event.createdAt, + })), + ) + } + } + } + + const nextImagePredicate = (input: string) => input.includes('/_next/image') + const hits: Array<{ + taskId: string + taskType: string + source: 'task.payload' | 'task.result' | 'task.event' + path: string + value: string + }> = [] + + let failedCount = 0 + const failedByClass: Record = { + normalize: 0, + model: 0, + cancelled: 0, + other: 0, + } + const failedByCode: Record = {} + + for (const task of tasks) { + const taskEventsForTask = eventsByTaskId.get(task.id) || [] + + if (task.payload) { + for (const match of findStringMatches(task.payload, nextImagePredicate)) { + hits.push({ + taskId: task.id, + taskType: task.type, + source: 'task.payload', + path: match.path, + value: match.value, + }) + } + } + + if (task.result) { + for (const match of findStringMatches(task.result, nextImagePredicate)) { + hits.push({ + taskId: task.id, + taskType: task.type, + source: 'task.result', + path: match.path, + value: match.value, + }) + } + } + + for (const event of taskEventsForTask) { + if (!event.payload) continue + for (const match of findStringMatches(event.payload, nextImagePredicate)) { + hits.push({ + taskId: task.id, + taskType: task.type, + source: 'task.event', + path: match.path, + value: match.value, + }) + } + } + + if (task.status === 'failed') { + failedCount += 1 + const code = (task.errorCode || 'UNKNOWN').trim() || 'UNKNOWN' + failedByCode[code] = (failedByCode[code] || 0) + 1 + const failureType = classifyFailure({ + errorCode: task.errorCode, + errorMessage: task.errorMessage, + result: task.result, + events: taskEventsForTask, + }) + failedByClass[failureType] += 1 + } + } + + const typeCount = tasks.reduce>((acc, item) => { + acc[item.type] = (acc[item.type] || 0) + 1 + return acc + }, {}) + + process.stdout.write( + `[check:outbound-image-runtime-sample] window=${options.minutes}m sampled=${tasks.length} events=${eventCount} includeEvents=${options.includeEvents} next_image_hits=${hits.length}\n`, + ) + process.stdout.write(`[check:outbound-image-runtime-sample] task_types=${JSON.stringify(typeCount)}\n`) + process.stdout.write( + `[check:outbound-image-runtime-sample] failures total=${failedCount} normalize=${failedByClass.normalize} model=${failedByClass.model} cancelled=${failedByClass.cancelled} other=${failedByClass.other} by_code=${JSON.stringify(failedByCode)}\n`, + ) + + if (options.json) { + process.stdout.write( + `${JSON.stringify({ + windowMinutes: options.minutes, + sampled: tasks.length, + events: eventCount, + includeEvents: options.includeEvents, + nextImageHits: hits.length, + taskTypes: typeCount, + failures: { + total: failedCount, + byClass: failedByClass, + byCode: failedByCode, + }, + })}\n`, + ) + } + + if (hits.length > 0) { + process.stderr.write('[check:outbound-image-runtime-sample] found /_next/image contamination:\n') + for (const hit of hits.slice(0, 20)) { + process.stderr.write( + `- task=${hit.taskId} type=${hit.taskType} source=${hit.source} path=${hit.path} value=${toExcerpt(hit.value)}\n`, + ) + } + process.exit(1) + } +} + +main() + .catch((error) => { + const message = error instanceof Error ? error.message : String(error) + process.stderr.write(`[check:outbound-image-runtime-sample] failed: ${message}\n`) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/check-outbound-image-success-rate.ts b/scripts/check-outbound-image-success-rate.ts new file mode 100644 index 0000000..919cd84 --- /dev/null +++ b/scripts/check-outbound-image-success-rate.ts @@ -0,0 +1,224 @@ +import { prisma } from '@/lib/prisma' +import { TASK_STATUS, TASK_TYPE } from '@/lib/task/types' + +type StatusCount = Record + +type WindowSummary = { + total: number + finishedTotal: number + completed: number + failed: number + successRate: number | null + byStatus: StatusCount + byType: Record +} + +type Options = { + minutes: number + baselineMinutes: number + baselineOffsetMinutes: number + projectId: string | null + tolerancePct: number + minFinishedSamples: number + strict: boolean + json: boolean +} + +const DEFAULT_MINUTES = 60 * 24 * 7 +const DEFAULT_TOLERANCE_PCT = 2 +const DEFAULT_MIN_FINISHED_SAMPLES = 20 + +function parseNumberArg(name: string, fallback: number): number { + const raw = process.argv.find((arg) => arg.startsWith(`--${name}=`)) + if (!raw) return fallback + const value = Number.parseFloat(raw.split('=')[1] || '') + return Number.isFinite(value) && value > 0 ? value : fallback +} + +function parseBooleanArg(name: string, fallback = false): boolean { + const raw = process.argv.find((arg) => arg.startsWith(`--${name}=`)) + if (!raw) return fallback + const value = (raw.split('=')[1] || '').trim().toLowerCase() + return value === '1' || value === 'true' || value === 'yes' || value === 'on' +} + +function parseStringArg(name: string): string | null { + const raw = process.argv.find((arg) => arg.startsWith(`--${name}=`)) + if (!raw) return null + const value = (raw.split('=')[1] || '').trim() + return value || null +} + +function parseOptions(): Options { + const minutes = parseNumberArg('minutes', DEFAULT_MINUTES) + const baselineMinutes = parseNumberArg('baselineMinutes', minutes) + const baselineOffsetMinutes = parseNumberArg('baselineOffsetMinutes', minutes) + return { + minutes, + baselineMinutes, + baselineOffsetMinutes, + projectId: parseStringArg('projectId'), + tolerancePct: parseNumberArg('tolerancePct', DEFAULT_TOLERANCE_PCT), + minFinishedSamples: parseNumberArg('minFinishedSamples', DEFAULT_MIN_FINISHED_SAMPLES), + strict: parseBooleanArg('strict', false), + json: parseBooleanArg('json', false), + } +} + +function asPct(value: number | null): string { + return value === null ? 'N/A' : `${value.toFixed(2)}%` +} + +function getSuccessRate(completed: number, failed: number): number | null { + const total = completed + failed + if (total <= 0) return null + return (completed / total) * 100 +} + +function summarizeRows( + rows: Array<{ status: string; type: string }>, +): WindowSummary { + const byStatus: StatusCount = {} + const byType: Record = {} + for (const row of rows) { + byStatus[row.status] = (byStatus[row.status] || 0) + 1 + byType[row.type] = (byType[row.type] || 0) + 1 + } + + const completed = byStatus[TASK_STATUS.COMPLETED] || 0 + const failed = byStatus[TASK_STATUS.FAILED] || 0 + const finishedTotal = completed + failed + + return { + total: rows.length, + finishedTotal, + completed, + failed, + successRate: getSuccessRate(completed, failed), + byStatus, + byType, + } +} + +async function fetchWindowSummary(params: { + from: Date + to: Date + projectId: string | null +}) { + const monitoredTypes = [ + TASK_TYPE.MODIFY_ASSET_IMAGE, + TASK_TYPE.ASSET_HUB_MODIFY, + TASK_TYPE.VIDEO_PANEL, + ] + + const rows = await prisma.task.findMany({ + where: { + type: { in: monitoredTypes }, + createdAt: { + gte: params.from, + lt: params.to, + }, + ...(params.projectId ? { projectId: params.projectId } : {}), + }, + select: { + status: true, + type: true, + }, + }) + + return summarizeRows(rows) +} + +async function main() { + const options = parseOptions() + const now = Date.now() + + const currentEnd = new Date(now) + const currentStart = new Date(now - options.minutes * 60_000) + + const baselineEnd = new Date(now - options.baselineOffsetMinutes * 60_000) + const baselineStart = new Date(baselineEnd.getTime() - options.baselineMinutes * 60_000) + + const [current, baseline] = await Promise.all([ + fetchWindowSummary({ + from: currentStart, + to: currentEnd, + projectId: options.projectId, + }), + fetchWindowSummary({ + from: baselineStart, + to: baselineEnd, + projectId: options.projectId, + }), + ]) + + const hasEnoughCurrent = current.finishedTotal >= options.minFinishedSamples + const hasEnoughBaseline = baseline.finishedTotal >= options.minFinishedSamples + const hasEnoughSamples = hasEnoughCurrent && hasEnoughBaseline + + const rateDeltaPct = + current.successRate !== null && baseline.successRate !== null + ? current.successRate - baseline.successRate + : null + + const meetsTolerance = + rateDeltaPct !== null + ? rateDeltaPct >= -Math.abs(options.tolerancePct) + : false + + const status = hasEnoughSamples + ? meetsTolerance + ? 'pass' + : 'fail' + : 'blocked' + + process.stdout.write( + `[check:outbound-image-success-rate] current=${asPct(current.successRate)} baseline=${asPct(baseline.successRate)} delta=${asPct(rateDeltaPct)} tolerance=-${Math.abs(options.tolerancePct).toFixed(2)}% status=${status}\n`, + ) + process.stdout.write( + `[check:outbound-image-success-rate] current_finished=${current.finishedTotal} baseline_finished=${baseline.finishedTotal} min_required=${options.minFinishedSamples}\n`, + ) + process.stdout.write( + `[check:outbound-image-success-rate] current_by_type=${JSON.stringify(current.byType)} baseline_by_type=${JSON.stringify(baseline.byType)}\n`, + ) + + if (options.json) { + process.stdout.write( + `${JSON.stringify({ + status, + tolerancePct: options.tolerancePct, + minFinishedSamples: options.minFinishedSamples, + windows: { + current: { + from: currentStart.toISOString(), + to: currentEnd.toISOString(), + ...current, + }, + baseline: { + from: baselineStart.toISOString(), + to: baselineEnd.toISOString(), + ...baseline, + }, + }, + rateDeltaPct, + hasEnoughSamples, + })}\n`, + ) + } + + if (!options.strict) return + + if (status === 'pass') return + if (status === 'blocked') process.exit(2) + process.exit(1) +} + +main() + .catch((error) => { + const message = error instanceof Error ? error.message : String(error) + process.stderr.write(`[check:outbound-image-success-rate] failed: ${message}\n`) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/check-outbound-image-unification.ts b/scripts/check-outbound-image-unification.ts new file mode 100644 index 0000000..60c061f --- /dev/null +++ b/scripts/check-outbound-image-unification.ts @@ -0,0 +1,172 @@ +import fs from 'node:fs' +import path from 'node:path' + +type Rule = { + file: string + pattern: RegExp + message: string +} + +function readFile(relativePath: string): string { + const fullPath = path.resolve(process.cwd(), relativePath) + return fs.readFileSync(fullPath, 'utf8') +} + +const mustIncludeRules: Rule[] = [ + { + file: 'src/lib/media/outbound-image.ts', + pattern: /export\s+async\s+function\s+normalizeToOriginalMediaUrl\s*\(/, + message: 'missing normalizeToOriginalMediaUrl export', + }, + { + file: 'src/lib/media/outbound-image.ts', + pattern: /export\s+async\s+function\s+normalizeToBase64ForGeneration\s*\(/, + message: 'missing normalizeToBase64ForGeneration export', + }, + { + file: 'src/lib/media/outbound-image.ts', + pattern: /export\s+async\s+function\s+normalizeReferenceImagesForGeneration\s*\(/, + message: 'missing normalizeReferenceImagesForGeneration export', + }, + { + file: 'src/lib/media/outbound-image.ts', + pattern: /class\s+OutboundImageNormalizeError\s+extends\s+Error/, + message: 'outbound-image.ts must expose structured normalize error type', + }, + { + file: 'src/lib/media/outbound-image.ts', + pattern: /OUTBOUND_IMAGE_FETCH_FAILED/, + message: 'outbound-image.ts must classify fetch failures with structured error codes', + }, + { + file: 'src/lib/media/outbound-image.ts', + pattern: /OUTBOUND_IMAGE_REFERENCE_ALL_FAILED/, + message: 'outbound-image.ts must fail explicitly when all references fail to normalize', + }, + { + file: 'src/lib/cos.ts', + pattern: /import\s+\{\s*normalizeToBase64ForGeneration\s*\}\s+from\s+'@\/lib\/media\/outbound-image'/, + message: 'cos.ts must import normalizeToBase64ForGeneration', + }, + { + file: 'src/lib/cos.ts', + pattern: /return\s+await\s+normalizeToBase64ForGeneration\(keyOrUrl\)/, + message: 'imageUrlToBase64 must delegate to normalizeToBase64ForGeneration', + }, + { + file: 'src/lib/workers/handlers/image-task-handlers-core.ts', + pattern: /normalizeToBase64ForGeneration\(currentUrl\)/, + message: 'image-task-handlers-core.ts must convert currentUrl to base64 before outbound', + }, + { + file: 'src/lib/workers/handlers/image-task-handlers-core.ts', + pattern: /normalizeReferenceImagesForGeneration\(extraReferenceInputs\)/, + message: 'image-task-handlers-core.ts must normalize extra references before outbound', + }, + { + file: 'src/lib/workers/video.worker.ts', + pattern: /const\s+sourceImageBase64\s*=\s*await\s+normalizeToBase64ForGeneration\(sourceImageUrl\)/, + message: 'video.worker.ts must normalize source frame to base64', + }, + { + file: 'src/lib/workers/video.worker.ts', + pattern: /lastFrameImageBase64\s*=\s*await\s+normalizeToBase64ForGeneration\(lastFrameUrl\)/, + message: 'video.worker.ts must normalize last frame to base64', + }, + { + file: 'src/app/api/novel-promotion/[projectId]/modify-asset-image/route.ts', + pattern: /sanitizeImageInputsForTaskPayload/, + message: 'modify-asset-image route must sanitize image inputs', + }, + { + file: 'src/app/api/novel-promotion/[projectId]/modify-storyboard-image/route.ts', + pattern: /sanitizeImageInputsForTaskPayload/, + message: 'modify-storyboard-image route must sanitize image inputs', + }, + { + file: 'src/app/api/asset-hub/modify-image/route.ts', + pattern: /sanitizeImageInputsForTaskPayload/, + message: 'asset-hub modify-image route must sanitize image inputs', + }, + { + file: 'src/components/ui/ImagePreviewModal.tsx', + pattern: /import\s+\{\s*resolveOriginalImageUrl,\s*toDisplayImageUrl\s*\}\s+from\s+'@\/lib\/media\/image-url'/, + message: 'ImagePreviewModal must use shared image-url helpers', + }, + { + file: 'src/lib/novel-promotion/stages/video-stage-runtime-core.tsx', + pattern: /onPreviewImage=\{setPreviewImage\}/, + message: 'Video stage runtime must wire preview callback to VideoPanelCard', + }, + { + file: 'src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/video/panel-card/types.ts', + pattern: /onPreviewImage\?:\s*\(imageUrl:\s*string\)\s*=>\s*void/, + message: 'VideoPanelCard runtime props must expose onPreviewImage', + }, + { + file: 'src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/video/panel-card/VideoPanelCardHeader.tsx', + pattern: /className="absolute left-1\/2 top-1\/2 z-10 h-16 w-16 -translate-x-1\/2 -translate-y-1\/2 rounded-full"/, + message: 'VideoPanelCard play trigger must be centered small button (preview/play separation)', + }, +] + +const mustNotIncludeRules: Rule[] = [ + { + file: 'src/lib/workers/handlers/image-task-handlers-core.ts', + pattern: /referenceImages:\s*\[currentUrl\]/, + message: 'image-task-handlers-core.ts must not pass raw currentUrl directly as outbound reference', + }, + { + file: 'src/lib/workers/video.worker.ts', + pattern: /imageUrl:\s*sourceImageUrl/, + message: 'video.worker.ts must not pass raw sourceImageUrl to generator', + }, + { + file: 'src/lib/media/outbound-image.ts', + pattern: /return\s+await\s+toFetchableAbsoluteUrl\(mediaPath\)/, + message: 'outbound-image.ts must not silently fallback when /m route cannot resolve storage key', + }, + { + file: 'src/lib/media/outbound-image.ts', + pattern: /return\s+await\s+toFetchableAbsoluteUrl\(unwrappedInput\)/, + message: 'outbound-image.ts must not silently fallback unknown inputs to fetchable url', + }, +] + +function main() { + const errors: string[] = [] + const cache = new Map() + + const getContent = (file: string) => { + if (!cache.has(file)) cache.set(file, readFile(file)) + return cache.get(file) as string + } + + for (const rule of mustIncludeRules) { + const content = getContent(rule.file) + if (!rule.pattern.test(content)) { + errors.push(`${rule.file}: ${rule.message}`) + } + } + + for (const rule of mustNotIncludeRules) { + const content = getContent(rule.file) + if (rule.pattern.test(content)) { + errors.push(`${rule.file}: ${rule.message}`) + } + } + + if (errors.length > 0) { + process.stderr.write('[check:outbound-image-unification] found violations:\n') + for (const error of errors) { + process.stderr.write(`- ${error}\n`) + } + process.exit(1) + } + + process.stdout.write( + `[check:outbound-image-unification] ok include_checks=${mustIncludeRules.length} exclude_checks=${mustNotIncludeRules.length}\n`, + ) +} + +main() diff --git a/scripts/check-pricing-catalog.mjs b/scripts/check-pricing-catalog.mjs new file mode 100644 index 0000000..8fa3ea0 --- /dev/null +++ b/scripts/check-pricing-catalog.mjs @@ -0,0 +1,293 @@ +import { promises as fs } from 'node:fs' +import path from 'node:path' + +const CATALOG_DIR = path.resolve(process.cwd(), 'standards/pricing') +const CAPABILITY_CATALOG_FILE = path.resolve(process.cwd(), 'standards/capabilities/image-video.catalog.json') +const API_TYPES = new Set(['text', 'image', 'video', 'voice', 'voice-design', 'lip-sync']) +const PRICING_MODES = new Set(['flat', 'capability']) +const TEXT_TOKEN_TYPES = new Set(['input', 'output']) + +function isRecord(value) { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function isNonEmptyString(value) { + return typeof value === 'string' && value.trim().length > 0 +} + +function isCapabilityValue(value) { + return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' +} + +function isFiniteNumber(value) { + return typeof value === 'number' && Number.isFinite(value) +} + +function pushIssue(issues, file, index, field, message) { + issues.push({ file, index, field, message }) +} + +function getProviderKey(providerId) { + const marker = providerId.indexOf(':') + return marker === -1 ? providerId : providerId.slice(0, marker) +} + +function buildModelKey(modelType, provider, modelId) { + return `${modelType}::${provider}::${modelId}` +} + +async function listCatalogFiles() { + const entries = await fs.readdir(CATALOG_DIR, { withFileTypes: true }) + return entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.json')) + .map((entry) => path.join(CATALOG_DIR, entry.name)) +} + +async function readCatalog(filePath) { + const raw = await fs.readFile(filePath, 'utf8') + const parsed = JSON.parse(raw) + if (!Array.isArray(parsed)) { + throw new Error(`catalog must be an array: ${filePath}`) + } + return parsed +} + +async function readCapabilityCatalog() { + const raw = await fs.readFile(CAPABILITY_CATALOG_FILE, 'utf8') + const parsed = JSON.parse(raw) + if (!Array.isArray(parsed)) { + throw new Error(`capability catalog must be an array: ${CAPABILITY_CATALOG_FILE}`) + } + return parsed +} + +function extractCapabilityOptionFields(modelType, capabilities) { + if (!isRecord(capabilities)) return new Set() + const namespace = capabilities[modelType] + if (!isRecord(namespace)) return new Set() + + const fields = new Set() + for (const [key, value] of Object.entries(namespace)) { + if (!key.endsWith('Options')) continue + if (!Array.isArray(value) || value.length === 0) continue + const field = key.slice(0, -'Options'.length) + fields.add(field) + } + return fields +} + +function buildCapabilityOptionFieldMap(capabilityEntries) { + const map = new Map() + for (const entry of capabilityEntries) { + if (!isRecord(entry)) continue + const modelType = typeof entry.modelType === 'string' ? entry.modelType.trim() : '' + const provider = typeof entry.provider === 'string' ? entry.provider.trim() : '' + const modelId = typeof entry.modelId === 'string' ? entry.modelId.trim() : '' + if (!modelType || !provider || !modelId) continue + + const fields = extractCapabilityOptionFields(modelType, entry.capabilities) + map.set(buildModelKey(modelType, provider, modelId), fields) + const providerKey = getProviderKey(provider) + const fallbackKey = buildModelKey(modelType, providerKey, modelId) + if (!map.has(fallbackKey)) { + map.set(fallbackKey, fields) + } + } + return map +} + +function validateTier(issues, file, index, tier, tierIndex) { + if (!isRecord(tier)) { + pushIssue(issues, file, index, `pricing.tiers[${tierIndex}]`, 'tier must be object') + return + } + + if (!isRecord(tier.when) || Object.keys(tier.when).length === 0) { + pushIssue(issues, file, index, `pricing.tiers[${tierIndex}].when`, 'when must be non-empty object') + } else { + for (const [field, value] of Object.entries(tier.when)) { + if (!isCapabilityValue(value)) { + pushIssue( + issues, + file, + index, + `pricing.tiers[${tierIndex}].when.${field}`, + 'condition value must be string/number/boolean', + ) + } + } + } + + if (!isFiniteNumber(tier.amount) || tier.amount < 0) { + pushIssue(issues, file, index, `pricing.tiers[${tierIndex}].amount`, 'amount must be finite number >= 0') + } +} + +function validateTextCapabilityTiers(issues, file, index, tiers) { + const seenTokenTypes = new Set() + + for (let tierIndex = 0; tierIndex < tiers.length; tierIndex += 1) { + const tier = tiers[tierIndex] + if (!isRecord(tier) || !isRecord(tier.when)) continue + + const whenFields = Object.keys(tier.when) + if (whenFields.length !== 1 || whenFields[0] !== 'tokenType') { + pushIssue(issues, file, index, `pricing.tiers[${tierIndex}].when`, 'text capability tier must only contain tokenType') + continue + } + + const tokenType = tier.when.tokenType + if (typeof tokenType !== 'string' || !TEXT_TOKEN_TYPES.has(tokenType)) { + pushIssue(issues, file, index, `pricing.tiers[${tierIndex}].when.tokenType`, 'tokenType must be input or output') + continue + } + + if (seenTokenTypes.has(tokenType)) { + pushIssue(issues, file, index, `pricing.tiers[${tierIndex}].when.tokenType`, `duplicate tokenType tier: ${tokenType}`) + continue + } + seenTokenTypes.add(tokenType) + } + + for (const requiredTokenType of TEXT_TOKEN_TYPES) { + if (!seenTokenTypes.has(requiredTokenType)) { + pushIssue(issues, file, index, 'pricing.tiers', `missing text tier tokenType=${requiredTokenType}`) + } + } +} + +function validateMediaCapabilityTierFields(issues, file, index, item, tiers, capabilityOptionFieldsMap) { + const modelType = item.apiType + const provider = item.provider + const modelId = item.modelId + const modelKey = buildModelKey(modelType, provider, modelId) + const fallbackKey = buildModelKey(modelType, getProviderKey(provider), modelId) + const optionFields = capabilityOptionFieldsMap.get(modelKey) || capabilityOptionFieldsMap.get(fallbackKey) + + if (!optionFields || optionFields.size === 0) { + pushIssue(issues, file, index, 'pricing.tiers', `no capability option fields found for ${modelType} ${provider}/${modelId}`) + return + } + + for (let tierIndex = 0; tierIndex < tiers.length; tierIndex += 1) { + const tier = tiers[tierIndex] + if (!isRecord(tier) || !isRecord(tier.when)) continue + for (const field of Object.keys(tier.when)) { + if (!optionFields.has(field)) { + pushIssue( + issues, + file, + index, + `pricing.tiers[${tierIndex}].when.${field}`, + `field ${field} is not declared in capabilities options for ${modelType} ${provider}/${modelId}`, + ) + } + } + } +} + +function validateDuplicateCapabilityTiers(issues, file, index, tiers) { + const seen = new Set() + for (let tierIndex = 0; tierIndex < tiers.length; tierIndex += 1) { + const tier = tiers[tierIndex] + if (!isRecord(tier) || !isRecord(tier.when)) continue + const signature = JSON.stringify(Object.entries(tier.when).sort((left, right) => left[0].localeCompare(right[0]))) + if (seen.has(signature)) { + pushIssue(issues, file, index, `pricing.tiers[${tierIndex}].when`, 'duplicate capability tier condition') + continue + } + seen.add(signature) + } +} + +function validatePricing(issues, file, index, item, capabilityOptionFieldsMap) { + const pricing = item.pricing + if (!isRecord(pricing)) { + pushIssue(issues, file, index, 'pricing', 'pricing must be object') + return + } + + if (!isNonEmptyString(pricing.mode) || !PRICING_MODES.has(pricing.mode)) { + pushIssue(issues, file, index, 'pricing.mode', 'pricing.mode must be flat or capability') + return + } + + if (pricing.mode === 'flat') { + if (!isFiniteNumber(pricing.flatAmount) || pricing.flatAmount < 0) { + pushIssue(issues, file, index, 'pricing.flatAmount', 'flatAmount must be finite number >= 0') + } + return + } + + if (!Array.isArray(pricing.tiers) || pricing.tiers.length === 0) { + pushIssue(issues, file, index, 'pricing.tiers', 'tiers must be non-empty array') + return + } + + for (let tierIndex = 0; tierIndex < pricing.tiers.length; tierIndex += 1) { + validateTier(issues, file, index, pricing.tiers[tierIndex], tierIndex) + } + + validateDuplicateCapabilityTiers(issues, file, index, pricing.tiers) + + if (item.apiType === 'text') { + validateTextCapabilityTiers(issues, file, index, pricing.tiers) + return + } + + if (item.apiType === 'image' || item.apiType === 'video') { + validateMediaCapabilityTierFields(issues, file, index, item, pricing.tiers, capabilityOptionFieldsMap) + } +} + +async function main() { + const issues = [] + const files = await listCatalogFiles() + const capabilityCatalog = await readCapabilityCatalog() + const capabilityOptionFieldsMap = buildCapabilityOptionFieldMap(capabilityCatalog) + if (files.length === 0) { + throw new Error(`no pricing files found in ${CATALOG_DIR}`) + } + + for (const filePath of files) { + const items = await readCatalog(filePath) + for (let index = 0; index < items.length; index += 1) { + const item = items[index] + if (!isRecord(item)) { + pushIssue(issues, filePath, index, 'entry', 'entry must be object') + continue + } + + if (!isNonEmptyString(item.apiType) || !API_TYPES.has(item.apiType)) { + pushIssue(issues, filePath, index, 'apiType', 'apiType must be one of text/image/video/voice/voice-design/lip-sync') + } + if (!isNonEmptyString(item.provider)) { + pushIssue(issues, filePath, index, 'provider', 'provider must be non-empty string') + } + if (!isNonEmptyString(item.modelId)) { + pushIssue(issues, filePath, index, 'modelId', 'modelId must be non-empty string') + } + + validatePricing(issues, filePath, index, item, capabilityOptionFieldsMap) + } + } + + if (issues.length === 0) { + process.stdout.write(`[check-pricing-catalog] OK (${files.length} files)\n`) + return + } + + const maxPrint = 50 + for (const issue of issues.slice(0, maxPrint)) { + process.stdout.write(`[check-pricing-catalog] ${issue.file}#${issue.index} ${issue.field}: ${issue.message}\n`) + } + if (issues.length > maxPrint) { + process.stdout.write(`[check-pricing-catalog] ... ${issues.length - maxPrint} more issues\n`) + } + process.exitCode = 1 +} + +main().catch((error) => { + process.stderr.write(`[check-pricing-catalog] failed: ${String(error)}\n`) + process.exitCode = 1 +}) diff --git a/scripts/cleanup-remove-legacy-voice-data.ts b/scripts/cleanup-remove-legacy-voice-data.ts new file mode 100644 index 0000000..067848c --- /dev/null +++ b/scripts/cleanup-remove-legacy-voice-data.ts @@ -0,0 +1,197 @@ +import { prisma } from '@/lib/prisma' + +type CharacterVoiceRecord = { + id: string + customVoiceUrl: string | null +} + +type SpeakerVoiceConfig = { + voiceType?: unknown + voiceId?: unknown + audioUrl?: unknown + [key: string]: unknown +} + +type CleanupSummary = { + projectCharactersUpdated: number + globalCharactersUpdated: number + episodeSpeakerVoicesUpdated: number + episodeSpeakerVoicesCleared: number + invalidSpeakerVoicesSkipped: number +} + +function hasPlayableAudioUrl(value: unknown) { + return typeof value === 'string' && value.trim().length > 0 +} + +function normalizeVoiceType(customVoiceUrl: string | null) { + return hasPlayableAudioUrl(customVoiceUrl) ? 'custom' : null +} + +async function cleanupCharacterTable(records: CharacterVoiceRecord[], table: 'project' | 'global') { + let updated = 0 + for (const row of records) { + const nextVoiceType = normalizeVoiceType(row.customVoiceUrl) + if (table === 'project') { + await prisma.novelPromotionCharacter.update({ + where: { id: row.id }, + data: { + voiceType: nextVoiceType, + voiceId: null, + }, + }) + } else { + await prisma.globalCharacter.update({ + where: { id: row.id }, + data: { + voiceType: nextVoiceType, + voiceId: null, + }, + }) + } + updated += 1 + } + return updated +} + +function normalizeSpeakerVoices(payload: string): { + ok: true + changed: boolean + cleared: boolean + next: string | null +} | { + ok: false +} { + let parsed: unknown + try { + parsed = JSON.parse(payload) + } catch { + return { ok: false } + } + + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return { ok: false } + } + + const source = parsed as Record + const next: Record = {} + let changed = false + + for (const [speaker, value] of Object.entries(source)) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return { ok: false } + } + + const config = { ...(value as SpeakerVoiceConfig) } + if (config.voiceType === 'azure') { + if (hasPlayableAudioUrl(config.audioUrl)) { + config.voiceType = 'custom' + config.voiceId = null + next[speaker] = config + } else { + // No usable audio, drop stale azure speaker config. + } + changed = true + continue + } + + next[speaker] = config + } + + const keys = Object.keys(next) + if (keys.length === 0) { + return { + ok: true, + changed, + cleared: true, + next: null, + } + } + + return { + ok: true, + changed, + cleared: false, + next: changed ? JSON.stringify(next) : payload, + } +} + +async function main() { + const summary: CleanupSummary = { + projectCharactersUpdated: 0, + globalCharactersUpdated: 0, + episodeSpeakerVoicesUpdated: 0, + episodeSpeakerVoicesCleared: 0, + invalidSpeakerVoicesSkipped: 0, + } + + const [projectCharacters, globalCharacters] = await Promise.all([ + prisma.novelPromotionCharacter.findMany({ + where: { voiceType: 'azure' }, + select: { + id: true, + customVoiceUrl: true, + }, + }), + prisma.globalCharacter.findMany({ + where: { voiceType: 'azure' }, + select: { + id: true, + customVoiceUrl: true, + }, + }), + ]) + + summary.projectCharactersUpdated = await cleanupCharacterTable(projectCharacters, 'project') + summary.globalCharactersUpdated = await cleanupCharacterTable(globalCharacters, 'global') + + const episodes = await prisma.novelPromotionEpisode.findMany({ + where: { + speakerVoices: { not: null }, + }, + select: { + id: true, + speakerVoices: true, + }, + }) + + for (const row of episodes) { + const speakerVoices = row.speakerVoices + if (!speakerVoices || !speakerVoices.includes('"voiceType":"azure"')) { + continue + } + const normalized = normalizeSpeakerVoices(speakerVoices) + if (!normalized.ok) { + summary.invalidSpeakerVoicesSkipped += 1 + continue + } + if (!normalized.changed) { + continue + } + await prisma.novelPromotionEpisode.update({ + where: { id: row.id }, + data: { + speakerVoices: normalized.next, + }, + }) + summary.episodeSpeakerVoicesUpdated += 1 + if (normalized.cleared) { + summary.episodeSpeakerVoicesCleared += 1 + } + } + + process.stdout.write(`${JSON.stringify({ + ok: true, + checkedAt: new Date().toISOString(), + summary, + }, null, 2)}\n`) +} + +main() + .catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/guards/file-line-count-guard.mjs b/scripts/guards/file-line-count-guard.mjs new file mode 100644 index 0000000..ff895f2 --- /dev/null +++ b/scripts/guards/file-line-count-guard.mjs @@ -0,0 +1,89 @@ +#!/usr/bin/env node +import fs from 'fs' +import path from 'path' + +const ROOT = process.cwd() + +const RULES = [ + { + label: 'component', + dir: 'src', + include: (relPath) => + relPath.includes('/components/') + && /\.(ts|tsx)$/.test(relPath), + limit: 500, + }, + { + label: 'hook', + dir: 'src', + include: (relPath) => + (relPath.includes('/hooks/') || /\/use[A-Z].+\.(ts|tsx)$/.test(relPath)) + && /\.(ts|tsx)$/.test(relPath), + limit: 400, + }, + { + label: 'worker-handler', + dir: 'src/lib/workers/handlers', + include: (relPath) => /\.(ts|tsx)$/.test(relPath), + limit: 300, + }, + { + label: 'mutation', + dir: 'src/lib/query/mutations', + include: (relPath) => /\.(ts|tsx)$/.test(relPath) && !relPath.endsWith('/index.ts'), + limit: 300, + }, +] + +const walkFiles = (absDir, relBase = '') => { + if (!fs.existsSync(absDir)) return [] + const entries = fs.readdirSync(absDir, { withFileTypes: true }) + const out = [] + for (const entry of entries) { + const abs = path.join(absDir, entry.name) + const rel = path.join(relBase, entry.name).replace(/\\/g, '/') + if (entry.isDirectory()) { + out.push(...walkFiles(abs, rel)) + continue + } + out.push({ absPath: abs, relPath: rel }) + } + return out +} + +const countLines = (absPath) => { + const raw = fs.readFileSync(absPath, 'utf8') + if (raw.length === 0) return 0 + return raw.split('\n').length +} + +const violations = [] + +for (const rule of RULES) { + const absDir = path.join(ROOT, rule.dir) + const files = walkFiles(absDir, rule.dir).filter((f) => rule.include(f.relPath)) + for (const file of files) { + const lineCount = countLines(file.absPath) + if (lineCount > rule.limit) { + violations.push({ + label: rule.label, + relPath: file.relPath, + lineCount, + limit: rule.limit, + }) + } + } +} + +if (violations.length === 0) { + process.stdout.write('[file-line-count-guard] PASS\n') + process.exit(0) +} + +process.stderr.write('[file-line-count-guard] FAIL: file size budget exceeded\n') +for (const violation of violations) { + process.stderr.write( + `- [${violation.label}] ${violation.relPath}: ${violation.lineCount} > ${violation.limit}\n`, + ) +} +process.exit(1) diff --git a/scripts/guards/no-api-direct-llm-call.mjs b/scripts/guards/no-api-direct-llm-call.mjs new file mode 100644 index 0000000..0a458a2 --- /dev/null +++ b/scripts/guards/no-api-direct-llm-call.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const scanRoots = ['src/app/api', 'src/pages/api'] +const allowedPrefixes = [] +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) + +function fail(title, details = []) { + console.error(`\n[no-api-direct-llm-call] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + const ext = path.extname(entry.name) + if (sourceExtensions.has(ext)) { + out.push(fullPath) + } + } + return out +} + +function isAllowedFile(relPath) { + return allowedPrefixes.some((prefix) => relPath.startsWith(prefix)) +} + +function collectViolations(fullPath) { + const relPath = toRel(fullPath) + if (isAllowedFile(relPath)) return [] + + const content = fs.readFileSync(fullPath, 'utf8') + const lines = content.split('\n') + const violations = [] + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i] + if (/from\s+['"]@\/lib\/llm-client['"]/.test(line)) { + violations.push(`${relPath}:${i + 1} forbidden import from '@/lib/llm-client'`) + } + if (/\bchatCompletion[A-Za-z0-9_]*\s*\(/.test(line)) { + violations.push(`${relPath}:${i + 1} forbidden direct chatCompletion* call`) + } + if (/\bisInternalTaskExecution\b/.test(line)) { + violations.push(`${relPath}:${i + 1} forbidden dual-track fallback marker isInternalTaskExecution`) + } + } + + return violations +} + +const allFiles = scanRoots.flatMap((scanRoot) => walk(path.join(root, scanRoot))) +const violations = allFiles.flatMap((fullPath) => collectViolations(fullPath)) + +if (violations.length > 0) { + fail('Found forbidden direct LLM execution in production API routes', violations) +} + +console.log('[no-api-direct-llm-call] OK') diff --git a/scripts/guards/no-duplicate-endpoint-entry.mjs b/scripts/guards/no-duplicate-endpoint-entry.mjs new file mode 100644 index 0000000..eedd193 --- /dev/null +++ b/scripts/guards/no-duplicate-endpoint-entry.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node +import fs from 'fs' +import path from 'path' + +const ROOT = process.cwd() +const API_ROOT = path.join(ROOT, 'src', 'app', 'api') + +const KNOWN_DUPLICATE_GROUPS = [ + { + key: 'user-llm-test-connection', + candidates: [ + 'src/app/api/user/api-config/test-connection/route.ts', + 'src/app/api/user/test-llm-provider/route.ts', + ], + }, +] + +const exists = (relPath) => fs.existsSync(path.join(ROOT, relPath)) + +const failures = [] +for (const group of KNOWN_DUPLICATE_GROUPS) { + const present = group.candidates.filter(exists) + if (present.length > 1) { + failures.push({ key: group.key, present }) + } +} + +if (!fs.existsSync(API_ROOT)) { + process.stdout.write('[no-duplicate-endpoint-entry] PASS (api dir missing)\n') + process.exit(0) +} + +if (failures.length === 0) { + process.stdout.write('[no-duplicate-endpoint-entry] PASS\n') + process.exit(0) +} + +process.stderr.write('[no-duplicate-endpoint-entry] FAIL: duplicated endpoint entry detected\n') +for (const failure of failures) { + process.stderr.write(`- ${failure.key}\n`) + for (const relPath of failure.present) { + process.stderr.write(` - ${relPath}\n`) + } +} +process.exit(1) diff --git a/scripts/guards/no-hardcoded-model-capabilities.mjs b/scripts/guards/no-hardcoded-model-capabilities.mjs new file mode 100644 index 0000000..b4c381e --- /dev/null +++ b/scripts/guards/no-hardcoded-model-capabilities.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) +const scanRoots = ['src'] +const allowConstantDefinitionsIn = new Set([ + 'src/lib/constants.ts', +]) +const forbiddenCapabilityConstants = [ + 'VIDEO_MODELS', + 'FIRST_LAST_FRAME_MODELS', + 'AUDIO_SUPPORTED_MODELS', + 'BANANA_MODELS', + 'BANANA_RESOLUTION_OPTIONS', +] + +function fail(title, details = []) { + console.error(`\n[no-hardcoded-model-capabilities] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + if (sourceExtensions.has(path.extname(entry.name))) { + out.push(fullPath) + } + } + return out +} + +const files = scanRoots.flatMap((scanRoot) => walk(path.join(root, scanRoot))) +const violations = [] + +for (const fullPath of files) { + const relPath = toRel(fullPath) + if (allowConstantDefinitionsIn.has(relPath)) continue + + const lines = fs.readFileSync(fullPath, 'utf8').split('\n') + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] + for (const token of forbiddenCapabilityConstants) { + const tokenPattern = new RegExp(`\\b${token}\\b`) + if (tokenPattern.test(line)) { + violations.push(`${relPath}:${index + 1} forbidden hardcoded model capability token ${token}`) + } + } + } +} + +if (violations.length > 0) { + fail('Found hardcoded model capability usage', violations) +} + +console.log('[no-hardcoded-model-capabilities] OK') diff --git a/scripts/guards/no-internal-task-sync-fallback.mjs b/scripts/guards/no-internal-task-sync-fallback.mjs new file mode 100644 index 0000000..2e94ece --- /dev/null +++ b/scripts/guards/no-internal-task-sync-fallback.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const scanRoots = ['src/app/api', 'src/pages/api'] +const allowedPrefixes = ['src/app/api/ui-review/', 'src/pages/api/ui-review/'] +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) + +function fail(title, details = []) { + console.error(`\n[no-internal-task-sync-fallback] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + if (sourceExtensions.has(path.extname(entry.name))) { + out.push(fullPath) + } + } + return out +} + +function isAllowedFile(relPath) { + return allowedPrefixes.some((prefix) => relPath.startsWith(prefix)) +} + +function collectViolations(fullPath) { + const relPath = toRel(fullPath) + if (isAllowedFile(relPath)) return [] + + const content = fs.readFileSync(fullPath, 'utf8') + const lines = content.split('\n') + const violations = [] + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i] + if (/\bisInternalTaskExecution\b/.test(line)) { + violations.push(`${relPath}:${i + 1} forbidden dual-track fallback marker isInternalTaskExecution`) + } + if (/\bshouldRunSyncTask\s*\(/.test(line)) { + violations.push(`${relPath}:${i + 1} forbidden sync-mode branch helper shouldRunSyncTask`) + } + } + + if (/\bmaybeSubmitLLMTask\s*\(/.test(content) && !/sync mode is disabled for this route/.test(content)) { + violations.push(`${relPath} missing explicit sync-disabled guard after maybeSubmitLLMTask`) + } + + return violations +} + +const allFiles = scanRoots.flatMap((scanRoot) => walk(path.join(root, scanRoot))) +const violations = allFiles.flatMap((fullPath) => collectViolations(fullPath)) + +if (violations.length > 0) { + fail('Found potential sync fallback or dual-track task branch in production API routes', violations) +} + +console.log('[no-internal-task-sync-fallback] OK') diff --git a/scripts/guards/no-media-provider-bypass.mjs b/scripts/guards/no-media-provider-bypass.mjs new file mode 100644 index 0000000..7646f38 --- /dev/null +++ b/scripts/guards/no-media-provider-bypass.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) +const allowFactoryImportIn = new Set([ + 'src/lib/generator-api.ts', + 'src/lib/generators/factory.ts', +]) + +function fail(title, details = []) { + console.error(`\n[no-media-provider-bypass] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + if (sourceExtensions.has(path.extname(entry.name))) { + out.push(fullPath) + } + } + return out +} + +const generatorApiPath = path.join(root, 'src/lib/generator-api.ts') +if (!fs.existsSync(generatorApiPath)) { + fail('Missing src/lib/generator-api.ts') +} + +const generatorApiContent = fs.readFileSync(generatorApiPath, 'utf8') +const resolveModelSelectionHits = (generatorApiContent.match(/resolveModelSelection\s*\(/g) || []).length +if (resolveModelSelectionHits < 2) { + fail('generator-api must route both image and video generation through resolveModelSelection', [ + 'expected >= 2 resolveModelSelection(...) calls in src/lib/generator-api.ts', + ]) +} + +const allFiles = walk(path.join(root, 'src')) +const violations = [] + +for (const fullPath of allFiles) { + const relPath = toRel(fullPath) + const content = fs.readFileSync(fullPath, 'utf8') + const lines = content.split('\n') + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i] + + if ( + relPath !== 'src/lib/generators/factory.ts' && + (/\bcreateImageGeneratorByModel\s*\(/.test(line) || /\bcreateVideoGeneratorByModel\s*\(/.test(line)) + ) { + violations.push(`${relPath}:${i + 1} forbidden provider-bypass factory call create*GeneratorByModel(...)`) + } + + if ((/\bgetImageApiKey\s*\(/.test(line) || /\bgetVideoApiKey\s*\(/.test(line)) && relPath !== 'src/lib/api-config.ts') { + violations.push(`${relPath}:${i + 1} forbidden direct getImageApiKey/getVideoApiKey usage outside api-config`) + } + + if (/from\s+['"]@\/lib\/generators\/factory['"]/.test(line) && !allowFactoryImportIn.has(relPath)) { + violations.push(`${relPath}:${i + 1} forbidden direct import from '@/lib/generators/factory' (must go through generator-api)`) + } + } +} + +if (violations.length > 0) { + fail('Found media provider routing bypass', violations) +} + +console.log('[no-media-provider-bypass] OK') diff --git a/scripts/guards/no-model-key-downgrade.mjs b/scripts/guards/no-model-key-downgrade.mjs new file mode 100644 index 0000000..59c8c80 --- /dev/null +++ b/scripts/guards/no-model-key-downgrade.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) +const scanRoots = ['src/app', 'src/lib'] +const modelFields = [ + 'analysisModel', + 'characterModel', + 'locationModel', + 'storyboardModel', + 'editModel', + 'videoModel', +] + +function fail(title, details = []) { + console.error(`\n[no-model-key-downgrade] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + if (sourceExtensions.has(path.extname(entry.name))) { + out.push(fullPath) + } + } + return out +} + +function collectViolations(filePath) { + const relPath = toRel(filePath) + const lines = fs.readFileSync(filePath, 'utf8').split('\n') + const violations = [] + + const modelFieldPattern = new RegExp(`\\b(${modelFields.join('|')})\\s*:\\s*[^,\\n]*\\bmodelId\\b`) + const optionModelIdPattern = /value=\{model\.modelId\}/ + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] + if (modelFieldPattern.test(line)) { + violations.push(`${relPath}:${index + 1} default model field must persist model_key, not modelId`) + } + if (optionModelIdPattern.test(line)) { + violations.push(`${relPath}:${index + 1} UI option value must use modelKey, not model.modelId`) + } + } + + return violations +} + +function assertFileContains(relativePath, requiredSnippets) { + const fullPath = path.join(root, relativePath) + if (!fs.existsSync(fullPath)) { + fail('Missing required contract file', [relativePath]) + } + const content = fs.readFileSync(fullPath, 'utf8') + const missing = requiredSnippets.filter((snippet) => !content.includes(snippet)) + if (missing.length > 0) { + fail('Model key contract anchor missing', missing.map((snippet) => `${relativePath} missing: ${snippet}`)) + } +} + +const files = scanRoots.flatMap((scanRoot) => walk(path.join(root, scanRoot))) +const violations = files.flatMap((filePath) => collectViolations(filePath)) + +assertFileContains('src/lib/model-config-contract.ts', ['parseModelKeyStrict', 'markerIndex === -1) return null']) +assertFileContains('src/lib/config-service.ts', ['parseModelKeyStrict']) +assertFileContains('src/app/api/user/api-config/route.ts', ['validateDefaultModelKey', 'must be provider::modelId']) +assertFileContains('src/app/api/novel-promotion/[projectId]/route.ts', ['must be provider::modelId']) + +if (violations.length > 0) { + fail('Found model key downgrade pattern', violations) +} + +console.log('[no-model-key-downgrade] OK') diff --git a/scripts/guards/no-multiple-sources-of-truth.mjs b/scripts/guards/no-multiple-sources-of-truth.mjs new file mode 100644 index 0000000..9f2e668 --- /dev/null +++ b/scripts/guards/no-multiple-sources-of-truth.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) + +const lineScanRoots = [ + 'src/app/[locale]/workspace/[projectId]/modes/novel-promotion', + 'src/lib/query/hooks', +] + +const fileScanRoots = [ + 'src/app/api/novel-promotion', + 'src/lib/workers/handlers', +] + +const lineRules = [ + { + name: 'shadow state localStoryboards', + test: (line) => /const\s*\[\s*localStoryboards\s*,\s*setLocalStoryboards\s*\]\s*=\s*useState/.test(line), + }, + { + name: 'shadow state localVoiceLines', + test: (line) => /const\s*\[\s*localVoiceLines\s*,\s*setLocalVoiceLines\s*\]\s*=\s*useState/.test(line), + }, + { + name: 'hardcoded queryKey array', + test: (line) => /queryKey\s*:\s*\[/.test(line), + }, +] + +function fail(title, details = []) { + console.error(`\n[no-multiple-sources-of-truth] ${title}`) + for (const detail of details) { + console.error(` - ${detail}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + if (sourceExtensions.has(path.extname(entry.name))) out.push(fullPath) + } + return out +} + +function collectLineViolations(fullPath) { + const relPath = toRel(fullPath) + const content = fs.readFileSync(fullPath, 'utf8') + const lines = content.split('\n') + const violations = [] + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i] + for (const rule of lineRules) { + if (rule.test(line)) { + violations.push(`${relPath}:${i + 1} forbidden: ${rule.name}`) + } + } + } + + return violations +} + +function collectFileViolations(fullPath) { + const relPath = toRel(fullPath) + const content = fs.readFileSync(fullPath, 'utf8') + const violations = [] + + const updateCallRegex = /novelPromotionProject\.update\(\{[\s\S]*?\n\s*\}\)/g + for (const match of content.matchAll(updateCallRegex)) { + const block = match[0] + const hasStageWrite = /\bdata\s*:\s*\{[\s\S]*?\bstage\s*:/.test(block) + if (!hasStageWrite) continue + const before = content.slice(0, match.index ?? 0) + const lineNumber = before.split('\n').length + violations.push(`${relPath}:${lineNumber} forbidden: DB stage write in novelPromotionProject.update`) + } + + return violations +} + +const lineFiles = lineScanRoots.flatMap((scanRoot) => walk(path.join(root, scanRoot))) +const fileFiles = fileScanRoots.flatMap((scanRoot) => walk(path.join(root, scanRoot))) + +const lineViolations = lineFiles.flatMap((fullPath) => collectLineViolations(fullPath)) +const fileViolations = fileFiles.flatMap((fullPath) => collectFileViolations(fullPath)) +const allViolations = [...lineViolations, ...fileViolations] + +if (allViolations.length > 0) { + fail('Found multiple-sources-of-truth regressions', allViolations) +} + +console.log('[no-multiple-sources-of-truth] OK') diff --git a/scripts/guards/no-provider-guessing.mjs b/scripts/guards/no-provider-guessing.mjs new file mode 100644 index 0000000..d78fb32 --- /dev/null +++ b/scripts/guards/no-provider-guessing.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) +const scanRoots = ['src/lib', 'src/app/api'] +const allowModelRegistryUsage = new Set() + +function fail(title, details = []) { + console.error(`\n[no-provider-guessing] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + if (sourceExtensions.has(path.extname(entry.name))) { + out.push(fullPath) + } + } + return out +} + +const apiConfigPath = path.join(root, 'src/lib/api-config.ts') +if (!fs.existsSync(apiConfigPath)) { + fail('Missing src/lib/api-config.ts') +} +const legacyRegistryPath = path.join(root, 'src/lib/model-registry.ts') +if (fs.existsSync(legacyRegistryPath)) { + fail('Legacy runtime registry must be removed', ['src/lib/model-registry.ts']) +} +const apiConfigText = fs.readFileSync(apiConfigPath, 'utf8') + +const forbiddenApiConfigTokens = [ + 'includeAnyType', + 'crossTypeCandidates', + 'matches multiple providers across media types', +] +const apiViolations = forbiddenApiConfigTokens + .filter((token) => apiConfigText.includes(token)) + .map((token) => `src/lib/api-config.ts contains forbidden provider-guessing token: ${token}`) + +// 验证 api-config.ts 使用严格 provider.id 精确匹配(不按 type 过滤,不做 providerKey 模糊匹配) +if (!apiConfigText.includes('pickProviderStrict(')) { + apiViolations.push('src/lib/api-config.ts missing strict provider resolution function (pickProviderStrict)') +} + +const files = scanRoots.flatMap((scanRoot) => walk(path.join(root, scanRoot))) +const violations = [...apiViolations] + +for (const fullPath of files) { + const relPath = toRel(fullPath) + const content = fs.readFileSync(fullPath, 'utf8') + const lines = content.split('\n') + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] + if ( + /from\s+['"]@\/lib\/model-registry['"]/.test(line) + && !allowModelRegistryUsage.has(relPath) + ) { + violations.push(`${relPath}:${index + 1} forbidden model-registry import outside allowed boundary`) + } + + if ( + (/\bgetModelRegistryEntry\s*\(/.test(line) || /\blistRegisteredModels\s*\(/.test(line)) + && !allowModelRegistryUsage.has(relPath) + ) { + violations.push(`${relPath}:${index + 1} forbidden model-registry runtime mapping usage`) + } + } +} + +if (violations.length > 0) { + fail('Found provider guessing / registry mapping violation', violations) +} + +console.log('[no-provider-guessing] OK') diff --git a/scripts/guards/no-server-mirror-state.mjs b/scripts/guards/no-server-mirror-state.mjs new file mode 100644 index 0000000..db7a7c3 --- /dev/null +++ b/scripts/guards/no-server-mirror-state.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const scanRoots = [ + 'src/app/[locale]/workspace/[projectId]/modes/novel-promotion', +] +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) + +const forbiddenRules = [ + { + name: 'localProject/localEpisode mirror state', + test: (line) => /\blocalProject\b|\blocalEpisode\b/.test(line), + }, + { + name: 'server mirror useState(projectData.*)', + test: (line) => /useState\s*\(\s*projectData\./.test(line), + }, + { + name: 'server mirror useState(episode?.*)', + test: (line) => /useState\s*\(\s*episode\?\./.test(line), + }, +] + +function fail(title, details = []) { + console.error(`\n[no-server-mirror-state] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + const ext = path.extname(entry.name) + if (sourceExtensions.has(ext)) out.push(fullPath) + } + return out +} + +function collectViolations(fullPath) { + const relPath = toRel(fullPath) + const content = fs.readFileSync(fullPath, 'utf8') + const lines = content.split('\n') + const violations = [] + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i] + for (const rule of forbiddenRules) { + if (rule.test(line)) { + violations.push(`${relPath}:${i + 1} forbidden: ${rule.name}`) + } + } + } + + return violations +} + +const allFiles = scanRoots.flatMap((scanRoot) => walk(path.join(root, scanRoot))) +const violations = allFiles.flatMap((fullPath) => collectViolations(fullPath)) + +if (violations.length > 0) { + fail('Found forbidden server mirror state patterns', violations) +} + +console.log('[no-server-mirror-state] OK') diff --git a/scripts/guards/prompt-ab-regression.mjs b/scripts/guards/prompt-ab-regression.mjs new file mode 100644 index 0000000..6bcb84b --- /dev/null +++ b/scripts/guards/prompt-ab-regression.mjs @@ -0,0 +1,143 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const catalogPath = path.join(root, 'src', 'lib', 'prompt-i18n', 'catalog.ts') +const singlePlaceholderPattern = /\{([A-Za-z0-9_]+)\}/g +const doublePlaceholderPattern = /\{\{([A-Za-z0-9_]+)\}\}/g +const unresolvedPlaceholderPattern = /\{\{?[A-Za-z0-9_]+\}?\}/g + +function fail(title, details = []) { + console.error(`\n[prompt-ab-regression] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function parseCatalog(text) { + const entries = [] + const entryPattern = /pathStem:\s*'([^']+)'\s*,[\s\S]*?variableKeys:\s*\[([\s\S]*?)\]\s*,/g + for (const match of text.matchAll(entryPattern)) { + const pathStem = match[1] + const rawKeys = match[2] || '' + const keys = Array.from(rawKeys.matchAll(/'([^']+)'/g)).map((item) => item[1]) + entries.push({ pathStem, variableKeys: keys }) + } + return entries +} + +function extractPlaceholders(template) { + const keys = new Set() + for (const match of template.matchAll(singlePlaceholderPattern)) { + if (match[1]) keys.add(match[1]) + } + for (const match of template.matchAll(doublePlaceholderPattern)) { + if (match[1]) keys.add(match[1]) + } + return Array.from(keys) +} + +function replaceAll(template, variables) { + let rendered = template + for (const [key, value] of Object.entries(variables)) { + const pattern = new RegExp(`\\{\\{${key}\\}\\}|\\{${key}\\}`, 'g') + rendered = rendered.replace(pattern, value) + } + return rendered +} + +function setDiff(left, right) { + const rightSet = new Set(right) + return left.filter((item) => !rightSet.has(item)) +} + +if (!fs.existsSync(catalogPath)) { + fail('catalog.ts not found', ['src/lib/prompt-i18n/catalog.ts']) +} + +const catalogText = fs.readFileSync(catalogPath, 'utf8') +const entries = parseCatalog(catalogText) +if (entries.length === 0) { + fail('failed to parse prompt catalog entries') +} + +const violations = [] + +for (const entry of entries) { + const zhPath = path.join(root, 'lib', 'prompts', `${entry.pathStem}.zh.txt`) + const enPath = path.join(root, 'lib', 'prompts', `${entry.pathStem}.en.txt`) + if (!fs.existsSync(zhPath)) { + violations.push(`missing zh template: lib/prompts/${entry.pathStem}.zh.txt`) + continue + } + if (!fs.existsSync(enPath)) { + violations.push(`missing en template: lib/prompts/${entry.pathStem}.en.txt`) + continue + } + + const zhTemplate = fs.readFileSync(zhPath, 'utf8') + const enTemplate = fs.readFileSync(enPath, 'utf8') + const declared = entry.variableKeys + const zhPlaceholders = extractPlaceholders(zhTemplate) + const enPlaceholders = extractPlaceholders(enTemplate) + + const missingInZh = setDiff(declared, zhPlaceholders) + const missingInEn = setDiff(declared, enPlaceholders) + const extraInZh = setDiff(zhPlaceholders, declared) + const extraInEn = setDiff(enPlaceholders, declared) + const zhOnly = setDiff(zhPlaceholders, enPlaceholders) + const enOnly = setDiff(enPlaceholders, zhPlaceholders) + + for (const key of missingInZh) { + violations.push(`missing {${key}} in zh template: lib/prompts/${entry.pathStem}.zh.txt`) + } + for (const key of missingInEn) { + violations.push(`missing {${key}} in en template: lib/prompts/${entry.pathStem}.en.txt`) + } + for (const key of extraInZh) { + violations.push(`unexpected {${key}} in zh template: lib/prompts/${entry.pathStem}.zh.txt`) + } + for (const key of extraInEn) { + violations.push(`unexpected {${key}} in en template: lib/prompts/${entry.pathStem}.en.txt`) + } + for (const key of zhOnly) { + violations.push(`placeholder {${key}} exists only in zh template: ${entry.pathStem}`) + } + for (const key of enOnly) { + violations.push(`placeholder {${key}} exists only in en template: ${entry.pathStem}`) + } + + const variables = Object.fromEntries( + declared.map((key) => [key, `__AB_SAMPLE_${key.toUpperCase()}__`]), + ) + const renderedZh = replaceAll(zhTemplate, variables) + const renderedEn = replaceAll(enTemplate, variables) + + const unresolvedZh = renderedZh.match(unresolvedPlaceholderPattern) || [] + const unresolvedEn = renderedEn.match(unresolvedPlaceholderPattern) || [] + if (unresolvedZh.length > 0) { + violations.push(`unresolved placeholders in zh template: ${entry.pathStem} -> ${unresolvedZh.join(', ')}`) + } + if (unresolvedEn.length > 0) { + violations.push(`unresolved placeholders in en template: ${entry.pathStem} -> ${unresolvedEn.join(', ')}`) + } + + for (const [key, sample] of Object.entries(variables)) { + if (!renderedZh.includes(sample)) { + violations.push(`zh template variable not used after render: ${entry.pathStem}.{${key}}`) + } + if (!renderedEn.includes(sample)) { + violations.push(`en template variable not used after render: ${entry.pathStem}.{${key}}`) + } + } +} + +if (violations.length > 0) { + fail('A/B regression check failed', violations) +} + +console.log(`[prompt-ab-regression] OK (${entries.length} templates checked)`) diff --git a/scripts/guards/prompt-i18n-guard.mjs b/scripts/guards/prompt-i18n-guard.mjs new file mode 100644 index 0000000..ac0af4f --- /dev/null +++ b/scripts/guards/prompt-i18n-guard.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) +const scanRoots = ['src', 'scripts'] +const allowedPromptTemplateReaders = new Set([ + 'src/lib/prompt-i18n/template-store.ts', + 'scripts/guards/prompt-i18n-guard.mjs', + 'scripts/guards/prompt-semantic-regression.mjs', + 'scripts/guards/prompt-ab-regression.mjs', + 'scripts/guards/prompt-json-canary-guard.mjs', +]) +const languageDirectiveAllowList = new Set([ + 'scripts/guards/prompt-i18n-guard.mjs', +]) +const languageDirectivePattern = /请用中文|中文输出|use Chinese|output in Chinese/i + +function fail(title, details = []) { + console.error(`\n[prompt-i18n-guard] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + out.push(fullPath) + } + return out +} + +function listSourceFiles() { + return scanRoots + .flatMap((scanRoot) => walk(path.join(root, scanRoot))) + .filter((fullPath) => sourceExtensions.has(path.extname(fullPath))) +} + +function collectDirectPromptReadViolations() { + const violations = [] + const files = listSourceFiles() + for (const filePath of files) { + const relPath = toRel(filePath) + if (allowedPromptTemplateReaders.has(relPath)) continue + const content = fs.readFileSync(filePath, 'utf8') + const hasReadFileSync = /\breadFileSync\s*\(/.test(content) + if (!hasReadFileSync) continue + const hasPromptPathToken = + content.includes('lib/prompts') + || ( + /['"]lib['"]/.test(content) + && /['"]prompts['"]/.test(content) + ) + if (hasPromptPathToken) { + violations.push(`${relPath} direct prompt file read is forbidden; use buildPrompt/getPromptTemplate`) + } + } + return violations +} + +function collectLanguageDirectiveViolations() { + const violations = [] + + for (const filePath of listSourceFiles()) { + const relPath = toRel(filePath) + if (languageDirectiveAllowList.has(relPath)) continue + const lines = fs.readFileSync(filePath, 'utf8').split('\n') + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] + if (languageDirectivePattern.test(line)) { + violations.push(`${relPath}:${index + 1} hardcoded language directive is forbidden`) + } + } + } + + const promptFiles = walk(path.join(root, 'lib', 'prompts')) + .filter((fullPath) => fullPath.endsWith('.en.txt')) + for (const filePath of promptFiles) { + const relPath = toRel(filePath) + const lines = fs.readFileSync(filePath, 'utf8').split('\n') + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] + if (languageDirectivePattern.test(line)) { + violations.push(`${relPath}:${index + 1} English template cannot require Chinese output`) + } + } + } + + return violations +} + +function collectLegacyPromptFiles() { + return walk(path.join(root, 'lib', 'prompts')) + .map((fullPath) => toRel(fullPath)) + .filter((relPath) => relPath.endsWith('.txt') && !relPath.endsWith('.zh.txt') && !relPath.endsWith('.en.txt')) +} + +function verifyPromptCatalogCoverage() { + const catalogPath = path.join(root, 'src', 'lib', 'prompt-i18n', 'catalog.ts') + if (!fs.existsSync(catalogPath)) { + fail('Missing prompt catalog file', ['src/lib/prompt-i18n/catalog.ts']) + } + + const catalogText = fs.readFileSync(catalogPath, 'utf8') + const stems = Array.from(catalogText.matchAll(/pathStem:\s*'([^']+)'/g)).map((match) => match[1]) + if (stems.length === 0) { + fail('No prompt pathStem found in catalog.ts') + } + + const missing = [] + for (const stem of stems) { + const zhPath = path.join(root, 'lib', 'prompts', `${stem}.zh.txt`) + const enPath = path.join(root, 'lib', 'prompts', `${stem}.en.txt`) + if (!fs.existsSync(zhPath)) { + missing.push(`missing zh template: lib/prompts/${stem}.zh.txt`) + } + if (!fs.existsSync(enPath)) { + missing.push(`missing en template: lib/prompts/${stem}.en.txt`) + } + } + + if (missing.length > 0) { + fail('Prompt template coverage check failed', missing) + } +} + +const legacyPromptFiles = collectLegacyPromptFiles() +if (legacyPromptFiles.length > 0) { + fail('Legacy prompt files found (.txt without locale suffix)', legacyPromptFiles) +} + +verifyPromptCatalogCoverage() + +const promptReadViolations = collectDirectPromptReadViolations() +if (promptReadViolations.length > 0) { + fail('Found direct prompt template reads', promptReadViolations) +} + +const languageViolations = collectLanguageDirectiveViolations() +if (languageViolations.length > 0) { + fail('Found hardcoded language directives', languageViolations) +} + +console.log('[prompt-i18n-guard] OK') diff --git a/scripts/guards/prompt-json-canary-guard.mjs b/scripts/guards/prompt-json-canary-guard.mjs new file mode 100644 index 0000000..8088e14 --- /dev/null +++ b/scripts/guards/prompt-json-canary-guard.mjs @@ -0,0 +1,250 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() + +const CANARY_FILES = { + clips: 'standards/prompt-canary/story_to_script_clips.canary.json', + screenplay: 'standards/prompt-canary/screenplay_conversion.canary.json', + storyboardPanels: 'standards/prompt-canary/storyboard_panels.canary.json', + voiceAnalysis: 'standards/prompt-canary/voice_analysis.canary.json', +} + +const TEMPLATE_TOKEN_REQUIREMENTS = { + 'novel-promotion/agent_clip': ['start', 'end', 'summary', 'location', 'characters'], + 'novel-promotion/screenplay_conversion': [ + 'clip_id', + 'original_text', + 'scenes', + 'heading', + 'content', + 'type', + 'action', + 'dialogue', + 'voiceover', + ], + 'novel-promotion/agent_storyboard_plan': [ + 'panel_number', + 'description', + 'characters', + 'location', + 'scene_type', + 'source_text', + ], + 'novel-promotion/agent_storyboard_detail': [ + 'panel_number', + 'description', + 'characters', + 'location', + 'scene_type', + 'source_text', + 'shot_type', + 'camera_move', + 'video_prompt', + ], + 'novel-promotion/agent_storyboard_insert': [ + 'panel_number', + 'description', + 'characters', + 'location', + 'scene_type', + 'source_text', + 'shot_type', + 'camera_move', + 'video_prompt', + ], + 'novel-promotion/voice_analysis': [ + 'lineIndex', + 'speaker', + 'content', + 'emotionStrength', + 'matchedPanel', + 'storyboardId', + 'panelIndex', + ], +} + +function fail(title, details = []) { + console.error(`\n[prompt-json-canary-guard] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function isRecord(value) { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function isString(value) { + return typeof value === 'string' +} + +function isNumber(value) { + return typeof value === 'number' && Number.isFinite(value) +} + +function readJson(relativePath) { + const fullPath = path.join(root, relativePath) + if (!fs.existsSync(fullPath)) { + fail('Missing canary fixture', [relativePath]) + } + try { + return JSON.parse(fs.readFileSync(fullPath, 'utf8')) + } catch (error) { + fail('Invalid canary fixture JSON', [`${relativePath}: ${error instanceof Error ? error.message : String(error)}`]) + } +} + +function validateClipCanary(value) { + if (!Array.isArray(value) || value.length === 0) return 'clips fixture must be a non-empty array' + for (let i = 0; i < value.length; i += 1) { + const row = value[i] + if (!isRecord(row)) return `clips[${i}] must be an object` + if (!isString(row.start) || row.start.length < 5) return `clips[${i}].start must be string length >= 5` + if (!isString(row.end) || row.end.length < 5) return `clips[${i}].end must be string length >= 5` + if (!isString(row.summary) || row.summary.length === 0) return `clips[${i}].summary must be non-empty string` + if (!(row.location === null || isString(row.location))) return `clips[${i}].location must be string or null` + if (!Array.isArray(row.characters) || !row.characters.every((item) => isString(item))) { + return `clips[${i}].characters must be string array` + } + } + return null +} + +function validateScreenplayCanary(value) { + if (!isRecord(value)) return 'screenplay fixture must be an object' + if (!isString(value.clip_id) || !value.clip_id) return 'screenplay.clip_id must be non-empty string' + if (!isString(value.original_text)) return 'screenplay.original_text must be string' + if (!Array.isArray(value.scenes) || value.scenes.length === 0) return 'screenplay.scenes must be non-empty array' + + for (let i = 0; i < value.scenes.length; i += 1) { + const scene = value.scenes[i] + if (!isRecord(scene)) return `screenplay.scenes[${i}] must be object` + if (!isNumber(scene.scene_number)) return `screenplay.scenes[${i}].scene_number must be number` + if (!isRecord(scene.heading)) return `screenplay.scenes[${i}].heading must be object` + if (!isString(scene.heading.int_ext)) return `screenplay.scenes[${i}].heading.int_ext must be string` + if (!isString(scene.heading.location)) return `screenplay.scenes[${i}].heading.location must be string` + if (!isString(scene.heading.time)) return `screenplay.scenes[${i}].heading.time must be string` + if (!isString(scene.description)) return `screenplay.scenes[${i}].description must be string` + if (!Array.isArray(scene.characters) || !scene.characters.every((item) => isString(item))) { + return `screenplay.scenes[${i}].characters must be string array` + } + if (!Array.isArray(scene.content) || scene.content.length === 0) return `screenplay.scenes[${i}].content must be non-empty array` + + for (let j = 0; j < scene.content.length; j += 1) { + const segment = scene.content[j] + if (!isRecord(segment)) return `screenplay.scenes[${i}].content[${j}] must be object` + if (!isString(segment.type)) return `screenplay.scenes[${i}].content[${j}].type must be string` + if (segment.type === 'action') { + if (!isString(segment.text)) return `screenplay action[${i}:${j}].text must be string` + } else if (segment.type === 'dialogue') { + if (!isString(segment.character)) return `screenplay dialogue[${i}:${j}].character must be string` + if (!isString(segment.lines)) return `screenplay dialogue[${i}:${j}].lines must be string` + if (segment.parenthetical !== undefined && !isString(segment.parenthetical)) { + return `screenplay dialogue[${i}:${j}].parenthetical must be string when present` + } + } else if (segment.type === 'voiceover') { + if (!isString(segment.text)) return `screenplay voiceover[${i}:${j}].text must be string` + if (segment.character !== undefined && !isString(segment.character)) { + return `screenplay voiceover[${i}:${j}].character must be string when present` + } + } else { + return `screenplay.scenes[${i}].content[${j}].type must be action/dialogue/voiceover` + } + } + } + + return null +} + +function validateStoryboardPanelsCanary(value) { + if (!Array.isArray(value) || value.length === 0) return 'storyboard panels fixture must be non-empty array' + for (let i = 0; i < value.length; i += 1) { + const panel = value[i] + if (!isRecord(panel)) return `storyboardPanels[${i}] must be object` + if (!isNumber(panel.panel_number)) return `storyboardPanels[${i}].panel_number must be number` + if (!isString(panel.description)) return `storyboardPanels[${i}].description must be string` + if (!isString(panel.location)) return `storyboardPanels[${i}].location must be string` + if (!isString(panel.scene_type)) return `storyboardPanels[${i}].scene_type must be string` + if (!isString(panel.source_text)) return `storyboardPanels[${i}].source_text must be string` + if (!isString(panel.shot_type)) return `storyboardPanels[${i}].shot_type must be string` + if (!isString(panel.camera_move)) return `storyboardPanels[${i}].camera_move must be string` + if (!isString(panel.video_prompt)) return `storyboardPanels[${i}].video_prompt must be string` + if (panel.duration !== undefined && !isNumber(panel.duration)) return `storyboardPanels[${i}].duration must be number when present` + if (!Array.isArray(panel.characters)) return `storyboardPanels[${i}].characters must be array` + for (let j = 0; j < panel.characters.length; j += 1) { + const character = panel.characters[j] + if (!isRecord(character)) return `storyboardPanels[${i}].characters[${j}] must be object` + if (!isString(character.name)) return `storyboardPanels[${i}].characters[${j}].name must be string` + if (character.appearance !== undefined && !isString(character.appearance)) { + return `storyboardPanels[${i}].characters[${j}].appearance must be string when present` + } + } + } + return null +} + +function validateVoiceAnalysisCanary(value) { + if (!Array.isArray(value) || value.length === 0) return 'voice analysis fixture must be non-empty array' + for (let i = 0; i < value.length; i += 1) { + const row = value[i] + if (!isRecord(row)) return `voiceAnalysis[${i}] must be object` + if (!isNumber(row.lineIndex)) return `voiceAnalysis[${i}].lineIndex must be number` + if (!isString(row.speaker)) return `voiceAnalysis[${i}].speaker must be string` + if (!isString(row.content)) return `voiceAnalysis[${i}].content must be string` + if (!isNumber(row.emotionStrength)) return `voiceAnalysis[${i}].emotionStrength must be number` + if (row.matchedPanel !== null) { + if (!isRecord(row.matchedPanel)) return `voiceAnalysis[${i}].matchedPanel must be object or null` + if (!isString(row.matchedPanel.storyboardId)) return `voiceAnalysis[${i}].matchedPanel.storyboardId must be string` + if (!isNumber(row.matchedPanel.panelIndex)) return `voiceAnalysis[${i}].matchedPanel.panelIndex must be number` + } + } + return null +} + +function checkTemplateTokens(pathStem, requiredTokens) { + const violations = [] + for (const locale of ['zh', 'en']) { + const relPath = `lib/prompts/${pathStem}.${locale}.txt` + const fullPath = path.join(root, relPath) + if (!fs.existsSync(fullPath)) { + violations.push(`missing template: ${relPath}`) + continue + } + const content = fs.readFileSync(fullPath, 'utf8') + for (const token of requiredTokens) { + if (!content.includes(token)) { + violations.push(`missing token ${token} in ${relPath}`) + } + } + } + return violations +} + +const violations = [] + +const clipsErr = validateClipCanary(readJson(CANARY_FILES.clips)) +if (clipsErr) violations.push(clipsErr) + +const screenplayErr = validateScreenplayCanary(readJson(CANARY_FILES.screenplay)) +if (screenplayErr) violations.push(screenplayErr) + +const panelsErr = validateStoryboardPanelsCanary(readJson(CANARY_FILES.storyboardPanels)) +if (panelsErr) violations.push(panelsErr) + +const voiceErr = validateVoiceAnalysisCanary(readJson(CANARY_FILES.voiceAnalysis)) +if (voiceErr) violations.push(voiceErr) + +for (const [pathStem, requiredTokens] of Object.entries(TEMPLATE_TOKEN_REQUIREMENTS)) { + violations.push(...checkTemplateTokens(pathStem, requiredTokens)) +} + +if (violations.length > 0) { + fail('JSON schema canary check failed', violations) +} + +console.log('[prompt-json-canary-guard] OK') diff --git a/scripts/guards/prompt-semantic-regression.mjs b/scripts/guards/prompt-semantic-regression.mjs new file mode 100644 index 0000000..afa62b2 --- /dev/null +++ b/scripts/guards/prompt-semantic-regression.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() +const catalogPath = path.join(root, 'src', 'lib', 'prompt-i18n', 'catalog.ts') +const chineseCharPattern = /[\p{Script=Han}]/u +const singlePlaceholderPattern = /\{([A-Za-z0-9_]+)\}/g +const doublePlaceholderPattern = /\{\{([A-Za-z0-9_]+)\}\}/g + +const criticalTemplateTokens = new Map([ + ['novel-promotion/voice_analysis', ['"lineIndex"', '"speaker"', '"content"', '"emotionStrength"', '"matchedPanel"']], + ['novel-promotion/agent_storyboard_plan', ['"panel_number"', '"description"', '"characters"', '"location"', '"scene_type"', '"source_text"', '"shot_type"', '"camera_move"', '"video_prompt"']], + ['novel-promotion/agent_storyboard_detail', ['"panel_number"', '"description"', '"characters"', '"location"', '"scene_type"', '"source_text"', '"shot_type"', '"camera_move"', '"video_prompt"']], + ['novel-promotion/agent_storyboard_insert', ['"panel_number"', '"description"', '"characters"', '"location"', '"scene_type"', '"source_text"', '"shot_type"', '"camera_move"', '"video_prompt"']], + ['novel-promotion/screenplay_conversion', ['"clip_id"', '"scenes"', '"heading"', '"content"', '"dialogue"', '"voiceover"']], + ['novel-promotion/select_location', ['"locations"', '"name"', '"summary"', '"descriptions"']], + ['novel-promotion/episode_split', ['"analysis"', '"episodes"', '"startMarker"', '"endMarker"', '"validation"']], + ['novel-promotion/image_prompt_modify', ['"image_prompt"', '"video_prompt"']], + ['novel-promotion/character_create', ['"prompt"']], + ['novel-promotion/location_create', ['"prompt"']], +]) + +function fail(title, details = []) { + console.error(`\n[prompt-semantic-regression] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function parseCatalog(text) { + const entries = [] + const entryPattern = /pathStem:\s*'([^']+)'\s*,[\s\S]*?variableKeys:\s*\[([\s\S]*?)\]\s*,/g + for (const match of text.matchAll(entryPattern)) { + const pathStem = match[1] + const rawKeys = match[2] || '' + const keys = Array.from(rawKeys.matchAll(/'([^']+)'/g)).map((item) => item[1]) + entries.push({ pathStem, variableKeys: keys }) + } + return entries +} + +function extractPlaceholders(template) { + const keys = new Set() + for (const match of template.matchAll(singlePlaceholderPattern)) { + if (match[1]) keys.add(match[1]) + } + for (const match of template.matchAll(doublePlaceholderPattern)) { + if (match[1]) keys.add(match[1]) + } + return Array.from(keys) +} + +if (!fs.existsSync(catalogPath)) { + fail('catalog.ts not found', ['src/lib/prompt-i18n/catalog.ts']) +} + +const catalogText = fs.readFileSync(catalogPath, 'utf8') +const entries = parseCatalog(catalogText) +if (entries.length === 0) { + fail('failed to parse prompt catalog entries') +} + +const violations = [] +for (const entry of entries) { + const templatePath = path.join(root, 'lib', 'prompts', `${entry.pathStem}.en.txt`) + if (!fs.existsSync(templatePath)) { + violations.push(`missing template: lib/prompts/${entry.pathStem}.en.txt`) + continue + } + + const template = fs.readFileSync(templatePath, 'utf8') + if (chineseCharPattern.test(template)) { + violations.push(`unexpected Chinese content in English template: lib/prompts/${entry.pathStem}.en.txt`) + } + + const placeholders = extractPlaceholders(template) + const placeholderSet = new Set(placeholders) + const variableKeySet = new Set(entry.variableKeys) + + for (const key of entry.variableKeys) { + if (!placeholderSet.has(key)) { + violations.push(`missing placeholder {${key}} in lib/prompts/${entry.pathStem}.en.txt`) + } + } + + for (const key of placeholders) { + if (!variableKeySet.has(key)) { + violations.push(`unexpected placeholder {${key}} in lib/prompts/${entry.pathStem}.en.txt`) + } + } + + const requiredTokens = criticalTemplateTokens.get(entry.pathStem) || [] + for (const token of requiredTokens) { + if (!template.includes(token)) { + violations.push(`missing semantic token ${token} in lib/prompts/${entry.pathStem}.en.txt`) + } + } +} + +if (violations.length > 0) { + fail('semantic regression check failed', violations) +} + +console.log(`[prompt-semantic-regression] OK (${entries.length} templates checked)`) diff --git a/scripts/guards/task-loading-baseline.json b/scripts/guards/task-loading-baseline.json new file mode 100644 index 0000000..1c7686b --- /dev/null +++ b/scripts/guards/task-loading-baseline.json @@ -0,0 +1,9 @@ +{ + "allowedDirectTaskStateUsageFiles": [ + "src/lib/query/hooks/useTaskTargetStates.ts", + "src/lib/query/hooks/useTaskPresentation.ts", + "src/lib/query/hooks/useProjectAssets.ts", + "src/lib/query/hooks/useGlobalAssets.ts" + ], + "allowedLegacyGeneratingUsageFiles": [] +} diff --git a/scripts/guards/task-loading-guard.mjs b/scripts/guards/task-loading-guard.mjs new file mode 100644 index 0000000..f60aba6 --- /dev/null +++ b/scripts/guards/task-loading-guard.mjs @@ -0,0 +1,132 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const workspaceRoot = process.cwd() +const baselinePath = path.join(workspaceRoot, 'scripts/guards/task-loading-baseline.json') + +function walkFiles(dir, out = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walkFiles(fullPath, out) + } else { + out.push(fullPath) + } + } + return out +} + +function toPosixRelative(filePath) { + return path.relative(workspaceRoot, filePath).split(path.sep).join('/') +} + +function collectMatches(files, pattern) { + const matches = [] + for (const fullPath of files) { + if (!fullPath.endsWith('.ts') && !fullPath.endsWith('.tsx')) continue + const relPath = toPosixRelative(fullPath) + const content = fs.readFileSync(fullPath, 'utf8') + const lines = content.split('\n') + for (let i = 0; i < lines.length; i += 1) { + if (lines[i].includes(pattern)) { + matches.push(`${relPath}:${i + 1}`) + } + } + } + return matches +} + +function fail(title, lines) { + console.error(`\n[task-loading-guard] ${title}`) + for (const line of lines) { + console.error(` - ${line}`) + } + process.exit(1) +} + +if (!fs.existsSync(baselinePath)) { + fail('Missing baseline file', [toPosixRelative(baselinePath)]) +} + +const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')) +const allowedFiles = new Set(baseline.allowedDirectTaskStateUsageFiles || []) +const allowedLegacyGeneratingFiles = new Set(baseline.allowedLegacyGeneratingUsageFiles || []) +const allFiles = walkFiles(path.join(workspaceRoot, 'src')) + +const directTaskStateUsage = collectMatches(allFiles, 'useTaskTargetStates(') +const directUsageOutOfAllowlist = directTaskStateUsage + .map((entry) => entry.split(':')[0]) + .filter((file) => !allowedFiles.has(file)) + +if (directUsageOutOfAllowlist.length > 0) { + fail( + 'Found component-level direct useTaskTargetStates outside baseline allowlist', + Array.from(new Set(directUsageOutOfAllowlist)), + ) +} + +const crossDomainLabels = collectMatches(allFiles, 'video.panelCard.generating') +if (crossDomainLabels.length > 0) { + fail('Found cross-domain loading label reuse (video.panelCard.generating)', crossDomainLabels) +} + +const uiFiles = allFiles.filter((file) => { + const relPath = toPosixRelative(file) + return relPath.startsWith('src/app/') || relPath.startsWith('src/components/') +}) +const legacyGeneratingPatterns = [ + 'appearance.generating', + 'panel.generatingImage', + 'shot.generatingImage', + 'line.generating', +] +const legacyGeneratingMatches = legacyGeneratingPatterns.flatMap((pattern) => + collectMatches(uiFiles, pattern), +) +const legacyGeneratingOutOfAllowlist = legacyGeneratingMatches + .map((entry) => entry.split(':')[0]) + .filter((file) => !allowedLegacyGeneratingFiles.has(file)) +if (legacyGeneratingOutOfAllowlist.length > 0) { + fail( + 'Found legacy generating truth usage in UI components', + Array.from(new Set(legacyGeneratingOutOfAllowlist)), + ) +} + +const hooksIndexPath = path.join(workspaceRoot, 'src/lib/query/hooks/index.ts') +if (fs.existsSync(hooksIndexPath)) { + const hooksIndex = fs.readFileSync(hooksIndexPath, 'utf8') + const bannedReexports = [ + { + pattern: /export\s*\{[^}]*useGenerateCharacterImage[^}]*\}\s*from\s*['"]\.\/useGlobalAssets['"]/m, + message: 'hooks/index.ts must not export useGenerateCharacterImage from useGlobalAssets', + }, + { + pattern: /export\s*\{[^}]*useGenerateLocationImage[^}]*\}\s*from\s*['"]\.\/useGlobalAssets['"]/m, + message: 'hooks/index.ts must not export useGenerateLocationImage from useGlobalAssets', + }, + { + pattern: /export\s*\{[^}]*useGenerateProjectCharacterImage[^}]*\}\s*from\s*['"]\.\/useProjectAssets['"]/m, + message: 'hooks/index.ts must not export useGenerateProjectCharacterImage from useProjectAssets', + }, + { + pattern: /export\s*\{[^}]*useGenerateProjectLocationImage[^}]*\}\s*from\s*['"]\.\/useProjectAssets['"]/m, + message: 'hooks/index.ts must not export useGenerateProjectLocationImage from useProjectAssets', + }, + ] + + const violations = bannedReexports + .filter((item) => item.pattern.test(hooksIndex)) + .map((item) => item.message) + + if (violations.length > 0) { + fail('Found non-canonical mutation re-exports', violations) + } +} + +console.log('[task-loading-guard] OK') diff --git a/scripts/guards/task-state-unification-guard.sh b/scripts/guards/task-state-unification-guard.sh new file mode 100644 index 0000000..d8ff8f1 --- /dev/null +++ b/scripts/guards/task-state-unification-guard.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +failed=0 + +check_absent() { + local label="$1" + local pattern="$2" + shift 2 + local output + output="$(git grep --untracked -nE "$pattern" -- "$@" || true)" + if [[ -n "$output" ]]; then + echo "$output" + echo "::error title=${label}::${label}" + failed=1 + fi +} + +check_absent \ + "Do not branch UI status on cancelled" \ + "status[[:space:]]*===[[:space:]]*['\\\"]cancelled['\\\"]|status[[:space:]]*==[[:space:]]*['\\\"]cancelled['\\\"]" \ + src/app \ + src/components \ + src/features \ + src/lib/query + +check_absent \ + "useTaskHandoff is forbidden" \ + "useTaskHandoff" \ + src + +check_absent \ + "Do not use legacy task hooks in app layer" \ + "useActiveTasks\\(|useTaskStatus\\(" \ + src/app \ + src/features + +if [[ "$failed" -ne 0 ]]; then + exit 1 +fi + +echo "task-state-unification guard passed" diff --git a/scripts/guards/task-status-cutover-audit.sh b/scripts/guards/task-status-cutover-audit.sh new file mode 100644 index 0000000..9766c95 --- /dev/null +++ b/scripts/guards/task-status-cutover-audit.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(git rev-parse --show-toplevel)" +cd "$ROOT_DIR" + +FAILED=0 + +print_header() { + echo + echo "============================================================" + echo "$1" + echo "============================================================" +} + +print_ok() { + echo "[PASS] $1" +} + +print_fail() { + echo "[FAIL] $1" +} + +run_zero_match_check() { + local title="$1" + local pattern="$2" + shift 2 + local paths=("$@") + local output + output="$(git grep -n -E "$pattern" -- "${paths[@]}" || true)" + if [[ -z "$output" ]]; then + print_ok "$title" + else + print_fail "$title" + echo "$output" + FAILED=1 + fi +} + +run_usetasktargetstates_check() { + local title="useTaskTargetStates 仅允许在 useProjectAssets/useGlobalAssets 中使用" + local output + output="$(git grep -n "useTaskTargetStates" -- src || true)" + + if [[ -z "$output" ]]; then + print_ok "$title (当前 0 命中)" + return + fi + + local filtered + filtered="$(echo "$output" | grep -v "src/lib/query/hooks/useProjectAssets.ts" | grep -v "src/lib/query/hooks/useGlobalAssets.ts" || true)" + + if [[ -z "$filtered" ]]; then + print_ok "$title" + else + print_fail "$title" + echo "$filtered" + FAILED=1 + fi +} + +print_header "Task Status Cutover Audit" + +run_zero_match_check \ + "禁止 useTaskHandoff" \ + "useTaskHandoff" \ + src + +run_zero_match_check \ + "禁止 manualRegeneratingItems/setRegeneratingItems/clearRegeneratingItem" \ + "manualRegeneratingItems|setRegeneratingItems|clearRegeneratingItem" \ + src + +run_zero_match_check \ + "禁止业务层直接判断 status ===/!== cancelled" \ + "status\\s*===\\s*['\\\"]cancelled['\\\"]|status\\s*!==\\s*['\\\"]cancelled['\\\"]" \ + src + +run_zero_match_check \ + "禁止 generatingImage/generatingVideo/generatingLipSync 字段" \ + "\\bgeneratingImage\\b|\\bgeneratingVideo\\b|\\bgeneratingLipSync\\b" \ + src + +run_usetasktargetstates_check + +run_zero_match_check \ + "禁止 novel-promotion/asset-hub/shared-assets 中 useState(false) 作为生成态命名" \ + "const \\[[^\\]]*(Generating|Regenerating|WaitingForGeneration|AnalyzingAssets|GeneratingAll|CopyingFromGlobal)[^\\]]*\\]\\s*=\\s*useState\\(false\\)" \ + "src/app/[locale]/workspace/[projectId]/modes/novel-promotion" \ + "src/app/[locale]/workspace/asset-hub" \ + "src/components/shared/assets" + +print_header "Audit Result" +if [[ "$FAILED" -eq 0 ]]; then + echo "All checks passed." + exit 0 +fi + +echo "Audit failed. Please fix findings above." +exit 1 diff --git a/scripts/guards/task-target-states-no-polling-guard.mjs b/scripts/guards/task-target-states-no-polling-guard.mjs new file mode 100644 index 0000000..40042af --- /dev/null +++ b/scripts/guards/task-target-states-no-polling-guard.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' +import process from 'process' + +const root = process.cwd() + +function fail(title, details = []) { + console.error(`\n[task-target-states-no-polling-guard] ${title}`) + for (const line of details) { + console.error(` - ${line}`) + } + process.exit(1) +} + +function readFile(relativePath) { + const fullPath = path.join(root, relativePath) + if (!fs.existsSync(fullPath)) { + fail('Missing required file', [relativePath]) + } + return fs.readFileSync(fullPath, 'utf8') +} + +function walk(dir, out = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(full, out) + } else { + out.push(full) + } + } + return out +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +function collectPattern(pattern) { + const files = walk(path.join(root, 'src')) + const hits = [] + for (const fullPath of files) { + if (!fullPath.endsWith('.ts') && !fullPath.endsWith('.tsx')) continue + const text = fs.readFileSync(fullPath, 'utf8') + const lines = text.split('\n') + for (let i = 0; i < lines.length; i += 1) { + if (pattern.test(lines[i])) { + hits.push(`${toRel(fullPath)}:${i + 1}`) + } + } + } + return hits +} + +const refetchIntervalMsHits = collectPattern(/\brefetchIntervalMs\b/) +if (refetchIntervalMsHits.length > 0) { + fail('Found forbidden refetchIntervalMs usage', refetchIntervalMsHits) +} + +const voiceStagePath = + 'src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/VoiceStage.tsx' +const voiceStageText = readFile(voiceStagePath) +if (voiceStageText.includes('setInterval(')) { + fail('VoiceStage must not use timer polling', [voiceStagePath]) +} + +const targetStateMapPath = 'src/lib/query/hooks/useTaskTargetStateMap.ts' +const targetStateMapText = readFile(targetStateMapPath) +if (!/refetchInterval:\s*false/.test(targetStateMapText)) { + fail('useTaskTargetStateMap must keep refetchInterval disabled', [targetStateMapPath]) +} + +const ssePath = 'src/lib/query/hooks/useSSE.ts' +const sseText = readFile(ssePath) +const targetStatesInvalidateExprMatch = sseText.match( + /const shouldInvalidateTargetStates\s*=\s*([\s\S]*?)\n\s*\n/, +) +if (!targetStatesInvalidateExprMatch) { + fail('Unable to locate shouldInvalidateTargetStates expression', [ssePath]) +} +const targetStatesInvalidateExpr = targetStatesInvalidateExprMatch[1] +if (!/TASK_EVENT_TYPE\.COMPLETED/.test(targetStatesInvalidateExpr) || !/TASK_EVENT_TYPE\.FAILED/.test(targetStatesInvalidateExpr)) { + fail('useSSE must invalidate target states only for terminal events', [ssePath]) +} +if (/TASK_EVENT_TYPE\.CREATED/.test(targetStatesInvalidateExpr)) { + fail('useSSE target-state invalidation must not include CREATED', [ssePath]) +} +if (/TASK_EVENT_TYPE\.PROCESSING/.test(targetStatesInvalidateExpr)) { + fail('useSSE target-state invalidation must not include PROCESSING', [ssePath]) +} + +console.log('[task-target-states-no-polling-guard] OK') diff --git a/scripts/guards/test-behavior-quality-guard.mjs b/scripts/guards/test-behavior-quality-guard.mjs new file mode 100644 index 0000000..66e09da --- /dev/null +++ b/scripts/guards/test-behavior-quality-guard.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' + +const root = process.cwd() +const targetDirs = [ + path.join(root, 'tests', 'integration', 'api', 'contract'), + path.join(root, 'tests', 'integration', 'chain'), +] + +function fail(title, details = []) { + console.error(`\n[test-behavior-quality-guard] ${title}`) + for (const detail of details) { + console.error(` - ${detail}`) + } + process.exit(1) +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === 'node_modules') continue + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(full, out) + continue + } + if (entry.isFile() && entry.name.endsWith('.test.ts')) out.push(full) + } + return out +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +const files = targetDirs.flatMap((dir) => walk(dir)) +if (files.length === 0) { + fail('No target test files found', targetDirs.map((dir) => toRel(dir))) +} + +const violations = [] + +for (const file of files) { + const rel = toRel(file) + const text = fs.readFileSync(file, 'utf8') + + const hasSourceRead = /(readFileSync|fs\.readFileSync)\s*\([\s\S]{0,240}src\/(app|lib)\//m.test(text) + if (hasSourceRead) { + violations.push(`${rel}: reading source code text is forbidden in behavior contract/chain tests`) + } + + const forbiddenStringContracts = [ + /toContain\(\s*['"]apiHandler['"]\s*\)/, + /toContain\(\s*['"]submitTask['"]\s*\)/, + /toContain\(\s*['"]maybeSubmitLLMTask['"]\s*\)/, + /includes\(\s*['"]apiHandler['"]\s*\)/, + /includes\(\s*['"]submitTask['"]\s*\)/, + /includes\(\s*['"]maybeSubmitLLMTask['"]\s*\)/, + ] + + for (const pattern of forbiddenStringContracts) { + if (pattern.test(text)) { + violations.push(`${rel}: forbidden structural string assertion matched ${pattern}`) + break + } + } + + const hasWeakCallAssertion = /toHaveBeenCalled\(\s*\)/.test(text) + const hasStrongCallAssertion = /toHaveBeenCalledWith\(/.test(text) + if (hasWeakCallAssertion && !hasStrongCallAssertion) { + violations.push(`${rel}: has toHaveBeenCalled() without any toHaveBeenCalledWith() result assertions`) + } +} + +if (violations.length > 0) { + fail('Behavior quality violations found', violations) +} + +console.log(`[test-behavior-quality-guard] OK files=${files.length}`) diff --git a/scripts/guards/test-behavior-route-coverage-guard.mjs b/scripts/guards/test-behavior-route-coverage-guard.mjs new file mode 100644 index 0000000..6d8e43b --- /dev/null +++ b/scripts/guards/test-behavior-route-coverage-guard.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' + +const root = process.cwd() +const catalogPath = path.join(root, 'tests', 'contracts', 'route-catalog.ts') +const matrixPath = path.join(root, 'tests', 'contracts', 'route-behavior-matrix.ts') + +function fail(title, details = []) { + console.error(`\n[test-behavior-route-coverage-guard] ${title}`) + for (const detail of details) { + console.error(` - ${detail}`) + } + process.exit(1) +} + +if (!fs.existsSync(catalogPath)) { + fail('route catalog is missing', ['tests/contracts/route-catalog.ts']) +} +if (!fs.existsSync(matrixPath)) { + fail('route behavior matrix is missing', ['tests/contracts/route-behavior-matrix.ts']) +} + +const catalogText = fs.readFileSync(catalogPath, 'utf8') +const matrixText = fs.readFileSync(matrixPath, 'utf8') + +if (!matrixText.includes('ROUTE_CATALOG.map')) { + fail('route behavior matrix must derive entries from ROUTE_CATALOG.map') +} + +const routeFilesBlockMatch = catalogText.match(/const ROUTE_FILES = \[([\s\S]*?)\] as const/) +if (!routeFilesBlockMatch) { + fail('unable to parse ROUTE_FILES block from route catalog') +} +const routeFilesBlock = routeFilesBlockMatch ? routeFilesBlockMatch[1] : '' +const routeCount = Array.from(routeFilesBlock.matchAll(/'src\/app\/api\/[^']+\/route\.ts'/g)).length +if (routeCount === 0) { + fail('no routes detected in route catalog') +} + +const testFiles = Array.from(matrixText.matchAll(/'tests\/[a-zA-Z0-9_\-/.]+\.test\.ts'/g)) + .map((match) => match[0].slice(1, -1)) + +if (testFiles.length === 0) { + fail('route behavior matrix does not declare any behavior test files') +} + +const missingTests = Array.from(new Set(testFiles)).filter((file) => !fs.existsSync(path.join(root, file))) +if (missingTests.length > 0) { + fail('route behavior matrix references missing test files', missingTests) +} + +console.log(`[test-behavior-route-coverage-guard] OK routes=${routeCount} tests=${new Set(testFiles).size}`) diff --git a/scripts/guards/test-behavior-tasktype-coverage-guard.mjs b/scripts/guards/test-behavior-tasktype-coverage-guard.mjs new file mode 100644 index 0000000..c3f26ae --- /dev/null +++ b/scripts/guards/test-behavior-tasktype-coverage-guard.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' + +const root = process.cwd() +const catalogPath = path.join(root, 'tests', 'contracts', 'task-type-catalog.ts') +const matrixPath = path.join(root, 'tests', 'contracts', 'tasktype-behavior-matrix.ts') + +function fail(title, details = []) { + console.error(`\n[test-behavior-tasktype-coverage-guard] ${title}`) + for (const detail of details) { + console.error(` - ${detail}`) + } + process.exit(1) +} + +if (!fs.existsSync(catalogPath)) { + fail('task type catalog is missing', ['tests/contracts/task-type-catalog.ts']) +} +if (!fs.existsSync(matrixPath)) { + fail('tasktype behavior matrix is missing', ['tests/contracts/tasktype-behavior-matrix.ts']) +} + +const catalogText = fs.readFileSync(catalogPath, 'utf8') +const matrixText = fs.readFileSync(matrixPath, 'utf8') + +if (!matrixText.includes('TASK_TYPE_CATALOG.map')) { + fail('tasktype behavior matrix must derive entries from TASK_TYPE_CATALOG.map') +} + +const taskTypeCount = Array.from(catalogText.matchAll(/\[TASK_TYPE\.([A-Z_]+)\]/g)).length +if (taskTypeCount === 0) { + fail('no task types detected in task type catalog') +} + +const testFiles = Array.from(matrixText.matchAll(/'tests\/[a-zA-Z0-9_\-/.]+\.test\.ts'/g)) + .map((match) => match[0].slice(1, -1)) + +if (testFiles.length === 0) { + fail('tasktype behavior matrix does not declare any behavior test files') +} + +const missingTests = Array.from(new Set(testFiles)).filter((file) => !fs.existsSync(path.join(root, file))) +if (missingTests.length > 0) { + fail('tasktype behavior matrix references missing test files', missingTests) +} + +console.log(`[test-behavior-tasktype-coverage-guard] OK taskTypes=${taskTypeCount} tests=${new Set(testFiles).size}`) diff --git a/scripts/guards/test-route-coverage-guard.mjs b/scripts/guards/test-route-coverage-guard.mjs new file mode 100644 index 0000000..9dca4e6 --- /dev/null +++ b/scripts/guards/test-route-coverage-guard.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' + +const root = process.cwd() +const apiDir = path.join(root, 'src', 'app', 'api') +const catalogPath = path.join(root, 'tests', 'contracts', 'route-catalog.ts') + +function fail(title, details = []) { + console.error(`\n[test-route-coverage-guard] ${title}`) + for (const detail of details) { + console.error(` - ${detail}`) + } + process.exit(1) +} + +function walk(dir, out = []) { + if (!fs.existsSync(dir)) return out + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(fullPath, out) + continue + } + if (entry.name === 'route.ts') out.push(fullPath) + } + return out +} + +function toRel(fullPath) { + return path.relative(root, fullPath).split(path.sep).join('/') +} + +if (!fs.existsSync(catalogPath)) { + fail('route-catalog.ts is missing', ['tests/contracts/route-catalog.ts']) +} + +const actualRoutes = walk(apiDir).map(toRel).sort() +const catalogText = fs.readFileSync(catalogPath, 'utf8') +const catalogRoutes = Array.from(catalogText.matchAll(/'src\/app\/api\/[^']+\/route\.ts'/g)) + .map((match) => match[0].slice(1, -1)) + .sort() + +const missingInCatalog = actualRoutes.filter((routeFile) => !catalogRoutes.includes(routeFile)) +const staleInCatalog = catalogRoutes.filter((routeFile) => !actualRoutes.includes(routeFile)) + +if (missingInCatalog.length > 0) { + fail('Missing routes in tests/contracts/route-catalog.ts', missingInCatalog) +} +if (staleInCatalog.length > 0) { + fail('Stale route entries found in tests/contracts/route-catalog.ts', staleInCatalog) +} + +console.log(`[test-route-coverage-guard] OK routes=${actualRoutes.length}`) diff --git a/scripts/guards/test-tasktype-coverage-guard.mjs b/scripts/guards/test-tasktype-coverage-guard.mjs new file mode 100644 index 0000000..2652cc2 --- /dev/null +++ b/scripts/guards/test-tasktype-coverage-guard.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +import fs from 'fs' +import path from 'path' + +const root = process.cwd() +const taskTypesPath = path.join(root, 'src', 'lib', 'task', 'types.ts') +const catalogPath = path.join(root, 'tests', 'contracts', 'task-type-catalog.ts') + +function fail(title, details = []) { + console.error(`\n[test-tasktype-coverage-guard] ${title}`) + for (const detail of details) { + console.error(` - ${detail}`) + } + process.exit(1) +} + +if (!fs.existsSync(taskTypesPath)) { + fail('Task type source file is missing', ['src/lib/task/types.ts']) +} +if (!fs.existsSync(catalogPath)) { + fail('Task type catalog file is missing', ['tests/contracts/task-type-catalog.ts']) +} + +const taskTypesText = fs.readFileSync(taskTypesPath, 'utf8') +const catalogText = fs.readFileSync(catalogPath, 'utf8') + +const taskTypeBlockMatch = taskTypesText.match(/export const TASK_TYPE = \{([\s\S]*?)\n\} as const/) +if (!taskTypeBlockMatch) { + fail('Unable to parse TASK_TYPE block from src/lib/task/types.ts') +} +const taskTypeBlock = taskTypeBlockMatch ? taskTypeBlockMatch[1] : '' +const taskTypeKeys = Array.from(taskTypeBlock.matchAll(/^\s+([A-Z_]+):\s'[^']+',?$/gm)).map((match) => match[1]) +const catalogKeys = Array.from(catalogText.matchAll(/\[TASK_TYPE\.([A-Z_]+)\]/g)).map((match) => match[1]) + +const missingKeys = taskTypeKeys.filter((key) => !catalogKeys.includes(key)) +const staleKeys = catalogKeys.filter((key) => !taskTypeKeys.includes(key)) + +if (missingKeys.length > 0) { + fail('Missing TASK_TYPE owners in tests/contracts/task-type-catalog.ts', missingKeys) +} +if (staleKeys.length > 0) { + fail('Stale TASK_TYPE keys in tests/contracts/task-type-catalog.ts', staleKeys) +} + +console.log(`[test-tasktype-coverage-guard] OK taskTypes=${taskTypeKeys.length}`) diff --git a/scripts/media-archive-legacy-refs.ts b/scripts/media-archive-legacy-refs.ts new file mode 100644 index 0000000..68bb7ea --- /dev/null +++ b/scripts/media-archive-legacy-refs.ts @@ -0,0 +1,127 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { createHash } from 'node:crypto' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { prisma } from '@/lib/prisma' +import { MEDIA_MODEL_MAPPINGS } from './media-mapping' + +const BACKUP_ROOT = path.join(process.cwd(), 'data', 'migration-backups') +const BATCH_SIZE = 500 +type DynamicModel = { + findMany: (args: unknown) => Promise>> + createMany?: (args: unknown) => Promise +} +const prismaDynamic = prisma as unknown as Record + +function nowStamp() { + return new Date().toISOString().replace(/[:.]/g, '-') +} + +function checksum(value: string) { + return createHash('sha256').update(value).digest('hex') +} + +function toSelect(fields: string[]) { + const select: Record = { id: true } + for (const field of fields) select[field] = true + return select +} + +async function main() { + const runId = nowStamp() + const backupDir = path.join(BACKUP_ROOT, runId) + await fs.mkdir(backupDir, { recursive: true }) + + const allRows: Array<{ + runId: string + tableName: string + rowId: string + fieldName: string + legacyValue: string + checksum: string + }> = [] + + for (const mapping of MEDIA_MODEL_MAPPINGS) { + const model = prismaDynamic[mapping.model] + if (!model) continue + + const select = toSelect(mapping.fields.map((f) => f.legacyField)) + let cursor: string | null = null + + while (true) { + const page = await model.findMany({ + select, + ...(cursor + ? { + cursor: { id: cursor }, + skip: 1, + } + : {}), + orderBy: { id: 'asc' }, + take: BATCH_SIZE, + }) + if (!page.length) break + + for (const row of page) { + for (const field of mapping.fields) { + const value = row[field.legacyField] + if (typeof value !== 'string' || !value.trim()) continue + allRows.push({ + runId, + tableName: mapping.tableName, + rowId: String(row.id), + fieldName: field.legacyField, + legacyValue: value, + checksum: checksum(value), + }) + } + } + + cursor = String(page[page.length - 1].id) + } + } + + if (allRows.length > 0) { + try { + const backupModel = prismaDynamic.legacyMediaRefBackup + if (!backupModel?.createMany) { + throw new Error('Prisma model not found: legacyMediaRefBackup') + } + for (let i = 0; i < allRows.length; i += 1000) { + const chunk = allRows.slice(i, i + 1000) + await backupModel.createMany({ data: chunk }) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + _ulogError('[media-archive-legacy-refs] db backup table unavailable, fallback to file snapshot only', message) + } + } + + const snapshotPath = path.join(backupDir, 'legacy-media-refs.json') + await fs.writeFile(snapshotPath, JSON.stringify(allRows, null, 2), 'utf8') + const snapshotHash = checksum(await fs.readFile(snapshotPath, 'utf8')) + + const summary = { + runId, + createdAt: new Date().toISOString(), + backupDir, + archivedCount: allRows.length, + snapshotFile: path.basename(snapshotPath), + snapshotSha256: snapshotHash, + } + + await fs.writeFile(path.join(backupDir, 'legacy-media-refs-summary.json'), JSON.stringify(summary, null, 2), 'utf8') + + _ulogInfo(`[media-archive-legacy-refs] runId=${runId}`) + _ulogInfo(`[media-archive-legacy-refs] archived=${allRows.length}`) + _ulogInfo(`[media-archive-legacy-refs] snapshot=${snapshotPath}`) +} + +main() + .catch((error) => { + _ulogError('[media-archive-legacy-refs] failed:', error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/media-backfill-refs.ts b/scripts/media-backfill-refs.ts new file mode 100644 index 0000000..1ed8ce4 --- /dev/null +++ b/scripts/media-backfill-refs.ts @@ -0,0 +1,122 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { prisma } from '@/lib/prisma' +import { resolveMediaRefFromLegacyValue } from '@/lib/media/service' +import { MEDIA_MODEL_MAPPINGS } from './media-mapping' + +const BATCH_SIZE = 200 +type DynamicModel = { + findMany: (args: unknown) => Promise>> + update: (args: unknown) => Promise +} +const prismaDynamic = prisma as unknown as Record + +function toSelect(fields: string[]) { + const select: Record = { id: true } + for (const field of fields) select[field] = true + return select +} + +async function backfillModel(mapping: (typeof MEDIA_MODEL_MAPPINGS)[number]) { + const model = prismaDynamic[mapping.model] + if (!model) { + throw new Error(`Prisma model not found: ${mapping.model}`) + } + + const selectFields = mapping.fields.flatMap((f) => [f.legacyField, f.mediaIdField]) + const select = toSelect(selectFields) + + let cursor: string | null = null + let scanned = 0 + let updated = 0 + + try { + while (true) { + const rows = await model.findMany({ + select, + ...(cursor + ? { + cursor: { id: cursor }, + skip: 1, + } + : {}), + orderBy: { id: 'asc' }, + take: BATCH_SIZE, + }) + + if (!rows.length) break + + for (const row of rows) { + scanned += 1 + const patch: Record = {} + + for (const field of mapping.fields) { + const mediaId = row[field.mediaIdField] + const legacyValue = row[field.legacyField] + if (mediaId || typeof legacyValue !== 'string' || !legacyValue.trim()) { + continue + } + + const media = await resolveMediaRefFromLegacyValue(legacyValue) + if (!media) continue + patch[field.mediaIdField] = media.id + } + + if (Object.keys(patch).length > 0) { + await model.update({ + where: { id: String(row.id) }, + data: patch, + }) + updated += 1 + } + } + + cursor = String(rows[rows.length - 1].id) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + if (message.includes('does not exist') || message.includes('Unknown column')) { + _ulogError( + `[media-backfill-refs] skip ${mapping.tableName}: migration columns not available yet`, + message, + ) + return { scanned: 0, updated: 0, skipped: true } + } + throw error + } + + return { scanned, updated, skipped: false } +} + +async function main() { + const startedAt = new Date() + _ulogInfo(`[media-backfill-refs] started at ${startedAt.toISOString()}`) + + let totalScanned = 0 + let totalUpdated = 0 + + for (const mapping of MEDIA_MODEL_MAPPINGS) { + const result = await backfillModel(mapping) + totalScanned += result.scanned + totalUpdated += result.updated + if (result.skipped) { + _ulogInfo(`[media-backfill-refs] ${mapping.tableName}: skipped (run add-only DB migration first)`) + } else { + _ulogInfo( + `[media-backfill-refs] ${mapping.tableName}: scanned=${result.scanned} updatedRows=${result.updated}`, + ) + } + } + + _ulogInfo( + `[media-backfill-refs] done scanned=${totalScanned} updatedRows=${totalUpdated} durationMs=${Date.now() - startedAt.getTime()}`, + ) +} + +main() + .catch((error) => { + _ulogError('[media-backfill-refs] failed:', error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/media-build-unreferenced-index.ts b/scripts/media-build-unreferenced-index.ts new file mode 100644 index 0000000..a0b06b2 --- /dev/null +++ b/scripts/media-build-unreferenced-index.ts @@ -0,0 +1,202 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import COS from 'cos-nodejs-sdk-v5' +import { prisma } from '@/lib/prisma' +import { resolveStorageKeyFromMediaValue } from '@/lib/media/service' +import { MEDIA_MODEL_MAPPINGS } from './media-mapping' + +type StorageEntry = { + key: string + sizeBytes: number + lastModified: string | null +} +type CosBucketPage = { + Contents?: Array<{ Key: string; Size?: string | number; LastModified?: string }> + IsTruncated?: string | boolean + NextMarker?: string +} +type DynamicModel = { + findMany: (args: unknown) => Promise>> +} +const prismaDynamic = prisma as unknown as Record + +const BACKUP_ROOT = path.join(process.cwd(), 'data', 'migration-backups') + +function nowStamp() { + return new Date().toISOString().replace(/[:.]/g, '-') +} + +async function listLocalObjects(): Promise { + const uploadDir = process.env.UPLOAD_DIR || './data/uploads' + const rootDir = path.isAbsolute(uploadDir) ? uploadDir : path.join(process.cwd(), uploadDir) + const exists = await fs.stat(rootDir).then(() => true).catch(() => false) + if (!exists) return [] + + const rows: StorageEntry[] = [] + const queue = [''] + + while (queue.length > 0) { + const rel = queue.shift() as string + const full = path.join(rootDir, rel) + const entries = await fs.readdir(full, { withFileTypes: true }) + for (const entry of entries) { + const childRel = path.join(rel, entry.name) + if (entry.isDirectory()) { + queue.push(childRel) + continue + } + if (!entry.isFile()) continue + const stat = await fs.stat(path.join(rootDir, childRel)) + rows.push({ + key: childRel.split(path.sep).join('/'), + sizeBytes: stat.size, + lastModified: stat.mtime.toISOString(), + }) + } + } + + return rows +} + +async function listCosObjects(): Promise { + const secretId = process.env.COS_SECRET_ID + const secretKey = process.env.COS_SECRET_KEY + const bucket = process.env.COS_BUCKET + const region = process.env.COS_REGION + + if (!secretId || !secretKey || !bucket || !region) { + throw new Error('Missing COS env: COS_SECRET_ID/COS_SECRET_KEY/COS_BUCKET/COS_REGION') + } + + const cos = new COS({ SecretId: secretId, SecretKey: secretKey, Timeout: 60_000 }) + const rows: StorageEntry[] = [] + let marker = '' + + while (true) { + const page = await new Promise((resolve, reject) => { + cos.getBucket( + { + Bucket: bucket, + Region: region, + Marker: marker, + MaxKeys: 1000, + }, + (err, data) => (err ? reject(err) : resolve(data as unknown as CosBucketPage)), + ) + }) + + const contents = page.Contents || [] + for (const item of contents) { + rows.push({ + key: item.Key, + sizeBytes: Number(item.Size || 0), + lastModified: item.LastModified || null, + }) + } + + const truncated = String(page.IsTruncated || 'false') === 'true' + if (!truncated) break + const nextMarker = typeof page.NextMarker === 'string' ? page.NextMarker : '' + marker = nextMarker || (contents.length ? contents[contents.length - 1].Key : '') + if (!marker) break + } + + return rows +} + +async function listStorageObjects() { + const storageType = process.env.STORAGE_TYPE || 'cos' + if (storageType === 'local') { + return { storageType, rows: await listLocalObjects() } + } + return { storageType, rows: await listCosObjects() } +} + +async function buildReferencedKeySet() { + const refs = new Set() + + try { + const mediaRows = await prismaDynamic.mediaObject.findMany({ + select: { storageKey: true }, + }) + for (const row of mediaRows) { + if (typeof row.storageKey === 'string' && row.storageKey.trim()) refs.add(row.storageKey) + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + _ulogError('[media-build-unreferenced-index] media_objects unavailable, fallback to legacy field scan', message) + } + + for (const mapping of MEDIA_MODEL_MAPPINGS) { + const model = prismaDynamic[mapping.model] + if (!model) continue + + const select: Record = { id: true } + for (const field of mapping.fields) select[field.legacyField] = true + + let cursor: string | null = null + while (true) { + const rows = await model.findMany({ + select, + ...(cursor + ? { + cursor: { id: cursor }, + skip: 1, + } + : {}), + orderBy: { id: 'asc' }, + take: 500, + }) + if (!rows.length) break + + for (const row of rows) { + for (const field of mapping.fields) { + const value = row[field.legacyField] + if (typeof value !== 'string' || !value.trim()) continue + const key = await resolveStorageKeyFromMediaValue(value) + if (key) refs.add(key) + } + } + + cursor = String(rows[rows.length - 1].id) + } + } + + return refs +} + +async function main() { + const stamp = nowStamp() + const backupDir = path.join(BACKUP_ROOT, stamp) + await fs.mkdir(backupDir, { recursive: true }) + + const referenced = await buildReferencedKeySet() + const storage = await listStorageObjects() + const unreferenced = storage.rows.filter((row) => !referenced.has(row.key)) + + const output = { + createdAt: new Date().toISOString(), + storageType: storage.storageType, + totalStorageObjects: storage.rows.length, + referencedKeyCount: referenced.size, + unreferencedCount: unreferenced.length, + objects: unreferenced, + } + + const filePath = path.join(backupDir, 'unreferenced-storage-objects-index.json') + await fs.writeFile(filePath, JSON.stringify(output, null, 2), 'utf8') + + _ulogInfo(`[media-build-unreferenced-index] storageType=${storage.storageType}`) + _ulogInfo(`[media-build-unreferenced-index] total=${storage.rows.length} unreferenced=${unreferenced.length}`) + _ulogInfo(`[media-build-unreferenced-index] output=${filePath}`) +} + +main() + .catch((error) => { + _ulogError('[media-build-unreferenced-index] failed:', error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/media-mapping.ts b/scripts/media-mapping.ts new file mode 100644 index 0000000..297e558 --- /dev/null +++ b/scripts/media-mapping.ts @@ -0,0 +1,90 @@ +export type MediaFieldMapping = { + legacyField: string + mediaIdField: string +} + +export type MediaModelMapping = { + model: string + tableName: string + fields: MediaFieldMapping[] +} + +export const MEDIA_MODEL_MAPPINGS: MediaModelMapping[] = [ + { + model: 'characterAppearance', + tableName: 'character_appearances', + fields: [{ legacyField: 'imageUrl', mediaIdField: 'imageMediaId' }], + }, + { + model: 'locationImage', + tableName: 'location_images', + fields: [{ legacyField: 'imageUrl', mediaIdField: 'imageMediaId' }], + }, + { + model: 'novelPromotionCharacter', + tableName: 'novel_promotion_characters', + fields: [{ legacyField: 'customVoiceUrl', mediaIdField: 'customVoiceMediaId' }], + }, + { + model: 'novelPromotionEpisode', + tableName: 'novel_promotion_episodes', + fields: [{ legacyField: 'audioUrl', mediaIdField: 'audioMediaId' }], + }, + { + model: 'novelPromotionPanel', + tableName: 'novel_promotion_panels', + fields: [ + { legacyField: 'imageUrl', mediaIdField: 'imageMediaId' }, + { legacyField: 'videoUrl', mediaIdField: 'videoMediaId' }, + { legacyField: 'lipSyncVideoUrl', mediaIdField: 'lipSyncVideoMediaId' }, + { legacyField: 'sketchImageUrl', mediaIdField: 'sketchImageMediaId' }, + { legacyField: 'previousImageUrl', mediaIdField: 'previousImageMediaId' }, + ], + }, + { + model: 'novelPromotionShot', + tableName: 'novel_promotion_shots', + fields: [{ legacyField: 'imageUrl', mediaIdField: 'imageMediaId' }], + }, + { + model: 'supplementaryPanel', + tableName: 'supplementary_panels', + fields: [{ legacyField: 'imageUrl', mediaIdField: 'imageMediaId' }], + }, + { + model: 'novelPromotionVoiceLine', + tableName: 'novel_promotion_voice_lines', + fields: [{ legacyField: 'audioUrl', mediaIdField: 'audioMediaId' }], + }, + { + model: 'voicePreset', + tableName: 'voice_presets', + fields: [{ legacyField: 'audioUrl', mediaIdField: 'audioMediaId' }], + }, + { + model: 'globalCharacter', + tableName: 'global_characters', + fields: [{ legacyField: 'customVoiceUrl', mediaIdField: 'customVoiceMediaId' }], + }, + { + model: 'globalCharacterAppearance', + tableName: 'global_character_appearances', + fields: [ + { legacyField: 'imageUrl', mediaIdField: 'imageMediaId' }, + { legacyField: 'previousImageUrl', mediaIdField: 'previousImageMediaId' }, + ], + }, + { + model: 'globalLocationImage', + tableName: 'global_location_images', + fields: [ + { legacyField: 'imageUrl', mediaIdField: 'imageMediaId' }, + { legacyField: 'previousImageUrl', mediaIdField: 'previousImageMediaId' }, + ], + }, + { + model: 'globalVoice', + tableName: 'global_voices', + fields: [{ legacyField: 'customVoiceUrl', mediaIdField: 'customVoiceMediaId' }], + }, +] diff --git a/scripts/media-restore-dry-run.ts b/scripts/media-restore-dry-run.ts new file mode 100644 index 0000000..16fc41e --- /dev/null +++ b/scripts/media-restore-dry-run.ts @@ -0,0 +1,111 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { prisma } from '@/lib/prisma' + +const BACKUP_ROOT = path.join(process.cwd(), 'data', 'migration-backups') + +type CountMap = Record + +async function findLatestBackupDir() { + const exists = await fs.stat(BACKUP_ROOT).then(() => true).catch(() => false) + if (!exists) { + throw new Error(`Backup root not found: ${BACKUP_ROOT}`) + } + const dirs = (await fs.readdir(BACKUP_ROOT, { withFileTypes: true })) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort() + const validDirs: string[] = [] + for (const dir of dirs) { + const metadataPath = path.join(BACKUP_ROOT, dir, 'metadata.json') + const exists = await fs.stat(metadataPath).then(() => true).catch(() => false) + if (exists) validDirs.push(dir) + } + + if (!validDirs.length) { + throw new Error(`No backup directories found in ${BACKUP_ROOT}`) + } + return path.join(BACKUP_ROOT, validDirs[validDirs.length - 1]) +} + +async function readExpectedCounts(backupDir: string): Promise { + const metadataPath = path.join(backupDir, 'metadata.json') + const raw = await fs.readFile(metadataPath, 'utf8') + const parsed = JSON.parse(raw) + return (parsed.tableCounts || {}) as CountMap +} + +async function currentCounts(): Promise { + const entries: Array<[string, string]> = [ + ['projects', 'projects'], + ['novel_promotion_projects', 'novel_promotion_projects'], + ['novel_promotion_episodes', 'novel_promotion_episodes'], + ['novel_promotion_panels', 'novel_promotion_panels'], + ['novel_promotion_voice_lines', 'novel_promotion_voice_lines'], + ['global_characters', 'global_characters'], + ['global_character_appearances', 'global_character_appearances'], + ['global_locations', 'global_locations'], + ['global_location_images', 'global_location_images'], + ['global_voices', 'global_voices'], + ['tasks', 'tasks'], + ['task_events', 'task_events'], + ] + + const resolved = await Promise.all(entries.map(async ([name, tableName]) => { + const rows = (await prisma.$queryRawUnsafe( + `SELECT COUNT(*) AS c FROM \`${tableName}\``, + )) as Array> + const raw = rows[0] || {} + const firstValue = Object.values(raw)[0] + const count = Number(firstValue || 0) + return [name, Number.isFinite(count) ? count : 0] as const + })) + const out: CountMap = {} + for (const [name, count] of resolved) out[name] = count + return out +} + +function printDiff(expected: CountMap, actual: CountMap) { + const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort() + let hasDiff = false + + _ulogInfo('table\texpected\tactual\tdelta') + for (const key of keys) { + const e = expected[key] ?? 0 + const a = actual[key] ?? 0 + const d = a - e + if (d !== 0) hasDiff = true + _ulogInfo(`${key}\t${e}\t${a}\t${d >= 0 ? '+' : ''}${d}`) + } + + return hasDiff +} + +async function main() { + const explicit = process.argv.find((arg) => arg.startsWith('--backup=')) + const backupDir = explicit ? path.resolve(explicit.split('=')[1]) : await findLatestBackupDir() + + _ulogInfo(`[media-restore-dry-run] backupDir=${backupDir}`) + + const expected = await readExpectedCounts(backupDir) + const actual = await currentCounts() + const hasDiff = printDiff(expected, actual) + + if (hasDiff) { + _ulogInfo('[media-restore-dry-run] drift detected (dry-run only, no writes executed).') + process.exitCode = 2 + return + } + + _ulogInfo('[media-restore-dry-run] ok: counts match expected snapshot.') +} + +main() + .catch((error) => { + _ulogError('[media-restore-dry-run] failed:', error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/media-safety-backup.ts b/scripts/media-safety-backup.ts new file mode 100644 index 0000000..d53cac6 --- /dev/null +++ b/scripts/media-safety-backup.ts @@ -0,0 +1,247 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { createHash } from 'node:crypto' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import COS from 'cos-nodejs-sdk-v5' +import { prisma } from '@/lib/prisma' + +type SnapshotTask = { + name: string + tableName: string +} + +type StorageIndexRow = { + key: string + hash: string | null + sizeBytes: number + lastModified: string | null +} + +type CosBucketPage = { + Contents?: Array<{ + Key: string + ETag?: string + Size?: string | number + LastModified?: string + }> + IsTruncated?: string | boolean + NextMarker?: string +} + +const BACKUP_ROOT = path.join(process.cwd(), 'data', 'migration-backups') + +function nowStamp() { + return new Date().toISOString().replace(/[:.]/g, '-') +} + +function toJson(value: unknown) { + return JSON.stringify( + value, + (_key, val) => (typeof val === 'bigint' ? String(val) : val), + 2, + ) +} + +async function writeJson(filePath: string, data: unknown) { + await fs.writeFile(filePath, toJson(data), 'utf8') +} + +function sha256Text(input: string) { + return createHash('sha256').update(input).digest('hex') +} + +function resolveDatabaseFilePath(databaseUrl: string | undefined): string | null { + if (!databaseUrl) return null + if (databaseUrl.startsWith('file:')) { + const raw = databaseUrl.slice('file:'.length) + if (!raw) return null + return path.isAbsolute(raw) ? raw : path.join(process.cwd(), raw) + } + return null +} + +async function listLocalFilesRecursively(rootDir: string, prefix = ''): Promise { + const fullDir = path.join(rootDir, prefix) + const entries = await fs.readdir(fullDir, { withFileTypes: true }) + const out: StorageIndexRow[] = [] + + for (const entry of entries) { + const rel = path.join(prefix, entry.name) + if (entry.isDirectory()) { + out.push(...(await listLocalFilesRecursively(rootDir, rel))) + continue + } + if (!entry.isFile()) continue + + const filePath = path.join(rootDir, rel) + const stat = await fs.stat(filePath) + const buf = await fs.readFile(filePath) + out.push({ + key: rel.split(path.sep).join('/'), + hash: createHash('sha256').update(buf).digest('hex'), + sizeBytes: stat.size, + lastModified: stat.mtime.toISOString(), + }) + } + + return out +} + +async function listCosObjects(): Promise { + const secretId = process.env.COS_SECRET_ID + const secretKey = process.env.COS_SECRET_KEY + const bucket = process.env.COS_BUCKET + const region = process.env.COS_REGION + + if (!secretId || !secretKey || !bucket || !region) { + throw new Error('Missing COS env: COS_SECRET_ID/COS_SECRET_KEY/COS_BUCKET/COS_REGION') + } + + const cos = new COS({ SecretId: secretId, SecretKey: secretKey, Timeout: 60_000 }) + const out: StorageIndexRow[] = [] + let marker = '' + + while (true) { + const page = await new Promise((resolve, reject) => { + cos.getBucket( + { + Bucket: bucket, + Region: region, + Marker: marker, + MaxKeys: 1000, + }, + (err, data) => (err ? reject(err) : resolve((data || {}) as CosBucketPage)), + ) + }) + + const contents = page.Contents || [] + for (const item of contents) { + out.push({ + key: item.Key, + hash: item.ETag ? String(item.ETag).replaceAll('"', '') : null, + sizeBytes: Number(item.Size || 0), + lastModified: item.LastModified || null, + }) + } + + const truncated = String(page.IsTruncated || 'false') === 'true' + if (!truncated) break + marker = page.NextMarker || (contents.length ? contents[contents.length - 1].Key : '') + if (!marker) break + } + + return out +} + +async function buildStorageIndex(): Promise<{ storageType: string; rows: StorageIndexRow[] }> { + const storageType = process.env.STORAGE_TYPE || 'cos' + if (storageType === 'local') { + const uploadDir = process.env.UPLOAD_DIR || './data/uploads' + const rootDir = path.isAbsolute(uploadDir) ? uploadDir : path.join(process.cwd(), uploadDir) + const exists = await fs.stat(rootDir).then(() => true).catch(() => false) + if (!exists) { + return { storageType, rows: [] } + } + const rows = await listLocalFilesRecursively(rootDir) + return { storageType, rows } + } + + const rows = await listCosObjects() + return { storageType, rows } +} + +async function snapshotTables(backupDir: string) { + const tasks: SnapshotTask[] = [ + { name: 'projects', tableName: 'projects' }, + { name: 'novel_promotion_projects', tableName: 'novel_promotion_projects' }, + { name: 'novel_promotion_episodes', tableName: 'novel_promotion_episodes' }, + { name: 'novel_promotion_panels', tableName: 'novel_promotion_panels' }, + { name: 'novel_promotion_voice_lines', tableName: 'novel_promotion_voice_lines' }, + { name: 'global_characters', tableName: 'global_characters' }, + { name: 'global_character_appearances', tableName: 'global_character_appearances' }, + { name: 'global_locations', tableName: 'global_locations' }, + { name: 'global_location_images', tableName: 'global_location_images' }, + { name: 'global_voices', tableName: 'global_voices' }, + { name: 'tasks', tableName: 'tasks' }, + { name: 'task_events', tableName: 'task_events' }, + ] + + const counts: Record = {} + for (const task of tasks) { + const rows = (await prisma.$queryRawUnsafe(`SELECT * FROM \`${task.tableName}\``)) as unknown[] + counts[task.name] = rows.length + await writeJson(path.join(backupDir, `${task.name}.json`), rows) + } + + return counts +} + +async function writeChecksums(backupDir: string) { + const files = (await fs.readdir(backupDir)).sort() + const sums: Record = {} + + for (const file of files) { + const filePath = path.join(backupDir, file) + const stat = await fs.stat(filePath) + if (!stat.isFile()) continue + const buf = await fs.readFile(filePath) + sums[file] = createHash('sha256').update(buf).digest('hex') + } + + await writeJson(path.join(backupDir, 'checksums.json'), sums) +} + +async function backupDbFile(backupDir: string) { + const dbFile = resolveDatabaseFilePath(process.env.DATABASE_URL) + if (!dbFile) return null + + const stat = await fs.stat(dbFile).catch(() => null) + if (!stat || !stat.isFile()) return null + + const fileName = path.basename(dbFile) + const target = path.join(backupDir, `db-file-${fileName}`) + await fs.copyFile(dbFile, target) + return path.basename(target) +} + +async function main() { + const stamp = nowStamp() + const backupDir = path.join(BACKUP_ROOT, stamp) + await fs.mkdir(backupDir, { recursive: true }) + + const meta: Record = { + createdAt: new Date().toISOString(), + backupDir, + databaseUrl: process.env.DATABASE_URL || null, + storageType: process.env.STORAGE_TYPE || 'cos', + nodeEnv: process.env.NODE_ENV || null, + } + + const copiedDbFile = await backupDbFile(backupDir) + meta.copiedDbFile = copiedDbFile + + const tableCounts = await snapshotTables(backupDir) + meta.tableCounts = tableCounts + + const storage = await buildStorageIndex() + meta.storageType = storage.storageType + meta.storageObjectCount = storage.rows.length + await writeJson(path.join(backupDir, 'storage-object-index.json'), storage.rows) + + await writeChecksums(backupDir) + meta.metadataChecksum = sha256Text(toJson(meta)) + await writeJson(path.join(backupDir, 'metadata.json'), meta) + + _ulogInfo(`[media-safety-backup] done: ${backupDir}`) + _ulogInfo(`[media-safety-backup] tableCounts=${JSON.stringify(tableCounts)}`) + _ulogInfo(`[media-safety-backup] storageObjects=${storage.rows.length}`) +} + +main() + .catch((error) => { + _ulogError('[media-safety-backup] failed:', error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/migrate-cancelled-to-failed.ts b/scripts/migrate-cancelled-to-failed.ts new file mode 100644 index 0000000..ecb9257 --- /dev/null +++ b/scripts/migrate-cancelled-to-failed.ts @@ -0,0 +1,72 @@ +import { prisma } from '@/lib/prisma' + +const OLD_STATUS = 'cancelled' +const NEW_STATUS = 'failed' +const OLD_EVENT_TYPE = 'task.cancelled' +const NEW_EVENT_TYPE = 'task.failed' +const MIGRATION_ERROR_CODE = 'USER_CANCELLED' +const MIGRATION_ERROR_MESSAGE = '用户已停止任务。' + +function log(message: string) { + process.stdout.write(`${message}\n`) +} + +function logError(message: string) { + process.stderr.write(`${message}\n`) +} + +async function main() { + const totalTasks = await prisma.task.count({ + where: { status: OLD_STATUS }, + }) + const totalEvents = await prisma.taskEvent.count({ + where: { eventType: OLD_EVENT_TYPE }, + }) + + log(`[migrate-cancelled-to-failed] matched tasks: ${totalTasks}`) + log(`[migrate-cancelled-to-failed] matched events: ${totalEvents}`) + if (totalTasks === 0 && totalEvents === 0) { + log('[migrate-cancelled-to-failed] no rows to migrate') + return + } + + const taskEmptyMessageResult = await prisma.task.updateMany({ + where: { + status: OLD_STATUS, + OR: [{ errorMessage: null }, { errorMessage: '' }], + }, + data: { + status: NEW_STATUS, + errorCode: MIGRATION_ERROR_CODE, + errorMessage: MIGRATION_ERROR_MESSAGE, + }, + }) + + const taskResult = await prisma.task.updateMany({ + where: { status: OLD_STATUS }, + data: { + status: NEW_STATUS, + errorCode: MIGRATION_ERROR_CODE, + }, + }) + + const eventResult = await prisma.taskEvent.updateMany({ + where: { eventType: OLD_EVENT_TYPE }, + data: { + eventType: NEW_EVENT_TYPE, + }, + }) + + log(`[migrate-cancelled-to-failed] updated tasks (empty message): ${taskEmptyMessageResult.count}`) + log(`[migrate-cancelled-to-failed] updated tasks (remaining): ${taskResult.count}`) + log(`[migrate-cancelled-to-failed] updated events: ${eventResult.count}`) +} + +main() + .catch((error) => { + logError(`[migrate-cancelled-to-failed] failed: ${error instanceof Error ? error.stack || error.message : String(error)}`) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/migrate-image-urls-contract.ts b/scripts/migrate-image-urls-contract.ts new file mode 100644 index 0000000..3a3e77f --- /dev/null +++ b/scripts/migrate-image-urls-contract.ts @@ -0,0 +1,231 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { prisma } from '@/lib/prisma' +import { encodeImageUrls } from '@/lib/contracts/image-urls-contract' + +type AppearanceRow = { + id: string + imageUrls: string | null + previousImageUrls: string | null +} + +type DynamicModel = { + findMany: (args: unknown) => Promise + update: (args: unknown) => Promise +} + +type FieldName = 'imageUrls' | 'previousImageUrls' + +type NormalizeResult = { + next: string + changed: boolean + reason: 'ok' | 'null' | 'invalid_json' | 'not_array' | 'filtered_non_string' | 'normalized_json' +} + +type ModelStats = { + scanned: number + updatedRows: number + changedFields: number + reasons: Record +} + +const BATCH_SIZE = 200 +const APPLY = process.argv.includes('--apply') + +const MODELS: Array<{ name: string; model: string }> = [ + { name: 'CharacterAppearance', model: 'characterAppearance' }, + { name: 'GlobalCharacterAppearance', model: 'globalCharacterAppearance' }, +] + +const prismaDynamic = prisma as unknown as Record + +function print(message: string) { + process.stdout.write(`${message}\n`) +} + +function normalizeField(raw: string | null): NormalizeResult { + if (raw === null) { + return { + next: encodeImageUrls([]), + changed: true, + reason: 'null', + } + } + + try { + const parsed = JSON.parse(raw) as unknown + if (!Array.isArray(parsed)) { + return { + next: encodeImageUrls([]), + changed: true, + reason: 'not_array', + } + } + + const stringOnly = parsed.filter((item): item is string => typeof item === 'string') + const next = encodeImageUrls(stringOnly) + + if (parsed.length !== stringOnly.length) { + return { + next, + changed: true, + reason: 'filtered_non_string', + } + } + + if (raw !== next) { + return { + next, + changed: true, + reason: 'normalized_json', + } + } + + return { + next, + changed: false, + reason: 'ok', + } + } catch { + return { + next: encodeImageUrls([]), + changed: true, + reason: 'invalid_json', + } + } +} + +async function migrateModel(modelName: string, modelKey: string) { + const model = prismaDynamic[modelKey] + if (!model) { + throw new Error(`Prisma model not found: ${modelKey}`) + } + + const stats: ModelStats = { + scanned: 0, + updatedRows: 0, + changedFields: 0, + reasons: { + ok: 0, + null: 0, + invalid_json: 0, + not_array: 0, + filtered_non_string: 0, + normalized_json: 0, + }, + } + + const samples: Array<{ id: string; field: FieldName; reason: NormalizeResult['reason']; before: string | null; after: string }> = [] + + let cursor: string | null = null + + while (true) { + const rows = await model.findMany({ + select: { + id: true, + imageUrls: true, + previousImageUrls: true, + }, + ...(cursor + ? { + cursor: { id: cursor }, + skip: 1, + } + : {}), + orderBy: { id: 'asc' }, + take: BATCH_SIZE, + }) + + if (rows.length === 0) break + + for (const row of rows) { + stats.scanned += 1 + + const imageUrlsResult = normalizeField(row.imageUrls) + const previousImageUrlsResult = normalizeField(row.previousImageUrls) + + stats.reasons[imageUrlsResult.reason] += 1 + stats.reasons[previousImageUrlsResult.reason] += 1 + + const data: Partial> = {} + + if (imageUrlsResult.changed) { + data.imageUrls = imageUrlsResult.next + stats.changedFields += 1 + if (samples.length < 20) { + samples.push({ + id: row.id, + field: 'imageUrls', + reason: imageUrlsResult.reason, + before: row.imageUrls, + after: imageUrlsResult.next, + }) + } + } + + if (previousImageUrlsResult.changed) { + data.previousImageUrls = previousImageUrlsResult.next + stats.changedFields += 1 + if (samples.length < 20) { + samples.push({ + id: row.id, + field: 'previousImageUrls', + reason: previousImageUrlsResult.reason, + before: row.previousImageUrls, + after: previousImageUrlsResult.next, + }) + } + } + + if (Object.keys(data).length > 0) { + stats.updatedRows += 1 + if (APPLY) { + await model.update({ + where: { id: row.id }, + data, + }) + } + } + } + + cursor = rows[rows.length - 1]?.id || null + } + + const summary = `[migrate-image-urls-contract] ${modelName}: scanned=${stats.scanned} updatedRows=${stats.updatedRows} changedFields=${stats.changedFields}` + _ulogInfo(summary) + print(summary) + print(`[migrate-image-urls-contract] ${modelName}: reasons=${JSON.stringify(stats.reasons)}`) + + if (samples.length > 0) { + print(`[migrate-image-urls-contract] ${modelName}: sampleChanges=${JSON.stringify(samples, null, 2)}`) + } + + return stats +} + +async function main() { + print(`[migrate-image-urls-contract] mode=${APPLY ? 'apply' : 'dry-run'}`) + + const totals = { + scanned: 0, + updatedRows: 0, + changedFields: 0, + } + + for (const target of MODELS) { + const stats = await migrateModel(target.name, target.model) + totals.scanned += stats.scanned + totals.updatedRows += stats.updatedRows + totals.changedFields += stats.changedFields + } + + print(`[migrate-image-urls-contract] done scanned=${totals.scanned} updatedRows=${totals.updatedRows} changedFields=${totals.changedFields} mode=${APPLY ? 'apply' : 'dry-run'}`) +} + +main() + .catch((error) => { + _ulogError('[migrate-image-urls-contract] failed:', error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/migrations/migrate-capability-selections.ts b/scripts/migrations/migrate-capability-selections.ts new file mode 100644 index 0000000..ea0a514 --- /dev/null +++ b/scripts/migrations/migrate-capability-selections.ts @@ -0,0 +1,310 @@ +import { prisma } from '@/lib/prisma' +import { + parseModelKeyStrict, + type CapabilitySelections, + type CapabilityValue, +} from '@/lib/model-config-contract' +import { findBuiltinCapabilities } from '@/lib/model-capabilities/catalog' + +const APPLY = process.argv.includes('--apply') + +const USER_IMAGE_MODEL_FIELDS = [ + 'characterModel', + 'locationModel', + 'storyboardModel', + 'editModel', +] as const + +const PROJECT_IMAGE_MODEL_FIELDS = [ + 'characterModel', + 'locationModel', + 'storyboardModel', + 'editModel', +] as const + +type UserImageModelField = typeof USER_IMAGE_MODEL_FIELDS[number] +type ProjectImageModelField = typeof PROJECT_IMAGE_MODEL_FIELDS[number] + +interface UserPreferenceRow { + id: string + userId: string + imageResolution: string + capabilityDefaults: string | null + characterModel: string | null + locationModel: string | null + storyboardModel: string | null + editModel: string | null +} + +interface ProjectRow { + id: string + projectId: string + imageResolution: string + videoResolution: string + capabilityOverrides: string | null + characterModel: string | null + locationModel: string | null + storyboardModel: string | null + editModel: string | null + videoModel: string | null +} + +interface MigrationSummary { + mode: 'dry-run' | 'apply' + userPreference: { + scanned: number + updated: number + migratedImageResolution: number + } + novelPromotionProject: { + scanned: number + updated: number + migratedImageResolution: number + migratedVideoResolution: number + } +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function isCapabilityValue(value: unknown): value is CapabilityValue { + return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' +} + +function normalizeSelections(raw: unknown): CapabilitySelections { + if (!isRecord(raw)) return {} + + const normalized: CapabilitySelections = {} + for (const [modelKey, rawSelection] of Object.entries(raw)) { + if (!isRecord(rawSelection)) continue + + const nextSelection: Record = {} + for (const [field, value] of Object.entries(rawSelection)) { + if (isCapabilityValue(value)) { + nextSelection[field] = value + } + } + + normalized[modelKey] = nextSelection + } + + return normalized +} + +function parseSelections(raw: string | null): CapabilitySelections { + if (!raw) return {} + try { + return normalizeSelections(JSON.parse(raw) as unknown) + } catch { + return {} + } +} + +function serializeSelections(selections: CapabilitySelections): string | null { + if (Object.keys(selections).length === 0) return null + return JSON.stringify(selections) +} + +function getCapabilityResolutionOptions( + modelType: 'image' | 'video', + modelKey: string, +): string[] { + const parsed = parseModelKeyStrict(modelKey) + if (!parsed) return [] + + const capabilities = findBuiltinCapabilities(modelType, parsed.provider, parsed.modelId) + const namespace = capabilities?.[modelType] + if (!namespace || !isRecord(namespace)) return [] + + const resolutionOptions = namespace.resolutionOptions + if (!Array.isArray(resolutionOptions)) return [] + + return resolutionOptions.filter((item): item is string => typeof item === 'string' && item.trim().length > 0) +} + +function ensureModelResolutionSelection(input: { + modelType: 'image' | 'video' + modelKey: string + resolution: string + selections: CapabilitySelections +}): boolean { + const options = getCapabilityResolutionOptions(input.modelType, input.modelKey) + if (options.length === 0) return false + if (!options.includes(input.resolution)) return false + + const current = input.selections[input.modelKey] + if (current && current.resolution !== undefined) { + return false + } + + input.selections[input.modelKey] = { + ...(current || {}), + resolution: input.resolution, + } + return true +} + +function collectModelKeys( + row: RowType, + fields: readonly (keyof RowType)[], +): string[] { + const modelKeys: string[] = [] + for (const field of fields) { + const value = row[field] + if (typeof value !== 'string') continue + const trimmed = value.trim() + if (!trimmed) continue + modelKeys.push(trimmed) + } + return modelKeys +} + +async function migrateUserPreferences(summary: MigrationSummary) { + const rows = await prisma.userPreference.findMany({ + select: { + id: true, + userId: true, + imageResolution: true, + capabilityDefaults: true, + characterModel: true, + locationModel: true, + storyboardModel: true, + editModel: true, + }, + }) as UserPreferenceRow[] + + summary.userPreference.scanned = rows.length + + for (const row of rows) { + const nextSelections = parseSelections(row.capabilityDefaults) + const modelKeys = collectModelKeys(row, USER_IMAGE_MODEL_FIELDS) + let changed = false + + for (const modelKey of modelKeys) { + if (ensureModelResolutionSelection({ + modelType: 'image', + modelKey, + resolution: row.imageResolution, + selections: nextSelections, + })) { + changed = true + summary.userPreference.migratedImageResolution += 1 + } + } + + if (!changed) continue + summary.userPreference.updated += 1 + + if (APPLY) { + await prisma.userPreference.update({ + where: { id: row.id }, + data: { + capabilityDefaults: serializeSelections(nextSelections), + }, + }) + } + } +} + +async function migrateProjects(summary: MigrationSummary) { + const rows = await prisma.novelPromotionProject.findMany({ + select: { + id: true, + projectId: true, + imageResolution: true, + videoResolution: true, + capabilityOverrides: true, + characterModel: true, + locationModel: true, + storyboardModel: true, + editModel: true, + videoModel: true, + }, + }) as ProjectRow[] + + summary.novelPromotionProject.scanned = rows.length + + for (const row of rows) { + const nextSelections = parseSelections(row.capabilityOverrides) + const imageModelKeys = collectModelKeys(row, PROJECT_IMAGE_MODEL_FIELDS) + let changed = false + + for (const modelKey of imageModelKeys) { + if (ensureModelResolutionSelection({ + modelType: 'image', + modelKey, + resolution: row.imageResolution, + selections: nextSelections, + })) { + changed = true + summary.novelPromotionProject.migratedImageResolution += 1 + } + } + + if (typeof row.videoModel === 'string' && row.videoModel.trim()) { + if (ensureModelResolutionSelection({ + modelType: 'video', + modelKey: row.videoModel.trim(), + resolution: row.videoResolution, + selections: nextSelections, + })) { + changed = true + summary.novelPromotionProject.migratedVideoResolution += 1 + } + } + + if (!changed) continue + summary.novelPromotionProject.updated += 1 + + if (APPLY) { + await prisma.novelPromotionProject.update({ + where: { id: row.id }, + data: { + capabilityOverrides: serializeSelections(nextSelections), + }, + }) + } + } +} + +async function main() { + const summary: MigrationSummary = { + mode: APPLY ? 'apply' : 'dry-run', + userPreference: { + scanned: 0, + updated: 0, + migratedImageResolution: 0, + }, + novelPromotionProject: { + scanned: 0, + updated: 0, + migratedImageResolution: 0, + migratedVideoResolution: 0, + }, + } + + await migrateUserPreferences(summary) + await migrateProjects(summary) + + process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`) +} + +main() + .catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error) + const missingColumn = + message.includes('capabilityDefaults') || message.includes('capabilityOverrides') + if (missingColumn && message.includes('does not exist')) { + process.stderr.write( + '[migrate-capability-selections] FAILED: required DB columns are missing. ' + + 'Apply SQL migration `prisma/migrations/20260215_add_capability_selection_columns.sql` first.\n', + ) + } else { + process.stderr.write(`[migrate-capability-selections] FAILED: ${message}\n`) + } + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/migrations/migrate-model-config-contract.ts b/scripts/migrations/migrate-model-config-contract.ts new file mode 100644 index 0000000..2ef375b --- /dev/null +++ b/scripts/migrations/migrate-model-config-contract.ts @@ -0,0 +1,498 @@ +import fs from 'fs' +import path from 'path' +import { prisma } from '@/lib/prisma' +import { + composeModelKey, + parseModelKeyStrict, + validateModelCapabilities, + type ModelCapabilities, + type UnifiedModelType, +} from '@/lib/model-config-contract' + +type ModelField = + | 'analysisModel' + | 'characterModel' + | 'locationModel' + | 'storyboardModel' + | 'editModel' + | 'videoModel' + +type PreferenceRow = { + id: string + userId: string + customModels: string | null + analysisModel: string | null + characterModel: string | null + locationModel: string | null + storyboardModel: string | null + editModel: string | null + videoModel: string | null +} + +type ProjectRow = { + id: string + projectId: string + analysisModel: string | null + characterModel: string | null + locationModel: string | null + storyboardModel: string | null + editModel: string | null + videoModel: string | null + project: { + userId: string + } +} + +type MigrationIssue = { + table: 'userPreference' | 'novelPromotionProject' + rowId: string + userId?: string + field: string + kind: + | 'CUSTOM_MODELS_JSON_INVALID' + | 'MODEL_SHAPE_INVALID' + | 'MODEL_TYPE_INVALID' + | 'MODEL_KEY_INCOMPLETE' + | 'MODEL_KEY_MISMATCH' + | 'MODEL_CAPABILITY_INVALID' + | 'LEGACY_MODEL_ID_NOT_FOUND' + | 'LEGACY_MODEL_ID_AMBIGUOUS' + rawValue?: string | null + candidates?: string[] + message: string +} + +type MigrationReport = { + generatedAt: string + mode: 'dry-run' | 'apply' + userPreference: { + scanned: number + updated: number + updatedCustomModels: number + updatedDefaultFields: number + } + novelPromotionProject: { + scanned: number + updated: number + updatedFields: number + } + issues: MigrationIssue[] +} + +type NormalizedModel = { + provider: string + modelId: string + modelKey: string + name: string + type: UnifiedModelType + price: number + resolution?: '2K' | '4K' + capabilities?: ModelCapabilities +} + +const APPLY = process.argv.includes('--apply') +const MAX_ISSUES = 500 +const MODEL_FIELDS: readonly ModelField[] = [ + 'analysisModel', + 'characterModel', + 'locationModel', + 'storyboardModel', + 'editModel', + 'videoModel', +] + +const LEGACY_MODEL_ID_MAP = new Map([ + ['anthropic/claude-sonnet-4.5', 'openrouter::anthropic/claude-sonnet-4.5'], + ['google/gemini-3-pro-preview', 'openrouter::google/gemini-3-pro-preview'], + ['openai/gpt-5.2', 'openrouter::openai/gpt-5.2'], + ['banana', 'fal::banana'], + ['banana-2k', 'fal::banana'], + ['seedream', 'ark::doubao-seedream-4-0-250828'], + ['seedream4.5', 'ark::doubao-seedream-4-5-251128'], + ['gemini-3-pro-image-preview', 'google::gemini-3-pro-image-preview'], + ['gemini-3-pro-image-preview-batch', 'google::gemini-3-pro-image-preview-batch'], + ['nano-banana-pro', 'google::gemini-3-pro-image-preview'], + ['gemini-3.0-pro-image-portrait', 'flow2api::gemini-3.0-pro-image-portrait'], + ['imagen-4.0-ultra-generate-001', 'google::imagen-4.0-ultra-generate-001'], + ['doubao-seedance-1-0-pro-250528', 'ark::doubao-seedance-1-0-pro-250528'], + ['doubao-seedance-1-0-pro-fast-251015', 'ark::doubao-seedance-1-0-pro-fast-251015'], + ['doubao-seedance-1-0-pro-fast-251015-batch', 'ark::doubao-seedance-1-0-pro-fast-251015-batch'], +]) + +function parseReportPathArg(): string { + const flagPrefix = '--report=' + const inline = process.argv.find((arg) => arg.startsWith(flagPrefix)) + if (inline) return inline.slice(flagPrefix.length) + const flagIndex = process.argv.findIndex((arg) => arg === '--report') + if (flagIndex !== -1 && process.argv[flagIndex + 1]) { + return process.argv[flagIndex + 1] + } + return 'scripts/migrations/reports/model-config-migration-report.json' +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function toTrimmedString(value: unknown): string { + return typeof value === 'string' ? value.trim() : '' +} + +function isUnifiedModelType(value: unknown): value is UnifiedModelType { + return value === 'llm' + || value === 'image' + || value === 'video' + || value === 'audio' + || value === 'lipsync' +} + +function stableStringify(value: unknown): string { + return JSON.stringify(value) +} + +function parseCustomModels(raw: string | null): { ok: true; value: unknown[] } | { ok: false } { + if (!raw) return { ok: true, value: [] } + try { + const parsed = JSON.parse(raw) as unknown + if (!Array.isArray(parsed)) return { ok: false } + return { ok: true, value: parsed } + } catch { + return { ok: false } + } +} + +function normalizeModel( + raw: unknown, +): { normalized: NormalizedModel | null; changed: boolean; issue?: Omit } { + if (!isRecord(raw)) { + return { + normalized: null, + changed: false, + issue: { + field: 'customModels', + kind: 'MODEL_SHAPE_INVALID', + message: 'customModels item must be object', + }, + } + } + + const modelType = raw.type + if (!isUnifiedModelType(modelType)) { + return { + normalized: null, + changed: false, + issue: { + field: 'customModels.type', + kind: 'MODEL_TYPE_INVALID', + rawValue: String(raw.type ?? ''), + message: 'custom model type must be llm/image/video/audio/lipsync', + }, + } + } + + const providerField = toTrimmedString(raw.provider) + const modelIdField = toTrimmedString(raw.modelId) + const parsedModelKey = parseModelKeyStrict(toTrimmedString(raw.modelKey)) + + const provider = providerField || parsedModelKey?.provider || '' + const modelId = modelIdField || parsedModelKey?.modelId || '' + const modelKey = composeModelKey(provider, modelId) + if (!modelKey) { + return { + normalized: null, + changed: false, + issue: { + field: 'customModels.modelKey', + kind: 'MODEL_KEY_INCOMPLETE', + rawValue: toTrimmedString(raw.modelKey), + message: 'provider/modelId/modelKey cannot compose a valid model_key', + }, + } + } + + if (parsedModelKey && parsedModelKey.modelKey !== modelKey) { + return { + normalized: null, + changed: false, + issue: { + field: 'customModels.modelKey', + kind: 'MODEL_KEY_MISMATCH', + rawValue: toTrimmedString(raw.modelKey), + message: 'modelKey conflicts with provider/modelId', + }, + } + } + + const rawResolution = toTrimmedString(raw.resolution) + const resolution = rawResolution === '2K' || rawResolution === '4K' ? rawResolution : undefined + const capabilities = isRecord(raw.capabilities) + ? ({ ...(raw.capabilities as ModelCapabilities) }) + : undefined + const capabilityIssues = validateModelCapabilities(modelType, capabilities) + if (capabilityIssues.length > 0) { + const firstIssue = capabilityIssues[0] + return { + normalized: null, + changed: false, + issue: { + field: firstIssue.field, + kind: 'MODEL_CAPABILITY_INVALID', + message: `${firstIssue.code}: ${firstIssue.message}`, + }, + } + } + + const name = toTrimmedString(raw.name) || modelId + const price = typeof raw.price === 'number' && Number.isFinite(raw.price) ? raw.price : 0 + + const normalized: NormalizedModel = { + provider, + modelId, + modelKey, + name, + type: modelType, + price, + ...(resolution ? { resolution } : {}), + ...(capabilities ? { capabilities } : {}), + } + + const changed = stableStringify(raw) !== stableStringify(normalized) + return { normalized, changed } +} + +function addIssue(report: MigrationReport, issue: MigrationIssue) { + if (report.issues.length >= MAX_ISSUES) return + report.issues.push(issue) +} + +function normalizeModelFieldValue( + rawValue: string | null, + field: ModelField, + mappingByModelId: Map, +): { nextValue: string | null; changed: boolean; issue?: Omit } { + if (!rawValue || !rawValue.trim()) return { nextValue: null, changed: rawValue !== null } + const trimmed = rawValue.trim() + const parsed = parseModelKeyStrict(trimmed) + if (parsed) { + return { nextValue: parsed.modelKey, changed: parsed.modelKey !== rawValue } + } + + const candidates = mappingByModelId.get(trimmed) || [] + if (candidates.length === 1) { + return { nextValue: candidates[0], changed: candidates[0] !== rawValue } + } + if (candidates.length === 0) { + const mappedModelKey = LEGACY_MODEL_ID_MAP.get(trimmed) + if (mappedModelKey) { + return { nextValue: mappedModelKey, changed: mappedModelKey !== rawValue } + } + } + if (candidates.length === 0) { + return { + nextValue: rawValue, + changed: false, + issue: { + field, + kind: 'LEGACY_MODEL_ID_NOT_FOUND', + rawValue, + message: `${field} legacy modelId cannot be mapped`, + }, + } + } + return { + nextValue: rawValue, + changed: false, + issue: { + field, + kind: 'LEGACY_MODEL_ID_AMBIGUOUS', + rawValue, + candidates, + message: `${field} legacy modelId maps to multiple providers`, + }, + } +} + +async function main() { + const reportPath = parseReportPathArg() + const report: MigrationReport = { + generatedAt: new Date().toISOString(), + mode: APPLY ? 'apply' : 'dry-run', + userPreference: { + scanned: 0, + updated: 0, + updatedCustomModels: 0, + updatedDefaultFields: 0, + }, + novelPromotionProject: { + scanned: 0, + updated: 0, + updatedFields: 0, + }, + issues: [], + } + + const userPrefs = await prisma.userPreference.findMany({ + select: { + id: true, + userId: true, + customModels: true, + analysisModel: true, + characterModel: true, + locationModel: true, + storyboardModel: true, + editModel: true, + videoModel: true, + }, + }) + + const userMappings = new Map>() + + for (const pref of userPrefs) { + report.userPreference.scanned += 1 + const updateData: Partial> = {} + + const parsedCustomModels = parseCustomModels(pref.customModels) + const normalizedModels: NormalizedModel[] = [] + let customModelsChanged = false + + if (!parsedCustomModels.ok) { + addIssue(report, { + table: 'userPreference', + rowId: pref.id, + userId: pref.userId, + field: 'customModels', + kind: 'CUSTOM_MODELS_JSON_INVALID', + rawValue: pref.customModels, + message: 'customModels JSON is invalid', + }) + } else { + for (let index = 0; index < parsedCustomModels.value.length; index += 1) { + const normalizedResult = normalizeModel(parsedCustomModels.value[index]) + if (normalizedResult.issue) { + addIssue(report, { + table: 'userPreference', + rowId: pref.id, + userId: pref.userId, + ...normalizedResult.issue, + }) + continue + } + if (normalizedResult.normalized) { + normalizedModels.push(normalizedResult.normalized) + if (normalizedResult.changed) customModelsChanged = true + } + } + } + + const mappingByModelId = new Map() + for (const model of normalizedModels) { + const existing = mappingByModelId.get(model.modelId) || [] + if (!existing.includes(model.modelKey)) existing.push(model.modelKey) + mappingByModelId.set(model.modelId, existing) + } + userMappings.set(pref.userId, mappingByModelId) + + if (customModelsChanged) { + updateData.customModels = JSON.stringify(normalizedModels) + report.userPreference.updatedCustomModels += 1 + } + + for (const field of MODEL_FIELDS) { + const normalizedField = normalizeModelFieldValue(pref[field], field, mappingByModelId) + if (normalizedField.issue) { + addIssue(report, { + table: 'userPreference', + rowId: pref.id, + userId: pref.userId, + ...normalizedField.issue, + }) + } + if (normalizedField.changed) { + updateData[field] = normalizedField.nextValue + report.userPreference.updatedDefaultFields += 1 + } + } + + if (Object.keys(updateData).length > 0) { + report.userPreference.updated += 1 + if (APPLY) { + await prisma.userPreference.update({ + where: { id: pref.id }, + data: updateData, + }) + } + } + } + + const projects = await prisma.novelPromotionProject.findMany({ + select: { + id: true, + projectId: true, + analysisModel: true, + characterModel: true, + locationModel: true, + storyboardModel: true, + editModel: true, + videoModel: true, + project: { + select: { + userId: true, + }, + }, + }, + }) + + for (const row of projects as ProjectRow[]) { + report.novelPromotionProject.scanned += 1 + const mappingByModelId = userMappings.get(row.project.userId) || new Map() + const updateData: Partial> = {} + + for (const field of MODEL_FIELDS) { + const normalizedField = normalizeModelFieldValue(row[field], field, mappingByModelId) + if (normalizedField.issue) { + addIssue(report, { + table: 'novelPromotionProject', + rowId: row.id, + userId: row.project.userId, + ...normalizedField.issue, + }) + } + if (normalizedField.changed) { + updateData[field] = normalizedField.nextValue + report.novelPromotionProject.updatedFields += 1 + } + } + + if (Object.keys(updateData).length > 0) { + report.novelPromotionProject.updated += 1 + if (APPLY) { + await prisma.novelPromotionProject.update({ + where: { id: row.id }, + data: updateData, + }) + } + } + } + + const absoluteReportPath = path.isAbsolute(reportPath) + ? reportPath + : path.join(process.cwd(), reportPath) + fs.mkdirSync(path.dirname(absoluteReportPath), { recursive: true }) + fs.writeFileSync(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8') + + process.stdout.write( + `[migrate-model-config-contract] mode=${report.mode} ` + + `prefs=${report.userPreference.scanned}/${report.userPreference.updated} ` + + `projects=${report.novelPromotionProject.scanned}/${report.novelPromotionProject.updated} ` + + `issues=${report.issues.length} report=${absoluteReportPath}\n`, + ) +} + +main() + .catch((error) => { + process.stderr.write(`[migrate-model-config-contract] failed: ${String(error)}\n`) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/migrations/reports/model-config-migration-report.apply.json b/scripts/migrations/reports/model-config-migration-report.apply.json new file mode 100644 index 0000000..a3c3733 --- /dev/null +++ b/scripts/migrations/reports/model-config-migration-report.apply.json @@ -0,0 +1,1187 @@ +{ + "generatedAt": "2026-02-12T12:51:54.103Z", + "mode": "apply", + "userPreference": { + "scanned": 7, + "updated": 1, + "updatedCustomModels": 1, + "updatedDefaultFields": 2 + }, + "novelPromotionProject": { + "scanned": 70, + "updated": 65, + "updatedFields": 189 + }, + "issues": [ + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "anthropic/claude-sonnet-4.5", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "anthropic/claude-sonnet-4.5", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0574a234-e128-4196-a103-1fa8305f3ca0", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0574a234-e128-4196-a103-1fa8305f3ca0", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0a235755-3a90-44b1-813d-d00d163662b9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0a235755-3a90-44b1-813d-d00d163662b9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0a235755-3a90-44b1-813d-d00d163662b9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "155e9723-b2ef-414f-8c5e-f9e1e34de536", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "1979ed4c-0d16-4696-a9ea-4a15e0c4cd0f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "1979ed4c-0d16-4696-a9ea-4a15e0c4cd0f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "1a296d18-c305-4639-9b11-3c694cd0718d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "1a296d18-c305-4639-9b11-3c694cd0718d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "2f898804-cda1-417a-9f58-6d19fa0f134d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3.0-pro-image-portrait", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "374c975a-8223-453e-81a4-d110c7e4b685", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "374c975a-8223-453e-81a4-d110c7e4b685", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "374c975a-8223-453e-81a4-d110c7e4b685", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "49909329-a771-4b18-a305-ff7d284bcf4e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "49909329-a771-4b18-a305-ff7d284bcf4e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "49909329-a771-4b18-a305-ff7d284bcf4e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "49909329-a771-4b18-a305-ff7d284bcf4e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "4f0eaccc-5fe4-4440-bcc0-be70892626c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "4f0eaccc-5fe4-4440-bcc0-be70892626c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "55e56c35-f59f-4d32-9e99-d67b46846870", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "55e56c35-f59f-4d32-9e99-d67b46846870", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5cdef710-6aef-445a-b217-2cdb6987a68a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5cdef710-6aef-445a-b217-2cdb6987a68a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5cdef710-6aef-445a-b217-2cdb6987a68a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5cdef710-6aef-445a-b217-2cdb6987a68a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5d97db15-8e16-45dc-8fa7-5efa5a237a99", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5d97db15-8e16-45dc-8fa7-5efa5a237a99", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "673acab3-9d09-4b1f-87cc-9cafbe60af80", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "673acab3-9d09-4b1f-87cc-9cafbe60af80", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "anthropic/claude-sonnet-4.5", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "77b44773-1264-419f-b145-4c2a9cbbd977", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "77b44773-1264-419f-b145-4c2a9cbbd977", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "77b44773-1264-419f-b145-4c2a9cbbd977", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "77b44773-1264-419f-b145-4c2a9cbbd977", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "788c5fc0-e3b6-48dd-a2e5-719069726a08", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "788c5fc0-e3b6-48dd-a2e5-719069726a08", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "811a2cde-2a8e-4934-947b-18bf9d6331e0", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana-2k", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "88dbc942-e549-48f8-808b-53e57fbdb97f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "88dbc942-e549-48f8-808b-53e57fbdb97f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "8ef54b9c-0142-49ad-a9cd-ec94c5132731", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "8ef54b9c-0142-49ad-a9cd-ec94c5132731", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "8ef54b9c-0142-49ad-a9cd-ec94c5132731", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "9b48f96d-7d46-4264-abfd-82ee338eecd7", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b1013460-0798-43f2-87bd-7b018cca1d58", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana-2k", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b8740c4d-0694-4106-9831-db665225a0c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b8740c4d-0694-4106-9831-db665225a0c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b8740c4d-0694-4106-9831-db665225a0c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b9f3f933-13d6-461a-bb80-1e94b48c855d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b9f3f933-13d6-461a-bb80-1e94b48c855d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b9f3f933-13d6-461a-bb80-1e94b48c855d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b9f3f933-13d6-461a-bb80-1e94b48c855d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bcf736cd-aeb9-49ce-bb36-d878400bbfd6", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bcf736cd-aeb9-49ce-bb36-d878400bbfd6", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bcf736cd-aeb9-49ce-bb36-d878400bbfd6", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bcf736cd-aeb9-49ce-bb36-d878400bbfd6", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c38b1c73-9baa-4a03-86b7-5f6dbecc0e1e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c38b1c73-9baa-4a03-86b7-5f6dbecc0e1e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ca374b6b-5666-4940-a859-23761fbacd62", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "openai/gpt-5.2", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d1bbeef0-94d5-4247-b90f-105b432649e1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d1bbeef0-94d5-4247-b90f-105b432649e1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d2c7979d-839b-4baa-bbf0-c726f2cc09bb", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "imagen-4.0-ultra-generate-001", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d2c7979d-839b-4baa-bbf0-c726f2cc09bb", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d2c7979d-839b-4baa-bbf0-c726f2cc09bb", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "dc0669ec-effc-4626-a038-bbf48994d65a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "dc0669ec-effc-4626-a038-bbf48994d65a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "dc0669ec-effc-4626-a038-bbf48994d65a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "de428a7c-e719-4cda-874a-7c54878a14bc", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana-2k", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ec2526c0-1acd-4081-b13d-019d5d425710", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ec2526c0-1acd-4081-b13d-019d5d425710", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ec2526c0-1acd-4081-b13d-019d5d425710", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ed2d93ad-5cfa-4bd0-b373-a65aae679ef5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ee5e7440-c825-4c0c-b903-0a51d983db6f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ee5e7440-c825-4c0c-b903-0a51d983db6f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f36d983d-ba52-4849-bcda-0d710b2e04f9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "nano-banana-pro", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f36d983d-ba52-4849-bcda-0d710b2e04f9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "nano-banana-pro", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f45efd24-37b1-4ca6-8808-0b5c9de205f1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f45efd24-37b1-4ca6-8808-0b5c9de205f1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f45efd24-37b1-4ca6-8808-0b5c9de205f1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ff9129c7-f76f-4ab0-8fae-84be89b62390", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ff9129c7-f76f-4ab0-8fae-84be89b62390", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ff9129c7-f76f-4ab0-8fae-84be89b62390", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + } + ] +} diff --git a/scripts/migrations/reports/model-config-migration-report.post-alias-apply.json b/scripts/migrations/reports/model-config-migration-report.post-alias-apply.json new file mode 100644 index 0000000..11c8dc8 --- /dev/null +++ b/scripts/migrations/reports/model-config-migration-report.post-alias-apply.json @@ -0,0 +1,16 @@ +{ + "generatedAt": "2026-02-12T12:53:18.381Z", + "mode": "apply", + "userPreference": { + "scanned": 7, + "updated": 4, + "updatedCustomModels": 0, + "updatedDefaultFields": 24 + }, + "novelPromotionProject": { + "scanned": 70, + "updated": 40, + "updatedFields": 106 + }, + "issues": [] +} diff --git a/scripts/migrations/reports/model-config-migration-report.post-alias-dryrun.json b/scripts/migrations/reports/model-config-migration-report.post-alias-dryrun.json new file mode 100644 index 0000000..df431ee --- /dev/null +++ b/scripts/migrations/reports/model-config-migration-report.post-alias-dryrun.json @@ -0,0 +1,16 @@ +{ + "generatedAt": "2026-02-12T12:53:12.288Z", + "mode": "dry-run", + "userPreference": { + "scanned": 7, + "updated": 4, + "updatedCustomModels": 0, + "updatedDefaultFields": 24 + }, + "novelPromotionProject": { + "scanned": 70, + "updated": 40, + "updatedFields": 106 + }, + "issues": [] +} diff --git a/scripts/migrations/reports/model-config-migration-report.pre-apply.json b/scripts/migrations/reports/model-config-migration-report.pre-apply.json new file mode 100644 index 0000000..9ece5bc --- /dev/null +++ b/scripts/migrations/reports/model-config-migration-report.pre-apply.json @@ -0,0 +1,1187 @@ +{ + "generatedAt": "2026-02-12T12:51:46.934Z", + "mode": "dry-run", + "userPreference": { + "scanned": 7, + "updated": 1, + "updatedCustomModels": 1, + "updatedDefaultFields": 2 + }, + "novelPromotionProject": { + "scanned": 70, + "updated": 65, + "updatedFields": 189 + }, + "issues": [ + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "anthropic/claude-sonnet-4.5", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "0c3ddcbd-d6f7-4b57-90d0-3c2fe0d48c5c", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "4c73593e-148d-4707-a2c2-087d2fd43a68", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "b33bc1c7-0ba8-475e-87e0-ba6ef897c2d2", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "userPreference", + "rowId": "bb2a2aa3-29f9-4f85-9e93-6363faf17dc4", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "anthropic/claude-sonnet-4.5", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "04ebb651-dc90-4c69-99d4-343dd037a9b9", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0574a234-e128-4196-a103-1fa8305f3ca0", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0574a234-e128-4196-a103-1fa8305f3ca0", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0a235755-3a90-44b1-813d-d00d163662b9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0a235755-3a90-44b1-813d-d00d163662b9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "0a235755-3a90-44b1-813d-d00d163662b9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "155e9723-b2ef-414f-8c5e-f9e1e34de536", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "1979ed4c-0d16-4696-a9ea-4a15e0c4cd0f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "1979ed4c-0d16-4696-a9ea-4a15e0c4cd0f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "1a296d18-c305-4639-9b11-3c694cd0718d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "1a296d18-c305-4639-9b11-3c694cd0718d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "2f898804-cda1-417a-9f58-6d19fa0f134d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3.0-pro-image-portrait", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "374c975a-8223-453e-81a4-d110c7e4b685", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "374c975a-8223-453e-81a4-d110c7e4b685", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "374c975a-8223-453e-81a4-d110c7e4b685", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "49909329-a771-4b18-a305-ff7d284bcf4e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "49909329-a771-4b18-a305-ff7d284bcf4e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "49909329-a771-4b18-a305-ff7d284bcf4e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "49909329-a771-4b18-a305-ff7d284bcf4e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "4f0eaccc-5fe4-4440-bcc0-be70892626c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "4f0eaccc-5fe4-4440-bcc0-be70892626c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "55e56c35-f59f-4d32-9e99-d67b46846870", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "55e56c35-f59f-4d32-9e99-d67b46846870", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5cdef710-6aef-445a-b217-2cdb6987a68a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5cdef710-6aef-445a-b217-2cdb6987a68a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5cdef710-6aef-445a-b217-2cdb6987a68a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5cdef710-6aef-445a-b217-2cdb6987a68a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5d97db15-8e16-45dc-8fa7-5efa5a237a99", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "5d97db15-8e16-45dc-8fa7-5efa5a237a99", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "673acab3-9d09-4b1f-87cc-9cafbe60af80", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "673acab3-9d09-4b1f-87cc-9cafbe60af80", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "anthropic/claude-sonnet-4.5", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "6bbb564a-14bd-4bc2-83e7-1a8e8612eb8e", + "userId": "edb99912-94ce-403d-97bb-2a29d5d20cb6", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "77b44773-1264-419f-b145-4c2a9cbbd977", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "77b44773-1264-419f-b145-4c2a9cbbd977", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "77b44773-1264-419f-b145-4c2a9cbbd977", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "77b44773-1264-419f-b145-4c2a9cbbd977", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "788c5fc0-e3b6-48dd-a2e5-719069726a08", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "788c5fc0-e3b6-48dd-a2e5-719069726a08", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "811a2cde-2a8e-4934-947b-18bf9d6331e0", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana-2k", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "88dbc942-e549-48f8-808b-53e57fbdb97f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "88dbc942-e549-48f8-808b-53e57fbdb97f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "8ef54b9c-0142-49ad-a9cd-ec94c5132731", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "8ef54b9c-0142-49ad-a9cd-ec94c5132731", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "8ef54b9c-0142-49ad-a9cd-ec94c5132731", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "9b48f96d-7d46-4264-abfd-82ee338eecd7", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b1013460-0798-43f2-87bd-7b018cca1d58", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana-2k", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b8740c4d-0694-4106-9831-db665225a0c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b8740c4d-0694-4106-9831-db665225a0c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b8740c4d-0694-4106-9831-db665225a0c5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b9f3f933-13d6-461a-bb80-1e94b48c855d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b9f3f933-13d6-461a-bb80-1e94b48c855d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b9f3f933-13d6-461a-bb80-1e94b48c855d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "b9f3f933-13d6-461a-bb80-1e94b48c855d", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bc31aa21-9eec-47d2-9853-e4b34d926b6a", + "userId": "5a28aff0-e370-4337-acf6-0f8ce86f346a", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bcf736cd-aeb9-49ce-bb36-d878400bbfd6", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bcf736cd-aeb9-49ce-bb36-d878400bbfd6", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bcf736cd-aeb9-49ce-bb36-d878400bbfd6", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "bcf736cd-aeb9-49ce-bb36-d878400bbfd6", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c38b1c73-9baa-4a03-86b7-5f6dbecc0e1e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c38b1c73-9baa-4a03-86b7-5f6dbecc0e1e", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "c61f1a58-42e8-4989-968b-5a373ae77e1a", + "userId": "bea0c5b7-73f0-4048-8cb9-e6ef18650a0f", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ca374b6b-5666-4940-a859-23761fbacd62", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "openai/gpt-5.2", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d1bbeef0-94d5-4247-b90f-105b432649e1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d1bbeef0-94d5-4247-b90f-105b432649e1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d2c7979d-839b-4baa-bbf0-c726f2cc09bb", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "imagen-4.0-ultra-generate-001", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d2c7979d-839b-4baa-bbf0-c726f2cc09bb", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "d2c7979d-839b-4baa-bbf0-c726f2cc09bb", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "dc0669ec-effc-4626-a038-bbf48994d65a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "dc0669ec-effc-4626-a038-bbf48994d65a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "dc0669ec-effc-4626-a038-bbf48994d65a", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-fast-251015-batch", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "de428a7c-e719-4cda-874a-7c54878a14bc", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana-2k", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ec2526c0-1acd-4081-b13d-019d5d425710", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ec2526c0-1acd-4081-b13d-019d5d425710", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ec2526c0-1acd-4081-b13d-019d5d425710", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ed2d93ad-5cfa-4bd0-b373-a65aae679ef5", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ee5e7440-c825-4c0c-b903-0a51d983db6f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ee5e7440-c825-4c0c-b903-0a51d983db6f", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f36d983d-ba52-4849-bcda-0d710b2e04f9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "nano-banana-pro", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f36d983d-ba52-4849-bcda-0d710b2e04f9", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "editModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "nano-banana-pro", + "message": "editModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "analysisModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "google/gemini-3-pro-preview", + "message": "analysisModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "banana", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f38e00d3-09b0-47a5-8caf-71d491bae4d1", + "userId": "91c9fe74-556d-4036-a2a0-ebd3336fd8d8", + "field": "videoModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "doubao-seedance-1-0-pro-250528", + "message": "videoModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f45efd24-37b1-4ca6-8808-0b5c9de205f1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f45efd24-37b1-4ca6-8808-0b5c9de205f1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "f45efd24-37b1-4ca6-8808-0b5c9de205f1", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ff9129c7-f76f-4ab0-8fae-84be89b62390", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "characterModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "characterModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ff9129c7-f76f-4ab0-8fae-84be89b62390", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "locationModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "seedream4.5", + "message": "locationModel legacy modelId cannot be mapped" + }, + { + "table": "novelPromotionProject", + "rowId": "ff9129c7-f76f-4ab0-8fae-84be89b62390", + "userId": "3d84c341-87d7-4165-971d-a3f6c576aa21", + "field": "storyboardModel", + "kind": "LEGACY_MODEL_ID_NOT_FOUND", + "rawValue": "gemini-3-pro-image-preview-batch", + "message": "storyboardModel legacy modelId cannot be mapped" + } + ] +} diff --git a/scripts/publish-opensource.sh b/scripts/publish-opensource.sh new file mode 100644 index 0000000..4f32fd2 --- /dev/null +++ b/scripts/publish-opensource.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# ============================================================ +# 开源版本发布脚本 (orphan branch 方式,无 git 历史) +# 用法: bash scripts/publish-opensource.sh +# ============================================================ + +set -e + +echo "" +echo "🚀 开始发布开源版本..." + +# 确保当前在 main 分支,且工作区干净 +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$CURRENT_BRANCH" != "main" ]; then + echo "❌ 请先切换到 main 分支再运行发布脚本" + exit 1 +fi + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "❌ 工作区有未提交的改动,请先 commit 再发布" + exit 1 +fi + +# 1. 创建无历史的孤儿分支 +echo "📦 创建干净的孤儿分支..." +git checkout --orphan release-public + +# 2. 暂存所有文件(.gitignore 自动排除 logs、data 等) +git add -A + +# 3. 从提交中移除不应公开的内容 +echo "🧹 清理私有内容..." +git rm --cached .env -f 2>/dev/null || true # 本地 env(含真实配置) +git rm -r --cached .github/workflows/ 2>/dev/null || true # CI 流水线(不对外) +git rm -r --cached .agent/ 2>/dev/null || true # AI 工具目录 +git rm -r --cached .artifacts/ 2>/dev/null || true # AI 工具数据 +git rm -r --cached .shared/ 2>/dev/null || true # AI 工具数据 + +# 4. 提交快照 +TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') +git commit -m "release: opensource snapshot $TIMESTAMP" +echo "✅ 快照 commit 已创建" + +# 5. 强推到公开仓库的 main 分支 +echo "⬆️ 推送到公开仓库..." +git push public release-public:main --force + +echo "" +echo "==============================================" +echo "✅ 开源版本发布成功!" +echo "🔗 https://github.com/saturndec/waoowaoo" +echo "==============================================" +echo "" + +# 6. 切回 main 分支,删除临时孤儿分支 +git checkout main +git branch -D release-public + +echo "🔙 已切回 main 分支,孤儿分支已清理" +echo "" diff --git a/scripts/task-error-stats.ts b/scripts/task-error-stats.ts new file mode 100644 index 0000000..c188af8 --- /dev/null +++ b/scripts/task-error-stats.ts @@ -0,0 +1,53 @@ +import { logInfo as _ulogInfo, logError as _ulogError } from '@/lib/logging/core' +import { prisma } from '@/lib/prisma' + +function parseMinutesArg() { + const raw = process.argv.find((arg) => arg.startsWith('--minutes=')) + const value = raw ? Number.parseInt(raw.split('=')[1], 10) : 5 + return Number.isFinite(value) && value > 0 ? value : 5 +} + +async function main() { + const minutes = parseMinutesArg() + const since = new Date(Date.now() - minutes * 60_000) + + const rows = await prisma.task.groupBy({ + by: ['errorCode'], + where: { + status: 'failed', + finishedAt: { gte: since }, + }, + _count: { + _all: true, + }, + orderBy: { + _count: { + errorCode: 'desc', + }, + }, + }) + + const total = rows.reduce((sum: number, row) => sum + (row._count?._all || 0), 0) + + _ulogInfo(`[TaskErrorStats] window=${minutes}m failed_total=${total}`) + if (!rows.length) { + _ulogInfo('No failed tasks in the selected window.') + return + } + + for (const row of rows) { + const code = row.errorCode || 'UNKNOWN' + const count = row?._count?._all || 0 + const ratio = total > 0 ? ((count / total) * 100).toFixed(1) : '0.0' + _ulogInfo(`${code}\t${count}\t${ratio}%`) + } +} + +main() + .catch((error) => { + _ulogError('[TaskErrorStats] failed:', error?.message || error) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/scripts/test-regression-runner.sh b/scripts/test-regression-runner.sh new file mode 100644 index 0000000..0aa4bad --- /dev/null +++ b/scripts/test-regression-runner.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -eq 0 ]; then + echo "[regression-runner] missing command" + exit 2 +fi + +LOG_FILE="$(mktemp -t regression-runner.XXXXXX.log)" + +set +e +"$@" 2>&1 | tee "$LOG_FILE" +CMD_STATUS=${PIPESTATUS[0]} +set -e + +if [ "$CMD_STATUS" -ne 0 ]; then + echo + echo "[regression-runner] regression failed, collecting diagnostics..." + + FAILED_FILES="$(grep -E '^ FAIL ' "$LOG_FILE" | sed -E 's/^ FAIL ([^ ]+).*/\1/' | sort -u || true)" + if [ -z "$FAILED_FILES" ]; then + echo "[regression-runner] no explicit FAIL file lines found in output" + else + echo "[regression-runner] failed files:" + while IFS= read -r file; do + [ -z "$file" ] && continue + echo " - $file" + LAST_COMMIT="$(git log -n 1 --format='%h %ad %an %s' --date=short -- "$file" || true)" + FIRST_COMMIT="$(git log --diff-filter=A --follow --format='%h %ad %an %s' --date=short -- "$file" | tail -n 1 || true)" + if [ -n "$LAST_COMMIT" ]; then + echo " latest: $LAST_COMMIT" + fi + if [ -n "$FIRST_COMMIT" ]; then + echo " first: $FIRST_COMMIT" + fi + done <<< "$FAILED_FILES" + fi +fi + +rm -f "$LOG_FILE" +exit "$CMD_STATUS" diff --git a/scripts/tmp-cleanup-project-models.mjs b/scripts/tmp-cleanup-project-models.mjs new file mode 100644 index 0000000..3c4d7f2 --- /dev/null +++ b/scripts/tmp-cleanup-project-models.mjs @@ -0,0 +1,41 @@ +import { PrismaClient } from '@prisma/client'; +const p = new PrismaClient(); +setTimeout(() => { console.error('TIMEOUT'); process.exit(1); }, 30000); + +const userId = '3d84c341-87d7-4165-971d-a3f6c576aa21'; +const needle = 'gemini-compatible:5b127c32-136e-4e5a-af74-8bae3e28be7a'; +const modelFields = ['characterModel', 'locationModel', 'storyboardModel', 'editModel']; + +// novelPromotionData is a relation, query directly +const npProjects = await p.novelPromotionProject.findMany({ + where: { project: { userId } }, + select: { id: true, projectId: true, characterModel: true, locationModel: true, storyboardModel: true, editModel: true, project: { select: { name: true } } } +}); + +let totalCleaned = 0; + +for (const np of npProjects) { + const updates = {}; + const cleanedFields = []; + + for (const field of modelFields) { + if (typeof np[field] === 'string' && np[field].includes(needle)) { + updates[field] = ''; + cleanedFields.push(`${field}: ${np[field]}`); + } + } + + if (cleanedFields.length > 0) { + await p.novelPromotionProject.update({ + where: { id: np.id }, + data: updates + }); + console.log(`✓ ${np.project.name} (${np.projectId}): cleared ${cleanedFields.length} fields`); + cleanedFields.forEach(f => console.log(` - ${f}`)); + totalCleaned++; + } +} + +console.log(`\nDone. Cleaned ${totalCleaned} projects.`); +await p.$disconnect(); +process.exit(0); diff --git a/scripts/tmp-find-old-model.mjs b/scripts/tmp-find-old-model.mjs new file mode 100644 index 0000000..c7cd735 --- /dev/null +++ b/scripts/tmp-find-old-model.mjs @@ -0,0 +1,43 @@ +import { PrismaClient } from '@prisma/client'; +const p = new PrismaClient(); +setTimeout(() => { console.error('TIMEOUT'); process.exit(1); }, 15000); + +const userId = '3d84c341-87d7-4165-971d-a3f6c576aa21'; +const needle = 'gemini-compatible:5b'; + +// 1. Check userPreference default models +const pref = await p.userPreference.findUnique({ + where: { userId }, + select: { analysisModel: true, characterModel: true, locationModel: true, storyboardModel: true, editModel: true, videoModel: true } +}); +console.log('=== UserPreference defaults ==='); +let found = false; +for (const [k, v] of Object.entries(pref || {})) { + if (typeof v === 'string' && v.includes(needle)) { + console.log(' FOUND in', k, ':', v); + found = true; + } +} +if (!found) console.log(' (clean)'); + +// 2. Check novelPromotionData JSON for any reference +const projects = await p.project.findMany({ + where: { userId }, + select: { id: true, name: true, novelPromotionData: true } +}); +console.log('\n=== Project novelPromotionData ==='); +for (const proj of projects) { + const data = JSON.stringify(proj.novelPromotionData || {}); + if (data.includes(needle)) { + // Find which keys reference it + const parsed = proj.novelPromotionData; + for (const [k, v] of Object.entries(parsed || {})) { + if (typeof v === 'string' && v.includes(needle)) { + console.log(' FOUND in project', proj.id, '(' + proj.name + ') field:', k, '=', v); + } + } + } +} + +await p.$disconnect(); +process.exit(0); diff --git a/scripts/watchdog.ts b/scripts/watchdog.ts new file mode 100644 index 0000000..593daf3 --- /dev/null +++ b/scripts/watchdog.ts @@ -0,0 +1,216 @@ +import { createScopedLogger } from '@/lib/logging/core' +import { prisma } from '@/lib/prisma' +import { addTaskJob } from '@/lib/task/queues' +import { resolveTaskLocaleFromBody } from '@/lib/task/resolve-locale' +import { markTaskFailed } from '@/lib/task/service' +import { publishTaskEvent } from '@/lib/task/publisher' +import { TASK_EVENT_TYPE, TASK_TYPE, type TaskType } from '@/lib/task/types' + +const INTERVAL_MS = Number.parseInt(process.env.WATCHDOG_INTERVAL_MS || '30000', 10) || 30000 +const HEARTBEAT_TIMEOUT_MS = Number.parseInt(process.env.TASK_HEARTBEAT_TIMEOUT_MS || '90000', 10) || 90000 +const TASK_TYPE_SET: ReadonlySet = new Set(Object.values(TASK_TYPE)) +const logger = createScopedLogger({ + module: 'watchdog', + action: 'watchdog.tick', +}) + +function toTaskType(value: string): TaskType | null { + if (TASK_TYPE_SET.has(value)) { + return value as TaskType + } + return null +} + +function toTaskPayload(value: unknown): Record | null { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return value as Record + } + return null +} + +async function recoverQueuedTasks() { + const rows = await prisma.task.findMany({ + where: { + status: 'queued', + enqueuedAt: null, + }, + take: 100, + orderBy: { createdAt: 'asc' }, + }) + + for (const task of rows) { + const taskType = toTaskType(task.type) + if (!taskType) { + logger.error({ + action: 'watchdog.reenqueue_invalid_type', + message: `invalid task type: ${task.type}`, + taskId: task.id, + projectId: task.projectId, + userId: task.userId, + errorCode: 'INVALID_PARAMS', + retryable: false, + }) + continue + } + try { + const locale = resolveTaskLocaleFromBody(task.payload) + if (!locale) { + await markTaskFailed(task.id, 'TASK_LOCALE_REQUIRED', 'task locale is missing') + logger.error({ + action: 'watchdog.reenqueue_locale_missing', + message: 'task locale is missing', + taskId: task.id, + projectId: task.projectId, + userId: task.userId, + errorCode: 'TASK_LOCALE_REQUIRED', + retryable: false, + }) + continue + } + + await addTaskJob({ + taskId: task.id, + type: taskType, + locale, + projectId: task.projectId, + episodeId: task.episodeId, + targetType: task.targetType, + targetId: task.targetId, + payload: toTaskPayload(task.payload), + userId: task.userId, + }) + await prisma.task.update({ + where: { id: task.id }, + data: { + enqueuedAt: new Date(), + enqueueAttempts: { increment: 1 }, + lastEnqueueError: null, + }, + }) + logger.info({ + action: 'watchdog.reenqueue', + message: 'watchdog re-enqueued queued task', + taskId: task.id, + projectId: task.projectId, + userId: task.userId, + details: { + type: task.type, + targetType: task.targetType, + targetId: task.targetId, + }, + }) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 're-enqueue failed' + await prisma.task.update({ + where: { id: task.id }, + data: { + enqueueAttempts: { increment: 1 }, + lastEnqueueError: message, + }, + }) + logger.error({ + action: 'watchdog.reenqueue_failed', + message, + taskId: task.id, + projectId: task.projectId, + userId: task.userId, + errorCode: 'EXTERNAL_ERROR', + retryable: true, + }) + } + } +} + +async function cleanupZombieProcessingTasks() { + const timeoutAt = new Date(Date.now() - HEARTBEAT_TIMEOUT_MS) + const rows = await prisma.task.findMany({ + where: { + status: 'processing', + heartbeatAt: { lt: timeoutAt }, + }, + take: 100, + }) + + for (const task of rows) { + if ((task.attempt || 0) >= (task.maxAttempts || 5)) { + await markTaskFailed(task.id, 'WATCHDOG_TIMEOUT', 'Task heartbeat timeout') + await publishTaskEvent({ + taskId: task.id, + projectId: task.projectId, + userId: task.userId, + type: TASK_EVENT_TYPE.FAILED, + payload: { reason: 'watchdog_timeout' }, + }) + logger.error({ + action: 'watchdog.fail_timeout', + message: 'watchdog marked task as failed due to heartbeat timeout', + taskId: task.id, + projectId: task.projectId, + userId: task.userId, + errorCode: 'WATCHDOG_TIMEOUT', + retryable: true, + }) + continue + } + + await prisma.task.update({ + where: { id: task.id }, + data: { + status: 'queued', + enqueuedAt: null, + heartbeatAt: null, + startedAt: null, + }, + }) + await publishTaskEvent({ + taskId: task.id, + projectId: task.projectId, + userId: task.userId, + type: TASK_EVENT_TYPE.CREATED, + payload: { reason: 'watchdog_requeue' }, + }) + logger.warn({ + action: 'watchdog.requeue_processing', + message: 'watchdog re-queued stalled processing task', + taskId: task.id, + projectId: task.projectId, + userId: task.userId, + retryable: true, + }) + } +} + +async function tick() { + const startedAt = Date.now() + try { + await recoverQueuedTasks() + await cleanupZombieProcessingTasks() + logger.info({ + action: 'watchdog.tick.ok', + message: 'watchdog tick completed', + durationMs: Date.now() - startedAt, + }) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'watchdog tick failed' + logger.error({ + action: 'watchdog.tick.failed', + message, + durationMs: Date.now() - startedAt, + errorCode: 'INTERNAL_ERROR', + retryable: true, + }) + } +} + +logger.info({ + action: 'watchdog.started', + message: 'watchdog started', + details: { + intervalMs: INTERVAL_MS, + heartbeatTimeoutMs: HEARTBEAT_TIMEOUT_MS, + }, +}) +void tick() +setInterval(() => { + void tick() +}, INTERVAL_MS) diff --git a/src/app/[locale]/auth/signin/page.tsx b/src/app/[locale]/auth/signin/page.tsx new file mode 100644 index 0000000..946c1b5 --- /dev/null +++ b/src/app/[locale]/auth/signin/page.tsx @@ -0,0 +1,121 @@ +'use client' + +import { useState } from "react" +import { signIn } from "next-auth/react" +import { useRouter } from "next/navigation" +import Link from "next/link" +import { useTranslations } from 'next-intl' +import Navbar from "@/components/Navbar" + +export default function SignIn() { + const [username, setUsername] = useState("") + const [password, setPassword] = useState("") + const [loading, setLoading] = useState(false) + const [error, setError] = useState("") + const router = useRouter() + const t = useTranslations('auth') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError("") + + try { + const result = await signIn("credentials", { + username, + password, + redirect: false, + }) + + if (result?.error) { + setError(t('loginFailed')) + } else { + router.push("/") + router.refresh() + } + } catch { + setError(t('loginError')) + } finally { + setLoading(false) + } + } + + return ( +
+ +
+
+
+
+

+ {t('welcomeBack')} +

+

{t('loginTo')}

+
+ +
+
+ + setUsername(e.target.value)} + required + className="glass-input-base w-full px-4 py-3" + placeholder={t('phoneNumberPlaceholder')} + /> +
+ +
+ + setPassword(e.target.value)} + required + className="glass-input-base w-full px-4 py-3" + placeholder={t('passwordPlaceholder')} + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +
+

+ {t('noAccount')}{" "} + + {t('signupNow')} + +

+
+ +
+ + {t('backToHome')} + +
+
+
+
+
+ ) +} diff --git a/src/app/[locale]/auth/signup/page.tsx b/src/app/[locale]/auth/signup/page.tsx new file mode 100644 index 0000000..90ffe67 --- /dev/null +++ b/src/app/[locale]/auth/signup/page.tsx @@ -0,0 +1,165 @@ +'use client' + +import { useState } from "react" +import { useRouter } from "next/navigation" +import Link from "next/link" +import { useTranslations } from 'next-intl' +import Navbar from "@/components/Navbar" + +export default function SignUp() { + const [name, setName] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [loading, setLoading] = useState(false) + const [error, setError] = useState("") + const [success, setSuccess] = useState("") + const router = useRouter() + const t = useTranslations('auth') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError("") + setSuccess("") + + if (password !== confirmPassword) { + setError(t('passwordMismatch')) + setLoading(false) + return + } + + if (password.length < 6) { + setError(t('passwordTooShort')) + setLoading(false) + return + } + + try { + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + password, + }), + }) + + const data = await response.json() + + if (response.ok) { + setSuccess(t('signupSuccess')) + setTimeout(() => { + router.push("/auth/signin") + }, 2000) + } else { + setError(data.message || t('signupFailed')) + } + } catch { + setError(t('signupError')) + } finally { + setLoading(false) + } + } + + return ( +
+ +
+
+
+
+

+ {t('createAccount')} +

+

{t('joinPlatform')}

+
+ +
+
+ + setName(e.target.value)} + required + className="glass-input-base w-full px-4 py-3" + placeholder={t('phoneNumberPlaceholder')} + /> +
+ +
+ + setPassword(e.target.value)} + required + className="glass-input-base w-full px-4 py-3" + placeholder={t('passwordMinPlaceholder')} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + className="glass-input-base w-full px-4 py-3" + placeholder={t('confirmPasswordPlaceholder')} + /> +
+ + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ {success} +
+ )} + + +
+ +
+

+ {t('hasAccount')}{" "} + + {t('signinNow')} + +

+
+ +
+ + {t('backToHome')} + +
+
+
+
+
+ ) +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..7014d18 --- /dev/null +++ b/src/app/[locale]/layout.tsx @@ -0,0 +1,87 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono, Poppins, Open_Sans } from "next/font/google"; +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages, getTranslations } from 'next-intl/server'; +import { notFound } from 'next/navigation'; +import "../globals.css"; +import { Providers } from "./providers"; +import { SpeedInsights } from "@vercel/speed-insights/next"; +import { locales } from '@/i18n/routing'; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +// UI/UX Pro Max typography: Modern Professional +const poppins = Poppins({ + variable: "--font-heading", + subsets: ["latin"], + weight: ["400", "500", "600", "700"], +}); + +const openSans = Open_Sans({ + variable: "--font-body", + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700"], +}); + +type SupportedLocale = (typeof locales)[number] + +// 动态元数据生成 +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: 'layout' }) + + return { + title: t('title'), + description: t('description'), + icons: { + icon: '/logo.ico?v=2', + shortcut: '/logo.ico?v=2', + apple: '/logo.png?v=2', + }, + }; +} + +export function generateStaticParams() { + return locales.map((locale) => ({ locale })); +} + +export default async function LocaleLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + + // 验证 locale 是否有效 + if (!locales.includes(locale as SupportedLocale)) { + notFound(); + } + + // 获取翻译消息 + const messages = await getMessages(); + + return ( + + + + + {children} + + + + + + ); +} diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx new file mode 100644 index 0000000..6455495 --- /dev/null +++ b/src/app/[locale]/page.tsx @@ -0,0 +1,85 @@ +'use client' + +import { useTranslations } from 'next-intl' +import Link from 'next/link' +import { useSession } from 'next-auth/react' +import Navbar from '@/components/Navbar' + +export default function Home() { + const t = useTranslations('landing') + const { data: session } = useSession() + + return ( +
+ {/* Navbar */} +
+ +
+ + {/* Background */} +
+
+
+ +
+
+
+
+

+ + {t('title')} + + + {t('subtitle')} + +

+ +
+ {session ? ( + + {t('enterWorkspace')} + + ) : ( + + {t('getStarted')} + + )} +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) +} diff --git a/src/app/[locale]/profile/components/ApiConfigTab.tsx b/src/app/[locale]/profile/components/ApiConfigTab.tsx new file mode 100644 index 0000000..ee806f1 --- /dev/null +++ b/src/app/[locale]/profile/components/ApiConfigTab.tsx @@ -0,0 +1,7 @@ +'use client' + +import { ApiConfigTabContainer } from './api-config-tab/ApiConfigTabContainer' + +export default function ApiConfigTab() { + return +} diff --git a/src/app/[locale]/profile/components/api-config-tab/ApiConfigProviderList.tsx b/src/app/[locale]/profile/components/api-config-tab/ApiConfigProviderList.tsx new file mode 100644 index 0000000..7f56d29 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config-tab/ApiConfigProviderList.tsx @@ -0,0 +1,118 @@ +'use client' + +import type { CustomModel, Provider } from '../api-config' +import { ProviderCard, ProviderSection } from '../api-config' +import { AppIcon } from '@/components/ui/icons' + +interface DefaultModels { + analysisModel?: string + characterModel?: string + locationModel?: string + storyboardModel?: string + editModel?: string + videoModel?: string + lipSyncModel?: string +} + +interface ApiConfigProviderListProps { + modelProviders: Provider[] + allModels: CustomModel[] + defaultModels: DefaultModels + audioProviders: Provider[] + getModelsForProvider: (providerId: string) => CustomModel[] + onAddGeminiProvider: () => void + onToggleModel: (modelKey: string, providerId: string) => void + onUpdateApiKey: (providerId: string, apiKey: string) => void + onUpdateBaseUrl: (providerId: string, baseUrl: string) => void + onDeleteModel: (modelKey: string, providerId: string) => void + onUpdateModel: (modelKey: string, updates: Partial, providerId: string) => void + onDeleteProvider: (providerId: string) => void + onAddModel: (model: Omit) => void + labels: { + providerPool: string + addGeminiProvider: string + otherProviders: string + audioCategory: string + audioApiKey: string + } +} + +const AUDIO_ICON = ( + +) + +export function ApiConfigProviderList({ + modelProviders, + allModels, + defaultModels, + audioProviders, + getModelsForProvider, + onAddGeminiProvider, + onToggleModel, + onUpdateApiKey, + onUpdateBaseUrl, + onDeleteModel, + onUpdateModel, + onDeleteProvider, + onAddModel, + labels, +}: ApiConfigProviderListProps) { + const hasAudioProviders = audioProviders.length > 0 + const hasOtherProviders = hasAudioProviders + + return ( + <> +
+
+

{labels.providerPool}

+ +
+
+ {modelProviders.map((provider) => ( + onToggleModel(modelKey, provider.id)} + onUpdateApiKey={onUpdateApiKey} + onUpdateBaseUrl={onUpdateBaseUrl} + onDeleteModel={(modelKey) => onDeleteModel(modelKey, provider.id)} + onUpdateModel={(modelKey, updates) => onUpdateModel(modelKey, updates, provider.id)} + onDeleteProvider={onDeleteProvider} + onAddModel={onAddModel} + /> + ))} +
+
+ + {hasOtherProviders && ( +
+

+ {labels.otherProviders} + + ({labels.audioCategory}) + +

+
+ {hasAudioProviders && ( + + )} +
+
+ )} + + ) +} diff --git a/src/app/[locale]/profile/components/api-config-tab/ApiConfigTabContainer.tsx b/src/app/[locale]/profile/components/api-config-tab/ApiConfigTabContainer.tsx new file mode 100644 index 0000000..f37c28e --- /dev/null +++ b/src/app/[locale]/profile/components/api-config-tab/ApiConfigTabContainer.tsx @@ -0,0 +1,458 @@ +'use client' + +import { useState } from 'react' +import { useLocale, useTranslations } from 'next-intl' +import { GlassModalShell } from '@/components/ui/primitives' +import { resolveTaskPresentationState } from '@/lib/task/presentation' +import type { CapabilityValue } from '@/lib/model-config-contract' +import { + encodeModelKey, + getProviderDisplayName, + parseModelKey, + useProviders, +} from '../api-config' +import { ApiConfigToolbar } from './ApiConfigToolbar' +import { ApiConfigProviderList } from './ApiConfigProviderList' +import { useApiConfigFilters } from './hooks/useApiConfigFilters' +import { ModelCapabilityDropdown } from '@/components/ui/config-modals/ModelCapabilityDropdown' +import { AppIcon } from '@/components/ui/icons' + +type CustomProviderType = 'gemini-compatible' | 'openai-compatible' +type DefaultModelField = + | 'analysisModel' + | 'characterModel' + | 'locationModel' + | 'storyboardModel' + | 'editModel' + | 'videoModel' + | 'lipSyncModel' + +const MONO_ICON_BADGE = + 'inline-flex items-center justify-center rounded-lg bg-[var(--glass-bg-surface)] p-1 text-[var(--glass-text-secondary)]' + +const Icons = { + settings: () => ( + + ), + llm: () => ( + + ), + image: () => ( + + ), + video: () => ( + + ), + lipsync: () => ( + + ), + chevronDown: () => ( + + ), +} + +interface DefaultModelCardConfig { + field: DefaultModelField + modelType: 'llm' | 'image' | 'video' | 'lipsync' + title: string + icon: keyof Pick +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function isCapabilityValue(value: unknown): value is CapabilityValue { + return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' +} + +function parseBySample(input: string, sample: CapabilityValue): CapabilityValue { + if (typeof sample === 'number') return Number(input) + if (typeof sample === 'boolean') return input === 'true' + return input +} + +function toCapabilityFieldLabel(field: string): string { + return field.replace(/([A-Z])/g, ' $1').replace(/^./, (char) => char.toUpperCase()) +} + +export function ApiConfigTabContainer() { + const locale = useLocale() + const { + providers, + models, + defaultModels, + capabilityDefaults, + loading, + saveStatus, + updateProviderApiKey, + updateProviderBaseUrl, + addProvider, + deleteProvider, + toggleModel, + deleteModel, + addModel, + updateModel, + updateDefaultModel, + updateCapabilityDefault, + } = useProviders() + + const t = useTranslations('apiConfig') + const tc = useTranslations('common') + const tp = useTranslations('providerSection') + + const savingState = + saveStatus === 'saving' + ? resolveTaskPresentationState({ + phase: 'processing', + intent: 'modify', + resource: 'text', + hasOutput: true, + }) + : null + + const { + modelProviders, + audioProviders, + getModelsForProvider, + getEnabledModelsByType, + } = useApiConfigFilters({ + providers, + models, + }) + + const [showAddGeminiProvider, setShowAddGeminiProvider] = useState(false) + const [newGeminiProvider, setNewGeminiProvider] = useState<{ + name: string + baseUrl: string + apiKey: string + apiType: CustomProviderType + }>({ + name: '', + baseUrl: '', + apiKey: '', + apiType: 'gemini-compatible', + }) + + const handleAddGeminiProvider = () => { + if (!newGeminiProvider.name || !newGeminiProvider.baseUrl) { + alert(tp('fillRequired')) + return + } + + const uuid = crypto.randomUUID() + const providerId = `${newGeminiProvider.apiType}:${uuid}` + const name = newGeminiProvider.name.trim() + const baseUrl = newGeminiProvider.baseUrl.trim() + const apiKey = newGeminiProvider.apiKey.trim() + + addProvider({ + id: providerId, + name, + baseUrl, + apiKey, + }) + + setNewGeminiProvider({ + name: '', + baseUrl: '', + apiKey: '', + apiType: 'gemini-compatible', + }) + setShowAddGeminiProvider(false) + } + + const handleCancelAddGeminiProvider = () => { + setNewGeminiProvider({ + name: '', + baseUrl: '', + apiKey: '', + apiType: 'gemini-compatible', + }) + setShowAddGeminiProvider(false) + } + + if (loading) { + return ( +
+ {tc('loading')} +
+ ) + } + + const defaultModelCards: DefaultModelCardConfig[] = [ + { field: 'analysisModel', modelType: 'llm', title: t('textDefault'), icon: 'llm' }, + { field: 'characterModel', modelType: 'image', title: t('characterDefault'), icon: 'image' }, + { field: 'locationModel', modelType: 'image', title: t('locationDefault'), icon: 'image' }, + { field: 'storyboardModel', modelType: 'image', title: t('storyboardDefault'), icon: 'image' }, + { field: 'editModel', modelType: 'image', title: t('editDefault'), icon: 'image' }, + { field: 'videoModel', modelType: 'video', title: t('videoDefault'), icon: 'video' }, + { field: 'lipSyncModel', modelType: 'lipsync', title: t('lipsyncDefault'), icon: 'lipsync' }, + ] + + return ( +
+ + +
+
+
+
+ + + +

{t('defaultModels')}

+
+

+ {t('defaultModel.hint')} +

+
+ {defaultModelCards.map((card) => { + const options = getEnabledModelsByType(card.modelType) + const currentKey = defaultModels[card.field] + const parsed = parseModelKey(currentKey) + const normalizedKey = parsed ? encodeModelKey(parsed.provider, parsed.modelId) : '' + const current = normalizedKey + ? options.find((option) => option.modelKey === normalizedKey) + : null + const capabilityFields = (() => { + if (!current || !current.capabilities) return [] as Array<{ field: string; options: CapabilityValue[] }> + const namespace = current.capabilities[card.modelType] + if (!isRecord(namespace)) return [] as Array<{ field: string; options: CapabilityValue[] }> + return Object.entries(namespace) + .filter(([key, value]) => key.endsWith('Options') && Array.isArray(value) && value.every(isCapabilityValue) && value.length > 0) + .map(([key, value]) => ({ + field: key.slice(0, -'Options'.length), + options: value as CapabilityValue[], + })) + })() + const ModelIcon = Icons[card.icon] + + return ( +
+
+ + + + + {card.title} + +
+ {card.modelType === 'video' || card.modelType === 'image' || card.modelType === 'llm' ? ( + /* Unified model capability dropdown */ + ({ + value: opt.modelKey, + label: opt.name, + provider: opt.provider, + providerName: opt.providerName || getProviderDisplayName(opt.provider, locale), + }))} + value={normalizedKey || undefined} + onModelChange={(v) => updateDefaultModel(card.field, v, capabilityFields)} + capabilityFields={capabilityFields.map((d) => ({ + ...d, + label: toCapabilityFieldLabel(d.field), + }))} + capabilityOverrides={ + current + ? Object.fromEntries( + capabilityFields + .filter((d) => capabilityDefaults[current.modelKey]?.[d.field] !== undefined) + .map((d) => [d.field, capabilityDefaults[current.modelKey][d.field]]) + ) + : {} + } + onCapabilityChange={(field, rawValue, sample) => { + if (!current) return + if (!rawValue) { + updateCapabilityDefault(current.modelKey, field, null) + return + } + updateCapabilityDefault( + current.modelKey, + field, + parseBySample(rawValue, sample), + ) + }} + placeholder={t('selectDefault')} + /> + ) : ( + /* Non-video models: keep native select */ + <> +
+ +
+ +
+
+ {current && card.modelType !== 'lipsync' && ( +
+ + {current.providerName} + +
+ )} + + )} +
+ ) + })} +
+
+ + setShowAddGeminiProvider(true)} + onToggleModel={toggleModel} + onUpdateApiKey={updateProviderApiKey} + onUpdateBaseUrl={updateProviderBaseUrl} + onDeleteModel={deleteModel} + onUpdateModel={updateModel} + onDeleteProvider={deleteProvider} + onAddModel={addModel} + labels={{ + providerPool: t('providerPool'), + addGeminiProvider: t('addGeminiProvider'), + otherProviders: t('otherProviders'), + audioCategory: t('audioCategory'), + audioApiKey: t('sections.audioApiKey'), + }} + /> +
+
+ + + + +
+ } + > +
+
+ +
+ +
+ +
+
+
+ +
+ + + setNewGeminiProvider({ + ...newGeminiProvider, + name: event.target.value, + }) + } + placeholder={tp('name')} + className="glass-input-base w-full px-3 py-2.5 text-sm" + /> +
+ +
+ + + setNewGeminiProvider({ + ...newGeminiProvider, + baseUrl: event.target.value, + }) + } + placeholder={t('baseUrl')} + className="glass-input-base w-full px-3 py-2.5 text-sm font-mono" + /> +
+ +
+ + + setNewGeminiProvider({ + ...newGeminiProvider, + apiKey: event.target.value, + }) + } + placeholder={t('apiKeyLabel')} + className="glass-input-base w-full px-3 py-2.5 text-sm" + /> +
+
+ + + ) +} diff --git a/src/app/[locale]/profile/components/api-config-tab/ApiConfigToolbar.tsx b/src/app/[locale]/profile/components/api-config-tab/ApiConfigToolbar.tsx new file mode 100644 index 0000000..9ea8e19 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config-tab/ApiConfigToolbar.tsx @@ -0,0 +1,49 @@ +'use client' + +import type { ComponentProps } from 'react' +import TaskStatusInline from '@/components/task/TaskStatusInline' +import { AppIcon } from '@/components/ui/icons' + +interface ApiConfigToolbarProps { + title: string + saveStatus: 'idle' | 'saving' | 'saved' | 'error' + savingState: ComponentProps['state'] | null + savingLabel: string + savedLabel: string + saveFailedLabel: string +} + +export function ApiConfigToolbar({ + title, + saveStatus, + savingState, + savingLabel, + savedLabel, + saveFailedLabel, +}: ApiConfigToolbarProps) { + return ( +
+

{title}

+
+ {saveStatus === 'saving' && ( + + + {savingLabel} + + )} + {saveStatus === 'saved' && ( + + + {savedLabel} + + )} + {saveStatus === 'error' && ( + + + {saveFailedLabel} + + )} +
+
+ ) +} diff --git a/src/app/[locale]/profile/components/api-config-tab/hooks/useApiConfigFilters.ts b/src/app/[locale]/profile/components/api-config-tab/hooks/useApiConfigFilters.ts new file mode 100644 index 0000000..4658420 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config-tab/hooks/useApiConfigFilters.ts @@ -0,0 +1,127 @@ +'use client' + +import { useMemo } from 'react' +import type { CustomModel, Provider } from '../../api-config' +import { PRESET_PROVIDERS, getProviderKey } from '../../api-config' + +interface UseApiConfigFiltersParams { + providers: Provider[] + models: CustomModel[] +} + +interface EnabledModelOption extends CustomModel { + providerName: string +} + +const DYNAMIC_PROVIDER_PREFIXES = ['gemini-compatible', 'openai-compatible'] +const ALWAYS_SHOW_PROVIDERS: string[] = [] +const MODEL_TYPES: Array<'llm' | 'image' | 'video' | 'lipsync'> = ['llm', 'image', 'video', 'lipsync'] +const MODEL_PROVIDER_KEYS = [ + 'ark', + 'google', + 'openrouter', + 'minimax', + 'vidu', + 'fal', + 'gemini-compatible', + 'openai-compatible', +] +const AUDIO_PROVIDER_KEYS = ['qwen'] + +function isModelProviderType(type: CustomModel['type']): type is 'llm' | 'image' | 'video' | 'lipsync' { + return MODEL_TYPES.includes(type as 'llm' | 'image' | 'video' | 'lipsync') +} + +function hasProviderApiKey(provider: Provider | undefined): boolean { + if (!provider) return false + if (provider.hasApiKey === true) return true + const apiKey = typeof provider.apiKey === 'string' ? provider.apiKey.trim() : '' + return apiKey.length > 0 +} + +export function useApiConfigFilters({ + providers, + models, +}: UseApiConfigFiltersParams) { + const modelProviderKeys = useMemo(() => { + const keys = new Set(MODEL_PROVIDER_KEYS) + models.forEach((model) => { + if (!isModelProviderType(model.type)) return + keys.add(getProviderKey(model.provider)) + }) + return keys + }, [models]) + const audioProviderKeys = useMemo(() => { + const keys = new Set(AUDIO_PROVIDER_KEYS) + models.forEach((model) => { + if (model.type !== 'audio') return + keys.add(getProviderKey(model.provider)) + }) + return keys + }, [models]) + + const isPresetProvider = (providerId: string) => { + return PRESET_PROVIDERS.some( + (provider) => provider.id === getProviderKey(providerId), + ) + } + + const modelProviders = useMemo(() => { + return providers.filter((provider) => { + const providerKey = getProviderKey(provider.id) + const isCustomProvider = !isPresetProvider(provider.id) + const isDynamicProvider = + DYNAMIC_PROVIDER_PREFIXES.includes(providerKey) && provider.id.includes(':') + + return ( + (isCustomProvider && modelProviderKeys.has(providerKey)) || + modelProviderKeys.has(providerKey) || + ALWAYS_SHOW_PROVIDERS.includes(providerKey) || + isDynamicProvider + ) + }) + }, [modelProviderKeys, providers]) + + const audioProviders = useMemo( + () => + providers.filter((provider) => { + const providerKey = getProviderKey(provider.id) + if (providerKey === 'fal') return false + return audioProviderKeys.has(providerKey) + }), + [audioProviderKeys, providers], + ) + + const enabledModelsByType = useMemo(() => { + const grouped: Record<'llm' | 'image' | 'video' | 'lipsync', EnabledModelOption[]> = { + llm: [], + image: [], + video: [], + lipsync: [], + } + + const providersById = new Map(providers.map((provider) => [provider.id, provider] as const)) + + for (const model of models) { + if (!model.enabled) continue + if (!isModelProviderType(model.type)) continue + const provider = providersById.get(model.provider) + if (!hasProviderApiKey(provider)) continue + + grouped[model.type].push({ + ...model, + providerName: provider?.name || model.provider, + }) + } + + return grouped + }, [models, providers]) + + return { + modelProviders, + audioProviders, + getModelsForProvider: (providerId: string) => + models.filter((model) => model.provider === providerId), + getEnabledModelsByType: (type: 'llm' | 'image' | 'video' | 'lipsync') => enabledModelsByType[type], + } +} diff --git a/src/app/[locale]/profile/components/api-config/DefaultModelSection.tsx b/src/app/[locale]/profile/components/api-config/DefaultModelSection.tsx new file mode 100644 index 0000000..02dd4f0 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/DefaultModelSection.tsx @@ -0,0 +1,76 @@ +'use client' + +import React from 'react' +import { useTranslations } from 'next-intl' +import { CustomModel } from './types' +import { AppIcon } from '@/components/ui/icons' + +interface DefaultModelSectionProps { + type: 'llm' | 'image' | 'video' | 'lipsync' + models: CustomModel[] + defaultModels: { + analysisModel?: string + imageModel?: string + videoModel?: string + lipSyncModel?: string + } + onUpdateDefault: (field: string, modelKey: string) => void +} + +export function DefaultModelSection({ + type, + models, + defaultModels, + onUpdateDefault +}: DefaultModelSectionProps) { + const t = useTranslations('apiConfig') + + // 只显示已启用的模型 + const enabledModels = models.filter(m => m.enabled) + + if (enabledModels.length === 0) { + return null + } + + // 根据类型确定要显示的选择器 + const selectors = type === 'llm' + ? [{ field: 'analysisModel', label: t('defaultModel.analysis') }] + : type === 'image' + ? [{ field: 'imageModel', label: t('defaultModel.image') }] + : type === 'video' + ? [{ field: 'videoModel', label: t('defaultModel.video') }] + : [{ field: 'lipSyncModel', label: t('lipsyncDefault') }] + + return ( +
+
+ + + +

{t('defaultModel.title')}

+
+ +

{t('defaultModel.hint')}

+ +
+ {selectors.map(({ field, label }) => ( +
+ + +
+ ))} +
+
+ ) +} diff --git a/src/app/[locale]/profile/components/api-config/ProviderCard.tsx b/src/app/[locale]/profile/components/api-config/ProviderCard.tsx new file mode 100644 index 0000000..aff0de5 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/ProviderCard.tsx @@ -0,0 +1,52 @@ +'use client' + +import { useTranslations } from 'next-intl' +import { ProviderAdvancedFields } from './provider-card/ProviderAdvancedFields' +import { ProviderBaseFields } from './provider-card/ProviderBaseFields' +import { ProviderCardShell } from './provider-card/ProviderCardShell' +import { useProviderCardState } from './provider-card/hooks/useProviderCardState' +import type { ProviderCardProps } from './provider-card/types' + +export function ProviderCard({ + provider, + models, + allModels, + defaultModels, + onToggleModel, + onUpdateApiKey, + onUpdateBaseUrl, + onDeleteModel, + onUpdateModel, + onDeleteProvider, + onAddModel, +}: ProviderCardProps) { + const t = useTranslations('apiConfig') + + const state = useProviderCardState({ + provider, + models, + allModels, + defaultModels, + onUpdateApiKey, + onUpdateBaseUrl, + onUpdateModel, + onAddModel, + t, + }) + + return ( + + + + + ) +} + +export default ProviderCard diff --git a/src/app/[locale]/profile/components/api-config/ProviderSection.tsx b/src/app/[locale]/profile/components/api-config/ProviderSection.tsx new file mode 100644 index 0000000..7f70556 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/ProviderSection.tsx @@ -0,0 +1,196 @@ +'use client' + +import React, { useState } from 'react' +import { useTranslations } from 'next-intl' +import { Provider, PRESET_PROVIDERS } from './types' +import { AppIcon } from '@/components/ui/icons' + +interface ProviderSectionProps { + title: string + icon: React.ReactNode + type: 'audio' | 'lipsync' + providers: Provider[] + onUpdateApiKey: (providerId: string, apiKey: string) => void + onUpdateInfo?: (providerId: string, name: string, baseUrl?: string) => void + onDelete?: (providerId: string) => void + onAdd?: (provider: Omit) => void + showBaseUrl?: boolean + showAddButton?: boolean +} + +export function ProviderSection({ + title, + icon, + providers, + onUpdateApiKey, + onUpdateInfo, + onDelete, + onAdd, + showBaseUrl = false, + showAddButton = false +}: ProviderSectionProps) { + const [showApiKeys, setShowApiKeys] = useState>({}) + const [editingId, setEditingId] = useState(null) + const [editData, setEditData] = useState({ name: '', baseUrl: '' }) + const [showAddForm, setShowAddForm] = useState(false) + const [newProvider, setNewProvider] = useState({ name: '', baseUrl: '', apiKey: '' }) + const t = useTranslations('providerSection') + const tc = useTranslations('common') + + const isPreset = (id: string) => PRESET_PROVIDERS.some(p => p.id === id) + + const handleSaveEdit = (provider: Provider) => { + onUpdateInfo?.(provider.id, editData.name, editData.baseUrl || undefined) + setEditingId(null) + } + + const handleAdd = () => { + if (!newProvider.name) { + alert(t('fillRequired')) + return + } + onAdd?.({ + id: `custom-${Date.now()}`, + name: newProvider.name, + baseUrl: newProvider.baseUrl || undefined, + apiKey: newProvider.apiKey + }) + setNewProvider({ name: '', baseUrl: '', apiKey: '' }) + setShowAddForm(false) + } + + return ( +
+
+

+ {icon} + {title} +

+ {showAddButton && ( + + )} +
+ + {/* 添加表单 */} + {showAddForm && ( +
+ setNewProvider({ ...newProvider, name: e.target.value })} + placeholder={t('name')} + className="glass-input-base w-24 px-2 py-1.5 text-sm" + /> + {showBaseUrl && ( + setNewProvider({ ...newProvider, baseUrl: e.target.value })} + placeholder="Base URL" + className="glass-input-base flex-1 px-2 py-1.5 text-sm font-mono" + /> + )} + setNewProvider({ ...newProvider, apiKey: e.target.value })} + placeholder="API Key" + className="glass-input-base w-40 px-2 py-1.5 text-sm" + /> + + +
+ )} + + {/* 提供商列表 */} +
+ {providers.map(provider => { + const isEditing = editingId === provider.id + const isVisible = showApiKeys[provider.id] + + if (isEditing && showBaseUrl) { + return ( +
+ setEditData({ ...editData, name: e.target.value })} + className="glass-input-base w-28 px-2 py-1.5 text-sm" + /> + setEditData({ ...editData, baseUrl: e.target.value })} + className="glass-input-base flex-1 px-2 py-1.5 text-sm font-mono" + /> + + +
+ ) + } + + return ( +
+ {showBaseUrl && ( + + )} + {provider.name} + {showBaseUrl && ( + {provider.baseUrl} + )} +
+ onUpdateApiKey(provider.id, e.target.value)} + placeholder="API Key" + className="glass-input-base w-full px-3 py-1.5 pr-9 text-sm" + /> + +
+ {provider.apiKey && ( + + + + )} + {!isPreset(provider.id) && onDelete && ( + + )} +
+ ) + })} +
+
+ ) +} diff --git a/src/app/[locale]/profile/components/api-config/hooks.ts b/src/app/[locale]/profile/components/api-config/hooks.ts new file mode 100644 index 0000000..bc72332 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/hooks.ts @@ -0,0 +1,603 @@ +'use client' +import { logError as _ulogError } from '@/lib/logging/core' +import { useLocale, useTranslations } from 'next-intl' + +import { useState, useEffect, useRef, useCallback } from 'react' +import { + Provider, + CustomModel, + PRESET_PROVIDERS, + PRESET_MODELS, + encodeModelKey, + getProviderKey, + isPresetComingSoonModelKey, + resolvePresetProviderName, + type PricingDisplayItem, + type PricingDisplayMap, +} from './types' +import type { CapabilitySelections, CapabilityValue } from '@/lib/model-config-contract' + +interface DefaultModels { + analysisModel?: string + characterModel?: string + locationModel?: string + storyboardModel?: string + editModel?: string + videoModel?: string + lipSyncModel?: string +} + +interface UseProvidersReturn { + providers: Provider[] + models: CustomModel[] + defaultModels: DefaultModels + capabilityDefaults: CapabilitySelections + loading: boolean + saveStatus: 'idle' | 'saving' | 'saved' | 'error' + updateProviderApiKey: (providerId: string, apiKey: string) => void + updateProviderBaseUrl: (providerId: string, baseUrl: string) => void + addProvider: (provider: Omit) => void + deleteProvider: (providerId: string) => void + updateProviderInfo: (providerId: string, name: string, baseUrl?: string) => void + toggleModel: (modelKey: string, providerId?: string) => void + updateModel: (modelKey: string, updates: Partial, providerId?: string) => void + addModel: (model: Omit) => void + deleteModel: (modelKey: string, providerId?: string) => void + updateDefaultModel: (field: string, modelKey: string, capabilityFieldsToDefault?: Array<{ field: string; options: CapabilityValue[] }>) => void + updateCapabilityDefault: (modelKey: string, field: string, value: string | number | boolean | null) => void + getModelsByType: (type: CustomModel['type']) => CustomModel[] +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function composePricingDisplayKey(type: CustomModel['type'], provider: string, modelId: string): string { + return `${type}::${provider}::${modelId}` +} + +function parsePricingDisplayMap(raw: unknown): PricingDisplayMap { + if (!isRecord(raw)) return {} + + const map: PricingDisplayMap = {} + for (const [key, value] of Object.entries(raw)) { + if (!isRecord(value)) continue + const min = typeof value.min === 'number' && Number.isFinite(value.min) ? value.min : null + const max = typeof value.max === 'number' && Number.isFinite(value.max) ? value.max : null + const label = typeof value.label === 'string' ? value.label.trim() : '' + const input = typeof value.input === 'number' && Number.isFinite(value.input) ? value.input : undefined + const output = typeof value.output === 'number' && Number.isFinite(value.output) ? value.output : undefined + if (min === null || max === null || !label) continue + map[key] = { + min, + max, + label, + ...(typeof input === 'number' ? { input } : {}), + ...(typeof output === 'number' ? { output } : {}), + } + } + return map +} + +/** + * Provider keys that share pricing display with a canonical provider. + */ +const PRICING_DISPLAY_ALIASES: Readonly> = { + 'gemini-compatible': 'google', +} + +function resolvePricingDisplay( + map: PricingDisplayMap, + type: CustomModel['type'], + provider: string, + modelId: string, +): PricingDisplayItem | null { + const exact = map[composePricingDisplayKey(type, provider, modelId)] + if (exact) return exact + + const providerKey = getProviderKey(provider) + if (providerKey !== provider) { + const fallback = map[composePricingDisplayKey(type, providerKey, modelId)] + if (fallback) return fallback + } + + // Fallback: check canonical provider alias (e.g. gemini-compatible → google) + const aliasTarget = PRICING_DISPLAY_ALIASES[providerKey] + if (aliasTarget) { + const aliasFallback = map[composePricingDisplayKey(type, aliasTarget, modelId)] + if (aliasFallback) return aliasFallback + } + return null +} + +function applyPricingDisplay(model: CustomModel, map: PricingDisplayMap): CustomModel { + const pricing = resolvePricingDisplay(map, model.type, model.provider, model.modelId) + if (!pricing) { + // Preserve existing server-provided pricing fields (e.g. from customPricing) + if (model.priceLabel && model.priceLabel !== '--') { + return model + } + return { + ...model, + price: 0, + priceLabel: '--', + priceMin: undefined, + priceMax: undefined, + priceInput: undefined, + priceOutput: undefined, + } + } + + return { + ...model, + price: pricing.min, + priceMin: pricing.min, + priceMax: pricing.max, + priceLabel: pricing.label, + ...(typeof pricing.input === 'number' ? { priceInput: pricing.input } : {}), + ...(typeof pricing.output === 'number' ? { priceOutput: pricing.output } : {}), + } +} + +export function useProviders(): UseProvidersReturn { + const locale = useLocale() + const t = useTranslations('apiConfig') + const presetProviders = PRESET_PROVIDERS.map((provider) => ({ + ...provider, + name: resolvePresetProviderName(provider.id, provider.name, locale), + })) + const [providers, setProviders] = useState( + presetProviders.map((provider) => ({ ...provider, apiKey: '', hasApiKey: false })), + ) + const [models, setModels] = useState( + PRESET_MODELS.map((model) => { + const modelKey = encodeModelKey(model.provider, model.modelId) + return { + ...model, + modelKey, + price: 0, + priceLabel: '--', + enabled: !isPresetComingSoonModelKey(modelKey), + } + }), + ) + const [defaultModels, setDefaultModels] = useState({}) + const [capabilityDefaults, setCapabilityDefaults] = useState({}) + const [loading, setLoading] = useState(true) + const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle') + const saveTimeoutRef = useRef(null) + const initializedRef = useRef(false) + + // 始终持有最新值的 refs,用于避免异步保存时读到旧的闭包值 + const latestModelsRef = useRef(models) + const latestProvidersRef = useRef(providers) + const latestDefaultModelsRef = useRef(defaultModels) + const latestCapabilityDefaultsRef = useRef(capabilityDefaults) + useEffect(() => { latestModelsRef.current = models }, [models]) + useEffect(() => { latestProvidersRef.current = providers }, [providers]) + useEffect(() => { latestDefaultModelsRef.current = defaultModels }, [defaultModels]) + useEffect(() => { latestCapabilityDefaultsRef.current = capabilityDefaults }, [capabilityDefaults]) + + // 加载配置 + useEffect(() => { + fetchConfig() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + async function fetchConfig() { + initializedRef.current = false + let loadedSuccessfully = false + try { + const res = await fetch('/api/user/api-config') + if (!res.ok) { + throw new Error(`api-config load failed: HTTP ${res.status}`) + } + + const data = await res.json() + const pricingDisplay = parsePricingDisplayMap((data as { pricingDisplay?: unknown }).pricingDisplay) + + // 合并预设和已保存的提供商 + const savedProviders: Provider[] = data.providers || [] + const allProviders = presetProviders.map(preset => { + const saved = savedProviders.find(p => getProviderKey(p.id) === preset.id) + return { + ...preset, + apiKey: saved?.apiKey || '', + hasApiKey: !!saved?.apiKey, + // 保留用户保存的 baseUrl(用于自建服务) + baseUrl: saved?.baseUrl || preset.baseUrl + } + }) + const customProviders = savedProviders.filter(p => + !PRESET_PROVIDERS.find(preset => preset.id === getProviderKey(p.id)) + ).map(p => ({ + ...p, + hasApiKey: !!p.apiKey + })) + setProviders([...allProviders, ...customProviders]) + + // 合并预设和已保存的模型 + const savedModelsRaw = data.models || [] + const savedModelsNormalized = savedModelsRaw.map((m: CustomModel) => ({ + ...m, + modelKey: m.modelKey || encodeModelKey(m.provider, m.modelId), + })) + const savedModels: CustomModel[] = [] + const seen = new Set() + for (const model of savedModelsNormalized) { + const key = model.modelKey + if (seen.has(key)) continue + seen.add(key) + savedModels.push(model) + } + const hasSavedModels = savedModels.length > 0 + const allModels = PRESET_MODELS.map(preset => { + const presetModelKey = encodeModelKey(preset.provider, preset.modelId) + const saved = savedModels.find((m: CustomModel) => + m.modelKey === presetModelKey + ) + const alwaysEnabledPreset = preset.type === 'lipsync' + const mergedPreset: CustomModel = { + ...preset, + modelKey: presetModelKey, + enabled: isPresetComingSoonModelKey(presetModelKey) + ? false + : (hasSavedModels ? (alwaysEnabledPreset || !!saved) : false), + price: 0, + capabilities: saved?.capabilities ?? preset.capabilities, + } + return applyPricingDisplay(mergedPreset, pricingDisplay) + }) + const customModels = savedModels.filter((m: CustomModel) => + !PRESET_MODELS.find((preset) => encodeModelKey(preset.provider, preset.modelId) === m.modelKey) + ).map((m: CustomModel) => ({ + ...applyPricingDisplay(m, pricingDisplay), + // 尊重服务端返回的 enabled 字段(后端对 disabled presets 会明确返回 enabled: false) + enabled: (m as CustomModel & { enabled?: boolean }).enabled !== false, + })) + + setModels([...allModels, ...customModels]) + + // 加载默认模型配置 + if (data.defaultModels) { + setDefaultModels(data.defaultModels) + } + if (data.capabilityDefaults && typeof data.capabilityDefaults === 'object') { + setCapabilityDefaults(data.capabilityDefaults as CapabilitySelections) + } + loadedSuccessfully = true + } catch (error) { + _ulogError('获取配置失败:', error) + setSaveStatus('error') + } finally { + setLoading(false) + if (loadedSuccessfully) { + // 延迟设置 initialized,确保所有状态更新完成后才开始监听 + setTimeout(() => { + initializedRef.current = true + }, 100) + } + } + } + + /** + * 核心保存函数:始终从 ref 读取最新值,支持传入覆盖值(解决异步闭包旧值问题) + * optimistic=true 时立刻显示「已保存」,不经历「保存中」状态,失败时才回退为「保存失败」 + */ + const performSave = useCallback(async (overrides?: { + defaultModels?: DefaultModels + capabilityDefaults?: CapabilitySelections + }, optimistic = false) => { + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current) + saveTimeoutRef.current = null + } + if (optimistic) { + // 与项目设置一致:立刻显示已保存,不等网络返回 + setSaveStatus('saved') + setTimeout(() => setSaveStatus('idle'), 3000) + } else { + setSaveStatus('saving') + } + try { + const currentModels = latestModelsRef.current + const currentProviders = latestProvidersRef.current + const currentDefaultModels = overrides?.defaultModels ?? latestDefaultModelsRef.current + const currentCapabilityDefaults = overrides?.capabilityDefaults ?? latestCapabilityDefaultsRef.current + const enabledModels = currentModels.filter(m => m.enabled) + const res = await fetch('/api/user/api-config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + models: enabledModels, + providers: currentProviders, + defaultModels: currentDefaultModels, + capabilityDefaults: currentCapabilityDefaults, + }), + }) + if (res.ok) { + if (!optimistic) { + setSaveStatus('saved') + setTimeout(() => setSaveStatus('idle'), 3000) + } + } else { + setSaveStatus('error') + } + } catch (error) { + _ulogError('保存失败:', error) + setSaveStatus('error') + } + }, []) // 无依赖,所有值均从 ref 读取 + + // 默认模型操作:选中即立刻显示已保存(与项目设置一致) + // capabilityFieldsToDefault:切换模型时自动将第一个 option 写入 capabilityDefaults(只填未配置字段) + const updateDefaultModel = useCallback(( + field: string, + modelKey: string, + capabilityFieldsToDefault?: Array<{ field: string; options: CapabilityValue[] }>, + ) => { + setDefaultModels(prev => { + const next = { ...prev, [field]: modelKey } + latestDefaultModelsRef.current = next + + if (capabilityFieldsToDefault && capabilityFieldsToDefault.length > 0) { + setCapabilityDefaults(prevCap => { + const nextCap: CapabilitySelections = { ...prevCap } + const existing = { ...(nextCap[modelKey] || {}) } + let changed = false + for (const def of capabilityFieldsToDefault) { + if (existing[def.field] === undefined && def.options.length > 0) { + existing[def.field] = def.options[0] + changed = true + } + } + if (changed) { + nextCap[modelKey] = existing + latestCapabilityDefaultsRef.current = nextCap + void performSave({ defaultModels: next, capabilityDefaults: nextCap }, true) + return nextCap + } + void performSave({ defaultModels: next }, true) // optimistic=true + return prevCap + }) + } else { + void performSave({ defaultModels: next }, true) // optimistic=true + } + return next + }) + }, [performSave]) + + const updateCapabilityDefault = useCallback((modelKey: string, field: string, value: string | number | boolean | null) => { + setCapabilityDefaults((previous) => { + const next: CapabilitySelections = { ...previous } + const current = { ...(next[modelKey] || {}) } + if (value === null) { + delete current[field] + } else { + current[field] = value + } + + if (Object.keys(current).length === 0) { + delete next[modelKey] + } else { + next[modelKey] = current + } + latestCapabilityDefaultsRef.current = next + void performSave({ capabilityDefaults: next }, true) // optimistic=true + return next + }) + }, [performSave]) + + // 提供商操作 + const updateProviderApiKey = useCallback((providerId: string, apiKey: string) => { + setProviders(prev => { + const next = prev.map(p => + p.id === providerId ? { ...p, apiKey, hasApiKey: !!apiKey } : p + ) + latestProvidersRef.current = next + void performSave(undefined, true) + return next + }) + }, [performSave]) + + const addProvider = useCallback((provider: Omit) => { + setProviders(prev => { + const targetProviderKey = getProviderKey(provider.id).toLowerCase() + if (prev.some(p => getProviderKey(p.id).toLowerCase() === targetProviderKey)) { + alert(t('providerIdExists')) + return prev + } + const newProvider: Provider = { ...provider, hasApiKey: !!provider.apiKey } + const next = [...prev, newProvider] + latestProvidersRef.current = next + + const providerKey = getProviderKey(provider.id) + if (providerKey === 'gemini-compatible') { + // 保存后直接 refetch:后端注入带完整 capabilities 的 Google 预设模型(disabled) + void performSave(undefined, true).then(() => void fetchConfig()) + } else { + void performSave(undefined, true) + } + return next + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [t, performSave]) + + const deleteProvider = useCallback((providerId: string) => { + if (PRESET_PROVIDERS.find(p => p.id === providerId)) { + alert(t('presetProviderCannotDelete')) + return + } + if (confirm(t('confirmDeleteProvider'))) { + setProviders(prev => { + const next = prev.filter(p => p.id !== providerId) + latestProvidersRef.current = next + return next + }) + setModels(prev => { + const nextModels = prev.filter(m => m.provider !== providerId) + setDefaultModels(prevDefaults => { + const updates: DefaultModels = { ...prevDefaults } + const remainingModelKeys = new Set(nextModels.map(m => m.modelKey)) + ; (['analysisModel', 'characterModel', 'locationModel', 'storyboardModel', 'editModel', 'videoModel', 'lipSyncModel'] as const) + .forEach(field => { + const current = updates[field] + if (current && !remainingModelKeys.has(current)) { + updates[field] = '' + } + }) + latestDefaultModelsRef.current = updates + return updates + }) + latestModelsRef.current = nextModels + void performSave(undefined, true) // 删除提供商:立刻保存 + return nextModels + }) + } + }, [t, performSave]) + + const updateProviderInfo = useCallback((providerId: string, name: string, baseUrl?: string) => { + setProviders(prev => { + const next = prev.map(p => + p.id === providerId ? { ...p, name, baseUrl } : p + ) + latestProvidersRef.current = next + void performSave(undefined, true) + return next + }) + }, [performSave]) + + const updateProviderBaseUrl = useCallback((providerId: string, baseUrl: string) => { + setProviders(prev => { + const next = prev.map(p => + p.id === providerId ? { ...p, baseUrl } : p + ) + latestProvidersRef.current = next + void performSave(undefined, true) + return next + }) + }, [performSave]) + + // 模型操作 + const toggleModel = useCallback((modelKey: string, providerId?: string) => { + if (isPresetComingSoonModelKey(modelKey)) { + return + } + setModels(prev => { + const next = prev.map(m => + m.modelKey === modelKey && (providerId ? m.provider === providerId : true) + ? { ...m, enabled: !m.enabled } + : m + ) + latestModelsRef.current = next + void performSave(undefined, true) // 开关操作:立刻保存 + return next + }) + }, [performSave]) + + const updateModel = useCallback((modelKey: string, updates: Partial, providerId?: string) => { + let nextModelKey = '' + setModels(prev => prev.map(m => { + if (m.modelKey !== modelKey || (providerId ? m.provider !== providerId : false)) return m + const mergedProvider = updates.provider ?? m.provider + const mergedModelId = updates.modelId ?? m.modelId + nextModelKey = encodeModelKey(mergedProvider, mergedModelId) + return { + ...m, + ...updates, + provider: mergedProvider, + modelId: mergedModelId, + modelKey: nextModelKey, + name: updates.name ?? m.name, + price: updates.price ?? m.price, + } + })) + if (nextModelKey && nextModelKey !== modelKey) { + setDefaultModels(prev => { + const next = { ...prev } + ; (['analysisModel', 'characterModel', 'locationModel', 'storyboardModel', 'editModel', 'videoModel', 'lipSyncModel'] as const) + .forEach(field => { + if (next[field] === modelKey) next[field] = nextModelKey + }) + return next + }) + } + }, []) + + const addModel = useCallback((model: Omit) => { + setModels(prev => { + const next = [ + ...prev, + { + ...model, + modelKey: model.modelKey || encodeModelKey(model.provider, model.modelId), + price: 0, + priceLabel: '--', + enabled: true, + }, + ] + latestModelsRef.current = next + void performSave(undefined, true) // 添加模型:立刻保存 + return next + }) + }, [performSave]) + + const deleteModel = useCallback((modelKey: string, providerId?: string) => { + if (PRESET_MODELS.find((model) => { + const presetModelKey = encodeModelKey(model.provider, model.modelId) + return presetModelKey === modelKey && (providerId ? model.provider === providerId : true) + })) { + alert(t('presetModelCannotDelete')) + return + } + if (confirm(t('confirmDeleteModel'))) { + setModels(prev => { + const nextModels = prev.filter(m => + !(m.modelKey === modelKey && (providerId ? m.provider === providerId : true)) + ) + setDefaultModels(prevDefaults => { + const nextDefaults = { ...prevDefaults } + const remainingModelKeys = new Set(nextModels.map(m => m.modelKey)) + ; (['analysisModel', 'characterModel', 'locationModel', 'storyboardModel', 'editModel', 'videoModel', 'lipSyncModel'] as const) + .forEach(field => { + const current = nextDefaults[field] + if (current && !remainingModelKeys.has(current)) { + nextDefaults[field] = '' + } + }) + latestDefaultModelsRef.current = nextDefaults + return nextDefaults + }) + latestModelsRef.current = nextModels + void performSave(undefined, true) // 删除模型:立刻保存 + return nextModels + }) + } + }, [t, performSave]) + + // 过滤器 + const getModelsByType = useCallback((type: CustomModel['type']) => { + return models.filter(m => m.type === type) + }, [models]) + + return { + providers, + models, + defaultModels, + capabilityDefaults, + loading, + saveStatus, + updateProviderApiKey, + updateProviderBaseUrl, + addProvider, + deleteProvider, + updateProviderInfo, + toggleModel, + updateModel, + addModel, + deleteModel, + updateDefaultModel, + updateCapabilityDefault, + getModelsByType + } +} diff --git a/src/app/[locale]/profile/components/api-config/index.ts b/src/app/[locale]/profile/components/api-config/index.ts new file mode 100644 index 0000000..6ca7a0d --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/index.ts @@ -0,0 +1,6 @@ +export { ProviderSection } from './ProviderSection' +export { DefaultModelSection } from './DefaultModelSection' +export { ProviderCard } from './ProviderCard' +export { useProviders } from './hooks' +export type { CustomModel, Provider } from './types' +export { getProviderDisplayName, getProviderKey, PRESET_PROVIDERS, encodeModelKey, parseModelKey, matchesModelKey } from './types' diff --git a/src/app/[locale]/profile/components/api-config/provider-card/ProviderAdvancedFields.tsx b/src/app/[locale]/profile/components/api-config/provider-card/ProviderAdvancedFields.tsx new file mode 100644 index 0000000..d2c4f9c --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/provider-card/ProviderAdvancedFields.tsx @@ -0,0 +1,496 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { AppIcon } from '@/components/ui/icons' +import { getProviderKey, isPresetComingSoonModel, type CustomModel } from '../types' +import type { UseProviderCardStateResult } from './hooks/useProviderCardState' +import type { + ProviderCardModelType, + ProviderCardProps, + ProviderCardTranslator, +} from './types' + +interface ProviderAdvancedFieldsProps { + provider: ProviderCardProps['provider'] + onToggleModel: ProviderCardProps['onToggleModel'] + onDeleteModel: ProviderCardProps['onDeleteModel'] + onUpdateModel: ProviderCardProps['onUpdateModel'] + t: ProviderCardTranslator + state: UseProviderCardStateResult +} + +const TypeIcon = ({ + type, + className = 'w-4 h-4', +}: { + type: ProviderCardModelType + className?: string +}) => { + switch (type) { + case 'llm': + return ( + + ) + case 'image': + return ( + + ) + case 'video': + return ( + + ) + case 'audio': + return ( + + ) + } +} + +const typeLabel = (type: ProviderCardModelType, t: ProviderCardTranslator) => { + switch (type) { + case 'llm': + return t('typeText') + case 'image': + return t('typeImage') + case 'video': + return t('typeVideo') + case 'audio': + return t('typeAudio') + } +} + +const MODEL_TYPES: readonly ProviderCardModelType[] = ['llm', 'image', 'video', 'audio'] + +function formatPriceAmount(amount: number): string { + const fixed = amount.toFixed(4) + const normalized = fixed.replace(/\.?0+$/, '') + return normalized || '0' +} + +function getModelPriceTexts(model: CustomModel, t: ProviderCardTranslator): string[] { + if ( + model.type === 'llm' + && typeof model.priceInput === 'number' + && Number.isFinite(model.priceInput) + && typeof model.priceOutput === 'number' + && Number.isFinite(model.priceOutput) + ) { + return [ + t('priceInput', { amount: `¥${formatPriceAmount(model.priceInput)}` }), + t('priceOutput', { amount: `¥${formatPriceAmount(model.priceOutput)}` }), + ] + } + + const label = typeof model.priceLabel === 'string' ? model.priceLabel.trim() : '' + if (label) { + return [label === '--' ? t('priceUnavailable') : `¥${label}`] + } + if (typeof model.price === 'number' && Number.isFinite(model.price) && model.price > 0) { + return [`¥${formatPriceAmount(model.price)}`] + } + return [t('priceUnavailable')] +} + +export function ProviderAdvancedFields({ + provider, + onToggleModel, + onDeleteModel, + onUpdateModel, + t, + state, +}: ProviderAdvancedFieldsProps) { + const providerKey = getProviderKey(provider.id) + const addableModelTypes = new Set( + providerKey === 'openai-compatible' + ? ['llm'] + : ['llm', 'image', 'video', 'audio'], + ) + const typesWithModels = useMemo( + () => + MODEL_TYPES.filter((type) => { + const modelsOfType = state.groupedModels[type] + return Array.isArray(modelsOfType) && modelsOfType.length > 0 + }), + [state.groupedModels], + ) + const [activeType, setActiveType] = useState( + typesWithModels[0] ?? null, + ) + const activeTypeSignature = typesWithModels.join('|') + + useEffect(() => { + if (typesWithModels.length === 0) { + setActiveType(null) + return + } + if (!activeType || !typesWithModels.includes(activeType)) { + setActiveType(typesWithModels[0]) + } + }, [activeType, activeTypeSignature, typesWithModels]) + + const currentType = activeType ?? typesWithModels[0] ?? null + const currentModels = currentType ? (state.groupedModels[currentType] ?? []) : [] + const shouldShowAddButton = + !!currentType + && addableModelTypes.has(currentType) + && state.showAddForm !== currentType + const defaultAddType: ProviderCardModelType = ( + providerKey === 'openrouter' || providerKey === 'openai-compatible' + ) ? 'llm' : 'image' + + return state.hasModels ? ( +
+
+
+ {typesWithModels.length > 0 && currentType && ( +
+ )} + {typesWithModels.map((type) => ( + + ))} +
+
+ + {currentType && ( +
+
+ + {typeLabel(currentType, t)} + + {currentModels.length} + +
+ {shouldShowAddButton && ( + + )} +
+ )} + + {currentType && state.showAddForm === currentType && addableModelTypes.has(currentType) && ( +
+
+ + state.setNewModel({ ...state.newModel, name: event.target.value }) + } + placeholder={t('modelDisplayName')} + className="glass-input-base px-3 py-1.5 text-[12px]" + autoFocus + /> + +
+
+ + state.setNewModel({ ...state.newModel, modelId: event.target.value }) + } + placeholder={t('modelActualId')} + className={`glass-input-base flex-1 px-3 py-1.5 text-[12px] font-mono ${currentType === 'video' && state.batchMode && provider.id === 'ark' ? 'rounded-r-none' : ''}`} + /> + {currentType === 'video' && state.batchMode && provider.id === 'ark' && ( + + -batch + + )} + +
+ {state.needsCustomPricing && ( +
+ + state.setNewModel({ ...state.newModel, priceInput: event.target.value }) + } + placeholder={t('pricingInputLabel')} + className="glass-input-base flex-1 px-3 py-1.5 text-[12px] font-mono" + /> + + state.setNewModel({ ...state.newModel, priceOutput: event.target.value }) + } + placeholder={t('pricingOutputLabel')} + className="glass-input-base flex-1 px-3 py-1.5 text-[12px] font-mono" + /> + ¥/M tokens +
+ )} + {currentType === 'video' && provider.id === 'ark' && ( +
+ + + {t('batchModeHalfPrice')} + +
+ )} +
+ )} + +
+
+
+ {currentModels.map((model, index) => ( + + ))} +
+
+
+
+ ) : ( +
+ {state.showAddForm === null ? ( +
+

{t('noModelsForProvider')}

+ +
+ ) : ( +
+
+ + state.setNewModel({ ...state.newModel, name: event.target.value }) + } + placeholder={t('modelDisplayName')} + className="glass-input-base px-3 py-1.5 text-[12px]" + autoFocus + /> + +
+
+ + state.setNewModel({ ...state.newModel, modelId: event.target.value }) + } + placeholder={t('modelActualId')} + className="glass-input-base flex-1 px-3 py-1.5 text-[12px] font-mono" + /> + +
+ {state.needsCustomPricing && state.showAddForm && ( +
+ + state.setNewModel({ ...state.newModel, priceInput: event.target.value }) + } + placeholder={t('pricingInputLabel')} + className="glass-input-base flex-1 px-3 py-1.5 text-[12px] font-mono" + /> + + state.setNewModel({ ...state.newModel, priceOutput: event.target.value }) + } + placeholder={t('pricingOutputLabel')} + className="glass-input-base flex-1 px-3 py-1.5 text-[12px] font-mono" + /> + ¥/M tokens +
+ )} +
+ )} +
+ ) +} + +interface ModelRowProps { + model: CustomModel + t: ProviderCardTranslator + state: UseProviderCardStateResult + onToggleModel: ProviderCardProps['onToggleModel'] + onDeleteModel: ProviderCardProps['onDeleteModel'] + onUpdateModel: ProviderCardProps['onUpdateModel'] +} + +function ModelRow({ + model, + t, + state, + onToggleModel, + onDeleteModel, + onUpdateModel, +}: ModelRowProps) { + const priceTexts = getModelPriceTexts(model, t) + const priceText = priceTexts.join(' / ') + const isComingSoonModel = isPresetComingSoonModel(model.provider, model.modelId) + const rowDisabledClass = model.enabled ? '' : 'opacity-50' + + return ( +
+ {state.editingModelId === model.modelKey ? ( + <> +
+ + state.setEditModel({ ...state.editModel, name: event.target.value }) + } + className="glass-input-base w-full px-3 py-1.5 text-[12px]" + placeholder={t('modelDisplayName')} + /> + + state.setEditModel({ ...state.editModel, modelId: event.target.value }) + } + className="glass-input-base w-full px-3 py-1.5 text-[12px] font-mono" + placeholder={t('modelActualId')} + /> +
{priceText}
+
+
+ + +
+ + ) : ( + <> +
+
+ + {model.name} + + {state.isDefaultModel(model) && model.enabled && ( + + {t('default')} + + )} + {priceText} +
+ {model.modelId} +
+ +
+ {!state.isPresetModel(model.modelKey) && onUpdateModel && ( + + )} + + + +
+ + )} +
+ ) +} diff --git a/src/app/[locale]/profile/components/api-config/provider-card/ProviderBaseFields.tsx b/src/app/[locale]/profile/components/api-config/provider-card/ProviderBaseFields.tsx new file mode 100644 index 0000000..53faf71 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/provider-card/ProviderBaseFields.tsx @@ -0,0 +1,163 @@ +'use client' + +import type { ProviderCardProps, ProviderCardTranslator } from './types' +import type { UseProviderCardStateResult } from './hooks/useProviderCardState' +import { AppIcon } from '@/components/ui/icons' + +interface ProviderBaseFieldsProps { + provider: ProviderCardProps['provider'] + t: ProviderCardTranslator + state: UseProviderCardStateResult +} + +export function ProviderBaseFields({ provider, t, state }: ProviderBaseFieldsProps) { + const baseUrlPlaceholder = (() => { + switch (state.providerKey) { + case 'gemini-compatible': + return 'https://your-api-domain.com' + case 'openai-compatible': + return 'https://api.openai.com/v1' + default: + return 'http://localhost:8000' + } + })() + + return ( + <> +
+
+ + {t('apiKeyLabel')} + + {state.isEditing ? ( +
+ state.setTempKey(event.target.value)} + placeholder={t('enterApiKey')} + className="glass-input-base flex-1 px-3 py-1.5 text-[12px]" + autoFocus + /> + + +
+ ) : ( +
+ {provider.hasApiKey ? ( + <> + + {state.showKey ? provider.apiKey : state.maskedKey} + +
+ + +
+ + ) : ( + + )} +
+ )} +
+
+ + {state.showBaseUrlEdit && ( +
+
+
+ + {t('baseUrl')} + + {state.isEditingUrl ? ( +
+ state.setTempUrl(event.target.value)} + placeholder={baseUrlPlaceholder} + className="glass-input-base flex-1 px-3 py-1.5 text-[12px] font-mono" + autoFocus + /> + + +
+ ) : ( +
+ {provider.baseUrl ? ( + + {provider.baseUrl} + + ) : ( + + )} + {provider.baseUrl && ( + + )} +
+ )} +
+
+
+ )} + + ) +} diff --git a/src/app/[locale]/profile/components/api-config/provider-card/ProviderCardShell.tsx b/src/app/[locale]/profile/components/api-config/provider-card/ProviderCardShell.tsx new file mode 100644 index 0000000..18aa9fb --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/provider-card/ProviderCardShell.tsx @@ -0,0 +1,127 @@ +'use client' + +import type { ReactNode } from 'react' +import type { ProviderCardProps, ProviderCardTranslator } from './types' +import type { UseProviderCardStateResult } from './hooks/useProviderCardState' +import { AppIcon } from '@/components/ui/icons' + +interface ProviderCardShellProps { + provider: ProviderCardProps['provider'] + onDeleteProvider: ProviderCardProps['onDeleteProvider'] + t: ProviderCardTranslator + state: UseProviderCardStateResult + children: ReactNode +} + +export function ProviderCardShell({ + provider, + onDeleteProvider, + t, + state, + children, +}: ProviderCardShellProps) { + return ( +
+
+
+
+ {provider.name.charAt(0)} +
+

{provider.name}

+ {provider.hasApiKey ? ( + + ) : ( + + )} +
+
+ {!state.isPresetProvider && onDeleteProvider && ( + + )} + {state.tutorial && ( + + )} +
+
+ + {state.showTutorial && state.tutorial && ( +
state.setShowTutorial(false)} + > +
event.stopPropagation()} + > +
+
+
+ +
+
+

+ {provider.name} {t('tutorial.title')} +

+

{t('tutorial.subtitle')}

+
+
+ +
+
+ {state.tutorial.steps.map((step, index) => ( +
+
+ {index + 1} +
+
+

+ {t(`tutorial.steps.${step.text}`)} +

+ {step.url && ( + + + {t('tutorial.openLink')} + + )} +
+
+ ))} +
+
+ +
+
+
+ )} + + {children} +
+ ) +} diff --git a/src/app/[locale]/profile/components/api-config/provider-card/hooks/useProviderCardState.ts b/src/app/[locale]/profile/components/api-config/provider-card/hooks/useProviderCardState.ts new file mode 100644 index 0000000..11069df --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/provider-card/hooks/useProviderCardState.ts @@ -0,0 +1,335 @@ +'use client' + +import { useState } from 'react' +import { + encodeModelKey, + PRESET_MODELS, + PRESET_PROVIDERS, + getProviderKey, + getProviderTutorial, + matchesModelKey, +} from '../../types' +import type { + ModelFormState, + ProviderCardGroupedModels, + ProviderCardModelType, + ProviderCardProps, + ProviderCardTranslator, +} from '../types' +import type { CustomModel } from '../../types' + +interface UseProviderCardStateParams { + provider: ProviderCardProps['provider'] + models: ProviderCardProps['models'] + allModels?: ProviderCardProps['allModels'] + defaultModels: ProviderCardProps['defaultModels'] + onUpdateApiKey: ProviderCardProps['onUpdateApiKey'] + onUpdateBaseUrl: ProviderCardProps['onUpdateBaseUrl'] + onUpdateModel: ProviderCardProps['onUpdateModel'] + onAddModel: ProviderCardProps['onAddModel'] + t: ProviderCardTranslator +} + +const EMPTY_MODEL_FORM: ModelFormState = { + name: '', + modelId: '', + priceInput: '', + priceOutput: '', +} + +/** + * Provider keys that require user-defined pricing when adding custom models + * (they are not in the built-in pricing catalog). + */ +const CUSTOM_PRICING_PROVIDER_KEYS = new Set(['openrouter', 'openai-compatible']) + +function toProviderCardModelType(type: CustomModel['type']): ProviderCardModelType | null { + if (type === 'llm' || type === 'image' || type === 'video' || type === 'audio') return type + if (type === 'lipsync') return 'audio' + return null +} + +export interface UseProviderCardStateResult { + providerKey: string + isPresetProvider: boolean + showBaseUrlEdit: boolean + tutorial: ReturnType + groupedModels: ProviderCardGroupedModels + hasModels: boolean + isEditing: boolean + isEditingUrl: boolean + showKey: boolean + tempKey: string + tempUrl: string + showTutorial: boolean + showAddForm: ProviderCardModelType | null + newModel: ModelFormState + batchMode: boolean + editingModelId: string | null + editModel: ModelFormState + maskedKey: string + isPresetModel: (modelKey: string) => boolean + isDefaultModel: (model: CustomModel) => boolean + setShowKey: (value: boolean) => void + setShowTutorial: (value: boolean) => void + setShowAddForm: (value: ProviderCardModelType | null) => void + setBatchMode: (value: boolean) => void + setNewModel: (value: ModelFormState) => void + setEditModel: (value: ModelFormState) => void + setTempKey: (value: string) => void + setTempUrl: (value: string) => void + startEditKey: () => void + startEditUrl: () => void + handleSaveKey: () => void + handleCancelEdit: () => void + handleSaveUrl: () => void + handleCancelUrlEdit: () => void + handleEditModel: (model: CustomModel) => void + handleCancelEditModel: () => void + handleSaveModel: (originalModelKey: string) => void + handleAddModel: (type: ProviderCardModelType) => void + handleCancelAdd: () => void + needsCustomPricing: boolean +} + +export function useProviderCardState({ + provider, + models, + allModels, + defaultModels, + onUpdateApiKey, + onUpdateBaseUrl, + onUpdateModel, + onAddModel, + t, +}: UseProviderCardStateParams): UseProviderCardStateResult { + const [isEditing, setIsEditing] = useState(false) + const [isEditingUrl, setIsEditingUrl] = useState(false) + const [showKey, setShowKey] = useState(false) + const [tempKey, setTempKey] = useState(provider.apiKey || '') + const [tempUrl, setTempUrl] = useState(provider.baseUrl || '') + const [showTutorial, setShowTutorial] = useState(false) + const [showAddForm, setShowAddForm] = useState(null) + const [newModel, setNewModel] = useState(EMPTY_MODEL_FORM) + const [batchMode, setBatchMode] = useState(false) + const [editingModelId, setEditingModelId] = useState(null) + const [editModel, setEditModel] = useState(EMPTY_MODEL_FORM) + + const providerKey = getProviderKey(provider.id) + const isPresetProvider = PRESET_PROVIDERS.some( + (presetProvider) => presetProvider.id === provider.id, + ) + const showBaseUrlEdit = + ['gemini-compatible', 'openai-compatible'].includes(providerKey) && + Boolean(onUpdateBaseUrl) + const tutorial = getProviderTutorial(provider.id) + + const groupedModels: ProviderCardGroupedModels = {} + for (const model of models) { + const groupedType = toProviderCardModelType(model.type) + if (!groupedType) continue + if (!groupedModels[groupedType]) { + groupedModels[groupedType] = [] + } + groupedModels[groupedType]!.push(model) + } + + const hasModels = Object.keys(groupedModels).length > 0 + const isPresetModel = (modelKey: string) => + PRESET_MODELS.some((model) => encodeModelKey(model.provider, model.modelId) === modelKey) + + const isDefaultModel = (model: CustomModel) => { + if (model.type === 'llm' && matchesModelKey(defaultModels.analysisModel, model.provider, model.modelId)) { + return true + } + + if (model.type === 'image') { + if (matchesModelKey(defaultModels.characterModel, model.provider, model.modelId)) return true + if (matchesModelKey(defaultModels.locationModel, model.provider, model.modelId)) return true + if (matchesModelKey(defaultModels.storyboardModel, model.provider, model.modelId)) return true + if (matchesModelKey(defaultModels.editModel, model.provider, model.modelId)) return true + } + + if (model.type === 'video' && matchesModelKey(defaultModels.videoModel, model.provider, model.modelId)) { + return true + } + + if (model.type === 'lipsync' && matchesModelKey(defaultModels.lipSyncModel, model.provider, model.modelId)) { + return true + } + + return false + } + + const startEditKey = () => { + setTempKey(provider.apiKey || '') + setIsEditing(true) + } + + const startEditUrl = () => { + setTempUrl(provider.baseUrl || '') + setIsEditingUrl(true) + } + + const handleSaveKey = () => { + onUpdateApiKey(provider.id, tempKey) + setIsEditing(false) + } + + const handleCancelEdit = () => { + setTempKey(provider.apiKey || '') + setIsEditing(false) + } + + const handleSaveUrl = () => { + onUpdateBaseUrl?.(provider.id, tempUrl) + setIsEditingUrl(false) + } + + const handleCancelUrlEdit = () => { + setTempUrl(provider.baseUrl || '') + setIsEditingUrl(false) + } + + const handleEditModel = (model: CustomModel) => { + setEditingModelId(model.modelKey) + setEditModel({ + name: model.name, + modelId: model.modelId, + }) + } + + const handleCancelEditModel = () => { + setEditingModelId(null) + setEditModel(EMPTY_MODEL_FORM) + } + + const handleSaveModel = (originalModelKey: string) => { + if (!editModel.name || !editModel.modelId) { + alert(t('fillComplete')) + return + } + + const nextModelKey = encodeModelKey(provider.id, editModel.modelId) + const all = allModels || models + const duplicate = all.some( + (model) => + model.modelKey === nextModelKey && + model.modelKey !== originalModelKey, + ) + + if (duplicate) { + alert(t('modelIdExists')) + return + } + + onUpdateModel?.(originalModelKey, { + name: editModel.name, + modelId: editModel.modelId, + }) + + handleCancelEditModel() + } + + const handleAddModel = (type: ProviderCardModelType) => { + if (!newModel.name || !newModel.modelId) { + alert(t('fillComplete')) + return + } + + const finalModelId = + type === 'video' && batchMode && provider.id === 'ark' + ? `${newModel.modelId}-batch` + : newModel.modelId + const finalModelKey = encodeModelKey(provider.id, finalModelId) + + const all = allModels || models + if (all.some((model) => model.modelKey === finalModelKey)) { + alert(t('modelIdExists')) + return + } + + const finalName = + type === 'video' && batchMode && provider.id === 'ark' + ? `${newModel.name} (Batch)` + : newModel.name + + // Build customPricing for providers that need it (OpenRouter = text only) + const needsCustomPricing = CUSTOM_PRICING_PROVIDER_KEYS.has(getProviderKey(provider.id)) + let customPricing: { input?: number; output?: number } | undefined + if (needsCustomPricing) { + const inputVal = parseFloat(newModel.priceInput || '') + const outputVal = parseFloat(newModel.priceOutput || '') + if (!Number.isFinite(inputVal) || inputVal < 0 || !Number.isFinite(outputVal) || outputVal < 0) { + alert(t('fillPricing')) + return + } + customPricing = { input: inputVal, output: outputVal } + } + + onAddModel({ + modelId: finalModelId, + modelKey: finalModelKey, + name: finalName, + type, + provider: provider.id, + price: 0, + ...(customPricing ? { customPricing } : {}), + }) + + setNewModel(EMPTY_MODEL_FORM) + setBatchMode(false) + setShowAddForm(null) + } + + const handleCancelAdd = () => { + setShowAddForm(null) + setNewModel(EMPTY_MODEL_FORM) + setBatchMode(false) + } + + const maskedKey = provider.apiKey ? '•'.repeat(20) : '' + + return { + providerKey, + isPresetProvider, + showBaseUrlEdit, + tutorial, + groupedModels, + hasModels, + isEditing, + isEditingUrl, + showKey, + tempKey, + tempUrl, + showTutorial, + showAddForm, + newModel, + batchMode, + editingModelId, + editModel, + maskedKey, + isPresetModel, + isDefaultModel, + setShowKey, + setShowTutorial, + setShowAddForm, + setBatchMode, + setNewModel, + setEditModel, + setTempKey, + setTempUrl, + startEditKey, + startEditUrl, + handleSaveKey, + handleCancelEdit, + handleSaveUrl, + handleCancelUrlEdit, + handleEditModel, + handleCancelEditModel, + handleSaveModel, + handleAddModel, + handleCancelAdd, + needsCustomPricing: CUSTOM_PRICING_PROVIDER_KEYS.has(providerKey), + } +} diff --git a/src/app/[locale]/profile/components/api-config/provider-card/types.ts b/src/app/[locale]/profile/components/api-config/provider-card/types.ts new file mode 100644 index 0000000..7f1d3ee --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/provider-card/types.ts @@ -0,0 +1,41 @@ +import type { CustomModel, Provider } from '../types' + +export interface ProviderCardDefaultModels { + analysisModel?: string + characterModel?: string + locationModel?: string + storyboardModel?: string + editModel?: string + videoModel?: string + lipSyncModel?: string +} + +export interface ProviderCardProps { + provider: Provider + models: CustomModel[] + allModels?: CustomModel[] + defaultModels: ProviderCardDefaultModels + onToggleModel: (modelKey: string) => void + onUpdateApiKey: (providerId: string, apiKey: string) => void + onUpdateBaseUrl?: (providerId: string, baseUrl: string) => void + onDeleteModel: (modelKey: string) => void + onUpdateModel?: (modelKey: string, updates: Partial) => void + onDeleteProvider?: (providerId: string) => void + onAddModel: (model: Omit) => void +} + +export interface ModelFormState { + name: string + modelId: string + priceInput?: string // LLM 输入 token 单价(用户输入字符串) + priceOutput?: string // LLM 输出 token 单价 +} + +export type ProviderCardModelType = 'llm' | 'image' | 'video' | 'audio' + +export type ProviderCardGroupedModels = Partial> + +export type ProviderCardTranslator = ( + key: string, + values?: Record, +) => string diff --git a/src/app/[locale]/profile/components/api-config/types.ts b/src/app/[locale]/profile/components/api-config/types.ts new file mode 100644 index 0000000..f079043 --- /dev/null +++ b/src/app/[locale]/profile/components/api-config/types.ts @@ -0,0 +1,354 @@ +/** + * API 配置类型定义和预设常量 + */ +import { + composeModelKey, + parseModelKeyStrict, + type ModelCapabilities, + type UnifiedModelType, +} from '@/lib/model-config-contract' + +// 统一提供商接口 +export interface Provider { + id: string + name: string + baseUrl?: string + apiKey?: string + hasApiKey?: boolean + apiMode?: 'gemini-sdk' // API 模式:gemini-sdk(默认) +} + +// 用户自定义定价(用于内置目录中没有的模型,如 OpenRouter) +export interface CustomModelPricing { + input?: number // LLM 输入 token 单价(每百万 token) + output?: number // LLM 输出 token 单价(每百万 token) +} + +// 模型接口 +export interface CustomModel { + modelId: string // 唯一标识符(如 anthropic/claude-sonnet-4.5) + modelKey: string // 唯一主键(provider::modelId) + name: string // 显示名称 + type: UnifiedModelType + provider: string + price: number + priceMin?: number + priceMax?: number + priceLabel?: string + priceInput?: number + priceOutput?: number + enabled: boolean + capabilities?: ModelCapabilities + customPricing?: CustomModelPricing +} + +export interface PricingDisplayItem { + min: number + max: number + label: string + input?: number + output?: number +} + +export type PricingDisplayMap = Record + +// API 配置响应 +export interface ApiConfig { + models: CustomModel[] + providers: Provider[] + pricingDisplay?: PricingDisplayMap +} + +type PresetModel = Omit + +// 预设模型 +export const PRESET_MODELS: PresetModel[] = [ + // 文本模型 + { modelId: 'google/gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', type: 'llm', provider: 'openrouter' }, + { modelId: 'google/gemini-3-pro-preview', name: 'Gemini 3 Pro', type: 'llm', provider: 'openrouter' }, + { modelId: 'google/gemini-3-flash-preview', name: 'Gemini 3 Flash', type: 'llm', provider: 'openrouter' }, + { modelId: 'anthropic/claude-sonnet-4.5', name: 'Claude Sonnet 4.5', type: 'llm', provider: 'openrouter' }, + { modelId: 'anthropic/claude-sonnet-4', name: 'Claude Sonnet 4', type: 'llm', provider: 'openrouter' }, + // Google AI Studio 文本模型 + { modelId: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', type: 'llm', provider: 'google' }, + { modelId: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', type: 'llm', provider: 'google' }, + { modelId: 'gemini-3-flash-preview', name: 'Gemini 3 Flash', type: 'llm', provider: 'google' }, + // 火山引擎 Doubao 文本模型 + { modelId: 'doubao-seed-1-8-251228', name: 'Doubao Seed 1.8', type: 'llm', provider: 'ark' }, + { modelId: 'doubao-seed-2-0-pro-260215', name: 'Doubao Seed 2.0 Pro', type: 'llm', provider: 'ark' }, + { modelId: 'doubao-seed-2-0-lite-260215', name: 'Doubao Seed 2.0 Lite', type: 'llm', provider: 'ark' }, + { modelId: 'doubao-seed-2-0-mini-260215', name: 'Doubao Seed 2.0 Mini', type: 'llm', provider: 'ark' }, + { modelId: 'doubao-seed-1-6-251015', name: 'Doubao Seed 1.6', type: 'llm', provider: 'ark' }, + { modelId: 'doubao-seed-1-6-lite-251015', name: 'Doubao Seed 1.6 Lite', type: 'llm', provider: 'ark' }, + + // 图像模型 + { modelId: 'banana', name: 'Banana Pro', type: 'image', provider: 'fal' }, + { modelId: 'banana-2', name: 'Banana 2', type: 'image', provider: 'fal' }, + { modelId: 'doubao-seedream-4-5-251128', name: 'Seedream 4.5', type: 'image', provider: 'ark' }, + { modelId: 'doubao-seedream-4-0-250828', name: 'Seedream 4.0', type: 'image', provider: 'ark' }, + { modelId: 'gemini-3-pro-image-preview', name: 'Banana Pro', type: 'image', provider: 'google' }, + { modelId: 'gemini-3.1-flash-image-preview', name: 'Nano Banana 2', type: 'image', provider: 'google' }, + { modelId: 'gemini-3-pro-image-preview-batch', name: 'Banana Pro (Batch)', type: 'image', provider: 'google' }, + { modelId: 'gemini-2.5-flash-image', name: 'Gemini 2.5 Flash Image', type: 'image', provider: 'google' }, + { modelId: 'imagen-4.0-generate-001', name: 'Imagen 4', type: 'image', provider: 'google' }, + { modelId: 'imagen-4.0-ultra-generate-001', name: 'Imagen 4 Ultra', type: 'image', provider: 'google' }, + { modelId: 'imagen-4.0-fast-generate-001', name: 'Imagen 4 Fast', type: 'image', provider: 'google' }, + // 视频模型 + { modelId: 'doubao-seedance-1-0-pro-fast-251015', name: 'Seedance 1.0 Pro Fast', type: 'video', provider: 'ark' }, + { modelId: 'doubao-seedance-1-0-lite-i2v-250428', name: 'Seedance 1.0 Lite', type: 'video', provider: 'ark' }, + { modelId: 'doubao-seedance-1-5-pro-251215', name: 'Seedance 1.5 Pro', type: 'video', provider: 'ark' }, + { modelId: 'doubao-seedance-2-0-260128', name: 'Seedance 2.0(待上线)', type: 'video', provider: 'ark' }, + { modelId: 'doubao-seedance-1-0-pro-250528', name: 'Seedance 1.0 Pro', type: 'video', provider: 'ark' }, + // Google Veo + { modelId: 'veo-3.1-generate-preview', name: 'Veo 3.1', type: 'video', provider: 'google' }, + { modelId: 'veo-3.1-fast-generate-preview', name: 'Veo 3.1 Fast', type: 'video', provider: 'google' }, + { modelId: 'veo-3.0-generate-001', name: 'Veo 3.0', type: 'video', provider: 'google' }, + { modelId: 'veo-3.0-fast-generate-001', name: 'Veo 3.0 Fast', type: 'video', provider: 'google' }, + { modelId: 'veo-2.0-generate-001', name: 'Veo 2.0', type: 'video', provider: 'google' }, + { modelId: 'fal-wan25', name: 'Wan 2.6', type: 'video', provider: 'fal' }, + { modelId: 'fal-veo31', name: 'Veo 3.1', type: 'video', provider: 'fal' }, + { modelId: 'fal-sora2', name: 'Sora 2', type: 'video', provider: 'fal' }, + { modelId: 'fal-ai/kling-video/v2.5-turbo/pro/image-to-video', name: 'Kling 2.5 Turbo Pro', type: 'video', provider: 'fal' }, + { modelId: 'fal-ai/kling-video/v3/standard/image-to-video', name: 'Kling 3 Standard', type: 'video', provider: 'fal' }, + { modelId: 'fal-ai/kling-video/v3/pro/image-to-video', name: 'Kling 3 Pro', type: 'video', provider: 'fal' }, + + // 音频模型 + { modelId: 'fal-ai/index-tts-2/text-to-speech', name: 'IndexTTS 2', type: 'audio', provider: 'fal' }, + // 口型同步模型 + { modelId: 'fal-ai/kling-video/lipsync/audio-to-video', name: 'Kling Lip Sync', type: 'lipsync', provider: 'fal' }, + { modelId: 'vidu-lipsync', name: 'Vidu Lip Sync', type: 'lipsync', provider: 'vidu' }, + + // MiniMax 视频模型 + { modelId: 'minimax-hailuo-2.3', name: 'Hailuo 2.3', type: 'video', provider: 'minimax' }, + { modelId: 'minimax-hailuo-2.3-fast', name: 'Hailuo 2.3 Fast', type: 'video', provider: 'minimax' }, + { modelId: 'minimax-hailuo-02', name: 'Hailuo 02', type: 'video', provider: 'minimax' }, + { modelId: 't2v-01', name: 'T2V-01', type: 'video', provider: 'minimax' }, + { modelId: 't2v-01-director', name: 'T2V-01 Director', type: 'video', provider: 'minimax' }, + + // Vidu 视频模型 + { modelId: 'viduq3-pro', name: 'Vidu Q3 Pro', type: 'video', provider: 'vidu' }, + { modelId: 'viduq2-pro-fast', name: 'Vidu Q2 Pro Fast', type: 'video', provider: 'vidu' }, + { modelId: 'viduq2-pro', name: 'Vidu Q2 Pro', type: 'video', provider: 'vidu' }, + { modelId: 'viduq2-turbo', name: 'Vidu Q2 Turbo', type: 'video', provider: 'vidu' }, + { modelId: 'viduq1', name: 'Vidu Q1', type: 'video', provider: 'vidu' }, + { modelId: 'viduq1-classic', name: 'Vidu Q1 Classic', type: 'video', provider: 'vidu' }, + { modelId: 'vidu2.0', name: 'Vidu 2.0', type: 'video', provider: 'vidu' }, +] + +const PRESET_COMING_SOON_MODEL_KEYS = new Set([ + encodeModelKey('ark', 'doubao-seedance-2-0-260128'), +]) + +export function isPresetComingSoonModel(provider: string, modelId: string): boolean { + return PRESET_COMING_SOON_MODEL_KEYS.has(encodeModelKey(provider, modelId)) +} + +export function isPresetComingSoonModelKey(modelKey: string): boolean { + return PRESET_COMING_SOON_MODEL_KEYS.has(modelKey) +} + +// 预设提供商(API Key 唯一归属于 provider id) +export const PRESET_PROVIDERS: Omit[] = [ + { id: 'ark', name: 'Volcengine Ark' }, + { id: 'google', name: 'Google AI Studio' }, + { id: 'openrouter', name: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1' }, + { id: 'minimax', name: 'MiniMax Hailuo' }, + { id: 'vidu', name: 'Vidu' }, + { id: 'fal', name: 'FAL' }, + { id: 'qwen', name: 'Qwen' }, +] + +const ZH_PROVIDER_NAME_MAP: Record = { + ark: '火山引擎 Ark', + minimax: '海螺 MiniMax', + vidu: '生数科技 Vidu', +} + +function isZhLocale(locale?: string): boolean { + return typeof locale === 'string' && locale.toLowerCase().startsWith('zh') +} + +export function resolvePresetProviderName(providerId: string, fallbackName: string, locale?: string): string { + if (!isZhLocale(locale)) return fallbackName + return ZH_PROVIDER_NAME_MAP[providerId] ?? fallbackName +} + +/** + * 提取提供商主键(用于多实例场景,如 gemini-compatible:uuid) + */ +export function getProviderKey(providerId?: string): string { + if (!providerId) return '' + const colonIndex = providerId.indexOf(':') + return colonIndex === -1 ? providerId : providerId.slice(0, colonIndex) +} + +/** + * 获取厂商的友好显示名称 + * @param providerId - 厂商ID(如 'ark', 'google') + * @returns 友好名称(如 '火山引擎(方舟)', 'Google AI Studio') + */ +export function getProviderDisplayName(providerId?: string, locale?: string): string { + if (!providerId) return '' + const providerKey = getProviderKey(providerId) + const provider = PRESET_PROVIDERS.find(p => p.id === providerKey) + if (!provider) return providerId + return resolvePresetProviderName(provider.id, provider.name, locale) +} + +/** + * 编码模型复合 Key(用于区分同名模型) + * @param provider - 厂商 ID + * @param modelId - 模型 ID + * @returns 复合 Key,格式为 `provider::modelId`(使用双冒号避免与 provider ID 中的冒号冲突) + */ +export function encodeModelKey(provider: string, modelId: string): string { + return composeModelKey(provider, modelId) +} + +/** + * 解析模型复合 Key + * @param key - 复合 Key(provider::modelId) + * @returns 解析后的 { provider, modelId },如果无法解析返回 null + */ +export function parseModelKey(key: string | undefined | null): { provider: string, modelId: string } | null { + const parsed = parseModelKeyStrict(key) + if (!parsed) return null + return { + provider: parsed.provider, + modelId: parsed.modelId, + } +} + +/** + * 检查一个复合 Key 是否匹配指定的模型 + * @param key - 复合 Key(provider::modelId) + * @param provider - 目标厂商 ID + * @param modelId - 目标模型 ID + * @returns 是否匹配 + */ +export function matchesModelKey(key: string | undefined | null, provider: string, modelId: string): boolean { + const parsed = parseModelKeyStrict(key) + if (!parsed) return false + return parsed.provider === provider && parsed.modelId === modelId +} + +// 教程步骤接口 +export interface TutorialStep { + text: string // 步骤描述 (i18n key) + url?: string // 可选的链接地址 +} + +// 厂商教程接口 +export interface ProviderTutorial { + providerId: string + steps: TutorialStep[] +} + +// 厂商开通教程配置 +// 注意: text 字段使用 i18n key, 翻译在 apiConfig.tutorials 下 +export const PROVIDER_TUTORIALS: ProviderTutorial[] = [ + { + providerId: 'ark', + steps: [ + { + text: 'ark_step1', + url: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D' + }, + { + text: 'ark_step2', + url: 'https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&advancedActiveKey=model' + } + ] + }, + { + providerId: 'openrouter', + steps: [ + { + text: 'openrouter_step1', + url: 'https://openrouter.ai/settings/keys' + } + ] + }, + { + providerId: 'fal', + steps: [ + { + text: 'fal_step1', + url: 'https://fal.ai/dashboard/keys' + } + ] + }, + { + providerId: 'google', + steps: [ + { + text: 'google_step1', + url: 'https://aistudio.google.com/api-keys' + } + ] + }, + { + providerId: 'minimax', + steps: [ + { + text: 'minimax_step1', + url: 'https://platform.minimaxi.com/user-center/basic-information/interface-key' + } + ] + }, + { + providerId: 'vidu', + steps: [ + { + text: 'vidu_step1', + url: 'https://platform.vidu.cn/api-keys' + } + ] + }, + { + providerId: 'gemini-compatible', + steps: [ + { + text: 'gemini_compatible_step1' + } + ] + }, + { + providerId: 'openai-compatible', + steps: [ + { + text: 'openai_compatible_step1' + } + ] + }, + { + providerId: 'qwen', + steps: [ + { + text: 'qwen_step1', + url: 'https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key' + } + ] + } +] + +/** + * 根据厂商ID获取教程配置 + * @param providerId - 厂商ID + * @returns 教程配置,如果不存在则返回 undefined + */ +export function getProviderTutorial(providerId: string): ProviderTutorial | undefined { + const providerKey = getProviderKey(providerId) + return PROVIDER_TUTORIALS.find(t => t.providerId === providerKey) +} + +/** + * 获取 Google 官方模型列表的克隆副本,provider 替换为指定 ID。 + * 用于 gemini-compatible 新增时自动预设模型。 + * 排除 batch 模型(Google 特有的异步批量处理)。 + */ +export function getGoogleCompatiblePresetModels(providerId: string): PresetModel[] { + return PRESET_MODELS + .filter((m) => m.provider === 'google' && !m.modelId.endsWith('-batch')) + .map((m) => ({ ...m, provider: providerId })) +} diff --git a/src/app/[locale]/profile/page.tsx b/src/app/[locale]/profile/page.tsx new file mode 100644 index 0000000..bb5f622 --- /dev/null +++ b/src/app/[locale]/profile/page.tsx @@ -0,0 +1,739 @@ +'use client' +import { logError as _ulogError } from '@/lib/logging/core' + +import { useCallback, useEffect, useState } from 'react' +import { useSession, signOut } from 'next-auth/react' +import { useRouter } from 'next/navigation' +import { useTranslations } from 'next-intl' +import Navbar from '@/components/Navbar' +import ApiConfigTab from './components/ApiConfigTab' +import { AppIcon, iconRegistry, type LucideIcon } from '@/components/ui/icons' + +interface BalanceInfo { + currency?: string + balance: number + frozenAmount: number + totalSpent: number +} + +interface Transaction { + id: string + type: 'recharge' | 'consume' + amount: number + balanceAfter: number + description: string | null + action: string | null + projectName: string | null + episodeNumber: number | null + episodeName: string | null + billingMeta: { + quantity?: number + unit?: string + model?: string + apiType?: string + resolution?: string + duration?: number + inputTokens?: number + outputTokens?: number + actualModels?: string[] + } | null + createdAt: string +} + +interface TransactionPagination { + page: number + pageSize: number + total: number + totalPages: number +} + +interface ProjectCost { + currency?: string + projectId: string + projectName: string + totalCost: number + recordCount: number +} + +interface CostByType { + apiType: string + _sum: { cost: number | null } + _count: number +} + +interface CostByAction { + action: string + _sum: { cost: number | null } + _count: number +} + +interface CostRecord { + id: string + apiType: string + model: string + action: string + quantity: number + unit: string + cost: number + createdAt: string +} + +interface ProjectDetails { + currency?: string + total: number + byType: CostByType[] + byAction: CostByAction[] + recentRecords: CostRecord[] +} + +// 类型对应的颜色 +const TYPE_COLORS: Record = { + image: { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' }, + video: { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' }, + text: { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' }, + tts: { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' }, + voice: { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' }, + voice_design: { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' }, + lip_sync: { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' }, +} + +function formatDate(dateString: string): string { + const date = new Date(dateString) + return date.toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) +} + +function getCurrencySymbol(currency: string): string { + const normalized = currency.toUpperCase() + if (normalized === 'USD') return '$' + if (normalized === 'EUR') return 'EUR ' + if (normalized === 'GBP') return 'GBP ' + return '¥' +} + +function formatMoney(amount: number, currency: string, digits = 2): string { + return `${getCurrencySymbol(currency)}${amount.toFixed(digits)}` +} + +// 交易流水图标映射(简洁黑白风格)- 使用统一 iconRegistry +const TX_ICON_MAP: Record = { + image: iconRegistry.image, + video: iconRegistry.clapperboard, + text: iconRegistry.brain, + voice: iconRegistry.mic, + tts: iconRegistry.mic, + 'voice-design': iconRegistry.audioWave, + 'lip-sync': iconRegistry.sparkles, +} + +function getTxIcon(tx: Transaction): LucideIcon { + if (tx.type === 'recharge') return iconRegistry.arrowDownCircle + const apiType = tx.billingMeta?.apiType as string | undefined + if (apiType && TX_ICON_MAP[apiType]) return TX_ICON_MAP[apiType] + return iconRegistry.bolt +} + +/** 根据 billingMeta 的 unit 字段生成人类可读的用量描述 */ +function formatBillingDetail( + meta: Transaction['billingMeta'], + translate: (key: string, values?: Record) => string, +): string | null { + if (!meta || !meta.unit) return null + const q = meta.quantity ?? 0 + switch (meta.unit) { + case 'image': + return meta.resolution + ? translate('billingDetail.imageWithRes', { count: q, resolution: meta.resolution }) + : translate('billingDetail.image', { count: q }) + case 'video': + return meta.resolution + ? translate('billingDetail.videoWithRes', { count: q, resolution: meta.resolution }) + : translate('billingDetail.video', { count: q }) + case 'token': { + const inT = meta.inputTokens ?? 0 + const outT = meta.outputTokens ?? 0 + if (inT > 0 || outT > 0) return translate('billingDetail.tokens', { count: (inT + outT).toLocaleString() }) + return q > 0 ? translate('billingDetail.tokens', { count: q.toLocaleString() }) : null + } + case 'second': + return translate('billingDetail.seconds', { count: q }) + case 'call': + return translate('billingDetail.calls', { count: q }) + default: + return q > 0 ? `${q} ${meta.unit}` : null + } +} + +export default function ProfilePage() { + const { data: session, status } = useSession() + const router = useRouter() + const t = useTranslations('profile') + const tc = useTranslations('common') + const tb = useTranslations('billing') + const [balance, setBalance] = useState(null) + const [transactions, setTransactions] = useState([]) + const [transactionPagination, setTransactionPagination] = useState(null) + const [projects, setProjects] = useState([]) + const [currency, setCurrency] = useState('CNY') + const [selectedProject, setSelectedProject] = useState('all') + const [projectDetails, setProjectDetails] = useState(null) + const [loading, setLoading] = useState(true) + const [detailsLoading, setDetailsLoading] = useState(false) + + // 主要分区:扣费记录 / API配置 + const [activeSection, setActiveSection] = useState<'billing' | 'apiConfig'>('apiConfig') + // 扣费记录内的子视图 + const [billingView, setBillingView] = useState<'transactions' | 'projects'>('transactions') + const [projectViewMode, setProjectViewMode] = useState<'summary' | 'records'>('summary') + const [recordsFilter, setRecordsFilter] = useState('all') + + // 账户流水筛选和分页状态 + const [txPage, setTxPage] = useState(1) + const [txType, setTxType] = useState<'all' | 'recharge' | 'consume'>('all') + const [txStartDate, setTxStartDate] = useState('') + const [txEndDate, setTxEndDate] = useState('') + const [showFilters, setShowFilters] = useState(false) + + const fetchTransactions = useCallback(async () => { + try { + const params = new URLSearchParams({ + page: txPage.toString(), + pageSize: '20', + }) + if (txType !== 'all') params.append('type', txType) + if (txStartDate) params.append('startDate', txStartDate) + if (txEndDate) params.append('endDate', txEndDate) + + const res = await fetch(`/api/user/transactions?${params}`) + if (res.ok) { + const data = await res.json() + if (typeof data.currency === 'string' && data.currency) setCurrency(data.currency) + setTransactions(data.transactions || []) + setTransactionPagination(data.pagination || null) + } + } catch (error) { + _ulogError('获取交易记录失败:', error) + } + }, [txEndDate, txPage, txStartDate, txType]) + + const fetchData = useCallback(async () => { + setLoading(true) + try { + const [balanceRes, costsRes] = await Promise.all([ + fetch('/api/user/balance'), + fetch('/api/user/costs') + ]) + if (balanceRes.ok) { + const payload = await balanceRes.json() + if (typeof payload.currency === 'string' && payload.currency) setCurrency(payload.currency) + setBalance({ + balance: Number(payload.balance || 0), + frozenAmount: Number(payload.frozenAmount || 0), + totalSpent: Number(payload.totalSpent || 0), + currency: typeof payload.currency === 'string' ? payload.currency : undefined, + }) + } + if (costsRes.ok) { + const data = await costsRes.json() + if (typeof data.currency === 'string' && data.currency) setCurrency(data.currency) + setProjects(data.byProject || []) + } + await fetchTransactions() + } catch (error) { + _ulogError('获取数据失败:', error) + } finally { + setLoading(false) + } + }, [fetchTransactions]) + + useEffect(() => { + if (status === 'loading') return + if (!session) { router.push('/auth/signin'); return } + void fetchData() + }, [fetchData, router, session, status]) + + useEffect(() => { + if (session) { + void fetchTransactions() + } + }, [fetchTransactions, session]) + + useEffect(() => { + if (selectedProject && selectedProject !== 'all') { + void fetchProjectDetails(selectedProject) + setProjectViewMode('summary') + setRecordsFilter('all') + } + }, [selectedProject]) + + async function fetchProjectDetails(projectId: string) { + setDetailsLoading(true) + try { + const res = await fetch(`/api/projects/${projectId}/costs`) + if (res.ok) { + const data = await res.json() + if (typeof data.currency === 'string' && data.currency) setCurrency(data.currency) + setProjectDetails({ + total: data.total || 0, + byType: data.byType || [], + byAction: data.byAction || [], + recentRecords: data.recentRecords || [], + currency: data.currency, + }) + } + } catch (error) { + _ulogError('获取项目费用失败:', error) + } finally { + setDetailsLoading(false) + } + } + + if (status === 'loading' || !session) { + return ( +
+
{tc('loading')}
+
+ ) + } + + const selectedProjectName = projects.find(p => p.projectId === selectedProject)?.projectName + const filteredRecords = projectDetails?.recentRecords?.filter(r => + recordsFilter === 'all' ? true : r.apiType === recordsFilter + ) || [] + const availableTypes = [...new Set(projectDetails?.recentRecords?.map(r => r.apiType) || [])] + + return ( +
+ + +
+
+ + {/* 左侧侧边栏 */} +
+
+ + {/* 用户信息 */} +
+
+

{session.user?.name || t('user')}

+

{t('personalAccount')}

+
+ + {/* 余额卡片 */} +
+
{t('availableBalance')}
+
{formatMoney(balance?.balance || 0, currency)}
+
+
+ {t('frozen')} + {formatMoney(balance?.frozenAmount || 0, currency)} +
+
+ {t('totalSpent')} + {formatMoney(balance?.totalSpent || 0, currency)} +
+
+
+
+ + {/* 导航菜单 */} + + + {/* 退出登录 */} + +
+
+ + {/* 右侧内容区 */} +
+
+ + {activeSection === 'apiConfig' ? ( + + ) : ( + <> + {/* 扣费记录标题栏 */} +
+
+ {/* 返回按钮 */} + {selectedProject !== 'all' && ( + + )} + + {/* 视图切换 */} + {(() => { + const tabs = ['transactions', 'projects'] as const + const activeTab = (billingView === 'transactions' && selectedProject === 'all') ? 'transactions' : 'projects' + const activeIdx = tabs.indexOf(activeTab) + return ( +
+
+
+ + +
+
+ ) + })()} +
+ + {/* 项目内视图切换 */} + {selectedProject !== 'all' && ( +
+
+ {(() => { + const modeTabs = ['summary', 'records'] as const + const modeIdx = modeTabs.indexOf(projectViewMode) + return ( +
+
+ + +
+ ) + })()} +
+ {projectViewMode === 'records' && ( + + )} +
+ )} +
+ + {/* 内容区域 */} +
+ {loading ? ( +
{[1, 2, 3, 4, 5].map(i =>
)}
+ ) : billingView === 'transactions' && selectedProject === 'all' ? ( + /* 账户流水 */ +
+ {/* 筛选按钮 */} +
+ +
+ + {/* 筛选栏 */} + {showFilters && ( +
+
+
+ + +
+
+ + { setTxStartDate(e.target.value); setTxPage(1) }} + className="glass-input-base w-full cursor-pointer px-3 py-2.5 text-sm" + /> +
+
+ + { setTxEndDate(e.target.value); setTxPage(1) }} + className="glass-input-base w-full cursor-pointer px-3 py-2.5 text-sm" + /> +
+ {(txType !== 'all' || txStartDate || txEndDate) && ( + + )} +
+
+ )} + + {/* 流水列表 */} +
+ {transactions.length > 0 ? ( +
+ {transactions.map(tx => ( +
+
+ {(() => { + const Icon = getTxIcon(tx) + return ( +
+ +
+ ) + })()} +
+
+ {tx.type === 'recharge' + ? (tx.description || t('recharge')) + : (tx.action + ? (t(`actionTypes.${tx.action}` as never) || tx.action) + : (tx.description || t('consume'))) + } +
+ {tx.type !== 'recharge' && (tx.billingMeta?.model || tx.projectName || tx.episodeNumber != null) && ( +
+ {tx.billingMeta?.model && ( + + + {tx.billingMeta.model} + + )} + {(() => { + const detail = formatBillingDetail(tx.billingMeta, t as (key: string, values?: Record) => string) + return detail ? ( + + + {detail} + + ) : null + })()} + {tx.projectName && ( + + + {tx.projectName} + + )} + {tx.episodeNumber != null && ( + + + {t('episodeLabel', { number: tx.episodeNumber })} + + )} +
+ )} +
{formatDate(tx.createdAt)}
+
+
+
+
+ {tx.type === 'recharge' ? '+' : ''}{formatMoney(Math.abs(tx.amount), currency)} +
+
{t('balanceAfter', { amount: formatMoney(tx.balanceAfter, currency) })}
+
+
+ ))} +
+ ) : ( +
+ +

{t('noTransactions')}

+
+ )} +
+ + {/* 分页 */} + {transactionPagination && transactionPagination.totalPages > 1 && ( +
+
+ {t('pagination', { total: transactionPagination.total, page: transactionPagination.page, totalPages: transactionPagination.totalPages })} +
+
+ + +
+
+ )} +
+ ) : selectedProject === 'all' ? ( + /* 项目列表 */ + projects.length > 0 ? ( +
+ {projects.map(p => ( +
setSelectedProject(p.projectId)} className="glass-surface-soft flex cursor-pointer items-center justify-between rounded-2xl border border-transparent p-4 transition-all hover:border-[var(--glass-stroke-focus)]"> +
+
{p.projectName}
+
{t('recordCount', { count: p.recordCount })}
+
+
{formatMoney(p.totalCost, currency)}
+
+ ))} +
+ ) : ( +

{t('noProjectCosts')}

+ ) + ) : detailsLoading ? ( +
{[1, 2, 3].map(i =>
)}
+ ) : projectDetails ? ( + projectViewMode === 'summary' ? ( + /* 汇总视图 */ +
+
+

{selectedProjectName}

+
{t('totalCost', { amount: formatMoney(projectDetails.total, currency) })}
+
+ +
+
{t('byType')}
+
+ {projectDetails.byType.map(item => { + const colors = TYPE_COLORS[item.apiType] || { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' } + return ( +
{ setProjectViewMode('records'); setRecordsFilter(item.apiType) }} + className={`${colors.bg} ${colors.border} border rounded-2xl p-4 cursor-pointer transition-colors hover:border-[var(--glass-stroke-focus)]`} + > +
{t(`apiTypes.${item.apiType}` as never) || item.apiType}
+
{formatMoney(item._sum.cost || 0, currency)}
+
{item._count} {t('times')}
+
+ ) + })} +
+
+ +
+
{t('byAction')}
+
+ {projectDetails.byAction.map(item => ( +
+
+
{t(`actionTypes.${item.action.replace(/-/g, '_')}` as never) || item.action}
+ {item._count} {t('times')} +
+
{formatMoney(item._sum.cost || 0, currency)}
+
+ ))} +
+
+
+ ) : ( + /* 流水视图 */ +
+ {filteredRecords.length > 0 ? filteredRecords.map(record => { + const colors = TYPE_COLORS[record.apiType] || { bg: 'bg-[var(--glass-bg-muted)]', text: 'text-[var(--glass-text-secondary)]', border: 'border-[var(--glass-stroke-base)]' } + return ( +
+
+
+ {(t(`apiTypes.${record.apiType}` as never) || record.apiType).charAt(0).toUpperCase()} +
+
+
{t(`actionTypes.${record.action.replace(/-/g, '_')}` as never) || record.action}
+
{record.model} · {formatDate(record.createdAt)}
+
+
+
{formatMoney(record.cost, currency, 4)}
+
+ ) + }) : ( +
{t('noRecords')}
+ )} +
+ ) + ) : ( +

{t('noDetails')}

+ )} +
+ + )} +
+
+
+
+
+ ) +} diff --git a/src/app/[locale]/providers.tsx b/src/app/[locale]/providers.tsx new file mode 100644 index 0000000..ccc42be --- /dev/null +++ b/src/app/[locale]/providers.tsx @@ -0,0 +1,20 @@ +'use client' + +import { SessionProvider } from "next-auth/react" +import { ToastProvider } from "@/contexts/ToastContext" +import { QueryProvider } from "@/components/providers/QueryProvider" + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ) +} diff --git a/src/app/[locale]/workspace/[projectId]/components/Sidebar.tsx b/src/app/[locale]/workspace/[projectId]/components/Sidebar.tsx new file mode 100644 index 0000000..013ce4f --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/components/Sidebar.tsx @@ -0,0 +1,359 @@ +'use client' +import { logError as _ulogError } from '@/lib/logging/core' +import { useTranslations } from 'next-intl' + +import { useState, useRef, useEffect } from 'react' +import { AppIcon } from '@/components/ui/icons' + +interface Episode { + id: string + episodeNumber: number + name: string + description?: string | null +} + +interface SidebarProps { + projectId: string + projectName: string + episodes: Episode[] + currentEpisodeId: string | null + onEpisodeSelect: (id: string) => void + onEpisodeCreate: (name: string, description?: string) => Promise + onEpisodeDelete: (id: string) => Promise + onEpisodeRename: (id: string, newName: string) => Promise + onGlobalAssetsClick: () => void + isGlobalAssetsView: boolean +} + +export default function Sidebar({ + projectId, + projectName, + episodes, + currentEpisodeId, + onEpisodeSelect, + onEpisodeCreate, + onEpisodeDelete, + onEpisodeRename, + onGlobalAssetsClick, + isGlobalAssetsView +}: SidebarProps) { + const t = useTranslations('workspaceDetail') + const [isExpanded, setIsExpanded] = useState(false) + void projectId + const [isCreating, setIsCreating] = useState(false) + const [newEpisodeName, setNewEpisodeName] = useState('') + const [editingId, setEditingId] = useState(null) + const [editingName, setEditingName] = useState('') + const [deleteConfirmId, setDeleteConfirmId] = useState(null) + + // 可拖动位置 + const [position, setPosition] = useState({ y: 200 }) // 初始Y位置 + const [isDragging, setIsDragging] = useState(false) + const dragStartY = useRef(0) + const dragStartPos = useRef(0) + + // 拖动逻辑 + const handleDragStart = (e: React.MouseEvent) => { + e.preventDefault() + setIsDragging(true) + dragStartY.current = e.clientY + dragStartPos.current = position.y + } + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging) return + const deltaY = e.clientY - dragStartY.current + const newY = Math.max(100, Math.min(window.innerHeight - 200, dragStartPos.current + deltaY)) + setPosition({ y: newY }) + } + + const handleMouseUp = () => { + setIsDragging(false) + } + + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + }, [isDragging]) + + // 创建剧集 + const handleCreate = async () => { + if (!newEpisodeName.trim()) return + try { + await onEpisodeCreate(newEpisodeName.trim()) + setNewEpisodeName('') + setIsCreating(false) + } catch (err) { + _ulogError('创建剧集失败:', err) + } + } + + // 重命名剧集 + const handleRename = async (id: string) => { + if (!editingName.trim()) return + try { + await onEpisodeRename(id, editingName.trim()) + setEditingId(null) + setEditingName('') + } catch (err) { + _ulogError('重命名失败:', err) + } + } + + // 删除剧集 + const handleDelete = async (id: string) => { + try { + await onEpisodeDelete(id) + setDeleteConfirmId(null) + } catch (err) { + _ulogError('删除失败:', err) + } + } + + return ( + <> + {/* 触发条 - 固定在左侧,可拖动 */} +
+ {/* 拖动手柄 + 触发按钮 */} +
+ {/* 拖动手柄 */} +
+
+
+
+
+
+
+ + {/* 展开按钮 */} +
setIsExpanded(!isExpanded)} + > + + + {t('episode')} + +
+
+
+ + {/* 弹出面板 */} + {isExpanded && ( + <> + {/* 背景遮罩 */} +
setIsExpanded(false)} + /> + + {/* 侧边面板 */} +
+ {/* 标题栏 */} +
+
+
+

+ + {t('sidebar.listTitle')} +

+

+ {projectName} +

+
+ + {t('sidebar.episodeCount', { count: episodes.length })} + +
+
+ + {/* 全局资产入口 */} +
+ +
+ + {/* 剧集列表 */} +
+ {episodes.length === 0 ? ( +
+ {t('sidebar.empty')} +
+ ) : ( + episodes.map((ep) => ( +
+ {editingId === ep.id ? ( + // 编辑模式 +
+ setEditingName(e.target.value)} + className="glass-input-base flex-1 px-2 py-1.5 text-sm" + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') handleRename(ep.id) + if (e.key === 'Escape') setEditingId(null) + }} + /> + +
+ ) : deleteConfirmId === ep.id ? ( + // 删除确认 +
+

{t('sidebar.deleteConfirm', { name: ep.name })}

+
+ + +
+
+ ) : ( + // 正常显示 + + +
+ + )} +
+ )) + )} +
+ + {/* 添加剧集 */} +
+ {isCreating ? ( +
+ setNewEpisodeName(e.target.value)} + placeholder={t('sidebar.newEpisodePlaceholder')} + className="glass-input-base w-full px-3 py-2 text-sm rounded-lg" + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter') handleCreate() + if (e.key === 'Escape') { + setIsCreating(false) + setNewEpisodeName('') + } + }} + /> +
+ + +
+
+ ) : ( + + )} +
+
+ + )} + + ) +} diff --git a/src/app/[locale]/workspace/[projectId]/episode-selection.ts b/src/app/[locale]/workspace/[projectId]/episode-selection.ts new file mode 100644 index 0000000..d522d22 --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/episode-selection.ts @@ -0,0 +1,14 @@ +export interface EpisodeLike { + id: string +} + +export function resolveSelectedEpisodeId( + episodes: ReadonlyArray, + urlEpisodeId: string | null, +): string | null { + if (episodes.length === 0) return null + if (urlEpisodeId && episodes.some((episode) => episode.id === urlEpisodeId)) { + return urlEpisodeId + } + return episodes[0].id +} diff --git a/src/app/[locale]/workspace/[projectId]/hooks/useProject.ts b/src/app/[locale]/workspace/[projectId]/hooks/useProject.ts new file mode 100644 index 0000000..27cce6e --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/hooks/useProject.ts @@ -0,0 +1,146 @@ +import { logError as _ulogError } from '@/lib/logging/core' +import { useState, useCallback } from 'react' +import { Project } from '@/types/project' + +/** + * 刷新范围 + * - all: 刷新项目数据 + 资产数据 + * - project: 只刷新项目数据 + * - assets: 只刷新资产数据 + */ +export type RefreshScope = 'all' | 'project' | 'assets' + +/** + * 刷新模式 + * - full: 显示 loading 状态 + * - silent: 静默刷新,不显示 loading + */ +export type RefreshMode = 'full' | 'silent' + +/** + * 刷新选项 + */ +export interface RefreshOptions { + scope?: RefreshScope // 默认 'all' + mode?: RefreshMode // 默认 'silent' +} + +/** + * 通用项目数据管理Hook + * + * 🔥 V2: 统一刷新架构 + * - 单一 refresh(options) 函数,替代原有的 loadProject/loadAssets/silentRefresh/silentRefreshAssets + * - 通过 scope 和 mode 参数控制刷新行为 + * - 消除刷新行为不一致问题 + */ +export function useProject(projectId: string) { + const [project, setProject] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [assetsLoaded, setAssetsLoaded] = useState(false) + const [assetsLoading, setAssetsLoading] = useState(false) + + /** + * 🔥 统一刷新函数 + * + * @param options.scope - 刷新范围:'all' | 'project' | 'assets',默认 'all' + * @param options.mode - 刷新模式:'full' | 'silent',默认 'silent' + * + * 调用示例: + * - refresh() → 静默刷新全部(最常用) + * - refresh({ scope: 'assets' }) → 只刷新资产 + * - refresh({ scope: 'project' }) → 只刷新项目(不刷资产) + * - refresh({ mode: 'full' }) → 完整刷新带 loading + */ + const refresh = useCallback(async (options: RefreshOptions = {}) => { + const { scope = 'all', mode = 'silent' } = options + + // 完整刷新模式:显示 loading + if (mode === 'full') { + setLoading(true) + setError(null) + } + + // 资产刷新时显示 assetsLoading + if (scope === 'assets') { + setAssetsLoading(true) + } + + try { + // 刷新项目数据 + if (scope === 'all' || scope === 'project') { + const res = await fetch(`/api/projects/${projectId}/data`) + if (!res.ok) { + const errorData = await res.json() + throw new Error(errorData.error || 'Failed to load project') + } + const data = await res.json() + setProject(data.project) + + // 完整刷新时重置资产加载状态 + if (mode === 'full') { + setAssetsLoaded(false) + } + } + + // 刷新资产数据 + if (scope === 'all' || scope === 'assets') { + const res = await fetch(`/api/projects/${projectId}/assets`) + if (res.ok) { + const assets = await res.json() + setProject(prev => { + if (!prev?.novelPromotionData) return prev + return { + ...prev, + novelPromotionData: { + ...prev.novelPromotionData, + characters: assets.characters || [], + locations: assets.locations || [] + } + } + }) + setAssetsLoaded(true) + } + } + } catch (err: unknown) { + _ulogError('Refresh error:', err) + if (mode === 'full') { + setError(getErrorMessage(err)) + } + // 静默刷新不设置错误状态,避免干扰用户 + } finally { + if (mode === 'full') { + setLoading(false) + } + if (scope === 'assets') { + setAssetsLoading(false) + } + } + }, [projectId]) + + /** + * 更新项目数据(乐观更新) + */ + const updateProject = useCallback((updates: Partial) => { + setProject(prev => prev ? { ...prev, ...updates } : null) + }, []) + + return { + // 状态 + project, + loading, + error, + assetsLoaded, + assetsLoading, + + // 🔥 统一刷新函数 + refresh, + + // 乐观更新 + updateProject + } +} + const getErrorMessage = (err: unknown): string => { + if (err instanceof Error) return err.message + return String(err) + } diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/NovelPromotionWorkspace.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/NovelPromotionWorkspace.tsx new file mode 100644 index 0000000..fa11c2b --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/NovelPromotionWorkspace.tsx @@ -0,0 +1,132 @@ +'use client' + +import ProgressToast from '@/components/ProgressToast' +import ConfirmDialog from '@/components/ConfirmDialog' +import { AnimatedBackground } from '@/components/ui/SharedComponents' +import { WorkspaceProvider } from './WorkspaceProvider' +import WorkspaceRunStreamConsoles from './components/WorkspaceRunStreamConsoles' +import WorkspaceStageContent from './components/WorkspaceStageContent' +import WorkspaceAssetLibraryModal from './components/WorkspaceAssetLibraryModal' +import WorkspaceHeaderShell from './components/WorkspaceHeaderShell' +import { WorkspaceStageRuntimeProvider } from './WorkspaceStageRuntimeContext' +import { useNovelPromotionWorkspaceController } from './hooks/useNovelPromotionWorkspaceController' +import type { NovelPromotionWorkspaceProps } from './types' +import '@/styles/animations.css' + +function NovelPromotionWorkspaceContent(props: NovelPromotionWorkspaceProps) { + const vm = useNovelPromotionWorkspaceController(props) + + const { + project, + projectId, + episodeId, + episodes = [], + onEpisodeSelect, + onEpisodeCreate, + onEpisodeRename, + onEpisodeDelete, + } = props + + if (!vm.project.projectData) { + return
{vm.i18n.tc('loading')}
+ } + + return ( +
+ + + vm.ui.setIsSettingsModalOpen(false)} + onCloseWorldContextModal={() => vm.ui.setIsWorldContextModalOpen(false)} + availableModels={vm.ui.userModelsForSettings || undefined} + modelsLoaded={vm.ui.userModelsLoaded} + artStyle={vm.project.artStyle} + analysisModel={vm.project.analysisModel} + characterModel={vm.project.characterModel} + locationModel={vm.project.locationModel} + storyboardModel={vm.project.storyboardModel} + editModel={vm.project.editModel} + videoModel={vm.project.videoModel} + capabilityOverrides={vm.project.capabilityOverrides} + videoRatio={vm.project.videoRatio} + ttsRate={vm.project.ttsRate !== undefined && vm.project.ttsRate !== null ? String(vm.project.ttsRate) : undefined} + onUpdateConfig={vm.actions.handleUpdateConfig} + globalAssetText={vm.project.globalAssetText} + projectName={project.name} + episodes={episodes} + currentEpisodeId={episodeId} + onEpisodeSelect={onEpisodeSelect} + onEpisodeCreate={onEpisodeCreate} + onEpisodeRename={onEpisodeRename} + onEpisodeDelete={onEpisodeDelete} + capsuleNavItems={vm.stageNav.capsuleNavItems} + currentStage={vm.stageNav.currentStage} + onStageChange={vm.stageNav.handleStageChange} + projectId={projectId} + episodeId={episodeId} + onOpenAssetLibrary={() => vm.ui.openAssetLibrary()} + onOpenSettingsModal={() => vm.ui.setIsSettingsModalOpen(true)} + onRefresh={() => vm.ui.onRefresh({ mode: 'full' })} + assetLibraryLabel={vm.i18n.t('buttons.assetLibrary')} + settingsLabel={vm.i18n.t('buttons.settings')} + refreshTitle={vm.i18n.t('buttons.refreshData')} + /> + +
+ + + + + 0} + hasLocations={vm.project.projectLocations.length > 0} + projectId={projectId} + isAnalyzingAssets={vm.execution.isAssetAnalysisRunning} + focusCharacterId={vm.ui.assetLibraryFocusCharacterId} + focusCharacterRequestId={vm.ui.assetLibraryFocusRequestId} + triggerGlobalAnalyze={vm.ui.triggerGlobalAnalyzeOnOpen} + onGlobalAnalyzeComplete={() => vm.ui.setTriggerGlobalAnalyzeOnOpen(false)} + /> + + {vm.execution.showCreatingToast && ( + + )} + + + + +
+
+ ) +} + +export default function NovelPromotionWorkspace(props: NovelPromotionWorkspaceProps) { + const { projectId, episodeId } = props + return ( + + + + ) +} diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/StageNavigation.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/StageNavigation.tsx new file mode 100644 index 0000000..06ece1c --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/StageNavigation.tsx @@ -0,0 +1,99 @@ +/** + * 小说推文模式 - 阶段导航组件 + */ + +import Link from 'next/link' +import { useTranslations } from 'next-intl' +import { AppIcon } from '@/components/ui/icons' + +interface StageNavigationProps { + projectId: string // 用于构建链接 + episodeId?: string | null // 当前剧集ID,用于新标签页打开时保持剧集 + currentStage: string + hasNovelText: boolean // 是否有文本输入(用于启用配音阶段) + hasAudio: boolean + hasAssets: boolean + hasStoryboards: boolean + hasTextStoryboards: boolean // 是否有文字分镜(用于启用分镜面板) + hasVideos?: boolean + hasVoiceLines?: boolean // 是否有配音台词 + isDisabled: boolean + onStageClick: (stage: string) => void +} + +export function StageNavigation({ + projectId, + episodeId, + currentStage, + hasNovelText, + hasAudio, + hasAssets, + hasStoryboards, + hasTextStoryboards, + hasVideos, + hasVoiceLines, + isDisabled, + onStageClick +}: StageNavigationProps) { + const t = useTranslations('stages') + // 如果 currentStage 是旧的 'text-storyboard',自动重定向到 'storyboard' + const effectiveStage = currentStage === 'text-storyboard' ? 'storyboard' : currentStage + + const stages = [ + { id: 'config', label: t('config'), enabled: true }, + { id: 'assets', label: t('assets'), enabled: hasAudio || hasAssets }, + { id: 'storyboard', label: t('storyboard'), enabled: hasTextStoryboards || hasStoryboards }, + { id: 'videos', label: t('videos'), enabled: hasStoryboards || hasVideos }, + // 配音阶段只要有文本输入就可以启用,不受其他条件限制 + { id: 'voice', label: t('voice'), enabled: hasNovelText || hasVoiceLines } + ] + + return ( +
+ {stages.map((stage, index) => { + const isEnabled = stage.enabled && !isDisabled + const isCurrent = effectiveStage === stage.id + // 构建 URL,包含 episode 参数以支持新标签页打开时保持当前剧集 + const href = episodeId + ? `/workspace/${projectId}?stage=${stage.id}&episode=${episodeId}` + : `/workspace/${projectId}?stage=${stage.id}` + + const className = `px-5 py-2.5 rounded-xl transition-all font-medium inline-block ${isCurrent + ? 'bg-[var(--glass-accent-from)] text-white shadow-md' + : isEnabled + ? 'bg-[var(--glass-bg-muted)] text-[var(--glass-text-secondary)] hover:bg-[var(--glass-bg-muted)] cursor-pointer' + : 'bg-[var(--glass-bg-muted)] text-[var(--glass-text-tertiary)] cursor-not-allowed pointer-events-none' + }` + + return ( +
+ {isEnabled ? ( + { + // 左键点击时阻止默认行为,使用 onStageClick + if (e.button === 0 && !e.ctrlKey && !e.metaKey && !e.shiftKey) { + e.preventDefault() + onStageClick(stage.id) + } + // 中键点击或 Ctrl/Cmd+点击 会使用默认的链接行为打开新标签 + }} + className={className} + > + {stage.label} + + ) : ( + + {stage.label} + + )} + {index < stages.length - 1 && ( + + )} +
+ ) + })} +
+ ) +} + diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/WorkspaceProvider.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/WorkspaceProvider.tsx new file mode 100644 index 0000000..c8865bd --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/WorkspaceProvider.tsx @@ -0,0 +1,101 @@ +'use client' + +import { + createContext, + useCallback, + useContext, + useMemo, + useRef, + type ReactNode, +} from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { queryKeys } from '@/lib/query/keys' +import { useSSE } from '@/lib/query/hooks/useSSE' +import type { SSEEvent } from '@/lib/task/types' + +type RefreshScope = 'all' | 'assets' | 'project' +type RefreshOptions = { scope?: string; mode?: string } +type TaskEventListener = (event: SSEEvent) => void + +interface WorkspaceContextValue { + projectId: string + episodeId?: string + refreshData: (scope?: RefreshScope) => Promise + onRefresh: (options?: RefreshOptions) => Promise + subscribeTaskEvents: (listener: TaskEventListener) => () => void +} + +interface WorkspaceProviderProps { + projectId: string + episodeId?: string + children: ReactNode +} + +const WorkspaceContext = createContext(null) + +export function WorkspaceProvider({ projectId, episodeId, children }: WorkspaceProviderProps) { + const queryClient = useQueryClient() + const listenersRef = useRef(new Set()) + + const refreshData = useCallback(async (scope?: RefreshScope) => { + const promises: Promise[] = [] + + if (!scope || scope === 'all' || scope === 'project') { + promises.push(queryClient.refetchQueries({ queryKey: queryKeys.projectData(projectId) })) + } + + if (!scope || scope === 'all' || scope === 'assets') { + promises.push(queryClient.refetchQueries({ queryKey: queryKeys.projectAssets.all(projectId) })) + } + + if (episodeId) { + promises.push(queryClient.refetchQueries({ queryKey: queryKeys.episodeData(projectId, episodeId) })) + promises.push(queryClient.refetchQueries({ queryKey: queryKeys.storyboards.all(episodeId) })) + promises.push(queryClient.refetchQueries({ queryKey: queryKeys.voiceLines.all(episodeId) })) + } + + await Promise.all(promises) + }, [episodeId, projectId, queryClient]) + + const onRefresh = useCallback(async (options?: RefreshOptions) => { + await refreshData(options?.scope as RefreshScope | undefined) + }, [refreshData]) + + const subscribeTaskEvents = useCallback((listener: TaskEventListener) => { + listenersRef.current.add(listener) + return () => { + listenersRef.current.delete(listener) + } + }, []) + + const handleTaskEvent = useCallback((event: SSEEvent) => { + for (const listener of listenersRef.current) { + listener(event) + } + }, []) + + useSSE({ + projectId, + episodeId, + enabled: !!projectId, + onEvent: handleTaskEvent, + }) + + const value = useMemo(() => ({ + projectId, + episodeId, + refreshData, + onRefresh, + subscribeTaskEvents, + }), [episodeId, onRefresh, projectId, refreshData, subscribeTaskEvents]) + + return {children} +} + +export function useWorkspaceProvider() { + const context = useContext(WorkspaceContext) + if (!context) { + throw new Error('useWorkspaceProvider must be used within WorkspaceProvider') + } + return context +} diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/WorkspaceStageRuntimeContext.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/WorkspaceStageRuntimeContext.tsx new file mode 100644 index 0000000..87c3bfe --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/WorkspaceStageRuntimeContext.tsx @@ -0,0 +1,80 @@ +'use client' + +import { createContext, useContext, type ReactNode } from 'react' +import type { CapabilitySelections, ModelCapabilities } from '@/lib/model-config-contract' +import type { VideoPricingTier } from '@/lib/model-pricing/video-tier' +import type { BatchVideoGenerationParams, VideoGenerationOptions } from './components/video' + +export interface WorkspaceStageVideoModelOption { + value: string + label: string + provider?: string + providerName?: string + capabilities?: ModelCapabilities + videoPricingTiers?: VideoPricingTier[] +} + +export interface WorkspaceStageRuntimeValue { + assetsLoading: boolean + isSubmittingTTS: boolean + isTransitioning: boolean + isConfirmingAssets: boolean + videoRatio: string | null | undefined + artStyle: string | null | undefined + videoModel: string | null | undefined + capabilityOverrides: CapabilitySelections + userVideoModels: WorkspaceStageVideoModelOption[] + onNovelTextChange: (value: string) => Promise + onVideoRatioChange: (value: string) => Promise + onArtStyleChange: (value: string) => Promise + onRunStoryToScript: () => Promise + onClipUpdate: (clipId: string, data: unknown) => Promise + onOpenAssetLibrary: () => void + onRunScriptToStoryboard: () => Promise + onStageChange: (stage: string) => void + onGenerateVideo: ( + storyboardId: string, + panelIndex: number, + model?: string, + firstLastFrame?: { + lastFrameStoryboardId: string + lastFramePanelIndex: number + flModel: string + customPrompt?: string + }, + generationOptions?: VideoGenerationOptions, + panelId?: string, + ) => Promise + onGenerateAllVideos: (options?: BatchVideoGenerationParams) => Promise + onUpdateVideoPrompt: ( + storyboardId: string, + panelIndex: number, + value: string, + field?: 'videoPrompt' | 'firstLastFramePrompt', + ) => Promise + onUpdatePanelVideoModel: (storyboardId: string, panelIndex: number, model: string) => Promise + onOpenAssetLibraryForCharacter: (characterId?: string | null, refreshAssets?: boolean) => void +} + +const WorkspaceStageRuntimeContext = createContext(null) + +interface WorkspaceStageRuntimeProviderProps { + value: WorkspaceStageRuntimeValue + children: ReactNode +} + +export function WorkspaceStageRuntimeProvider({ value, children }: WorkspaceStageRuntimeProviderProps) { + return ( + + {children} + + ) +} + +export function useWorkspaceStageRuntime() { + const context = useContext(WorkspaceStageRuntimeContext) + if (!context) { + throw new Error('useWorkspaceStageRuntime must be used within WorkspaceStageRuntimeProvider') + } + return context +} diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/AssetLibrary.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/AssetLibrary.tsx new file mode 100644 index 0000000..20006aa --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/AssetLibrary.tsx @@ -0,0 +1,73 @@ +'use client' + +/** + * 资产库 - 全局浮动按钮,打开后显示完整的资产管理界面 + * 复用AssetsStage组件,保持功能完全一致 + * + * 🔥 V6.5 重构:删除 characters/locations props,AssetsStage 现在内部直接订阅 + * 🔥 V6.6 重构:删除 onGenerateImage prop,AssetsStage 现在内部使用 mutation hooks + */ + +import { useState } from 'react' +import { useTranslations } from 'next-intl' +import AssetsStage from './AssetsStage' +import { AppIcon } from '@/components/ui/icons' + +interface AssetLibraryProps { + projectId: string + isAnalyzingAssets: boolean +} + +export default function AssetLibrary({ + projectId, + isAnalyzingAssets +}: AssetLibraryProps) { + const [isOpen, setIsOpen] = useState(false) + const t = useTranslations('assets') + + return ( + <> + {/* 触发按钮 - 现代玻璃态风格 */} + + + {/* 全屏弹窗 - 现代玻璃态风格 */} + {isOpen && ( +
+
+ {/* 头部 */} +
+
+
+ +
+

{t('assetLibrary.title')}

+
+ +
+ + {/* 内容区域 - 复用AssetsStage,现在 AssetsStage 内部直接订阅和处理图片生成 */} +
+ +
+
+
+ )} + + ) +} diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/AssetsStage.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/AssetsStage.tsx new file mode 100644 index 0000000..03423f9 --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/AssetsStage.tsx @@ -0,0 +1,384 @@ +'use client' + +import { useTranslations } from 'next-intl' +/** + * 资产确认阶段 - 小说推文模式专用 + * 包含TTS生成和资产分析 + * + * 重构说明 v2: + * - 角色和场景操作函数已提取到 hooks/useCharacterActions 和 hooks/useLocationActions + * - 批量生成逻辑已提取到 hooks/useBatchGeneration + * - TTS/音色逻辑已提取到 hooks/useTTSGeneration + * - 弹窗状态已提取到 hooks/useAssetModals + * - 档案管理已提取到 hooks/useProfileManagement + * - UI已拆分为 CharacterSection, LocationSection, AssetToolbar, AssetModals 组件 + */ + +import { useState, useCallback, useMemo } from 'react' +// 移除了 useRouter 导入,因为不再需要在组件中操作 URL +import { Character, CharacterAppearance } from '@/types/project' +import { resolveTaskPresentationState } from '@/lib/task/presentation' +import { + useGenerateProjectCharacterImage, + useGenerateProjectLocationImage, + useProjectAssets, + useRefreshProjectAssets, +} from '@/lib/query/hooks' + +// Hooks +import { useCharacterActions } from './assets/hooks/useCharacterActions' +import { useLocationActions } from './assets/hooks/useLocationActions' +import { useBatchGeneration } from './assets/hooks/useBatchGeneration' +import { useTTSGeneration } from './assets/hooks/useTTSGeneration' +import { useAssetModals } from './assets/hooks/useAssetModals' +import { useProfileManagement } from './assets/hooks/useProfileManagement' +import { useAssetsCopyFromHub } from './assets/hooks/useAssetsCopyFromHub' +import { useAssetsGlobalActions } from './assets/hooks/useAssetsGlobalActions' +import { useAssetsImageEdit } from './assets/hooks/useAssetsImageEdit' + +// Components +import CharacterSection from './assets/CharacterSection' +import LocationSection from './assets/LocationSection' +import AssetToolbar from './assets/AssetToolbar' +import AssetsStageStatusOverlays from './assets/AssetsStageStatusOverlays' +import UnconfirmedProfilesSection from './assets/UnconfirmedProfilesSection' +import AssetsStageModals from './assets/AssetsStageModals' + +interface AssetsStageProps { + projectId: string + isAnalyzingAssets: boolean + focusCharacterId?: string | null + focusCharacterRequestId?: number + // 🔥 通过 props 触发全局分析(避免 URL 参数竞态条件) + triggerGlobalAnalyze?: boolean + onGlobalAnalyzeComplete?: () => void +} + +export default function AssetsStage({ + projectId, + isAnalyzingAssets, + focusCharacterId = null, + focusCharacterRequestId = 0, + triggerGlobalAnalyze = false, + onGlobalAnalyzeComplete +}: AssetsStageProps) { + // 🔥 V6.5 重构:直接订阅缓存,消除 props drilling + const { data: assets } = useProjectAssets(projectId) + // 🔧 使用 useMemo 稳定引用,防止 useCallback/useEffect 依赖问题 + const characters = useMemo(() => assets?.characters ?? [], [assets?.characters]) + const locations = useMemo(() => assets?.locations ?? [], [assets?.locations]) + // 🔥 使用 React Query 刷新,替代 onRefresh prop + const refreshAssets = useRefreshProjectAssets(projectId) + const onRefresh = useCallback(() => { refreshAssets() }, [refreshAssets]) + + // 🔥 V6.6 重构:使用 mutation hooks 替代 onGenerateImage prop + const generateCharacterImage = useGenerateProjectCharacterImage(projectId) + const generateLocationImage = useGenerateProjectLocationImage(projectId) + + // 🔥 内部图片生成函数 - 使用 mutation hooks 实现乐观更新 + const handleGenerateImage = useCallback(async (type: 'character' | 'location', id: string, appearanceId?: string) => { + if (type === 'character' && appearanceId) { + await generateCharacterImage.mutateAsync({ characterId: id, appearanceId }) + } else if (type === 'location') { + // 场景生成默认使用 imageIndex: 0 + await generateLocationImage.mutateAsync({ locationId: id, imageIndex: 0 }) + } + }, [generateCharacterImage, generateLocationImage]) + + const t = useTranslations('assets') + // 计算资产总数 + const totalAppearances = characters.reduce((sum, char) => sum + (char.appearances?.length || 0), 0) + const totalLocations = locations.length + const totalAssets = totalAppearances + totalLocations + + // 本地 UI 状态 + const [previewImage, setPreviewImage] = useState(null) + const [toast, setToast] = useState<{ message: string; type: 'success' | 'warning' | 'error' } | null>(null) + + // 辅助:获取角色形象 + const getAppearances = (character: Character): CharacterAppearance[] => { + return character.appearances || [] + } + + // 显示提示 + const showToast = useCallback((message: string, type: 'success' | 'warning' | 'error' = 'success', duration = 3000) => { + setToast({ message, type }) + setTimeout(() => setToast(null), duration) + }, []) + + // === 使用提取的 Hooks === + + // 🔥 V6.5 重构:hooks 现在内部订阅 useProjectAssets,不再需要传 characters/locations + + // 批量生成 + const { + isBatchSubmitting, + batchProgress, + activeTaskKeys, + clearTransientTaskKey, + handleGenerateAllImages, + handleRegenerateAllImages + } = useBatchGeneration({ + projectId, + handleGenerateImage + }) + + const { + isGlobalAnalyzing, + globalAnalyzingState, + handleGlobalAnalyze, + } = useAssetsGlobalActions({ + projectId, + triggerGlobalAnalyze, + onGlobalAnalyzeComplete, + onRefresh, + showToast, + t, + }) + + const { + copyFromGlobalTarget, + isGlobalCopyInFlight, + handleCopyFromGlobal, + handleCopyLocationFromGlobal, + handleVoiceSelectFromHub, + handleConfirmCopyFromGlobal, + handleCloseCopyPicker, + } = useAssetsCopyFromHub({ + projectId, + onRefresh, + showToast, + }) + + // 角色操作 + const { + handleDeleteCharacter, + handleDeleteAppearance, + handleSelectCharacterImage, + handleConfirmSelection, + handleRegenerateSingleCharacter, + handleRegenerateCharacterGroup + } = useCharacterActions({ + projectId, + showToast + }) + + // 场景操作 + const { + handleDeleteLocation, + handleSelectLocationImage, + handleConfirmLocationSelection, + handleRegenerateSingleLocation, + handleRegenerateLocationGroup + } = useLocationActions({ + projectId, + showToast + }) + + // TTS/音色 + const { + voiceDesignCharacter, + handleVoiceChange, + handleOpenVoiceDesign, + handleVoiceDesignSave, + handleCloseVoiceDesign + } = useTTSGeneration({ + projectId + }) + + // 弹窗状态 + const { + editingAppearance, + editingLocation, + showAddCharacter, + showAddLocation, + imageEditModal, + characterImageEditModal, + setShowAddCharacter, + setShowAddLocation, + handleEditAppearance, + handleEditLocation, + handleOpenLocationImageEdit, + handleOpenCharacterImageEdit, + closeEditingAppearance, + closeEditingLocation, + closeAddCharacter, + closeAddLocation, + closeImageEditModal, + closeCharacterImageEditModal + } = useAssetModals({ + projectId + }) + // 档案管理 + const { + unconfirmedCharacters, + isConfirmingCharacter, + deletingCharacterId, + batchConfirming, + editingProfile, + handleEditProfile, + handleConfirmProfile, + handleBatchConfirm, + handleDeleteProfile, + setEditingProfile + } = useProfileManagement({ + projectId, + showToast + }) + const batchConfirmingState = batchConfirming + ? resolveTaskPresentationState({ + phase: 'processing', + intent: 'modify', + resource: 'image', + hasOutput: false, + }) + : null + + const { + handleUndoCharacter, + handleUndoLocation, + handleLocationImageEdit, + handleCharacterImageEdit, + handleUpdateAppearanceDescription, + handleUpdateLocationDescription, + } = useAssetsImageEdit({ + projectId, + t, + showToast, + onRefresh, + editingAppearance, + editingLocation, + imageEditModal, + characterImageEditModal, + closeEditingAppearance, + closeEditingLocation, + closeImageEditModal, + closeCharacterImageEditModal, + }) + + return ( +
+ setToast(null)} + isGlobalAnalyzing={isGlobalAnalyzing} + globalAnalyzingState={globalAnalyzingState} + globalAnalyzingTitle={t('toolbar.globalAnalyzing')} + globalAnalyzingHint={t('toolbar.globalAnalyzingHint')} + globalAnalyzingTip={t('toolbar.globalAnalyzingTip')} + /> + + {/* 资产工具栏 */} + + + + + {/* 角色资产区块 */} + setShowAddCharacter(true)} + onDeleteCharacter={handleDeleteCharacter} + onDeleteAppearance={handleDeleteAppearance} + onEditAppearance={handleEditAppearance} + handleGenerateImage={handleGenerateImage} + onSelectImage={handleSelectCharacterImage} + onConfirmSelection={handleConfirmSelection} + onRegenerateSingle={handleRegenerateSingleCharacter} + onRegenerateGroup={handleRegenerateCharacterGroup} + onUndo={handleUndoCharacter} + onImageClick={setPreviewImage} + onImageEdit={(charId, appIdx, imgIdx, name) => handleOpenCharacterImageEdit(charId, appIdx, imgIdx, name)} + onVoiceChange={(characterId, customVoiceUrl) => handleVoiceChange(characterId, 'custom', characterId, customVoiceUrl)} + onVoiceDesign={handleOpenVoiceDesign} + onVoiceSelectFromHub={handleVoiceSelectFromHub} + onCopyFromGlobal={handleCopyFromGlobal} + getAppearances={getAppearances} + /> + + {/* 场景资产区块 */} + setShowAddLocation(true)} + onDeleteLocation={handleDeleteLocation} + onEditLocation={handleEditLocation} + handleGenerateImage={handleGenerateImage} + onSelectImage={handleSelectLocationImage} + onConfirmSelection={handleConfirmLocationSelection} + onRegenerateSingle={handleRegenerateSingleLocation} + onRegenerateGroup={handleRegenerateLocationGroup} + onUndo={handleUndoLocation} + onImageClick={setPreviewImage} + onImageEdit={(locId, imgIdx) => handleOpenLocationImageEdit(locId, imgIdx)} + onCopyFromGlobal={handleCopyLocationFromGlobal} + /> + + setPreviewImage(null)} + handleGenerateImage={handleGenerateImage} + handleUpdateAppearanceDescription={handleUpdateAppearanceDescription} + handleUpdateLocationDescription={handleUpdateLocationDescription} + handleLocationImageEdit={handleLocationImageEdit} + handleCharacterImageEdit={handleCharacterImageEdit} + handleCloseVoiceDesign={handleCloseVoiceDesign} + handleVoiceDesignSave={handleVoiceDesignSave} + handleCloseCopyPicker={handleCloseCopyPicker} + handleConfirmCopyFromGlobal={handleConfirmCopyFromGlobal} + handleConfirmProfile={handleConfirmProfile} + closeEditingAppearance={closeEditingAppearance} + closeEditingLocation={closeEditingLocation} + closeAddCharacter={closeAddCharacter} + closeAddLocation={closeAddLocation} + closeImageEditModal={closeImageEditModal} + closeCharacterImageEditModal={closeCharacterImageEditModal} + isConfirmingCharacter={isConfirmingCharacter} + setEditingProfile={setEditingProfile} + previewImage={previewImage} + imageEditModal={imageEditModal} + characterImageEditModal={characterImageEditModal} + editingAppearance={editingAppearance} + editingLocation={editingLocation} + showAddCharacter={showAddCharacter} + showAddLocation={showAddLocation} + voiceDesignCharacter={voiceDesignCharacter} + editingProfile={editingProfile} + copyFromGlobalTarget={copyFromGlobalTarget} + isGlobalCopyInFlight={isGlobalCopyInFlight} + /> +
+ ) +} diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/ConfigStage.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/ConfigStage.tsx new file mode 100644 index 0000000..1890c0b --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/ConfigStage.tsx @@ -0,0 +1,25 @@ +'use client' + +import NovelInputStage from './NovelInputStage' +import { useWorkspaceStageRuntime } from '../WorkspaceStageRuntimeContext' +import { useWorkspaceEpisodeStageData } from '../hooks/useWorkspaceEpisodeStageData' + +export default function ConfigStage() { + const runtime = useWorkspaceStageRuntime() + const { episodeName, novelText } = useWorkspaceEpisodeStageData() + + return ( + + ) +} diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/NovelInputStage.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/NovelInputStage.tsx new file mode 100644 index 0000000..ae7f9ad --- /dev/null +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/NovelInputStage.tsx @@ -0,0 +1,344 @@ +'use client' + +/** + * 小说推文模式 - 故事输入阶段 (Story View) + * V3.2 UI: 极简版,专注剧本输入,资产管理移至资产库 + */ + +import { useTranslations } from 'next-intl' +import { useState, useRef, useEffect } from 'react' +import '@/styles/animations.css' +import { ART_STYLES, VIDEO_RATIOS } from '@/lib/constants' +import TaskStatusInline from '@/components/task/TaskStatusInline' +import { resolveTaskPresentationState } from '@/lib/task/presentation' +import { AppIcon, RatioPreviewIcon } from '@/components/ui/icons' + +/** + * RatioIcon - 比例预览图标组件 + */ +function RatioIcon({ ratio, size = 24, selected = false }: { ratio: string; size?: number; selected?: boolean }) { + return +} + +/** + * RatioSelector - 比例选择下拉组件 + */ +function RatioSelector({ + value, + onChange, + options +}: { + value: string + onChange: (value: string) => void + options: { value: string; label: string }[] +}) { + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + const selectedOption = options.find(o => o.value === value) + + return ( +
+ {/* 触发按钮 */} + + + {/* 下拉面板 - 横向网格布局 */} + {isOpen && ( +
+
+ {options.map((option) => ( + + ))} +
+
+ )} +
+ ) +} + +/** + * StyleSelector - 视觉风格选择抽屉组件 + */ +function StyleSelector({ + value, + onChange, + options +}: { + value: string + onChange: (value: string) => void + options: { value: string; label: string; preview: string }[] +}) { + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + const selectedOption = options.find(o => o.value === value) || options[0] + + return ( +
+ {/* 触发按钮 */} + + + {/* 下拉面板 */} + {isOpen && ( +
+
+ {options.map((option) => ( + + ))} +
+
+ )} +
+ ) +} + +interface NovelInputStageProps { + // 核心数据 + novelText: string + // 当前剧集名称 + episodeName?: string + // 回调函数 + onNovelTextChange: (value: string) => void + onNext: () => void + // 状态 + isSubmittingTask?: boolean + isSwitchingStage?: boolean + // 旁白开关 + enableNarration?: boolean + onEnableNarrationChange?: (enabled: boolean) => void + // 配置项 - 比例与风格 + videoRatio?: string + artStyle?: string + onVideoRatioChange?: (value: string) => void + onArtStyleChange?: (value: string) => void +} + +export default function NovelInputStage({ + novelText, + episodeName, + onNovelTextChange, + onNext, + isSubmittingTask = false, + isSwitchingStage = false, + enableNarration = false, + onEnableNarrationChange, + videoRatio = '9:16', + artStyle = 'american-comic', + onVideoRatioChange, + onArtStyleChange +}: NovelInputStageProps) { + const t = useTranslations('novelPromotion') + const hasContent = novelText.trim().length > 0 + const stageSwitchingState = isSwitchingStage + ? resolveTaskPresentationState({ + phase: 'processing', + intent: 'generate', + resource: 'text', + hasOutput: false, + }) + : null + + return ( +
+ + {/* 当前编辑剧集提示 - 顶部居中醒目显示 */} + {episodeName && ( +
+
+ {t("storyInput.currentEditing", { name: episodeName })} +
+
{t("storyInput.editingTip")}
+
+ )} + + {/* 主输入区域 */} +
+
+ {/* 字数统计 */} +
+ + {t("storyInput.wordCount")} {novelText.length} + +
+ + {/* 剧本输入框 */} +