Merge remote-tracking branch 'origin/pr-573' into codex/pr-557-589-security-integration
This commit is contained in:
@@ -1778,7 +1778,10 @@ PR #507 核心能力已按主分支实现保留,并补齐小视口尺寸约束
|
|||||||
|
|
||||||
## 创作输入区参考内容管理
|
## 创作输入区参考内容管理
|
||||||
|
|
||||||
|
- [x] 视频创作按同名模型组汇总图片、视频和音频参考能力;文生视频路由本身不接收参考素材时,仍可先添加参考内容,再由现有兼容路由选择图生视频或全模态参考模型。素材库和提交前按媒体类型分别执行上限校验。
|
||||||
|
- [x] 创作参考、素材库入口和模型路由 47 项专项测试、前端 TypeScript 检查与生产构建通过。前端已部署为 `1.5.7-creation-video-references`,容器健康,HTTPS 创作页与 API 健康入口返回 200,线上创作页脚本与本次构建哈希一致;未调用真实视频上游。
|
||||||
- [x] 自动化覆盖首个、中间和末尾参考内容按稳定 ID 独立删除,且删除后其余顺序保持不变。
|
- [x] 自动化覆盖首个、中间和末尾参考内容按稳定 ID 独立删除,且删除后其余顺序保持不变。
|
||||||
|
- [ ] 在真实创作台分别选择图片、视频和音频参考,确认素材库只允许当前模型组支持的类型,达到各类型上限时提示明确,并检查任务详情中的 `referenceImages`、`referenceVideos`、`referenceAudios` 与所选素材一致。
|
||||||
- [x] 折叠态提供明确的“查看全部”入口,展开态提供图片、视频、音频、文件筛选、清空全部和收起入口。
|
- [x] 折叠态提供明确的“查看全部”入口,展开态提供图片、视频、音频、文件筛选、清空全部和收起入口。
|
||||||
- [x] 在桌面浏览器验证上方“参考内容”打开素材选择、折叠堆叠态中间卡片删除、展开态任意卡片删除、筛选、拖动排序和收起。
|
- [x] 在桌面浏览器验证上方“参考内容”打开素材选择、折叠堆叠态中间卡片删除、展开态任意卡片删除、筛选、拖动排序和收起。
|
||||||
- [ ] 在真实登录数据下验证“清空全部素材”同步移除全部附件引用,不影响技能引用和素材库原始文件。
|
- [ ] 在真实登录数据下验证“清空全部素材”同步移除全部附件引用,不影响技能引用和素材库原始文件。
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ export type CreationReference = CanvasResourceReference & {
|
|||||||
attachmentId?: string;
|
attachmentId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreationReferenceLimits = {
|
||||||
|
maxImages: number;
|
||||||
|
maxVideos: number;
|
||||||
|
maxAudios: number;
|
||||||
|
};
|
||||||
|
|
||||||
export function buildCreationMentionReferences(skills: Skill[], attachments: CreationAttachment[] = [], snapshots: CreationReference[] = []) {
|
export function buildCreationMentionReferences(skills: Skill[], attachments: CreationAttachment[] = [], snapshots: CreationReference[] = []) {
|
||||||
const counts = { image: 0, video: 0, audio: 0, file: 0 };
|
const counts = { image: 0, video: 0, audio: 0, file: 0 };
|
||||||
const attachmentReferences = attachments.map((attachment) => {
|
const attachmentReferences = attachments.map((attachment) => {
|
||||||
@@ -34,6 +40,25 @@ export function reconcileCreationAttachmentLimit(attachments: CreationAttachment
|
|||||||
return { attachments: nextAttachments, removedReferences };
|
return { attachments: nextAttachments, removedReferences };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reconcileCreationAttachmentLimits(attachments: CreationAttachment[], references: CreationReference[], limits: CreationReferenceLimits) {
|
||||||
|
const remaining = {
|
||||||
|
image: Math.max(0, Math.floor(limits.maxImages)),
|
||||||
|
video: Math.max(0, Math.floor(limits.maxVideos)),
|
||||||
|
audio: Math.max(0, Math.floor(limits.maxAudios)),
|
||||||
|
};
|
||||||
|
const nextAttachments = attachments.filter((attachment) => {
|
||||||
|
const kind = creationAttachmentKind(attachment);
|
||||||
|
if (kind === "file" || remaining[kind] <= 0) return false;
|
||||||
|
remaining[kind] -= 1;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (nextAttachments.length === attachments.length) return { attachments, removedReferences: [] as CreationReference[] };
|
||||||
|
|
||||||
|
const retainedIds = new Set(nextAttachments.map((attachment) => attachment.id));
|
||||||
|
const removedReferences = references.filter((reference) => reference.attachmentId && !retainedIds.has(reference.attachmentId));
|
||||||
|
return { attachments: nextAttachments, removedReferences };
|
||||||
|
}
|
||||||
|
|
||||||
export function removeCreationReferenceTokens(value: string, references: CreationReference[]) {
|
export function removeCreationReferenceTokens(value: string, references: CreationReference[]) {
|
||||||
return references.reduce((current, reference) => current.split(canvasResourceMentionToken(reference)).join("").split(`@${reference.label}`).join(""), value);
|
return references.reduce((current, reference) => current.split(canvasResourceMentionToken(reference)).join("").split(`@${reference.label}`).join(""), value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ export function CreationComposer(props: ComposerProps) {
|
|||||||
: "描述镜头内容、运动、光线与节奏";
|
: "描述镜头内容、运动、光线与节奏";
|
||||||
const emptyPlaceholder = "输入你的镜头、画面或故事。也可以添加参考图开始创作";
|
const emptyPlaceholder = "输入你的镜头、画面或故事。也可以添加参考图开始创作";
|
||||||
const imageReferencesSupported = props.imageProfile.references.maxImages > 0;
|
const imageReferencesSupported = props.imageProfile.references.maxImages > 0;
|
||||||
const referencesSupported = props.mode === "image" ? imageReferencesSupported : props.mode !== "video" || props.videoProfile.operations.includes("image_to_video");
|
const referencesSupported = props.mode === "image" ? imageReferencesSupported : props.mode !== "video" || props.maxReferences > 0;
|
||||||
const canAddMoreReferences = referencesSupported && props.attachments.length < props.maxReferences;
|
const canAddMoreReferences = referencesSupported && props.attachments.length < props.maxReferences;
|
||||||
const addReferenceLabel = interactionBusy ? (props.referenceReplacementBusy ? "正在替换参考图" : "生成中暂不能添加参考内容") : canAddMoreReferences ? "添加更多参考内容" : `已达到当前模型的参考内容上限(${props.maxReferences} 个)`;
|
const addReferenceLabel = interactionBusy ? (props.referenceReplacementBusy ? "正在替换参考图" : "生成中暂不能添加参考内容") : canAddMoreReferences ? "添加更多参考内容" : `已达到当前模型的参考内容上限(${props.maxReferences} 个)`;
|
||||||
const referenceCounts = useMemo(() => props.attachments.reduce((counts, attachment) => {
|
const referenceCounts = useMemo(() => props.attachments.reduce((counts, attachment) => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getActiveUserScope } from "@/lib/user-scope";
|
|||||||
import { continueCreationConversationOnCanvas } from "@/services/creation-canvas-conversation";
|
import { continueCreationConversationOnCanvas } from "@/services/creation-canvas-conversation";
|
||||||
import { useExternalAssetSources } from "@/hooks/use-external-asset-sources";
|
import { useExternalAssetSources } from "@/hooks/use-external-asset-sources";
|
||||||
import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, videoDurationAllowed, videoDurationOptions } from "@/lib/model-capabilities";
|
import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, videoDurationAllowed, videoDurationOptions } from "@/lib/model-capabilities";
|
||||||
import { inferVideoOperation, resolveCompatibleModel, mergedImageCapabilityConfig, type ModelRequirements } from "@/lib/model-selection";
|
import { inferVideoOperation, modelGroupReferenceLimits, resolveCompatibleModel, mergedImageCapabilityConfig, type ModelRequirements } from "@/lib/model-selection";
|
||||||
import type { BackendGenerationResult } from "@/services/api/generation-task";
|
import type { BackendGenerationResult } from "@/services/api/generation-task";
|
||||||
import type { Skill } from "@/services/api/skills";
|
import type { Skill } from "@/services/api/skills";
|
||||||
import type { GenerationTask } from "@/services/api/task-center";
|
import type { GenerationTask } from "@/services/api/task-center";
|
||||||
@@ -27,7 +27,7 @@ import type { PromptOptimizerProvider } from "@/lib/plugins/plugin-types";
|
|||||||
import { promptOptimizerPlugin, PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer";
|
import { promptOptimizerPlugin, PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer";
|
||||||
import { createPluginHostContext } from "@/services/plugin-host";
|
import { createPluginHostContext } from "@/services/plugin-host";
|
||||||
import { usePluginStore } from "@/stores/use-plugin-store";
|
import { usePluginStore } from "@/stores/use-plugin-store";
|
||||||
import { buildCreationMentionReferences, expandCreationPrompt, reconcileCreationAttachmentLimit, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences, type CreationReference } from "./creation-references";
|
import { buildCreationMentionReferences, expandCreationPrompt, reconcileCreationAttachmentLimit, reconcileCreationAttachmentLimits, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences, type CreationReference, type CreationReferenceLimits } from "./creation-references";
|
||||||
import { creationAttachmentFromAsset, creationAttachmentFromAudio, creationAttachmentFromAudioAsset, creationAttachmentFromDocument, creationAttachmentFromExternalAsset, creationAttachmentFromImage, creationAttachmentFromVideo, creationAttachmentFromVideoAsset, creationAttachmentKind, creationAudioAsset, creationFileAccepted, creationImageAsset, creationMediaAspectRatio, creationUploadAccept, creationVideoAsset, removeCreationAttachment, splitCreationAttachments, type CreationAttachment } from "./creation-assets";
|
import { creationAttachmentFromAsset, creationAttachmentFromAudio, creationAttachmentFromAudioAsset, creationAttachmentFromDocument, creationAttachmentFromExternalAsset, creationAttachmentFromImage, creationAttachmentFromVideo, creationAttachmentFromVideoAsset, creationAttachmentKind, creationAudioAsset, creationFileAccepted, creationImageAsset, creationMediaAspectRatio, creationUploadAccept, creationVideoAsset, removeCreationAttachment, splitCreationAttachments, type CreationAttachment } from "./creation-assets";
|
||||||
import { defaultCreationMode, modeLabels, type CreationConversation, type CreationMessage, type CreationMode, type CreationRetryContext, type CreationSettings, type CreationShotRailEntry, type CreationStatus } from "./creation-types";
|
import { defaultCreationMode, modeLabels, type CreationConversation, type CreationMessage, type CreationMode, type CreationRetryContext, type CreationSettings, type CreationShotRailEntry, type CreationStatus } from "./creation-types";
|
||||||
import { attachCreationTaskContexts, completedCreationGenerationTask, conversationTimestamp, creationShotRail, creationVideoShotOrdinal, isImageAttachment, isVideoAttachment, materializeCreationTaskResults, newConversation, newMessage, reconcileCreationTaskMessages } from "./creation-conversations";
|
import { attachCreationTaskContexts, completedCreationGenerationTask, conversationTimestamp, creationShotRail, creationVideoShotOrdinal, isImageAttachment, isVideoAttachment, materializeCreationTaskResults, newConversation, newMessage, reconcileCreationTaskMessages } from "./creation-conversations";
|
||||||
@@ -42,6 +42,18 @@ type CreationRuntime = Awaited<ReturnType<typeof loadCreationRuntime>>;
|
|||||||
|
|
||||||
const TEXT_STREAMING_PREF_KEY = "creation.composer.text-streaming";
|
const TEXT_STREAMING_PREF_KEY = "creation.composer.text-streaming";
|
||||||
const TEXT_THINKING_PREF_KEY = "creation.composer.text-thinking";
|
const TEXT_THINKING_PREF_KEY = "creation.composer.text-thinking";
|
||||||
|
|
||||||
|
function creationLibraryDisabledReason(mode: CreationMode, kind: string | undefined, videoLimits?: CreationReferenceLimits) {
|
||||||
|
if (!kind) return undefined;
|
||||||
|
if (mode === "image") return kind === "image" ? undefined : "图片创作仅支持参考图";
|
||||||
|
if (mode !== "video") return undefined;
|
||||||
|
|
||||||
|
const maximum = kind === "image" ? videoLimits?.maxImages : kind === "video" ? videoLimits?.maxVideos : kind === "audio" ? videoLimits?.maxAudios : 0;
|
||||||
|
if ((maximum || 0) > 0) return undefined;
|
||||||
|
const label = kind === "video" ? "视频" : kind === "audio" ? "音频" : kind === "image" ? "图片" : "此类素材";
|
||||||
|
return `当前视频模型不支持参考${label}`;
|
||||||
|
}
|
||||||
|
|
||||||
function readComposerPref(key: string, fallback: boolean): boolean {
|
function readComposerPref(key: string, fallback: boolean): boolean {
|
||||||
try {
|
try {
|
||||||
const stored = window.localStorage.getItem(key);
|
const stored = window.localStorage.getItem(key);
|
||||||
@@ -152,7 +164,14 @@ export default function CreatePage() {
|
|||||||
: config, [config, mode, ratio, seconds, selectedModel, videoQuality]);
|
: config, [config, mode, ratio, seconds, selectedModel, videoQuality]);
|
||||||
const imageProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).image!, [config, selectedModel]);
|
const imageProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).image!, [config, selectedModel]);
|
||||||
const videoProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).video!, [config, selectedModel]);
|
const videoProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).video!, [config, selectedModel]);
|
||||||
const maxReferences = mode === "video" ? videoProfile.operations.includes("image_to_video") ? videoProfile.references.maxImages : 0 : mode === "image" ? imageProfile.references.maxImages : 6;
|
// 同名逻辑模型可能把文生视频、图生视频和全模态参考拆到不同路由。
|
||||||
|
// 入口需要展示整个模型组的引用能力,添加素材后再由兼容路由选择具体模型。
|
||||||
|
const videoReferenceLimits = useMemo(() => mode === "video"
|
||||||
|
? modelGroupReferenceLimits(config, preferredModel || selectedModel, "video") || { maxImages: 0, maxVideos: 0, maxAudios: 0 }
|
||||||
|
: undefined, [config, mode, preferredModel, selectedModel]);
|
||||||
|
const maxReferences = mode === "video"
|
||||||
|
? (videoReferenceLimits?.maxImages || 0) + (videoReferenceLimits?.maxVideos || 0) + (videoReferenceLimits?.maxAudios || 0)
|
||||||
|
: mode === "image" ? imageProfile.references.maxImages : 6;
|
||||||
const referenceImageSize = useMemo(() => {
|
const referenceImageSize = useMemo(() => {
|
||||||
const imageAttachments = attachments.filter(isImageAttachment);
|
const imageAttachments = attachments.filter(isImageAttachment);
|
||||||
if (imageAttachments.length !== 1) return undefined;
|
if (imageAttachments.length !== 1) return undefined;
|
||||||
@@ -225,16 +244,16 @@ export default function CreatePage() {
|
|||||||
setSeconds(normalized.seconds);
|
setSeconds(normalized.seconds);
|
||||||
setRatio(normalized.ratio);
|
setRatio(normalized.ratio);
|
||||||
setVideoQuality(normalized.resolution.replace(/p$/i, ""));
|
setVideoQuality(normalized.resolution.replace(/p$/i, ""));
|
||||||
const maxReferences = videoProfile.operations.includes("image_to_video") ? videoProfile.references.maxImages : 0;
|
|
||||||
if (attachments.length > maxReferences) setAttachments((current) => current.slice(0, maxReferences));
|
|
||||||
}, [composerPreferencesHydrated, composerPreferencesInitialized, mode, selectedModel, videoProfile]);
|
}, [composerPreferencesHydrated, composerPreferencesInitialized, mode, selectedModel, videoProfile]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const reconciled = reconcileCreationAttachmentLimit(attachments, mentionReferences, maxReferences);
|
const reconciled = mode === "video" && videoReferenceLimits
|
||||||
|
? reconcileCreationAttachmentLimits(attachments, mentionReferences, videoReferenceLimits)
|
||||||
|
: reconcileCreationAttachmentLimit(attachments, mentionReferences, maxReferences);
|
||||||
if (reconciled.attachments === attachments) return;
|
if (reconciled.attachments === attachments) return;
|
||||||
setAttachments(reconciled.attachments);
|
setAttachments(reconciled.attachments);
|
||||||
if (reconciled.removedReferences.length) setPrompt((current) => removeCreationReferenceTokens(current, reconciled.removedReferences));
|
if (reconciled.removedReferences.length) setPrompt((current) => removeCreationReferenceTokens(current, reconciled.removedReferences));
|
||||||
}, [attachments, maxReferences, mentionReferences]);
|
}, [attachments, maxReferences, mentionReferences, mode, videoReferenceLimits]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -387,9 +406,9 @@ export default function CreatePage() {
|
|||||||
const externalLibraryItems = useMemo<AssetLibraryPickerItem[]>(
|
const externalLibraryItems = useMemo<AssetLibraryPickerItem[]>(
|
||||||
() => externalAssetSources.items.map((item) => ({
|
() => externalAssetSources.items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
disabledReason: mode === "image" && item.external?.item.kind !== "image" ? "图片创作仅支持参考图" : undefined,
|
disabledReason: creationLibraryDisabledReason(mode, item.external?.item.kind, videoReferenceLimits),
|
||||||
})),
|
})),
|
||||||
[externalAssetSources.items, mode],
|
[externalAssetSources.items, mode, videoReferenceLimits],
|
||||||
);
|
);
|
||||||
const libraryItems = useMemo<AssetLibraryPickerItem[]>(() => [
|
const libraryItems = useMemo<AssetLibraryPickerItem[]>(() => [
|
||||||
...assets
|
...assets
|
||||||
@@ -401,10 +420,10 @@ export default function CreatePage() {
|
|||||||
kindLabel: asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : "图片",
|
kindLabel: asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : "图片",
|
||||||
asset,
|
asset,
|
||||||
searchText: (asset.tags || []).join(" "),
|
searchText: (asset.tags || []).join(" "),
|
||||||
disabledReason: mode === "image" && asset.kind !== "image" ? "图片创作仅支持参考图" : undefined,
|
disabledReason: creationLibraryDisabledReason(mode, asset.kind, videoReferenceLimits),
|
||||||
})),
|
})),
|
||||||
...externalLibraryItems,
|
...externalLibraryItems,
|
||||||
], [assets, externalLibraryItems, mode]);
|
], [assets, externalLibraryItems, mode, videoReferenceLimits]);
|
||||||
const uploadCreationAsset = async (file: File) => {
|
const uploadCreationAsset = async (file: File) => {
|
||||||
const { uploadImage, uploadMediaFile } = await loadCreationRuntime();
|
const { uploadImage, uploadMediaFile } = await loadCreationRuntime();
|
||||||
if (file.type.startsWith("video/")) {
|
if (file.type.startsWith("video/")) {
|
||||||
@@ -455,7 +474,14 @@ export default function CreatePage() {
|
|||||||
return external ? [creationAttachmentFromExternalAsset(external)] : [];
|
return external ? [creationAttachmentFromExternalAsset(external)] : [];
|
||||||
});
|
});
|
||||||
if (!next.length) return;
|
if (!next.length) return;
|
||||||
setAttachments((current) => [...current.filter((item) => !next.some((candidate) => candidate.id === item.id)), ...next].slice(0, maxReferences));
|
setAttachments((current) => {
|
||||||
|
const candidates = [...current.filter((item) => !next.some((candidate) => candidate.id === item.id)), ...next];
|
||||||
|
const reconciled = mode === "video" && videoReferenceLimits
|
||||||
|
? reconcileCreationAttachmentLimits(candidates, [], videoReferenceLimits)
|
||||||
|
: reconcileCreationAttachmentLimit(candidates, [], maxReferences);
|
||||||
|
if (reconciled.attachments.length < candidates.length) toast.warning("部分素材超出当前模型的参考内容上限,未添加到创作中");
|
||||||
|
return reconciled.attachments;
|
||||||
|
});
|
||||||
setLibraryOpen(false);
|
setLibraryOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -556,7 +582,10 @@ export default function CreatePage() {
|
|||||||
releaseSubmitGate();
|
releaseSubmitGate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (attachments.length > maxReferences) {
|
const reconciledAttachments = mode === "video" && videoReferenceLimits
|
||||||
|
? reconcileCreationAttachmentLimits(attachments, mentionReferences, videoReferenceLimits)
|
||||||
|
: reconcileCreationAttachmentLimit(attachments, mentionReferences, maxReferences);
|
||||||
|
if (reconciledAttachments.attachments !== attachments) {
|
||||||
toast.warning("参考内容正在按当前模型能力调整,请稍后重试");
|
toast.warning("参考内容正在按当前模型能力调整,请稍后重试");
|
||||||
releaseRetryLock();
|
releaseRetryLock();
|
||||||
releaseSubmitGate();
|
releaseSubmitGate();
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ describe("creation library button", () => {
|
|||||||
expect(source).toContain("个素材已上传到素材库并自动选中");
|
expect(source).toContain("个素材已上传到素材库并自动选中");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("视频创作使用同名模型组的全部参考能力开放素材入口", () => {
|
||||||
|
const source = readCreateSource();
|
||||||
|
const workspace = readCreateWorkspaceSource();
|
||||||
|
|
||||||
|
expect(source).toContain('modelGroupReferenceLimits(config, preferredModel || selectedModel, "video")');
|
||||||
|
expect(source).toContain("reconcileCreationAttachmentLimits(attachments, mentionReferences, videoReferenceLimits)");
|
||||||
|
expect(workspace).toContain('props.mode !== "video" || props.maxReferences > 0');
|
||||||
|
expect(workspace).not.toContain('props.videoProfile.operations.includes("image_to_video")');
|
||||||
|
});
|
||||||
|
|
||||||
test("previews prompt reference images without removing them", () => {
|
test("previews prompt reference images without removing them", () => {
|
||||||
const createSource = readCreateWorkspaceSource();
|
const createSource = readCreateWorkspaceSource();
|
||||||
const canvasSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-node-prompt-panel.tsx"), "utf8");
|
const canvasSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-node-prompt-panel.tsx"), "utf8");
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ import { describe, expect, test } from "bun:test";
|
|||||||
|
|
||||||
import { canvasResourceMentionToken } from "../src/lib/canvas/canvas-resource-references";
|
import { canvasResourceMentionToken } from "../src/lib/canvas/canvas-resource-references";
|
||||||
import { creationAttachmentKind, creationFileAccepted, creationMediaAspectRatio, creationUploadAccept, type CreationAttachment } from "../src/pages/create/creation-assets";
|
import { creationAttachmentKind, creationFileAccepted, creationMediaAspectRatio, creationUploadAccept, type CreationAttachment } from "../src/pages/create/creation-assets";
|
||||||
import { buildCreationMentionReferences, displayCreationPrompt, reconcileCreationAttachmentLimit, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences } from "../src/pages/create/creation-references";
|
import {
|
||||||
|
buildCreationMentionReferences,
|
||||||
|
displayCreationPrompt,
|
||||||
|
reconcileCreationAttachmentLimit,
|
||||||
|
reconcileCreationAttachmentLimits,
|
||||||
|
removeCreationReferenceTokens,
|
||||||
|
replaceCreationAttachmentReference,
|
||||||
|
selectedCreationReferences,
|
||||||
|
} from "../src/pages/create/creation-references";
|
||||||
|
|
||||||
function imageAttachment(id: string): CreationAttachment {
|
function imageAttachment(id: string): CreationAttachment {
|
||||||
return {
|
return {
|
||||||
@@ -14,6 +22,10 @@ function imageAttachment(id: string): CreationAttachment {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mediaAttachment(id: string, type: string): CreationAttachment {
|
||||||
|
return { id, name: id, type, url: `blob:${id}`, storageKey: `${id}:key`, previewUrl: "" };
|
||||||
|
}
|
||||||
|
|
||||||
describe("creation references", () => {
|
describe("creation references", () => {
|
||||||
test("removes attachments and prompt tokens beyond the current model limit", () => {
|
test("removes attachments and prompt tokens beyond the current model limit", () => {
|
||||||
const attachments = [imageAttachment("first"), imageAttachment("second"), imageAttachment("third")];
|
const attachments = [imageAttachment("first"), imageAttachment("second"), imageAttachment("third")];
|
||||||
@@ -37,6 +49,23 @@ describe("creation references", () => {
|
|||||||
expect(result.removedReferences).toEqual([]);
|
expect(result.removedReferences).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("视频参考内容分别遵守图片、视频和音频上限", () => {
|
||||||
|
const attachments = [imageAttachment("image-1"), mediaAttachment("video-1", "video/mp4"), imageAttachment("image-2"), mediaAttachment("audio-1", "audio/mpeg"), mediaAttachment("video-2", "video/mp4")];
|
||||||
|
const references = buildCreationMentionReferences([], attachments);
|
||||||
|
const result = reconcileCreationAttachmentLimits(attachments, references, { maxImages: 1, maxVideos: 2, maxAudios: 0 });
|
||||||
|
|
||||||
|
expect(result.attachments.map((attachment) => attachment.id)).toEqual(["image-1", "video-1", "video-2"]);
|
||||||
|
expect(result.removedReferences.map((reference) => reference.attachmentId)).toEqual(["image-2", "audio-1"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("视频参考内容在各类型均未超限时保留原数组", () => {
|
||||||
|
const attachments = [imageAttachment("image"), mediaAttachment("video", "video/mp4"), mediaAttachment("audio", "audio/mpeg")];
|
||||||
|
const result = reconcileCreationAttachmentLimits(attachments, buildCreationMentionReferences([], attachments), { maxImages: 1, maxVideos: 1, maxAudios: 1 });
|
||||||
|
|
||||||
|
expect(result.attachments).toBe(attachments);
|
||||||
|
expect(result.removedReferences).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("文本创作允许媒体和常用文档,图片创作仍只接受图片", () => {
|
test("文本创作允许媒体和常用文档,图片创作仍只接受图片", () => {
|
||||||
expect(creationFileAccepted("text", { name: "story.pdf", type: "application/pdf" })).toBe(true);
|
expect(creationFileAccepted("text", { name: "story.pdf", type: "application/pdf" })).toBe(true);
|
||||||
expect(creationFileAccepted("text", { name: "clip.mp4", type: "video/mp4" })).toBe(true);
|
expect(creationFileAccepted("text", { name: "clip.mp4", type: "video/mp4" })).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user