Files
Genarrative/src/components/creation-home/creationShowcaseModel.ts
T
kdletters 73eb3f6dc8 隐藏精选部分素材分类
隐藏陶泥儿精选中的特效、场景和游戏分类入口

补充创作首页精选分类隐藏断言
2026-06-29 17:21:09 +08:00

744 lines
21 KiB
TypeScript

import type {
EditorAssetGenerationInputReference,
EditorAssetSnapshot,
EditorProjectResourceSnapshot,
} 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 =
| 'packs'
| 'characters'
| 'ui'
| 'music'
| 'marketing';
export type ShowcaseAssetMediaType = 'image' | 'video' | 'audio';
export type ShowcaseAssetPreview = {
id: string;
label: string;
src: string;
objectKey?: string | null;
mediaType: ShowcaseAssetMediaType;
width?: number;
height?: number;
};
export type ShowcaseAssetItem = {
id: string;
label: string;
previews: ShowcaseAssetPreview[];
prompt: string;
author: string;
cost: string;
};
type ShowcaseAssetGroup = {
id: string;
label?: string;
assets: EditorAssetSnapshot[];
};
const UNKNOWN_META_VALUE = '-';
const SHOWCASE_COST_KEYS = [
'priceMudPoints',
'costMudPoints',
'generationCostMudPoints',
'mudPointCost',
] as const;
const SHOWCASE_AUTHOR_KEYS = ['authorDisplayName', 'authorPublicUserCode'] as const;
export const SHOWCASE_TABS: Array<{ id: ShowcaseTabId; label: string }> = [
{ id: 'packs', label: '素材包' },
{ id: 'characters', label: '角色' },
{ id: 'ui', label: 'UI' },
{ id: 'music', label: '音乐' },
{ id: 'marketing', label: '美宣' },
];
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 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.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,
generationInputs: resource.generationInputs,
createdAt: resource.createdAt,
updatedAt: resource.updatedAt,
} as EditorAssetSnapshot;
}
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 groupPackAssets(assets: EditorAssetSnapshot[]) {
const groups = new Map<string, ShowcaseAssetGroup>();
assets.forEach((asset) => {
const specReference = findSpecReference(asset);
if (specReference) {
addAssetToGroup(groups, getReferenceKey(specReference), asset, specReference.label);
return;
}
if (isSpecAsset(asset)) {
addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label);
return;
}
addAssetToGroup(groups, `standalone:${asset.assetId}`, asset, asset.label);
});
const specAssets = assets.filter(isSpecAsset);
specAssets.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 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 === 'packs') {
return groupPackAssets(assets);
}
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 resolveAssetPrompt(asset: EditorAssetSnapshot) {
const directPrompt = asset.prompt?.trim() || asset.actualPrompt?.trim();
if (directPrompt) {
return directPrompt;
}
const generationFields = asset.generationInputs?.fields ?? [];
const firstPromptField =
generationFields.find((field) => /提示词|描述|设定|prompt/u.test(field.title)) ??
generationFields[0];
return firstPromptField?.value?.trim() || 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 {
return {
id: asset.assetId,
label: asset.label,
src: asset.imageSrc,
objectKey: asset.objectKey,
mediaType: resolveAssetMediaType(asset),
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];
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),
};
}
export function buildCreationShowcaseItems({
activeTab,
projectResources,
fallbackAuthorName,
}: {
activeTab: ShowcaseTabId;
projectResources: EditorProjectResourceSnapshot[];
fallbackAuthorName?: string | null;
}) {
const projectItems = getGroupsForTab(
buildGeneratedProjectResourceAssets(projectResources),
activeTab,
)
.map((group) => toLibraryShowcaseItem(group, fallbackAuthorName))
.filter((item): item is ShowcaseAssetItem => item !== null);
return projectItems.slice(0, 36);
}