fix: remove source workspace layout tuning

This commit is contained in:
2026-05-20 21:27:19 +08:00
parent d03b38d75a
commit caa7b730a6
2 changed files with 31 additions and 153 deletions

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@ import { type DragEvent as ReactDragEvent, type MouseEvent as ReactMouseEvent, t
import { createPortal } from "react-dom"
import {
AlertTriangle, BookOpen, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Info, Link2, Loader2, Minus,
MessageSquare, Mic, Moon, Package, PanelRight, Play, Plus, RefreshCw, RotateCcw, Scissors, Send, SlidersHorizontal, Sparkles, Sun, Trash2, Upload, Wand2,
MessageSquare, Mic, Moon, Package, PanelRight, Play, Plus, RefreshCw, Scissors, Send, Sparkles, Sun, Trash2, Upload, Wand2,
} from "lucide-react"
import { toast } from "sonner"
import {
@@ -108,34 +108,12 @@ const BOARD_FRAME_HEIGHT = 1000
const BOARD_MIN_SCALE = 0.72
const BOARD_MAX_SCALE = 1.6
const BOARD_SCALE_PRESETS = [0.72, 0.76, 0.8, 0.86, 0.92, 1, 1.06, 1.16, 1.24, 1.34, 1.48, 1.6]
const SOURCE_WORKSPACE_LAYOUT_STORAGE_KEY = "skg-source-workspace-layout:v1"
type SourceWorkspaceLayout = {
leftWidth: number
videoHeight: number
transcriptHeight: number
referenceWidth: number
conversionHeight: number
subjectEmptyHeight: number
}
const DEFAULT_SOURCE_WORKSPACE_LAYOUT: SourceWorkspaceLayout = {
leftWidth: 360,
videoHeight: 510,
transcriptHeight: 260,
referenceWidth: 140,
conversionHeight: 500,
subjectEmptyHeight: 78,
}
const SOURCE_WORKSPACE_LAYOUT_LIMITS: Record<keyof SourceWorkspaceLayout, { min: number; max: number; step: number; label: string; suffix: string }> = {
leftWidth: { min: 320, max: 460, step: 10, label: "左列宽", suffix: "px" },
videoHeight: { min: 430, max: 560, step: 10, label: "视频高", suffix: "px" },
transcriptHeight: { min: 180, max: 360, step: 10, label: "时间轴高", suffix: "px" },
referenceWidth: { min: 118, max: 180, step: 2, label: "参考池宽", suffix: "px" },
conversionHeight: { min: 420, max: 640, step: 10, label: "转换层高", suffix: "px" },
subjectEmptyHeight: { min: 56, max: 140, step: 4, label: "主体空态", suffix: "px" },
}
const SOURCE_LEFT_COLUMN_WIDTH = 380
const SOURCE_VIDEO_HEIGHT = 500
const SOURCE_TRANSCRIPT_MAX_HEIGHT = 270
const SOURCE_REFERENCE_POOL_WIDTH = 140
const SOURCE_CONVERSION_HEIGHT = 500
const SOURCE_SUBJECT_EMPTY_HEIGHT = 78
const resolveBoardScale = (viewportWidth: number) => {
const maxFitScale = clampNumber(viewportWidth / BOARD_FRAME_WIDTH, BOARD_MIN_SCALE, BOARD_MAX_SCALE)
@@ -746,25 +724,6 @@ function clampNumber(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value))
}
function normalizeSourceWorkspaceLayout(value: Partial<SourceWorkspaceLayout> = {}): SourceWorkspaceLayout {
const next = { ...DEFAULT_SOURCE_WORKSPACE_LAYOUT, ...value }
return Object.fromEntries(
(Object.keys(DEFAULT_SOURCE_WORKSPACE_LAYOUT) as Array<keyof SourceWorkspaceLayout>).map((key) => {
const limits = SOURCE_WORKSPACE_LAYOUT_LIMITS[key]
return [key, clampNumber(Number(next[key]) || DEFAULT_SOURCE_WORKSPACE_LAYOUT[key], limits.min, limits.max)]
}),
) as SourceWorkspaceLayout
}
function loadSourceWorkspaceLayout() {
if (typeof window === "undefined") return DEFAULT_SOURCE_WORKSPACE_LAYOUT
try {
return normalizeSourceWorkspaceLayout(JSON.parse(window.localStorage.getItem(SOURCE_WORKSPACE_LAYOUT_STORAGE_KEY) || "{}"))
} catch {
return DEFAULT_SOURCE_WORKSPACE_LAYOUT
}
}
async function decodeAudioFeatures(url: string, targetFrames = 640): Promise<AudioFeature[]> {
const res = await fetch(url)
if (!res.ok) throw new Error(`audio ${res.status}`)
@@ -2776,62 +2735,6 @@ function MaterialColumn({
)
}
function SourceWorkspaceLayoutPanel({
layout,
onChange,
onReset,
}: {
layout: SourceWorkspaceLayout
onChange: (layout: SourceWorkspaceLayout) => void
onReset: () => void
}) {
const update = (key: keyof SourceWorkspaceLayout, value: number) => {
onChange(normalizeSourceWorkspaceLayout({ ...layout, [key]: value }))
}
return (
<div className="mb-2 rounded-md border border-[#d6b36a]/24 bg-[#d6b36a]/[0.055] p-2">
<div className="mb-2 flex items-center justify-between gap-2">
<div>
<div className="text-[10.5px] font-semibold text-[#f4dc88]"></div>
<div className="mt-0.5 text-[9px] text-white/36"></div>
</div>
<button
type="button"
onClick={onReset}
className="inline-flex h-7 items-center gap-1 rounded-md border border-white/10 bg-black/28 px-2 text-[10px] font-semibold text-white/48 transition hover:border-white/24 hover:text-white/76"
title="恢复推荐参数"
>
<RotateCcw className="h-3.5 w-3.5" />
</button>
</div>
<div className="grid grid-cols-3 gap-2">
{(Object.keys(SOURCE_WORKSPACE_LAYOUT_LIMITS) as Array<keyof SourceWorkspaceLayout>).map((key) => {
const item = SOURCE_WORKSPACE_LAYOUT_LIMITS[key]
return (
<label key={key} className="min-w-0 rounded border border-white/8 bg-black/18 px-2 py-1.5">
<div className="mb-1 flex items-center justify-between gap-2">
<span className="text-[9.5px] font-semibold text-white/56">{item.label}</span>
<span className="font-mono text-[9px] text-white/34">{layout[key]}{item.suffix}</span>
</div>
<input
type="range"
min={item.min}
max={item.max}
step={item.step}
value={layout[key]}
onChange={(event) => update(key, Number(event.target.value))}
className="h-4 w-full accent-[#d6b36a]"
/>
</label>
)
})}
</div>
</div>
)
}
function AudioIntakePanel({
job,
selectedFrames,
@@ -2862,8 +2765,6 @@ function AudioIntakePanel({
const [filmstripStatus, setFilmstripStatus] = useState<FilmstripStatus>("idle")
const [filmstripDragTime, setFilmstripDragTime] = useState<number | null>(null)
const [filmstripBusyTime, setFilmstripBusyTime] = useState<number | null>(null)
const [layoutOpen, setLayoutOpen] = useState(false)
const [workspaceLayout, setWorkspaceLayout] = useState<SourceWorkspaceLayout>(() => loadSourceWorkspaceLayout())
const videoRef = useRef<HTMLVideoElement | null>(null)
const transcriptScrollRef = useRef<HTMLDivElement | null>(null)
const rowRefs = useRef<Record<number, HTMLDivElement | null>>({})
@@ -2889,12 +2790,6 @@ function AudioIntakePanel({
? `当前句 ${activeSegment.start.toFixed(1)}-${activeSegment.end.toFixed(1)}s`
: "指针 -"
useEffect(() => {
try {
window.localStorage.setItem(SOURCE_WORKSPACE_LAYOUT_STORAGE_KEY, JSON.stringify(workspaceLayout))
} catch { /* ignore unavailable storage */ }
}, [workspaceLayout])
useEffect(() => {
if (!job?.id || !audioSrcUrl) {
setAudioFeatures([])
@@ -3070,39 +2965,17 @@ function AudioIntakePanel({
<section className="rounded-lg border border-white/10 bg-black/28 p-2.5">
<div className="mb-2 flex items-center justify-between gap-3">
<SectionTitle icon={<Film className="h-4 w-4" />} title="源视频工作区" />
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 font-mono text-[11px] text-white/38">
<span>{job.transcript.length} </span>
<span>{formatSeconds(job.duration)}</span>
</div>
<button
type="button"
onClick={() => setLayoutOpen((open) => !open)}
className={`inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-[10px] font-semibold transition ${
layoutOpen
? "border-[#d6b36a]/55 bg-[#d6b36a]/13 text-[#f4dc88]"
: "border-white/10 bg-black/24 text-white/42 hover:border-white/24 hover:text-white/70"
}`}
title="临时调节源视频工作区布局"
>
<SlidersHorizontal className="h-3.5 w-3.5" />
</button>
<div className="flex items-center gap-2 font-mono text-[11px] text-white/38">
<span>{job.transcript.length} </span>
<span>{formatSeconds(job.duration)}</span>
</div>
</div>
<div className="grid gap-2 border-t border-white/8 pt-2">
{layoutOpen ? (
<SourceWorkspaceLayoutPanel
layout={workspaceLayout}
onChange={setWorkspaceLayout}
onReset={() => setWorkspaceLayout(DEFAULT_SOURCE_WORKSPACE_LAYOUT)}
/>
) : null}
<div className="grid gap-2">
<div
className="grid gap-3"
style={{ gridTemplateColumns: `${workspaceLayout.leftWidth}px minmax(0,1fr)` }}
style={{ gridTemplateColumns: `${SOURCE_LEFT_COLUMN_WIDTH}px minmax(0,1fr)` }}
>
<div className="min-w-0 space-y-2">
<div className="mb-2 flex items-center justify-between gap-3">
@@ -3111,7 +2984,7 @@ function AudioIntakePanel({
</div>
<div
className="relative mx-auto aspect-[9/16] overflow-hidden rounded-md border border-white/10 bg-black"
style={{ height: workspaceLayout.videoHeight }}
style={{ height: SOURCE_VIDEO_HEIGHT }}
>
{job.video_url ? (
<video
@@ -3151,7 +3024,6 @@ function AudioIntakePanel({
activeSegmentIndex={activeSegment?.index ?? null}
scrollRef={transcriptScrollRef}
rowRefs={rowRefs}
maxHeight={workspaceLayout.transcriptHeight}
onSeek={seekTo}
/>
</div>
@@ -3209,7 +3081,6 @@ function AudioIntakePanel({
runtimeModels={runtimeModels}
filmstripDragging={filmstripDragTime !== null}
onDropFilmstripFrame={(time) => addFilmstripFrame(time)}
layout={workspaceLayout}
/>
</div>
</div>
@@ -3226,7 +3097,6 @@ function TranscriptTimelinePanel({
activeSegmentIndex,
scrollRef,
rowRefs,
maxHeight,
onSeek,
}: {
job: Job
@@ -3234,7 +3104,6 @@ function TranscriptTimelinePanel({
activeSegmentIndex: number | null
scrollRef: RefObject<HTMLDivElement | null>
rowRefs: { current: Record<number, HTMLDivElement | null> }
maxHeight: number
onSeek: (time: number) => void
}) {
return (
@@ -3249,7 +3118,7 @@ function TranscriptTimelinePanel({
<div></div>
<div> / </div>
</div>
<div ref={scrollRef} className="overflow-y-auto" style={{ maxHeight }}>
<div ref={scrollRef} className="overflow-y-auto" style={{ maxHeight: SOURCE_TRANSCRIPT_MAX_HEIGHT }}>
{job.transcript.map((segment) => {
const active = activeSegmentIndex === segment.index
return (
@@ -3507,7 +3376,6 @@ function SourceSubjectPipeline({
runtimeModels,
filmstripDragging,
onDropFilmstripFrame,
layout,
}: {
job: Job
frames: KeyFrame[]
@@ -3521,7 +3389,6 @@ function SourceSubjectPipeline({
runtimeModels?: RuntimeModels
filmstripDragging?: boolean
onDropFilmstripFrame?: (time: number) => Promise<KeyFrame | null> | KeyFrame | null | void
layout: SourceWorkspaceLayout
}) {
const [referenceDropActive, setReferenceDropActive] = useState(false)
const [agentDropActive, setAgentDropActive] = useState(false)
@@ -4086,7 +3953,7 @@ function SourceSubjectPipeline({
<div className="space-y-2">
<div
className="grid gap-2"
style={{ gridTemplateColumns: `${layout.referenceWidth}px minmax(0,1fr)` }}
style={{ gridTemplateColumns: `${SOURCE_REFERENCE_POOL_WIDTH}px minmax(0,1fr)` }}
>
<div className="min-w-0">
<div className="mb-2 flex items-center justify-between gap-2">
@@ -4138,7 +4005,7 @@ function SourceSubjectPipeline({
<span>{frames.length} </span>
<span>{filmstripDragging ? "松手加入" : "点击选择"}</span>
</div>
<div className="flex flex-col gap-1 overflow-y-auto pr-0.5" style={{ maxHeight: layout.conversionHeight }}>
<div className="flex flex-col gap-1 overflow-y-auto pr-0.5" style={{ maxHeight: SOURCE_CONVERSION_HEIGHT }}>
{frames.map((frame, index) => {
const selected = selectedFrames.has(frame.index)
return (
@@ -4205,7 +4072,7 @@ function SourceSubjectPipeline({
</div>
<div
className="flex flex-col overflow-y-auto rounded-md border border-white/10 bg-black/24 p-2"
style={{ height: layout.conversionHeight }}
style={{ height: SOURCE_CONVERSION_HEIGHT }}
>
<div className="mb-2 grid grid-cols-2 gap-1.5">
{SUBJECT_MODEL_BUNDLE_OPTIONS.map((option) => (
@@ -4573,7 +4440,7 @@ function SourceSubjectPipeline({
) : (
<div
className="flex items-center justify-center rounded border border-dashed border-white/12 px-3 text-center text-[10.5px] leading-snug text-white/34"
style={{ minHeight: layout.subjectEmptyHeight }}
style={{ minHeight: SOURCE_SUBJECT_EMPTY_HEIGHT }}
>
</div>