diff --git a/.env.example b/.env.example index dfb8cd06..88aaf88c 100644 --- a/.env.example +++ b/.env.example @@ -21,7 +21,7 @@ DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1" DASHSCOPE_API_KEY="sk-65a0c6fa5e294b9887ace860f9d65990" # Optional model name for custom-world scene image generation. -DASHSCOPE_IMAGE_MODEL="wan2.2-t2i-flash" +DASHSCOPE_IMAGE_MODEL="wan2.7-image" # Optional model names for character asset studio. DASHSCOPE_CHARACTER_VISUAL_MODEL="wan2.7-image-pro" diff --git a/docs/design/CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md b/docs/design/CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md new file mode 100644 index 00000000..becee1b0 --- /dev/null +++ b/docs/design/CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md @@ -0,0 +1,660 @@ +# 自定义世界自有设定层优化方案 + +更新时间:`2026-04-08` + +## 0. 目标 + +这份文档要解决的问题是: + +**当前自定义世界虽然已经是唯一正式可玩的世界入口,但它底层仍依赖武侠 / 仙侠模板设定。** + +本次优化目标不是直接删除这些依赖,而是把它们逐步改造成: + +**属于自定义世界自身的设定层,并且这套设定层必须能通用于任何题材。** + +同时必须满足一条底线: + +**不能破坏当前自定义世界生成流程的任何可用功能。** + +一句话定义: + +**让自定义世界从“借模板世界运行”,升级成“拥有自有设定层、可跨题材运行”的架构。** + +--- + +## 1. 设计原则 + +这次优化必须同时遵守 4 条原则: + +1. 设定归自定义世界自身所有 + - 运行时、生成期、表现层真正依赖的世界设定,应该落回 `CustomWorldProfile` 或由它直接编译出的配置。 + +2. 设定必须跨题材 + - 新设定不能只是把“武侠 / 仙侠”换一个更抽象的名字。 + - 它必须能支撑奇幻、科幻、悬疑、校园、末世、神话、现代、海洋、裂界等任何题材。 + +3. 优化优先做兼容迁移 + - 不能先删旧字段,再补新结构。 + - 必须先补新设定层,再逐步迁读,最后再让旧模板字段退化成兼容层。 + +4. 不能增加创作者负担 + - 这次不是让创作者多填一堆底层 schema。 + - 这些设定仍然应由 AI / 系统编译出来,只是所有权从模板世界转移到自定义世界自己。 + +--- + +## 2. 当前自定义世界实际依赖了什么 + +根据 [CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md](../reference/CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md),当前依赖可以归纳成 6 组: + +1. 模板锚点字段 + - `templateWorldType` + - `WorldTemplateType` + +2. 规则桥接 + - `resolveRuleWorldType(...)` + +3. 主题与词汇底板 + - `detectCustomWorldThemeMode(...)` + - `buildThemePackFromWorldProfile(...)` + +4. 属性、资源词与经济 fallback + - 预设属性 schema + - 资源命名 + - 初始货币规则 + +5. 内容骨架 + - 角色模板骨架 + - 怪物模板池 + - 场景视觉参考池 + +6. prompt 兼容字段 + - `framework` 生成仍要求输出 `templateWorldType` + +这些东西现在还不能直接删,因为: + +**它们不是单纯的预设世界残留,而是当前自定义世界生成与运行时的真实支撑层。** + +--- + +## 3. 本次优化的核心思路 + +这次不建议新增很多彼此平行的新系统,而是把现有模板依赖统一收束成: + +**自定义世界自己的 5 层设定层。** + +这 5 层分别是: + +1. 语义锚层 +2. 规则层 +3. 表现层 +4. 原型参考层 +5. 兼容迁移层 + +这样可以让现在分散在: + +- 模板世界枚举 +- 预设 schema +- 视觉参考池 +- 怪物池 +- ThemePack 底板 + +这些地方的依赖,被重新编译回 `CustomWorldProfile` 自己的配置。 + +--- + +## 4. 目标结构 + +## 4.1 语义锚层:替代 `templateWorldType` + +当前 `templateWorldType` 实际在回答的不是“你是不是武侠”,而是: + +1. 这个世界更接近哪类冲突结构 +2. 这个世界更接近哪类制度和禁忌 +3. 这个世界更接近哪类叙事载体与力量来源 + +所以应把它升级成一套真正属于自定义世界自己的语义锚: + +```ts +interface CustomWorldSemanticAnchor { + genreSignals: string[]; + conflictForms: string[]; + institutionTypes: string[]; + tabooTypes: string[]; + carrierTypes: string[]; + forceSystemTypes: string[]; + atmosphereTags: string[]; +} +``` + +这层应该回答: + +1. 这个世界的主要矛盾像什么 +2. 这个世界的秩序结构像什么 +3. 这个世界的危险和禁忌像什么 +4. 这个世界的线索、遗物、文书、证物、技术、仪式像什么 + +关键点: + +- 这里不再出现“武侠 / 仙侠”的模板世界名 +- 只保留通用语义 + +例如: + +- `institutionTypes` + - 宗门 + - 公司 + - 学园 + - 教团 + - 调查局 + - 舰队 + - 部族 + +- `forceSystemTypes` + - 灵脉 + - 科技 + - 仪式 + - 契约 + - 血统 + - 神谕 + - 污染 + +这就天然跨题材了。 + +--- + +## 4.2 规则层:把 fallback 变成自定义世界自己的规则设定 + +当前很多规则仍然会回落到武侠 / 仙侠: + +- 属性 schema +- 资源命名 +- 经济命名 +- 初始货币 + +优化后应由自定义世界自己持有: + +```ts +interface CustomWorldRuleProfile { + attributeSchema: WorldAttributeSchema; + resourceLabels: { + hp: string; + mp: string; + maxHp: string; + maxMp: string; + damage: string; + guard: string; + range: string; + cooldown: string; + manaCost: string; + currency: string; + }; + economyProfile: { + initialCurrency: number; + }; +} +``` + +这意味着以后运行时读取逻辑应优先变成: + +1. 先读 `profile.ruleProfile` +2. 再读 `profile.attributeSchema` +3. 最后才允许读兼容 fallback + +而不是: + +1. 先判断 `WUXIA / XIANXIA` +2. 再猜自定义世界应该像哪边 + +这层的原则是: + +**规则是这个自定义世界自己的,不再借模板世界托管。** + +--- + +## 4.3 表现层:把 ThemePack 变成自定义世界自身的派生物 + +当前 `ThemePack` 仍然是“预设底板 + 自定义词汇补丁”。 + +优化后应改成: + +```ts +interface CustomWorldExpressionProfile { + themePack: ThemePack; + presentationTone: string[]; + namingDirectives: string[]; + clueDirectives: string[]; + revealDirectives: string[]; +} +``` + +也就是说: + +- `ThemePack` 仍然可以保留 +- 但它不再被理解为“武侠底板 / 仙侠底板的延伸” +- 它应当是: + - `semanticAnchor` + - `creatorIntent` + - `majorFactions` + - `coreConflicts` + - `world text` + 共同编译出来的结果 + +这一步非常关键,因为它会决定: + +1. 物件命名 +2. 势力命名 +3. 线索形式 +4. 提示词词汇风格 +5. reveal 风格 + +只有把这层做成自定义世界自己的派生物,后面跨题材才会真的稳。 + +--- + +## 4.4 原型参考层:把模板骨架改成通用原型库 + +当前自定义世界借用模板的最深部分是: + +1. 角色模板骨架 +2. 场景视觉参考池 +3. 怪物模板池 + +这三类不能粗暴删除,但可以改造成: + +**通用原型参考层。** + +建议统一成: + +```ts +interface CustomWorldReferenceProfile { + roleArchetypes: RoleArchetypeProfile[]; + sceneBuckets: SceneArchetypeBucket[]; + creatureArchetypes: CreatureArchetypeProfile[]; +} +``` + +### 4.4.1 角色原型 + +角色原型应描述的是: + +- 正面推进型 +- 远程压制型 +- 控场解构型 +- 续航承压型 +- 潜行爆发型 + +而不是: + +- 剑之公主 +- 神箭游侠 +- 双刃旅者 + +也就是说: + +**保留战斗骨架和技能骨架,不保留模板角色人格设定。** + +### 4.4.2 场景原型 + +场景原型应描述的是: + +- 高压入口区 +- 雨湿街巷区 +- 临水渡口区 +- 仪式神殿区 +- 高空通路区 +- 工业热区 +- 地底遗迹区 +- 群落聚居区 + +而不是: + +- 竹林古道 +- 云海仙门 + +也就是说: + +**保留“空间语义 -> 视觉参考”的逻辑,不保留模板世界场景名作为中心。** + +### 4.4.3 生物原型 + +生物原型应描述的是: + +- 潜伏袭击者 +- 重甲承压者 +- 群居骚扰者 +- 远程威胁者 +- 异化污染体 +- 灵体回响体 +- 机关守卫体 + +而不是: + +- 某个武侠怪 +- 某个仙侠怪 + +这样之后: + +- 自定义世界依赖的是“通用原型” +- 原型再映射到底层素材与 preset + +--- + +## 4.5 兼容迁移层:旧字段继续保留一段时间 + +为了不破坏当前流程,短期内不能直接删除: + +- `templateWorldType` +- `WorldTemplateType` +- 以及相关 normalize / save / read 逻辑 + +这层应被降级成: + +```ts +interface CustomWorldCompatibilityProfile { + legacyTemplateWorldType?: 'WUXIA' | 'XIANXIA' | null; + migrationVersion: string; +} +``` + +作用: + +1. 旧存档兼容 +2. 旧 prompt 兼容 +3. 旧测试兼容 +4. 旧工具链兼容 + +但它不再应是新生成世界的第一真相来源。 + +--- + +## 5. 现有每类依赖如何改造成自定义世界自己的设定 + +下面把现有依赖逐项映射成未来目标。 + +## 5.1 `templateWorldType` + +当前作用: + +- 世界锚点 +- 规则 fallback +- 视觉和怪物参考入口 + +目标改造: + +- 拆成: + - `semanticAnchor` + - `ruleProfile` + - `referenceProfile` + - `compatibilityProfile` + +迁移方式: + +1. 旧字段保留 +2. 新字段生成后优先读新字段 +3. 旧字段只做迁移 fallback + +## 5.2 `resolveRuleWorldType(...)` + +当前作用: + +- 把 `CUSTOM` 解析回 `WUXIA / XIANXIA` + +目标改造: + +- 改成: + +```ts +resolveCustomWorldRuleProfile(profile) +``` + +运行时不再需要知道“它更像武侠还是仙侠”,而只需要知道: + +- 这个世界的规则 profile 是什么 + +## 5.3 `getPresetWorldAttributeSchema(...)` + +当前作用: + +- 自定义世界 schema 的参考底板 + +目标改造: + +- 把预设 schema 底板重构成: + - 通用 schema seeds + +例如按功能分: + +1. 承压轴 +2. 机动轴 +3. 洞察轴 +4. 决断轴 +5. 共鸣轴 +6. 续航轴 + +然后由自定义世界的 `semanticAnchor + creatorIntent` 生成最终命名与说明。 + +## 5.4 `PRESET_CHARACTERS` + +当前作用: + +- 自定义世界角色的战斗模板和技能骨架 + +目标改造: + +- 抽出: + - `RoleArchetypeProfile` + - `SkillArchetypeProfile` + +也就是说: + +- 还可以继续复用当前角色的战斗结构 +- 但不应再让自定义世界依赖那些模板角色的人设文本 + +## 5.5 场景图片参考池 + +当前作用: + +- 自定义世界默认场景图匹配 + +目标改造: + +- 改成: + - `SceneArchetypeBucket` + +每个 bucket 只表达通用空间语义,不表达模板世界名。 + +## 5.6 怪物池 + +当前作用: + +- 自定义世界敌对单位匹配 preset + +目标改造: + +- 改成: + - `CreatureArchetypeProfile` + +由 archetype 再映射到底层怪物素材与 preset。 + +## 5.7 `buildThemePackFromWorldProfile(...)` + +当前作用: + +- 以模板题材包为底生成自定义世界 ThemePack + +目标改造: + +- 变成: + - `buildThemePackFromCustomWorldSemanticAnchor(...)` + +即 ThemePack 以自定义世界自己的语义锚和词汇锚为底。 + +--- + +## 6. 不破坏当前流程的迁移顺序 + +这是最关键的落地顺序。 + +## 阶段 A:先给 `CustomWorldProfile` 补新设定层 + +先补: + +1. `semanticAnchor` +2. `ruleProfile` +3. `expressionProfile` +4. `referenceProfile` +5. `compatibilityProfile` + +这一步只做新增,不删旧字段。 + +## 阶段 B:旧字段自动编译新字段 + +当前已有 profile、旧存档、旧生成结果,先统一经过: + +```ts +compileOwnedSettingLayersFromLegacyTemplate(profile) +``` + +让: + +- 旧世界也拥有新设定层 +- 新运行时可优先消费新字段 + +## 阶段 C:生成链开始直接产出新设定层 + +修改: + +- `framework prompt` +- `normalizeCustomWorldGenerationFramework(...)` +- `customWorld.ts` + +使新生成世界优先产出: + +- `semanticAnchor` +- `ruleProfile hints` +- `referenceProfile hints` + +而不是只产出 `templateWorldType` + +## 阶段 D:运行时逐步改读新设定层 + +优先改: + +1. 资源词与货币 +2. attribute schema +3. ThemePack +4. 视觉参考 +5. 怪物映射 +6. 角色原型引用 + +要求: + +- 每次只切一层 +- 每层都保留 fallback + +## 阶段 E:把旧模板字段降级为兼容层 + +当上面的读取已经都切走后: + +- `templateWorldType` 就不再是主链依赖 +- 只作为: + - migration + - 老存档兼容 + - 老工具兼容 + +--- + +## 7. 推荐新增的最小字段 + +为了避免系统膨胀,建议只先补最小集合。 + +## 7.1 `CustomWorldOwnedSettingLayers` + +```ts +interface CustomWorldOwnedSettingLayers { + semanticAnchor: CustomWorldSemanticAnchor; + ruleProfile: CustomWorldRuleProfile; + expressionProfile: CustomWorldExpressionProfile; + referenceProfile: CustomWorldReferenceProfile; + compatibilityProfile?: CustomWorldCompatibilityProfile | null; +} +``` + +然后挂到: + +```ts +interface CustomWorldProfile { + ownedSettingLayers?: CustomWorldOwnedSettingLayers | null; +} +``` + +这样好处是: + +1. 不用在 `CustomWorldProfile` 顶层堆太多字段 +2. 迁移更集中 +3. 后续删除兼容层更容易 + +--- + +## 8. 对当前功能链的保护要求 + +这次优化过程中,下面这些能力不能坏: + +1. 自定义世界创建 +2. 自定义世界保存 / 读取 +3. 自定义世界角色生成 +4. 自定义世界场景生成 +5. 自定义世界开局 +6. 自定义世界运行时的: + - 属性 + - 资源词 + - 经济 + - 场景图 + - 敌对实体 + - ThemePack + - prompt 组织 + +也就是说: + +**任何一次迭代都必须是“新层可用 + 旧层仍可兜底”。** + +--- + +## 9. 验收标准 + +当下面这些标准成立时,说明这套优化开始有效: + +1. 新生成的自定义世界不再必须依赖 `templateWorldType` 才能表达自身设定。 +2. 运行时优先读取 `ownedSettingLayers`,而不是先问武侠 / 仙侠。 +3. 自定义世界的属性、资源词、经济、视觉参考、怪物映射、ThemePack 都能从自身设定层派生出来。 +4. 新设定层描述的是通用语义,不是模板世界换皮。 +5. 当前自定义世界生成流程、旧存档、旧结果页工作台仍然可用。 + +--- + +## 10. 最后结论 + +如果目标是: + +**让这些依赖都变成属于自定义世界的设定,而且这些设定要通用于任何题材。** + +那么最正确的方向不是“继续弱化武侠 / 仙侠字样”,而是: + +**把模板支撑层整体迁移成自定义世界自己的设定层。** + +更具体地说,就是把当前依赖重组为: + +1. 自定义世界自己的语义锚 +2. 自定义世界自己的规则 profile +3. 自定义世界自己的表达 profile +4. 自定义世界自己的原型参考 profile +5. 只负责兼容的旧模板字段 + +这样之后,自定义世界才会真正从: + +**模板依赖型生成架构** + +迁移成: + +**跨题材、自有设定层、且兼容当前流程的生成架构。** diff --git a/docs/design/CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md b/docs/design/CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md new file mode 100644 index 00000000..392299b2 --- /dev/null +++ b/docs/design/CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md @@ -0,0 +1,656 @@ +# 自定义世界去模板依赖与跨题材通用化优化设计 + +更新时间:`2026-04-08` + +## 0. 目标 + +这份文档解决的是一个已经明确暴露出来的问题: + +**当前玩家主流程虽然已经移除了武侠 / 仙侠两个预设世界,但自定义世界底层仍然依赖武侠 / 仙侠模板设定。** + +本次优化的目标不是简单“删掉模板字段”,而是要在不破坏当前自定义世界生成流程的前提下,把这些依赖逐步改造成: + +**属于自定义世界自身、且能通用于任何题材的设定层。** + +一句话定义: + +**让自定义世界从“挂靠武侠 / 仙侠模板运行”,升级成“基于通用世界设定层独立运行”。** + +--- + +## 1. 优化原则 + +这次优化必须同时满足 3 条硬原则: + +1. 设定归自定义世界自身所有 + - 任何运行时、生成期、表现层真正依赖的设定,都应尽量落回 `CustomWorldProfile` 或由它直接编译出的通用配置,不再默认挂在 `WUXIA / XIANXIA` 两个模板世界上。 + +2. 设定必须跨题材通用 + - 不能把“自定义世界去模板化”理解成“再做一套更抽象的武侠 / 仙侠替代字段”。 + - 新结构必须能容纳奇幻、科幻、悬疑、末世、现代、神话、校园等任何题材。 + +3. 不能破坏当前自定义世界生成流程 + - 现有 `framework -> themePack -> storyGraph -> role / landmark -> runtime` 主链必须继续能跑。 + - 优化应以兼容迁移为主,而不是大爆破式重写。 + +--- + +## 2. 当前问题归纳 + +根据 [CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md](../reference/CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md),当前自定义世界仍依赖模板层的地方主要有: + +1. 模板锚点类型 + - `templateWorldType` + - `WorldTemplateType` + - `resolveRuleWorldType(...)` + +2. 主题与规则回退 + - `detectCustomWorldThemeMode(...)` + - `resolveCustomWorldAnchorWorldType(...)` + - `buildThemePackFromWorldProfile(...)` 底板 + +3. 属性与表现 + - 预设世界属性 schema + - 资源词、数值命名、货币命名 + +4. 角色骨架 + - `PRESET_CHARACTERS` + - 模板技能定义 + - 模板 opening 接口 + +5. 场景与视觉参考 + - 武侠 / 仙侠场景图片参考池 + - 模板 camp 场景映射 + +6. 怪物模板池 + - 武侠 / 仙侠怪物 preset 池 + +这些依赖本质上说明: + +**当前自定义世界不是完全自足,而是“先生成自定义内容,再把它映射回两套模板世界骨架”。** + +--- + +## 3. 这次优化不应该怎么做 + +先明确几个错误方向: + +## 3.1 不能直接删掉 `templateWorldType` + +如果直接删除: + +- `templateWorldType` +- `WorldTemplateType` +- `WUXIA / XIANXIA` 相关回退 + +而不先补新的通用设定层,那么当前自定义世界会立刻失去: + +1. 规则桥接 +2. 主题底板 +3. 场景图参考 +4. 怪物池匹配 +5. 属性 schema fallback + +这会直接打断现有生成和运行链路。 + +## 3.2 不能把新设定继续写成“武侠 / 仙侠的抽象别名” + +例如下面这种思路是不够的: + +- 把 `templateWorldType` 改名成 `worldFamily` +- 但值仍然只有两类近似武侠 / 仙侠的内部枚举 + +这不是真正跨题材,只是换了名字。 + +## 3.3 不能让创作者承担更多底层配置工作 + +这次优化不是让创作者额外填写: + +- 怪物模板表 +- 场景参考池 +- 属性 schema 槽位 +- 规则 profile + +正确方向应该是: + +**这些通用设定仍由系统生成 / 编译,但所有权回到自定义世界自身。** + +--- + +## 4. 目标结构:把模板依赖改造成自定义世界自己的 4 层通用设定 + +为了避免系统越改越散,这次建议不要新增很多彼此平行的新系统,而是把现有模板依赖统一收束成 4 层通用设定。 + +## 4.1 第一层:世界语义锚层 + +这是替代 `templateWorldType` 的核心层。 + +它不再回答“你更像武侠还是仙侠”,而回答: + +1. 这个世界的主要冲突形式是什么 +2. 这个世界的制度、禁忌、叙事载体、力量来源是什么 +3. 这个世界更接近哪类表现模式 + +建议由自定义世界自己持有一个通用结构,例如: + +```ts +interface CustomWorldSemanticAnchor { + genreSignals: string[]; + conflictModel: string[]; + institutionHints: string[]; + tabooHints: string[]; + carrierHints: string[]; + forceSystemHints: string[]; +} +``` + +说明: + +- 它是自定义世界自己的语义锚。 +- 它可以表示: + - 宗门与灵脉 + - 财团与实验体 + - 学园与旧规 + - 边境与裂界 + - 海潮与失落群岛 + - 神话与誓约 +- 不再强制回落到武侠 / 仙侠二选一。 + +## 4.2 第二层:规则与表现配置层 + +这是替代: + +- `resolveRuleWorldType(...)` +- 预设属性 schema fallback +- 资源命名 fallback +- 经济 fallback + +建议改成由自定义世界持有一份通用 `WorldRuleProfile`: + +```ts +interface WorldRuleProfile { + attributeSchema: WorldAttributeSchema; + resourceLabels: { + hp: string; + mp: string; + maxHp: string; + maxMp: string; + damage: string; + guard: string; + range: string; + cooldown: string; + manaCost: string; + currency: string; + }; + economyProfile: { + initialCurrency: number; + rarityValueScale: Record; + }; +} +``` + +关键点: + +1. 以后运行时不要再优先问“这是武侠还是仙侠”。 +2. 应该直接问: + - `profile.ruleProfile.attributeSchema` + - `profile.ruleProfile.resourceLabels` + - `profile.ruleProfile.economyProfile` + +这样: + +- 奇幻世界可以叫“体魄 / 法力” +- 末世世界可以叫“生命 / 电量” +- 校园悬疑世界甚至可以弱化“mana”概念,改成“专注 / 压力” + +## 4.3 第三层:内容骨架与参考层 + +这是替代: + +- 预设角色模板骨架 +- 武侠 / 仙侠场景图参考池 +- 武侠 / 仙侠怪物 preset 池 + +这里不建议让自定义世界自己保存大批素材,而是建议让自定义世界持有: + +**对内容骨架的“编译后引用配置”。** + +建议统一成一个 `ContentReferenceProfile`: + +```ts +interface ContentReferenceProfile { + roleArchetypes: RoleArchetypeProfile[]; + sceneReferenceBuckets: SceneReferenceBucket[]; + creatureArchetypes: CreatureArchetypeProfile[]; +} +``` + +其中每个子项都应是通用语义,而不是模板世界名: + +1. `RoleArchetypeProfile` + - 例如: + - 正面压制型 + - 远程游击型 + - 灵术控制型 + - 重装承压型 + - 潜行刺击型 + +2. `SceneReferenceBucket` + - 例如: + - 高压门禁区 + - 雨夜街巷区 + - 高空通道区 + - 神殿 / 仪式区 + - 工业热区 + - 潮湿临水区 + +3. `CreatureArchetypeProfile` + - 例如: + - 潜伏掠食者 + - 重甲承压者 + - 群居灵体 + - 远程骚扰者 + - 寄生污染体 + +关键点: + +- 这些 archetype 可以继续映射到底层素材和 preset。 +- 但对自定义世界来说,它依赖的是“通用原型”,不是“武侠怪物池 / 仙侠怪物池”。 + +## 4.4 第四层:叙事与词汇编译层 + +这是替代: + +- 以模板世界为底的 `ThemePack` fallback +- prompt 中的模板世界兼容字段 + +建议做法: + +1. `ThemePack` 继续保留,但它的来源变成: + - `semanticAnchor + creatorIntent + world text + content reference profile` + +2. 生成框架 prompt 不再要求输出: + - `templateWorldType: WUXIA|XIANXIA` + +3. 改为要求输出: + - 世界语义锚 + - 规则表现关键词 + - 冲突和制度线索 + +例如: + +```ts +interface CustomWorldGenerationFramework { + name: string; + subtitle: string; + summary: string; + tone: string; + playerGoal: string; + semanticAnchor: CustomWorldSemanticAnchor; + majorFactions: string[]; + coreConflicts: string[]; + camp: CampOutline; +} +``` + +这样: + +- 生成流程仍然是 `framework -> themePack -> storyGraph -> role / landmark` +- 只是 framework 的核心锚不再依赖预设世界枚举 + +--- + +## 5. 现有依赖如何一一改造 + +## 5.1 `templateWorldType` -> `semanticAnchor + ruleProfile` + +当前用途: + +- 表示自定义世界挂靠武侠还是仙侠 + +目标改造: + +- 运行时不再直接读取 `templateWorldType` +- 改为读取: + - `semanticAnchor` + - `ruleProfile` + +迁移策略: + +1. 先保留 `templateWorldType` 作为兼容字段 +2. 新增 `semanticAnchor / ruleProfile` +3. 由旧字段自动编译出新字段 +4. 所有读取逻辑逐步切到新字段 +5. 最后把 `templateWorldType` 降级为 migration-only 字段 + +## 5.2 `resolveRuleWorldType(...)` -> `resolveWorldRuleProfile(...)` + +当前用途: + +- 把 `CUSTOM` 解析回 `WUXIA / XIANXIA` + +目标改造: + +- 不再返回模板 world type +- 直接返回自定义世界自己的 `ruleProfile` + +即: + +```ts +resolveWorldRuleProfile(worldType, customWorldProfile) +``` + +返回: + +- `attributeSchema` +- `resourceLabels` +- `economyProfile` +- 其它规则信息 + +## 5.3 预设属性 schema -> 通用 attribute schema seed + +当前用途: + +- 用武侠 / 仙侠 schema 作为自定义世界 schema 参考底板 + +目标改造: + +- 不再直接引用“武侠六脉 / 仙侠六轴”作为唯一底板 +- 改为维护一组通用 `attribute schema seeds` + +例如可按世界体验而不是题材命名: + +1. 正面对抗型 +2. 机动博弈型 +3. 灵知洞察型 +4. 共鸣契约型 +5. 生存续航型 +6. 高危推进型 + +然后由自定义世界的 `semanticAnchor` 决定如何组合或命名这些槽位。 + +## 5.4 预设角色模板 -> 通用角色原型骨架 + +当前用途: + +- 从 `PRESET_CHARACTERS` 选模板角色,再覆写自定义世界内容 + +目标改造: + +- 把当前模板角色骨架抽成“通用战斗原型角色” + +例如保留的是: + +- 技能骨架 +- 动作风格 +- build 倾向 +- 动画资源挂载方式 + +而不是保留“剑之公主 / 神箭游侠”这样的预设世界人格设定。 + +关键点: + +- 动画素材和技能骨架可以保留 +- 但运行时不应再依赖具体模板角色的人设文本 + +## 5.5 武侠 / 仙侠场景图参考池 -> 通用场景参考桶 + +当前用途: + +- 用武侠 / 仙侠场景关键词为自定义世界匹配默认背景图 + +目标改造: + +- 改成通用 `SceneReferenceBucket` +- 每个 bucket 对应一类空间语义 + +例如: + +1. 落脚处 / 归舍 +2. 高压入口 +3. 雨湿街巷 +4. 高空通路 +5. 仪式空间 +6. 工业热区 +7. 临水空间 +8. 地底遗迹 + +这样就能让: + +- 科幻世界 +- 校园世界 +- 神话世界 +- 末世世界 + +都共享同一套“空间语义 -> 视觉参考”的逻辑。 + +## 5.6 武侠 / 仙侠怪物池 -> 通用生物原型库 + +当前用途: + +- 从武侠 / 仙侠怪物池里为自定义世界匹配怪物 + +目标改造: + +- 改成通用 `CreatureArchetypeProfile` +- 再由 archetype 去映射底层 preset / 数值 / 动画素材 + +这样做的好处: + +1. 自定义世界依赖的是“潜伏者 / 重压者 / 群居体 / 异化体” +2. 而不是“这更像武侠怪还是仙侠怪” + +## 5.7 `ThemePack` 底板 -> 通用语义底板 + +当前用途: + +- 自定义世界的 ThemePack 还是从预设题材包底板开始 + +目标改造: + +- 让 ThemePack 直接从: + - `semanticAnchor` + - `creatorIntent` + - `majorFactions` + - `coreConflicts` + - `contentReferenceProfile` + 编译出来 + +也就是说: + +**ThemePack 应变成自定义世界自己的派生物,而不是模板世界的扩写版。** + +--- + +## 6. 不破坏现有生成流程的迁移方案 + +这是这份文档最重要的部分。 + +正确做法不是一口气替换,而是兼容迁移。 + +## 阶段 A:新增通用设定字段,但不删旧字段 + +先做: + +1. 在 `CustomWorldProfile` 上新增: + - `semanticAnchor` + - `ruleProfile` + - `contentReferenceProfile` + +2. 保留: + - `templateWorldType` + +3. 由当前旧字段自动编译出新字段 + +目标: + +- 先让新结构出现 +- 但现有流程完全不受影响 + +## 阶段 B:生成流程改为优先产出新字段 + +先改: + +- `framework prompt` +- `customWorld.ts` + +让 AI 先输出: + +- 通用语义锚 +- 规则提示 +- 通用 archetype 线索 + +同时在 normalize 层继续兼容旧的 `templateWorldType` + +目标: + +- 新生成的自定义世界开始“原生带通用设定” +- 旧存档仍可继续读取 + +## 阶段 C:运行时读取切到新设定 + +依次改: + +1. 规则层 + - 从 `resolveRuleWorldType` 切到 `resolveWorldRuleProfile` + +2. 属性层 + - 优先读 `profile.ruleProfile.attributeSchema` + +3. 资源词与经济层 + - 优先读 `profile.ruleProfile.resourceLabels / economyProfile` + +4. 场景图与怪物映射 + - 优先读 `contentReferenceProfile` + +目标: + +- 让模板世界字段不再是运行时第一来源 + +## 阶段 D:模板世界字段退化为兼容层 + +这一步完成后: + +1. `templateWorldType` 只用于: + - 旧存档迁移 + - 老测试兼容 + - 数据修复 fallback + +2. 不再用于: + - 正式生成主链 + - 正式运行时主链 + +## 阶段 E:再做深清理 + +只有当上面几步都完成后,才适合继续清理: + +1. 非必要的模板回退逻辑 +2. 非必要的主流程模板枚举消费点 +3. 非必要的审计 / 工具硬编码 + +--- + +## 7. 对当前主链的兼容要求 + +这次优化过程中,下面这些链路必须始终可用: + +1. `PreGameSelectionFlow -> generateCustomWorldProfile(...)` +2. `framework -> themePack -> storyGraph -> role / landmark` +3. 保存 / 读取自定义世界 profile +4. 自定义世界开局 +5. 自定义世界角色与场景生成 +6. 自定义世界运行时怪物 / 物品 / 场景图 /词汇表现 + +也就是说: + +**任何迁移都必须先补新字段,再迁读,再退旧字段,不能先删旧字段。** + +--- + +## 8. 建议新增或改造的最小数据结构 + +为了避免系统膨胀,这次不建议引入很多彼此割裂的新系统,建议只在 `CustomWorldProfile` 周边增量补三块。 + +## 8.1 `semanticAnchor` + +```ts +interface CustomWorldSemanticAnchor { + genreSignals: string[]; + conflictModel: string[]; + institutionHints: string[]; + tabooHints: string[]; + carrierHints: string[]; + forceSystemHints: string[]; +} +``` + +## 8.2 `ruleProfile` + +```ts +interface WorldRuleProfile { + attributeSchema: WorldAttributeSchema; + resourceLabels: { + hp: string; + mp: string; + maxHp: string; + maxMp: string; + damage: string; + guard: string; + range: string; + cooldown: string; + manaCost: string; + currency: string; + }; + economyProfile: { + initialCurrency: number; + }; +} +``` + +## 8.3 `contentReferenceProfile` + +```ts +interface ContentReferenceProfile { + roleArchetypes: string[]; + sceneReferenceBuckets: string[]; + creatureArchetypes: string[]; +} +``` + +这三块足够作为第一轮去模板化的最小自定义世界设定层。 + +--- + +## 9. 验收标准 + +做到以下几点,才能说明自定义世界真正开始脱离模板依赖: + +1. 新生成的自定义世界框架不再需要直接输出 `WUXIA|XIANXIA` 才能工作。 +2. 运行时核心逻辑优先读取 `semanticAnchor / ruleProfile / contentReferenceProfile`。 +3. 自定义世界的属性 schema、资源词、经济词不再默认直接回退到武侠 / 仙侠文案。 +4. 自定义世界的角色骨架、场景视觉、怪物映射能通过通用 archetype 表达。 +5. 现有生成流程、存档读取、运行时体验不受破坏。 +6. 旧存档仍能通过兼容层运行。 + +--- + +## 10. 最后结论 + +当前自定义世界真正缺的,不是“再删一点武侠 / 仙侠字样”,而是: + +**把模板世界支撑层改写成自定义世界自己的通用设定层。** + +更准确地说,这次优化要完成的是: + +1. 把模板锚点改成自定义世界自己的语义锚 +2. 把模板回退改成自定义世界自己的规则配置 +3. 把模板角色 / 场景 / 怪物参考改成通用原型引用 +4. 把 ThemePack 和生成 prompt 从“依附模板世界”改成“直接从自定义世界自身编译” + +同时整个过程必须遵守一条底线: + +**任何优化都不能破坏当前自定义世界生成与运行主链。** + +所以这不是“删模板”的问题,而是一次: + +**在兼容现有流程前提下,把自定义世界从模板依赖型架构,迁移成真正跨题材、自足型架构。** diff --git a/docs/design/README.md b/docs/design/README.md index 3bc4ddf7..3a55daa6 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -5,6 +5,8 @@ ## 文档列表 - [CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md](./CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md):自定义世界里创作者输入与 AI 分工边界设计。 +- [CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md](./CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md):把自定义世界从武侠/仙侠模板依赖迁到跨题材通用设定层的优化设计。 +- [CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md](./CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md):把模板依赖逐步迁成自定义世界自有设定层,并保证不破坏当前生成流程的优化方案。 - [AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md](./AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md):运行时物品生成系统重设计。 - [EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md](./EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md):配装构筑与合成/锻造闭环设计。 - [COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md](./COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md):角色首遇感、关系分层解锁、私聊系统设计。 @@ -15,6 +17,8 @@ - 做物品、Build、锻造相关需求时,先看前两份。 - 做自定义世界创作工作台、创作者输入边界、AI 分工设计时,先看第一份。 +- 做自定义世界去模板依赖、跨题材泛化、兼容迁移设计时,优先看新增的去模板化优化设计稿。 +- 做“模板依赖如何真正变成自定义世界自有设定层”的具体迁移方案时,优先看新增的自有设定层优化方案。 - 做角色关系、同伴互动、对话表现时,先看后两份。 - 做剧情引擎章节化、场景闭环、章节任务接入时,优先看新增的场景章节设计稿。 - 如果要判断是否符合目标,再和 `docs/prd/` 中对应 PRD 对照阅读。 diff --git a/docs/reference/CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md b/docs/reference/CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md new file mode 100644 index 00000000..68e8e341 --- /dev/null +++ b/docs/reference/CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md @@ -0,0 +1,398 @@ +# 自定义世界依赖的模板设定清单 + +更新时间:`2026-04-08` + +## 0. 这份清单回答什么 + +这份文档只回答一个问题: + +**当前仓库里,自定义世界仍然依赖哪些“模板世界设定”。** + +这里的“模板世界设定”不是指玩家还能进入的 `武侠 / 仙侠` 预设世界流程,而是指: + +1. 自定义世界在生成时仍借用哪些模板锚点。 +2. 自定义世界在运行时仍复用哪些模板 schema、词库、怪物池、角色模板、图片参考池。 +3. 哪些引用是“必须保留”,否则会直接伤到自定义世界。 +4. 哪些引用只是编辑器 / 审计 / 测试残留,不属于自定义世界主链硬依赖。 + +一句话结论先说: + +**当前主流程虽然已经不再让玩家进入武侠 / 仙侠预设世界,但自定义世界底层仍明确依赖“模板世界锚点层”。** + +--- + +## 1. 依赖总览 + +自定义世界当前仍依赖的模板设定,主要分成 7 类: + +1. 模板世界锚点类型 +2. 主题判定与规则回退 +3. 属性 schema 与术语体系 +4. 角色模板与技能骨架 +5. 场景图参考池与营地图像逻辑 +6. 怪物 / 敌对实体模板池 +7. 叙事 ThemePack 与生成 prompt 兼容字段 + +除此之外,还有一类是: + +8. 编辑器 / 审计 / 测试中的残留模板引用 + +这类不一定属于自定义世界正式运行时硬依赖,但当前仓库里仍然存在。 + +--- + +## 2. 核心硬依赖 + +## 2.1 模板世界锚点类型 + +这是最底层、也是目前最不能直接删的部分。 + +相关文件: + +- `src/types/core.ts` +- `src/types/customWorld.ts` +- `src/services/customWorld.ts` +- `src/data/customWorldLibrary.ts` + +当前依赖点: + +1. `WorldType.WUXIA / WorldType.XIANXIA / WorldType.CUSTOM` + - 自定义世界虽然运行时是 `CUSTOM`,但其模板锚点仍通过 `WUXIA / XIANXIA` 表达。 + +2. `WorldTemplateType = Exclude` + - 这就是“自定义世界挂靠哪个模板锚”的类型定义。 + +3. `CustomWorldProfile.templateWorldType` + - 当前自定义世界 profile 里明确保存这个字段。 + - 它是兼容字段,但目前仍被多个系统直接消费。 + +4. `buildCustomWorldFrameworkPrompt(...)` + - 生成框架 prompt 仍要求模型输出: + - `"templateWorldType": "WUXIA|XIANXIA"` + +5. `customWorldLibrary.normalizeProfile(...)` + - 本地读取自定义世界时,也会把 `templateWorldType` 归一化到: + - `WUXIA` + - `XIANXIA` + +结论: + +**如果直接删掉 `WUXIA / XIANXIA / WorldTemplateType / templateWorldType` 这层,自定义世界的生成、存档归一化和多处运行时回退都会直接断。** + +--- + +## 2.2 主题判定与规则回退 + +相关文件: + +- `src/services/customWorldTheme.ts` +- `src/data/customWorldRuntime.ts` +- `src/services/storyEngine/themePack.ts` + +当前依赖点: + +1. `detectCustomWorldThemeMode(profile)` + - 先把自定义世界识别成: + - `martial` + - `arcane` + - `machina` + - `tide` + - `rift` + - `mythic` + +2. `resolveCustomWorldAnchorWorldType(profile)` + - 再把主题模式压回模板锚点: + - `arcane -> XIANXIA` + - 其它默认回到 `WUXIA` + +3. `resolveRuleWorldType(worldType, customWorldProfile)` + - 这是当前运行时非常关键的桥接函数。 + - 当 `worldType === CUSTOM` 时,它会把规则世界解析成模板锚点。 + - 如果没有 profile,还会默认回退到 `WUXIA`。 + +4. `buildThemePackFromWorldProfile(profile)` + - 自定义世界最终的 `ThemePack` 并不是纯空中生成。 + - 它是从预设的主题包底板开始,再用自定义世界自己的词汇补进去。 + +结论: + +**自定义世界现在不是完全脱离模板世界独立运行,而是“先判定自己的主题模式,再回落到模板锚点做规则支撑”。** + +--- + +## 2.3 属性 schema 与术语体系 + +相关文件: + +- `src/data/worldAttributeSchemas.ts` +- `src/services/attributeSchemaGenerator.ts` +- `src/services/customWorldPresentation.ts` +- `src/data/economy.ts` + +当前依赖点: + +1. `PRESET_WORLD_ATTRIBUTE_SCHEMAS` + - 当前仓库里有两套预设属性 schema: + - 武侠:`江湖六脉` + - 仙侠:`灵界六轴` + +2. `getPresetWorldAttributeSchema(...)` + - 多个地方仍然用它作为生成自定义世界 schema 的参考底板。 + +3. `generateWorldAttributeSchema(...)` + - 自定义世界自己的 attribute schema 虽然是生成的, + - 但内部会参考预设世界 schema 的槽位结构和 fallback 逻辑。 + +4. `getAttributeLabelsForWorld(...) / getResourceLabelsForWorld(...)` + - 如果当前没有显式自定义 world presentation,会按 `WUXIA / XIANXIA` 走回退术语。 + +5. `getCurrencyName(...) / getInitialPlayerCurrency(...)` + - 经济层仍按模板世界决定初始货币命名和初始数量。 + +结论: + +**自定义世界现在虽然有自己的 presentation 和 attribute schema,但模板世界仍然是它们的 fallback 和参考骨架。** + +--- + +## 2.4 角色模板与技能骨架 + +相关文件: + +- `src/data/characterPresets.ts` + +当前依赖点: + +1. `PRESET_CHARACTERS` + - 自定义世界的可玩角色 / 场景角色运行时形态,是从现有预设角色模板变体化出来的。 + +2. `pickCustomWorldRoleTemplateCharacter(...)` + - 会从 `PRESET_CHARACTERS` 里挑一个模板角色作为骨架。 + +3. `buildCustomWorldRoleCharacter(...)` + - 会把自定义世界角色内容覆盖到模板角色上: + - 名字 + - 背景 + - 描述 + - 技能文案 + - 外观 + - 等等 + +4. `buildCustomWorldSkillVariant(...)` + - 自定义世界角色技能的数值和命名,也是从模板技能定义变体生成出来的。 + +5. `adventureOpenings` + - 当前实现里,自定义世界角色 opening 同时写入: + - `WUXIA` + - `XIANXIA` + - `CUSTOM` + - 说明角色开局结构仍沿用模板世界的旧接口习惯。 + +6. `getCharacterHomeSceneId / getCharacterNpcSceneIds` + - 对 `CUSTOM` 路径已经优先走自定义世界自己的 landmark 映射; + - 但非 CUSTOM 路径仍大量依赖模板世界的基础场景绑定表。 + +结论: + +**自定义世界角色并不是从零独立建模,而是“自定义内容 + 预设角色模板骨架”的组合。** + +--- + +## 2.5 场景图参考池与营地图像逻辑 + +相关文件: + +- `src/data/customWorldVisuals.ts` +- `src/services/customWorldCamp.ts` + +当前依赖点: + +1. `WUXIA_SCENE_IMAGE_REFERENCES` +2. `XIANXIA_SCENE_IMAGE_REFERENCES` +3. `WORLD_SCENE_IMAGE_REFERENCES` + +当前自定义世界场景图不是纯随机抽图,而是: + +1. 先确定模板锚点世界 +2. 再从对应模板世界的场景参考词池里匹配: + - 场景名称 + - 关键词 + - 图片参考 + +具体依赖: + +1. `collectWorldSceneImagePool(worldType)` + - 按模板世界从背景包里抽参考池。 + +2. `buildSceneReferencePool(worldType)` + - 用武侠 / 仙侠各自的场景参考名和关键词构造图像匹配池。 + +3. `getDefaultCustomWorldSceneImage(...)` + - 自定义世界 landmark / camp / 场景默认图,会基于模板世界参考池挑选。 + +4. `resolveCustomWorldCampSceneImage(profile)` + - 开局归处场景图也依赖 `templateWorldType` 和主题判定。 + +结论: + +**当前自定义世界的场景视觉虽然是独立输出,但“默认图像匹配逻辑”仍然是挂在武侠 / 仙侠两套参考池上的。** + +--- + +## 2.6 怪物 / 敌对实体模板池 + +相关文件: + +- `src/data/customWorldNpcMonsters.ts` +- `src/data/hostileNpcPresets.ts` +- `src/data/hostileNpcs.ts` +- `src/data/scenePresets.ts` + +当前依赖点: + +1. `resolveCustomWorldNpcMonsterPreset(...)` + - 自定义世界中带敌意的 NPC / 怪物,会从模板怪物池里找最接近的 preset。 + +2. `getMonsterPresetPool(worldType?)` + - 如果传了 worldType,就取对应模板世界的怪物池。 + - 如果没传,就把武侠和仙侠怪物池拼起来一起选。 + +3. `resolveRuleWorldType(...)` + - `hostileNpcPresets.ts` 和 `hostileNpcs.ts` 在处理 `CUSTOM` 时,会先解析到模板锚点,再决定取哪个怪物 preset / schema。 + +4. `getMonsterPresetsByWorld(...) / getHostileNpcPresetById(...)` + - 自定义世界的运行时怪物表现,目前仍然依赖这套模板怪物 preset 查询接口。 + +结论: + +**自定义世界当前没有完全独立的怪物体系,仍然是基于武侠 / 仙侠预设怪物池做匹配和包装。** + +--- + +## 2.7 叙事 ThemePack 与 prompt 兼容字段 + +相关文件: + +- `src/services/customWorld.ts` +- `src/services/storyEngine/themePack.ts` +- `src/services/ai.ts` + +当前依赖点: + +1. 自定义世界框架 prompt 仍强制要求 `templateWorldType` +2. `normalizeCustomWorldGenerationFramework(...)` + - 会把模型输出的模板世界字段规范化 +3. `buildThemePackFromWorldProfile(...)` + - 会以预设主题包为底,再混入自定义世界词汇 +4. `ai.ts` 里某些 fallback 逻辑仍根据 `templateWorldType` 决定主题包回退 + +结论: + +**自定义世界的 AI 生成链条目前明确假设“世界框架里存在模板锚点字段”。** + +--- + +## 3. 运行时硬依赖 vs 非运行时残留 + +## 3.1 自定义世界正式运行时硬依赖 + +这些目前不能轻易删: + +1. `WorldType.WUXIA / WorldType.XIANXIA / WorldTemplateType` +2. `CustomWorldProfile.templateWorldType` +3. `detectCustomWorldThemeMode / resolveCustomWorldAnchorWorldType / resolveRuleWorldType` +4. `PRESET_WORLD_ATTRIBUTE_SCHEMAS / getPresetWorldAttributeSchema` +5. `PRESET_CHARACTERS` 作为自定义角色模板骨架 +6. 武侠 / 仙侠场景图参考池 +7. 武侠 / 仙侠怪物 preset 池 +8. `buildThemePackFromWorldProfile` 的模板底板 +9. `customWorld.ts` 里生成框架 prompt 的 `templateWorldType` 字段约束 + +## 3.2 不是主流程硬依赖,但仓库里仍存在的残留引用 + +这些更多是编辑器 / 工具 / 审计 / 测试引用: + +1. 预设编辑器 + - `preset-editor/*` + +2. 一些开发工具页 + - 例如 `ItemCatalogEditor.tsx` + - `StateFunctionEditor.tsx` + +3. 审计 / 报告工具 + - `storyAuditReport.ts` + +4. 各类基于武侠 / 仙侠的测试 + +5. 一些 UI 图标与世界按钮贴图 + - 例如 `uiAssets.ts` 里的图标键位仍保留 + +这些不一定影响当前玩家主流程,但如果目标是“代码库级彻底清理”,它们也需要后续处理。 + +--- + +## 4. 当前最不能动的边界 + +如果前提是: + +**不要动素材和自定义世界的任何设定。** + +那么当前最不能直接删除的是: + +1. `templateWorldType` +2. `WorldTemplateType` +3. `resolveRuleWorldType(...)` +4. `detectCustomWorldThemeMode(...)` +5. `resolveCustomWorldAnchorWorldType(...)` +6. `PRESET_WORLD_ATTRIBUTE_SCHEMAS` +7. `PRESET_CHARACTERS` +8. `customWorldVisuals.ts` 里的模板场景参考池 +9. `customWorldNpcMonsters.ts` 对模板怪物池的映射 +10. `themePack.ts` 的模板底板 + +原因很简单: + +**这些不是“预设世界可玩入口”,而是自定义世界当前仍在使用的模板支撑层。** + +--- + +## 5. 可以安全理解为“已从玩家主流程移除”的部分 + +目前已经可以视作从正式玩家流程移除的,是: + +1. 世界选择页里的武侠 / 仙侠入口 +2. 主流程的世界选择 API +3. 继续游戏入口中的武侠 / 仙侠旧存档 + +但这不等于: + +- 代码库内部已经完全不再依赖模板世界 + +这两件事要分开看。 + +--- + +## 6. 最后结论 + +当前仓库里,自定义世界对模板设定的依赖,本质上是: + +**“不再复用预设世界的玩家入口,但仍然复用预设世界的模板支撑层。”** + +最准确的理解是: + +1. 玩家已经不能直接进入武侠 / 仙侠预设世界 +2. 但自定义世界仍借用武侠 / 仙侠作为: + - 模板锚点 + - 规则回退 + - 属性 schema 参考 + - 角色模板骨架 + - 场景图参考池 + - 怪物模板池 + - ThemePack 底板 + +所以如果后续要继续“深度清理”,正确顺序不是直接删光 `WUXIA / XIANXIA`,而应该是: + +1. 先识别哪些是主流程入口,哪些是模板支撑层 +2. 再决定是否要把自定义世界从“模板依赖型”重构成“完全自足型” + +在那一步没做完之前,模板支撑层仍然是自定义世界当前可用性的真实依赖。 diff --git a/docs/reference/README.md b/docs/reference/README.md index c1d1b9e4..1693def1 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -3,8 +3,11 @@ ## 当前入口 - [FUNCTION_SCRIPT_CATALOG_2026-04-04.md](./FUNCTION_SCRIPT_CATALOG_2026-04-04.md):Function 独立脚本目录与分类速查。 +- [TASK_GENERATION_TRACE_2026-04-08.md](./TASK_GENERATION_TRACE_2026-04-08.md):任务描述、达成条件与奖励生成链路梳理。 +- [CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md](./CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md):自定义世界当前仍依赖哪些模板世界设定的清单。 ## 使用建议 - 需要快速定位 Function 脚本,而不是阅读长篇方案时,优先看这里。 +- 需要判断“武侠 / 仙侠模板层”哪些还能删、哪些不能删时,优先看自定义世界模板依赖清单。 - 如果要评估 Function 分层是否合理,再配合 `docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md` 一起看。 diff --git a/docs/reference/TASK_GENERATION_TRACE_2026-04-08.md b/docs/reference/TASK_GENERATION_TRACE_2026-04-08.md new file mode 100644 index 00000000..14971fae --- /dev/null +++ b/docs/reference/TASK_GENERATION_TRACE_2026-04-08.md @@ -0,0 +1,248 @@ +# 任务生成链路与简化建议 + +更新时间:`2026-04-08` + +## 0. 简化版结论 + +推荐把任务系统收敛成一条主链: + +1. `npcInteractions.ts` + - 只负责判断“现在是否适合接任务”,不再提前本地造整份任务预览。 +2. `npcEncounterActions.ts` + - 玩家真正点击“接下委托”时,才调用 AI 任务导演。 +3. `questDirector.ts + questPrompt.ts` + - 用 AI 原生剧情引擎根据当前局势生成任务意图。 +4. `questFlow.ts` + - 只负责把 AI 意图编译成可追踪任务、生成本地奖励、推进步骤。 +5. `goalDirector.ts` + - 把任务编译成“当前目标 / 下一步”。 +6. `AdventurePanelOverlays.tsx` + - 只负责展示,不再自己承载任务生成逻辑。 + +一句话: + +**任务内容主要由 AI 原生剧情引擎生成,本地代码只保留状态推进、奖励结算和失败兜底。** + +## 1. 现在建议保留的主流程 + +## 1.1 接任务 + +推荐主流程: + +`npcInteractions.ts` +-> 展示“可接任务”入口 +-> `npcEncounterActions.ts` +-> `generateQuestForNpcEncounter(...)` +-> `questDirector.ts` +-> `questPrompt.ts` +-> `questFlow.ts` +-> 写入 `QuestLogEntry` + +当前已经做的简化: + +- NPC 面板不再为了预览,先本地生成一整份 fallback 任务。 +- 现在只做任务机会判断,并提示“接取后将由 AI 剧情引擎根据当前局势生成具体目标、步骤与奖励”。 + +这样可以直接消掉一层不必要的双轨: + +- 旧流程: + - 预览先本地生成 + - 正式接取再 AI 生成 +- 新流程: + - 预览只判断有没有任务机会 + - 正式接取时再真正生成任务 + +## 1.2 任务描述 + +简化后应理解成: + +- 任务描述的主来源是 AI 生成的 `QuestIntent.description` +- 本地只负责把它写入 `QuestLogEntry.description` +- UI 只负责展示 `quest.description` + +主脚本: + +- `src/services/questPrompt.ts` +- `src/services/questDirector.ts` +- `src/data/questFlow.ts` + +## 1.3 达成条件 + +简化后应理解成: + +- AI 负责给出“这件事大概要怎么做”的意图 +- 本地把意图编译成 `steps / objective` +- UI 根据当前 `activeStep` 生成最短的“下一步” + +也就是: + +**AI 决定任务方向,本地决定可追踪步骤,UI 只显示当前这一步。** + +主脚本: + +- `src/data/questFlow.ts` +- `src/components/adventure-panel/AdventurePanelOverlays.tsx` + +## 1.4 奖励 + +奖励不建议交给 AI 直接写死。 + +更稳的边界是: + +- AI 只给 `rewardTheme` +- 本地生成具体金币、物品、好感奖励 +- 交付时由本地状态系统结算 + +主脚本: + +- `src/data/questFlow.ts` +- `src/data/runtimeItemDirector.ts` +- `src/hooks/story/sessionActions.ts` +- `src/hooks/story/npcEncounterActions.ts` + +## 2. 你这次最关心的三个问题 + +## 2.1 任务描述怎么生成 + +主生成链: + +- `src/services/questPrompt.ts` + - 约束 AI 输出 `title / description / summary / rewardTheme` +- `src/services/questDirector.ts` + - 请求 AI,得到 `QuestIntent` +- `src/data/questFlow.ts` + - `compileQuestIntentToQuest(...)` 把 `description` 写入 `QuestLogEntry` + +前台展示: + +- `src/components/adventure-panel/AdventurePanelOverlays.tsx` + - 优先显示 `quest.description` + +## 2.2 达成条件怎么生成 + +主生成链: + +- `src/data/questFlow.ts` + - `buildPrimaryQuestStep(...)` + - `buildTalkBackStep(...)` + - 把 AI 任务意图编译成 `steps` + +前台展示: + +- `src/components/adventure-panel/AdventurePanelOverlays.tsx` + - `buildQuestConditionText(...)` + - 根据当前 `activeStep / objective` 重算成一句玩家看得懂的话 + +## 2.3 任务奖励怎么生成 + +主生成链: + +- `src/data/questFlow.ts` + - `buildQuestReward(...)` +- `src/data/runtimeItemContext.ts` + - 给奖励物品生成器准备上下文 +- `src/data/runtimeItemDirector.ts` + - 根据 seed 和频道生成奖励物品 +- `src/data/runtimeItemNarrative.ts` + - 把奖励物品摊平回 `reward.items` + +结算链: + +- `src/hooks/story/sessionActions.ts` +- `src/hooks/story/npcEncounterActions.ts` + +## 3. 哪些预设逻辑应该收缩 + +## 3.1 应该保留 + +- `buildFallbackQuestIntent(...)` + - 保留 + - 但只作为 AI 失败兜底 +- `buildQuestReward(...)` + - 保留 + - 因为奖励和数值应该继续走本地规则 +- `buildPrimaryQuestStep(...) / buildTalkBackStep(...)` + - 保留 + - 因为任务必须能被本地追踪和结算 + +## 3.2 应该降级 + +- `buildQuestForEncounter(...)` + - 不再作为 NPC 面板预览主路径 + - 只保留给 fallback 和测试使用 +- `buildFallbackQuestIntent(...)` 里的多套模板 + - 可以继续精简 + - 最终只保留最小 2 到 3 种兜底 archetype 即可 + +## 3.3 应该谨慎接线 + +- `buildChapterQuestForScene(...)` + - 当前更像“备用章节任务生成器” + - 但我还没有在运行时主链里找到直接调用点 +- `SCENE_CHAPTER_OVERRIDES` + - 不应该继续扩大 + - 只保留少量关键样板场景 + +如果要坚持“AI 原生任务引擎”为主,这部分不应继续膨胀成大规模预设系统。 + +## 4. 脚本解释速查 + +| 脚本 | 作用 | 简化方案中的定位 | +| --- | --- | --- | +| `src/data/npcInteractions.ts` | 生成 NPC 面板选项、礼物/交易/帮助等交互入口 | 只负责任务机会判断和入口展示,不再预生成整任务 | +| `src/hooks/story/npcEncounterActions.ts` | 处理点击“接任务 / 交任务 / 切磋 / 离开”等真实执行逻辑 | 任务接取与交付的运行时主入口 | +| `src/services/questDirector.ts` | 调用 AI,拿到任务意图 `QuestIntent` | AI 原生任务生成主入口 | +| `src/services/questPrompt.ts` | 组织任务 prompt,约束 AI 输出格式 | AI 任务生成的提示词层 | +| `src/data/questFlow.ts` | 把任务意图编译成 quest、推进 steps、生成 reward、做 fallback | 任务数据层主脚本,保留但收缩预设分支 | +| `src/services/storyEngine/goalDirector.ts` | 把 quest / chapter / journeyBeat 编译成当前目标和下一步 | 负责“目标感”,不是负责生成任务本身 | +| `src/components/adventure-panel/AdventurePanelOverlays.tsx` | 展示任务描述、达成条件、奖励和日志 | 纯展示层 | +| `src/data/runtimeItemContext.ts` | 给奖励物品生成器准备上下文 | 奖励生成辅助层 | +| `src/data/runtimeItemDirector.ts` | 生成奖励物品 | 奖励物品生成主脚本 | +| `src/data/runtimeItemNarrative.ts` | 整理奖励物品和叙事 hint | 奖励辅助层 | +| `src/hooks/story/sessionActions.ts` | 统一处理领奖、章节同步等动作 | 非 NPC 面板路径下的奖励结算入口 | + +## 5. 当前代码现状 + +截至这次整理,任务主链可以这样理解: + +### 已经对齐到“AI 优先”的部分 + +- 真正点击“接下委托”时,优先调用 `generateQuestForNpcEncounter(...)` +- AI 返回任务意图后,再由 `questFlow.ts` 编译成本地任务 + +### 仍然是本地规则负责的部分 + +- `steps / objective` 推进 +- 奖励数值和奖励物品 +- 领奖与状态结算 + +### 仍然偏预设、但建议继续收缩的部分 + +- `buildFallbackQuestIntent(...)` +- `buildChapterQuestForScene(...)` +- `SCENE_CHAPTER_OVERRIDES` + +## 6. 推荐后的最简架构 + +如果后面继续收缩,我建议把目标定成下面这版: + +1. NPC 面板只判断“能不能接任务” +2. 点击接任务后,统一走 `questDirector.ts` +3. AI 只产出: + - `title` + - `description` + - `summary` + - `recommendedObjectiveKinds` + - `rewardTheme` +4. `questFlow.ts` 只负责: + - 编译 steps + - 生成 reward + - 推进 status +5. `goalDirector.ts` 只负责把 quest 变成“当前目标 / 下一步” +6. 章节任务生成器只做少量 fallback,不再做大规模场景预设扩张 + +这样之后,代码层的职责会更清楚: + +- AI 负责“这件事讲什么” +- 本地规则负责“这件事怎么追踪、怎么结算” +- UI 负责“把当前最重要的一步展示给玩家” diff --git a/public/generated-character-drafts/_jobs/animation/15838831-6431-4867-a814-b6e7f1613169.json b/public/generated-character-drafts/_jobs/animation/15838831-6431-4867-a814-b6e7f1613169.json new file mode 100644 index 00000000..c270dd49 --- /dev/null +++ b/public/generated-character-drafts/_jobs/animation/15838831-6431-4867-a814-b6e7f1613169.json @@ -0,0 +1,16 @@ +{ + "taskId": "15838831-6431-4867-a814-b6e7f1613169", + "kind": "animation", + "status": "completed", + "characterId": "qwen-sprite-demo", + "animation": "idle", + "strategy": "image-to-video", + "model": "wan2.2-kf2v-flash", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。 角色固定为图1同一角色,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 参考图不仅用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,还要严格约束身体结构骨架。生成结果必须优先沿用参考图的人形动作角色身体结构,包括头、躯干、双臂、双腿、站姿重心和整体头身比;可以变化发型、服装、主题配饰和材质,但不要脱离参考图的人形身体结构。 请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。 默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。 视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。 示例:“水母国王”默认应理解为严格沿用参考图人形身体结构的国王角色,再穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。 动作结构:1-4 帧:稳定站姿,轻微呼吸起伏;5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化;9-12 帧:呼气回落,重心恢复;13-16 帧:逐渐回到与首帧接近的站姿。结尾要求:第 16 帧自然衔接第 1 帧。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。 动作补充细节:动作清晰,幅度明确,适合后续抽帧成横版游戏精灵表。 角色设定:黑熊首领 目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T07:33:00.095Z", + "updatedAt": "2026-04-08T07:33:31.916Z", + "result": { + "previewVideoPath": "/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633611867/preview.mp4", + "draftRelativeDir": "generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633611867" + } +} diff --git a/public/generated-character-drafts/_jobs/animation/433b4f4f-d6f0-4b3b-8246-5ae4b626cb4c.json b/public/generated-character-drafts/_jobs/animation/433b4f4f-d6f0-4b3b-8246-5ae4b626cb4c.json new file mode 100644 index 00000000..58ede935 --- /dev/null +++ b/public/generated-character-drafts/_jobs/animation/433b4f4f-d6f0-4b3b-8246-5ae4b626cb4c.json @@ -0,0 +1,16 @@ +{ + "taskId": "433b4f4f-d6f0-4b3b-8246-5ae4b626cb4c", + "kind": "animation", + "status": "completed", + "characterId": "qwen-sprite-demo", + "animation": "run", + "strategy": "image-to-video", + "model": "wan2.2-kf2v-flash", + "prompt": "单人 NPC 全身动作视频,动作主题是 run。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 奔跑循环。 角色固定为图1同一角色,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 参考图不仅用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,还要严格约束身体结构骨架。生成结果必须优先沿用参考图的人形动作角色身体结构,包括头、躯干、双臂、双腿、站姿重心和整体头身比;可以变化发型、服装、主题配饰和材质,但不要脱离参考图的人形身体结构。 请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。 默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。 视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。 示例:“水母国王”默认应理解为严格沿用参考图人形身体结构的国王角色,再穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。 动作结构:1-4 帧:右腿前摆,左腿后蹬,身体略前倾;5-8 帧:双腿交叉经过身体下方,手臂反向摆动;9-12 帧:左腿前摆,右腿后蹬,继续前倾;13-16 帧:完成另一半跑步循环并回到可接第 1 帧的状态。结尾要求:第 16 帧能无缝接回第 1 帧。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。 动作补充细节:保持动作清晰、节奏明确、适合后续抽帧为 sprite sheet。 角色设定:黑熊首领 目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T07:34:34.233Z", + "updatedAt": "2026-04-08T07:35:08.207Z", + "result": { + "previewVideoPath": "/generated-character-drafts/qwen-sprite-demo/animation/run/animation-video-1775633708180/preview.mp4", + "draftRelativeDir": "generated-character-drafts/qwen-sprite-demo/animation/run/animation-video-1775633708180" + } +} diff --git a/public/generated-character-drafts/_jobs/animation/5867f357-ac12-4e7c-848b-71c4274ba717.json b/public/generated-character-drafts/_jobs/animation/5867f357-ac12-4e7c-848b-71c4274ba717.json new file mode 100644 index 00000000..1def956c --- /dev/null +++ b/public/generated-character-drafts/_jobs/animation/5867f357-ac12-4e7c-848b-71c4274ba717.json @@ -0,0 +1,16 @@ +{ + "taskId": "5867f357-ac12-4e7c-848b-71c4274ba717", + "kind": "animation", + "status": "completed", + "characterId": "qwen-sprite-demo", + "animation": "idle", + "strategy": "image-to-video", + "model": "wan2.7-i2v", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。 角色固定为图1同一人,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 动作结构:1-4 帧:稳定站姿,轻微呼吸起伏;5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化;9-12 帧:呼气回落,重心恢复;13-16 帧:逐渐回到与首帧接近的站姿。结尾要求:第 16 帧自然衔接第 1 帧。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。 动作补充细节:边跳边走 目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T05:02:04.795Z", + "updatedAt": "2026-04-08T05:05:09.119Z", + "result": { + "previewVideoPath": "/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775624709088/preview.mp4", + "draftRelativeDir": "generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775624709088" + } +} diff --git a/public/generated-character-drafts/_jobs/animation/92f0d503-7b86-4f43-9637-81e5119aa646.json b/public/generated-character-drafts/_jobs/animation/92f0d503-7b86-4f43-9637-81e5119aa646.json new file mode 100644 index 00000000..666e3d9f --- /dev/null +++ b/public/generated-character-drafts/_jobs/animation/92f0d503-7b86-4f43-9637-81e5119aa646.json @@ -0,0 +1,13 @@ +{ + "taskId": "92f0d503-7b86-4f43-9637-81e5119aa646", + "kind": "animation", + "status": "failed", + "characterId": "qwen-sprite-demo", + "animation": "idle", + "strategy": "image-to-video", + "model": "wan2.7-i2v", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。 角色固定为图1同一角色,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 风格参考图只用于约束像素画风、颜色组织、头身比例、身体结构、右朝向和镜头语言,不复制参考图的具体种族、发型、服装或武器。 请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。 默认优先使用人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。 视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。 示例:“水母国王”默认应理解为人形国王角色,穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。 只有在文字设定明确写出非人结构时,才保留对应非人身体,例如“非人本体、触手下肢、无双腿、半透明伞盖头部、漂浮水母身体”。 动作结构:1-4 帧:稳定站姿,轻微呼吸起伏;5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化;9-12 帧:呼气回落,重心恢复;13-16 帧:逐渐回到与首帧接近的站姿。结尾要求:第 16 帧自然衔接第 1 帧。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。 动作补充细节:动作清晰,幅度明确,适合后续抽帧成横版游戏精灵表。 角色设定:水母公主 目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T05:49:32.962Z", + "updatedAt": "2026-04-08T05:49:32.962Z", + "errorMessage": "{\"request_id\":\"260207f2-45ab-9fd2-82a1-21cce8ea0465\",\"output\":{\"task_id\":\"92f0d503-7b86-4f43-9637-81e5119aa646\",\"task_status\":\"FAILED\",\"submit_time\":\"2026-04-08 13:43:29.724\",\"scheduled_time\":\"2026-04-08 13:43:31.170\",\"end_time\":\"2026-04-08 13:49:32.185\",\"code\":\"DataInspectionFailed\",\"message\":\"Output data may contain inappropriate content.\"}}" +} diff --git a/public/generated-character-drafts/_jobs/animation/bd19ca9c-e8c8-4c02-b016-d184c8457463.json b/public/generated-character-drafts/_jobs/animation/bd19ca9c-e8c8-4c02-b016-d184c8457463.json new file mode 100644 index 00000000..1a317efe --- /dev/null +++ b/public/generated-character-drafts/_jobs/animation/bd19ca9c-e8c8-4c02-b016-d184c8457463.json @@ -0,0 +1,13 @@ +{ + "taskId": "bd19ca9c-e8c8-4c02-b016-d184c8457463", + "kind": "animation", + "status": "failed", + "characterId": "qwen-sprite-demo", + "animation": "idle", + "strategy": "image-to-video", + "model": "wan2.7-i2v", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。角色固定为图1同一人,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。动作结构:1-4 帧:稳定站姿,轻微呼吸起伏;5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化;9-12 帧:呼气回落,重心恢复;13-16 帧:逐渐回到与首帧接近的站姿。结尾要求:第 16 帧自然衔接第 1 帧。背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。动作补充细节:头部轻微上下起伏,整体保持可爱Q版待机感。目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T04:49:32.756Z", + "updatedAt": "2026-04-08T04:49:32.756Z", + "errorMessage": "{\"request_id\":\"c6092653-cfc9-919c-bbc5-a8dc3fedbda8\",\"output\":{\"task_id\":\"bd19ca9c-e8c8-4c02-b016-d184c8457463\",\"task_status\":\"FAILED\",\"submit_time\":\"2026-04-08 12:48:32.251\",\"scheduled_time\":\"2026-04-08 12:48:34.671\",\"end_time\":\"2026-04-08 12:49:22.912\",\"code\":\"InvalidParameter\",\"message\":\"Input should be 'first_frame', 'last_frame', 'driving_audio' or 'first_clip': input.media.0.type\"}}" +} diff --git a/public/generated-character-drafts/_jobs/animation/e0ca440a-8a50-45a7-88a2-9e550a582272.json b/public/generated-character-drafts/_jobs/animation/e0ca440a-8a50-45a7-88a2-9e550a582272.json new file mode 100644 index 00000000..c0fce8bb --- /dev/null +++ b/public/generated-character-drafts/_jobs/animation/e0ca440a-8a50-45a7-88a2-9e550a582272.json @@ -0,0 +1,13 @@ +{ + "taskId": "e0ca440a-8a50-45a7-88a2-9e550a582272", + "kind": "animation", + "status": "failed", + "characterId": "qwen-sprite-demo", + "animation": "idle", + "strategy": "image-to-video", + "model": "wan2.7-i2v", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。角色固定为图1同一人,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。动作结构:1-4 帧:稳定站姿,轻微呼吸起伏;5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化;9-12 帧:呼气回落,重心恢复;13-16 帧:逐渐回到与首帧接近的站姿。结尾要求:第 16 帧自然衔接第 1 帧。背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。动作补充细节:头部轻微上下起伏,整体保持可爱Q版待机感。目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T04:53:22.156Z", + "updatedAt": "2026-04-08T04:53:22.156Z", + "errorMessage": "{\"request_id\":\"c76e2066-9ee8-9551-a687-f805c33deeca\",\"output\":{\"task_id\":\"e0ca440a-8a50-45a7-88a2-9e550a582272\",\"task_status\":\"FAILED\",\"submit_time\":\"2026-04-08 12:50:35.698\",\"scheduled_time\":\"2026-04-08 12:50:39.177\",\"end_time\":\"2026-04-08 12:53:07.303\",\"code\":\"DataInspectionFailed\",\"message\":\"Output data may contain inappropriate content.\"}}" +} diff --git a/public/generated-character-drafts/_jobs/animation/ebaee9f5-9245-489b-a03f-2a84ea8ea936.json b/public/generated-character-drafts/_jobs/animation/ebaee9f5-9245-489b-a03f-2a84ea8ea936.json new file mode 100644 index 00000000..e2566114 --- /dev/null +++ b/public/generated-character-drafts/_jobs/animation/ebaee9f5-9245-489b-a03f-2a84ea8ea936.json @@ -0,0 +1,16 @@ +{ + "taskId": "ebaee9f5-9245-489b-a03f-2a84ea8ea936", + "kind": "animation", + "status": "completed", + "characterId": "qwen-sprite-demo", + "animation": "idle", + "strategy": "image-to-video", + "model": "wan2.2-kf2v-flash", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。角色固定为图1同一角色,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色。", + "createdAt": "2026-04-08T07:28:15.160Z", + "updatedAt": "2026-04-08T07:28:48.456Z", + "result": { + "previewVideoPath": "/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633328425/preview.mp4", + "draftRelativeDir": "generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633328425" + } +} diff --git a/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775624709088/job.json b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775624709088/job.json new file mode 100644 index 00000000..f98d2754 --- /dev/null +++ b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775624709088/job.json @@ -0,0 +1,9 @@ +{ + "taskId": "5867f357-ac12-4e7c-848b-71c4274ba717", + "model": "wan2.7-i2v", + "strategy": "image-to-video", + "animation": "idle", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。 角色固定为图1同一人,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 动作结构:1-4 帧:稳定站姿,轻微呼吸起伏;5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化;9-12 帧:呼气回落,重心恢复;13-16 帧:逐渐回到与首帧接近的站姿。结尾要求:第 16 帧自然衔接第 1 帧。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。 动作补充细节:边跳边走 目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T05:05:09.116Z", + "videoUrl": "https://dashscope-a717.oss-accelerate.aliyuncs.com/1d/c0/20260408/b1fbde2b/27777303-metadata_c156c3d3f8c73ab0.mp4?Expires=1775711089&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=q8juQS13NdFUUQ%2FeGC1%2B2xYHLlI%3D" +} diff --git a/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775624709088/preview.mp4 b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775624709088/preview.mp4 new file mode 100644 index 00000000..e6dbbbf8 Binary files /dev/null and b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775624709088/preview.mp4 differ diff --git a/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633328425/job.json b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633328425/job.json new file mode 100644 index 00000000..cc2e1e62 --- /dev/null +++ b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633328425/job.json @@ -0,0 +1,9 @@ +{ + "taskId": "ebaee9f5-9245-489b-a03f-2a84ea8ea936", + "model": "wan2.2-kf2v-flash", + "strategy": "image-to-video", + "animation": "idle", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。角色固定为图1同一角色,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色。", + "createdAt": "2026-04-08T07:28:48.453Z", + "videoUrl": "https://dashscope-7c2c.oss-cn-shanghai.aliyuncs.com/1d/69/20260408/1ce55b62/ebaee9f5-9245-489b-a03f-2a84ea8ea936.mp4?Expires=1775719713&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=pd3vhAP%2FOb5IXC2iVMGkERXQyHc%3D" +} diff --git a/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633328425/preview.mp4 b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633328425/preview.mp4 new file mode 100644 index 00000000..74b27f48 Binary files /dev/null and b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633328425/preview.mp4 differ diff --git a/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633611867/job.json b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633611867/job.json new file mode 100644 index 00000000..c91c268d --- /dev/null +++ b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633611867/job.json @@ -0,0 +1,9 @@ +{ + "taskId": "15838831-6431-4867-a814-b6e7f1613169", + "model": "wan2.2-kf2v-flash", + "strategy": "image-to-video", + "animation": "idle", + "prompt": "单人 NPC 全身动作视频,动作主题是 idle。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 待机循环。 角色固定为图1同一角色,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 参考图不仅用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,还要严格约束身体结构骨架。生成结果必须优先沿用参考图的人形动作角色身体结构,包括头、躯干、双臂、双腿、站姿重心和整体头身比;可以变化发型、服装、主题配饰和材质,但不要脱离参考图的人形身体结构。 请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。 默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。 视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。 示例:“水母国王”默认应理解为严格沿用参考图人形身体结构的国王角色,再穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。 动作结构:1-4 帧:稳定站姿,轻微呼吸起伏;5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化;9-12 帧:呼气回落,重心恢复;13-16 帧:逐渐回到与首帧接近的站姿。结尾要求:第 16 帧自然衔接第 1 帧。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。 动作补充细节:动作清晰,幅度明确,适合后续抽帧成横版游戏精灵表。 角色设定:黑熊首领 目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T07:33:31.912Z", + "videoUrl": "https://dashscope-7c2c.oss-cn-shanghai.aliyuncs.com/1d/4f/20260408/1ce55b62/15838831-6431-4867-a814-b6e7f1613169.mp4?Expires=1775719998&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=WyNwCc%2BZ50%2BODrEm8cewZVywInk%3D" +} diff --git a/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633611867/preview.mp4 b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633611867/preview.mp4 new file mode 100644 index 00000000..dc95b51d Binary files /dev/null and b/public/generated-character-drafts/qwen-sprite-demo/animation/idle/animation-video-1775633611867/preview.mp4 differ diff --git a/public/generated-character-drafts/qwen-sprite-demo/animation/run/animation-video-1775633708180/job.json b/public/generated-character-drafts/qwen-sprite-demo/animation/run/animation-video-1775633708180/job.json new file mode 100644 index 00000000..e8120e80 --- /dev/null +++ b/public/generated-character-drafts/qwen-sprite-demo/animation/run/animation-video-1775633708180/job.json @@ -0,0 +1,9 @@ +{ + "taskId": "433b4f4f-d6f0-4b3b-8246-5ae4b626cb4c", + "model": "wan2.2-kf2v-flash", + "strategy": "image-to-video", + "animation": "run", + "prompt": "单人 NPC 全身动作视频,动作主题是 run。 角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。 动作连贯,避免服装、发型、面部、武器随机漂移。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。 单人全身角色动作视频,动作主题是 奔跑循环。 角色固定为图1同一角色,始终侧身朝右,镜头稳定,轮廓清晰。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 参考图不仅用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,还要严格约束身体结构骨架。生成结果必须优先沿用参考图的人形动作角色身体结构,包括头、躯干、双臂、双腿、站姿重心和整体头身比;可以变化发型、服装、主题配饰和材质,但不要脱离参考图的人形身体结构。 请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。 默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。 视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。 示例:“水母国王”默认应理解为严格沿用参考图人形身体结构的国王角色,再穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。 动作结构:1-4 帧:右腿前摆,左腿后蹬,身体略前倾;5-8 帧:双腿交叉经过身体下方,手臂反向摆动;9-12 帧:左腿前摆,右腿后蹬,继续前倾;13-16 帧:完成另一半跑步循环并回到可接第 1 帧的状态。结尾要求:第 16 帧能无缝接回第 1 帧。 背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。 动作补充细节:保持动作清晰、节奏明确、适合后续抽帧为 sprite sheet。 角色设定:黑熊首领 目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。", + "createdAt": "2026-04-08T07:35:08.203Z", + "videoUrl": "https://dashscope-7c2c.oss-cn-shanghai.aliyuncs.com/1d/d9/20260408/1ce55b62/433b4f4f-d6f0-4b3b-8246-5ae4b626cb4c.mp4?Expires=1775720090&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=Lj5K18exe6OkFaKf6xCTAhZELdU%3D" +} diff --git a/public/generated-character-drafts/qwen-sprite-demo/animation/run/animation-video-1775633708180/preview.mp4 b/public/generated-character-drafts/qwen-sprite-demo/animation/run/animation-video-1775633708180/preview.mp4 new file mode 100644 index 00000000..d5fcde5b Binary files /dev/null and b/public/generated-character-drafts/qwen-sprite-demo/animation/run/animation-video-1775633708180/preview.mp4 differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/candidate-01.png new file mode 100644 index 00000000..26cbfabb Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/candidate-02.png new file mode 100644 index 00000000..53e9747b Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/job.json new file mode 100644 index 00000000..64c0c8c9 --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775621887918", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,全身,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,站立待机姿态,脚底完整可见,武器完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色完整置于画面中央,不要裁切头顶和脚底,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,武器握持合理,便于后续连续动作生成。\n\nQ版大头身少女冒险者,头部占比更大,约 2 到 3 头身,金棕色头发,明亮表情,轻甲或冒险服,动作角色轮廓清楚。", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 0, + "drafts": [ + { + "id": "qwen-master-1775621887918-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/1c/20260408/76483b06/b258b423-90f9-4d09-9ca5-71c0ff84ef36.png?Expires=1776227687&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=6RbPDqWGIVf5BrEy%2FpScw0S891U%3D" + }, + { + "id": "qwen-master-1775621887918-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775621887918/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/79/20260408/76483b06/35b45710-ee4a-41b1-bdbe-339df531f7bd.png?Expires=1776227687&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=SGyX9pdu%2BUqdsV9p41cmVbcDFpk%3D" + } + ], + "createdAt": "2026-04-08T04:18:10.374Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/candidate-01.png new file mode 100644 index 00000000..a2dba999 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/candidate-02.png new file mode 100644 index 00000000..bb752b85 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/job.json new file mode 100644 index 00000000..d6565132 --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775621901628", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,全身,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,站立待机姿态,脚底完整可见,武器完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色完整置于画面中央,不要裁切头顶和脚底,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,武器握持合理,便于后续连续动作生成。\n\nQ版大头身少女冒险者,头部占比更大,约 2 到 3 头身,金棕色头发,明亮表情,轻甲或冒险服,动作角色轮廓清楚。", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 0, + "drafts": [ + { + "id": "qwen-master-1775621901628-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/43/20260408/76483b06/3e1302c8-84f7-4794-baa6-b745d01c69f1.png?Expires=1776227701&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=7G%2FzQKhvDjFTDjJRVxNHnbGwh6I%3D" + }, + { + "id": "qwen-master-1775621901628-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775621901628/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/b8/20260408/76483b06/6ea29373-6458-404e-b7be-c2fea0e72a36.png?Expires=1776227701&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=CjwS1ksjowlMWOHXY1k6I3SQb9Q%3D" + } + ], + "createdAt": "2026-04-08T04:18:24.134Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/candidate-01.png new file mode 100644 index 00000000..b1c87790 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/candidate-02.png new file mode 100644 index 00000000..df794eb4 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/job.json new file mode 100644 index 00000000..f6661d6e --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775621956284", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,全身,2D 横版游戏角色标准设定图,角色始终朝右侧,站立待机姿态,脚底完整可见,武器完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色完整置于画面中央,不要裁切头顶和脚底,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,武器握持合理,便于后续连续动作生成。\n\nQ版大头身少女冒险者,头部占比更大,约 2 到 3 头身,金棕色头发,明亮表情,轻甲或冒险服,动作角色轮廓清楚。", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 0, + "drafts": [ + { + "id": "qwen-master-1775621956284-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/78/20260408/76483b06/7bc06ee3-5087-4c76-9a63-c3a504f913e2.png?Expires=1776227755&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=w9NQpmwkjYPvUCXqka7Npy4DPxg%3D" + }, + { + "id": "qwen-master-1775621956284-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775621956284/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/f3/20260408/76483b06/5cc25a8e-62d8-4d1e-a5cb-c7158f4a1a08.png?Expires=1776227755&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=gfbCHs09gqPQMPxsH5l87VGkD%2BI%3D" + } + ], + "createdAt": "2026-04-08T04:19:18.856Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/candidate-01.png new file mode 100644 index 00000000..a87acf8d Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/candidate-02.png new file mode 100644 index 00000000..fe89f1de Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/job.json new file mode 100644 index 00000000..dea4a69c --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775624392456", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,全身,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,站立待机姿态,脚底完整可见,武器完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色完整置于画面中央,不要裁切头顶和脚底,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,武器握持合理,便于后续连续动作生成。\n\n海妖", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775624392456-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/6e/20260408/8b3aee91/ee234373-ea8c-442e-ba82-82d39512b2bd.png?Expires=1776230191&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=213U2JTKtHnMxYsVD%2BvEAdRaQls%3D" + }, + { + "id": "qwen-master-1775624392456-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775624392456/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/d4/20260408/8b3aee91/ace63da1-4906-4545-8ee1-a5662a1524a8.png?Expires=1776230192&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=ZQrv0t1btJ9NuacOlu9jvGKntrQ%3D" + } + ], + "createdAt": "2026-04-08T04:59:54.906Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/candidate-01.png new file mode 100644 index 00000000..8ef1b2d9 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/candidate-02.png new file mode 100644 index 00000000..70c9ef72 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/job.json new file mode 100644 index 00000000..71be0cae --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775624876722", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,全身,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,站立待机姿态,脚底完整可见,武器完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色完整置于画面中央,不要裁切头顶和脚底,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,武器握持合理,便于后续连续动作生成。\n水母国王", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775624876722-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/42/20260408/8b3aee91/0770d986-ed4f-41ed-a8b0-8a265cb847f7.png?Expires=1776230676&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=bBtx%2B%2FOisKA0scmab3%2F4icAOQtM%3D" + }, + { + "id": "qwen-master-1775624876722-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775624876722/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/f0/20260408/8b3aee91/eecc33d6-ec36-44cc-8d51-7dbf13ccc383.png?Expires=1776230676&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=vd7lRhTwthHRC4u4ah0ogzMDkeA%3D" + } + ], + "createdAt": "2026-04-08T05:07:59.699Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/candidate-01.png new file mode 100644 index 00000000..d7389af5 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/candidate-02.png new file mode 100644 index 00000000..ae03f185 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/job.json new file mode 100644 index 00000000..ce2b81ae --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775625635778", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 风格参考图只用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,不复制参考图的具体种族、身体结构、发型、服装或武器。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,武器握持合理,便于后续连续动作生成。\n文字设定优先决定角色的物种、身体结构、肢体数量、头部形态、尾巴、触手、翅膀、下肢形式和材质特征。如果文字设定是非人、怪物、海洋生物、植物体、机械体、凝胶体或亡灵体,就必须保留对应非人形态,不要自动人类化。\n角色主体底部结构必须服从文字设定。如果角色是鱼尾、触手、漂浮体、凝胶体、蛇尾、机械履带或其他非人下肢结构,就保持对应结构,不要强行补成人类双腿、脚或站立姿势。\n水母国王", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素,不要默认生成普通人类,不要把非人设定自动改成人类,不要强行补成人类脸、人类双腿、人类四肢、人类耳朵或普通人类皮肤结构,除非文字设定明确要求", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775625635778-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/fc/20260408/8b3aee91/5301a16e-1464-4b8f-a9a8-fa9dc8c9bc62.png?Expires=1776231435&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=jPUpwJUN%2Bz8eoK5dH2wPmAqS6Ew%3D" + }, + { + "id": "qwen-master-1775625635778-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775625635778/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/84/20260408/8b3aee91/15ade2f0-e86a-4d18-bc1c-d665a2ba48ee.png?Expires=1776231435&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=quEzTquQCJ2r%2BEfq3AJVlEGV%2BHw%3D" + } + ], + "createdAt": "2026-04-08T05:20:38.510Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/candidate-01.png new file mode 100644 index 00000000..710ad82b Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/candidate-02.png new file mode 100644 index 00000000..3af62e0d Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/job.json new file mode 100644 index 00000000..b13fc647 --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775626712086", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 风格参考图只用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,不复制参考图的具体种族、身体结构、发型、服装或武器。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,道具/权杖/武器如有则存在关系合理,便于后续连续动作生成。\n请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。\n默认优先使用人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。\n视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。\n示例:“水母国王”默认应理解为人形国王角色,穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。\n只有在文字设定明确写出非人结构时,才保留对应非人身体,例如“非人本体、触手下肢、无双腿、半透明伞盖头部、漂浮水母身体”。\n水母公主", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素,不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775626712086-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/9a/20260408/8b3aee91/a3ad1312-0bfb-4ed5-b852-a9222d6bdeae.png?Expires=1776232511&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=SsFMW587lgbD9J5hkw%2BTTUw4VPw%3D" + }, + { + "id": "qwen-master-1775626712086-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775626712086/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/00/20260408/8b3aee91/0785aeca-5eba-4bab-8eba-05a5db22b8e4.png?Expires=1776232511&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=%2F42%2BwjoM0mtahK%2FwocvxadeuU6g%3D" + } + ], + "createdAt": "2026-04-08T05:38:34.575Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/candidate-01.png new file mode 100644 index 00000000..ec6d8d45 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/candidate-02.png new file mode 100644 index 00000000..9a430260 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/job.json new file mode 100644 index 00000000..a295863d --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775627051206", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 风格参考图只用于约束像素画风、颜色组织、头身比例、身体结构、右朝向和镜头语言,不复制参考图的具体种族、发型、服装或武器。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,道具/权杖/武器如有则存在关系合理,便于后续连续动作生成。\n请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。\n默认优先使用人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。\n视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。\n示例:“水母国王”默认应理解为人形国王角色,穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。\n只有在文字设定明确写出非人结构时,才保留对应非人身体,例如“非人本体、触手下肢、无双腿、半透明伞盖头部、漂浮水母身体”。\n水母公主", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素,不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775627051206-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/28/20260408/8b3aee91/60fe96fa-3fe4-4ad5-89a5-62c154003c18.png?Expires=1776232850&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=XunqrTOrrh%2BBG4ToyejUXNHDxjw%3D" + }, + { + "id": "qwen-master-1775627051206-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775627051206/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/4f/20260408/8b3aee91/9a1521ff-ec60-4d2a-8f0d-ef383cd5d154.png?Expires=1776232850&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=825mPHtUdTw0DHeG2wIeOhBpmlY%3D" + } + ], + "createdAt": "2026-04-08T05:44:13.679Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/candidate-01.png new file mode 100644 index 00000000..28945b4f Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/candidate-02.png new file mode 100644 index 00000000..03120a8c Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/job.json new file mode 100644 index 00000000..a62b6e3e --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775627124102", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 风格参考图只用于约束像素画风、颜色组织、头身比例、身体结构、右朝向和镜头语言,不复制参考图的具体种族、发型、服装或武器。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,道具/权杖/武器如有则存在关系合理,便于后续连续动作生成。\n请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。\n默认优先使用人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。\n视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。\n示例:“水母国王”默认应理解为人形国王角色,穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。\n只有在文字设定明确写出非人结构时,才保留对应非人身体,例如“非人本体、触手下肢、无双腿、半透明伞盖头部、漂浮水母身体”。\n高贵的水母公主", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素,不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775627124102-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/b3/20260408/8b3aee91/c6ec3cf0-9b35-4eba-bf5f-e21072b6a924.png?Expires=1776232923&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=J9sr%2FCWxWs2BFY70eHdWCIwRugo%3D" + }, + { + "id": "qwen-master-1775627124102-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775627124102/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/fd/20260408/8b3aee91/c2573141-524b-44ed-9200-152a18d0fbb4.png?Expires=1776232923&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=5fSRKI48uKApHz11F655w6khEFI%3D" + } + ], + "createdAt": "2026-04-08T05:45:26.542Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/candidate-01.png new file mode 100644 index 00000000..532d2ed5 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/candidate-02.png new file mode 100644 index 00000000..b150a216 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/job.json new file mode 100644 index 00000000..de1f05e8 --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775629317889", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 风格参考图只用于约束像素画风、颜色组织、头身比例、身体结构、右朝向和镜头语言,不复制参考图的具体种族、发型、服装或武器。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,道具/权杖/武器如有则存在关系合理,便于后续连续动作生成。\n请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。\n默认优先使用人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。\n视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。\n示例:“水母国王”默认应理解为人形国王角色,穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。\n只有在文字设定明确写出非人结构时,才保留对应非人身体,例如“非人本体、触手下肢、无双腿、半透明伞盖头部、漂浮水母身体”。\n黑熊战神", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素,不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775629317889-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/67/20260408/8b3aee91/3567ac38-fc5f-4448-9923-cea09a0d318f.png?Expires=1776235117&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=f6h12%2BQ%2FAct29kHJfUFl4SpK4Wc%3D" + }, + { + "id": "qwen-master-1775629317889-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775629317889/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/57/20260408/8b3aee91/0ee1d5d2-377c-4de0-98a6-7dd5495fd2f0.png?Expires=1776235117&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=wA0K8Kpl%2FeAvOUue2UAiNIkVJ0o%3D" + } + ], + "createdAt": "2026-04-08T06:22:00.397Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/candidate-01.png new file mode 100644 index 00000000..fa0343d4 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/candidate-02.png new file mode 100644 index 00000000..12254791 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/job.json new file mode 100644 index 00000000..f384b8a1 --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775630268164", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 参考图不仅用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,还要严格约束身体结构骨架。生成结果必须优先沿用参考图的人形动作角色身体结构,包括头、躯干、双臂、双腿、站姿重心和整体头身比;可以变化发型、服装、主题配饰和材质,但不要脱离参考图的人形身体结构。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,道具/权杖/武器如有则存在关系合理,便于后续连续动作生成。\n请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。\n默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。\n视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。\n示例:“水母国王”默认应理解为严格沿用参考图人形身体结构的国王角色,再穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。\n只有在文字设定明确写出非人结构时,才保留对应非人身体,例如“非人本体、触手下肢、无双腿、半透明伞盖头部、漂浮水母身体”。\n黑熊首领", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素,不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775630268164-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/54/20260408/8b3aee91/4357f0aa-7a1b-4331-a2a0-9ef78f92e3f3.png?Expires=1776236067&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=bwac03%2BDrGMucwsrsf8%2F4nlloRE%3D" + }, + { + "id": "qwen-master-1775630268164-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775630268164/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/d9/20260408/8b3aee91/b0d395b6-43b6-449c-ac81-4e00584be65e.png?Expires=1776236067&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=xNAxVvSjij2SU4wdFFPyzkwC3gU%3D" + } + ], + "createdAt": "2026-04-08T06:37:50.861Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/candidate-01.png new file mode 100644 index 00000000..d40d799b Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/candidate-02.png new file mode 100644 index 00000000..90a39d75 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/job.json new file mode 100644 index 00000000..bcac0ccf --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775631112051", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 参考图不仅用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,还要严格约束身体结构骨架。生成结果必须优先沿用参考图的人形动作角色身体结构,包括头、躯干、双臂、双腿、站姿重心和整体头身比;可以变化发型、服装、主题配饰和材质,但不要脱离参考图的人形身体结构。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,道具/权杖/武器如有则存在关系合理,便于后续连续动作生成。\n请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。\n默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。\n视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。\n示例:“水母国王”默认应理解为严格沿用参考图人形身体结构的国王角色,再穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。\n黑熊首领", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素,不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775631112051-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/0e/20260408/8b3aee91/74803635-ce09-481e-9a51-1287a5ef729f.png?Expires=1776236911&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=d6nWY0doc0bZCDLMq%2Fq3rmpserE%3D" + }, + { + "id": "qwen-master-1775631112051-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775631112051/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/01/20260408/8b3aee91/5225636f-3b25-440a-82ce-477371f2d251.png?Expires=1776236911&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=SUY9fJdU4cDGZYcZAGNyL0EhhP0%3D" + } + ], + "createdAt": "2026-04-08T06:51:56.620Z" +} diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/candidate-01.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/candidate-01.png new file mode 100644 index 00000000..cb919305 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/candidate-02.png b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/candidate-02.png new file mode 100644 index 00000000..07d5d569 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/job.json b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/job.json new file mode 100644 index 00000000..1ea384ca --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-master-1775633556964", + "kind": "master", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "单人,2D 横版游戏角色标准设定图,角色始终朝右侧,侧视角为主,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。\n画面要求:1:1 正方形画布,纯色浅背景,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要多角色,不要复杂环境,不要镜头透视,不要特写。\n风格要求:Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。 即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色,方便读图和后续动画化。 参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。 参考图不仅用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,还要严格约束身体结构骨架。生成结果必须优先沿用参考图的人形动作角色身体结构,包括头、躯干、双臂、双腿、站姿重心和整体头身比;可以变化发型、服装、主题配饰和材质,但不要脱离参考图的人形身体结构。 高可读性游戏角色设定图,偏像素动画前置设计稿,形体清晰,服装层次明确,道具/权杖/武器如有则存在关系合理,便于后续连续动作生成。\n请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。\n默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时,才改为对应非人身体。\n视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。\n示例:“水母国王”默认应理解为严格沿用参考图人形身体结构的国王角色,再穿带有水母主题的服装和配饰,例如半透明蓝紫色披肩或裙摆、像水母伞盖的王冠、荧光斑点、海洋质感袖摆、水母权杖,而不是完整水母怪物本体。\n黑熊首领", + "negativePrompt": "正面视角,左朝向,镜头透视,半身像,脚被裁切,头顶被裁切,多角色,复杂背景,武器消失,武器换手,额外手臂,额外腿,服装变化,脸部变化,模糊,运动模糊,文字,水印,UI 元素,不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体", + "promptExtend": true, + "seed": 1101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-master-1775633556964-1", + "label": "主图 1", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/d9/20260408/8b3aee91/ea17e524-8af3-41a0-9246-1183e306b608.png?Expires=1776239356&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=GOIqUHovAYNxNzEBaIifBiWsUHU%3D" + }, + { + "id": "qwen-master-1775633556964-2", + "label": "主图 2", + "imageSrc": "/generated-qwen-sprites/_drafts/master/qwen-master-1775633556964/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/2f/20260408/8b3aee91/6e921ff7-8c7a-4a24-bab7-b3bd47c60912.png?Expires=1776239356&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=7E7ef9RlGkM%2BFi8BtBdUk96ew4g%3D" + } + ], + "createdAt": "2026-04-08T07:32:40.381Z" +} diff --git a/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/candidate-01.png b/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/candidate-01.png new file mode 100644 index 00000000..9c37b930 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/candidate-01.png differ diff --git a/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/candidate-02.png b/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/candidate-02.png new file mode 100644 index 00000000..fa7917d7 Binary files /dev/null and b/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/candidate-02.png differ diff --git a/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/job.json b/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/job.json new file mode 100644 index 00000000..e574556e --- /dev/null +++ b/public/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/job.json @@ -0,0 +1,27 @@ +{ + "draftId": "qwen-sheet-1775622034018", + "kind": "sheet", + "model": "qwen-image-2.0", + "size": "1024*1024", + "promptText": "使用图1作为唯一角色身份参考。生成一张 4x4 的 sprite sheet,共 16 帧,展示同一个角色的连续动作。角色始终朝右,全身完整出现在每一个格子里,脚底始终可见,地面线高度基本一致,角色在每一格中的尺度基本一致,镜头固定不变,不要切换景别,不要切换视角,不要左右翻转。Q版大头身动作角色,头部占比明显更大,约 2 到 3 头身,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。\n动作名:待机循环\n是否循环:是\n身体位移:原地\n武器规则:武器始终在主手,位置稳定\n1-4 帧:稳定站姿,轻微呼吸起伏\n5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化\n9-12 帧:呼气回落,重心恢复\n13-16 帧:逐渐回到与首帧接近的站姿\n结尾要求:第 16 帧自然衔接第 1 帧\n输出要求:每一格都要清晰分开,网格顺序从左到右、从上到下,动作连续,首尾关系明确,轮廓稳定,发型稳定,服装结构稳定,武器始终在正确的手中,背景为纯浅色,适合后续切成 sprite frames。\nQ版大头身少女冒险者,头部占比更大,约 2 到 3 头身,金棕色头发,明亮表情,轻甲或冒险服,动作角色轮廓清楚。\n每格边界清晰,背景纯浅色,适合后续切帧。", + "negativePrompt": "多角色,左右朝向混乱,前视图,背视图,镜头切换,景别变化,特写,脚底裁切,头顶裁切,缺手,缺脚,额外肢体,武器消失,武器换手,服装变化,脸部变化,发型变化,动作不连续,重复帧过多,构图混乱,背景复杂,强透视,运动模糊,残影,文字,水印,UI,边框覆盖角色", + "promptExtend": false, + "seed": 2101, + "candidateCount": 2, + "referenceImageCount": 1, + "drafts": [ + { + "id": "qwen-sheet-1775622034018-1", + "label": "精灵表 1", + "imageSrc": "/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/candidate-01.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/0a/20260408/8b3aee91/a60efc3a-dc00-42c3-90a0-987ffeb94973.png?Expires=1776227833&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=vRjvyHXKKq6IKjvRM9%2BoKnT8FNg%3D" + }, + { + "id": "qwen-sheet-1775622034018-2", + "label": "精灵表 2", + "imageSrc": "/generated-qwen-sprites/_drafts/sheet/qwen-sheet-1775622034018/candidate-02.png", + "remoteUrl": "https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/ad/20260408/8b3aee91/28305000-f9b6-484b-a70b-5bb80ca55d08.png?Expires=1776227833&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=dILDDQDifvGxx3MFYopC%2BXtKoOs%3D" + } + ], + "createdAt": "2026-04-08T04:20:37.029Z" +} diff --git a/scripts/dev-server/characterAssetStudioPlugins.ts b/scripts/dev-server/characterAssetStudioPlugins.ts index 5e302044..91c11eb0 100644 --- a/scripts/dev-server/characterAssetStudioPlugins.ts +++ b/scripts/dev-server/characterAssetStudioPlugins.ts @@ -9,6 +9,12 @@ import path from 'node:path'; import { loadEnv, type Plugin } from 'vite'; +import { + buildMasterPrompt, + buildVideoActionPrompt, + getActionTemplateById, +} from '../../src/tools/qwenSpriteSheetToolModel'; + const CHARACTER_VISUAL_GENERATE_PATH = '/api/character-visual/generate'; const CHARACTER_VISUAL_JOBS_PATH = '/api/character-visual/jobs/'; const CHARACTER_ANIMATION_GENERATE_PATH = '/api/animation/generate'; @@ -17,7 +23,7 @@ const CHARACTER_ANIMATION_IMPORT_VIDEO_PATH = '/api/animation/import-video'; const CHARACTER_ANIMATION_TEMPLATES_PATH = '/api/animation/templates'; const DEFAULT_DASHSCOPE_BASE_URL = 'https://dashscope.aliyuncs.com/api/v1'; const DEFAULT_CHARACTER_VISUAL_MODEL = 'wan2.7-image-pro'; -const DEFAULT_CHARACTER_VIDEO_MODEL = 'wan2.7-i2v'; +const DEFAULT_CHARACTER_VIDEO_MODEL = 'wan2.2-kf2v-flash'; const DEFAULT_CHARACTER_REFERENCE_VIDEO_MODEL = 'wan2.7-r2v'; const DEFAULT_CHARACTER_MOTION_TRANSFER_MODEL = 'wan2.2-animate-move'; const DASHSCOPE_IMAGE_TASK_POLL_INTERVAL_MS = 2500; @@ -302,6 +308,18 @@ async function resolveMediaSourcePayload( }; } +async function resolveMediaSourceAsDataUrl( + rootDir: string, + source: string, +) { + if (/^data:/u.test(source)) { + return source; + } + + const payload = await resolveMediaSourcePayload(rootDir, source); + return `data:${payload.mimeType};base64,${payload.buffer.toString('base64')}`; +} + function requestResponse( urlString: string, options: { @@ -684,14 +702,15 @@ function extractImageUrls(payload: Record) { return [...new Set(urls)]; } -function buildNpcVisualPrompt(promptText: string) { - const trimmed = promptText.trim(); - return [ - '单人 NPC 角色形象,全身,侧身朝右,站姿稳定,武器与手完整可见。', - '画面简洁,背景干净,角色轮廓清楚,适合后续做动作与裁切。', - '不要多人,不要复杂场景,不要夸张透视,不要截断脚底。', - trimmed || '江湖风格角色,服装完整,姿态自然。', - ].join(' '); +function buildNpcVisualPrompt( + promptText: string, + characterBriefText = '', +) { + const mergedBrief = [characterBriefText.trim(), promptText.trim()] + .filter(Boolean) + .join('\n'); + + return buildMasterPrompt(mergedBrief || '江湖风格角色,服装完整,姿态自然。'); } function buildImageSequencePrompt( @@ -713,19 +732,36 @@ function buildImageSequencePrompt( .join(' '); } -function buildNpcAnimationPrompt( - animation: string, - promptText: string, - useChromaKey: boolean, -) { +function buildNpcAnimationPrompt(options: { + animation: string; + promptText: string; + useChromaKey: boolean; + characterBriefText?: string; + actionTemplateId?: string; +}) { + if (options.actionTemplateId) { + return buildVideoActionPrompt({ + actionTemplate: getActionTemplateById( + options.actionTemplateId as Parameters[0], + ), + actionDetailText: options.promptText, + useChromaKey: options.useChromaKey, + characterBrief: + options.characterBriefText?.trim() || `${options.animation} 动作角色`, + }); + } + return [ - `单人 NPC 全身动作视频,动作主题是 ${animation}。`, + `单人 NPC 全身动作视频,动作主题是 ${options.animation}。`, '角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。', '动作连贯,避免服装、发型、面部、武器随机漂移。', - useChromaKey + options.useChromaKey ? '背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。' : '背景简洁纯净,无复杂场景。', - promptText.trim(), + options.characterBriefText?.trim() + ? `角色设定:${options.characterBriefText.trim()}` + : '', + options.promptText.trim(), ] .filter(Boolean) .join(' '); @@ -787,13 +823,17 @@ async function handleGenerateCharacterVisuals( typeof body.characterId === 'string' ? body.characterId.trim() : 'character'; - const sourceMode = - typeof body.sourceMode === 'string' ? body.sourceMode.trim() : ''; - const promptText = - typeof body.promptText === 'string' ? body.promptText.trim() : ''; - const referenceImageDataUrls = isStringArray(body.referenceImageDataUrls) - ? body.referenceImageDataUrls.slice(0, 4) - : []; + const sourceMode = + typeof body.sourceMode === 'string' ? body.sourceMode.trim() : ''; + const promptText = + typeof body.promptText === 'string' ? body.promptText.trim() : ''; + const characterBriefText = + typeof body.characterBriefText === 'string' + ? body.characterBriefText.trim() + : ''; + const referenceImageDataUrls = isStringArray(body.referenceImageDataUrls) + ? body.referenceImageDataUrls.slice(0, 4) + : []; const candidateCountRaw = typeof body.candidateCount === 'number' ? body.candidateCount : 3; const candidateCount = Math.max( @@ -818,20 +858,20 @@ async function handleGenerateCharacterVisuals( return; } - if (!promptText && sourceMode === 'text-to-image') { - sendJson(res, 400, { - error: { message: '文生主形象需要填写角色设定。' }, - }); + if (!promptText && !characterBriefText && sourceMode === 'text-to-image') { + sendJson(res, 400, { + error: { message: '文生主形象需要填写角色设定。' }, + }); return; } - let activeTaskId = ''; - let activePrompt = ''; - try { - const finalPrompt = buildNpcVisualPrompt(promptText); - activePrompt = finalPrompt; - const content = [ - { text: finalPrompt }, + let activeTaskId = ''; + let activePrompt = ''; + try { + const finalPrompt = buildNpcVisualPrompt(promptText, characterBriefText); + activePrompt = finalPrompt; + const content = [ + { text: finalPrompt }, ...referenceImageDataUrls.map((image) => ({ image })), ]; const createTaskResponse = await proxyJsonRequest( @@ -1059,6 +1099,14 @@ async function handleGenerateCharacterAnimation( typeof body.animation === 'string' ? body.animation.trim() : 'idle'; const promptText = typeof body.promptText === 'string' ? body.promptText.trim() : ''; + const characterBriefText = + typeof body.characterBriefText === 'string' + ? body.characterBriefText.trim() + : ''; + const actionTemplateId = + typeof body.actionTemplateId === 'string' + ? body.actionTemplateId.trim() + : ''; const visualSource = typeof body.visualSource === 'string' ? body.visualSource.trim() : ''; const referenceImageDataUrls = isStringArray(body.referenceImageDataUrls) @@ -1076,7 +1124,7 @@ async function handleGenerateCharacterAnimation( typeof body.frameCount === 'number' && Number.isFinite(body.frameCount) ? Math.max(2, Math.min(16, Math.round(body.frameCount))) : 8; - const durationSeconds = + const requestedDurationSeconds = typeof body.durationSeconds === 'number' && Number.isFinite(body.durationSeconds) ? Math.max(1, Math.min(8, Math.round(body.durationSeconds))) @@ -1098,6 +1146,10 @@ async function handleGenerateCharacterAnimation( ? body.videoModel.trim() : runtimeEnv.DASHSCOPE_CHARACTER_VIDEO_MODEL || DEFAULT_CHARACTER_VIDEO_MODEL; + const durationSeconds = + videoModel === 'wan2.2-kf2v-flash' ? 5 : requestedDurationSeconds; + const normalizedResolution = + videoModel === 'wan2.2-kf2v-flash' ? '480P' : resolution; const referenceVideoModel = typeof body.referenceVideoModel === 'string' && body.referenceVideoModel.trim() @@ -1295,62 +1347,70 @@ async function handleGenerateCharacterAnimation( return; } - const modelForVisualUpload = - strategy === 'reference-to-video' - ? referenceVideoModel - : strategy === 'motion-transfer' - ? motionTransferModel - : videoModel; - const visualUrl = await uploadFileToDashScope( - baseUrl, - apiKey, - modelForVisualUpload, - `${characterId}-${animation}-visual`, - await resolveMediaSourcePayload(rootDir, visualSource), - ); - if (strategy === 'image-to-video') { - const finalPrompt = buildNpcAnimationPrompt( + const finalPrompt = buildNpcAnimationPrompt({ animation, promptText, useChromaKey, - ); + characterBriefText, + actionTemplateId, + }); activePrompt = finalPrompt; activeModel = videoModel; - const media = [ - { type: 'image', url: visualUrl, role: 'first_frame' }, - ...(lastFrameImageDataUrl - ? [ - { - type: 'image', - url: await uploadFileToDashScope( - baseUrl, - apiKey, - videoModel, - `${characterId}-${animation}-last-frame`, - await resolveMediaSourcePayload( - rootDir, - lastFrameImageDataUrl, - ), - ), - role: 'last_frame', - }, - ] - : []), - ]; + const isKf2vFlash = videoModel === 'wan2.2-kf2v-flash'; + const visualInputRef = isKf2vFlash + ? await resolveMediaSourceAsDataUrl(rootDir, visualSource) + : await uploadFileToDashScope( + baseUrl, + apiKey, + videoModel, + `${characterId}-${animation}-visual`, + await resolveMediaSourcePayload(rootDir, visualSource), + ); + const lastFrameRef = lastFrameImageDataUrl + ? isKf2vFlash + ? await resolveMediaSourceAsDataUrl(rootDir, lastFrameImageDataUrl) + : await uploadFileToDashScope( + baseUrl, + apiKey, + videoModel, + `${characterId}-${animation}-last-frame`, + await resolveMediaSourcePayload( + rootDir, + lastFrameImageDataUrl, + ), + ) + : ''; + const inputPayload = + isKf2vFlash + ? { + prompt: finalPrompt, + first_frame_url: visualInputRef, + ...(lastFrameRef ? { last_frame_url: lastFrameRef } : {}), + } + : { + prompt: finalPrompt, + media: [ + { type: 'first_frame', url: visualInputRef }, + ...(lastFrameRef + ? [{ type: 'last_frame', url: lastFrameRef }] + : []), + ], + }; + const videoSynthesisEndpoint = isKf2vFlash + ? `${baseUrl}/services/aigc/image2video/video-synthesis` + : `${baseUrl}/services/aigc/video-generation/video-synthesis`; const createTaskResponse = await proxyJsonRequest( - `${baseUrl}/services/aigc/video-generation/video-synthesis`, + videoSynthesisEndpoint, apiKey, { model: videoModel, - input: { - prompt: finalPrompt, - media, - }, + input: inputPayload, parameters: { duration: durationSeconds, - resolution, + resolution: normalizedResolution, + ...(isKf2vFlash ? { prompt_extend: true, watermark: false } : {}), }, }, { @@ -1482,6 +1542,20 @@ async function handleGenerateCharacterAnimation( return; } + const modelForVisualUpload = + strategy === 'reference-to-video' + ? referenceVideoModel + : strategy === 'motion-transfer' + ? motionTransferModel + : videoModel; + const visualUrl = await uploadFileToDashScope( + baseUrl, + apiKey, + modelForVisualUpload, + `${characterId}-${animation}-visual`, + await resolveMediaSourcePayload(rootDir, visualSource), + ); + if (strategy === 'motion-transfer') { if (referenceVideoDataUrls.length === 0) { sendJson(res, 400, { @@ -1490,11 +1564,12 @@ async function handleGenerateCharacterAnimation( return; } - const finalPrompt = buildNpcAnimationPrompt( + const finalPrompt = buildNpcAnimationPrompt({ animation, promptText, useChromaKey, - ); + characterBriefText, + }); activePrompt = finalPrompt; activeModel = motionTransferModel; const referenceVideoUrl = await uploadFileToDashScope( @@ -1679,11 +1754,12 @@ async function handleGenerateCharacterAnimation( return; } - const finalPrompt = buildNpcAnimationPrompt( + const finalPrompt = buildNpcAnimationPrompt({ animation, promptText, useChromaKey, - ); + characterBriefText, + }); activePrompt = finalPrompt; activeModel = referenceVideoModel; const createTaskResponse = await proxyJsonRequest( diff --git a/scripts/dev-server/localApiPlugins.ts b/scripts/dev-server/localApiPlugins.ts index 4d2a47ec..cc5e3e33 100644 --- a/scripts/dev-server/localApiPlugins.ts +++ b/scripts/dev-server/localApiPlugins.ts @@ -27,7 +27,7 @@ const CHARACTER_VISUAL_PUBLISH_PATH = '/api/character-visual/publish'; const CHARACTER_ANIMATION_PUBLISH_PATH = '/api/animation/publish'; const CUSTOM_WORLD_SCENE_IMAGE_PATH = '/api/custom-world/scene-image'; const DEFAULT_DASHSCOPE_BASE_URL = 'https://dashscope.aliyuncs.com/api/v1'; -const DEFAULT_DASHSCOPE_SCENE_IMAGE_MODEL = 'wan2.2-t2i-flash'; +const DEFAULT_DASHSCOPE_SCENE_IMAGE_MODEL = 'wan2.7-image'; const DASHSCOPE_TASK_POLL_INTERVAL_MS = 2000; const DASHSCOPE_TASK_TIMEOUT_MS = 150000; @@ -541,6 +541,43 @@ async function resolveAssetSourcePayload( }; } +async function resolveAssetSourceAsDataUrl( + rootDir: string, + source: string, + fallbackMessage: string, +) { + if (/^data:image\/[^;]+;base64,/u.test(source)) { + return source; + } + + const payload = await resolveAssetSourcePayload( + rootDir, + source, + fallbackMessage, + ); + const mimeType = (() => { + switch (payload.extension) { + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'webp': + return 'image/webp'; + default: + return 'image/png'; + } + })(); + + return `data:${mimeType};base64,${payload.buffer.toString('base64')}`; +} + +function resolveDashScopeSceneImageModel(model: string) { + if (/^wan2\.7-image(?:-pro)?$/u.test(model)) { + return model; + } + + return DEFAULT_DASHSCOPE_SCENE_IMAGE_MODEL; +} + function resolveImageExtension( contentTypeHeader: string | string[] | undefined, sourceUrl: string, @@ -657,6 +694,44 @@ function getDashScopeImageUrl(taskResponse: Record) { } } + const choices = output && Array.isArray(output.choices) ? output.choices : []; + for (const choice of choices) { + if (!isRecordValue(choice)) { + continue; + } + + const message = isRecordValue(choice.message) ? choice.message : null; + const content = + message && Array.isArray(message.content) ? message.content : []; + + for (const entry of content) { + if (!isRecordValue(entry)) { + continue; + } + + const imageUrl = + typeof entry.image === 'string' && entry.image.trim() + ? entry.image.trim() + : typeof entry.url === 'string' && entry.url.trim() + ? entry.url.trim() + : ''; + + if (imageUrl) { + return { + url: imageUrl, + actualPrompt: + typeof entry.actual_prompt === 'string' && + entry.actual_prompt.trim() + ? entry.actual_prompt.trim() + : typeof entry.revised_prompt === 'string' && + entry.revised_prompt.trim() + ? entry.revised_prompt.trim() + : undefined, + }; + } + } + } + throw new Error('场景图片生成成功,但没有返回可下载的图片地址。'); } @@ -886,10 +961,11 @@ function createCustomWorldSceneImagePlugin( typeof body.size === 'string' && body.size.trim() ? body.size.trim() : '1280*720'; - const model = + const requestedModel = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : defaultModel; + const model = resolveDashScopeSceneImageModel(requestedModel); const worldName = typeof body.worldName === 'string' ? body.worldName.trim() : ''; const profileId = @@ -898,6 +974,10 @@ function createCustomWorldSceneImagePlugin( typeof body.landmarkName === 'string' ? body.landmarkName.trim() : ''; const landmarkId = typeof body.landmarkId === 'string' ? body.landmarkId.trim() : ''; + const referenceImageSrc = + typeof body.referenceImageSrc === 'string' + ? body.referenceImageSrc.trim() + : ''; if (!prompt) { sendJson(res, 400, { error: { message: 'prompt is required.' } }); @@ -912,20 +992,37 @@ function createCustomWorldSceneImagePlugin( } try { + const messageContent: Array<{ image: string } | { text: string }> = []; + if (referenceImageSrc) { + messageContent.push({ + image: await resolveAssetSourceAsDataUrl( + rootDir, + referenceImageSrc, + '参考图必须来自 public 目录或使用 Data URL。', + ), + }); + } + messageContent.push({ text: prompt }); + const createTaskResponse = await proxyJsonRequest( - `${baseUrl}/services/aigc/text2image/image-synthesis`, + `${baseUrl}/services/aigc/image-generation/generation`, apiKey, { model, input: { - prompt, - ...(negativePrompt ? { negative_prompt: negativePrompt } : {}), + messages: [ + { + role: 'user', + content: messageContent, + }, + ], }, parameters: { n: 1, size, prompt_extend: true, watermark: false, + ...(negativePrompt ? { negative_prompt: negativePrompt } : {}), }, }, { @@ -1023,6 +1120,7 @@ function createCustomWorldSceneImagePlugin( size, prompt, negativePrompt, + referenceImageSrc: referenceImageSrc || undefined, actualPrompt: imageResult.actualPrompt, remoteUrl: imageResult.url, imageSrc, @@ -1189,6 +1287,7 @@ function createCharacterVisualPublishPlugin(rootDir: string): Plugin { typeof body.height === 'number' && Number.isFinite(body.height) ? body.height : 1536; + const updateCharacterOverride = body.updateCharacterOverride !== false; if (!characterId) { sendJson(res, 400, { error: { message: 'characterId is required.' } }); @@ -1259,26 +1358,30 @@ function createCharacterVisualPublishPlugin(rootDir: string): Plugin { 'utf8', ); - const overrideMap = await readJsonObjectFile(characterOverridesFilePath); - const existingOverride = overrideMap[characterId]; - const nextOverride = - existingOverride && - typeof existingOverride === 'object' && - !Array.isArray(existingOverride) - ? { ...(existingOverride as Record) } - : {}; - nextOverride.generatedVisualAssetId = assetId; - nextOverride.portrait = masterImagePath; - overrideMap[characterId] = nextOverride; - await writeJsonObjectFile(characterOverridesFilePath, overrideMap); + let overrideMap: Record = {}; + if (updateCharacterOverride) { + overrideMap = await readJsonObjectFile(characterOverridesFilePath); + const existingOverride = overrideMap[characterId]; + const nextOverride = + existingOverride && + typeof existingOverride === 'object' && + !Array.isArray(existingOverride) + ? { ...(existingOverride as Record) } + : {}; + nextOverride.generatedVisualAssetId = assetId; + nextOverride.portrait = masterImagePath; + overrideMap[characterId] = nextOverride; + await writeJsonObjectFile(characterOverridesFilePath, overrideMap); + } sendJson(res, 200, { ok: true, assetId, portraitPath: masterImagePath, overrideMap, - saveMessage: - '主形象已发布到 public/generated-characters,并更新角色覆盖。', + saveMessage: updateCharacterOverride + ? '主形象已发布到 public/generated-characters,并更新角色覆盖。' + : '主形象已保存到 public/generated-characters,可直接写回当前自定义世界角色。', }); } catch (error) { sendJson(res, 500, { @@ -1333,6 +1436,7 @@ function createCharacterAnimationPublishPlugin(rootDir: string): Plugin { !Array.isArray(body.animations) ? (body.animations as Record) : null; + const updateCharacterOverride = body.updateCharacterOverride !== false; if (!characterId) { sendJson(res, 400, { error: { message: 'characterId is required.' } }); @@ -1476,35 +1580,40 @@ function createCharacterAnimationPublishPlugin(rootDir: string): Plugin { 'utf8', ); - const overrideMap = await readJsonObjectFile(characterOverridesFilePath); - const existingOverride = overrideMap[characterId]; - const nextOverride = - existingOverride && - typeof existingOverride === 'object' && - !Array.isArray(existingOverride) - ? { ...(existingOverride as Record) } - : {}; - const existingAnimationMap = - nextOverride.animationMap && - typeof nextOverride.animationMap === 'object' && - !Array.isArray(nextOverride.animationMap) - ? (nextOverride.animationMap as Record) - : {}; - nextOverride.generatedAnimationSetId = animationSetId; - nextOverride.generatedVisualAssetId = visualAssetId; - nextOverride.animationMap = { - ...existingAnimationMap, - ...nextAnimationMap, - }; - overrideMap[characterId] = nextOverride; - await writeJsonObjectFile(characterOverridesFilePath, overrideMap); + let overrideMap: Record = {}; + if (updateCharacterOverride) { + overrideMap = await readJsonObjectFile(characterOverridesFilePath); + const existingOverride = overrideMap[characterId]; + const nextOverride = + existingOverride && + typeof existingOverride === 'object' && + !Array.isArray(existingOverride) + ? { ...(existingOverride as Record) } + : {}; + const existingAnimationMap = + nextOverride.animationMap && + typeof nextOverride.animationMap === 'object' && + !Array.isArray(nextOverride.animationMap) + ? (nextOverride.animationMap as Record) + : {}; + nextOverride.generatedAnimationSetId = animationSetId; + nextOverride.generatedVisualAssetId = visualAssetId; + nextOverride.animationMap = { + ...existingAnimationMap, + ...nextAnimationMap, + }; + overrideMap[characterId] = nextOverride; + await writeJsonObjectFile(characterOverridesFilePath, overrideMap); + } sendJson(res, 200, { ok: true, animationSetId, overrideMap, - saveMessage: - '基础动作资源已发布到 public/generated-animations,并更新角色覆盖。', + animationMap: nextAnimationMap, + saveMessage: updateCharacterOverride + ? '基础动作资源已发布到 public/generated-animations,并更新角色覆盖。' + : '基础动作资源已保存到 public/generated-animations,可直接写回当前自定义世界角色。', }); } catch (error) { sendJson(res, 500, { diff --git a/src/App.tsx b/src/App.tsx index 6cd097f3..46d25331 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,7 +20,7 @@ export default function App() { isMapOpen, setIsMapOpen, resetGame, - handleWorldSelect: selectWorld, + handleCustomWorldSelect: selectCustomWorld, handleBackToWorldSelect: backToWorldSelect, handleCharacterSelect: selectCharacter, } = useGameFlow(); @@ -73,12 +73,11 @@ export default function App() { return () => window.clearInterval(intervalId); }, [gameState.currentScene, gameState.playerCharacter, setGameState]); - const handleWorldSelect = ( - worldType: Parameters[0], - customWorldProfile?: Parameters[1], + const handleCustomWorldSelect = ( + customWorldProfile: Parameters[0], ) => { storyFlow.resetStoryState(); - selectWorld(worldType, customWorldProfile); + selectCustomWorld(customWorldProfile); }; const handleCharacterSelect = ( @@ -152,7 +151,7 @@ export default function App() { handleContinueGame, handleStartNewGame, handleSaveAndExit, - handleWorldSelect, + handleCustomWorldSelect, handleBackToWorldSelect, handleCharacterSelect, }; diff --git a/src/components/CustomWorldEntityCatalog.tsx b/src/components/CustomWorldEntityCatalog.tsx index 07c1e1d6..1798f892 100644 --- a/src/components/CustomWorldEntityCatalog.tsx +++ b/src/components/CustomWorldEntityCatalog.tsx @@ -1,8 +1,7 @@ -import { type ReactNode, useDeferredValue, useMemo, useState } from 'react'; +import { type ReactNode, useDeferredValue, useEffect, useMemo, useState } from 'react'; import { getCustomWorldSceneRelativePositionLabel, - normalizeCustomWorldLandmarks, } from '../data/customWorldSceneGraph'; import { resolveCustomWorldCampSceneImage, @@ -25,11 +24,8 @@ interface CustomWorldEntityCatalogProps { onActiveTabChange: (tab: ResultTab) => void; onEditTarget: (target: CustomWorldEditorTarget) => void; onProfileChange: (profile: CustomWorldProfile) => void; - onRegeneratePlayableNpc?: (id: string) => void; - onRegenerateStoryNpc?: (id: string) => void; - onRegenerateLandmark?: (id: string) => void; - onRegenerateStoryExpansion?: () => void; - onRegenerateLandmarkNetwork?: () => void; + onDeleteStoryNpcs?: (ids: string[]) => void; + onDeleteLandmarks?: (ids: string[]) => void; createActionLabel?: string; onCreateAction?: () => void; } @@ -146,6 +142,57 @@ function EmptyState({ title }: { title: string }) { ); } +function CatalogCard({ + title, + description, + media, + isSelectionMode, + isSelected, + onClick, +}: { + title: string; + description: string; + media: ReactNode; + isSelectionMode: boolean; + isSelected: boolean; + onClick: () => void; +}) { + return ( + + ); +} + function matchText(text: string, query: string) { return text.toLowerCase().includes(query.toLowerCase()); } @@ -161,6 +208,8 @@ type CatalogRole = | CustomWorldProfile['playableNpcs'][number] | CustomWorldProfile['storyNpcs'][number]; +type BulkDeleteTab = 'story' | 'landmarks'; + function buildRoleSearchText(role: CatalogRole) { return [ role.name, @@ -215,15 +264,14 @@ export function CustomWorldEntityCatalog({ onActiveTabChange, onEditTarget, onProfileChange, - onRegeneratePlayableNpc, - onRegenerateStoryNpc, - onRegenerateLandmark, - onRegenerateStoryExpansion, - onRegenerateLandmarkNetwork, + onDeleteStoryNpcs, + onDeleteLandmarks, createActionLabel, onCreateAction, }: CustomWorldEntityCatalogProps) { const [searchDraft, setSearchDraft] = useState(''); + const [bulkDeleteMode, setBulkDeleteMode] = useState(null); + const [selectedBulkIds, setSelectedBulkIds] = useState([]); const deferredSearch = useDeferredValue(searchDraft.trim()); const storyNpcById = useMemo( @@ -289,16 +337,6 @@ export function CustomWorldEntityCatalog({ ), [profile.creatorIntent], ); - const lockedLandmarkNames = useMemo( - () => - new Set( - profile.creatorIntent?.keyLandmarks - .filter((entry) => entry.locked) - .map((entry) => entry.name.trim()) - .filter(Boolean) ?? [], - ), - [profile.creatorIntent], - ); const counts = { world: 1, @@ -308,6 +346,17 @@ export function CustomWorldEntityCatalog({ landmarks: profile.landmarks.length, } satisfies Record; + const bulkDeleteTab: BulkDeleteTab | null = + activeTab === 'story' || activeTab === 'landmarks' ? activeTab : null; + const isBulkDeleteMode = bulkDeleteMode === bulkDeleteTab; + + useEffect(() => { + if (bulkDeleteMode && bulkDeleteMode !== activeTab) { + setBulkDeleteMode(null); + setSelectedBulkIds([]); + } + }, [activeTab, bulkDeleteMode]); + const removePlayable = (id: string, name: string) => { if (profile.playableNpcs.length <= 1) { window.alert('至少保留一个可扮演角色,才能正常进入自定义世界。'); @@ -320,37 +369,43 @@ export function CustomWorldEntityCatalog({ }); }; - const removeStoryNpc = (id: string, name: string) => { - if (!window.confirm(`确认删除场景角色「${name}」吗?`)) return; - const nextStoryNpcs = profile.storyNpcs.filter(npc => npc.id !== id); - onProfileChange({ - ...profile, - storyNpcs: nextStoryNpcs, - landmarks: normalizeCustomWorldLandmarks({ - landmarks: profile.landmarks.map((landmark) => ({ - ...landmark, - sceneNpcIds: landmark.sceneNpcIds.filter((npcId) => npcId !== id), - })), - storyNpcs: nextStoryNpcs, - }), - }); + const startBulkDelete = (tab: BulkDeleteTab) => { + setBulkDeleteMode(tab); + setSelectedBulkIds([]); }; - const removeLandmark = (id: string, name: string) => { - if (!window.confirm(`确认删除场景「${name}」吗?`)) return; - const nextLandmarks = profile.landmarks.filter(landmark => landmark.id !== id); - onProfileChange({ - ...profile, - landmarks: normalizeCustomWorldLandmarks({ - landmarks: nextLandmarks.map((landmark) => ({ - ...landmark, - connections: landmark.connections.filter( - (connection) => connection.targetLandmarkId !== id, - ), - })), - storyNpcs: profile.storyNpcs, - }), - }); + const cancelBulkDelete = () => { + setBulkDeleteMode(null); + setSelectedBulkIds([]); + }; + + const toggleBulkSelected = (id: string) => { + setSelectedBulkIds((current) => + current.includes(id) + ? current.filter((entry) => entry !== id) + : [...current, id], + ); + }; + + const confirmBulkDelete = () => { + if (!bulkDeleteTab || selectedBulkIds.length === 0) { + return; + } + + const label = bulkDeleteTab === 'story' ? '场景角色' : '场景'; + const confirmed = window.confirm( + `确认批量删除 ${selectedBulkIds.length} 个${label}吗?`, + ); + if (!confirmed) { + return; + } + + if (bulkDeleteTab === 'story') { + onDeleteStoryNpcs?.(selectedBulkIds); + } else { + onDeleteLandmarks?.(selectedBulkIds); + } + cancelBulkDelete(); }; return ( @@ -378,13 +433,37 @@ export function CustomWorldEntityCatalog({ {activeTab !== 'world' && activeTab !== 'anchors' ? ( -
+
- {createActionLabel && onCreateAction ? ( - {createActionLabel} - ) : null} +
+ {isBulkDeleteMode ? ( + <> +
+ 已选 {selectedBulkIds.length} +
+ 取消 + + 删除选中 + + + ) : ( + <> + {createActionLabel && onCreateAction ? ( + {createActionLabel} + ) : null} + {bulkDeleteTab && ((bulkDeleteTab === 'story' && onDeleteStoryNpcs) || (bulkDeleteTab === 'landmarks' && onDeleteLandmarks)) ? ( + startBulkDelete(bulkDeleteTab)} tone="rose"> + 批量删除 + + ) : null} + + )} +
) : null}
@@ -560,14 +639,6 @@ export function CustomWorldEntityCatalog({ subtitle={role.title} actions={(
- {onRegeneratePlayableNpc && !lockedCharacterNames.has(role.name.trim()) ? ( - onRegeneratePlayableNpc(role.id)} - tone="sky" - > - AI重生成 - - ) : null} onEditTarget({ kind: 'playable', mode: 'edit', id: role.id })} tone="sky">编辑 removePlayable(role.id, role.name)} tone="rose">删除
@@ -646,111 +717,32 @@ export function CustomWorldEntityCatalog({ {activeTab === 'story' ? (
-
- 场景角色默认可组合中世纪奇幻角色形象;当角色文本明显指向怪物型 NPC 且初始好感偏敌对时,预览也会自动尝试引用怪物素材。 - {onRegenerateStoryExpansion ? ( -
- - 重生成长尾场景角色 - -
- ) : null} -
{filteredStory.length === 0 ? ( ) : ( filteredStory.map(npc => (
-
- {onRegenerateStoryNpc && !lockedCharacterNames.has(npc.name.trim()) ? ( - onRegenerateStoryNpc(npc.id)} - tone="sky" - > - AI重生成 - - ) : null} - onEditTarget({ kind: 'story', mode: 'edit', id: npc.id })} tone="sky">编辑 - removeStoryNpc(npc.id, npc.name)} tone="rose">删除 -
- )} - > -
+ description={npc.description} + isSelectionMode={isBulkDeleteMode} + isSelected={selectedBulkIds.includes(npc.id)} + onClick={() => + isBulkDeleteMode + ? toggleBulkSelected(npc.id) + : onEditTarget({ kind: 'story', mode: 'edit', id: npc.id }) + } + media={( -
- {lockedCharacterNames.has(npc.name.trim()) ? ( -
- 创作者锁定角色 -
- ) : null} -
{npc.description}
-
- 公开背景:{npc.backstoryReveal.publicSummary || '未填写'} -
-
-
头衔:{npc.title}
-
初始好感:{npc.initialAffinity}
-
性格:{npc.personality || '未填写'}
-
战斗:{npc.combatStyle || '未填写'}
-
- {npc.backstory ? ( -
背景:{npc.backstory}
- ) : null} -
动机:{npc.motivation}
-
-
好感背景章节
-
- {npc.backstoryReveal.chapters.map(chapter => ( -
- {chapter.affinityRequired} 好感 · {chapter.title}:{chapter.teaser} -
- ))} -
-
-
-
技能
-
- {npc.skills.map(skill => ( -
- {skill.name} · {skill.style}:{skill.summary} -
- ))} -
-
-
-
初始物品
-
- {npc.initialItems.map(item => ( -
- {item.name} x{item.quantity} · {item.category} · {item.rarity}:{item.description} -
- ))} -
-
-
- {npc.relationshipHooks.map(hook => ( - - {hook} - - ))} - {npc.tags.map(tag => ( - - {tag} - - ))} -
-
-
- + )} + />
)) )} @@ -759,85 +751,30 @@ export function CustomWorldEntityCatalog({ {activeTab === 'landmarks' ? (
-
- 场景图会同步用于结果页和正式世界中的背景展示;这里还能看到每个场景承载的 NPC 和连接关系。 - {onRegenerateLandmarkNetwork ? ( -
- - 重生成场景网络 - -
- ) : null} -
{filteredLandmarks.length === 0 ? ( ) : ( filteredLandmarks.map(landmark => (
-
- {onRegenerateLandmark && !lockedLandmarkNames.has(landmark.name.trim()) ? ( - onRegenerateLandmark(landmark.id)} - tone="sky" - > - AI重生成 - - ) : null} - onEditTarget({ kind: 'landmark', mode: 'edit', id: landmark.id })} tone="sky">编辑 - removeLandmark(landmark.id, landmark.name)} tone="rose">删除 -
- )} - > -
- {lockedLandmarkNames.has(landmark.name.trim()) ? ( -
- 创作者锁定场景 -
- ) : null} + description={landmark.description} + isSelectionMode={isBulkDeleteMode} + isSelected={selectedBulkIds.includes(landmark.id)} + onClick={() => + isBulkDeleteMode + ? toggleBulkSelected(landmark.id) + : onEditTarget({ kind: 'landmark', mode: 'edit', id: landmark.id }) + } + media={( -
{landmark.description}
-
- 危险度:{landmark.dangerLevel || '未填写'} -
-
-
场景内 NPC
-
- {landmark.sceneNpcIds.length > 0 ? ( - landmark.sceneNpcIds.map((npcId) => ( - - {storyNpcById.get(npcId)?.name ?? '未匹配角色'} - - )) - ) : ( - 尚未分配场景角色 - )} -
-
-
-
连接关系
-
- {landmark.connections.length > 0 ? ( - landmark.connections.map((connection) => ( -
- {getCustomWorldSceneRelativePositionLabel(connection.relativePosition)} · {landmarkById.get(connection.targetLandmarkId)?.name ?? '未匹配场景'} - {connection.summary ? `:${connection.summary}` : ''} -
- )) - ) : ( -
尚未配置连接关系
- )} -
-
-
- + )} + />
)) )} diff --git a/src/components/CustomWorldEntityEditorModal.tsx b/src/components/CustomWorldEntityEditorModal.tsx index 5b17831f..0ccea4eb 100644 --- a/src/components/CustomWorldEntityEditorModal.tsx +++ b/src/components/CustomWorldEntityEditorModal.tsx @@ -1,8 +1,8 @@ -import { Children, type ReactNode, useEffect, useMemo, useState } from 'react'; +import type { ChangeEvent } from 'react'; +import { Children, type ReactNode, useEffect, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; -import { - AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS, -} from '../data/affinityLevels'; +import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS } from '../data/affinityLevels'; import { buildCustomWorldPlayableCharacters, PRESET_CHARACTERS, @@ -21,10 +21,6 @@ import { type CustomWorldSceneImageResult, generateCustomWorldSceneImage, } from '../services/ai'; -import { - buildCustomWorldSceneImagePrompt, - DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT, -} from '../services/customWorld'; import { resolveCustomWorldCampScene } from '../services/customWorldCamp'; import { AnimationState, @@ -42,6 +38,7 @@ import { CustomWorldNpcPortrait, CustomWorldNpcVisualEditor, } from './CustomWorldNpcVisualEditor'; +import { CustomWorldRoleAssetStudioModal } from './CustomWorldRoleAssetStudioModal'; import { PixelIcon } from './PixelIcon'; export type CustomWorldEditorTarget = @@ -170,6 +167,15 @@ function useDraft(value: T) { return [draft, setDraft] as const; } +function readImageFileAsDataUrl(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result ?? '')); + reader.onerror = () => reject(reader.error ?? new Error('读取图片失败。')); + reader.readAsDataURL(file); + }); +} + function ModalShell({ title, subtitle, @@ -179,6 +185,7 @@ function ModalShell({ overlayClassName = 'z-[98]', bodyClassName = '', disableClose = false, + usePixelFont = false, }: { title: string; subtitle?: string; @@ -188,6 +195,7 @@ function ModalShell({ overlayClassName?: string; bodyClassName?: string; disableClose?: boolean; + usePixelFont?: boolean; }) { return (
event.stopPropagation()} > @@ -229,6 +237,83 @@ function ModalShell({ ); } +function _PortalModalShell(props: { + title: string; + subtitle?: string; + onClose: () => void; + children: ReactNode; + panelClassName?: string; + overlayClassName?: string; + bodyClassName?: string; + disableClose?: boolean; + usePixelFont?: boolean; +}) { + if (typeof document === 'undefined') { + return null; + } + + return createPortal(, document.body); +} + +function CompactDialogShell({ + title, + onClose, + children, + overlayClassName = 'z-[140]', + disableClose = false, + usePixelFont = false, +}: { + title: string; + onClose: () => void; + children: ReactNode; + overlayClassName?: string; + disableClose?: boolean; + usePixelFont?: boolean; +}) { + return ( +
+
event.stopPropagation()} + > +
+
+ {title} +
+ +
+
{children}
+
+
+ ); +} + +function PortalCompactDialogShell(props: { + title: string; + onClose: () => void; + children: ReactNode; + overlayClassName?: string; + disableClose?: boolean; + usePixelFont?: boolean; +}) { + if (typeof document === 'undefined') { + return null; + } + + return createPortal(, document.body); +} + function Field({ label, children }: { label: string; children: ReactNode }) { const hasVisibleChildren = Children.toArray(children).some( (child) => !(typeof child === 'string' && child.trim().length === 0), @@ -401,12 +486,14 @@ function ActionButton({ }: { label: string; onClick: () => void; - tone?: 'default' | 'sky'; + tone?: 'default' | 'sky' | 'rose'; disabled?: boolean; }) { const toneClassName = tone === 'sky' ? 'border-sky-300/22 bg-sky-500/12 text-sky-50 hover:border-sky-200/40 hover:text-white' + : tone === 'rose' + ? 'border-rose-300/22 bg-rose-500/12 text-rose-50 hover:border-rose-200/40 hover:text-white' : 'border-white/12 bg-black/20 text-zinc-200 hover:border-white/22 hover:text-white'; return ( @@ -556,36 +643,7 @@ function ScenePresetPickerModal({ ); } -function AiComingSoonModal({ - title, - subtitle, - onClose, -}: { - title: string; - subtitle: string; - onClose: () => void; -}) { - return ( - -
-
-
- 功能开发中{'\n'}敬请期待 -
-
-
- -
-
-
- ); -} - -const SCENE_IMAGE_SIZE_OPTIONS = [ - { value: '1280*720', label: '横版 16:9(推荐)' }, - { value: '1280*1280', label: '方图 1:1' }, - { value: '960*1280', label: '竖版 3:4' }, -] as const; +const FIXED_SCENE_IMAGE_SIZE = '1280*720'; function SceneImageGenerationModal({ profile, @@ -598,27 +656,17 @@ function SceneImageGenerationModal({ onApply: (result: CustomWorldSceneImageResult) => void; onClose: () => void; }) { - const defaultPrompt = useMemo( - () => buildCustomWorldSceneImagePrompt(profile, landmark), - [profile, landmark], - ); - const [prompt, setPrompt] = useDraft(defaultPrompt); - const [negativePrompt, setNegativePrompt] = useDraft( - DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT, - ); - const [size, setSize] = useDraft( - SCENE_IMAGE_SIZE_OPTIONS[0]?.value ?? '1280*720', + const [userPrompt, setUserPrompt] = useDraft( + landmark.name.trim() || landmark.description.trim(), ); + const [referenceImageSrc, setReferenceImageSrc] = useState(''); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); const [latestResult, setLatestResult] = useState(null); + const [isExitConfirmOpen, setIsExitConfirmOpen] = useState(false); - const previewImageSrc = useMemo(() => { - if (latestResult?.imageSrc) { - return latestResult.imageSrc; - } - + const originalImageSrc = useMemo(() => { const landmarkIndex = profile.landmarks.findIndex( (entry) => entry.id === landmark.id, ); @@ -632,11 +680,46 @@ function SceneImageGenerationModal({ .map((entry) => entry.imageSrc) .filter((imageSrc): imageSrc is string => Boolean(imageSrc)), ); - }, [landmark, latestResult, profile]); + }, [landmark, profile]); + + const previewImageSrc = latestResult?.imageSrc || originalImageSrc; + + const handleReferenceImageChange = async ( + event: ChangeEvent, + ) => { + const file = event.target.files?.[0]; + event.currentTarget.value = ''; + if (!file) { + return; + } + + try { + const dataUrl = await readImageFileAsDataUrl(file); + setReferenceImageSrc(dataUrl); + setError(null); + } catch (uploadError) { + setError( + uploadError instanceof Error + ? uploadError.message + : '参考图读取失败,请重试。', + ); + } + }; + + const handleRequestClose = () => { + if (isGenerating) { + return; + } + if (latestResult) { + setIsExitConfirmOpen(true); + return; + } + onClose(); + }; const handleGenerate = async () => { - if (!prompt.trim()) { - setError('请先填写场景提示词。'); + if (!userPrompt.trim()) { + setError('请先描述想要生成的画面内容。'); return; } @@ -647,12 +730,11 @@ function SceneImageGenerationModal({ const result = await generateCustomWorldSceneImage({ profile, landmark, - prompt, - negativePrompt, - size, + userPrompt, + size: FIXED_SCENE_IMAGE_SIZE, + ...(referenceImageSrc ? { referenceImageSrc } : {}), }); setLatestResult(result); - onApply(result); } catch (generationError) { setError( generationError instanceof Error @@ -664,136 +746,195 @@ function SceneImageGenerationModal({ } }; + const handleSave = () => { + if (!latestResult || isGenerating) { + return; + } + onApply(latestResult); + onClose(); + }; + return ( - -
-
-
- 这里会把世界设定、场景描述和危险度组织成默认提示词。生成成功后,上层场景预览会立即替换,但仍需要点击“保存修改”才会正式写入当前世界档案。 + <> + +
+
+ +