lightweight image tools

revert: remove async polling logic, restore synchronous behavior

The Hack Club AI proxy doesn't support async predictions API,
so reverting to the original synchronous approach that works.

Kept: token validation, file size tracking, status positioning

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>

dunkirk.sh 8d4bbb41 593cdbe6

verified
+36 -182
+23 -91
public/index.html
··· 699 699 if (!currentImage) return; 700 700 701 701 hideError(); 702 - showStatus('Starting background removal...'); 702 + showStatus('Removing background...'); 703 703 document.getElementById('removeBgBtn').disabled = true; 704 704 705 705 // Get current image as data URL ··· 713 713 const selectedModel = document.getElementById('bgRemoverModel').value; 714 714 715 715 try { 716 - // Start the prediction 717 716 const res = await fetch('/api/remove-bg', { 718 717 method: 'POST', 719 718 headers: { 'Content-Type': 'application/json' }, ··· 722 721 const data = await res.json(); 723 722 if (!res.ok) throw new Error(typeof data.error === 'string' ? data.error : JSON.stringify(data.error)); 724 723 725 - // Check if we got direct output (no polling needed) or prediction ID 726 - if (data.predictionId) { 727 - // Poll for status 728 - await pollStatus(data.predictionId, tk); 729 - } else if (data.status === 'succeeded' && data.output) { 730 - // Direct output, load immediately 731 - const img = new Image(); 732 - img.crossOrigin = 'anonymous'; 733 - img.onload = async () => { 734 - currentImage = img; 735 - currentFileSize = await new Promise(resolve => { 736 - const tempCanvas = document.createElement('canvas'); 737 - tempCanvas.width = img.width; 738 - tempCanvas.height = img.height; 739 - tempCanvas.getContext('2d').drawImage(img, 0, 0); 740 - tempCanvas.toBlob(blob => resolve(blob.size), 'image/png'); 741 - }); 742 - hideStatus(); 743 - document.getElementById('removeBgBtn').disabled = false; 744 - updatePreview(); 745 - }; 746 - img.onerror = () => { 747 - hideStatus(); 748 - document.getElementById('removeBgBtn').disabled = false; 749 - showError('Failed to load result image'); 750 - }; 751 - img.src = data.output; 752 - } 724 + const img = new Image(); 725 + img.crossOrigin = 'anonymous'; 726 + img.onload = async () => { 727 + currentImage = img; 728 + // Reset currentFileSize to the new processed image size 729 + currentFileSize = await new Promise(resolve => { 730 + const tempCanvas = document.createElement('canvas'); 731 + tempCanvas.width = img.width; 732 + tempCanvas.height = img.height; 733 + tempCanvas.getContext('2d').drawImage(img, 0, 0); 734 + tempCanvas.toBlob(blob => resolve(blob.size), 'image/png'); 735 + }); 736 + hideStatus(); 737 + document.getElementById('removeBgBtn').disabled = false; 738 + updatePreview(); 739 + }; 740 + img.onerror = () => { 741 + hideStatus(); 742 + document.getElementById('removeBgBtn').disabled = false; 743 + showError('Failed to load result image'); 744 + }; 745 + img.src = data.output; 753 746 } catch (e) { 754 747 hideStatus(); 755 748 document.getElementById('removeBgBtn').disabled = false; 756 749 showError(e.message); 757 750 } 758 - } 759 - 760 - async function pollStatus(predictionId, token) { 761 - const startTime = Date.now(); 762 - 763 - const poll = async () => { 764 - try { 765 - const res = await fetch(`/api/check-status?id=${predictionId}&token=${encodeURIComponent(token)}`); 766 - const data = await res.json(); 767 - 768 - if (!res.ok) throw new Error(typeof data.error === 'string' ? data.error : JSON.stringify(data.error)); 769 - 770 - const elapsed = Math.floor((Date.now() - startTime) / 1000); 771 - 772 - if (data.status === 'succeeded') { 773 - showStatus(`Processing complete (${elapsed}s)`); 774 - 775 - const img = new Image(); 776 - img.crossOrigin = 'anonymous'; 777 - img.onload = async () => { 778 - currentImage = img; 779 - // Reset currentFileSize to the new processed image size 780 - currentFileSize = await new Promise(resolve => { 781 - const tempCanvas = document.createElement('canvas'); 782 - tempCanvas.width = img.width; 783 - tempCanvas.height = img.height; 784 - tempCanvas.getContext('2d').drawImage(img, 0, 0); 785 - tempCanvas.toBlob(blob => resolve(blob.size), 'image/png'); 786 - }); 787 - hideStatus(); 788 - document.getElementById('removeBgBtn').disabled = false; 789 - updatePreview(); 790 - }; 791 - img.onerror = () => { 792 - hideStatus(); 793 - document.getElementById('removeBgBtn').disabled = false; 794 - showError('Failed to load result image'); 795 - }; 796 - img.src = data.output; 797 - } else if (data.status === 'failed') { 798 - hideStatus(); 799 - document.getElementById('removeBgBtn').disabled = false; 800 - showError(data.error || 'Background removal failed'); 801 - } else if (data.status === 'processing' || data.status === 'starting') { 802 - showStatus(`Processing... ${elapsed}s elapsed`); 803 - // Poll again in 2 seconds 804 - setTimeout(poll, 2000); 805 - } else { 806 - // Unknown status, keep polling 807 - showStatus(`Status: ${data.status} (${elapsed}s)`); 808 - setTimeout(poll, 2000); 809 - } 810 - } catch (e) { 811 - hideStatus(); 812 - document.getElementById('removeBgBtn').disabled = false; 813 - showError(e.message); 814 - } 815 - }; 816 - 817 - // Start polling 818 - poll(); 819 751 } 820 752 821 753 // Apply background color (bakes it into the image)
+13 -91
src/index.js
··· 16 16 return handleRemoveBg(request, env); 17 17 } 18 18 19 - if (url.pathname === "/api/check-status" && request.method === "GET") { 20 - return handleCheckStatus(request, env); 21 - } 22 - 23 19 // For non-API routes, return 404 to let assets handle it 24 20 return new Response("Not Found", { status: 404 }); 25 21 }, ··· 44 40 45 41 const input = { image }; 46 42 47 - // Select model based on parameter - use full model ID format 43 + // Select model based on parameter 48 44 const modelId = model === '851labs' 49 45 ? "851-labs/background-remover:a029dff38972b5fda4ec5d75d7d1cd25aeff621d2cf4946a41055d7db66b80bc" 50 46 : "lucataco/remove-bg:95fcc2a26d3899cd6c2691c900465aaeff466285a65c14638cc5f36f34befaf1"; 51 47 52 - console.log("Running model:", modelId); 53 - 54 - // Use replicate.run with stream option to get prediction ID 55 - const prediction = await replicate.run(modelId, { 56 - input, 57 - wait: false // Don't wait for completion 58 - }); 48 + const output = await replicate.run(modelId, { input }); 59 49 60 - console.log("Prediction response:", prediction); 61 - 62 - // Check if we got a prediction object or direct output 63 - if (prediction && prediction.id) { 64 - // Got a prediction object, can poll it 65 - return jsonResponse({ 66 - predictionId: prediction.id, 67 - status: prediction.status 68 - }); 50 + // Handle different output formats 51 + let outputUrl; 52 + if (typeof output === "string") { 53 + outputUrl = output; 54 + } else if (output && typeof output.url === "function") { 55 + outputUrl = output.url(); 56 + } else if (output && output.url) { 57 + outputUrl = output.url; 69 58 } else { 70 - // Got direct output (proxy may have waited), return it 71 - let outputUrl; 72 - if (typeof prediction === "string") { 73 - outputUrl = prediction; 74 - } else if (Array.isArray(prediction) && prediction.length > 0) { 75 - outputUrl = prediction[0]; 76 - } else if (prediction && prediction.url) { 77 - outputUrl = prediction.url; 78 - } else { 79 - outputUrl = prediction; 80 - } 81 - 82 - return jsonResponse({ 83 - status: 'succeeded', 84 - output: outputUrl 85 - }); 86 - } 87 - } catch (e) { 88 - console.error("Error:", e); 89 - return jsonResponse({ error: e.message }, 500); 90 - } 91 - } 92 - 93 - async function handleCheckStatus(request, env) { 94 - try { 95 - const url = new URL(request.url); 96 - const predictionId = url.searchParams.get('id'); 97 - const token = url.searchParams.get('token'); 98 - 99 - if (!predictionId) { 100 - return jsonResponse({ error: "Missing prediction ID" }, 400); 101 - } 102 - 103 - if (!token) { 104 - return jsonResponse({ error: "Missing token" }, 400); 105 - } 106 - 107 - const replicate = new Replicate({ 108 - auth: token, 109 - baseUrl: "https://ai.hackclub.com/proxy/v1/replicate", 110 - }); 111 - 112 - const prediction = await replicate.predictions.get(predictionId); 113 - 114 - // Return status and output if available 115 - const response = { 116 - status: prediction.status, 117 - }; 118 - 119 - if (prediction.status === 'succeeded' && prediction.output) { 120 - // Handle different output formats 121 - let outputUrl; 122 - if (typeof prediction.output === "string") { 123 - outputUrl = prediction.output; 124 - } else if (prediction.output && typeof prediction.output.url === "function") { 125 - outputUrl = prediction.output.url(); 126 - } else if (prediction.output && prediction.output.url) { 127 - outputUrl = prediction.output.url; 128 - } else if (Array.isArray(prediction.output) && prediction.output.length > 0) { 129 - outputUrl = prediction.output[0]; 130 - } else { 131 - outputUrl = prediction.output; 132 - } 133 - response.output = outputUrl; 59 + outputUrl = output; 134 60 } 135 61 136 - if (prediction.status === 'failed') { 137 - response.error = prediction.error || 'Prediction failed'; 138 - } 139 - 140 - return jsonResponse(response); 62 + return jsonResponse({ output: outputUrl }); 141 63 } catch (e) { 142 - console.error("Error checking status:", e); 64 + console.error("Error:", e); 143 65 return jsonResponse({ error: e.message }, 500); 144 66 } 145 67 }