fix: improve filmstrip picking and audio retry

This commit is contained in:
2026-05-19 20:01:45 +08:00
parent fe60d5dc99
commit aabddef486
5 changed files with 74 additions and 18 deletions

View File

@@ -60,6 +60,11 @@ const DEFAULT_PRODUCT_LIBRARY_IDS = [
]
const VIDEO_READY_STATUSES: Job["status"][] = ["downloaded", "frames_extracted", "transcribed", "failed"]
function isAudioProcessing(job?: Job | null) {
if (!job) return false
return job.audio_script?.status === "rewriting" || (job.status === "transcribing" && job.audio_script?.status !== "failed")
}
const PRODUCT_FUSION_WEARING_PROMPT = [
"Product placement must be physically correct:",
"The SKG device is a rigid opaque white U-shaped neck massager, not a soft scarf, necklace, cable, collar, sticker, implant, or transparent body part.",
@@ -448,7 +453,7 @@ export default function Home() {
if (!options?.silent) toast.info("视频导入完成后,可在音频卡片点击提取音频")
return
}
if (target.status === "transcribing" || target.audio_script?.status === "rewriting") {
if (isAudioProcessing(target)) {
if (!options?.silent) toast.info("音频正在处理中")
return
}
@@ -466,8 +471,9 @@ export default function Home() {
if (!videoReady) return
const audioKey = `${target.id}:audio`
const hasAudioResult = !!target.audio_script?.source_text || target.transcript.length > 0
const audioRunning = target.status === "transcribing" || target.audio_script?.status === "rewriting"
const audioFailed = target.audio_script?.status === "failed"
const hasAudioResult = !audioFailed && (!!target.audio_script?.source_text || target.transcript.length > 0)
const audioRunning = isAudioProcessing(target)
if (!hasAudioResult && !audioRunning && !autoTriggeredRef.current.has(audioKey)) {
autoTriggeredRef.current.add(audioKey)
try {

View File

@@ -139,6 +139,27 @@ const FILMSTRIP_DENSITIES: Array<{ value: FilmstripDensitySeconds; label: string
const FILMSTRIP_TILT_CLASSES = ["-rotate-[8deg]", "-rotate-[6deg]", "-rotate-[9deg]"]
const FILMSTRIP_VERTICAL_OFFSET_CLASSES = ["translate-y-0", "translate-y-2", "-translate-y-1.5", "translate-y-1", "-translate-y-2"]
const FILMSTRIP_HOVER_SCALE = 4.8
const FILMSTRIP_CACHE_LIMIT = 8
const filmstripPreviewCache = new Map<string, FilmstripPreviewFrame[]>()
function filmstripCacheKey(jobId: string, videoUrl: string, density: FilmstripDensitySeconds, duration: number) {
return `${jobId}:${videoUrl}:${density}:${Math.round(duration * 10) / 10}`
}
function rememberFilmstripPreview(key: string, frames: FilmstripPreviewFrame[]) {
filmstripPreviewCache.delete(key)
filmstripPreviewCache.set(key, frames)
while (filmstripPreviewCache.size > FILMSTRIP_CACHE_LIMIT) {
const oldest = filmstripPreviewCache.keys().next().value
if (!oldest) break
filmstripPreviewCache.delete(oldest)
}
}
function isAudioProcessing(job?: Job | null) {
if (!job) return false
return job.audio_script?.status === "rewriting" || (job.status === "transcribing" && job.audio_script?.status !== "failed")
}
type AudioStoryboardRow = {
index: number
@@ -1910,7 +1931,7 @@ export function AdRecreationBoard({
const readySegments = countReadySegments(job, draftSegments)
const transcriptCount = job?.transcript.length ?? 0
const backgroundReady = !!job?.audio_script?.background_audio_profile?.trim()
const audioRunning = job?.status === "transcribing" || job?.audio_script?.status === "rewriting"
const audioRunning = isAudioProcessing(job)
const visualRunning = job?.status === "splitting"
const visualReady = (job?.frames.length ?? 0) > 0
const subjectAssetCount = countSubjectAssetViews(job)
@@ -2393,7 +2414,7 @@ function AudioIntakePanel({
const syncFrameRef = useRef<number | null>(null)
const audioSrcUrl = job ? apiAssetUrl(job.source_audio_url) || sourceAudioUrl(job.id) : ""
const videoSrcUrl = job ? apiAssetUrl(job.video_url) || videoUrl(job.id) : ""
const processing = !!job && (job.status === "transcribing" || job.audio_script?.status === "rewriting")
const processing = isAudioProcessing(job)
const timelineDuration = useMemo(() => {
if (!job) return 1
const lastTranscriptEnd = job.transcript.reduce((max, segment) => Math.max(max, segment.end || 0), 0)
@@ -2449,12 +2470,20 @@ function AudioIntakePanel({
setFilmstripStatus("idle")
return
}
const cacheKey = filmstripCacheKey(job.id, videoSrcUrl, filmstripDensity, timelineDuration)
const cached = filmstripPreviewCache.get(cacheKey)
if (cached) {
setFilmstripPreviews(cached)
setFilmstripStatus(cached.length ? "ready" : "idle")
return
}
let cancelled = false
setFilmstripPreviews([])
setFilmstripStatus("loading")
captureVideoFilmstrip(videoSrcUrl, timelineDuration, filmstripDensity, () => cancelled)
.then((frames) => {
if (!cancelled) {
rememberFilmstripPreview(cacheKey, frames)
setFilmstripPreviews(frames)
setFilmstripStatus(frames.length ? "ready" : "idle")
}
@@ -2655,6 +2684,7 @@ function AudioIntakePanel({
selectedTimes={frames.map((frame) => frame.timestamp)}
busyTime={filmstripBusyTime}
onSeek={seekTo}
onAddFrame={(time) => void addFilmstripFrame(time)}
onDragStart={setFilmstripDragTime}
onDragEnd={() => setFilmstripDragTime(null)}
/>
@@ -2750,6 +2780,7 @@ function TimelineFilmstrip({
selectedTimes,
busyTime,
onSeek,
onAddFrame,
onDragStart,
onDragEnd,
}: {
@@ -2762,6 +2793,7 @@ function TimelineFilmstrip({
selectedTimes: number[]
busyTime: number | null
onSeek: (time: number) => void
onAddFrame: (time: number) => void
onDragStart: (time: number) => void
onDragEnd: () => void
}) {
@@ -2838,6 +2870,10 @@ function TimelineFilmstrip({
onMouseEnter={(event) => showHoverPreview(event, frame, active, selected, busy)}
onMouseMove={(event) => showHoverPreview(event, frame, active, selected, busy)}
onMouseLeave={() => setHoverPreview(null)}
onDoubleClick={(event) => {
event.preventDefault()
if (!busy) onAddFrame(frame.time)
}}
onDragStart={(event) => {
setHoverPreview(null)
event.dataTransfer.setData(FILMSTRIP_DRAG_TYPE, frame.time.toFixed(2))
@@ -2864,9 +2900,10 @@ function TimelineFilmstrip({
disablePreview
selected={selected}
onClick={() => onSeek(frame.time)}
title="击跳到该时间点,拖入关键帧库才正式选取"
title="击跳到该时间点,双击或拖入参考帧池才正式选取"
topLeft={selected ? <span className="rounded bg-emerald-500/85 px-1 text-[8.5px] font-semibold text-black"></span> : undefined}
topRight={busy ? <Loader2 className="h-3 w-3 animate-spin text-cyan-100" /> : selected ? <Check className="h-3 w-3 text-emerald-200" /> : undefined}
bottom={<span className="block rounded bg-black/74 px-1 py-0.5 text-center font-mono text-[9px] text-white/68">{frame.time.toFixed(1)}s</span>}
bottom={<span className={`block rounded px-1 py-0.5 text-center font-mono text-[9px] ${selected ? "bg-emerald-400/82 text-black" : "bg-black/74 text-white/68"}`}>{selected ? "已添加" : `${frame.time.toFixed(1)}s`}</span>}
/>
</div>
</div>
@@ -2906,8 +2943,9 @@ function TimelineFilmstrip({
objectFit="contain"
disablePreview
selected={hoverPreview.selected}
topLeft={hoverPreview.selected ? <span className="rounded-md bg-emerald-500/88 px-2 py-1 text-[22px] font-semibold leading-none text-black"></span> : undefined}
topRight={hoverPreview.busy ? <Loader2 className="h-6 w-6 animate-spin text-cyan-100" /> : hoverPreview.selected ? <Check className="h-6 w-6 text-emerald-200" /> : undefined}
bottom={<span className="block rounded-md bg-black/74 px-2 py-1 text-center font-mono text-[42px] leading-none text-white/68">{hoverPreview.time.toFixed(1)}s</span>}
bottom={<span className={`block rounded-md px-2 py-1 text-center font-mono text-[42px] leading-none ${hoverPreview.selected ? "bg-emerald-400/86 text-black" : "bg-black/74 text-white/68"}`}>{hoverPreview.selected ? `已添加 · ${hoverPreview.time.toFixed(1)}s` : `${hoverPreview.time.toFixed(1)}s`}</span>}
/>
</div>,
document.body,