- Two-gate threshold: Otsu >= 60 uses Otsu; 40-59 uses fixed 100
(catches strong scratches on borderline images)
- Remove morphological OPEN after component filtering: it was eroding
thin scratch lines that were correctly detected
- Lower Otsu gate from 60 to 40 to avoid false-negating borderline images
The transparency-fixer now directly detects the baked-in checkerboard
pattern using per-pixel chroma analysis instead of BiRefNet AI matting.
Achromatic pixels in the gray range are classified as background
(transparent), chromatic pixels as foreground (opaque), with smooth
transitions at anti-aliased edges.
- No longer requires Python sidecar or background-removal bundle
- Watermark removal uses Sharp median(5) filter pre-processing
- Moved tool from "ai" to "utilities" category
- Removed from PYTHON_SIDECAR_TOOLS and background-removal enablesTools
- Near-instant processing (pure Sharp, no model inference)
Add TIER_PARAMS dict with fast/balanced/high presets controlling band
size, mask dilation, seam strip width, and Telea pre-inpainting. Parse
tier from sys.argv[7] with balanced fallback. Conditional Telea and
seam refinement steps skip cleanly for fast tier. Progressive outpaint
now accepts band_size and progress bounds for tier-appropriate scaling.
- Fix dispatcher pipe deadlock: drain stdout pipe in a background thread
to prevent blocking when ONNX runtime output exceeds 64KB pipe buffer
- Add 5-minute SSE stall timeout so the UI shows an error instead of
hanging forever when async AI processing stalls
- Guard CPU colorization: skip for images >2MP on CPU and when DDColor
model is not installed, with clear user-facing messages
- Add AVIF decode fallback via ImageMagick for bitstream variants that
Sharp's bundled libheif cannot decode (affects all tools)
AVIF (and other Sharp-native formats) were written as raw bytes to a
.png temp file, causing PIL to fail with "cannot identify image file".
Every other AI module wrapper already converts via sharp().png().toBuffer()
before writing; face-landmarks was the only one that skipped this step.
- Refactor use-tool-processor and use-pipeline-processor hooks
- Enhance dropzone component with improved UX
- Improve seam carving with better error handling and tests
- Add JXL format encoding support to format-encoders
- Update tool routes for consistent format handling
- Add dropzone unit tests
The binary search exited early at 5% tolerance, producing output like
48.3KB for a 50KB target. Reducing to 1% with 12 iterations makes the
output much closer to the requested target.
The binary search found the right quality but sharp(buffer).toBuffer()
re-encoded at default quality 80, inflating the output (e.g. 50KB target
producing 90KB). Replaced buffer-wrapping with proper Sharp pipelines
that include .toFormat() with the proven quality. Also added progressive
dimension reduction when quality alone cannot reach the target, and
tightened tolerance to only accept at-or-below-target results.
CLAHE width/height is tile size in pixels, not tile count. A 3px tile on
a 992x1088 image created ~330x360 independent histogram regions, producing
crosshatch/etching artifacts. Now uses image_dimension/8 (clamped 8-256)
for ~8 tiles per axis. Also strips alpha before enhancement and re-joins
after to prevent CLAHE/normalise/linear from corrupting transparency.
CLAHE provides adaptive local contrast, normalise stretches the
histogram, and gamma adjusts exposure perceptually. Replaces the old
modulate/linear pipeline that compounded errors and darkened images.
Preset multipliers now include clahe and normalise entries.
Key fixes beyond the spec:
- maxSlope rounded to integer (Sharp requirement)
- White balance uses linear() instead of recomb() to avoid float-cast
that breaks CLAHE in the libvips pipeline
- CLAHE tile size adapts to image dimensions (1x1 for tiny images)
- Gamma clamped to Sharp's valid range (1.0-3.0)
- Normalise lower/upper correctly mapped to percentile cutoffs
Contrast score now uses linear stdevLum/1.2 centered at 50 (was
miscalibrated 25-75 range centered at 75). Corrections use dead zones
(score 40-60 = zero) so well-exposed images get near-zero adjustments
instead of being darkened.
Add colorBlindness() operation with 8 simulation matrices (Vienot/Machado)
for protanopia, deuteranopia, tritanopia, protanomaly, deuteranomaly,
tritanomaly, achromatopsia, and blue cone monochromacy.
Users can no longer customize the app name or logo. The branding API
endpoints, permission, frontend UI, env vars (APP_NAME, MAX_LOGO_SIZE_KB),
and all related tests are removed. Includes a migration to clean up
branding data from existing databases.
- Fix resize 20% failure rate: add Zod refine requiring at least one
dimension, enforce integer/max constraints, clamp percentage scaling
to minimum 1px, and guard against missing metadata in withoutEnlargement
- Fix PostHog init race condition: move consent check before async import
so frontend events (search, pageview) are no longer silently dropped
- Fix identify() passing nested $set/$set_once wrappers instead of flat
properties, so version person property now appears on PostHog profiles
- Add error_code and error_message to failed tool_used analytics events
for debugging tool failures from PostHog
The BiRefNetHRMattingSession.predict normalization crashes when all
pixels share the same value (ma == mi). Use a guarded denominator so
uniform-alpha inputs produce a zero mask instead of a NaN explosion.
Also adds _register_birefnet_hr_matting() to install_feature.py so the
HR-matting model can be downloaded during feature installation, matching
the existing registration in remove_bg.py.
The upscale function called runPythonWithProgress without a timeout parameter,
defaulting to the bridge's 10-minute hard limit. On CPU-only systems like
Synology NAS devices, Real-ESRGAN 4x upscaling easily exceeds this for modest
images. Additionally, when the timeout fired on the dispatcher path, the Python
process was left running and blocked all subsequent AI operations.
This fix adds an adaptive timeout based on input megapixels, scale factor, and
GPU availability (180s/effective-MP on CPU, 30s/effective-MP on GPU, floor of
10 minutes). It also kills the dispatcher on timeout so subsequent requests can
proceed via a fresh restart.
Closes#119
The dispatcher was lazy-initialized on first AI request, but a race
condition meant the first call always missed it (dispatcherReady still
false) and fell through to cold per-request Python. initDispatcher()
starts the dispatcher eagerly and returns a Promise that resolves with
GPU status once ready (or after a timeout).