1
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import type { CustomWorldGenerationProgress } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { CreateVisualNovelSessionRequest } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type { CustomWorldStructuredAnchorEntry } from '../../services/customWorldAgentGenerationProgress';
|
||||
|
||||
export type VisualNovelEntryFormPayload = Omit<
|
||||
CreateVisualNovelSessionRequest,
|
||||
'seedText' | 'sourceMode' | 'sourceAssetIds'
|
||||
> & {
|
||||
sourceMode: 'idea';
|
||||
seedText: string;
|
||||
sourceAssetIds: string[];
|
||||
ideaText: string;
|
||||
visualStyleId: VisualNovelStyleOptionId;
|
||||
visualStyleLabel: string;
|
||||
visualStylePrompt: string;
|
||||
};
|
||||
|
||||
type VisualNovelStyleOptionId =
|
||||
| 'cinematic-anime'
|
||||
| 'watercolor'
|
||||
| 'pixel-noir'
|
||||
| 'ink-fantasy'
|
||||
| 'soft-pastel'
|
||||
| 'dark-gothic';
|
||||
|
||||
export function buildVisualNovelEntryGenerationAnchorEntries(
|
||||
payload: VisualNovelEntryFormPayload | null | undefined,
|
||||
): CustomWorldStructuredAnchorEntry[] {
|
||||
if (!payload) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'visual-novel-idea',
|
||||
label: '一句话',
|
||||
value: payload.ideaText,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-style',
|
||||
label: '视觉画风',
|
||||
value: payload.visualStyleLabel,
|
||||
},
|
||||
].filter((entry) => entry.value.trim());
|
||||
}
|
||||
|
||||
export function buildVisualNovelEntryGenerationProgress(
|
||||
startedAtMs: number | null,
|
||||
phase: 'generating' | 'ready' | 'failed',
|
||||
nowMs = Date.now(),
|
||||
): CustomWorldGenerationProgress {
|
||||
const elapsedMs = startedAtMs ? Math.max(0, nowMs - startedAtMs) : 0;
|
||||
const timeline: [
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
weight: number;
|
||||
durationMs: number;
|
||||
},
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
weight: number;
|
||||
durationMs: number;
|
||||
},
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
weight: number;
|
||||
durationMs: number;
|
||||
},
|
||||
] = [
|
||||
{
|
||||
id: 'visual-novel-session',
|
||||
label: '创建创作会话',
|
||||
detail: '写入一句话与视觉画风,准备生成视觉小说底稿。',
|
||||
weight: 24,
|
||||
durationMs: 5_000,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-draft',
|
||||
label: '生成故事底稿',
|
||||
detail: '整理世界观、角色、场景和剧情阶段。',
|
||||
weight: 56,
|
||||
durationMs: 22_000,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-ready',
|
||||
label: '准备草稿页',
|
||||
detail: '校验可编辑字段并进入草稿页。',
|
||||
weight: 20,
|
||||
durationMs: 4_000,
|
||||
},
|
||||
];
|
||||
let elapsedBeforeStep = 0;
|
||||
const activeStepIndex =
|
||||
phase === 'ready'
|
||||
? timeline.length - 1
|
||||
: timeline.findIndex((step) => {
|
||||
const elapsedInStep = elapsedMs - elapsedBeforeStep;
|
||||
const isActive = elapsedInStep < step.durationMs;
|
||||
if (!isActive) {
|
||||
elapsedBeforeStep += step.durationMs;
|
||||
}
|
||||
return isActive;
|
||||
});
|
||||
const normalizedActiveStepIndex =
|
||||
activeStepIndex >= 0 ? activeStepIndex : timeline.length - 1;
|
||||
const activeStep = timeline[normalizedActiveStepIndex] ?? timeline[0];
|
||||
const activeElapsed =
|
||||
elapsedMs -
|
||||
timeline
|
||||
.slice(0, normalizedActiveStepIndex)
|
||||
.reduce((sum, step) => sum + step.durationMs, 0);
|
||||
const activeRatio =
|
||||
phase === 'ready'
|
||||
? 1
|
||||
: phase === 'failed'
|
||||
? 0
|
||||
: Math.max(0, Math.min(1, activeElapsed / activeStep.durationMs));
|
||||
const completedWeight = timeline
|
||||
.slice(0, phase === 'ready' ? timeline.length : normalizedActiveStepIndex)
|
||||
.reduce((sum, step) => sum + step.weight, 0);
|
||||
const overallProgress =
|
||||
phase === 'ready'
|
||||
? 100
|
||||
: phase === 'failed'
|
||||
? Math.max(1, completedWeight)
|
||||
: Math.min(98, completedWeight + activeStep.weight * activeRatio);
|
||||
|
||||
return {
|
||||
phaseId: phase,
|
||||
phaseLabel:
|
||||
phase === 'ready'
|
||||
? '生成完成'
|
||||
: phase === 'failed'
|
||||
? '生成失败'
|
||||
: activeStep.label,
|
||||
phaseDetail:
|
||||
phase === 'ready'
|
||||
? '视觉小说草稿已准备完成。'
|
||||
: phase === 'failed'
|
||||
? '草稿生成失败,请返回入口页调整后重试。'
|
||||
: activeStep.detail,
|
||||
batchLabel: activeStep.label,
|
||||
overallProgress: Math.max(0, Math.min(100, Math.round(overallProgress))),
|
||||
completedWeight: Math.max(0, Math.min(100, Math.round(overallProgress))),
|
||||
totalWeight: 100,
|
||||
elapsedMs,
|
||||
estimatedRemainingMs:
|
||||
phase === 'ready' ? 0 : Math.max(0, 31_000 - elapsedMs),
|
||||
activeStepIndex: normalizedActiveStepIndex,
|
||||
steps: timeline.map((step, index) => {
|
||||
const isCompleted =
|
||||
phase === 'ready' || index < normalizedActiveStepIndex;
|
||||
const isActive =
|
||||
phase !== 'failed' &&
|
||||
!isCompleted &&
|
||||
index === normalizedActiveStepIndex;
|
||||
const status: 'completed' | 'active' | 'pending' = isCompleted
|
||||
? 'completed'
|
||||
: isActive
|
||||
? 'active'
|
||||
: 'pending';
|
||||
return {
|
||||
id: step.id,
|
||||
label: step.label,
|
||||
detail: step.detail,
|
||||
completed: isCompleted ? 1 : isActive ? activeRatio : 0,
|
||||
total: 1,
|
||||
status,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user