feat: add visual novel AI image entry points
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from './visualNovelCreationClient';
|
||||
export * from './visualNovelAssetClient';
|
||||
export * from './visualNovelAudioGenerationClient';
|
||||
export * from './visualNovelCreationClient';
|
||||
export * from './visualNovelImageGenerationClient';
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import type {
|
||||
VisualNovelCharacterDraft,
|
||||
VisualNovelResultDraft,
|
||||
VisualNovelSceneDraft,
|
||||
} from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type {
|
||||
CustomWorldSceneImageRequest,
|
||||
CustomWorldSceneImageResult,
|
||||
} from '../aiTypes';
|
||||
import { generateRpgWorldSceneImage } from '../rpg-creation/rpgCreationAssetClient';
|
||||
|
||||
export type VisualNovelImageGenerationKind =
|
||||
| 'cover'
|
||||
| 'scene_background'
|
||||
| 'character_standee';
|
||||
|
||||
export type VisualNovelImageGenerationRequest = {
|
||||
kind: VisualNovelImageGenerationKind;
|
||||
draft: VisualNovelResultDraft;
|
||||
scene?: VisualNovelSceneDraft | null;
|
||||
character?: VisualNovelCharacterDraft | null;
|
||||
prompt?: string;
|
||||
referenceImageSrc?: string;
|
||||
};
|
||||
|
||||
function buildVisualNovelProfile(
|
||||
draft: VisualNovelResultDraft,
|
||||
): CustomWorldSceneImageRequest['profile'] {
|
||||
return {
|
||||
id: draft.profileId?.trim() || 'visual-novel-draft',
|
||||
name: draft.workTitle.trim() || draft.world.title.trim() || '视觉小说作品',
|
||||
subtitle: draft.world.title.trim() || draft.workTitle.trim() || '视觉小说',
|
||||
summary: draft.workDescription.trim() || draft.world.summary.trim(),
|
||||
tone:
|
||||
draft.world.defaultTone.trim() || draft.world.literaryStyle.trim() || '视觉小说',
|
||||
playerGoal: draft.world.playerRole.trim() || '推进剧情并完成关键选择',
|
||||
settingText: [
|
||||
draft.world.premise,
|
||||
draft.world.background,
|
||||
draft.world.literaryStyle,
|
||||
]
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
function buildVisualNovelLandmark(
|
||||
payload: VisualNovelImageGenerationRequest,
|
||||
): CustomWorldSceneImageRequest['landmark'] {
|
||||
if (payload.kind === 'scene_background' && payload.scene) {
|
||||
return {
|
||||
id: payload.scene.sceneId,
|
||||
name: payload.scene.name.trim() || '视觉小说场景',
|
||||
description: payload.scene.description.trim() || payload.draft.world.summary,
|
||||
};
|
||||
}
|
||||
|
||||
if (payload.kind === 'character_standee' && payload.character) {
|
||||
return {
|
||||
id: payload.character.characterId,
|
||||
name: `${payload.character.name.trim() || '视觉小说角色'}立绘`,
|
||||
description: [
|
||||
payload.character.appearance,
|
||||
payload.character.personality,
|
||||
payload.character.role,
|
||||
payload.character.relationshipToPlayer,
|
||||
]
|
||||
.map((part) => part?.trim() ?? '')
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: payload.draft.profileId?.trim() || 'visual-novel-cover',
|
||||
name: `${payload.draft.workTitle.trim() || '视觉小说'}封面`,
|
||||
description:
|
||||
payload.draft.workDescription.trim() ||
|
||||
payload.draft.world.summary.trim() ||
|
||||
payload.draft.world.premise.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildDefaultVisualNovelImagePrompt(
|
||||
payload: VisualNovelImageGenerationRequest,
|
||||
) {
|
||||
const draft = payload.draft;
|
||||
if (payload.kind === 'scene_background' && payload.scene) {
|
||||
return [
|
||||
`视觉小说场景背景:${payload.scene.name}`,
|
||||
payload.scene.description,
|
||||
draft.world.defaultTone,
|
||||
'16:9 横版背景图,无文字,无 UI,无人物特写',
|
||||
]
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
if (payload.kind === 'character_standee' && payload.character) {
|
||||
return [
|
||||
`视觉小说角色立绘:${payload.character.name}`,
|
||||
payload.character.appearance,
|
||||
payload.character.personality,
|
||||
payload.character.tone,
|
||||
'透明感二次元全身或半身立绘,干净背景,无文字,无 UI',
|
||||
]
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
return [
|
||||
`视觉小说作品封面:${draft.workTitle}`,
|
||||
draft.workDescription,
|
||||
draft.world.summary,
|
||||
draft.world.defaultTone,
|
||||
'精致视觉小说封面构图,无文字,无 UI,适合 4:3/16:9 裁切',
|
||||
]
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
function resolveVisualNovelImageSize(kind: VisualNovelImageGenerationKind) {
|
||||
if (kind === 'character_standee') {
|
||||
return '768*1024';
|
||||
}
|
||||
return '1280*720';
|
||||
}
|
||||
|
||||
export async function generateVisualNovelImageAsset(
|
||||
payload: VisualNovelImageGenerationRequest,
|
||||
): Promise<CustomWorldSceneImageResult> {
|
||||
const userPrompt =
|
||||
payload.prompt?.trim() || buildDefaultVisualNovelImagePrompt(payload);
|
||||
|
||||
if (!userPrompt.trim()) {
|
||||
throw new Error('请先补充图片生成提示词。');
|
||||
}
|
||||
|
||||
return generateRpgWorldSceneImage({
|
||||
profile: buildVisualNovelProfile(payload.draft),
|
||||
landmark: buildVisualNovelLandmark(payload),
|
||||
userPrompt,
|
||||
size: resolveVisualNovelImageSize(payload.kind),
|
||||
...(payload.referenceImageSrc?.trim()
|
||||
? { referenceImageSrc: payload.referenceImageSrc.trim() }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function buildVisualNovelImageGenerationPrompt(
|
||||
payload: VisualNovelImageGenerationRequest,
|
||||
) {
|
||||
return buildDefaultVisualNovelImagePrompt(payload);
|
||||
}
|
||||
Reference in New Issue
Block a user