zip deformation outputs on download
This commit is contained in:
@@ -295,6 +295,9 @@ export default function App() {
|
||||
};
|
||||
|
||||
const fileUrl = (path?: string) => path ? `${API_BASE}/api/file?path=${encodeURIComponent(path)}` : '';
|
||||
const deformationDownloadUrl = (target: string) => deformationJob?.id
|
||||
? `${API_BASE}/api/deformation/download?job=${encodeURIComponent(deformationJob.id)}&target=${encodeURIComponent(target)}`
|
||||
: '';
|
||||
|
||||
const clearDeformationTask = () => {
|
||||
setDeformationJob(null);
|
||||
@@ -829,10 +832,10 @@ export default function App() {
|
||||
<div className={`h-full ${deformationJob?.status === 'failed' ? 'bg-red-500' : 'bg-blue-600'} transition-all`} style={{ width: `${deformationJob?.status === 'completed' ? 100 : progress}%` }} />
|
||||
</div>
|
||||
{deformationJob?.error && <p className="text-[10px] text-red-500 font-bold mt-2 break-all">{deformationJob.error}</p>}
|
||||
{deformationJob?.status === 'completed' && deformationJob.result?.zip?.path && (
|
||||
{deformationJob?.status === 'completed' && deformationJob.result?.outputs && (
|
||||
<a
|
||||
href={fileUrl(deformationJob.result.zip.path)}
|
||||
download={deformationJob.result.zip.name}
|
||||
href={deformationDownloadUrl('all')}
|
||||
download
|
||||
className="mt-3 w-full py-3 bg-green-600 text-white rounded-xl text-xs font-bold hover:bg-green-700 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<Download size={14} /> 下载四状态 ZIP
|
||||
@@ -917,7 +920,6 @@ export default function App() {
|
||||
].map(t => {
|
||||
const screenshotDir = deformationJob?.result?.previews?.screenshots;
|
||||
const imagePath = screenshotDir ? `${screenshotDir}/${t.key}.png` : '';
|
||||
const stateZip = deformationJob?.result?.stateZips?.[t.key];
|
||||
return (
|
||||
<div key={t.key} className="bg-white p-4 rounded-2xl border flex flex-col hover:border-blue-200 transition-colors shadow-sm group min-h-[285px]">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
@@ -934,10 +936,10 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{stateZip?.path && (
|
||||
{deformationJob?.status === 'completed' && deformationJob.result?.outputs?.[t.key] && (
|
||||
<a
|
||||
href={fileUrl(stateZip.path)}
|
||||
download={stateZip.name}
|
||||
href={deformationDownloadUrl(t.key)}
|
||||
download
|
||||
className="mt-3 py-2.5 bg-slate-100 text-slate-600 rounded-xl text-[11px] font-black hover:bg-green-600 hover:text-white transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<Download size={13} /> 下载本状态 DICOM ZIP
|
||||
|
||||
146
web_backend.py
146
web_backend.py
@@ -430,40 +430,6 @@ def zip_folder(source_dir, zip_path):
|
||||
return zip_path
|
||||
|
||||
|
||||
def zip_result_bundle(zip_path, state_zips, preview_paths):
|
||||
zip_path = Path(zip_path).resolve()
|
||||
safe_mkdir(zip_path.parent)
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
|
||||
comparison_path = Path(preview_paths["comparison"]).resolve()
|
||||
screenshots_dir = Path(preview_paths["screenshots"]).resolve()
|
||||
|
||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
for state_key, state_zip in state_zips.items():
|
||||
source_zip = Path(state_zip).resolve()
|
||||
archive.write(
|
||||
source_zip,
|
||||
f"dicom_zips/{source_zip.name}",
|
||||
compress_type=zipfile.ZIP_STORED,
|
||||
)
|
||||
|
||||
if comparison_path.exists():
|
||||
archive.write(
|
||||
comparison_path,
|
||||
f"previews/{comparison_path.name}",
|
||||
)
|
||||
|
||||
if screenshots_dir.exists():
|
||||
for file_path in screenshots_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
archive.write(
|
||||
file_path,
|
||||
Path("previews/process_screenshots") / file_path.relative_to(screenshots_dir),
|
||||
)
|
||||
return zip_path
|
||||
|
||||
|
||||
def read_user_tasks():
|
||||
tasks = read_json_file(USER_TASKS_META, {})
|
||||
return tasks if isinstance(tasks, dict) else {}
|
||||
@@ -595,30 +561,51 @@ def make_preview(input_dir, angle_degrees):
|
||||
}
|
||||
|
||||
|
||||
def zip_metadata(zip_path):
|
||||
zip_path = Path(zip_path).resolve()
|
||||
return {
|
||||
"path": str(zip_path),
|
||||
"name": zip_path.name,
|
||||
"size": zip_path.stat().st_size,
|
||||
}
|
||||
|
||||
|
||||
def serialize_outputs(output_paths, preview_paths, zip_path=None, state_zips=None):
|
||||
def serialize_outputs(output_paths, preview_paths):
|
||||
return {
|
||||
"outputs": {key: str(Path(value).resolve()) for key, value in output_paths.items()},
|
||||
"previews": {
|
||||
"comparison": str(Path(preview_paths["comparison"]).resolve()),
|
||||
"screenshots": str(Path(preview_paths["screenshots"]).resolve()),
|
||||
},
|
||||
"zip": zip_metadata(zip_path) if zip_path else None,
|
||||
"stateZips": {
|
||||
key: zip_metadata(value)
|
||||
for key, value in (state_zips or {}).items()
|
||||
},
|
||||
"zip": None,
|
||||
"stateZips": {},
|
||||
}
|
||||
|
||||
|
||||
def prepare_deformation_zip(job_id, target):
|
||||
job = get_job(job_id)
|
||||
if not job:
|
||||
raise RuntimeError("任务不存在。")
|
||||
if job.get("kind") != "deformation":
|
||||
raise RuntimeError("该任务不是四状态生成任务。")
|
||||
if job.get("status") != "completed":
|
||||
raise RuntimeError("四状态任务尚未完成,无法下载。")
|
||||
|
||||
result = job.get("result") or {}
|
||||
outputs = result.get("outputs") or {}
|
||||
job_root = RESULT_DIR / job_id
|
||||
if target == "all":
|
||||
output_dir = job_root / "four_state_output"
|
||||
if not output_dir.exists():
|
||||
raise RuntimeError("四状态输出目录不存在。")
|
||||
return zip_folder(output_dir, job_root / f"head_ct_morph_{job_id}.zip")
|
||||
|
||||
state_labels = {
|
||||
"original": "original",
|
||||
"hard_boundary": "hard_boundary",
|
||||
"gaussian_smooth": "gaussian_smooth",
|
||||
"soft_transition": "soft_transition",
|
||||
}
|
||||
if target not in state_labels:
|
||||
raise RuntimeError("未知的四状态下载类型。")
|
||||
|
||||
source_dir = Path(outputs.get(target, ""))
|
||||
if not source_dir.exists():
|
||||
raise RuntimeError("该状态的 DICOM 输出目录不存在。")
|
||||
return zip_folder(source_dir, job_root / f"{target}_{job_id}.zip")
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
print("%s - %s" % (self.address_string(), format % args))
|
||||
@@ -679,6 +666,17 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_json({"job": get_user_task_job(username, kind)})
|
||||
return
|
||||
|
||||
if parsed.path == "/api/deformation/download":
|
||||
params = parse_qs(parsed.query)
|
||||
job_id = params.get("job", [""])[0]
|
||||
target = params.get("target", ["all"])[0]
|
||||
try:
|
||||
zip_path = prepare_deformation_zip(job_id, target)
|
||||
self.send_file(zip_path, "application/zip", as_attachment=True)
|
||||
except Exception as exc:
|
||||
self.send_json({"error": str(exc)}, status=400)
|
||||
return
|
||||
|
||||
if parsed.path == "/api/file":
|
||||
params = parse_qs(parsed.query)
|
||||
file_path = Path(unquote(params.get("path", [""])[0])).resolve()
|
||||
@@ -692,15 +690,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
content_type = "video/mp4"
|
||||
elif file_path.suffix.lower() == ".zip":
|
||||
content_type = "application/zip"
|
||||
data = file_path.read_bytes()
|
||||
self.send_response(200)
|
||||
self.send_cors_headers()
|
||||
self.send_header("Content-Type", content_type)
|
||||
if file_path.suffix.lower() == ".zip":
|
||||
self.send_header("Content-Disposition", f'attachment; filename="{file_path.name}"')
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
self.send_file(
|
||||
file_path,
|
||||
content_type,
|
||||
as_attachment=file_path.suffix.lower() == ".zip",
|
||||
)
|
||||
return
|
||||
|
||||
self.send_json({"error": "接口不存在。"}, status=404)
|
||||
@@ -743,25 +737,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
transition_width,
|
||||
progress,
|
||||
)
|
||||
set_job(job_id, message="正在打包各状态 DICOM ZIP...", progress=88)
|
||||
state_zips = {}
|
||||
for state_key in [
|
||||
"original",
|
||||
"hard_boundary",
|
||||
"gaussian_smooth",
|
||||
"soft_transition",
|
||||
]:
|
||||
state_zips[state_key] = zip_folder(
|
||||
output_paths[state_key],
|
||||
job_root / f"{state_key}_{job_id}.zip",
|
||||
)
|
||||
set_job(job_id, message="正在整理四状态总 ZIP...", progress=96)
|
||||
zip_path = zip_result_bundle(
|
||||
job_root / f"head_ct_morph_{job_id}.zip",
|
||||
state_zips,
|
||||
preview_paths,
|
||||
)
|
||||
return serialize_outputs(output_paths, preview_paths, zip_path, state_zips)
|
||||
return serialize_outputs(output_paths, preview_paths)
|
||||
|
||||
self.send_json(
|
||||
start_job(
|
||||
@@ -848,6 +824,22 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return {}
|
||||
return json.loads(body.decode("utf-8"))
|
||||
|
||||
def send_file(self, file_path, content_type="application/octet-stream", as_attachment=False):
|
||||
file_path = Path(file_path).resolve()
|
||||
if not file_path.exists() or not file_path.is_file():
|
||||
self.send_json({"error": "文件不存在。"}, status=404)
|
||||
return
|
||||
|
||||
self.send_response(200)
|
||||
self.send_cors_headers()
|
||||
self.send_header("Content-Type", content_type)
|
||||
if as_attachment:
|
||||
self.send_header("Content-Disposition", f'attachment; filename="{file_path.name}"')
|
||||
self.send_header("Content-Length", str(file_path.stat().st_size))
|
||||
self.end_headers()
|
||||
with file_path.open("rb") as file_handle:
|
||||
shutil.copyfileobj(file_handle, self.wfile)
|
||||
|
||||
def send_cors_headers(self):
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
||||
|
||||
Reference in New Issue
Block a user