73eb3f6dc8
隐藏陶泥儿精选中的特效、场景和游戏分类入口 补充创作首页精选分类隐藏断言
744 lines
21 KiB
TypeScript
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);
|
|
}
|