Files
Genarrative/src/components/creation-home/creationShowcaseModel.ts
T
kdletters 862653c987 修复精选提示词展示
精选卡片优先展示用户可见的生成输入

过滤系统内置和素材提取提示词

补充精选提示词外露回归测试
2026-07-07 18:47:23 +08:00

950 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
EditorAssetGenerationInputReference,
EditorAssetSnapshot,
EditorProjectResourceSnapshot,
EditorShowcaseCampaignSnapshot,
} from '../../services/image-editor/editorProjectClient';
import {
calculateCharacterAnimationPrice,
calculateEditorBackgroundMusicPrice,
calculateEditorImageGenerationPrice,
calculateEditorSoundEffectPrice,
calculateEditorVideoPrice,
CHARACTER_ANIMATION_MODEL,
DEFAULT_BACKGROUND_MUSIC_MODEL,
DEFAULT_SOUND_EFFECT_MODEL,
DEFAULT_VIDEO_DURATION_SECONDS,
DEFAULT_VIDEO_MODEL,
} from '../image-editor/ImageCanvasGenerationModel';
export type ShowcaseTabId = 'all' | 'characters' | 'ui' | 'music' | 'marketing';
export type ShowcaseAssetMediaType = 'image' | 'video' | 'audio';
export type ShowcaseAssetPreview = {
id: string;
label: string;
src: string;
coverSrc?: string | null;
objectKey?: string | null;
mediaType: ShowcaseAssetMediaType;
width?: number;
height?: number;
};
export type ShowcaseAssetItem = {
id: string;
label: string;
previews: ShowcaseAssetPreview[];
prompt: string;
author: string;
cost: string;
likeCount?: number | null;
showcaseId?: string | null;
campaign?: boolean;
};
type ShowcaseAssetGroup = {
id: string;
label?: string;
assets: EditorAssetSnapshot[];
};
const UNKNOWN_META_VALUE = '-';
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.png';
const SHOWCASE_COST_KEYS = [
'priceMudPoints',
'costMudPoints',
'generationCostMudPoints',
'mudPointCost',
] as const;
const SHOWCASE_AUTHOR_KEYS = [
'authorDisplayName',
'authorPublicUserCode',
] as const;
const USER_PROMPT_INPUT_TITLES = new Set(
[
'生成提示词',
'视频描述',
'prompt',
'sound',
'gpt_description_prompt',
'音效提示词',
'背景音乐提示词',
'角色设定',
'用户输入',
'素材描述',
'自定义规范提示词',
'修改要求',
'快速编辑提示词',
'重绘提示词',
'动作描述',
].map((title) => title.toLowerCase()),
);
const STRUCTURED_USER_INPUT_TITLES = new Set(
[
'玩法设定',
'美术风格',
'头身比',
'角色视角',
'游戏名',
'游戏分类',
'一句话描述游戏',
].map((title) => title.toLowerCase()),
);
export const SHOWCASE_TABS: Array<{ id: ShowcaseTabId; label: string }> = [
{ id: 'all', label: '全部' },
{ id: 'characters', label: '角色' },
{ id: 'ui', label: 'UI' },
{ id: 'music', label: '音乐' },
{ id: 'marketing', label: '美宣' },
];
const SHOWCASE_CATEGORY_IDS = new Set(
SHOWCASE_TABS.filter((tab) => tab.id !== 'all').map((tab) => tab.id),
);
function assetRecord(asset: EditorAssetSnapshot) {
return asset as Record<string, unknown>;
}
function projectResourceRecord(resource: EditorProjectResourceSnapshot) {
return resource as unknown as Record<string, unknown>;
}
function normalizeText(value: unknown) {
return typeof value === 'string' && value.trim() ? value.trim() : '';
}
function normalizeGenerationInputTitle(value: string) {
return value.trim().toLowerCase();
}
function isBuiltInPromptText(value: string) {
const trimmedValue = value.trim();
return (
trimmedValue.startsWith('【系统内置】') ||
trimmedValue.startsWith('【宣发素材类型】') ||
trimmedValue.startsWith('参考图1的图标素材规范') ||
trimmedValue.startsWith('生成玩法UI原型图') ||
trimmedValue.includes('仅提取被红色框框选的素材') ||
trimmedValue.includes('图集背景必须使用所选单一纯色抠图背景')
);
}
function normalizeNumber(value: unknown) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim()) {
const numericValue = Number(value.trim());
return Number.isFinite(numericValue) ? numericValue : null;
}
return null;
}
function normalizeInt(
value: unknown,
fallback: number,
minValue = 1,
maxValue = Number.MAX_SAFE_INTEGER,
) {
const numericValue = normalizeNumber(value);
if (numericValue === null) {
return fallback;
}
return Math.min(maxValue, Math.max(minValue, Math.round(numericValue)));
}
function normalizeAssetKind(asset: EditorAssetSnapshot) {
return asset.assetKind?.trim() ?? '';
}
function getAssetHaystack(asset: EditorAssetSnapshot) {
return [
asset.label,
asset.prompt,
asset.actualPrompt,
asset.model,
asset.provider,
asset.sourceType,
asset.assetKind,
...(asset.generationInputs?.fields ?? []).flatMap((field) => [
field.title,
field.value,
]),
...(asset.generationInputs?.references ?? []).flatMap((reference) => [
reference.title,
reference.label,
reference.refType,
reference.refId,
]),
]
.filter(Boolean)
.join(' ')
.toLowerCase();
}
function getAssetReferences(asset: EditorAssetSnapshot) {
return asset.generationInputs?.references ?? [];
}
function getReferenceKey(reference: EditorAssetGenerationInputReference) {
return `${reference.refType}:${reference.refId}`;
}
function getSelfAssetKey(asset: EditorAssetSnapshot) {
return `asset:${asset.assetId}`;
}
function getAssetReferenceKeys(asset: EditorAssetSnapshot) {
const keys = [getSelfAssetKey(asset)];
if (asset.assetId?.trim()) {
keys.push(`project-resource:${asset.assetId.trim()}`);
}
if (asset.sourceResourceId?.trim()) {
keys.push(`project-resource:${asset.sourceResourceId.trim()}`);
}
if (asset.assetObjectId?.trim()) {
keys.push(`asset-object:${asset.assetObjectId.trim()}`);
}
if (asset.objectKey?.trim()) {
keys.push(`object:${asset.objectKey.trim()}`);
}
return keys;
}
function projectResourceToShowcaseAsset(
resource: EditorProjectResourceSnapshot,
): EditorAssetSnapshot {
const record = projectResourceRecord(resource);
const fallbackLabel =
normalizeText(record.label) ||
normalizeText(record.title) ||
resource.assetKind?.trim() ||
'项目素材';
return {
...record,
assetId:
resource.assetId?.trim() ||
resource.showcaseId?.trim() ||
resource.resourceId,
folderId: `project:${resource.projectId}`,
label: fallbackLabel,
imageSrc: resource.imageSrc,
objectKey: resource.objectKey,
assetObjectId: resource.assetObjectId,
width: resource.width,
height: resource.height,
sourceType: resource.sourceType,
prompt: resource.prompt ?? null,
actualPrompt: resource.actualPrompt ?? null,
model: resource.model ?? null,
provider: resource.provider ?? null,
taskId: resource.taskId ?? null,
durationSeconds: resource.durationSeconds,
sourceResourceId: resource.sourceResourceId,
assetKind: resource.assetKind,
showcaseCategory: normalizeShowcaseCategory(
normalizeText(record.showcaseCategory),
),
generationInputs: resource.generationInputs,
createdAt: resource.createdAt,
updatedAt: resource.updatedAt,
} as EditorAssetSnapshot;
}
function normalizeShowcaseCategory(value: string): ShowcaseTabId | null {
return SHOWCASE_CATEGORY_IDS.has(value as Exclude<ShowcaseTabId, 'all'>)
? (value as ShowcaseTabId)
: null;
}
function resolveAssetShowcaseCategory(asset: EditorAssetSnapshot) {
return normalizeShowcaseCategory(
normalizeText(assetRecord(asset).showcaseCategory),
);
}
function normalizeShowcaseMediaRef(value: string | null | undefined) {
const trimmedValue = value?.trim().split(/[?#]/u)[0]?.trim() ?? '';
if (!trimmedValue) {
return '';
}
const pathValue = /^https?:\/\//iu.test(trimmedValue)
? (() => {
try {
return new URL(trimmedValue).pathname;
} catch {
return trimmedValue;
}
})()
: trimmedValue;
return pathValue.replace(/^\/+/u, '');
}
function getProjectResourceMediaKey(resource: EditorProjectResourceSnapshot) {
const assetObjectId = resource.assetObjectId?.trim();
if (assetObjectId) {
return `asset-object:${assetObjectId}`;
}
const objectKey = normalizeShowcaseMediaRef(resource.objectKey);
if (objectKey) {
return `object:${objectKey}`;
}
const imageSrc = normalizeShowcaseMediaRef(resource.imageSrc);
return imageSrc ? `src:${imageSrc}` : '';
}
function isCopiedProjectResource(
resource: EditorProjectResourceSnapshot,
resourceById: Map<string, EditorProjectResourceSnapshot>,
) {
const sourceResourceId = resource.sourceResourceId?.trim();
if (!sourceResourceId) {
return false;
}
const sourceResource = resourceById.get(sourceResourceId);
if (!sourceResource) {
return false;
}
return (
getProjectResourceMediaKey(sourceResource) ===
getProjectResourceMediaKey(resource)
);
}
function buildGeneratedProjectResourceAssets(
resources: EditorProjectResourceSnapshot[],
) {
const seenResourceIds = new Set<string>();
const resourceById = new Map(
resources
.filter((resource) => resource.resourceId.trim())
.map((resource) => [resource.resourceId, resource]),
);
return resources
.filter(
(resource) =>
resource.publicShowcaseEnabled !== false &&
resource.sourceType === 'generated' &&
resource.imageSrc.trim(),
)
.filter((resource) => {
if (seenResourceIds.has(resource.resourceId)) {
return false;
}
seenResourceIds.add(resource.resourceId);
return true;
})
.filter((resource) => !isCopiedProjectResource(resource, resourceById))
.map(projectResourceToShowcaseAsset);
}
function findReference(
asset: EditorAssetSnapshot,
matcher: (reference: EditorAssetGenerationInputReference) => boolean,
) {
return getAssetReferences(asset).find(matcher) ?? null;
}
function findSpecReference(asset: EditorAssetSnapshot) {
return findReference(asset, (reference) => /规范/u.test(reference.title));
}
function findCharacterReference(asset: EditorAssetSnapshot) {
return findReference(asset, (reference) =>
/角色图片|角色图/u.test(reference.title),
);
}
function findUiDesignReference(asset: EditorAssetSnapshot) {
return findReference(asset, (reference) =>
/UI设计图|UI 图|UI图片/u.test(reference.title),
);
}
function isAssetKind(asset: EditorAssetSnapshot, kinds: readonly string[]) {
return kinds.includes(normalizeAssetKind(asset));
}
function isAudioAsset(asset: EditorAssetSnapshot) {
const mediaType = normalizeText(assetRecord(asset).mediaType);
return (
mediaType === 'audio' ||
isAssetKind(asset, ['sound-effect', 'background-music']) ||
/\.(mp3|wav|m4a|aac|ogg)(?:$|\?)/iu.test(asset.imageSrc)
);
}
function isVideoAsset(asset: EditorAssetSnapshot) {
const mediaType = normalizeText(assetRecord(asset).mediaType);
return (
mediaType === 'video' ||
isAssetKind(asset, ['video']) ||
/\.(mp4|webm|mov|m4v)(?:$|\?)/iu.test(asset.imageSrc)
);
}
function resolveAssetMediaType(
asset: EditorAssetSnapshot,
): ShowcaseAssetMediaType {
if (isAudioAsset(asset)) {
return 'audio';
}
if (isVideoAsset(asset)) {
return 'video';
}
return 'image';
}
function isSpecAsset(asset: EditorAssetSnapshot) {
return isAssetKind(asset, ['spec', 'icon-spec']);
}
function isCharacterAsset(asset: EditorAssetSnapshot) {
return isAssetKind(asset, ['character']);
}
function isCharacterAnimationAsset(asset: EditorAssetSnapshot) {
return isAssetKind(asset, ['character-animation']);
}
function isUiAsset(asset: EditorAssetSnapshot) {
return isAssetKind(asset, ['ui-design', 'icon', 'icon-spritesheet']);
}
function isMarketingAsset(asset: EditorAssetSnapshot) {
const haystack = getAssetHaystack(asset);
return (
isAssetKind(asset, ['publication-material']) ||
/美宣|宣发|宣传|poster|promo|marketing/u.test(haystack)
);
}
function addAssetToGroup(
groups: Map<string, ShowcaseAssetGroup>,
groupId: string,
asset: EditorAssetSnapshot,
label?: string,
) {
const group = groups.get(groupId);
if (group) {
if (label && !group.label) {
group.label = label;
}
group.assets.push(asset);
return;
}
groups.set(groupId, {
id: groupId,
label,
assets: [asset],
});
}
function mergeAssetAliasGroups(
groups: Map<string, ShowcaseAssetGroup>,
asset: EditorAssetSnapshot,
label?: string,
) {
const aliasGroups = getAssetReferenceKeys(asset)
.map((key) => groups.get(key))
.filter((group): group is ShowcaseAssetGroup => Boolean(group))
.filter(
(group, index, list) =>
list.findIndex((candidate) => candidate.id === group.id) === index,
);
const targetGroup = aliasGroups[0] ?? null;
if (!targetGroup) {
return null;
}
if (label && !targetGroup.label) {
targetGroup.label = label;
}
aliasGroups.slice(1).forEach((group) => {
if (!targetGroup.label && group.label) {
targetGroup.label = group.label;
}
group.assets.forEach((assetInGroup) => {
if (
!targetGroup.assets.some(
(existingAsset) => existingAsset.assetId === assetInGroup.assetId,
)
) {
targetGroup.assets.push(assetInGroup);
}
});
groups.delete(group.id);
});
return targetGroup;
}
function sortGroupAssets(assets: EditorAssetSnapshot[]) {
return [...assets].sort((left, right) => {
const leftRank =
isSpecAsset(left) ||
isCharacterAsset(left) ||
isAssetKind(left, ['ui-design'])
? 0
: 1;
const rightRank =
isSpecAsset(right) ||
isCharacterAsset(right) ||
isAssetKind(right, ['ui-design'])
? 0
: 1;
if (leftRank !== rightRank) {
return leftRank - rightRank;
}
return (left.createdAt ?? '').localeCompare(right.createdAt ?? '');
});
}
function groupCharacterAssets(assets: EditorAssetSnapshot[]) {
const groups = new Map<string, ShowcaseAssetGroup>();
assets
.filter(
(asset) =>
isCharacterAsset(asset) ||
isCharacterAnimationAsset(asset) ||
/角色|character/u.test(getAssetHaystack(asset)),
)
.forEach((asset) => {
if (isCharacterAsset(asset)) {
addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label);
return;
}
const characterReference = findCharacterReference(asset);
if (characterReference) {
addAssetToGroup(
groups,
getReferenceKey(characterReference),
asset,
characterReference.label,
);
return;
}
addAssetToGroup(
groups,
`standalone:${asset.assetId}`,
asset,
asset.label,
);
});
const characterAssets = assets.filter(isCharacterAsset);
characterAssets.forEach((asset) => {
const group = mergeAssetAliasGroups(groups, asset, asset.label);
if (
group &&
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
) {
group.assets.unshift(asset);
}
});
return [...groups.values()].map((group) => ({
...group,
assets: sortGroupAssets(group.assets),
}));
}
function groupUiAssets(assets: EditorAssetSnapshot[]) {
const groups = new Map<string, ShowcaseAssetGroup>();
assets
.filter(
(asset) =>
isUiAsset(asset) ||
/\bui\b|界面|图标|icon/u.test(getAssetHaystack(asset)),
)
.forEach((asset) => {
if (isAssetKind(asset, ['ui-design'])) {
addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label);
return;
}
const uiReference =
findUiDesignReference(asset) ?? findSpecReference(asset);
if (uiReference) {
addAssetToGroup(
groups,
getReferenceKey(uiReference),
asset,
uiReference.label,
);
return;
}
addAssetToGroup(
groups,
`standalone:${asset.assetId}`,
asset,
asset.label,
);
});
const uiDesignAssets = assets.filter((asset) =>
isAssetKind(asset, ['ui-design']),
);
uiDesignAssets.forEach((asset) => {
const group = mergeAssetAliasGroups(groups, asset, asset.label);
if (
group &&
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
) {
group.assets.unshift(asset);
}
});
return [...groups.values()].map((group) => ({
...group,
assets: sortGroupAssets(group.assets),
}));
}
function groupSingleAssets(
assets: EditorAssetSnapshot[],
predicate: (asset: EditorAssetSnapshot) => boolean,
) {
return assets.filter(predicate).map((asset) => ({
id: `asset:${asset.assetId}`,
label: asset.label,
assets: [asset],
}));
}
function getGroupsForTab(assets: EditorAssetSnapshot[], tab: ShowcaseTabId) {
if (tab === 'all') {
return groupSingleAssets(assets, () => true);
}
if (assets.some((asset) => resolveAssetShowcaseCategory(asset))) {
return groupSingleAssets(
assets,
(asset) => resolveAssetShowcaseCategory(asset) === tab,
);
}
if (tab === 'characters') {
return groupCharacterAssets(assets);
}
if (tab === 'ui') {
return groupUiAssets(assets);
}
if (tab === 'music') {
return groupSingleAssets(assets, isAudioAsset);
}
if (tab === 'marketing') {
return groupSingleAssets(assets, isMarketingAsset);
}
return [];
}
function resolveUserGenerationPrompt(asset: EditorAssetSnapshot) {
const fields = asset.generationInputs?.fields ?? [];
const promptField = fields.find((field) => {
const value = field.value.trim();
return (
value &&
USER_PROMPT_INPUT_TITLES.has(
normalizeGenerationInputTitle(field.title),
) &&
!isBuiltInPromptText(value)
);
});
const promptValue = promptField?.value.trim();
if (promptValue) {
return promptValue;
}
const structuredLines = fields.flatMap((field) => {
if (
!STRUCTURED_USER_INPUT_TITLES.has(
normalizeGenerationInputTitle(field.title),
)
) {
return [];
}
const value = field.value.trim();
return value && !isBuiltInPromptText(value)
? [`${field.title.trim()}:${value}`]
: [];
});
return structuredLines.join('\n');
}
function extractUserPromptFromAssembledPrompt(value: string) {
const trimmedValue = value.trim();
const sectionMatch =
trimmedValue.match(/【用户输入】\s*([\s\S]*?)(?=\n+【|$)/u) ??
trimmedValue.match(/【游戏输入】\s*([\s\S]*?)(?=\n+【|$)/u);
const sectionValue = sectionMatch?.[1]?.trim();
if (sectionValue && !isBuiltInPromptText(sectionValue)) {
return sectionValue;
}
const roleSettingMatch = trimmedValue.match(
/(?:^|\n)角色设定[::]\s*([^\n]+)/u,
);
const roleSettingValue = roleSettingMatch?.[1]?.trim();
return roleSettingValue && !isBuiltInPromptText(roleSettingValue)
? roleSettingValue
: '';
}
function resolveAssetPrompt(asset: EditorAssetSnapshot) {
const userPrompt = resolveUserGenerationPrompt(asset);
if (userPrompt) {
return userPrompt;
}
for (const value of [asset.prompt, asset.actualPrompt]) {
const directPrompt = value?.trim() ?? '';
if (!directPrompt) {
continue;
}
const extractedPrompt = extractUserPromptFromAssembledPrompt(directPrompt);
if (extractedPrompt) {
return extractedPrompt;
}
if (!isBuiltInPromptText(directPrompt)) {
return directPrompt;
}
}
return UNKNOWN_META_VALUE;
}
function resolveGroupPrompt(assets: EditorAssetSnapshot[]) {
return (
assets
.map(resolveAssetPrompt)
.find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE
);
}
function resolveAssetAuthor(
asset: EditorAssetSnapshot,
fallbackAuthorName?: string | null,
) {
const record = assetRecord(asset);
for (const key of SHOWCASE_AUTHOR_KEYS) {
const value = normalizeText(record[key]);
if (value) {
return value;
}
}
return fallbackAuthorName?.trim() || UNKNOWN_META_VALUE;
}
function resolveGroupAuthor(
assets: EditorAssetSnapshot[],
fallbackAuthorName?: string | null,
) {
return (
assets
.map((asset) => resolveAssetAuthor(asset, fallbackAuthorName))
.find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE
);
}
function resolveAssetCost(asset: EditorAssetSnapshot) {
const record = assetRecord(asset);
for (const key of SHOWCASE_COST_KEYS) {
const value = normalizeNumber(record[key]);
if (value !== null && value >= 0) {
return value;
}
}
const inferredValue = inferAssetCost(asset);
if (inferredValue !== null && inferredValue >= 0) {
return inferredValue;
}
return null;
}
function getAssetCostKey(asset: EditorAssetSnapshot) {
if (asset.taskId?.trim()) {
return `task:${asset.taskId.trim()}`;
}
if (asset.objectKey?.trim()) {
return `object:${asset.objectKey.trim()}`;
}
return `asset:${asset.assetId}`;
}
function inferAssetCost(asset: EditorAssetSnapshot) {
const kind = normalizeAssetKind(asset);
const model = asset.model;
if (kind === 'sound-effect') {
return calculateEditorSoundEffectPrice(model ?? DEFAULT_SOUND_EFFECT_MODEL);
}
if (kind === 'background-music') {
return calculateEditorBackgroundMusicPrice(
model ?? DEFAULT_BACKGROUND_MUSIC_MODEL,
);
}
if (kind === 'character-animation') {
return calculateCharacterAnimationPrice(
model ?? CHARACTER_ANIMATION_MODEL,
inferVideoResolution(asset, ['480p', '720p'], '480p') as '480p' | '720p',
normalizeInt(asset.durationSeconds, 4, 4, 6),
);
}
if (kind === 'video' || isVideoAsset(asset)) {
return calculateEditorVideoPrice(
model ?? DEFAULT_VIDEO_MODEL,
inferVideoResolution(asset, ['480p', '720p', '1080p'], '480p') as
| '480p'
| '720p'
| '1080p',
normalizeInt(
asset.durationSeconds,
DEFAULT_VIDEO_DURATION_SECONDS,
4,
15,
),
);
}
if (asset.sourceType === 'generated' || kind) {
return calculateEditorImageGenerationPrice({ kind, model });
}
return null;
}
function inferVideoResolution(
asset: EditorAssetSnapshot,
allowedValues: string[],
fallback: string,
) {
const fields = asset.generationInputs?.fields ?? [];
const fieldValue = fields
.flatMap((field) => [field.title, field.value])
.map((value) => value.trim())
.find((value) => /\b(?:480p|720p|1080p)\b/iu.test(value));
const matchedValue = fieldValue?.match(/\b(480p|720p|1080p)\b/iu)?.[1];
if (matchedValue && allowedValues.includes(matchedValue)) {
return matchedValue;
}
return fallback;
}
function resolveGroupCost(assets: EditorAssetSnapshot[]) {
const resolvedCosts = new Map<string, number>();
assets.forEach((asset) => {
const cost = resolveAssetCost(asset);
if (cost !== null) {
resolvedCosts.set(getAssetCostKey(asset), cost);
}
});
const costs = [...resolvedCosts.values()];
if (!costs.length) {
return UNKNOWN_META_VALUE;
}
const totalCost = costs.reduce((sum, value) => sum + value, 0);
return `${totalCost}泥点`;
}
function toAssetPreview(asset: EditorAssetSnapshot): ShowcaseAssetPreview {
const mediaType = resolveAssetMediaType(asset);
return {
id: asset.assetId,
label: asset.label,
src: asset.imageSrc,
coverSrc: mediaType === 'audio' ? AUDIO_ASSET_COVER_SRC : null,
objectKey: asset.objectKey,
mediaType,
width: asset.width,
height: asset.height,
};
}
function toLibraryShowcaseItem(
group: ShowcaseAssetGroup,
fallbackAuthorName?: string | null,
): ShowcaseAssetItem | null {
const sortedAssets = sortGroupAssets(group.assets);
const previews = sortedAssets
.map(toAssetPreview)
.filter((preview) => preview.src.trim());
if (!previews.length) {
return null;
}
const primaryAsset = sortedAssets[0];
if (!primaryAsset) {
return null;
}
const label =
group.label?.trim() ||
primaryAsset?.label?.trim() ||
`${previews[0]?.label ?? '素材'}组合`;
return {
id: group.id,
label,
previews,
prompt: resolveGroupPrompt(sortedAssets),
author: resolveGroupAuthor(sortedAssets, fallbackAuthorName),
cost: resolveGroupCost(sortedAssets),
likeCount: normalizeNumber(assetRecord(primaryAsset).likeCount),
showcaseId: normalizeText(assetRecord(primaryAsset).showcaseId),
};
}
function campaignToShowcaseItem(
campaign?: EditorShowcaseCampaignSnapshot | null,
): ShowcaseAssetItem | null {
const objectKey = campaign?.imageObjectKey?.trim().replace(/^\/+/u, '') ?? '';
const imageSrc =
campaign?.imageSrc.trim() || (objectKey ? `/${objectKey}` : '');
if (!campaign?.enabled || !imageSrc) {
return null;
}
return {
id: 'campaign:global',
label: campaign.title.trim() || '陶泥儿精选',
previews: [
{
id: 'campaign:global:image',
label: campaign.title.trim() || '陶泥儿精选',
src: imageSrc,
objectKey: objectKey || null,
mediaType: 'image',
width: campaign.imageWidth ?? undefined,
height: campaign.imageHeight ?? undefined,
},
],
prompt: campaign.prompt.trim() || UNKNOWN_META_VALUE,
author: campaign.author.trim() || UNKNOWN_META_VALUE,
cost: campaign.costText.trim() || UNKNOWN_META_VALUE,
campaign: true,
};
}
export function buildCreationShowcaseItems({
activeTab,
projectResources,
campaign,
fallbackAuthorName,
}: {
activeTab: ShowcaseTabId;
projectResources: EditorProjectResourceSnapshot[];
campaign?: EditorShowcaseCampaignSnapshot | null;
fallbackAuthorName?: string | null;
}) {
const projectItems = getGroupsForTab(
buildGeneratedProjectResourceAssets(projectResources),
activeTab,
)
.map((group) => toLibraryShowcaseItem(group, fallbackAuthorName))
.filter((item): item is ShowcaseAssetItem => item !== null);
const campaignItem = campaignToShowcaseItem(campaign);
return campaignItem ? [campaignItem, ...projectItems] : projectItems;
}