feat: tighten visual novel one-line generation flow
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
buildVisualNovelEntryGenerationAnchorEntries,
|
||||
buildVisualNovelEntryGenerationProgress,
|
||||
type VisualNovelEntryFormPayload,
|
||||
} from './visualNovelEntryGeneration';
|
||||
|
||||
function createVisualNovelPayload(
|
||||
overrides: Partial<VisualNovelEntryFormPayload> = {},
|
||||
): VisualNovelEntryFormPayload {
|
||||
return {
|
||||
sourceMode: 'idea',
|
||||
seedText:
|
||||
'雨夜旧图书馆里,失忆高中生发现一本会回应心声的日记。\n视觉画风:映画动画\n画风要求:电影感动画视觉小说画风。',
|
||||
sourceAssetIds: [],
|
||||
ideaText: '雨夜旧图书馆里,失忆高中生发现一本会回应心声的日记。',
|
||||
visualStyleId: 'cinematic-anime',
|
||||
visualStyleLabel: '映画动画',
|
||||
visualStylePrompt: '电影感动画视觉小说画风。',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('visualNovelEntryGeneration', () => {
|
||||
test('one-line visual novel generation exposes reference-flow stages', () => {
|
||||
const progress = buildVisualNovelEntryGenerationProgress(
|
||||
1_000,
|
||||
'generating',
|
||||
1_500,
|
||||
);
|
||||
|
||||
expect(progress.steps.map((step) => step.id)).toEqual([
|
||||
'visual-novel-intent',
|
||||
'visual-novel-world',
|
||||
'visual-novel-cast-scenes',
|
||||
'visual-novel-opening',
|
||||
'visual-novel-ready',
|
||||
]);
|
||||
expect(progress.phaseLabel).toBe('理解一句话创意');
|
||||
expect(progress.steps[0]?.detail).toBe(
|
||||
'提取核心题材、视觉画风、玩家身份和互动叙事目标。',
|
||||
);
|
||||
expect(progress.estimatedRemainingMs).toBe(44_500);
|
||||
expect(progress.overallProgress).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('one-line visual novel generation advances to opening choices before ready', () => {
|
||||
const progress = buildVisualNovelEntryGenerationProgress(
|
||||
1_000,
|
||||
'generating',
|
||||
35_000,
|
||||
);
|
||||
|
||||
expect(progress.phaseId).toBe('visual-novel-opening');
|
||||
expect(progress.phaseLabel).toBe('生成开场与选择');
|
||||
expect(progress.steps[2]?.status).toBe('completed');
|
||||
expect(progress.steps[3]?.status).toBe('active');
|
||||
expect(progress.overallProgress).toBeLessThan(99);
|
||||
});
|
||||
|
||||
test('one-line visual novel generation ready copy points to editable draft', () => {
|
||||
const progress = buildVisualNovelEntryGenerationProgress(
|
||||
1_000,
|
||||
'ready',
|
||||
46_000,
|
||||
);
|
||||
|
||||
expect(progress.phaseId).toBe('ready');
|
||||
expect(progress.phaseLabel).toBe('生成完成');
|
||||
expect(progress.phaseDetail).toBe(
|
||||
'视觉小说草稿已准备完成,可进入结果页编辑、保存并试玩。',
|
||||
);
|
||||
expect(progress.overallProgress).toBe(100);
|
||||
});
|
||||
|
||||
test('one-line visual novel generation anchors include source, style and target', () => {
|
||||
const entries = buildVisualNovelEntryGenerationAnchorEntries(
|
||||
createVisualNovelPayload(),
|
||||
);
|
||||
|
||||
expect(entries).toEqual([
|
||||
{
|
||||
id: 'visual-novel-idea',
|
||||
label: '一句话',
|
||||
value: '雨夜旧图书馆里,失忆高中生发现一本会回应心声的日记。',
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-style',
|
||||
label: '视觉画风',
|
||||
value: '映画动画',
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-target',
|
||||
label: '生成目标',
|
||||
value: '可编辑并可试玩的视觉小说草稿',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -41,6 +41,11 @@ export function buildVisualNovelEntryGenerationAnchorEntries(
|
||||
label: '视觉画风',
|
||||
value: payload.visualStyleLabel,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-target',
|
||||
label: '生成目标',
|
||||
value: '可编辑并可试玩的视觉小说草稿',
|
||||
},
|
||||
].filter((entry) => entry.value.trim());
|
||||
}
|
||||
|
||||
@@ -72,27 +77,55 @@ export function buildVisualNovelEntryGenerationProgress(
|
||||
weight: number;
|
||||
durationMs: number;
|
||||
},
|
||||
] = [
|
||||
{
|
||||
id: 'visual-novel-session',
|
||||
label: '创建创作会话',
|
||||
detail: '写入一句话与视觉画风,准备生成视觉小说底稿。',
|
||||
weight: 24,
|
||||
durationMs: 5_000,
|
||||
id: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
weight: number;
|
||||
durationMs: number;
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-draft',
|
||||
label: '生成故事底稿',
|
||||
detail: '整理世界观、角色、场景和剧情阶段。',
|
||||
weight: 56,
|
||||
durationMs: 22_000,
|
||||
id: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
weight: number;
|
||||
durationMs: number;
|
||||
},
|
||||
] = [
|
||||
{
|
||||
id: 'visual-novel-intent',
|
||||
label: '理解一句话创意',
|
||||
detail: '提取核心题材、视觉画风、玩家身份和互动叙事目标。',
|
||||
weight: 16,
|
||||
durationMs: 6_000,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-world',
|
||||
label: '扩展世界观',
|
||||
detail: '生成世界背景、故事前提、文学风格和玩家角色。',
|
||||
weight: 22,
|
||||
durationMs: 10_000,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-cast-scenes',
|
||||
label: '设计角色与场景',
|
||||
detail: '补齐主要角色、可生成立绘的外观描述和 opening 场景。',
|
||||
weight: 28,
|
||||
durationMs: 16_000,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-opening',
|
||||
label: '生成开场与选择',
|
||||
detail: '写入开场旁白、首句对白、剧情阶段和 2 到 4 个初始选择。',
|
||||
weight: 24,
|
||||
durationMs: 10_000,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel-ready',
|
||||
label: '准备草稿页',
|
||||
detail: '校验可编辑字段并进入草稿页。',
|
||||
weight: 20,
|
||||
durationMs: 4_000,
|
||||
detail: '校验可编辑字段并进入结果页,后续可保存作品和试玩。',
|
||||
weight: 10,
|
||||
durationMs: 3_000,
|
||||
},
|
||||
];
|
||||
let elapsedBeforeStep = 0;
|
||||
@@ -130,9 +163,13 @@ export function buildVisualNovelEntryGenerationProgress(
|
||||
: phase === 'failed'
|
||||
? Math.max(1, completedWeight)
|
||||
: Math.min(98, completedWeight + activeStep.weight * activeRatio);
|
||||
const estimatedTotalMs = timeline.reduce(
|
||||
(sum, step) => sum + step.durationMs,
|
||||
0,
|
||||
);
|
||||
|
||||
return {
|
||||
phaseId: phase,
|
||||
phaseId: phase === 'generating' ? activeStep.id : phase,
|
||||
phaseLabel:
|
||||
phase === 'ready'
|
||||
? '生成完成'
|
||||
@@ -141,7 +178,7 @@ export function buildVisualNovelEntryGenerationProgress(
|
||||
: activeStep.label,
|
||||
phaseDetail:
|
||||
phase === 'ready'
|
||||
? '视觉小说草稿已准备完成。'
|
||||
? '视觉小说草稿已准备完成,可进入结果页编辑、保存并试玩。'
|
||||
: phase === 'failed'
|
||||
? '草稿生成失败,请返回入口页调整后重试。'
|
||||
: activeStep.detail,
|
||||
@@ -151,7 +188,7 @@ export function buildVisualNovelEntryGenerationProgress(
|
||||
totalWeight: 100,
|
||||
elapsedMs,
|
||||
estimatedRemainingMs:
|
||||
phase === 'ready' ? 0 : Math.max(0, 31_000 - elapsedMs),
|
||||
phase === 'ready' ? 0 : Math.max(0, estimatedTotalMs - elapsedMs),
|
||||
activeStepIndex: normalizedActiveStepIndex,
|
||||
steps: timeline.map((step, index) => {
|
||||
const isCompleted =
|
||||
|
||||
Reference in New Issue
Block a user