summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-24 10:49:29 +0200
committerPaul Buetow <paul@buetow.org>2026-03-24 10:49:29 +0200
commitbce5acdf8361a27609c3af19ab7b2243ad56da3d (patch)
treefe696143e88a1a6afa2bc79daf6a98ebda656be6
parentc2d54f7a4823ca0de99fdb8cc0a094b0cdf4cbb4 (diff)
photo-enhance: add EXIF auto-rotate and fix ComfyUI cache bypass
- Auto-orient photos before uploading: creates a temp -auto-orient copy via magick, uploads that to ComfyUI (which strips EXIF), deletes temp after upload. Output images are always correctly oriented. - Bust ComfyUI execution cache: inject a random filename_prefix into SaveImage node on each submission. Without this, resubmitting the same image returns execution_cached with empty outputs {}, causing wait_for_output to spin until the 600s timeout. - Detect and raise on cached-but-empty completions: if ComfyUI reports completed=true/success but outputs={}, raise immediately rather than polling until timeout (fallback safety net for edge cases). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rwxr-xr-xphoto-enhance.rb44
1 files changed, 38 insertions, 6 deletions
diff --git a/photo-enhance.rb b/photo-enhance.rb
index 02a281a..63cdcd4 100755
--- a/photo-enhance.rb
+++ b/photo-enhance.rb
@@ -113,6 +113,15 @@ class ComfyUIClient
if result
outputs = extract_output_filenames(result)
return outputs unless outputs.empty?
+
+ # If ComfyUI marks the run complete but outputs are empty, it used a fully
+ # cached execution (execution_cached for all nodes) and wrote no new files.
+ # Raise immediately rather than spinning until timeout.
+ status = result.dig('status', 'status_str')
+ completed = result.dig('status', 'completed')
+ raise "ComfyUI returned empty outputs (cached execution?) for #{prompt_id}" \
+ if completed && status == 'success'
+
# ComfyUI may record the prompt before writing output nodes; keep polling.
end
@@ -272,8 +281,10 @@ class PhotoEnhancer
@out.puts "[#{Time.now.strftime('%H:%M:%S')}] Enhancing #{File.basename(src_path)}..."
- # Inject the input filename into the workflow LoadImage node.
- uploaded_name = @client.upload_image(src_path)
+ # Auto-rotate based on EXIF orientation before uploading. ComfyUI strips EXIF,
+ # so we bake the rotation into a temp file; this ensures output is correctly oriented.
+ upload_path = auto_orient_tempfile(src_path)
+ uploaded_name = @client.upload_image(upload_path)
workflow = inject_input_image(@workflow, uploaded_name)
prompt_id = @client.submit_prompt(workflow)
@out.puts " Submitted prompt #{prompt_id}, waiting for ComfyUI..."
@@ -287,6 +298,7 @@ class PhotoEnhancer
@client.download_output(filenames.first, tmp_path)
convert_to_original_format(tmp_path, dest_path, ext)
File.delete(tmp_path) if File.exist?(tmp_path)
+ File.delete(upload_path) if upload_path != src_path && File.exist?(upload_path)
@manifest.mark_done(src_path)
orig_size = File.size(src_path)
enhanced_size = File.size(dest_path)
@@ -295,6 +307,19 @@ class PhotoEnhancer
@out.puts " ERROR enhancing #{File.basename(src_path)}: #{e.message}"
end
+ # Apply EXIF auto-orientation to a copy of src_path and return the copy's path.
+ # If magick fails (e.g. not installed or no EXIF), returns src_path unchanged so
+ # the caller always has a valid upload path.
+ def auto_orient_tempfile(src_path)
+ ext = File.extname(src_path)
+ tmp = "#{src_path}.orient#{ext}"
+ success = system('magick', src_path, '-auto-orient', tmp)
+ return tmp if success && File.exist?(tmp)
+
+ @out.puts " Warning: auto-orient failed for #{File.basename(src_path)}, uploading original"
+ src_path
+ end
+
# Convert the PNG downloaded from ComfyUI into the desired output format.
# JPEG (.jpg/.jpeg) uses quality 92 to stay close to the original file size.
# All other formats fall back to a straight copy (PNG stays PNG).
@@ -311,14 +336,21 @@ class PhotoEnhancer
(bytes / 1024.0).round
end
- # Replace the placeholder filename in the LoadImage node so the workflow
- # processes the newly uploaded image rather than any hardcoded test image.
+ # Inject the input filename and a unique SaveImage prefix into the workflow.
+ # The unique prefix prevents ComfyUI from returning a fully-cached execution
+ # (outputs: {}) instead of actually running the pipeline and writing output files.
def inject_input_image(workflow, filename)
modified = JSON.parse(JSON.generate(workflow)) # deep dup
+ unique_prefix = "enhanced_#{Digest::SHA256.hexdigest(Time.now.to_f.to_s + rand.to_s)[0, 8]}_"
modified.each_value do |node|
- next unless node.is_a?(Hash) && node['class_type'] == 'LoadImage'
+ next unless node.is_a?(Hash)
- node['inputs']['image'] = filename
+ case node['class_type']
+ when 'LoadImage'
+ node['inputs']['image'] = filename
+ when 'SaveImage'
+ node['inputs']['filename_prefix'] = unique_prefix
+ end
end
modified
end