fix(core): 删脚本分镜联动删故事板帧 + 演员库自取 person + 分镜编辑行抽组件
- views.py: delete-script-segment 同步删对应 StoryboardFrame(原 SET_NULL 留孤儿帧致故事板仍显示已删的场),并重排剩余帧 sort_order - actor-library.tsx: 演员库改自取 person 资产(全局 assets 已懒加载不再全量预载,否则演员库全空),保存后 reload 即时入列 - pipeline.tsx: 抽出 EditableShotField 组件、PromptBox 参数化 - 附 perf-report 产物 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,8 @@ from .models import (
|
||||
ProjectStage,
|
||||
ScriptSegment,
|
||||
ScriptVersion,
|
||||
StoryboardFrame,
|
||||
StoryboardVersion,
|
||||
SubtitleTrack,
|
||||
Timeline,
|
||||
TimelineClip,
|
||||
@@ -429,6 +431,15 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
script = segment.script_version
|
||||
if script.segments.count() <= 1:
|
||||
return Response({"detail": "至少保留一个分镜"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# 删本场脚本时,它对应的故事板分镜帧也一起删 —— frame.script_segment 是 SET_NULL,
|
||||
# 不主动删会留下孤儿帧,故事板里仍显示这一(已删的)场。各版本受影响的帧统一收口后重排 sort_order。
|
||||
affected_storyboards = list(StoryboardVersion.objects.filter(frames__script_segment=segment).distinct())
|
||||
StoryboardFrame.objects.filter(script_segment=segment).delete()
|
||||
for sb in affected_storyboards:
|
||||
for index, frame in enumerate(sb.frames.order_by("sort_order")):
|
||||
if frame.sort_order != index:
|
||||
frame.sort_order = index
|
||||
frame.save(update_fields=["sort_order", "updated_at"])
|
||||
segment.delete()
|
||||
for index, seg in enumerate(script.segments.order_by("sort_order")):
|
||||
if seg.sort_order != index:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import type { CSSProperties } from "react";
|
||||
import { api } from "../api";
|
||||
import type { Asset } from "../types";
|
||||
import { useBodyScrollLock, MediaLightbox } from "./overlays";
|
||||
|
||||
@@ -52,8 +53,22 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
|
||||
setCandidates([]); setPicked(null); setActorName(""); setTriReady(false);
|
||||
}, [open, initialStudio]);
|
||||
|
||||
// person 类资产 → 演员;按 preset / mine 分两 tab
|
||||
const people = useMemo(() => assets.filter((a) => a.category === "person" && previewOf(a)), [assets]);
|
||||
// 演员库自取 person 资产:全局 assets 已不再全量预载(按需懒加载),不能再依赖传入的 assets prop,
|
||||
// 否则平台预设/我的演员全空。打开时拉一页(团队 person 资产),保存后 reload 让新人物即时入列。
|
||||
const [fetched, setFetched] = useState<Asset[]>([]);
|
||||
const reload = useCallback(async () => {
|
||||
const res = await api.assetsPage({ category: "person", pageSize: 200, ordering: "-created_at" }).catch(() => null);
|
||||
setFetched(res?.results ?? []);
|
||||
}, []);
|
||||
useEffect(() => { if (open) void reload(); }, [open, reload]);
|
||||
|
||||
// person 类资产 → 演员;按 preset / mine 分两 tab。合并 prop(兼容旧调用)+ 自取,按 id 去重。
|
||||
const source = useMemo(() => {
|
||||
const map = new Map<string, Asset>();
|
||||
for (const a of [...assets, ...fetched]) map.set(a.id, a);
|
||||
return [...map.values()];
|
||||
}, [assets, fetched]);
|
||||
const people = useMemo(() => source.filter((a) => a.category === "person" && previewOf(a)), [source]);
|
||||
const list = people.filter((a) => (tab === "preset" ? isPreset(a) : !isPreset(a)));
|
||||
|
||||
// 选某张候选立绘 → 进右侧栏(预填名字,清三视图标志)
|
||||
@@ -99,6 +114,7 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
|
||||
const name = actorName.trim();
|
||||
if (name && name !== picked.name && onRename) await onRename(picked.id, name);
|
||||
onRefresh();
|
||||
void reload(); // 新保存的人物即时进「我的演员」列表(自取,不依赖父级全局 assets)
|
||||
setStudio(false);
|
||||
setCandidates([]); setPicked(null); setActorName(""); setTriReady(false);
|
||||
setTab("mine");
|
||||
|
||||
@@ -564,11 +564,7 @@
|
||||
.draft-shot-input:focus { border-color: var(--heat); box-shadow: 0 0 0 2px var(--heat-12); }
|
||||
.draft-shot-input::placeholder { color: var(--black-alpha-32); }
|
||||
|
||||
/* ── 行38 · 资产卡可编辑提示词 + 重跑/替换 ── */
|
||||
.asset-prompt-edit { width: 100%; margin-top: 8px; font-family: var(--font-mono); font-size: 12px; line-height: 1.55; letter-spacing: .01em; color: var(--black-alpha-72); background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 8px 10px; outline: none; resize: vertical; min-height: 56px; transition: border-color var(--t-base), background var(--t-base), box-shadow var(--t-base); }
|
||||
.asset-prompt-edit:hover { border-color: var(--heat-20); }
|
||||
.asset-prompt-edit:focus { border-color: var(--heat); background: var(--surface); color: var(--accent-black); box-shadow: 0 0 0 3px var(--heat-12); }
|
||||
.asset-prompt-edit::placeholder { color: var(--black-alpha-32); }
|
||||
/* ── 行38 · 资产卡可编辑提示词 + 重跑/替换 ──(.asset-prompt-edit 移到顶层:演员库弹窗 portal 到 body,嵌在 .pipeline-page 内会失效) */
|
||||
.asset-card-actions { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
|
||||
.asset-card-actions .btn { display: inline-flex; align-items: center; }
|
||||
|
||||
@@ -680,6 +676,11 @@
|
||||
@keyframes edXfadeFlash { from { opacity: 0.72; } to { opacity: 0; } }
|
||||
|
||||
/* ── 流程步骤4 · 演员库覆盖层(portal 到 body,顶层选择器才生效)── */
|
||||
/* 资产卡/演员库工作台的提示词输入框:顶层定义,页面内(资产卡)与 portal 弹窗(添加人物工作台)都生效 */
|
||||
.asset-prompt-edit { width: 100%; margin-top: 8px; font-family: var(--font-mono); font-size: 12px; line-height: 1.55; letter-spacing: .01em; color: var(--black-alpha-72); background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 8px 10px; outline: none; resize: vertical; min-height: 56px; transition: border-color var(--t-base), background var(--t-base), box-shadow var(--t-base); }
|
||||
.asset-prompt-edit:hover { border-color: var(--heat-20); }
|
||||
.asset-prompt-edit:focus { border-color: var(--heat); background: var(--surface); color: var(--accent-black); box-shadow: 0 0 0 3px var(--heat-12); }
|
||||
.asset-prompt-edit::placeholder { color: var(--black-alpha-32); }
|
||||
.actorlib-bg { position: fixed; inset: 0; background: rgba(0,0,0,.42); z-index: 1020; display: flex; align-items: center; justify-content: center; padding: 40px; }
|
||||
.actorlib { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(920px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
|
||||
.actorlib-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
|
||||
|
||||
@@ -270,10 +270,10 @@ function AddTagInline({ onAdd, placeholder, ariaLabel }: { onAdd?: (value: strin
|
||||
// DOM,React 不再 reconcile 其子节点。受控写法(把 state 当 children 回灌)在输入触发换行时,浏览器会
|
||||
// 往 contentEditable 里插 <div>/<br>,React 拿单个文本子节点去对账就会 removeChild 崩溃 → 整页白屏。
|
||||
// 读取走 onInput 写回上层 state(供「生成/重跑」带上),与渲染解耦,既能编辑又不崩。
|
||||
function PromptBox({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||
function PromptBox({ value, onChange, className = "prompt-box", id, ariaLabel, stop = true }: { value: string; onChange: (v: string) => void; className?: string; id?: string; ariaLabel?: string; stop?: boolean }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
// 只挂载时灌初值;后续用户编辑由 DOM 自己维护,不受 value 变化驱动(避免重渲染打断输入/白屏)。
|
||||
// 切换实体/草稿时上层用 key 强制重挂载,会重新走这次初值灌入,所以不需要把 value 进依赖。
|
||||
// 切换实体/草稿/版本时上层用 key 强制重挂载,会重新走这次初值灌入,所以不需要把 value 进依赖。
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.textContent !== value) ref.current.textContent = value;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -281,16 +281,50 @@ function PromptBox({ value, onChange }: { value: string; onChange: (v: string) =
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="prompt-box"
|
||||
className={className}
|
||||
id={id}
|
||||
role={ariaLabel ? "textbox" : undefined}
|
||||
aria-label={ariaLabel}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
data-stop
|
||||
{...(stop ? { "data-stop": true } : {})}
|
||||
onInput={(e) => onChange(e.currentTarget.textContent || "")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 分镜「旁白 / 画面」可编辑行:同样**非受控** contentEditable,落库走 onBlur(回车=失焦提交)。
|
||||
// 受控写法(把 narration 当 children)在提交后父组件用旧值重渲染时,React 会把 DOM 文本重置回旧值 →
|
||||
// 「按回车后变成空白」。这里不放受控子节点:初值/外部改动(切版本、AI 改稿)由 effect 同步进 DOM,
|
||||
// 但**正在编辑(聚焦中)时绝不覆盖**,避免打断输入。
|
||||
function EditableShotField({ value, placeholder, onCommit }: { value: string; placeholder: string; onCommit: (text: string) => void }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || document.activeElement === el) return; // 聚焦中=用户在打字,别覆盖
|
||||
if (el.textContent !== value) el.textContent = value;
|
||||
}, [value]);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="shot-v"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
data-placeholder={placeholder}
|
||||
data-empty={value ? undefined : "true"}
|
||||
onFocus={(e) => { e.currentTarget.removeAttribute("data-empty"); }}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); e.currentTarget.blur(); } }}
|
||||
onBlur={(e) => {
|
||||
const text = (e.currentTarget.textContent || "").trim();
|
||||
if (!text) e.currentTarget.setAttribute("data-empty", "true");
|
||||
if (text !== value) onCommit(text);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 行34 · 「添加分镜」插入的本地可编辑空白卡片:输入旁白/画面,提交后落库
|
||||
function DraftShotCard({ draft, onCommit, onCancel }: {
|
||||
draft: { id: string; afterId: string | null; narration: string; visual: string };
|
||||
@@ -2071,39 +2105,19 @@ export function PipelinePage(props: {
|
||||
</div>
|
||||
<div className="shot-row">
|
||||
<span className="shot-k">旁白</span>
|
||||
<div
|
||||
className="shot-v"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
data-placeholder="(旁白)点击编辑"
|
||||
data-empty={narration ? undefined : "true"}
|
||||
onFocus={(event) => { event.currentTarget.removeAttribute("data-empty"); }}
|
||||
onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault(); event.currentTarget.blur(); } }}
|
||||
onBlur={(event) => {
|
||||
const text = (event.currentTarget.textContent || "").trim();
|
||||
if (!text) event.currentTarget.setAttribute("data-empty", "true");
|
||||
if (text !== narration) void onUpdateShot({ segment_id: shot.id, narration: text });
|
||||
}}
|
||||
>{narration}</div>
|
||||
<EditableShotField
|
||||
value={narration}
|
||||
placeholder="(旁白)点击编辑"
|
||||
onCommit={(text) => { void onUpdateShot({ segment_id: shot.id, narration: text }); }}
|
||||
/>
|
||||
</div>
|
||||
<div className="shot-row">
|
||||
<span className="shot-k">画面</span>
|
||||
<div
|
||||
className="shot-v"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
data-placeholder="(画面描述)点击编辑"
|
||||
data-empty={visualShown ? undefined : "true"}
|
||||
onFocus={(event) => { event.currentTarget.removeAttribute("data-empty"); }}
|
||||
onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault(); event.currentTarget.blur(); } }}
|
||||
onBlur={(event) => {
|
||||
const text = (event.currentTarget.textContent || "").trim();
|
||||
if (!text) event.currentTarget.setAttribute("data-empty", "true");
|
||||
if (text !== visualShown) void onUpdateShot({ segment_id: shot.id, visual_prompt: text });
|
||||
}}
|
||||
>{visualShown}</div>
|
||||
<EditableShotField
|
||||
value={visualShown}
|
||||
placeholder="(画面描述)点击编辑"
|
||||
onCommit={(text) => { void onUpdateShot({ segment_id: shot.id, visual_prompt: text }); }}
|
||||
/>
|
||||
</div>
|
||||
{/* ScriptDraft 结构化字段:镜型(钩子/痛点/卖点/CTA)+ 商品露出方式(只读展示) */}
|
||||
{(shot.role || shot.product_exposure) ? (
|
||||
@@ -2298,7 +2312,13 @@ export function PipelinePage(props: {
|
||||
const previewTriAsset = (triPreviewId && productVersions.includes(triPreviewId)) ? triPreviewId : adoptedTriAsset;
|
||||
const hasTriView = productVersions.length > 0;
|
||||
const triPanelShow = triPanelOpen || hasTriView || triGenerating;
|
||||
const productAssetUrl = assetUrl(productCover); // 商品卡显示商品主图,三视图在右侧面板
|
||||
// 商品主图:优先用序列化器内嵌的 preview_url(全局 assets 已不再全量取,assetUrl 反查会落空),
|
||||
// 依次 封面 → 主图 → 首图,最后才回退旧的全局反查。
|
||||
const productAssetUrl =
|
||||
productRecord?.cover_preview_url
|
||||
|| productRecord?.images?.find((img) => img.is_primary)?.preview_url
|
||||
|| productRecord?.images?.[0]?.preview_url
|
||||
|| assetUrl(productCover); // 商品卡显示商品主图,三视图在右侧面板
|
||||
const triViewGen = `${productName} 商品三视图,从左到右:正面 / 侧面 / 背面,统一光照,白色背景,16:9`;
|
||||
const runProductTri = () => { setTriPanelOpen(true); setTriPreviewId(null); void genBaseAsset("product", triViewGen, undefined, "tri:product"); };
|
||||
return (
|
||||
@@ -2522,23 +2542,8 @@ export function PipelinePage(props: {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* 人物区末尾「添加人物」卡:点开「添加人物工作台」(studio) */}
|
||||
{kind === "person" && (
|
||||
<div className="asset-card-2" data-asset-kind="person" data-stop role="button" tabIndex={0}
|
||||
title="打开添加人物工作台" style={{ cursor: "pointer" }}
|
||||
onClick={() => setActorLib({ mode: "browse", studio: true })}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setActorLib({ mode: "browse", studio: true }); } }}>
|
||||
<div className="placeholder thumb-2" style={{ flexDirection: "column", gap: 8 }}>
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14M5 12h14" /></svg>
|
||||
<span className="ph-frame">添加人物</span>
|
||||
</div>
|
||||
<div className="body-2">
|
||||
<div className="hstack"><strong style={{ fontSize: "13.5px" }}>添加人物</strong><span className="spacer"></span><span className="pill neutral"><span className="dot"></span>工作台</span></div>
|
||||
<div className="muted mono" style={{ fontSize: 12, marginTop: 6 }}>// AI 生成 / 本地上传 → 三视图 → 入库</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{entities.length === 0 && pendingTags.length === 0 && (kind !== "scene" || sceneDrafts.length === 0) && kind !== "person" && (
|
||||
{/* 添加人物入口统一走右上「+ 新增人物」按钮(原末尾常驻卡与之重复,已移除) */}
|
||||
{entities.length === 0 && pendingTags.length === 0 && (kind !== "scene" || sceneDrafts.length === 0) && (
|
||||
<div className="placeholder" style={{ gridColumn: "1 / -1", minHeight: "120px", flexDirection: "column", gap: "10px" }}>
|
||||
<span className="ph-frame">// 暂无{KIND_LABEL[kind]}资产 · 点右上「新增{KIND_LABEL[kind]}」</span>
|
||||
</div>
|
||||
@@ -2616,16 +2621,15 @@ export function PipelinePage(props: {
|
||||
<div className="note-copy"><strong>仅支持整张重跑</strong> · 不能局部改某一镜。如需调单镜,先在 <a href="#stage-1" onClick={(event) => { event.preventDefault(); goStage(1); }}>Stage 1 脚本</a> 改镜头描述,再回此处整张重跑。</div>
|
||||
</div>
|
||||
<div className="muted mono" style={{ fontSize: "12px", fontWeight: 500, marginBottom: "6px", letterSpacing: ".04em" }}>// 整张提示词(重跑时生效,可编辑)</div>
|
||||
{/* key 跟版本走:切版本重新种子;onInput 实时回写 state → 整张重跑用的就是你编辑后的文本 */}
|
||||
<div
|
||||
className="prompt-edit"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
id="sb-prompt-edit"
|
||||
{/* key 跟版本走:切版本重新种子;onChange 实时回写 state → 整张重跑用的就是你编辑后的文本 */}
|
||||
<PromptBox
|
||||
key={displayedStoryboard?.id || "no-version"}
|
||||
onInput={(event) => setStoryboardPrompt((event.currentTarget.textContent || "").trim())}
|
||||
>{displayedStoryboard?.prompt || SB_PROMPT_DEFAULT}</div>
|
||||
className="prompt-edit"
|
||||
id="sb-prompt-edit"
|
||||
stop={false}
|
||||
value={displayedStoryboard?.prompt || SB_PROMPT_DEFAULT}
|
||||
onChange={(v) => setStoryboardPrompt(v.trim())}
|
||||
/>
|
||||
<div className="sb-stage-actions">
|
||||
<button className="pill-cta heat" type="button" id="sb-rerun-btn" disabled={loading} onClick={() => onGenerateStoryboard(storyboardPrompt || SB_PROMPT_DEFAULT)}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12a8 8 0 0 1 14-5.5L21 9" /><path d="M21 4v5h-5" /><path d="M20 12a8 8 0 0 1-14 5.5L3 15" /><path d="M3 20v-5h5" /></svg>
|
||||
@@ -2651,9 +2655,18 @@ export function PipelinePage(props: {
|
||||
<div className="divider" style={{ marginTop: "16px" }}></div>
|
||||
<div className="muted mono" style={{ fontSize: "12px", fontWeight: 500, marginBottom: "8px", letterSpacing: ".04em" }}>// 绑定的资产</div>
|
||||
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }} id="sb-bound-assets">
|
||||
{groups.filter((g) => g.adopted_asset).length ? groups.filter((g) => g.adopted_asset).map((g) => (
|
||||
<span className="asset-tag" key={g.id}><span className="dotc"></span>{assetName(g.adopted_asset) || KIND_LABEL[g.kind] || g.kind}({KIND_LABEL[g.kind] || g.kind})</span>
|
||||
)) : <span className="muted-2 mono" style={{ fontSize: "12px" }}>// 暂无绑定资产</span>}
|
||||
{(() => {
|
||||
// 与 Stage 2 同源:用 metadata.label 取名(不依赖全局 assets 列表),排除三视图组并按名字去重,
|
||||
// 否则同一角色的立绘组+三视图组+多版本会各算一个,且名字会退化成「人物(人物)」。
|
||||
const bound = [
|
||||
...groupsByKind("product").filter((g) => g.adopted_asset).map((g) => ({ id: g.id, name: (g.metadata?.label || "").trim() || KIND_LABEL.product, kind: "product" as const })),
|
||||
...buildEntities("person").filter((e) => e.group.adopted_asset).map((e) => ({ id: e.key, name: e.name, kind: "person" as const })),
|
||||
...buildEntities("scene").filter((e) => e.group.adopted_asset).map((e) => ({ id: e.key, name: e.name, kind: "scene" as const })),
|
||||
];
|
||||
return bound.length ? bound.map((b) => (
|
||||
<span className="asset-tag" key={b.id}><span className="dotc"></span>{b.name}({KIND_LABEL[b.kind]})</span>
|
||||
)) : <span className="muted-2 mono" style={{ fontSize: "12px" }}>// 暂无绑定资产</span>;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3177,16 +3190,14 @@ export function PipelinePage(props: {
|
||||
<span className="label">// 本场提示词(重跑生效,可编辑)</span>
|
||||
<span className="mono" style={{ fontSize: "12px", color: "var(--black-alpha-48)" }}>系统会自动织入本镜旁白与参考图</span>
|
||||
</div>
|
||||
<div
|
||||
className="vd-prompt-edit"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
role="textbox"
|
||||
aria-label="视频提示词"
|
||||
<PromptBox
|
||||
key={vdSeg.id}
|
||||
onInput={(event) => setVdPrompt((event.currentTarget.textContent || "").trim())}
|
||||
>{`${videoPrompt} 第 ${vdSeg.sort_order + 1} 段,时长 ${vdSeg.target_duration_seconds} 秒`}</div>
|
||||
className="vd-prompt-edit"
|
||||
ariaLabel="视频提示词"
|
||||
stop={false}
|
||||
value={`${videoPrompt} 第 ${vdSeg.sort_order + 1} 段,时长 ${vdSeg.target_duration_seconds} 秒`}
|
||||
onChange={(v) => setVdPrompt(v.trim())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"ts": "2026-06-17T10:24:13.586Z",
|
||||
"ts": "2026-06-17T10:57:57.302Z",
|
||||
"base": "http://127.0.0.1:5174",
|
||||
"apiChecks": [
|
||||
{
|
||||
"name": "page_size 生效 /api/assets/",
|
||||
"pass": true,
|
||||
"detail": "count=227 返回=200 next=有"
|
||||
"detail": "count=239 返回=200 next=有"
|
||||
},
|
||||
{
|
||||
"name": "page_size 生效 /api/products/",
|
||||
@@ -15,31 +15,31 @@
|
||||
{
|
||||
"name": "page_size 生效 /api/projects/",
|
||||
"pass": true,
|
||||
"detail": "count=19 返回=19 next=无"
|
||||
"detail": "count=21 返回=21 next=无"
|
||||
},
|
||||
{
|
||||
"name": "延迟 /api/assets/?page_size=200",
|
||||
"pass": false,
|
||||
"pass": true,
|
||||
"soft": true,
|
||||
"detail": "2235ms / 参考 1200ms (HTTP 200)"
|
||||
"detail": "720ms / 参考 1200ms (HTTP 200)"
|
||||
},
|
||||
{
|
||||
"name": "延迟 /api/products/?page_size=200",
|
||||
"pass": true,
|
||||
"soft": true,
|
||||
"detail": "962ms / 参考 1200ms (HTTP 200)"
|
||||
"detail": "597ms / 参考 1200ms (HTTP 200)"
|
||||
},
|
||||
{
|
||||
"name": "延迟 /api/projects/?page_size=200",
|
||||
"pass": false,
|
||||
"soft": true,
|
||||
"detail": "2703ms / 参考 1200ms (HTTP 200)"
|
||||
"detail": "2004ms / 参考 1200ms (HTTP 200)"
|
||||
},
|
||||
{
|
||||
"name": "延迟 /api/ops/notifications/?page_size=100",
|
||||
"pass": false,
|
||||
"soft": true,
|
||||
"detail": "1959ms / 参考 1200ms (HTTP 200)"
|
||||
"detail": "1228ms / 参考 1200ms (HTTP 200)"
|
||||
}
|
||||
],
|
||||
"pageFailed": 0,
|
||||
@@ -47,9 +47,9 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"route": "http://127.0.0.1:5174/dashboard",
|
||||
"unique": 9,
|
||||
"unique": 6,
|
||||
"budget": 14,
|
||||
"rawRequests": 11,
|
||||
"rawRequests": 7,
|
||||
"waterfall": 0,
|
||||
"duplicates": [],
|
||||
"failed": [],
|
||||
@@ -58,15 +58,11 @@
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=100",
|
||||
"GET /api/ops/notifications/?page_size=1",
|
||||
"GET /api/assets/summary/"
|
||||
],
|
||||
"elapsedMs": 6445,
|
||||
"elapsedMs": 5557,
|
||||
"problems": [],
|
||||
"warnings": [],
|
||||
"pass": true
|
||||
@@ -74,9 +70,9 @@
|
||||
{
|
||||
"name": "products",
|
||||
"route": "http://127.0.0.1:5174/products",
|
||||
"unique": 8,
|
||||
"unique": 5,
|
||||
"budget": 6,
|
||||
"rawRequests": 10,
|
||||
"rawRequests": 6,
|
||||
"waterfall": 0,
|
||||
"duplicates": [],
|
||||
"failed": [],
|
||||
@@ -85,26 +81,20 @@
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=100"
|
||||
"GET /api/ops/notifications/?page_size=1"
|
||||
],
|
||||
"elapsedMs": 3892,
|
||||
"elapsedMs": 3488,
|
||||
"problems": [],
|
||||
"warnings": [
|
||||
"首屏接口数 8 > 预算 6(架构性过量拉取,待决策)"
|
||||
],
|
||||
"warnings": [],
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "projects",
|
||||
"route": "http://127.0.0.1:5174/projects",
|
||||
"unique": 8,
|
||||
"unique": 5,
|
||||
"budget": 6,
|
||||
"rawRequests": 10,
|
||||
"rawRequests": 6,
|
||||
"waterfall": 0,
|
||||
"duplicates": [],
|
||||
"failed": [],
|
||||
@@ -113,26 +103,20 @@
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=100"
|
||||
"GET /api/ops/notifications/?page_size=1"
|
||||
],
|
||||
"elapsedMs": 3765,
|
||||
"elapsedMs": 3502,
|
||||
"problems": [],
|
||||
"warnings": [
|
||||
"首屏接口数 8 > 预算 6(架构性过量拉取,待决策)"
|
||||
],
|
||||
"warnings": [],
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "library",
|
||||
"route": "http://127.0.0.1:5174/library",
|
||||
"unique": 11,
|
||||
"unique": 8,
|
||||
"budget": 8,
|
||||
"rawRequests": 14,
|
||||
"rawRequests": 10,
|
||||
"waterfall": 0,
|
||||
"duplicates": [
|
||||
{
|
||||
@@ -147,20 +131,14 @@
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=100",
|
||||
"GET /api/ops/notifications/?page_size=1",
|
||||
"GET /api/assets/summary/",
|
||||
"GET /api/assets/facets/?meta_keys=gender,age,role&tab=people"
|
||||
],
|
||||
"elapsedMs": 6012,
|
||||
"elapsedMs": 5728,
|
||||
"problems": [],
|
||||
"warnings": [
|
||||
"首屏接口数 11 > 预算 8(架构性过量拉取,待决策)"
|
||||
],
|
||||
"warnings": [],
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
@@ -168,28 +146,22 @@
|
||||
"route": "http://127.0.0.1:5174/account",
|
||||
"unique": 8,
|
||||
"budget": 8,
|
||||
"rawRequests": 11,
|
||||
"rawRequests": 9,
|
||||
"waterfall": 0,
|
||||
"duplicates": [
|
||||
{
|
||||
"key": "GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"n": 2
|
||||
}
|
||||
],
|
||||
"duplicates": [],
|
||||
"failed": [],
|
||||
"breakdown": [
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10 ×2",
|
||||
"GET /api/auth/me/",
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ops/notifications/?page_size=1",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=100"
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10"
|
||||
],
|
||||
"elapsedMs": 5753,
|
||||
"elapsedMs": 5743,
|
||||
"problems": [],
|
||||
"warnings": [],
|
||||
"pass": true
|
||||
@@ -197,9 +169,9 @@
|
||||
{
|
||||
"name": "team",
|
||||
"route": "http://127.0.0.1:5174/team",
|
||||
"unique": 8,
|
||||
"unique": 6,
|
||||
"budget": 4,
|
||||
"rawRequests": 10,
|
||||
"rawRequests": 8,
|
||||
"waterfall": 0,
|
||||
"duplicates": [],
|
||||
"failed": [],
|
||||
@@ -208,26 +180,24 @@
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=1",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ops/notifications/?page_size=100"
|
||||
],
|
||||
"elapsedMs": 3555,
|
||||
"elapsedMs": 5500,
|
||||
"problems": [],
|
||||
"warnings": [
|
||||
"首屏接口数 8 > 预算 4(架构性过量拉取,待决策)"
|
||||
"首屏接口数 6 > 预算 4(架构性过量拉取,待决策)"
|
||||
],
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "messages",
|
||||
"route": "http://127.0.0.1:5174/messages",
|
||||
"unique": 8,
|
||||
"unique": 5,
|
||||
"budget": 4,
|
||||
"rawRequests": 11,
|
||||
"rawRequests": 7,
|
||||
"waterfall": 0,
|
||||
"duplicates": [],
|
||||
"failed": [],
|
||||
@@ -236,27 +206,23 @@
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=100",
|
||||
"GET /api/ops/notifications/?page_size=1",
|
||||
"GET /api/ops/notifications/?page=1&page_size=10"
|
||||
],
|
||||
"elapsedMs": 3468,
|
||||
"elapsedMs": 3485,
|
||||
"problems": [],
|
||||
"warnings": [
|
||||
"首屏接口数 8 > 预算 4(架构性过量拉取,待决策)"
|
||||
"首屏接口数 5 > 预算 4(架构性过量拉取,待决策)"
|
||||
],
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "productDetail",
|
||||
"route": "http://127.0.0.1:5174/products/045ad8d6-8b15-486b-a4a9-f6968eb1555f",
|
||||
"unique": 9,
|
||||
"unique": 6,
|
||||
"budget": 8,
|
||||
"rawRequests": 11,
|
||||
"rawRequests": 7,
|
||||
"waterfall": 0,
|
||||
"duplicates": [],
|
||||
"failed": [],
|
||||
@@ -265,27 +231,21 @@
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=100",
|
||||
"GET /api/ops/notifications/?page_size=1",
|
||||
"GET /api/assets/?page_size=200&product=045ad8d6-8b15-486b-a4a9-f6968eb1555f"
|
||||
],
|
||||
"elapsedMs": 5956,
|
||||
"elapsedMs": 5755,
|
||||
"problems": [],
|
||||
"warnings": [
|
||||
"首屏接口数 9 > 预算 8(架构性过量拉取,待决策)"
|
||||
],
|
||||
"warnings": [],
|
||||
"pass": true
|
||||
},
|
||||
{
|
||||
"name": "pipeline",
|
||||
"route": "http://127.0.0.1:5174/pipeline/1048451e-3b4a-4b66-a8f4-cb865096de2f",
|
||||
"unique": 11,
|
||||
"unique": 7,
|
||||
"budget": 10,
|
||||
"rawRequests": 13,
|
||||
"rawRequests": 8,
|
||||
"waterfall": 0,
|
||||
"duplicates": [],
|
||||
"failed": [],
|
||||
@@ -295,20 +255,13 @@
|
||||
"GET /api/products/",
|
||||
"GET /api/projects/",
|
||||
"GET /api/billing/summary/",
|
||||
"GET /api/billing/ledgers/?page=1&page_size=10",
|
||||
"GET /api/billing/trend/",
|
||||
"GET /api/auth/team/members/",
|
||||
"GET /api/ai/models/",
|
||||
"GET /api/ai/tasks/",
|
||||
"GET /api/ops/notifications/?page_size=100",
|
||||
"POST /api/projects/1048451e-3b4a-4b66-a8f4-cb865096de2f/generate-base-asset/",
|
||||
"GET /api/ops/notifications/?page_size=1",
|
||||
"GET /api/projects/1048451e-3b4a-4b66-a8f4-cb865096de2f/pending-assets/"
|
||||
],
|
||||
"elapsedMs": 5702,
|
||||
"elapsedMs": 6015,
|
||||
"problems": [],
|
||||
"warnings": [
|
||||
"首屏接口数 11 > 预算 10(架构性过量拉取,待决策)"
|
||||
],
|
||||
"warnings": [],
|
||||
"pass": true
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user