This commit is contained in:
2026-06-03 00:57:50 +02:00
parent 4a7d1e6663
commit 5e941e6e6a
17 changed files with 0 additions and 914 deletions
-4
View File
@@ -1,4 +0,0 @@
with open("frontend/src/App.tsx", "r", encoding="utf-8") as f:
for line in f:
if "listSegmentEdits" in line:
print(line.strip())
-4
View File
@@ -1,4 +0,0 @@
with open("backend/app/mkv.py", "r", encoding="utf-8") as f:
for i, line in enumerate(f):
if "core_ranges" in line:
print(f"{i}: {line.strip()}")
-4
View File
@@ -1,4 +0,0 @@
with open("frontend/src/App.tsx", "r", encoding="utf-8") as f:
for i, line in enumerate(f):
if "value={segmentDrafts" in line:
print(f"{i}: {line.strip()}")
-4
View File
@@ -1,4 +0,0 @@
with open("backend/app/main.py", "r", encoding="utf-8") as f:
for line in f:
if "set_progress" in line or "job_manager.log" in line:
print(line.strip())
-4
View File
@@ -1,4 +0,0 @@
with open("frontend/src/App.tsx", "r", encoding="utf-8") as f:
for line in f:
if "const match = /^(" in line:
print(line.strip())
-253
View File
@@ -1,253 +0,0 @@
import sys
def patch_file():
with open('frontend/src/App.tsx', 'r', encoding='utf-8') as f:
content = f.read()
# 1. Add state variables
old1 = ''' const [job, setJob] = useState<Job | null>(null);
const [outputs, setOutputs] = useState<string[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const videoRef = useRef<HTMLVideoElement | null>(null);'''
new1 = ''' const [job, setJob] = useState<Job | null>(null);
const [outputs, setOutputs] = useState<string[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
type SegmentRow = {
kind: "intro" | "core" | "outro";
label: string;
start: number;
end: number;
startEditable: boolean;
endEditable: boolean;
coreIndex?: number;
};
const [isLinked, setIsLinked] = useState(true);
const [customSegmentRows, setCustomSegmentRows] = useState<SegmentRow[]>([]);
const videoRef = useRef<HTMLVideoElement | null>(null);'''
if old1 not in content: print('Failed old1')
content = content.replace(old1, new1)
# 2. Remove duplicate SegmentRow type declaration
old2 = ''' type SegmentRow = {
kind: "intro" | "core" | "outro";
label: string;
start: number;
end: number;
startEditable: boolean;
endEditable: boolean;
coreIndex?: number;
};
const introBoundary = useMemo(() => {'''
new2 = ''' const introBoundary = useMemo(() => {'''
if old2 not in content: print('Failed old2')
content = content.replace(old2, new2)
# 3. Rename segmentRows to derivedSegmentRows
old3 = ''' const segmentRows = useMemo(() => {
if (!duration || !project || coreBoundaries.length < 2) return [] as SegmentRow[];'''
new3 = ''' const derivedSegmentRows = useMemo(() => {
if (!duration || !project || coreBoundaries.length < 2) return [] as SegmentRow[];'''
if old3 not in content: print('Failed old3')
content = content.replace(old3, new3)
# 4. Add segmentRows constant
old4 = ''' });
return rows;
}, [duration, project, coreBoundaries, introBoundary, alignedOutroBoundary, lastFrameTime, frameStep, segments]);
useEffect(() => {
if (!segmentRows.length) {'''
new4 = ''' });
return rows;
}, [duration, project, coreBoundaries, introBoundary, alignedOutroBoundary, lastFrameTime, frameStep, segments]);
const segmentRows = isLinked ? derivedSegmentRows : customSegmentRows;
useEffect(() => {
if (!segmentRows.length) {'''
if old4 not in content: print('Failed old4')
content = content.replace(old4, new4)
# 5. handleUpload changes
old5 = ''' const handleUpload = async () => {
if (!project) {
setError("Configure project settings before uploading.");
setProjectDrawerOpen(true);
return;
}
if (videoFiles.length === 0) return;
setError(null);
setIsUploading(true);
try {
const uploadedItems = await uploadVideos(videoFiles);'''
new5 = ''' const handleUpload = async () => {
if (!project) {
setError("Configure project settings before uploading.");
setProjectDrawerOpen(true);
return;
}
if (videoFiles.length === 0) return;
setError(null);
setIsUploading(true);
setUploadProgress(0);
try {
const uploadedItems = await uploadVideos(videoFiles, (index, total, progress) => {
setUploadProgress((index + progress) / total);
});'''
if old5 not in content: print('Failed old5')
content = content.replace(old5, new5)
# 6. handleUpload linked reset
old6 = ''' setJob(null);
setOutputPrefix(uploaded.filename.replace(/\.[^.]+$/, ""));
}
setVideoFiles([]);'''
new6 = ''' setJob(null);
setOutputPrefix(uploaded.filename.replace(/\.[^.]+$/, ""));
setIsLinked(true);
}
setVideoFiles([]);'''
if old6 not in content: print('Failed old6')
content = content.replace(old6, new6)
# 7. handleSelectVideo reset
old7 = ''' setOutputPrefix(item.filename.replace(/\.[^.]+$/, ""));
setVideoFiles([]);
setPreviewUrl("");
try {'''
new7 = ''' setOutputPrefix(item.filename.replace(/\.[^.]+$/, ""));
setVideoFiles([]);
setPreviewUrl("");
setIsLinked(true);
try {'''
if old7 not in content: print('Failed old7')
content = content.replace(old7, new7)
# 8. commitSegmentDraft bypass
old8 = ''' const snapped = snapToFrame(parsed);
let boundaryIndex = -1;
let boundaryValue = snapped;'''
new8 = ''' const snapped = snapToFrame(parsed);
if (!isLinked) {
setCustomSegmentRows((prev) => {
const next = [...prev];
next[rowIndex] = { ...next[rowIndex], [field]: snapped };
return next;
});
setPendingSegmentEditPersist(true);
return;
}
let boundaryIndex = -1;
let boundaryValue = snapped;'''
if old8 not in content: print('Failed old8')
content = content.replace(old8, new8)
# 9. handleDeleteSegment bypass
old9 = ''' const handleDeleteSegment = async (row: SegmentRow) => {
setError(null);
if (row.kind === "intro") {'''
new9 = ''' const handleDeleteSegment = async (row: SegmentRow) => {
setError(null);
if (!isLinked) {
setCustomSegmentRows((prev) => prev.filter((r) => r !== row));
setPendingSegmentEditPersist(true);
return;
}
if (row.kind === "intro") {'''
if old9 not in content: print('Failed old9')
content = content.replace(old9, new9)
# 10. upload progress UI
old10 = ''' <Box>
<Button
variant="contained"
onClick={handleUpload}
disabled={videoFiles.length === 0 || isBusy || !project}
>
{isUploading ? "Uploading..." : "Upload to Backend"}
</Button>
<Button sx={{ ml: 1 }} onClick={() => setProjectDrawerOpen(true)}>
Project settings
</Button>
</Box>
<Box>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>'''
new10 = ''' <Box>
<Button
variant="contained"
onClick={handleUpload}
disabled={videoFiles.length === 0 || isBusy || !project}
>
{isUploading ? "Uploading..." : "Upload to Backend"}
</Button>
<Button sx={{ ml: 1 }} onClick={() => setProjectDrawerOpen(true)}>
Project settings
</Button>
</Box>
{isUploading && (
<Box sx={{ mt: 1 }}>
<LinearProgress variant="determinate" value={uploadProgress * 100} />
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
Uploading... {Math.round(uploadProgress * 100)}%
</Typography>
</Box>
)}
<Box>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>'''
if old10 not in content: print('Failed old10')
content = content.replace(old10, new10)
# 11. segments table unlink button
old11 = ''' <Grid item xs={12}>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems={{ sm: "center" }}>
<Button variant="outlined" onClick={handleAddMarker} disabled={!duration}>
Add marker at playhead
</Button>
<Button variant="outlined" onClick={handleSaveMarkers} disabled={!video || markers.length === 0}>
Save markers
</Button>
</Stack>
</Grid>'''
new11 = ''' <Grid item xs={12}>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems={{ sm: "center" }}>
<Button variant="outlined" onClick={handleAddMarker} disabled={!duration}>
Add marker at playhead
</Button>
<Button variant="outlined" onClick={handleSaveMarkers} disabled={!video || markers.length === 0}>
Save markers
</Button>
<Button
variant="outlined"
startIcon={isLinked ? <LinkOff /> : <Link />}
onClick={() => {
if (isLinked) {
setCustomSegmentRows(derivedSegmentRows);
}
setIsLinked(!isLinked);
}}
disabled={!duration}
>
{isLinked ? "Unlink timestamps" : "Link timestamps"}
</Button>
</Stack>
</Grid>'''
if old11 not in content: print('Failed old11')
content = content.replace(old11, new11)
with open('frontend/src/App.tsx', 'w', encoding='utf-8') as f:
f.write(content)
patch_file()
-9
View File
@@ -1,9 +0,0 @@
with open("frontend/src/api.ts", "r", encoding="utf-8") as f:
content = f.read()
import re
content = re.sub(r"return request\(.*?/segment-edits\);", "return request(`/api/videos/${videoId}/segment-edits`);", content)
with open("frontend/src/api.ts", "w", encoding="utf-8") as f:
f.write(content)
print("api.ts fixed properly")
-34
View File
@@ -1,34 +0,0 @@
with open("frontend/src/App.tsx", "r", encoding="utf-8") as f:
content = f.read()
old_add_marker = """ const handleAddMarker = () => {
if (!duration) return;
const snapped = snapToFrame(cursorTime);
setMarkers((prev) => normalizeMarkers([...prev, snapped]));
};"""
new_add_marker = """ const handleAddMarker = async () => {
if (!video || !duration) return;
const snapped = snapToFrame(cursorTime);
const nextMarkers = normalizeMarkers([...markers, snapped]);
setMarkers(nextMarkers);
try {
await replaceMarkers(video.id, nextMarkers);
setPendingSegmentEditPersist(true);
} catch (err) {
setError((err as Error).message);
}
};"""
content = content.replace(old_add_marker, new_add_marker)
# Remove "Save markers" button
old_button = """ <Button variant="outlined" onClick={handleSaveMarkers} disabled={!video || markers.length === 0}>
Save markers
</Button>"""
content = content.replace(old_button, "")
with open("frontend/src/App.tsx", "w", encoding="utf-8") as f:
f.write(content)
print("App.tsx add marker autosave patched")
-131
View File
@@ -1,131 +0,0 @@
with open("frontend/src/App.tsx", "r", encoding="utf-8") as f:
content = f.read()
old_export_progress = """ {(job?.kind === "split" || job?.kind === "split_all") && job.status !== "completed" && (
<Box>
<LinearProgress variant="determinate" value={Math.round(job.progress * 100)} />
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{job.message || `Status: ${job.status}`}
</Typography>
{job.details && (
<Stack direction={{ xs: "column", sm: "row" }} spacing={1} sx={{ mt: 1 }}>
<Chip size="small" label={`Stage: ${job.details.stage ?? "working"}`} />
<Chip size="small" label={`Stage ETA: ${job.details.stage_eta ?? "calculating"}`} />
<Chip size="small" label={`Total ETA: ${job.details.total_eta ?? "calculating"}`} />
</Stack>
)}
</Box>
)}"""
new_export_progress = """ {(job?.kind === "split" || job?.kind === "split_all") && job.status !== "completed" && (
<Box sx={{ p: 2, bgcolor: "background.paper", borderRadius: 4, border: "1px solid", borderColor: "divider" }}>
<Typography variant="h6" gutterBottom>
{job.kind === "split_all" ? "Bulk Export Progress" : "Export Progress"}
</Typography>
{job.kind === "split_all" && job.details?.label && (() => {
const match = /^(\d+)\\/(\d+) (.+)$/.exec(job.details.label);
if (match) {
const index = Number(match[1]);
const total = Number(match[2]);
const filename = match[3];
const videoProgress = Math.max(0, Math.min(100, (job.progress - (index - 1) / total) * total * 100));
return (
<Box sx={{ mb: 3 }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
Overall Bulk Progress ({index}/{total} videos)
</Typography>
<LinearProgress
variant="determinate"
value={Math.round(job.progress * 100)}
sx={{ height: 12, borderRadius: 6, mb: 2, bgcolor: "primary.light" }}
/>
<Typography variant="body2" color="text.primary" sx={{ fontWeight: "bold" }}>
Current Video: {filename}
</Typography>
<LinearProgress
variant="determinate"
value={Math.round(videoProgress)}
color="secondary"
sx={{ height: 8, borderRadius: 4, mb: 2 }}
/>
<Typography variant="body2" color="text.secondary">
Current Stage: {job.details.stage || "working"}
</Typography>
<LinearProgress
variant="determinate"
value={job.details.stage_percent || 0}
color="info"
sx={{ height: 4, borderRadius: 2 }}
/>
</Box>
);
}
return null;
})()}
{job.kind === "split" && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.primary" gutterBottom>
Video Progress
</Typography>
<LinearProgress
variant="determinate"
value={Math.round(job.progress * 100)}
color="secondary"
sx={{ height: 8, borderRadius: 4, mb: 2 }}
/>
<Typography variant="body2" color="text.secondary">
Current Stage: {job.details?.stage || "working"}
</Typography>
<LinearProgress
variant="determinate"
value={job.details?.stage_percent || 0}
color="info"
sx={{ height: 4, borderRadius: 2 }}
/>
</Box>
)}
<Stack direction={{ xs: "column", sm: "row" }} spacing={1} sx={{ mt: 2 }}>
{job.details?.stage_eta && <Chip size="small" label={`Stage ETA: ${job.details.stage_eta}`} />}
{job.details?.total_eta && <Chip size="small" label={`Total ETA: ${job.details.total_eta}`} />}
<Chip size="small" color={job.status === "running" ? "primary" : "default"} label={`Status: ${job.status}`} />
</Stack>
{job.kind === "split_all" && (
<Box sx={{ mt: 3, p: 2, bgcolor: "background.default", borderRadius: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Queue Queue
</Typography>
<Stack spacing={0.5}>
{videoList.map((v, i) => {
const match = job.details?.label ? /^(\d+)\\/(\d+) (.+)$/.exec(job.details.label) : null;
const currentIndex = match ? Number(match[1]) - 1 : -1;
const status = i < currentIndex ? "completed" : i === currentIndex ? "running" : "pending";
return (
<Typography
key={v.id}
variant="body2"
color={status === "completed" ? "success.main" : status === "running" ? "primary.main" : "text.disabled"}
sx={{ fontWeight: status === "running" ? "bold" : "normal" }}
>
{status === "completed" ? "" : status === "running" ? "" : ""}
{v.filename}
</Typography>
);
})}
</Stack>
</Box>
)}
</Box>
)}"""
content = content.replace(old_export_progress, new_export_progress)
with open("frontend/src/App.tsx", "w", encoding="utf-8") as f:
f.write(content)
print("App.tsx export UI patched")
-43
View File
@@ -1,43 +0,0 @@
with open("frontend/src/App.tsx", "r", encoding="utf-8") as f:
content = f.read()
import re
# Add listSegmentEdits to import
content = content.replace("listMarkers,", "listMarkers,\n listSegmentEdits,")
# Fetch segment_edits in handleSelectVideo
old_handle_select = """ try {
const response = await listMarkers(item.id);
setMarkers(normalizeMarkers(response.markers ?? []));
} catch (err) {"""
new_handle_select = """ try {
const response = await listMarkers(item.id);
setMarkers(normalizeMarkers(response.markers ?? []));
const editsResponse = await listSegmentEdits(item.id);
if (editsResponse.segments && editsResponse.segments.length > 0) {
setIsLinked(false);
setCustomSegmentRows(
editsResponse.segments.map((se) => ({
kind: se.segment_key.startsWith("segment_") ? "core" : (se.segment_key as any),
label: se.segment_key,
start: se.start_seconds,
end: se.end_seconds,
startEditable: true,
endEditable: true,
coreIndex: se.segment_key.startsWith("segment_") ? parseInt(se.segment_key.split("_")[1]) - 1 : undefined,
}))
);
} else {
setIsLinked(true);
setCustomSegmentRows([]);
}
} catch (err) {"""
content = content.replace(old_handle_select, new_handle_select)
with open("frontend/src/App.tsx", "w", encoding="utf-8") as f:
f.write(content)
print("App.tsx fetch segment_edits patched")
-22
View File
@@ -1,22 +0,0 @@
with open("frontend/src/App.tsx", "r", encoding="utf-8") as f:
content = f.read()
old_btn = """ <Button
variant="outlined"
startIcon={isLinked ? <LinkOff /> : <Link />}
onClick={() => {
if (isLinked) {
setCustomSegmentRows(derivedSegmentRows);
}
setIsLinked(!isLinked);
}}
disabled={!duration}
>
{isLinked ? "Unlink timestamps" : "Link timestamps"}
</Button>"""
content = content.replace(old_btn, "")
with open("frontend/src/App.tsx", "w", encoding="utf-8") as f:
f.write(content)
print("Removed global unlink button")
-22
View File
@@ -1,22 +0,0 @@
with open('backend/app/db.py', 'r', encoding='utf-8') as f:
content = f.read()
new_func = '''def list_segment_edits(video_id: str) -> list[dict]:
with _DB_LOCK:
conn = get_conn()
cur = conn.cursor()
cur.execute(
"SELECT * FROM segment_edits WHERE video_id = ? ORDER BY segment_key",
(video_id,),
)
rows = cur.fetchall()
conn.close()
return [_row_to_dict(row) for row in rows]
def replace_segment_edits'''
content = content.replace('def replace_segment_edits', new_func)
with open('backend/app/db.py', 'w', encoding='utf-8') as f:
f.write(content)
print('db.py patched')
-15
View File
@@ -1,15 +0,0 @@
with open('backend/app/main.py', 'r', encoding='utf-8') as f:
content = f.read()
new_func = '''@app.get("/api/videos/{video_id}/segment-edits", response_model=dict[str, list[schemas.SegmentEditOut]])
def list_segment_edits(video_id: str) -> dict:
_get_video_or_404(video_id)
return {"segments": db.list_segment_edits(video_id)}
@app.put("/api/videos/{video_id}/segment-edits"'''
content = content.replace('@app.put("/api/videos/{video_id}/segment-edits"', new_func)
with open('backend/app/main.py', 'w', encoding='utf-8') as f:
f.write(content)
print('main.py patched')
-146
View File
@@ -1,146 +0,0 @@
with open("backend/app/main.py", "r", encoding="utf-8") as f:
content = f.read()
old_split_logic = """ def run_job() -> dict:
project_dir = _project_dir(project["id"])
output_dir = project_dir / "outputs" / video_id
temp_dir = JOBS_DIR / job_id
progress = _make_progress_updater(job_id, Path(video["filename"]).stem)
outputs = mkv.build_episodes(
video["file_path"],
video["duration_seconds"],
project["intro_seconds"],
project["outro_seconds"],
markers,
output_dir,
temp_dir,
project_dir,
output_prefix=output_prefix,
reencode=project["reencode_enabled"],
ffmpeg_pass1_template=project["ffmpeg_pass1_template"],
ffmpeg_pass2_template=project["ffmpeg_pass2_template"],
progress_cb=progress,
log_cb=lambda message: job_manager.log(job_id, message),
)"""
new_split_logic = """ segment_edits = db.list_segment_edits(video_id)
custom_segments = None
if segment_edits:
custom_segments = [(se["start_seconds"], se["end_seconds"]) for se in segment_edits if se["segment_key"].startswith("segment_")]
def run_job() -> dict:
project_dir = _project_dir(project["id"])
output_dir = project_dir / "outputs" / video_id
temp_dir = JOBS_DIR / job_id
progress = _make_progress_updater(job_id, Path(video["filename"]).stem)
outputs = mkv.build_episodes(
video["file_path"],
video["duration_seconds"],
project["intro_seconds"],
project["outro_seconds"],
markers,
output_dir,
temp_dir,
project_dir,
output_prefix=output_prefix,
reencode=project["reencode_enabled"],
ffmpeg_pass1_template=project["ffmpeg_pass1_template"],
ffmpeg_pass2_template=project["ffmpeg_pass2_template"],
progress_cb=progress,
log_cb=lambda message: job_manager.log(job_id, message),
custom_segments=custom_segments,
)"""
content = content.replace(old_split_logic, new_split_logic)
old_split_all_ready = """ ready: list[tuple[dict, list[float]]] = []
skipped: list[dict] = []
for video in videos:
try:
_validate_durations(video["duration_seconds"], project["intro_seconds"], project["outro_seconds"])
except HTTPException as exc:
skipped.append({"video_id": video["id"], "filename": video["filename"], "reason": exc.detail})
continue
markers = sorted(db.list_markers(video["id"]))
if not markers:
skipped.append({"video_id": video["id"], "filename": video["filename"], "reason": "No saved markers"})
continue
ready.append((video, markers))"""
new_split_all_ready = """ ready: list[tuple[dict, list[float], list[tuple[float, float]] | None]] = []
skipped: list[dict] = []
for video in videos:
try:
_validate_durations(video["duration_seconds"], project["intro_seconds"], project["outro_seconds"])
except HTTPException as exc:
skipped.append({"video_id": video["id"], "filename": video["filename"], "reason": exc.detail})
continue
markers = sorted(db.list_markers(video["id"]))
segment_edits = db.list_segment_edits(video["id"])
custom_segments = None
if segment_edits:
custom_segments = [(se["start_seconds"], se["end_seconds"]) for se in segment_edits if se["segment_key"].startswith("segment_")]
if not markers and not custom_segments:
skipped.append({"video_id": video["id"], "filename": video["filename"], "reason": "No saved markers or segments"})
continue
ready.append((video, markers, custom_segments))"""
content = content.replace(old_split_all_ready, new_split_all_ready)
old_split_all_loop = """ for video_index, (video, markers) in enumerate(ready, start=1):
label = f"{video_index}/{total_videos} {Path(video['filename']).stem}"
output_dir = project_dir / "outputs" / video["id"]
temp_dir = JOBS_DIR / job_id / video["id"]
output_prefix = _sanitize_prefix(Path(video["filename"]).stem)
job_manager.log(job_id, f"Starting export for {video['filename']}")
outputs = mkv.build_episodes(
video["file_path"],
video["duration_seconds"],
project["intro_seconds"],
project["outro_seconds"],
markers,
output_dir,
temp_dir,
project_dir,
output_prefix=output_prefix,
reencode=project["reencode_enabled"],
ffmpeg_pass1_template=project["ffmpeg_pass1_template"],
ffmpeg_pass2_template=project["ffmpeg_pass2_template"],
progress_cb=progress,
log_cb=lambda message: job_manager.log(job_id, message),
)"""
new_split_all_loop = """ for video_index, (video, markers, custom_segments) in enumerate(ready, start=1):
label = f"{video_index}/{total_videos} {Path(video['filename']).stem}"
output_dir = project_dir / "outputs" / video["id"]
temp_dir = JOBS_DIR / job_id / video["id"]
output_prefix = _sanitize_prefix(Path(video["filename"]).stem)
job_manager.log(job_id, f"Starting export for {video['filename']}")
outputs = mkv.build_episodes(
video["file_path"],
video["duration_seconds"],
project["intro_seconds"],
project["outro_seconds"],
markers,
output_dir,
temp_dir,
project_dir,
output_prefix=output_prefix,
reencode=project["reencode_enabled"],
ffmpeg_pass1_template=project["ffmpeg_pass1_template"],
ffmpeg_pass2_template=project["ffmpeg_pass2_template"],
progress_cb=progress,
log_cb=lambda message: job_manager.log(job_id, message),
custom_segments=custom_segments,
)"""
content = content.replace(old_split_all_loop, new_split_all_loop)
with open("backend/app/main.py", "w", encoding="utf-8") as f:
f.write(content)
print("main.py export logic patched")
-102
View File
@@ -1,102 +0,0 @@
with open("backend/app/mkv.py", "r", encoding="utf-8") as f:
content = f.read()
import re
old_sig = """def build_episodes(
video_path: str,
total_duration: float,
intro_seconds: float,
outro_seconds: float,
cut_points: list[float],
output_dir: Path,
temp_dir: Path,
project_dir: Path,
output_prefix: str = "episode",
reencode: bool = False,
ffmpeg_pass1_template: str | None = None,
ffmpeg_pass2_template: str | None = None,
progress_cb: ProgressCallback | None = None,
log_cb: LogCallback | None = None,
) -> list[str]:"""
new_sig = """def build_episodes(
video_path: str,
total_duration: float,
intro_seconds: float,
outro_seconds: float,
cut_points: list[float],
output_dir: Path,
temp_dir: Path,
project_dir: Path,
output_prefix: str = "episode",
reencode: bool = False,
ffmpeg_pass1_template: str | None = None,
ffmpeg_pass2_template: str | None = None,
progress_cb: ProgressCallback | None = None,
log_cb: LogCallback | None = None,
custom_segments: list[tuple[float, float]] | None = None,
) -> list[str]:"""
content = content.replace(old_sig, new_sig)
old_logic = """ core_end = max(intro_seconds, total_duration - outro_seconds)
boundaries = [p for p in sorted(cut_points) if intro_seconds < p < core_end]
boundaries.append(core_end)
outputs: list[str] = []
prev = intro_seconds
safe_boundaries: list[float] = []
for end in boundaries:
if end - prev <= min_segment:
continue
safe_boundaries.append(end)
prev = end
if not safe_boundaries:
raise RuntimeError("No valid segments after filtering short ranges")
prev = intro_seconds
total_segments = len(safe_boundaries)
core_ranges: list[tuple[int, float, float]] = []
for index, end in enumerate(safe_boundaries, start=1):
core_ranges.append((index, prev, end))
prev = end"""
new_logic = """ outputs: list[str] = []
core_ranges: list[tuple[int, float, float]] = []
min_segment = 0.001
if custom_segments and len(custom_segments) > 0:
total_segments = len(custom_segments)
for index, (start, end) in enumerate(custom_segments, start=1):
core_ranges.append((index, start, end))
else:
core_end = max(intro_seconds, total_duration - outro_seconds)
boundaries = [p for p in sorted(cut_points) if intro_seconds < p < core_end]
boundaries.append(core_end)
prev = intro_seconds
safe_boundaries: list[float] = []
for end in boundaries:
if end - prev <= min_segment:
continue
safe_boundaries.append(end)
prev = end
if not safe_boundaries:
raise RuntimeError("No valid segments after filtering short ranges")
prev = intro_seconds
total_segments = len(safe_boundaries)
for index, end in enumerate(safe_boundaries, start=1):
core_ranges.append((index, prev, end))
prev = end"""
content = content.replace(old_logic, new_logic)
with open("backend/app/mkv.py", "w", encoding="utf-8") as f:
f.write(content)
print("mkv.py patched")
-114
View File
@@ -1,114 +0,0 @@
with open("frontend/src/App.tsx", "r", encoding="utf-8") as f:
content = f.read()
# Add ContentPaste to imports
content = content.replace("CallSplit,", "CallSplit,\n ContentPaste,")
content = content.replace("import { IconButton, Tooltip } from \"@mui/material\";", "import { IconButton, Tooltip, InputAdornment } from \"@mui/material\";")
if "InputAdornment" not in content:
content = content.replace("TextField,", "TextField,\n InputAdornment,")
old_start_input = """ <TextField
variant="outlined"
size="small"
value={segmentDrafts[rowIndex]?.start ?? formatTime(row.start)}
onChange={(e) => handleSegmentDraftChange(rowIndex, "start", e.target.value)}
onBlur={() => commitSegmentDraft(rowIndex, "start")}
onKeyDown={(e) => {
if (e.key === "Enter") {
commitSegmentDraft(rowIndex, "start");
}
}}
disabled={!row.startEditable}
inputProps={{ style: { fontFamily: "monospace" } }}
/>"""
new_start_input = """ <TextField
variant="outlined"
size="small"
value={segmentDrafts[rowIndex]?.start ?? formatTime(row.start)}
onChange={(e) => handleSegmentDraftChange(rowIndex, "start", e.target.value)}
onBlur={() => commitSegmentDraft(rowIndex, "start")}
onKeyDown={(e) => {
if (e.key === "Enter") {
commitSegmentDraft(rowIndex, "start");
}
}}
disabled={!row.startEditable}
inputProps={{ style: { fontFamily: "monospace" } }}
InputProps={{
endAdornment: row.startEditable ? (
<InputAdornment position="end" sx={{ opacity: 0, transition: "opacity 0.2s", ".MuiInputBase-root:hover &": { opacity: 1 } }}>
<IconButton
size="small"
onMouseDown={async (e) => {
e.preventDefault();
try {
const text = await navigator.clipboard.readText();
handleSegmentDraftChange(rowIndex, "start", text);
setTimeout(() => commitSegmentDraft(rowIndex, "start"), 50);
} catch (err) {}
}}
>
<ContentPaste fontSize="small" />
</IconButton>
</InputAdornment>
) : null
}}
/>"""
content = content.replace(old_start_input, new_start_input)
old_end_input = """ <TextField
variant="outlined"
size="small"
value={segmentDrafts[rowIndex]?.end ?? formatTime(row.end)}
onChange={(e) => handleSegmentDraftChange(rowIndex, "end", e.target.value)}
onBlur={() => commitSegmentDraft(rowIndex, "end")}
onKeyDown={(e) => {
if (e.key === "Enter") {
commitSegmentDraft(rowIndex, "end");
}
}}
disabled={!row.endEditable}
inputProps={{ style: { fontFamily: "monospace" } }}
/>"""
new_end_input = """ <TextField
variant="outlined"
size="small"
value={segmentDrafts[rowIndex]?.end ?? formatTime(row.end)}
onChange={(e) => handleSegmentDraftChange(rowIndex, "end", e.target.value)}
onBlur={() => commitSegmentDraft(rowIndex, "end")}
onKeyDown={(e) => {
if (e.key === "Enter") {
commitSegmentDraft(rowIndex, "end");
}
}}
disabled={!row.endEditable}
inputProps={{ style: { fontFamily: "monospace" } }}
InputProps={{
endAdornment: row.endEditable ? (
<InputAdornment position="end" sx={{ opacity: 0, transition: "opacity 0.2s", ".MuiInputBase-root:hover &": { opacity: 1 } }}>
<IconButton
size="small"
onMouseDown={async (e) => {
e.preventDefault();
try {
const text = await navigator.clipboard.readText();
handleSegmentDraftChange(rowIndex, "end", text);
setTimeout(() => commitSegmentDraft(rowIndex, "end"), 50);
} catch (err) {}
}}
>
<ContentPaste fontSize="small" />
</IconButton>
</InputAdornment>
) : null
}}
/>"""
content = content.replace(old_end_input, new_end_input)
with open("frontend/src/App.tsx", "w", encoding="utf-8") as f:
f.write(content)
print("App.tsx paste buttons patched")
-3
View File
@@ -1,3 +0,0 @@
with open("backend/app/mkv.py", "r", encoding="utf-8") as f:
lines = f.readlines()
print("".join(lines[350:400]))