Simplify custom world result editing controls
This commit is contained in:
@@ -180,9 +180,11 @@ export interface CustomWorldSceneImageRequest {
|
||||
CustomWorldProfile['landmarks'][number],
|
||||
'id' | 'name' | 'description' | 'dangerLevel'
|
||||
>;
|
||||
userPrompt?: string;
|
||||
prompt?: string;
|
||||
negativePrompt?: string;
|
||||
size?: string;
|
||||
referenceImageSrc?: string;
|
||||
}
|
||||
|
||||
export interface CustomWorldSceneImageResult {
|
||||
@@ -312,7 +314,9 @@ const CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS = [
|
||||
total: MIN_CUSTOM_WORLD_STORY_NPC_COUNT,
|
||||
weight: Math.max(
|
||||
1,
|
||||
Math.ceil(MIN_CUSTOM_WORLD_STORY_NPC_COUNT / CUSTOM_WORLD_STORY_BATCH_SIZE),
|
||||
Math.ceil(
|
||||
MIN_CUSTOM_WORLD_STORY_NPC_COUNT / CUSTOM_WORLD_STORY_BATCH_SIZE,
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -334,7 +338,9 @@ const CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS = [
|
||||
total: MIN_CUSTOM_WORLD_STORY_NPC_COUNT,
|
||||
weight: Math.max(
|
||||
1,
|
||||
Math.ceil(MIN_CUSTOM_WORLD_STORY_NPC_COUNT / CUSTOM_WORLD_STORY_BATCH_SIZE),
|
||||
Math.ceil(
|
||||
MIN_CUSTOM_WORLD_STORY_NPC_COUNT / CUSTOM_WORLD_STORY_BATCH_SIZE,
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -451,9 +457,8 @@ function resolveCustomWorldGenerationInput(
|
||||
settingText: normalizedSettingText,
|
||||
generationSeedText: generationSeedText.trim(),
|
||||
creatorIntent,
|
||||
generationMode: input.generationMode === 'fast'
|
||||
? ('fast' as const)
|
||||
: ('full' as const),
|
||||
generationMode:
|
||||
input.generationMode === 'fast' ? ('fast' as const) : ('full' as const),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -621,9 +626,7 @@ function getCustomWorldGenerationStageIdForRoleExpansion(
|
||||
stage: CustomWorldGenerationRoleBatchStage,
|
||||
): CustomWorldGenerationStageId {
|
||||
if (roleType === 'playable') {
|
||||
return stage === 'narrative'
|
||||
? 'playable-narrative'
|
||||
: 'playable-dossier';
|
||||
return stage === 'narrative' ? 'playable-narrative' : 'playable-dossier';
|
||||
}
|
||||
|
||||
return stage === 'narrative' ? 'story-narrative' : 'story-dossier';
|
||||
@@ -704,8 +707,7 @@ function createCustomWorldGenerationReporter(
|
||||
|
||||
const completedWeight = CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS.reduce(
|
||||
(sum, item) =>
|
||||
sum +
|
||||
(completedByStage[item.id] / item.total || 0) * item.weight,
|
||||
sum + (completedByStage[item.id] / item.total || 0) * item.weight,
|
||||
0,
|
||||
);
|
||||
const progressFraction =
|
||||
@@ -715,10 +717,7 @@ function createCustomWorldGenerationReporter(
|
||||
const elapsedMs = Math.max(0, performance.now() - startedAt);
|
||||
const estimatedRemainingMs =
|
||||
progressFraction > 0 && progressFraction < 1
|
||||
? Math.max(
|
||||
0,
|
||||
Math.round(elapsedMs / progressFraction - elapsedMs),
|
||||
)
|
||||
? Math.max(0, Math.round(elapsedMs / progressFraction - elapsedMs))
|
||||
: progressFraction >= 1
|
||||
? 0
|
||||
: null;
|
||||
@@ -1023,10 +1022,11 @@ async function expandCustomWorldRoleEntries<
|
||||
1,
|
||||
Math.ceil(roleBatchSource.length / batchSize),
|
||||
);
|
||||
const processedByStage: Record<CustomWorldGenerationRoleBatchStage, number> = {
|
||||
narrative: 0,
|
||||
dossier: 0,
|
||||
};
|
||||
const processedByStage: Record<CustomWorldGenerationRoleBatchStage, number> =
|
||||
{
|
||||
narrative: 0,
|
||||
dossier: 0,
|
||||
};
|
||||
|
||||
const requestBatchStage = async (
|
||||
roleBatch: typeof roleBatchSource,
|
||||
@@ -1070,7 +1070,7 @@ async function expandCustomWorldRoleEntries<
|
||||
? (stageRaw as Record<string, unknown>)[
|
||||
roleType === 'playable' ? 'playableNpcs' : 'storyNpcs'
|
||||
]
|
||||
: []
|
||||
: [],
|
||||
),
|
||||
);
|
||||
processedByStage[stage] = Math.min(
|
||||
@@ -1112,7 +1112,8 @@ async function generateCustomWorldThemePackWithAi(params: {
|
||||
repairPromptBuilder: (responseText) =>
|
||||
buildCustomWorldThemePackJsonRepairPrompt({ responseText }),
|
||||
repairDebugLabel: 'custom-world-theme-pack-json-repair',
|
||||
emptyResponseMessage: '自定义世界 ThemePack 生成失败:模型没有返回有效内容。',
|
||||
emptyResponseMessage:
|
||||
'自定义世界 ThemePack 生成失败:模型没有返回有效内容。',
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -1145,7 +1146,8 @@ async function generateCustomWorldStoryGraphWithAi(params: {
|
||||
repairPromptBuilder: (responseText) =>
|
||||
buildCustomWorldStoryGraphJsonRepairPrompt({ responseText }),
|
||||
repairDebugLabel: 'custom-world-story-graph-json-repair',
|
||||
emptyResponseMessage: '自定义世界 StoryGraph 生成失败:模型没有返回有效内容。',
|
||||
emptyResponseMessage:
|
||||
'自定义世界 StoryGraph 生成失败:模型没有返回有效内容。',
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -1177,11 +1179,17 @@ async function expandCustomWorldActorNarrativeProfiles<
|
||||
const roleBatchSource = baseEntries;
|
||||
const roleLabel = roleType === 'playable' ? '可扮演角色' : '场景角色';
|
||||
const stageId = getCustomWorldGenerationStageIdForActorProfile(roleType);
|
||||
const plannedBatchCount = Math.max(1, Math.ceil(roleBatchSource.length / batchSize));
|
||||
const plannedBatchCount = Math.max(
|
||||
1,
|
||||
Math.ceil(roleBatchSource.length / batchSize),
|
||||
);
|
||||
let mergedEntries = baseEntries.map((entry) => ({ ...entry })) as T[];
|
||||
let processedCount = 0;
|
||||
|
||||
for (const [batchIndex, roleBatch] of chunkArray(roleBatchSource, batchSize).entries()) {
|
||||
for (const [batchIndex, roleBatch] of chunkArray(
|
||||
roleBatchSource,
|
||||
batchSize,
|
||||
).entries()) {
|
||||
throwIfCustomWorldGenerationAborted(signal);
|
||||
reporter.update(stageId, processedCount, {
|
||||
phaseDetail: `正在补充${roleLabel}叙事档案,已完成 ${processedCount}/${roleBatchSource.length}。`,
|
||||
@@ -1217,7 +1225,10 @@ async function expandCustomWorldActorNarrativeProfiles<
|
||||
: [],
|
||||
),
|
||||
);
|
||||
processedCount = Math.min(roleBatchSource.length, processedCount + roleBatch.length);
|
||||
processedCount = Math.min(
|
||||
roleBatchSource.length,
|
||||
processedCount + roleBatch.length,
|
||||
);
|
||||
reporter.update(stageId, processedCount, {
|
||||
phaseDetail: `正在补充${roleLabel}叙事档案,已完成 ${processedCount}/${roleBatchSource.length}。`,
|
||||
batchLabel: `第 ${batchIndex + 1} / ${plannedBatchCount} 批`,
|
||||
@@ -1268,7 +1279,10 @@ async function parseCustomWorldStageResponseJson(params: {
|
||||
{
|
||||
timeoutMs: Math.max(
|
||||
30000,
|
||||
Math.min(90000, Math.round(CLIENT_CUSTOM_WORLD_REQUEST_TIMEOUT_MS / 2)),
|
||||
Math.min(
|
||||
90000,
|
||||
Math.round(CLIENT_CUSTOM_WORLD_REQUEST_TIMEOUT_MS / 2),
|
||||
),
|
||||
),
|
||||
debugLabel: repairDebugLabel,
|
||||
signal,
|
||||
@@ -1379,8 +1393,9 @@ function normalizeEncounterResult(
|
||||
const kind = typeof item.kind === 'string' ? item.kind.trim() : '';
|
||||
|
||||
if (kind === 'monster') {
|
||||
const fallbackHostileNpc =
|
||||
scene?.npcs.find((npc: SceneNpc) => isHostileSceneNpc(npc));
|
||||
const fallbackHostileNpc = scene?.npcs.find((npc: SceneNpc) =>
|
||||
isHostileSceneNpc(npc),
|
||||
);
|
||||
|
||||
return fallbackHostileNpc
|
||||
? { kind: 'npc', npcId: fallbackHostileNpc.id }
|
||||
@@ -1429,7 +1444,7 @@ function buildEncounterDrivenResolution(
|
||||
);
|
||||
if (sceneNpc?.monsterPresetId && isHostileSceneNpc(sceneNpc)) {
|
||||
return {
|
||||
monsters: createSceneHostileNpcsFromEncounters(
|
||||
monsters: createSceneHostileNpcsFromEncounters(
|
||||
worldType,
|
||||
[buildEncounterFromSceneNpc(sceneNpc, context.playerX)],
|
||||
context.playerX,
|
||||
@@ -1751,7 +1766,11 @@ async function repairStoryNarrativeLanguage(
|
||||
).inBattle;
|
||||
|
||||
if (!needsStoryLanguageRepair(response)) {
|
||||
return finalizeStoryNarrativeLanguage(response, context, responseBattleState);
|
||||
return finalizeStoryNarrativeLanguage(
|
||||
response,
|
||||
context,
|
||||
responseBattleState,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1783,7 +1802,11 @@ async function repairStoryNarrativeLanguage(
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Failed to repair mixed-language story response:', error);
|
||||
return finalizeStoryNarrativeLanguage(response, context, responseBattleState);
|
||||
return finalizeStoryNarrativeLanguage(
|
||||
response,
|
||||
context,
|
||||
responseBattleState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1913,12 +1936,17 @@ async function requestCompletion(
|
||||
export async function generateCustomWorldSceneImage({
|
||||
profile,
|
||||
landmark,
|
||||
userPrompt,
|
||||
prompt,
|
||||
negativePrompt,
|
||||
size = '1280*720',
|
||||
referenceImageSrc,
|
||||
}: CustomWorldSceneImageRequest): Promise<CustomWorldSceneImageResult> {
|
||||
const resolvedPrompt =
|
||||
prompt?.trim() || buildCustomWorldSceneImagePrompt(profile, landmark);
|
||||
prompt?.trim() ||
|
||||
buildCustomWorldSceneImagePrompt(profile, landmark, userPrompt, {
|
||||
hasReferenceImage: Boolean(referenceImageSrc?.trim()),
|
||||
});
|
||||
const resolvedNegativePrompt =
|
||||
negativePrompt?.trim() || DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT;
|
||||
const controller = new AbortController();
|
||||
@@ -1939,6 +1967,9 @@ export async function generateCustomWorldSceneImage({
|
||||
prompt: resolvedPrompt,
|
||||
negativePrompt: resolvedNegativePrompt,
|
||||
size,
|
||||
...(referenceImageSrc?.trim()
|
||||
? { referenceImageSrc: referenceImageSrc.trim() }
|
||||
: {}),
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
@@ -2045,15 +2076,14 @@ export async function generateCustomWorldProfile(
|
||||
reporter.begin('playable-outline', {
|
||||
phaseDetail: '正在生成可扮演角色骨架。',
|
||||
});
|
||||
const playableNpcs =
|
||||
(await generateCustomWorldRoleOutlineEntries({
|
||||
framework: frameworkBase,
|
||||
roleType: 'playable',
|
||||
totalCount: generationTargets.playableCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_PLAYABLE_OUTLINE_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
})) as CustomWorldGenerationFramework['playableNpcs'];
|
||||
const playableNpcs = (await generateCustomWorldRoleOutlineEntries({
|
||||
framework: frameworkBase,
|
||||
roleType: 'playable',
|
||||
totalCount: generationTargets.playableCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_PLAYABLE_OUTLINE_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
})) as CustomWorldGenerationFramework['playableNpcs'];
|
||||
reporter.complete('playable-outline', {
|
||||
phaseDetail: `可扮演角色骨架已完成,共 ${playableNpcs.length} 名。`,
|
||||
});
|
||||
@@ -2065,15 +2095,14 @@ export async function generateCustomWorldProfile(
|
||||
reporter.begin('story-outline', {
|
||||
phaseDetail: '正在生成场景角色骨架。',
|
||||
});
|
||||
const storyNpcs =
|
||||
(await generateCustomWorldRoleOutlineEntries({
|
||||
framework: frameworkWithPlayable,
|
||||
roleType: 'story',
|
||||
totalCount: generationTargets.storyCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_STORY_OUTLINE_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
})) as CustomWorldGenerationFramework['storyNpcs'];
|
||||
const storyNpcs = (await generateCustomWorldRoleOutlineEntries({
|
||||
framework: frameworkWithPlayable,
|
||||
roleType: 'story',
|
||||
totalCount: generationTargets.storyCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_STORY_OUTLINE_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
})) as CustomWorldGenerationFramework['storyNpcs'];
|
||||
reporter.complete('story-outline', {
|
||||
phaseDetail: `场景角色骨架已完成,共 ${storyNpcs.length} 名。`,
|
||||
});
|
||||
@@ -2085,14 +2114,13 @@ export async function generateCustomWorldProfile(
|
||||
reporter.begin('landmark-seed', {
|
||||
phaseDetail: '正在生成场景骨架。',
|
||||
});
|
||||
const landmarkSeeds =
|
||||
(await generateCustomWorldLandmarkSeedEntries({
|
||||
framework: frameworkWithStory,
|
||||
totalCount: generationTargets.landmarkCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_LANDMARK_SEED_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
})) as CustomWorldGenerationFramework['landmarks'];
|
||||
const landmarkSeeds = (await generateCustomWorldLandmarkSeedEntries({
|
||||
framework: frameworkWithStory,
|
||||
totalCount: generationTargets.landmarkCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_LANDMARK_SEED_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
})) as CustomWorldGenerationFramework['landmarks'];
|
||||
reporter.complete('landmark-seed', {
|
||||
phaseDetail: `场景骨架已完成,共 ${landmarkSeeds.length} 个地标。`,
|
||||
});
|
||||
@@ -2104,15 +2132,14 @@ export async function generateCustomWorldProfile(
|
||||
reporter.begin('landmark-network', {
|
||||
phaseDetail: '正在建立场景连接与场景角色分布。',
|
||||
});
|
||||
const landmarks =
|
||||
(await expandCustomWorldLandmarkNetworkEntries({
|
||||
framework: frameworkWithLandmarkSeeds,
|
||||
storyNpcs,
|
||||
baseEntries: landmarkSeeds,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_LANDMARK_NETWORK_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
})) as CustomWorldGenerationFramework['landmarks'];
|
||||
const landmarks = (await expandCustomWorldLandmarkNetworkEntries({
|
||||
framework: frameworkWithLandmarkSeeds,
|
||||
storyNpcs,
|
||||
baseEntries: landmarkSeeds,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_LANDMARK_NETWORK_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
})) as CustomWorldGenerationFramework['landmarks'];
|
||||
reporter.complete('landmark-network', {
|
||||
phaseDetail: `场景连接已完成,共整理 ${landmarks.length} 个地标网络。`,
|
||||
});
|
||||
@@ -2177,32 +2204,34 @@ export async function generateCustomWorldProfile(
|
||||
reporter.begin('playable-profile', {
|
||||
phaseDetail: '正在补充可扮演角色的叙事档案。',
|
||||
});
|
||||
const playableNpcsWithNarrativeProfile = await expandCustomWorldActorNarrativeProfiles({
|
||||
framework,
|
||||
roleType: 'playable',
|
||||
baseEntries: profileSeed.playableNpcs.map((npc) => ({ ...npc })),
|
||||
batchSize: CUSTOM_WORLD_PLAYABLE_BATCH_SIZE,
|
||||
themePack,
|
||||
storyGraph,
|
||||
reporter,
|
||||
signal,
|
||||
});
|
||||
const playableNpcsWithNarrativeProfile =
|
||||
await expandCustomWorldActorNarrativeProfiles({
|
||||
framework,
|
||||
roleType: 'playable',
|
||||
baseEntries: profileSeed.playableNpcs.map((npc) => ({ ...npc })),
|
||||
batchSize: CUSTOM_WORLD_PLAYABLE_BATCH_SIZE,
|
||||
themePack,
|
||||
storyGraph,
|
||||
reporter,
|
||||
signal,
|
||||
});
|
||||
reporter.complete('playable-profile', {
|
||||
phaseDetail: `可扮演角色叙事档案已完成,共 ${playableNpcsWithNarrativeProfile.length} 名。`,
|
||||
});
|
||||
reporter.begin('story-profile', {
|
||||
phaseDetail: '正在补充场景角色的叙事档案。',
|
||||
});
|
||||
const storyNpcsWithNarrativeProfile = await expandCustomWorldActorNarrativeProfiles({
|
||||
framework,
|
||||
roleType: 'story',
|
||||
baseEntries: profileSeed.storyNpcs.map((npc) => ({ ...npc })),
|
||||
batchSize: CUSTOM_WORLD_STORY_BATCH_SIZE,
|
||||
themePack,
|
||||
storyGraph,
|
||||
reporter,
|
||||
signal,
|
||||
});
|
||||
const storyNpcsWithNarrativeProfile =
|
||||
await expandCustomWorldActorNarrativeProfiles({
|
||||
framework,
|
||||
roleType: 'story',
|
||||
baseEntries: profileSeed.storyNpcs.map((npc) => ({ ...npc })),
|
||||
batchSize: CUSTOM_WORLD_STORY_BATCH_SIZE,
|
||||
themePack,
|
||||
storyGraph,
|
||||
reporter,
|
||||
signal,
|
||||
});
|
||||
reporter.complete('story-profile', {
|
||||
phaseDetail: `场景角色叙事档案已完成,共 ${storyNpcsWithNarrativeProfile.length} 名。`,
|
||||
});
|
||||
@@ -2236,9 +2265,11 @@ export async function generateCustomWorldProfile(
|
||||
settingText: normalizedSettingText || profile.settingText,
|
||||
creatorIntent,
|
||||
anchorPack:
|
||||
profile.anchorPack ?? buildCustomWorldAnchorPackFromIntent(creatorIntent),
|
||||
profile.anchorPack ??
|
||||
buildCustomWorldAnchorPackFromIntent(creatorIntent),
|
||||
lockState:
|
||||
profile.lockState ?? deriveCustomWorldLockStateFromIntent(creatorIntent),
|
||||
profile.lockState ??
|
||||
deriveCustomWorldLockStateFromIntent(creatorIntent),
|
||||
generationMode,
|
||||
generationStatus: generationTargets.generationStatus,
|
||||
items: [],
|
||||
|
||||
Reference in New Issue
Block a user