feat: replace SFTP drop with Syncthing share path

Remove SFTPGo; mount event data from the Syncthing folder, watch
incoming/, and name variants after the source stem.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank Schwenk
2026-07-18 14:06:57 +02:00
parent 83d4468b69
commit d5b44b221e
13 changed files with 167 additions and 189 deletions
+59 -9
View File
@@ -210,7 +210,7 @@ def preprocess_original(paths: JobPaths) -> JobPaths:
"""Downscale to MAX_EDGE_PX and normalize to sRGB JPEG as `original.jpg`.
Replaces any non-jpg original in the job dir. Side-effect free for the
inbox copy — only mutates jobs/<id>/.
incoming copy — only mutates jobs/<id>/.
"""
src = paths.original
if not src.exists():
@@ -371,12 +371,37 @@ def next_variant_id(manifest: dict[str, Any]) -> str:
return f"v{i}"
_UNSAFE_STEM_RE = re.compile(r"[^\w.\-]+", re.UNICODE)
def sanitize_stem(name: str) -> str:
"""Safe basename stem for variant files (no path separators / junk)."""
stem = Path(name).stem if name else ""
stem = stem.replace("/", "_").replace("\\", "_").strip().strip(".")
stem = _UNSAFE_STEM_RE.sub("_", stem).strip("._")
return stem or "photo"
def variant_filename(stem: str, variant_id: str) -> str:
return f"{sanitize_stem(stem)}_{variant_id}.png"
def resolve_source_stem(manifest: dict[str, Any] | None, paths: JobPaths) -> str:
"""Prefer manifest source_stem; fall back to original path stem."""
if manifest:
stored = manifest.get("source_stem")
if isinstance(stored, str) and stored.strip():
return sanitize_stem(stored)
return sanitize_stem(paths.original.stem)
def compose_variant(
paths: JobPaths,
assets: FilterAssets,
*,
variant_id: str,
source: str,
variant_stem: str,
bg_name: str | None = None,
bg_mode: str | None = None,
fg_name: str | None = None,
@@ -389,10 +414,13 @@ def compose_variant(
If bg_name/fg_name/bg_mode/fg_mode are given (remix path) they are used
directly. Otherwise a random working filter is picked with retries,
exactly like make_random.py's compose_one().
Final file is `variants/{stem}_{variant_id}.png`; manifest id stays `vN`.
"""
rng = rng or random.Random()
tmp_dir = paths.intermediates
tmp_dir.mkdir(parents=True, exist_ok=True)
stem = sanitize_stem(variant_stem)
bg_mode = bg_mode or rng.choice(assets.blend_modes)
fg_mode = fg_mode or rng.choice(assets.blend_modes)
@@ -421,7 +449,7 @@ def compose_variant(
"step2": tmp_dir / f"{variant_id}_rembg_alpha.png",
"fg_filtered": tmp_dir / f"{variant_id}_fg_filtered.png",
"composed": tmp_dir / f"{variant_id}_composed.png",
"final": paths.variants / f"{variant_id}.png",
"final": paths.variants / variant_filename(stem, variant_id),
}
paths.variants.mkdir(parents=True, exist_ok=True)
@@ -467,13 +495,19 @@ def compose_variant(
}
def process_job(job_id: str, source_path: Path, original_suffix: str) -> None:
"""Full pipeline for a freshly ingested inbox file: preprocess, rembg
def process_job(
job_id: str,
source_path: Path,
original_suffix: str,
*,
source_stem: str | None = None,
) -> None:
"""Full pipeline for a freshly ingested incoming file: preprocess, rembg
once, generate OUTPUT_COUNT variants, write manifest + status.
`source_path` must already be a private copy (jobs/<id>/original.*) —
callers (worker.py) are responsible for copying out of inbox first, so
the inbox file itself is never touched here.
callers (worker.py) are responsible for copying out of incoming first, so
the incoming file itself is never touched here.
Variants that hit FILTER_TIMEOUT during the normal pass are retried
once at the end with FILTER_TIMEOUT_LONG. Filter probes in
@@ -483,8 +517,9 @@ def process_job(job_id: str, source_path: Path, original_suffix: str) -> None:
paths.root.mkdir(parents=True, exist_ok=True)
paths.intermediates.mkdir(parents=True, exist_ok=True)
paths.variants.mkdir(parents=True, exist_ok=True)
stem = sanitize_stem(source_stem or source_path.stem)
write_status(paths, "processing", source_file=str(source_path.name))
write_status(paths, "processing", source_file=str(source_path.name), source_stem=stem)
try:
paths = preprocess_original(paths)
@@ -493,6 +528,7 @@ def process_job(job_id: str, source_path: Path, original_suffix: str) -> None:
"job_id": job_id,
"original_file": paths.original.name,
"rembg_file": paths.rembg.name,
"source_stem": stem,
"created_at": now_iso(),
"variants": [],
}
@@ -509,7 +545,14 @@ def process_job(job_id: str, source_path: Path, original_suffix: str) -> None:
for i in range(1, config.OUTPUT_COUNT + 1):
variant_id = f"v{i}"
try:
entry = compose_variant(paths, assets, variant_id=variant_id, source="auto", rng=rng)
entry = compose_variant(
paths,
assets,
variant_id=variant_id,
source="auto",
variant_stem=stem,
rng=rng,
)
manifest["variants"].append(entry)
write_manifest(paths, manifest)
except FilterTimeoutError as exc:
@@ -530,7 +573,14 @@ def process_job(job_id: str, source_path: Path, original_suffix: str) -> None:
with filter_timeout(config.FILTER_TIMEOUT_LONG):
for variant_id in timeout_retries:
try:
entry = compose_variant(paths, assets, variant_id=variant_id, source="auto", rng=rng)
entry = compose_variant(
paths,
assets,
variant_id=variant_id,
source="auto",
variant_stem=stem,
rng=rng,
)
manifest["variants"].append(entry)
write_manifest(paths, manifest)
except PipelineError as exc: