优化视频创作
This commit is contained in:
@@ -35,18 +35,16 @@ type PresetItem = {
|
||||
desc?: string;
|
||||
starter?: string;
|
||||
cover: string;
|
||||
previewVideo?: string;
|
||||
};
|
||||
|
||||
const VIDEO_PRESETS: PresetItem[] = [
|
||||
{ name: "真实使用演示", category: "commerce", mode: "video", title: "真实使用演示", desc: "从商品真实用途出发,拍清使用过程与卖点证据,避免不合理的夸张演示。", starter: "基于商品的真实用途创作一条可信的使用演示:拍清使用步骤、产品状态和核心卖点,不夸张、不出现不合理的使用场景。", cover: "/assets/prototype/video-oneclick-film-v3.png" },
|
||||
{ name: "剧情反转带货", category: "story", mode: "video", title: "剧情反转带货", desc: "用人物冲突与意外反转建立记忆点,商品承担剧情中的关键作用。", starter: "创作一条有前后反转的剧情带货视频,让商品自然成为解决问题的关键。", cover: "/assets/prototype/photo-1529139574466-a303027c1d8b.jpg" },
|
||||
{ name: "商品拟人广告", category: "commerce", mode: "video", title: "商品拟人广告", desc: "让商品成为故事主角,通过个性、动作和情绪完成趣味表达。", starter: "把商品塑造成有性格的拟人角色,创作一条轻快、有趣的短广告。", cover: "/assets/prototype/photo-1608248543803-ba4f8c70ae0b.jpg" },
|
||||
{ name: "达人口播种草", category: "speaker", mode: "video", title: "达人口播种草", desc: "用真实体验与生活化表达建立信任,适合电商和本地生活内容。", starter: "创作一条真实自然的达人口播种草视频,重点讲清使用场景和核心卖点。", cover: "/assets/prototype/photo-1494790108377-be9c29b29330.jpg" },
|
||||
{ name: "商品图一键成片", category: "commerce", mode: "video", title: "商品图一键成片", desc: "从商品参考图建立镜头语言,自动补齐场景、动作与转场。", starter: "根据我上传的商品图片生成一条节奏清晰的电商短视频。", cover: "/assets/prototype/photo-1556228578-8c89e6adf883.jpg" },
|
||||
{ name: "鱼眼换装", category: "visual", mode: "video", title: "鱼眼换装", desc: "强调近距离透视与连续变装节奏,适合服饰和人物视觉内容。", starter: "创作一条鱼眼镜头风格的连续换装视频,人物和服装需要保持稳定。", cover: "/assets/prototype/photo-1483985988355-763728e1935b.jpg" },
|
||||
{ name: "点触换款", category: "visual", mode: "video", title: "点触换款", desc: "用统一构图和动作触发商品变化,快速展示多个款式或 SKU。", starter: "创作一条通过点击动作连续切换不同商品款式的短视频。", cover: "/assets/prototype/unbranded-running-shoe-remix.png" },
|
||||
{ name: "探店漫游", category: "story", mode: "video", title: "探店漫游", desc: "以空间动线串联门店环境、服务细节与主推项目。", starter: "根据门店素材创作一条有路线感和空间氛围的探店漫游视频。", cover: "/assets/prototype/photo-1497366754035-f200968a6e72.jpg" },
|
||||
{ name: "品牌质感大片", category: "visual", mode: "video", title: "品牌质感大片", desc: "通过统一的光影、材质与镜头节奏建立更强的品牌识别。", starter: "创作一条强调光影、材质和品牌气质的高级感商品短片。", cover: "/assets/prototype/video-free-cinematic.png" },
|
||||
{ name: "痛点解决演示", category: "commerce", mode: "video", title: "痛点解决演示", desc: "从真实痛点到正常使用,再用看得见的过程证明商品价值。", starter: "基于商品的真实用途创作一条痛点解决演示:先让人看见困扰,再拍清使用步骤和真实结果,不夸张、不出现不合理的测试。", cover: "/assets/video-presets/pain-point-demo.jpg", previewVideo: "/assets/video-presets/pain-point-demo.mp4" },
|
||||
{ name: "剧情反转带货", category: "story", mode: "video", title: "剧情反转带货", desc: "用人物冲突与意外反转建立记忆点,商品承担剧情中的关键作用。", starter: "创作一条有前后反转的剧情带货视频,让商品自然成为解决问题的关键。", cover: "/assets/video-presets/plot-twist-commerce.jpg", previewVideo: "/assets/video-presets/plot-twist-commerce.mp4" },
|
||||
{ name: "达人口播种草", category: "speaker", mode: "video", title: "达人口播种草", desc: "用真实体验与生活化表达建立信任,适合电商和本地生活内容。", starter: "创作一条真实自然的达人口播种草视频,重点讲清使用场景和核心卖点。", cover: "/assets/video-presets/creator-recommendation.jpg", previewVideo: "/assets/video-presets/creator-recommendation.mp4" },
|
||||
{ name: "鱼眼换装", category: "visual", mode: "video", title: "鱼眼换装", desc: "强调近距离透视与连续变装节奏,适合服饰和人物视觉内容。", starter: "创作一条鱼眼镜头风格的连续换装视频,人物和服装需要保持稳定。", cover: "/assets/video-presets/rhythm-outfit-change.jpg", previewVideo: "/assets/video-presets/rhythm-outfit-change.mp4" },
|
||||
{ name: "多色商品换款", category: "visual", mode: "video", title: "多色商品换款", desc: "统一商品比例与机位,用动作连续展示不同颜色和款式。", starter: "创作一条多色商品换款视频:统一构图展示不同颜色或款式,用明确动作触发切换,最后给出全款总览。", cover: "/assets/video-presets/multi-sku-switch.jpg", previewVideo: "/assets/video-presets/multi-sku-switch.mp4" },
|
||||
{ name: "AI 宠物拟人", category: "story", mode: "video", title: "AI 宠物拟人", desc: "让宠物角色参与有趣小剧情,同时保留商品真实结构与用途。", starter: "创作一条 AI 宠物拟人短片:宠物有明确性格和动作,商品以真实结构和正常用法自然参与剧情。", cover: "/assets/video-presets/ai-pet-personification.jpg", previewVideo: "/assets/video-presets/ai-pet-personification.mp4" },
|
||||
];
|
||||
|
||||
const IMAGE_PRESETS: PresetItem[] = [
|
||||
@@ -64,19 +62,6 @@ const IMAGE_PRESETS: PresetItem[] = [
|
||||
{ name: "新中式", category: "style", mode: "image", title: "新中式", cover: "/assets/image-presets/neo-chinese.png" },
|
||||
];
|
||||
|
||||
const VIDEO_FILTERS = [
|
||||
{ key: "all", label: "全部" },
|
||||
{ key: "story", label: "剧情" },
|
||||
{ key: "commerce", label: "电商" },
|
||||
{ key: "speaker", label: "口播" },
|
||||
{ key: "visual", label: "视觉" },
|
||||
];
|
||||
|
||||
const IMAGE_FILTERS = [
|
||||
{ key: "all", label: "全部" },
|
||||
{ key: "style", label: "风格" },
|
||||
];
|
||||
|
||||
const MENTION_REF_LIMIT = 5;
|
||||
|
||||
const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: typeof ImageIcon }> = [
|
||||
@@ -134,7 +119,6 @@ export function OmniCreatePage({
|
||||
const [typeLabels, setTypeLabels] = useState<Record<string, string>>({});
|
||||
const [pendingRefs, setPendingRefs] = useState<CreationRef[]>([]);
|
||||
const [previewCase, setPreviewCase] = useState<PresetItem | null>(null);
|
||||
const [activeCategory, setActiveCategory] = useState("all");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const toolsRef = useRef<HTMLDivElement>(null);
|
||||
const [starting, setStarting] = useState(false);
|
||||
@@ -155,7 +139,6 @@ export function OmniCreatePage({
|
||||
setRatio("9:16");
|
||||
setDuration("智能时长");
|
||||
}
|
||||
setActiveCategory("all");
|
||||
setSelectedCase((prev) => (prev && prev.mode !== outputMode ? null : prev));
|
||||
}, [outputMode, modelConfigs]);
|
||||
|
||||
@@ -170,17 +153,13 @@ export function OmniCreatePage({
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, []);
|
||||
|
||||
const filters = outputMode === "video" ? VIDEO_FILTERS : IMAGE_FILTERS;
|
||||
const presets = useMemo(() => {
|
||||
const list = outputMode === "video" ? VIDEO_PRESETS : IMAGE_PRESETS;
|
||||
if (activeCategory === "all") return list;
|
||||
return list.filter((item) => item.category === activeCategory);
|
||||
}, [outputMode, activeCategory]);
|
||||
return outputMode === "video" ? VIDEO_PRESETS : IMAGE_PRESETS;
|
||||
}, [outputMode]);
|
||||
|
||||
const applyPreset = (preset: PresetItem) => {
|
||||
setSelectedCase(preset);
|
||||
// 图片预设只作为隐藏的生成风格,用户输入区保留自己的创作描述,不把内部 Prompt 展示出来。
|
||||
if (preset.mode !== "image" && preset.starter) setPrompt(preset.starter);
|
||||
// 预设只作为后台的创作方向。无论图片还是视频,都不把内部生成指令塞进用户输入区。
|
||||
setPreviewCase(null);
|
||||
onNotify?.("info", `已选择预设:${preset.name}`);
|
||||
};
|
||||
@@ -460,32 +439,12 @@ export function OmniCreatePage({
|
||||
|
||||
<section
|
||||
className={`omni-case-library${outputMode === "image" ? " is-image-presets" : ""}`}
|
||||
aria-labelledby={outputMode === "video" ? "omniCaseTitle" : undefined}
|
||||
aria-label={outputMode === "image" ? "图片风格预设" : undefined}
|
||||
aria-label={outputMode === "image" ? "图片风格预设" : "视频创作预设"}
|
||||
>
|
||||
{outputMode === "video" ? (
|
||||
<div className="omni-case-head">
|
||||
<strong className="omni-case-head-label" id="omniCaseTitle">预设分类</strong>
|
||||
<div className="omni-case-filters" role="tablist" aria-label="预设筛选">
|
||||
{filters.map((filter) => (
|
||||
<button
|
||||
type="button"
|
||||
key={filter.key}
|
||||
className={activeCategory === filter.key ? "active" : ""}
|
||||
onClick={() => setActiveCategory(filter.key)}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{outputMode === "image" ? (
|
||||
<div className="omni-image-preset-intro">
|
||||
<strong>从一个风格开始</strong>
|
||||
<span>可选,也可以直接描述你想要的画面。</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="omni-preset-intro">
|
||||
<strong>{outputMode === "video" ? "从一个视频方向开始" : "从一个风格开始"}</strong>
|
||||
<span>{outputMode === "video" ? "可选,也可以直接描述你想拍的视频。" : "可选,也可以直接描述你想要的画面。"}</span>
|
||||
</div>
|
||||
<div className="omni-case-grid">
|
||||
{presets.map((card) => (
|
||||
<button
|
||||
@@ -493,16 +452,39 @@ export function OmniCreatePage({
|
||||
className={`omni-case-card${selectedCase?.name === card.name ? " active" : ""}`}
|
||||
data-mode={card.mode}
|
||||
key={card.name}
|
||||
onMouseEnter={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
void video?.play().catch(() => undefined);
|
||||
}}
|
||||
onMouseLeave={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}}
|
||||
onFocus={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
void video?.play().catch(() => undefined);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const video = event.currentTarget.querySelector<HTMLVideoElement>("video");
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}}
|
||||
onClick={() => {
|
||||
if (card.mode === "image") applyPreset(card);
|
||||
else setPreviewCase(card);
|
||||
}}
|
||||
>
|
||||
<span className="omni-case-visual">
|
||||
<img src={card.cover} alt="" />
|
||||
<span className="omni-case-play">
|
||||
{card.mode === "video" ? <Play className="lucide-play" /> : <ImageIcon />}
|
||||
</span>
|
||||
{card.previewVideo ? (
|
||||
<video muted loop playsInline preload="metadata" poster={card.cover} aria-label={`${card.title} 样片预览`}>
|
||||
<source src={card.previewVideo} type="video/mp4" />
|
||||
</video>
|
||||
) : (
|
||||
<img src={card.cover} alt="" />
|
||||
)}
|
||||
</span>
|
||||
<span className="omni-case-copy">
|
||||
<strong>{card.title}</strong>
|
||||
@@ -530,8 +512,15 @@ export function OmniCreatePage({
|
||||
<X />
|
||||
</button>
|
||||
<div className="omni-preset-media-frame">
|
||||
<img src={previewCase.cover} alt="预设示例" />
|
||||
{previewCase.mode === "video" && (
|
||||
{previewCase.previewVideo ? (
|
||||
<video controls playsInline preload="metadata" poster={previewCase.cover}>
|
||||
<source src={previewCase.previewVideo} type="video/mp4" />
|
||||
当前浏览器不支持播放该示例视频。
|
||||
</video>
|
||||
) : (
|
||||
<img src={previewCase.cover} alt="预设示例" />
|
||||
)}
|
||||
{previewCase.mode === "video" && !previewCase.previewVideo && (
|
||||
<span className="omni-preset-video-mark">
|
||||
<Play />
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user