feat(v4): add docker-compose and production-ready application

This commit is contained in:
2026-07-09 10:15:44 +03:30
parent 2112e00982
commit ece63caa22
13 changed files with 482 additions and 30 deletions

View File

@@ -14,6 +14,11 @@ from .tasks import run_batch_job
def image_session_create(*, uploaded_file=None, image_base64=None):
"""Create a new image session and its initial S0 upload state.
The session groups all later processing states so a user can present the full workflow.
"""
if uploaded_file and uploaded_file.size > settings.MAX_UPLOAD_MB * 1024 * 1024:
raise ProcessingError(f"Upload exceeds {settings.MAX_UPLOAD_MB} MB.")
@@ -55,6 +60,11 @@ def image_session_create(*, uploaded_file=None, image_base64=None):
def compact_workspace_image(image):
"""Resize large uploads to the configured maximum dimension for faster processing.
This keeps classroom-sized experiments responsive even when the uploaded file is very large.
"""
max_dimension = int(getattr(settings, "IMAGE_WORKSPACE_MAX_DIMENSION", 1400))
if max_dimension <= 0:
return image
@@ -68,6 +78,11 @@ def compact_workspace_image(image):
def image_state_create(*, session, parent, image, operation, params, label=None, prefix="state"):
"""Persist one processed image state with its metadata, parent, and histogram.
Saving every result makes it possible to compare steps and combine previous states later.
"""
relative_path = save_image_array(image, prefix)
max_sequence = session.states.aggregate(value=Max("sequence"))["value"]
sequence = 0 if max_sequence is None else max_sequence + 1
@@ -89,6 +104,8 @@ def image_state_create(*, session, parent, image, operation, params, label=None,
def image_state_payload(*, state, include_image=True):
"""Serialize an image state for the frontend workspace."""
payload = {
"state_id": str(state.id),
"session_id": str(state.session_id),
@@ -114,10 +131,17 @@ def image_state_payload(*, state, include_image=True):
def image_states_payload(*, states):
"""Serialize a list of image states."""
return [image_state_payload(state=state, include_image=True) for state in states]
def image_state_delete(*, state):
"""Delete a non-S0 state while keeping child states available.
Use it to remove unhelpful experiments without losing later useful results.
"""
if state.sequence == 0 or state.operation == "upload":
raise ProcessingError("The original S0 upload state cannot be deleted.")
image_path = state.image
@@ -127,6 +151,11 @@ def image_state_delete(*, state):
def image_state_apply_operation(*, state, operation, params):
"""Apply one registered algorithm to a selected state and save the result as a new state.
This is the main workspace action: every filter or transform becomes a reproducible step.
"""
if state.session.expired:
raise ProcessingError("Image session has expired.")
source = load_image_array(state.image)
@@ -144,6 +173,11 @@ def image_state_apply_operation(*, state, operation, params):
def combine_states(*, states, operation, params=None):
"""Combine registered states using add, subtract, dot product, average, and/or.
Use it for MATLAB-like image arithmetic, change detection, masking, and K-image denoising.
"""
params = params or {}
if len(states) < 2:
raise ProcessingError("At least two states are required.")
@@ -185,6 +219,11 @@ def combine_states(*, states, operation, params=None):
def np_clip_sum(images):
"""Add several registered images and clip the result to [0, 255].
Use it to combine brightness/detail contributions while keeping the output displayable.
"""
total = np.zeros_like(images[0], dtype="float32")
for image in images:
total += image.astype("float32")
@@ -192,6 +231,11 @@ def np_clip_sum(images):
def image_session_process(*, session, operation, params):
"""Run a legacy single-image operation against the original session image.
This keeps the older API working while the state workspace handles the main presentation flow.
"""
if session.expired:
raise ProcessingError("Image session has expired.")
@@ -217,12 +261,19 @@ def image_session_process(*, session, operation, params):
def batch_job_create(*, operation, session_ids, params=None):
"""Create a Celery-backed processing job for heavier batch operations.
Use it when an operation may take longer than an interactive request should block.
"""
job = ProcessingJob.objects.create(operation=operation, params=params or {})
run_batch_job.delay(str(job.id), operation, [str(session_id) for session_id in session_ids])
return job
def processing_job_payload(*, job):
"""Serialize a processing job, including result image data when available."""
payload = {
"job_id": str(job.id),
"operation": job.operation,