auto-save 2026-05-17 21:09 (~4)
This commit is contained in:
@@ -19,8 +19,10 @@ import {
|
||||
type ProductViewAnalysisItem,
|
||||
type StoryboardScriptRewriteSegment,
|
||||
type StoryboardScene,
|
||||
type SubjectAsset,
|
||||
type SubjectKind,
|
||||
addElement,
|
||||
analyzeJob,
|
||||
analyzeProductViews,
|
||||
apiAssetUrl,
|
||||
cutoutElement,
|
||||
@@ -250,6 +252,23 @@ function guessSubjectKind(name: string): SubjectKind {
|
||||
: "object"
|
||||
}
|
||||
|
||||
function closestFrameForTime(frames: KeyFrame[], time: number) {
|
||||
if (!frames.length) return null
|
||||
const first = frames[0] as KeyFrame
|
||||
return frames.reduce((best, frame) =>
|
||||
Math.abs(frame.timestamp - time) < Math.abs(best.timestamp - time) ? frame : best,
|
||||
first)
|
||||
}
|
||||
|
||||
function isSimilarActorElement(element: KeyElement) {
|
||||
const name = `${element.name_zh || ""} ${element.name_en || ""}`.toLowerCase()
|
||||
return name.includes("相似主角") || name.includes("similar ad actor") || name.includes("similar actor")
|
||||
}
|
||||
|
||||
function subjectAssetUrl(job: Job, asset: SubjectAsset) {
|
||||
return apiAssetUrl(asset.url) || resolveImageRefUrl(job.id, { kind: "asset", frame_idx: 0, element_id: asset.id })
|
||||
}
|
||||
|
||||
function buildFallbackScene(job: Job, frame: KeyFrame, order: number): StoryboardScene {
|
||||
const frames = [...job.frames].sort((a, b) => a.timestamp - b.timestamp)
|
||||
const nextFrame = frames.find((item) => item.timestamp > frame.timestamp) ?? null
|
||||
@@ -816,11 +835,15 @@ export function AdRecreationBoard({
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<AudioIntakePanel job={job} />
|
||||
<AudioIntakePanel
|
||||
job={job}
|
||||
selectedFrames={data.selectedFrames}
|
||||
onToggleFrame={data.onToggleFrame}
|
||||
onJobUpdate={data.onJobUpdate}
|
||||
/>
|
||||
<AudioStoryboardPlanPanel
|
||||
job={job}
|
||||
onAddFrame={data.onAddManualFrameForJob}
|
||||
onOpenFrame={data.onOpenFramePanel}
|
||||
selectedFrames={data.selectedFrames}
|
||||
onJobUpdate={data.onJobUpdate}
|
||||
onGenerateVideo={onGenerateVideo}
|
||||
/>
|
||||
@@ -951,7 +974,17 @@ function AudioIntakeStatus({ job, audioReady }: { job: Job | null; audioReady: b
|
||||
)
|
||||
}
|
||||
|
||||
function AudioIntakePanel({ job }: { job: Job | null }) {
|
||||
function AudioIntakePanel({
|
||||
job,
|
||||
selectedFrames,
|
||||
onToggleFrame,
|
||||
onJobUpdate,
|
||||
}: {
|
||||
job: Job | null
|
||||
selectedFrames: Set<number>
|
||||
onToggleFrame: (idx: number) => void
|
||||
onJobUpdate: (job: Job) => void
|
||||
}) {
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [mediaDuration, setMediaDuration] = useState(0)
|
||||
const [audioFeatures, setAudioFeatures] = useState<AudioFeature[]>([])
|
||||
@@ -1082,7 +1115,7 @@ function AudioIntakePanel({ job }: { job: Job | null }) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 xl:grid-cols-[230px_minmax(0,1fr)]">
|
||||
<div className="grid gap-2 xl:grid-cols-[230px_320px_minmax(0,1fr)]">
|
||||
<div className="min-w-0">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<SectionTitle icon={<Play className="h-4 w-4" />} title="原版视频" />
|
||||
@@ -1113,6 +1146,13 @@ function AudioIntakePanel({ job }: { job: Job | null }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SourceReferenceBuildPanel
|
||||
job={job}
|
||||
selectedFrames={selectedFrames}
|
||||
onToggleFrame={onToggleFrame}
|
||||
onJobUpdate={onJobUpdate}
|
||||
/>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<SectionTitle icon={<FileText className="h-4 w-4" />} title="逐句时间轴" />
|
||||
|
||||
Reference in New Issue
Block a user