diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-24 10:49:29 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-24 10:49:29 +0200 |
| commit | bce5acdf8361a27609c3af19ab7b2243ad56da3d (patch) | |
| tree | fe696143e88a1a6afa2bc79daf6a98ebda656be6 | |
| parent | c2d54f7a4823ca0de99fdb8cc0a094b0cdf4cbb4 (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-x | photo-enhance.rb | 44 |
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 |
