Files
Genarrative/src/components/creation-home/creationShowcaseModel.ts
T
k88936 291724a984 合并主分支并解决画布生成冲突
同步主分支的角色动作序列、精选展示与外部编辑器契约更新
保留画布生成输入 V2 契约并合并视频、音效时长元数据
修复精选视频对数值型 V2 时长元数据的兼容
2026-08-06 17:09:39 +08:00

1041 lines
28 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,
EditorImageSequenceFrameResult,
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' | 'image-sequence';
export type ShowcaseAssetPreview = {
id: string;
label: string;
src: string;
coverSrc?: string | null;
objectKey?: string | null;
mediaType: ShowcaseAssetMediaType;
width?: number;
height?: number;
imageSequenceFrames?: EditorImageSequenceFrameResult[];
imageSequenceDurationMs?: 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.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,
taskId: resource.taskId ?? null,
imageSequenceFrames: resource.imageSequenceFrames,
imageSequenceDurationMs: resource.imageSequenceDurationMs,
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 (isCharacterAnimationAsset(asset)) {
return 'image-sequence';
}
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 = String(field.value).trim();
return (
value &&
USER_PROMPT_INPUT_TITLES.has(
normalizeGenerationInputTitle(field.title),
) &&
!isBuiltInPromptText(value)
);
});
const promptValue = promptField ? String(promptField.value).trim() : '';
if (promptValue) {
return promptValue;
}
const structuredLines = fields.flatMap((field) => {
if (
!STRUCTURED_USER_INPUT_TITLES.has(
normalizeGenerationInputTitle(field.title),
)
) {
return [];
}
const value = String(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.imageSequenceDurationMs
? asset.imageSequenceDurationMs / 1_000
: undefined,
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(
inferMediaDurationSeconds(asset),
DEFAULT_VIDEO_DURATION_SECONDS,
4,
15,
),
);
}
if (asset.sourceType === 'generated' || kind) {
return calculateEditorImageGenerationPrice({ kind, model });
}
return null;
}
function inferMediaDurationSeconds(asset: EditorAssetSnapshot) {
const durationField = asset.generationInputs?.fields.find((field) => {
const title = field.title.trim().toLowerCase();
return title === '时长' || title === 'duration';
});
const durationValue = durationField?.value;
if (typeof durationValue === 'number') {
return Number.isFinite(durationValue) ? durationValue : undefined;
}
if (typeof durationValue !== 'string') {
return undefined;
}
const matchedValue = durationValue.match(/\d+(?:\.\d+)?/u)?.[0];
return matchedValue ? Number(matchedValue) : undefined;
}
function inferVideoResolution(
asset: EditorAssetSnapshot,
allowedValues: string[],
fallback: string,
) {
const fields = asset.generationInputs?.fields ?? [];
const fieldValue = fields
.flatMap((field) => [field.title, field.value])
.map((value) => String(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 normalizeShowcaseImageSequence(asset: EditorAssetSnapshot) {
const frames = asset.imageSequenceFrames;
const durationMs = asset.imageSequenceDurationMs;
if (
!Array.isArray(frames) ||
frames.length < 2 ||
typeof durationMs !== 'number' ||
!Number.isFinite(durationMs) ||
durationMs <= 0
) {
return null;
}
const normalizedFrames = frames.flatMap((frame) => {
const imageSrc = frame.imageSrc?.trim() ?? '';
if (
!imageSrc ||
!Number.isFinite(frame.width) ||
frame.width <= 0 ||
!Number.isFinite(frame.height) ||
frame.height <= 0
) {
return [];
}
return [
{
...frame,
imageSrc,
objectKey: frame.objectKey?.trim() || null,
assetObjectId: frame.assetObjectId?.trim() || null,
},
];
});
if (normalizedFrames.length !== frames.length) {
return null;
}
return { frames: normalizedFrames, durationMs };
}
function toAssetPreview(
asset: EditorAssetSnapshot,
): ShowcaseAssetPreview | null {
const mediaType = resolveAssetMediaType(asset);
if (mediaType === 'image-sequence') {
const sequence = normalizeShowcaseImageSequence(asset);
if (!sequence) {
return null;
}
const firstFrame = sequence.frames[0];
if (!firstFrame) {
return null;
}
return {
id: asset.assetId,
label: asset.label,
src: firstFrame.imageSrc,
coverSrc: null,
objectKey: firstFrame.objectKey,
mediaType,
width: firstFrame.width,
height: firstFrame.height,
imageSequenceFrames: sequence.frames,
imageSequenceDurationMs: sequence.durationMs,
};
}
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 is ShowcaseAssetPreview =>
Boolean(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;
}