auto-save 2026-05-17 22:30 (~2)
This commit is contained in:
@@ -1006,6 +1006,7 @@ function AudioIntakePanel({
|
||||
const [mediaDuration, setMediaDuration] = useState(0)
|
||||
const [audioFeatures, setAudioFeatures] = useState<AudioFeature[]>([])
|
||||
const [audioFeatureStatus, setAudioFeatureStatus] = useState<AudioFeatureStatus>("idle")
|
||||
const [manualBusy, setManualBusy] = useState(false)
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
const rowRefs = useRef<Record<number, HTMLDivElement | null>>({})
|
||||
const syncFrameRef = useRef<number | null>(null)
|
||||
@@ -1094,6 +1095,17 @@ function AudioIntakePanel({
|
||||
setCurrentTime(next)
|
||||
}
|
||||
|
||||
const addFrameAtCurrentTime = async () => {
|
||||
if (!job || !onAddFrame) return
|
||||
const next = clampNumber(currentTime, 0, timelineDuration)
|
||||
setManualBusy(true)
|
||||
try {
|
||||
await onAddFrame(job.id, next)
|
||||
} finally {
|
||||
setManualBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!job) {
|
||||
return <EmptyState text="先在左侧粘贴 TK 链接或上传本地视频。点击开始后,会先下载视频,再自动解析原音频文案、讲话人节奏和背景音。" />
|
||||
}
|
||||
@@ -1132,11 +1144,23 @@ function AudioIntakePanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 xl:grid-cols-[340px_minmax(340px,1fr)_320px] 2xl:grid-cols-[410px_minmax(500px,1fr)_320px]">
|
||||
<div className="grid gap-2 xl:grid-cols-[360px_minmax(500px,1fr)_260px] 2xl:grid-cols-[440px_minmax(600px,1fr)_280px]">
|
||||
<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="原版视频" />
|
||||
<span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s / {formatSeconds(timelineDuration)}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s / {formatSeconds(timelineDuration)}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void addFrameAtCurrentTime()}
|
||||
disabled={!job.video_url || !onAddFrame || manualBusy || job.status === "splitting"}
|
||||
title={`按当前播放位置手动抽帧:${currentTime.toFixed(1)}s`}
|
||||
className="inline-flex h-7 items-center justify-center gap-1 rounded-md border border-emerald-300/20 bg-emerald-300/[0.08] px-2 text-[10.5px] font-semibold text-emerald-100 transition hover:border-emerald-200/45 hover:bg-emerald-300/[0.14] disabled:cursor-not-allowed disabled:opacity-35"
|
||||
>
|
||||
{manualBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
|
||||
当前点抽帧
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-md border border-white/10 bg-black/45">
|
||||
{job.video_url ? (
|
||||
@@ -1144,7 +1168,7 @@ function AudioIntakePanel({
|
||||
ref={videoRef}
|
||||
controls
|
||||
playsInline
|
||||
className="h-[270px] w-full bg-black object-contain 2xl:h-[300px]"
|
||||
className="h-[320px] w-full bg-black object-contain 2xl:h-[380px]"
|
||||
src={videoSrcUrl}
|
||||
onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime)}
|
||||
onSeeked={(event) => setCurrentTime(event.currentTarget.currentTime)}
|
||||
@@ -1158,7 +1182,7 @@ function AudioIntakePanel({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-[270px] items-center justify-center text-[12px] text-white/38 2xl:h-[300px]">等待原视频</div>
|
||||
<div className="flex h-[320px] items-center justify-center text-[12px] text-white/38 2xl:h-[380px]">等待原视频</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1168,9 +1192,6 @@ function AudioIntakePanel({
|
||||
selectedFrames={selectedFrames}
|
||||
onToggleFrame={onToggleFrame}
|
||||
onJobUpdate={onJobUpdate}
|
||||
currentTime={currentTime}
|
||||
duration={timelineDuration}
|
||||
onAddFrame={onAddFrame}
|
||||
onDeleteFrame={onDeleteFrame}
|
||||
/>
|
||||
|
||||
@@ -1186,7 +1207,7 @@ function AudioIntakePanel({
|
||||
<div>原文</div>
|
||||
<div>中文</div>
|
||||
</div>
|
||||
<div className="max-h-[270px] overflow-y-auto 2xl:max-h-[300px]">
|
||||
<div className="max-h-[320px] overflow-y-auto 2xl:max-h-[380px]">
|
||||
{job.transcript.map((segment) => {
|
||||
const active = activeSegment?.index === segment.index
|
||||
return (
|
||||
@@ -1223,22 +1244,15 @@ function SourceReferenceBuildPanel({
|
||||
selectedFrames,
|
||||
onToggleFrame,
|
||||
onJobUpdate,
|
||||
currentTime,
|
||||
duration,
|
||||
onAddFrame,
|
||||
onDeleteFrame,
|
||||
}: {
|
||||
job: Job
|
||||
selectedFrames: Set<number>
|
||||
onToggleFrame: (idx: number) => void
|
||||
onJobUpdate: (job: Job) => void
|
||||
currentTime: number
|
||||
duration: number
|
||||
onAddFrame?: (jobId: string, t: number) => Promise<void> | void
|
||||
onDeleteFrame?: (jobId: string, idx: number) => Promise<void> | void
|
||||
}) {
|
||||
const [extracting, setExtracting] = useState(false)
|
||||
const [manualBusy, setManualBusy] = useState(false)
|
||||
const [subjectBusy, setSubjectBusy] = useState(false)
|
||||
const [deletingFrame, setDeletingFrame] = useState<number | null>(null)
|
||||
const frames = useMemo(() => [...job.frames].sort((a, b) => a.timestamp - b.timestamp), [job.frames])
|
||||
@@ -1259,7 +1273,6 @@ function SourceReferenceBuildPanel({
|
||||
return null
|
||||
}, [frames, selectedReferenceFrames])
|
||||
const actorAssets = actorSource?.element.subject_assets ?? []
|
||||
const manualFrameTime = clampNumber(currentTime, 0, Math.max(duration, 1))
|
||||
|
||||
const extractKeyframes = async () => {
|
||||
setExtracting(true)
|
||||
@@ -1322,16 +1335,6 @@ function SourceReferenceBuildPanel({
|
||||
}
|
||||
}
|
||||
|
||||
const addFrameAtCurrentTime = async () => {
|
||||
if (!onAddFrame) return
|
||||
setManualBusy(true)
|
||||
try {
|
||||
await onAddFrame(job.id, manualFrameTime)
|
||||
} finally {
|
||||
setManualBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteReferenceFrame = async (idx: number) => {
|
||||
if (!onDeleteFrame) return
|
||||
setDeletingFrame(idx)
|
||||
@@ -1350,8 +1353,8 @@ function SourceReferenceBuildPanel({
|
||||
{frames.length ? `${frames.length} 张` : "待抽帧"} · 已选 {selectedReferenceFrames.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-[270px] overflow-y-auto rounded-md border border-white/10 bg-black/32 p-2 2xl:h-[300px]">
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
<div className="h-[320px] overflow-y-auto rounded-md border border-white/10 bg-black/32 p-2 2xl:h-[380px]">
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void extractKeyframes()}
|
||||
@@ -1362,16 +1365,6 @@ function SourceReferenceBuildPanel({
|
||||
{extracting || job.status === "splitting" ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Scissors className="h-3.5 w-3.5" />}
|
||||
抽参考 12 帧
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void addFrameAtCurrentTime()}
|
||||
disabled={!job.video_url || !onAddFrame || manualBusy || job.status === "splitting"}
|
||||
title={`按当前播放位置手动抽帧:${manualFrameTime.toFixed(1)}s`}
|
||||
className="inline-flex h-7 items-center justify-center gap-1 rounded-md border border-white/10 bg-white/[0.055] px-2 text-[10.5px] font-semibold text-white/66 transition hover:border-emerald-300/35 hover:text-emerald-100 disabled:cursor-not-allowed disabled:opacity-35"
|
||||
>
|
||||
{manualBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
|
||||
当前点抽帧
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generateSimilarActor()}
|
||||
@@ -1383,13 +1376,13 @@ function SourceReferenceBuildPanel({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-cols-4 gap-1.5">
|
||||
<div className="mt-2 grid grid-cols-3 gap-2 2xl:grid-cols-4">
|
||||
{frames.map((frame, index) => {
|
||||
const selected = selectedFrames.has(frame.index)
|
||||
return (
|
||||
<div
|
||||
key={frame.index}
|
||||
className={`group relative h-12 overflow-hidden rounded border bg-black transition ${
|
||||
className={`group relative aspect-[9/16] overflow-hidden rounded border bg-black transition ${
|
||||
selected ? "border-emerald-300/70" : "border-white/10 hover:border-cyan-300/40"
|
||||
}`}
|
||||
title={`关键帧 ${index + 1} · ${frame.timestamp.toFixed(1)}s`}
|
||||
@@ -1399,7 +1392,7 @@ function SourceReferenceBuildPanel({
|
||||
onClick={() => onToggleFrame(frame.index)}
|
||||
className="absolute inset-0 cursor-pointer overflow-hidden focus:outline-none focus:ring-1 focus:ring-cyan-200/70"
|
||||
>
|
||||
<img src={effectiveFrameUrl(job.id, frame)} alt="" className="h-full w-full object-cover" />
|
||||
<img src={effectiveFrameUrl(job.id, frame)} alt="" className="h-full w-full object-contain" />
|
||||
</button>
|
||||
<span className="absolute left-1 top-1 rounded bg-black/72 px-1 font-mono text-[9px] text-white/70">{String(index + 1).padStart(2, "0")}</span>
|
||||
<span className="absolute right-1 top-1 rounded-full bg-black/72 p-0.5">
|
||||
@@ -1424,8 +1417,8 @@ function SourceReferenceBuildPanel({
|
||||
)
|
||||
})}
|
||||
{!frames.length && (
|
||||
<div className="col-span-4 flex h-[106px] items-center justify-center rounded border border-dashed border-white/12 text-[11px] text-white/34">
|
||||
点击“抽参考 12 帧”,或播放到指定时间后用“当前点抽帧”补充人物参考。
|
||||
<div className="col-span-full flex h-[106px] items-center justify-center rounded border border-dashed border-white/12 text-[11px] text-white/34">
|
||||
点击“抽参考 12 帧”,或在原版视频播放器右上角用“当前点抽帧”补充人物参考。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user