fix: improve filmstrip picking and audio retry
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user