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 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 = () => {
|
const clearDeformationTask = () => {
|
||||||
setDeformationJob(null);
|
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 className={`h-full ${deformationJob?.status === 'failed' ? 'bg-red-500' : 'bg-blue-600'} transition-all`} style={{ width: `${deformationJob?.status === 'completed' ? 100 : progress}%` }} />
|
||||||
</div>
|
</div>
|
||||||
{deformationJob?.error && <p className="text-[10px] text-red-500 font-bold mt-2 break-all">{deformationJob.error}</p>}
|
{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
|
<a
|
||||||
href={fileUrl(deformationJob.result.zip.path)}
|
href={deformationDownloadUrl('all')}
|
||||||
download={deformationJob.result.zip.name}
|
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"
|
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
|
<Download size={14} /> 下载四状态 ZIP
|
||||||
@@ -917,7 +920,6 @@ export default function App() {
|
|||||||
].map(t => {
|
].map(t => {
|
||||||
const screenshotDir = deformationJob?.result?.previews?.screenshots;
|
const screenshotDir = deformationJob?.result?.previews?.screenshots;
|
||||||
const imagePath = screenshotDir ? `${screenshotDir}/${t.key}.png` : '';
|
const imagePath = screenshotDir ? `${screenshotDir}/${t.key}.png` : '';
|
||||||
const stateZip = deformationJob?.result?.stateZips?.[t.key];
|
|
||||||
return (
|
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 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">
|
<div className="flex justify-between items-center mb-3">
|
||||||
@@ -934,10 +936,10 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{stateZip?.path && (
|
{deformationJob?.status === 'completed' && deformationJob.result?.outputs?.[t.key] && (
|
||||||
<a
|
<a
|
||||||
href={fileUrl(stateZip.path)}
|
href={deformationDownloadUrl(t.key)}
|
||||||
download={stateZip.name}
|
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"
|
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
|
<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
|
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():
|
def read_user_tasks():
|
||||||
tasks = read_json_file(USER_TASKS_META, {})
|
tasks = read_json_file(USER_TASKS_META, {})
|
||||||
return tasks if isinstance(tasks, dict) else {}
|
return tasks if isinstance(tasks, dict) else {}
|
||||||
@@ -595,30 +561,51 @@ def make_preview(input_dir, angle_degrees):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def zip_metadata(zip_path):
|
def serialize_outputs(output_paths, preview_paths):
|
||||||
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):
|
|
||||||
return {
|
return {
|
||||||
"outputs": {key: str(Path(value).resolve()) for key, value in output_paths.items()},
|
"outputs": {key: str(Path(value).resolve()) for key, value in output_paths.items()},
|
||||||
"previews": {
|
"previews": {
|
||||||
"comparison": str(Path(preview_paths["comparison"]).resolve()),
|
"comparison": str(Path(preview_paths["comparison"]).resolve()),
|
||||||
"screenshots": str(Path(preview_paths["screenshots"]).resolve()),
|
"screenshots": str(Path(preview_paths["screenshots"]).resolve()),
|
||||||
},
|
},
|
||||||
"zip": zip_metadata(zip_path) if zip_path else None,
|
"zip": None,
|
||||||
"stateZips": {
|
"stateZips": {},
|
||||||
key: zip_metadata(value)
|
|
||||||
for key, value in (state_zips or {}).items()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
print("%s - %s" % (self.address_string(), 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)})
|
self.send_json({"job": get_user_task_job(username, kind)})
|
||||||
return
|
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":
|
if parsed.path == "/api/file":
|
||||||
params = parse_qs(parsed.query)
|
params = parse_qs(parsed.query)
|
||||||
file_path = Path(unquote(params.get("path", [""])[0])).resolve()
|
file_path = Path(unquote(params.get("path", [""])[0])).resolve()
|
||||||
@@ -692,15 +690,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
content_type = "video/mp4"
|
content_type = "video/mp4"
|
||||||
elif file_path.suffix.lower() == ".zip":
|
elif file_path.suffix.lower() == ".zip":
|
||||||
content_type = "application/zip"
|
content_type = "application/zip"
|
||||||
data = file_path.read_bytes()
|
self.send_file(
|
||||||
self.send_response(200)
|
file_path,
|
||||||
self.send_cors_headers()
|
content_type,
|
||||||
self.send_header("Content-Type", content_type)
|
as_attachment=file_path.suffix.lower() == ".zip",
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.send_json({"error": "接口不存在。"}, status=404)
|
self.send_json({"error": "接口不存在。"}, status=404)
|
||||||
@@ -743,25 +737,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
transition_width,
|
transition_width,
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
set_job(job_id, message="正在打包各状态 DICOM ZIP...", progress=88)
|
return serialize_outputs(output_paths, preview_paths)
|
||||||
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)
|
|
||||||
|
|
||||||
self.send_json(
|
self.send_json(
|
||||||
start_job(
|
start_job(
|
||||||
@@ -848,6 +824,22 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return {}
|
return {}
|
||||||
return json.loads(body.decode("utf-8"))
|
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):
|
def send_cors_headers(self):
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
||||||
|
|||||||
Reference in New Issue
Block a user