Changeset 3612360
- Timestamp:
- 07/18/2026 08:05:25 AM (3 months ago)
- Location:
- anydress-ai-virtual-try-on
- Files:
-
- 10 added
- 31 edited
- 1 copied
-
assets/screenshot-5.png (modified) ( previous)
-
tags/1.3.1 (copied) (copied from anydress-ai-virtual-try-on/trunk )
-
tags/1.3.1/anydress-ai-virtual-try-on.php (modified) (3 diffs)
-
tags/1.3.1/assets/css/frontend-tryon.css (modified) (1 diff)
-
tags/1.3.1/assets/js/admin-photoshoot.js (modified) (5 diffs)
-
tags/1.3.1/assets/js/admin-size-charts.js (added)
-
tags/1.3.1/assets/js/frontend-tryon.js (modified) (5 diffs)
-
tags/1.3.1/includes/admin/ajax/ajax-generate-edit.php (modified) (3 diffs)
-
tags/1.3.1/includes/admin/ajax/ajax-generate-photoshoot.php (modified) (3 diffs)
-
tags/1.3.1/includes/admin/ajax/ajax-magic-edit.php (modified) (3 diffs)
-
tags/1.3.1/includes/admin/class-anydress-photoshoot.php (modified) (1 diff)
-
tags/1.3.1/includes/admin/class-anydress-product-sizes.php (added)
-
tags/1.3.1/includes/admin/class-anydress-settings.php (modified) (2 diffs)
-
tags/1.3.1/includes/admin/class-anydress-size-charts.php (added)
-
tags/1.3.1/includes/anydress-size-config.php (added)
-
tags/1.3.1/includes/anydress-task-client.php (added)
-
tags/1.3.1/includes/frontend/ajax/ajax-frontend-tryon.php (modified) (4 diffs)
-
tags/1.3.1/includes/frontend/anydress-cron.php (modified) (1 diff)
-
tags/1.3.1/includes/frontend/class-anydress-my-account.php (modified) (4 diffs)
-
tags/1.3.1/includes/frontend/class-anydress-widget.php (modified) (3 diffs)
-
tags/1.3.1/readme.txt (modified) (4 diffs)
-
tags/1.3.1/uninstall.php (modified) (3 diffs)
-
trunk/anydress-ai-virtual-try-on.php (modified) (3 diffs)
-
trunk/assets/css/frontend-tryon.css (modified) (1 diff)
-
trunk/assets/js/admin-photoshoot.js (modified) (5 diffs)
-
trunk/assets/js/admin-size-charts.js (added)
-
trunk/assets/js/frontend-tryon.js (modified) (5 diffs)
-
trunk/includes/admin/ajax/ajax-generate-edit.php (modified) (3 diffs)
-
trunk/includes/admin/ajax/ajax-generate-photoshoot.php (modified) (3 diffs)
-
trunk/includes/admin/ajax/ajax-magic-edit.php (modified) (3 diffs)
-
trunk/includes/admin/class-anydress-photoshoot.php (modified) (1 diff)
-
trunk/includes/admin/class-anydress-product-sizes.php (added)
-
trunk/includes/admin/class-anydress-settings.php (modified) (2 diffs)
-
trunk/includes/admin/class-anydress-size-charts.php (added)
-
trunk/includes/anydress-size-config.php (added)
-
trunk/includes/anydress-task-client.php (added)
-
trunk/includes/frontend/ajax/ajax-frontend-tryon.php (modified) (4 diffs)
-
trunk/includes/frontend/anydress-cron.php (modified) (1 diff)
-
trunk/includes/frontend/class-anydress-my-account.php (modified) (4 diffs)
-
trunk/includes/frontend/class-anydress-widget.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/uninstall.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
anydress-ai-virtual-try-on/tags/1.3.1/anydress-ai-virtual-try-on.php
r3530062 r3612360 4 4 * Plugin URI: https://anydress.ai/plugin/ 5 5 * Description: Elevate your digital boutique with seamless AI virtual try-on. 6 * Version: 1. 0.06 * Version: 1.3.1 7 7 * Author: AnyDress.ai 8 8 * Author URI: https://anydress.ai … … 20 20 21 21 // Define plugin constants for easy reference later 22 define( 'ANYDRESS_VERSION', '1. 0.0' ); // Added version for cache-busting CSS/JS22 define( 'ANYDRESS_VERSION', '1.3.1' ); // Added version for cache-busting CSS/JS 23 23 define( 'ANYDRESS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 24 24 define( 'ANYDRESS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 50 50 } 51 51 52 // 0. Load the shared Size Config (needed by Admin UI, AJAX, and Frontend alike) 53 require_once ANYDRESS_PLUGIN_DIR . 'includes/anydress-size-config.php'; 54 52 55 // 1. Load Admin UI (Only in WP Admin, but NOT during AJAX) 53 56 if ( is_admin() && ! wp_doing_ajax() ) { 54 57 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-settings.php'; 55 58 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-photoshoot.php'; 59 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-size-charts.php'; 60 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-product-sizes.php'; 56 61 } 57 62 58 63 // 2. Load AJAX Handlers (Only when an AJAX request is firing) 59 64 if ( wp_doing_ajax() ) { 65 // Async task status relay (shared by frontend + admin) 66 require_once ANYDRESS_PLUGIN_DIR . 'includes/anydress-task-client.php'; 60 67 // Admin AJAX 61 68 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/ajax/ajax-generate-photoshoot.php'; -
anydress-ai-virtual-try-on/tags/1.3.1/assets/css/frontend-tryon.css
r3530062 r3612360 127 127 border-color: #222; /* Default border */ 128 128 } 129 /* ========================================================================== 130 Sizing & Fit section (optional accurate-fit inputs) 131 ========================================================================== */ 132 .anydress-fit-section { 133 margin: 18px 0 5px; 134 text-align: left; 135 } 136 137 .anydress-fit-section details { 138 border: 1px solid #e2e2e2; 139 border-radius: 8px; 140 background: rgba(255, 255, 255, 0.6); 141 } 142 143 .anydress-fit-section summary { 144 padding: 12px 15px; 145 cursor: pointer; 146 font-weight: 600; 147 font-size: 14px; 148 user-select: none; 149 } 150 151 .anydress-fit-body { 152 padding: 5px 15px 15px; 153 border-top: 1px solid #eee; 154 } 155 156 .anydress-fit-row { 157 display: flex; 158 align-items: center; 159 gap: 12px; 160 margin: 10px 0; 161 font-size: 13px; 162 } 163 164 .anydress-fit-row > label { 165 min-width: 140px; 166 font-weight: 600; 167 } 168 169 .anydress-fit-row select { 170 padding: 6px 10px; 171 border: 1px solid #ccc; 172 border-radius: 6px; 173 max-width: 180px; 174 } 175 176 .anydress-fit-unit-opt { 177 margin-right: 12px; 178 cursor: pointer; 179 } 180 181 .anydress-fit-hint { 182 font-size: 12px; 183 color: #777; 184 margin: 8px 0 10px; 185 line-height: 1.4; 186 } 187 188 .anydress-fit-fields { 189 display: grid; 190 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 191 gap: 10px; 192 } 193 194 .anydress-fit-field label { 195 display: block; 196 font-size: 12px; 197 font-weight: 600; 198 margin-bottom: 3px; 199 line-height: 1.3; 200 } 201 202 .anydress-fit-field small { 203 font-weight: normal; 204 color: #888; 205 } 206 207 .anydress-fit-field input { 208 width: 100%; 209 padding: 6px 8px; 210 border: 1px solid #ccc; 211 border-radius: 6px; 212 font-size: 13px; 213 box-sizing: border-box; 214 } 215 216 .anydress-fit-warning { 217 background: #fff8e5; 218 border-left: 4px solid #dba617; 219 padding: 10px 14px; 220 margin: 12px 0; 221 font-size: 13px; 222 text-align: left; 223 border-radius: 0 6px 6px 0; 224 line-height: 1.4; 225 } -
anydress-ai-virtual-try-on/tags/1.3.1/assets/js/admin-photoshoot.js
r3530062 r3612360 15 15 }); 16 16 } 17 // ====================================================================== 18 // ASYNC TASK RESOLUTION 19 // The server accepts jobs in seconds and returns a task_id; we poll a 20 // lightweight status action until the image is ready (or the task fails). 21 // ====================================================================== 22 async function anydressResolveTask(data) { 23 if (!data || !data.success || !data.data || !data.data.task_id) { 24 return data; // not a task response — pass through unchanged 25 } 26 const taskId = data.data.task_id; 27 28 for (let i = 0; i < 130; i++) { // ~9 minutes max (server times tasks out at 8) 29 await new Promise(r => setTimeout(r, i === 0 ? 6000 : 4000)); 30 try { 31 const fd = new FormData(); 32 fd.append('action', 'anydress_admin_check_task'); 33 fd.append('task_id', taskId); 34 fd.append('nonce', document.getElementById('anydress_nonce').value); 35 36 const resp = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: fd }); 37 const status = await resp.json(); 38 39 if (status && status.success && status.data && status.data.status === 'pending') { 40 continue; // still generating 41 } 42 return status; // completed (has image_url) or failed (error shape) 43 } catch (e) { 44 // transient network hiccup — keep polling 45 } 46 } 47 return { success: false, data: { message: 'Generation timed out. Please try again.' } }; 48 } 49 17 50 let originalImageUrl = ""; 18 51 let currentIsGhost = false; … … 436 469 let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 437 470 let data = await response.json(); 471 data = await anydressResolveTask(data); 438 472 439 473 // Fallback Interceptor … … 441 475 const loadingText = loadingBox.querySelector('p'); 442 476 if(loadingText) loadingText.innerText = "Optimizing... Please wait another 20-70 seconds"; 443 477 444 478 formData.set('force_fallback', 'true'); 445 479 response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 446 480 data = await response.json(); 481 data = await anydressResolveTask(data); 447 482 } 448 483 … … 662 697 let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 663 698 let data = await response.json(); 699 data = await anydressResolveTask(data); 664 700 665 701 // Fallback Interceptor 666 702 if (!data.success && data.data && data.data.error_code === 'require_fallback') { 667 703 loadingText.innerText = "Optimizing... Please wait another 20-70 seconds"; 668 704 669 705 formData.set('force_fallback', 'true'); 670 706 response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 671 707 data = await response.json(); 708 data = await anydressResolveTask(data); 672 709 } 673 710 … … 762 799 let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 763 800 let data = await response.json(); 801 data = await anydressResolveTask(data); 764 802 765 803 // Fallback Interceptor 766 804 if (!data.success && data.data && data.data.error_code === 'require_fallback') { 767 805 loadingText.innerText = "Optimizing... Please wait another 20-70 seconds"; 768 806 769 807 formData.set('force_fallback', 'true'); 770 808 response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 771 809 data = await response.json(); 810 data = await anydressResolveTask(data); 772 811 } 773 812 -
anydress-ai-virtual-try-on/tags/1.3.1/assets/js/frontend-tryon.js
r3530062 r3612360 75 75 } 76 76 77 // ====================================================================== 78 // SIZING & FIT (optional accurate-fit section) 79 // ====================================================================== 80 var sizing = anydressFrontend.sizing || null; 81 var fitSizeSelect = document.getElementById("anydress-fit-size"); 82 83 function fitActiveUnit() { 84 var checked = document.querySelector('input[name="anydress_fit_unit"]:checked'); 85 return checked ? checked.value : "cm"; 86 } 87 88 function fitUpdateUnitLabels() { 89 var unit = fitActiveUnit(); 90 document.querySelectorAll(".anydress-fit-unit-label").forEach(function (el) { 91 el.textContent = "(" + unit + ")"; 92 }); 93 } 94 95 function fitInitInputs() { 96 if (!sizing) { return; } 97 var unit = fitActiveUnit(); 98 var saved = sizing.saved || {}; 99 document.querySelectorAll(".anydress-fit-input").forEach(function (input) { 100 var key = input.getAttribute("data-key"); 101 var metric = saved[key]; // stored in cm (body) or mm (face) 102 if (typeof metric === "undefined" || metric === null || metric === "") { return; } 103 metric = parseFloat(metric); 104 if (!(metric > 0)) { return; } 105 if (input.getAttribute("data-fixed-unit") === "mm") { 106 input.value = metric; 107 } else { 108 input.value = (unit === "in") ? Math.round((metric / 2.54) * 10) / 10 : metric; 109 } 110 }); 111 fitUpdateUnitLabels(); 112 } 113 114 // Unit toggle: convert already-entered values between cm and in 115 document.querySelectorAll('input[name="anydress_fit_unit"]').forEach(function (radio) { 116 radio.addEventListener("change", function () { 117 var toUnit = radio.value; 118 document.querySelectorAll(".anydress-fit-input").forEach(function (input) { 119 if (input.getAttribute("data-fixed-unit") === "mm") { return; } 120 var v = parseFloat(input.value); 121 if (!(v > 0)) { return; } 122 input.value = (toUnit === "in") 123 ? Math.round((v / 2.54) * 10) / 10 124 : Math.round(v * 2.54 * 10) / 10; 125 }); 126 fitUpdateUnitLabels(); 127 }); 128 }); 129 130 // Sync the size picker with the WooCommerce variation dropdown (if one exists) 131 function fitSyncFromVariation() { 132 if (!sizing || !sizing.sizeAttrField || !fitSizeSelect) { return; } 133 var variationSelect = document.querySelector('select[name="' + sizing.sizeAttrField + '"]'); 134 if (!variationSelect || !variationSelect.value) { return; } 135 var chosen = variationSelect.selectedOptions.length 136 ? variationSelect.selectedOptions[0].textContent.trim() 137 : variationSelect.value; 138 var candidates = [variationSelect.value.toLowerCase(), chosen.toLowerCase()]; 139 for (var i = 0; i < fitSizeSelect.options.length; i++) { 140 var optVal = fitSizeSelect.options[i].value.toLowerCase().trim(); 141 if (optVal !== "" && candidates.indexOf(optVal) !== -1) { 142 fitSizeSelect.value = fitSizeSelect.options[i].value; 143 return; 144 } 145 } 146 } 147 148 if (sizing && sizing.sizeAttrField) { 149 var wooSizeSelect = document.querySelector('select[name="' + sizing.sizeAttrField + '"]'); 150 if (wooSizeSelect) { 151 wooSizeSelect.addEventListener("change", fitSyncFromVariation); 152 } 153 } 154 155 if (sizing) { 156 fitInitInputs(); 157 fitSyncFromVariation(); 158 } 159 160 // Append the shopper's sizing choices to an outgoing request 161 function fitAppendToForm(formData) { 162 if (!sizing) { return; } 163 164 if (fitSizeSelect && fitSizeSelect.value) { 165 formData.append("size_label", fitSizeSelect.value); 166 } 167 formData.append("measure_unit", fitActiveUnit()); 168 169 var any = false; 170 document.querySelectorAll(".anydress-fit-input").forEach(function (input) { 171 var v = parseFloat(input.value); 172 if (v > 0) { 173 formData.append("measurements[" + input.getAttribute("data-key") + "]", v); 174 any = true; 175 } 176 }); 177 178 if (any || (fitSizeSelect && fitSizeSelect.value)) { 179 formData.append("sizing_active", "true"); 180 } 181 } 182 183 // ====================================================================== 184 // ASYNC TASK RESOLUTION 185 // The server now accepts the job in seconds and returns a task_id; we poll 186 // a lightweight status action until the image is ready (or the task fails). 187 // ====================================================================== 188 async function anydressResolveTask(data) { 189 if (!data || !data.success || !data.data || !data.data.task_id) { 190 return data; // not a task response — pass through unchanged 191 } 192 var taskId = data.data.task_id; 193 194 for (var i = 0; i < 130; i++) { // ~9 minutes max (server times tasks out at 8) 195 await new Promise(function (r) { setTimeout(r, i === 0 ? 6000 : 4000); }); 196 try { 197 var fd = new FormData(); 198 fd.append("action", "anydress_check_task"); 199 fd.append("task_id", taskId); 200 fd.append("nonce", anydressFrontend.nonce); 201 202 var resp = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: fd }); 203 var status = await resp.json(); 204 205 if (status && status.success && status.data && status.data.status === "pending") { 206 continue; // still generating 207 } 208 return status; // completed (has image_url) or failed (error shape) 209 } catch (e) { 210 // transient network hiccup — keep polling 211 } 212 } 213 return { success: false, data: { message: "Generation timed out. Please try again." } }; 214 } 215 77 216 // Central function to send AJAX request 78 217 async function sendToEngine(formData, buttonToDisable) { … … 80 219 formData.append("force_fallback", "false"); 81 220 } 221 fitAppendToForm(formData); 82 222 83 223 buttonToDisable.innerText = "Processing... (This takes 20-70 seconds)"; … … 95 235 let response = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: formData }); 96 236 let data = await response.json(); 237 data = await anydressResolveTask(data); 97 238 98 239 if (!data.success && data.data && data.data.error_code === 'require_fallback') { 99 240 resultArea.innerHTML = "<p><i>Optimizing... Please wait another 20-70 seconds</i></p>"; 100 241 buttonToDisable.innerText = "Optimizing..."; 101 242 102 243 formData.set("force_fallback", "true"); 103 244 response = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: formData }); 104 245 data = await response.json(); 246 data = await anydressResolveTask(data); 105 247 } 106 248 … … 128 270 } 129 271 272 // Optional fit warning coming back from the sizing engine 273 var fitWarningHtml = ""; 274 if (data.data.fit_warning) { 275 var warnText = document.createElement("div"); 276 warnText.textContent = data.data.fit_warning; // XSS-safe 277 fitWarningHtml = "<div class='anydress-fit-warning'>⚠️ <b>" + (anydressFrontend.i18n.fitWarning || "Fit note:") + "</b> " + warnText.innerHTML + "</div>"; 278 } 279 130 280 // Display the image 131 resultArea.innerHTML = "<h3>Your Virtual Try-On:</h3> <img src='" + data.data.image_url + "' style='max-width:100%; max-height:60vh; object-fit:contain; display:block; margin: 15px auto 0; border-radius:8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);' />";281 resultArea.innerHTML = "<h3>Your Virtual Try-On:</h3>" + fitWarningHtml + "<img src='" + data.data.image_url + "' style='max-width:100%; max-height:60vh; object-fit:contain; display:block; margin: 15px auto 0; border-radius:8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);' />"; 132 282 } else { 133 283 // Error: Re-enable button and SHOW the link again 134 buttonToDisable.innerText = "Try Again";135 284 buttonToDisable.disabled = false; 136 285 if (showUploadLink) showUploadLink.style.display = ""; 137 138 // Handle object vs string error messages cleanly 286 287 // Handle object vs string error messages cleanly (XSS-safe) 139 288 let errorMsg = typeof data.data === 'string' ? data.data : (data.data.message || 'Unknown error'); 140 resultArea.innerHTML = "<p style='color:red; font-weight:bold;'>Error: " + errorMsg + "</p>"; 289 var safeMsg = document.createElement("div"); 290 safeMsg.textContent = errorMsg; 291 292 // Fit-advice blocks are helpful sizing guidance, not real errors — 293 // render them as a friendly amber notice instead of red error text. 294 var fitCodes = ["size_too_small", "size_too_large", "size_length_issue", 295 "frame_too_narrow", "frame_too_wide", "bridge_too_narrow", "bridge_too_wide", 296 "arms_too_short", "arms_too_long", "headwear_too_small", "headwear_too_large"]; 297 var errCode = (data.data && data.data.error_code) ? data.data.error_code : ""; 298 299 if (fitCodes.indexOf(errCode) !== -1) { 300 buttonToDisable.innerText = "Try Another Size"; 301 resultArea.innerHTML = "<div class='anydress-fit-warning'>📏 <b>Sizing advice:</b> " + safeMsg.innerHTML + "</div>"; 302 // Draw the shopper's eye to the size picker so they can switch and retry 303 var fitDetails = document.getElementById("anydress-fit-details"); 304 if (fitDetails) { fitDetails.open = true; } 305 } else { 306 buttonToDisable.innerText = "Try Again"; 307 resultArea.innerHTML = "<p style='color:red; font-weight:bold;'>Error: " + safeMsg.innerHTML + "</p>"; 308 } 141 309 } 142 310 … … 201 369 formData.append("use_saved_mannequin", "false"); 202 370 formData.append("face_image", compressedFace); 203 formData.append("face_image", compressedFace);204 371 formData.append("consent_given", "true"); 205 372 -
anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/ajax/ajax-generate-edit.php
r3530062 r3612360 75 75 'headers' => array( 'Content-Type' => 'application/json' ), 76 76 'body' => wp_json_encode( $payload ), 77 'timeout' => 12077 'timeout' => 60 78 78 )); 79 79 … … 91 91 wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) ); 92 92 } 93 if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits') {93 if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) { 94 94 wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) ); 95 95 } … … 97 97 } 98 98 99 if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 99 // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task' 100 if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 100 101 101 wp_send_json_success( array( ' image_url' => $data['image_url']) );102 wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) ); 102 103 } -
anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/ajax/ajax-generate-photoshoot.php
r3530062 r3612360 189 189 'headers' => array( 'Content-Type' => 'application/json' ), 190 190 'body' => wp_json_encode( $payload ), 191 'timeout' => 120191 'timeout' => 60 192 192 )); 193 193 … … 206 206 } 207 207 // NEW: Gracefully catch the out of credits error so the frontend can display an upgrade prompt 208 if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits') {208 if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) { 209 209 wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) ); 210 210 } … … 212 212 } 213 213 214 if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 215 216 wp_send_json_success( array( 'image_url' => $data['image_url'] ) ); 214 // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task' 215 if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 216 217 wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) ); 217 218 } 218 219 -
anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/ajax/ajax-magic-edit.php
r3530062 r3612360 75 75 'headers' => array( 'Content-Type' => 'application/json' ), 76 76 'body' => wp_json_encode( $payload ), 77 'timeout' => 12077 'timeout' => 60 78 78 )); 79 79 … … 91 91 wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) ); 92 92 } 93 if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits') {93 if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) { 94 94 wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) ); 95 95 } … … 97 97 } 98 98 99 if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 99 // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task' 100 if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 100 101 101 wp_send_json_success( array( ' image_url' => $data['image_url']) );102 wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) ); 102 103 } -
anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/class-anydress-photoshoot.php
r3530062 r3612360 242 242 <option value="4K"><?php esc_html_e( '4K (Ultra High Definition)', 'anydress-ai-virtual-try-on' ); ?></option> 243 243 </select> 244 <p style="font-size: 11px; color: #888; margin: 6px 0 0;"><?php esc_html_e( 'Note: swimwear, lingerie and similar items are processed by a dedicated engine that tops out at 2K — a 4K request for those items is generated and billed at 2K automatically.', 'anydress-ai-virtual-try-on' ); ?></p> 244 245 </div> 245 246 -
anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/class-anydress-settings.php
r3530062 r3612360 279 279 ); 280 280 281 register_setting( 'anydress_settings_group', 'anydress_fit_mode', array( $this, 'sanitize_fit_mode' ) ); 282 283 add_settings_field( 284 'anydress_fit_mode_field', 285 __( 'Sizing: Bad Fit Behavior', 'anydress-ai-virtual-try-on' ), 286 array( $this, 'fit_mode_render' ), 287 'anydress-settings', 288 'anydress_settings_section' 289 ); 290 291 register_setting( 'anydress_settings_group', 'anydress_vto_aspect', array( $this, 'sanitize_vto_aspect' ) ); 292 register_setting( 'anydress_settings_group', 'anydress_vto_resolution', array( $this, 'sanitize_vto_resolution' ) ); 293 294 add_settings_field( 295 'anydress_vto_aspect_field', 296 __( 'Try-On Image Aspect Ratio', 'anydress-ai-virtual-try-on' ), 297 array( $this, 'vto_aspect_render' ), 298 'anydress-settings', 299 'anydress_settings_section' 300 ); 301 302 add_settings_field( 303 'anydress_vto_resolution_field', 304 __( 'Try-On Image Resolution', 'anydress-ai-virtual-try-on' ), 305 array( $this, 'vto_resolution_render' ), 306 'anydress-settings', 307 'anydress_settings_section' 308 ); 309 281 310 // ========================================== 282 311 // 2. APPEARANCE SETTINGS … … 375 404 } 376 405 406 public function sanitize_fit_mode( $value ) { 407 return in_array( $value, array( 'block', 'warn' ), true ) ? $value : 'block'; 408 } 409 410 public function sanitize_vto_aspect( $value ) { 411 return in_array( $value, array( '2:3', '3:4', '4:5' ), true ) ? $value : '3:4'; 412 } 413 414 public function sanitize_vto_resolution( $value ) { 415 return in_array( $value, array( '1K', '2K' ), true ) ? $value : '1K'; 416 } 417 418 public function vto_aspect_render() { 419 $val = get_option( 'anydress_vto_aspect', '3:4' ); 420 $options = array( 421 '2:3' => __( '2:3 — Classic fashion editorial (tallest)', 'anydress-ai-virtual-try-on' ), 422 '3:4' => __( '3:4 — Balanced portrait (default)', 'anydress-ai-virtual-try-on' ), 423 '4:5' => __( '4:5 — Ecommerce / Instagram standard', 'anydress-ai-virtual-try-on' ), 424 ); 425 echo '<select name="anydress_vto_aspect">'; 426 foreach ( $options as $key => $label ) { 427 echo '<option value="' . esc_attr( $key ) . '" ' . selected( $val, $key, false ) . '>' . esc_html( $label ) . '</option>'; 428 } 429 echo '</select>'; 430 echo '<p class="description">' . esc_html__( 'Every shopper try-on image is generated in this fixed portrait format, so results always look consistent across your store.', 'anydress-ai-virtual-try-on' ) . '</p>'; 431 } 432 433 public function vto_resolution_render() { 434 $val = get_option( 'anydress_vto_resolution', '1K' ); 435 echo '<label style="margin-right: 15px;"><input type="radio" name="anydress_vto_resolution" value="1K" ' . checked( $val, '1K', false ) . ' /> ' . esc_html__( '1K — Standard (1 credit rate)', 'anydress-ai-virtual-try-on' ) . '</label>'; 436 echo '<label><input type="radio" name="anydress_vto_resolution" value="2K" ' . checked( $val, '2K', false ) . ' /> ' . esc_html__( '2K — High detail (1.5× credit rate)', 'anydress-ai-virtual-try-on' ) . '</label>'; 437 echo '<p class="description">' . esc_html__( 'Resolution of shopper try-on images. 2K looks sharper on zoom but each try-on costs 1.5× the credits and loads slightly slower on mobile.', 'anydress-ai-virtual-try-on' ) . '</p>'; 438 } 439 440 public function fit_mode_render() { 441 $val = get_option( 'anydress_fit_mode', 'block' ); 442 443 echo '<label style="display: block; margin-bottom: 8px;">'; 444 echo '<input type="radio" name="anydress_fit_mode" value="block" ' . checked( 'block', $val, false ) . ' /> '; 445 echo '<strong>' . esc_html__( 'Block generation', 'anydress-ai-virtual-try-on' ) . '</strong> — ' . esc_html__( 'if the shopper\'s measurements say the selected size won\'t fit, skip the image and suggest a better size. Saves credits.', 'anydress-ai-virtual-try-on' ); 446 echo '</label>'; 447 448 echo '<label style="display: block;">'; 449 echo '<input type="radio" name="anydress_fit_mode" value="warn" ' . checked( 'warn', $val, false ) . ' /> '; 450 echo '<strong>' . esc_html__( 'Warn but generate', 'anydress-ai-virtual-try-on' ) . '</strong> — ' . esc_html__( 'show a fit warning but still generate the image with a realistically tight or loose fit. Uses credits even for bad fits.', 'anydress-ai-virtual-try-on' ); 451 echo '</label>'; 452 453 echo '<p class="description">' . wp_kses_post( __( 'Only applies when a product has a <strong>Size Chart</strong> assigned and the shopper provides their body measurements. Manage charts under <strong>AnyDress AI → Size Charts</strong>.', 'anydress-ai-virtual-try-on' ) ) . '</p>'; 454 } 455 377 456 public function privacy_policy_url_render() { 378 457 $val = get_option( 'anydress_privacy_policy_url', '' ); -
anydress-ai-virtual-try-on/tags/1.3.1/includes/frontend/ajax/ajax-frontend-tryon.php
r3530062 r3612360 244 244 245 245 // ==================================================================== 246 // SIZING & FIT: Build size_data from the store's chart + shopper input 247 // ==================================================================== 248 $size_data = null; 249 250 $sizing_active = isset( $_POST['sizing_active'] ) && $_POST['sizing_active'] === 'true'; 251 252 if ( $sizing_active && function_exists( 'anydress_size_resolve_garment_data' ) ) { 253 254 $size_label = isset( $_POST['size_label'] ) ? sanitize_text_field( wp_unslash( $_POST['size_label'] ) ) : ''; 255 $unit = ( isset( $_POST['measure_unit'] ) && sanitize_text_field( wp_unslash( $_POST['measure_unit'] ) ) === 'in' ) ? 'in' : 'cm'; 256 257 // 1. Garment measurements: resolved server-side from the product's size chart. 258 // The browser only ever sends the size LABEL — measurements are trusted store data. 259 $size_data = anydress_size_resolve_garment_data( $product_id, $size_label ); 260 261 if ( $size_data ) { 262 // 2. Shopper measurements (optional). Sanitized against the product type's 263 // known field list and converted to metric. 264 $raw_measurements = array(); 265 if ( isset( $_POST['measurements'] ) && is_array( $_POST['measurements'] ) ) { 266 // Values are numeric strings; numeric validation happens field-by-field 267 // in anydress_size_sanitize_shopper_measurements() right after. 268 $raw_measurements = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['measurements'] ) ); 269 } 270 271 $shopper = anydress_size_sanitize_shopper_measurements( $raw_measurements, $size_data['product_type'], $unit ); 272 273 if ( ! empty( $shopper ) ) { 274 // Persist for reuse (same lifecycle as the mannequin: My Account deletion, 275 // 7-day cron, uninstall). Merge so switching product types keeps other fields. 276 $existing = get_user_meta( $user_id, 'anydress_shopper_measurements', true ); 277 if ( ! is_array( $existing ) ) { 278 $existing = array(); 279 } 280 update_user_meta( $user_id, 'anydress_shopper_measurements', array_merge( $existing, $shopper ) ); 281 update_user_meta( $user_id, 'anydress_shopper_unit', $unit ); 282 283 $size_data = array_merge( $size_data, $shopper ); 284 $size_data['fit_mode'] = anydress_size_get_fit_mode(); 285 } else { 286 // Size chosen but no body measurements: fit validation is impossible. 287 // Send nothing so the try-on behaves exactly like the standard flow. 288 $size_data = null; 289 } 290 } 291 } 292 293 // ==================================================================== 246 294 // SAAS ROUTING: Send to AnyDress.ai Server 247 295 // ==================================================================== … … 255 303 256 304 $payload = array( 257 'license_key' => $api_key, 305 'license_key' => $api_key, 258 306 'client_domain' => $safe_domain, 259 'user_hash' => $user_hash, 307 'user_hash' => $user_hash, 260 308 'force_fallback' => $force_fallback, 309 'aspect_ratio' => get_option( 'anydress_vto_aspect', '3:4' ), 310 'resolution' => get_option( 'anydress_vto_resolution', '1K' ), 261 311 'face_base64' => $face_base64, 262 312 'face_mime' => $face_mime, … … 267 317 ); 268 318 319 // Attach sizing payload only when fully resolved (garment + shopper data) 320 if ( ! empty( $size_data ) ) { 321 $payload['size_data'] = $size_data; 322 } 323 269 324 $response = wp_remote_post( $saas_endpoint, array( 270 325 'headers' => array( 'Content-Type' => 'application/json' ), 271 326 'body' => wp_json_encode( $payload ), 272 'timeout' => 120 // 120 seconds to allow for image generation327 'timeout' => 60 // Submission returns a task_id in seconds; polling handles the rest 273 328 )); 274 329 … … 284 339 // Handle errors returned by the SaaS Server 285 340 if ( $status_code !== 200 ) { 286 // Propagate the specific fallback error code to frontend shopper 287 if ( isset( $data['code'] ) && $data['code'] === 'require_fallback' ) { 288 wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) ); 289 } 290 $error_msg = isset( $data['message'] ) ? $data['message'] : __( 'Unknown Server Error.', 'anydress-ai-virtual-try-on' ); 291 wp_send_json_error( $error_msg ); 292 } 293 294 if ( empty( $data['image_url'] ) ) { 295 wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 296 } 341 // Always relay the server's error code so the JS can style fit-advice 342 // blocks (size_too_small etc.) differently from real errors. 343 $error_code = isset( $data['code'] ) ? sanitize_key( $data['code'] ) : 'server_error'; 344 $error_msg = isset( $data['message'] ) ? $data['message'] : __( 'Unknown Server Error.', 'anydress-ai-virtual-try-on' ); 345 wp_send_json_error( array( 'error_code' => $error_code, 'message' => $error_msg ) ); 346 } 347 348 // ASYNC: the server now returns a task_id immediately. The browser polls 349 // the 'anydress_check_task' action until the image is ready. 350 if ( empty( $data['task_id'] ) ) { 351 wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 352 } 353 354 // Count the daily-limit slot NOW (prevents bypassing the limit with parallel 355 // submissions). If the task terminally fails, the status relay refunds it. 297 356 update_user_meta( $user_id, 'anydress_usage_count', $usage_count + 1 ); 298 357 299 // Success! Send it to the frontend. 300 wp_send_json_success( array( 'image_url' => $data['image_url'] ) ); 358 wp_send_json_success( array( 359 'task_id' => sanitize_text_field( $data['task_id'] ), 360 'status' => 'pending', 361 ) ); 301 362 } -
anydress-ai-virtual-try-on/tags/1.3.1/includes/frontend/anydress-cron.php
r3530062 r3612360 56 56 // 4. Remove timestamp 57 57 delete_user_meta( $user_id, 'anydress_mannequin_upload_time' ); 58 59 // 5. Delete saved body measurements (same privacy lifecycle as photos) 60 delete_user_meta( $user_id, 'anydress_shopper_measurements' ); 61 delete_user_meta( $user_id, 'anydress_shopper_unit' ); 58 62 } 59 63 } -
anydress-ai-virtual-try-on/tags/1.3.1/includes/frontend/class-anydress-my-account.php
r3530062 r3612360 16 16 add_filter( 'woocommerce_account_menu_items', array( $this, 'add_my_account_menu_item' ) ); 17 17 add_action( 'template_redirect', array( $this, 'handle_mannequin_deletion' ) ); 18 add_action( 'template_redirect', array( $this, 'handle_measurements_save' ) ); 18 19 add_action( 'woocommerce_account_digital-mannequin_endpoint', array( $this, 'my_account_endpoint_content' ) ); 20 } 21 22 /** 23 * All body-measurement fields a shopper can store, across every product type. 24 * Returns array of payload_key => array( label, unit ). 25 */ 26 private function get_all_shopper_fields() { 27 $fields = array(); 28 if ( ! function_exists( 'anydress_size_get_product_types' ) ) { 29 return $fields; 30 } 31 foreach ( anydress_size_get_product_types() as $type ) { 32 foreach ( $type['shopper_fields'] as $key => $label ) { 33 if ( ! isset( $fields[ $key ] ) ) { 34 $fields[ $key ] = array( 'label' => $label, 'unit' => $type['unit'] ); 35 } 36 } 37 } 38 return $fields; 19 39 } 20 40 … … 84 104 85 105 // 4. Remove timestamp 86 delete_user_meta( $user_id, 'anydress_mannequin_upload_time' ); 106 delete_user_meta( $user_id, 'anydress_mannequin_upload_time' ); 107 108 // 5. Delete saved body measurements 109 delete_user_meta( $user_id, 'anydress_shopper_measurements' ); 110 delete_user_meta( $user_id, 'anydress_shopper_unit' ); 87 111 88 112 // Show a success message in WooCommerce … … 90 114 91 115 // Redirect back to the tab to refresh the page safely 116 wp_safe_redirect( wc_get_endpoint_url( 'digital-mannequin', '', wc_get_page_permalink( 'myaccount' ) ) ); 117 exit; 118 } 119 } 120 121 /** 122 * Handle saving of body measurements from the My Account tab 123 */ 124 public function handle_measurements_save() { 125 if ( is_account_page() && isset( $_POST['anydress_save_measurements'] ) && is_user_logged_in() ) { 126 if ( ! isset( $_POST['anydress_measurements_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['anydress_measurements_nonce'] ) ), 'save_measurements_action' ) ) { 127 wc_add_notice( __( 'Security check failed. Please try again.', 'anydress-ai-virtual-try-on' ), 'error' ); 128 return; 129 } 130 131 $user_id = get_current_user_id(); 132 $fields = $this->get_all_shopper_fields(); 133 $unit = ( isset( $_POST['anydress_unit'] ) && sanitize_text_field( wp_unslash( $_POST['anydress_unit'] ) ) === 'in' ) ? 'in' : 'cm'; 134 135 $clean = array(); 136 if ( isset( $_POST['anydress_m'] ) && is_array( $_POST['anydress_m'] ) ) { 137 // Values are numeric strings; validated numerically field-by-field below 138 $raw = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['anydress_m'] ) ); 139 foreach ( $fields as $key => $def ) { 140 if ( isset( $raw[ $key ] ) && $raw[ $key ] !== '' && is_numeric( $raw[ $key ] ) ) { 141 $val = (float) $raw[ $key ]; 142 if ( $val <= 0 ) { 143 continue; 144 } 145 // mm fields (face) are always entered in mm; cm fields follow the chosen unit 146 if ( $def['unit'] === 'mm' ) { 147 if ( $val <= 400 ) { 148 $clean[ $key ] = round( $val, 1 ); 149 } 150 } else { 151 $metric = ( $unit === 'in' ) ? round( $val * 2.54, 1 ) : round( $val, 1 ); 152 if ( $metric <= 350 ) { 153 $clean[ $key ] = $metric; 154 } 155 } 156 } 157 } 158 } 159 160 if ( ! empty( $clean ) ) { 161 update_user_meta( $user_id, 'anydress_shopper_measurements', $clean ); 162 update_user_meta( $user_id, 'anydress_shopper_unit', $unit ); 163 wc_add_notice( __( 'Your body measurements have been saved.', 'anydress-ai-virtual-try-on' ), 'success' ); 164 } else { 165 delete_user_meta( $user_id, 'anydress_shopper_measurements' ); 166 delete_user_meta( $user_id, 'anydress_shopper_unit' ); 167 wc_add_notice( __( 'Your body measurements have been removed.', 'anydress-ai-virtual-try-on' ), 'success' ); 168 } 169 92 170 wp_safe_redirect( wc_get_endpoint_url( 'digital-mannequin', '', wc_get_page_permalink( 'myaccount' ) ) ); 93 171 exit; … … 152 230 echo '<a href="' . esc_url( $shop_url ) . '" style="display: inline-block; background: #222; color: #fff; padding: 10px 20px; text-decoration: none; border-radius: 4px;">' . esc_html__( 'Go to Shop', 'anydress-ai-virtual-try-on' ) . '</a>'; 153 231 echo '</div>'; 154 155 echo '</div>'; 156 } 232 233 echo '</div>'; 234 } 235 236 $this->render_measurements_section( $user_id ); 237 } 238 239 /** 240 * The "My Body Measurements" edit form inside the My Account tab. 241 */ 242 private function render_measurements_section( $user_id ) { 243 $fields = $this->get_all_shopper_fields(); 244 if ( empty( $fields ) ) { 245 return; 246 } 247 248 $saved = get_user_meta( $user_id, 'anydress_shopper_measurements', true ); 249 if ( ! is_array( $saved ) ) { 250 $saved = array(); 251 } 252 $unit = get_user_meta( $user_id, 'anydress_shopper_unit', true ); 253 if ( ! in_array( $unit, array( 'cm', 'in' ), true ) ) { 254 $unit = 'cm'; 255 } 256 257 echo '<h3 style="margin-top: 40px;">' . esc_html__( '📏 My Body Measurements', 'anydress-ai-virtual-try-on' ) . '</h3>'; 258 echo '<p style="color: #666; font-size: 14px;">' . esc_html__( 'Used to make your Virtual Try-On fit as realistic as possible. All fields are optional. Like your mannequin photos, measurements are automatically deleted 7 days after your last mannequin upload.', 'anydress-ai-virtual-try-on' ) . '</p>'; 259 260 echo '<form method="post" style="background: #fff; padding: 20px; border: 1px solid #e2e2e2; border-radius: 8px;">'; 261 wp_nonce_field( 'save_measurements_action', 'anydress_measurements_nonce' ); 262 263 // Unit selector 264 echo '<p style="margin-top: 0;"><strong>' . esc_html__( 'Units:', 'anydress-ai-virtual-try-on' ) . '</strong> '; 265 echo '<label style="margin-right: 12px;"><input type="radio" name="anydress_unit" value="cm" ' . checked( $unit, 'cm', false ) . ' /> cm</label>'; 266 echo '<label><input type="radio" name="anydress_unit" value="in" ' . checked( $unit, 'in', false ) . ' /> in</label>'; 267 echo ' <small style="color: #888;">' . esc_html__( '(face measurements for eyewear are always in mm)', 'anydress-ai-virtual-try-on' ) . '</small></p>'; 268 269 echo '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px;">'; 270 foreach ( $fields as $key => $def ) { 271 $metric_val = isset( $saved[ $key ] ) ? (float) $saved[ $key ] : ''; 272 $display_val = $metric_val; 273 $unit_label = $def['unit']; 274 275 if ( $def['unit'] === 'cm' ) { 276 $unit_label = $unit; 277 if ( $metric_val !== '' && $unit === 'in' ) { 278 $display_val = round( $metric_val / 2.54, 1 ); 279 } 280 } 281 282 echo '<div>'; 283 echo '<label style="display: block; font-size: 13px; font-weight: 600; margin-bottom: 3px;">' . esc_html( $def['label'] ) . ' <small style="font-weight: normal; color: #888;">(' . esc_html( $unit_label ) . ')</small></label>'; 284 echo '<input type="number" step="0.1" min="0" name="anydress_m[' . esc_attr( $key ) . ']" value="' . esc_attr( $display_val ) . '" style="width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box;" />'; 285 echo '</div>'; 286 } 287 echo '</div>'; 288 289 echo '<div style="margin-top: 15px;">'; 290 echo '<button type="submit" name="anydress_save_measurements" value="1" style="background-color: #222; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: bold;">' . esc_html__( 'Save Measurements', 'anydress-ai-virtual-try-on' ) . '</button>'; 291 echo ' <small style="color: #888; margin-left: 10px;">' . esc_html__( 'Clear all fields and save to delete your measurements.', 'anydress-ai-virtual-try-on' ) . '</small>'; 292 echo '</div>'; 293 294 echo '</form>'; 157 295 } 158 296 } -
anydress-ai-virtual-try-on/tags/1.3.1/includes/frontend/class-anydress-widget.php
r3530062 r3612360 70 70 'productId' => $product_id, 71 71 'nonce' => wp_create_nonce( 'anydress_frontend_action' ), 72 'sizing' => $this->get_sizing_context( $product_id ), 72 73 'i18n' => array( 73 74 'uploadFace' => __( 'Please upload your face photo.', 'anydress-ai-virtual-try-on' ), 74 75 'faceCompressed' => __( '⚠️ Your face photo could not be compressed enough. Please try a different image.', 'anydress-ai-virtual-try-on' ), 75 76 'bodyCompressed' => __( '⚠️ Your body photo could not be compressed enough. Please try a different image.', 'anydress-ai-virtual-try-on' ), 76 'errorProcess' => __( 'Error processing images. Please try different photos.', 'anydress-ai-virtual-try-on' ) 77 'errorProcess' => __( 'Error processing images. Please try different photos.', 'anydress-ai-virtual-try-on' ), 78 'fitWarning' => __( 'Fit note:', 'anydress-ai-virtual-try-on' ) 77 79 ) 78 80 )); 81 } 82 83 /** 84 * Build the sizing context for the frontend JS. 85 * Returns null when the product has no size chart assigned (plugin behaves exactly as before). 86 * NOTE: garment measurements themselves never leave the server — only size labels go to the browser. 87 */ 88 private function get_sizing_context( $product_id ) { 89 if ( ! function_exists( 'anydress_size_get_product_chart' ) ) { 90 return null; 91 } 92 93 $chart = anydress_size_get_product_chart( $product_id ); 94 if ( ! $chart ) { 95 return null; 96 } 97 98 $type_def = anydress_size_get_type( $chart['product_type'] ); 99 if ( ! $type_def ) { 100 return null; 101 } 102 103 $size_labels = array(); 104 foreach ( $chart['rows'] as $row ) { 105 $size_labels[] = $row['label']; 106 } 107 108 // Locate the size attribute so JS can sync with the variation dropdown 109 $size_attr_field = ''; 110 $product = wc_get_product( $product_id ); 111 $size_attr = anydress_size_get_product_size_attribute( $product ); 112 if ( $size_attr ) { 113 $size_attr_field = 'attribute_' . sanitize_title( $size_attr['attribute_name'] ); 114 } 115 116 // Saved shopper measurements (stored in metric) 117 $user_id = get_current_user_id(); 118 $saved = $user_id ? get_user_meta( $user_id, 'anydress_shopper_measurements', true ) : array(); 119 if ( ! is_array( $saved ) ) { 120 $saved = array(); 121 } 122 $saved_unit = $user_id ? get_user_meta( $user_id, 'anydress_shopper_unit', true ) : ''; 123 if ( ! in_array( $saved_unit, array( 'cm', 'in' ), true ) ) { 124 $saved_unit = 'cm'; 125 } 126 127 return array( 128 'productType' => $chart['product_type'], 129 'metricUnit' => $type_def['unit'], // what the server expects (cm or mm) 130 'allowImperial' => (bool) $type_def['allow_imperial'], 131 'preferredUnit' => $saved_unit, 132 'sizeLabels' => $size_labels, 133 'sizeAttrField' => $size_attr_field, 134 'shopperFields' => $type_def['shopper_fields'], 135 'saved' => (object) $saved, 136 ); 79 137 } 80 138 … … 193 251 echo ' </div>'; 194 252 253 // --- SIZING & FIT SECTION (only when the product has a size chart) --- 254 // Note: get_queried_object_id() is reliable in wp_footer, unlike get_the_ID() after custom loops 255 $sizing = $this->get_sizing_context( get_queried_object_id() ); 256 if ( $sizing ) { 257 $this->render_fit_section( $sizing ); 258 } 259 195 260 echo ' <div id="anydress-result-area"></div>'; 196 261 … … 198 263 echo '</div>'; 199 264 } 265 266 /** 267 * Render the optional "Get an accurate fit" block inside the modal. 268 * Works for both the saved-mannequin flow and the new-upload flow. 269 */ 270 private function render_fit_section( $sizing ) { 271 $is_mm = ( $sizing['metricUnit'] === 'mm' ); 272 273 echo '<div id="anydress-fit-section" class="anydress-fit-section">'; 274 echo ' <details id="anydress-fit-details">'; 275 echo ' <summary>' . esc_html__( '📏 Get an accurate fit (optional)', 'anydress-ai-virtual-try-on' ) . '</summary>'; 276 echo ' <div class="anydress-fit-body">'; 277 278 // 1. Size picker (synced with the variation dropdown when one exists) 279 echo ' <div class="anydress-fit-row">'; 280 echo ' <label for="anydress-fit-size">' . esc_html__( 'Size you\'re trying on', 'anydress-ai-virtual-try-on' ) . '</label>'; 281 echo ' <select id="anydress-fit-size">'; 282 echo ' <option value="">' . esc_html__( '— Select size —', 'anydress-ai-virtual-try-on' ) . '</option>'; 283 foreach ( $sizing['sizeLabels'] as $label ) { 284 echo ' <option value="' . esc_attr( $label ) . '">' . esc_html( $label ) . '</option>'; 285 } 286 echo ' </select>'; 287 echo ' </div>'; 288 289 // 2. Unit toggle (hidden for mm-based products like eyewear) 290 if ( ! $is_mm && $sizing['allowImperial'] ) { 291 echo ' <div class="anydress-fit-row anydress-fit-unit-row">'; 292 echo ' <label>' . esc_html__( 'Units', 'anydress-ai-virtual-try-on' ) . '</label>'; 293 echo ' <span>'; 294 echo ' <label class="anydress-fit-unit-opt"><input type="radio" name="anydress_fit_unit" value="cm" ' . checked( $sizing['preferredUnit'], 'cm', false ) . ' /> cm</label>'; 295 echo ' <label class="anydress-fit-unit-opt"><input type="radio" name="anydress_fit_unit" value="in" ' . checked( $sizing['preferredUnit'], 'in', false ) . ' /> in</label>'; 296 echo ' </span>'; 297 echo ' </div>'; 298 } 299 300 // 3. Measurement inputs (values injected by JS from saved data, converted to the active unit) 301 echo ' <p class="anydress-fit-hint">' . esc_html__( 'Your measurements are optional — the more you fill in, the more realistic the generated fit. They are saved with your digital mannequin and automatically deleted after 7 days.', 'anydress-ai-virtual-try-on' ) . '</p>'; 302 echo ' <div class="anydress-fit-fields">'; 303 foreach ( $sizing['shopperFields'] as $key => $label ) { 304 $unit_suffix = $is_mm ? 'mm' : ''; // body-field suffix is drawn by JS (follows the toggle) 305 echo ' <div class="anydress-fit-field">'; 306 echo ' <label for="anydress-fit-' . esc_attr( $key ) . '">' . esc_html( $label ) . ( $unit_suffix ? ' <small>(' . esc_html( $unit_suffix ) . ')</small>' : ' <small class="anydress-fit-unit-label"></small>' ); 307 echo ' </label>'; 308 echo ' <input type="number" step="0.1" min="0" inputmode="decimal" id="anydress-fit-' . esc_attr( $key ) . '" class="anydress-fit-input" data-key="' . esc_attr( $key ) . '" data-fixed-unit="' . esc_attr( $is_mm ? 'mm' : '' ) . '" />'; 309 echo ' </div>'; 310 } 311 echo ' </div>'; 312 313 echo ' </div>'; 314 echo ' </details>'; 315 echo '</div>'; 316 } 200 317 } 201 318 -
anydress-ai-virtual-try-on/tags/1.3.1/readme.txt
r3530142 r3612360 1 === AnyDress AI Virtual Try-On===1 === AnyDress – AI Virtual Try-On for WooCommerce & AI Product Photos === 2 2 Contributors: anydress 3 Tags: woocommerce, virtual try on, ai, fashion, ecommerce3 Tags: virtual try on, woocommerce, ai, product images, clothing 4 4 Requires at least: 5.8 5 Tested up to: 6.96 Stable tag: 1. 0.05 Tested up to: 7.0 6 Stable tag: 1.3.1 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Elevate your digital boutique with seamless AI virtual try-on and instant studio-quality catalog photoshoots.11 AI virtual try-on for WooCommerce: shoppers try on clothes on their own photo, and you generate studio-quality product images without models. 12 12 13 13 == Description == 14 14 15 AnyDress AI Virtual Try-On revolutionizes your WooCommerce store by allowing shoppers to visually "try on" your clothing using artificial intelligence. Increase conversion rates, boost buyer confidence, and reduce return rates by letting customers see exactly how your garments will look on their own bodies.15 **AnyDress adds an AI virtual fitting room to your WooCommerce store — and an AI photo studio to your dashboard.** Shoppers can virtually try on clothes using their own photos before they buy, and store owners can generate professional product images without models, photographers, or physical photoshoots. 16 16 17 Additionally, store owners can use the Virtual Photoshoot dashboard to generate professional, studio-quality catalog images of their garments using AI models or ghost mannequins, saving thousands of dollars on physicalphotography.17 One plugin, two revenue levers: higher conversions from confident shoppers, and lower costs from AI-generated catalog photography. 18 18 19 = Features = 20 * **Shopper Virtual Try-On:** Adds a customizable "Try It On" button to WooCommerce product pages. 21 * **Digital Mannequin:** Logged-in shoppers can securely save their face and body photos for 1-click try-ons across your whole store. 22 * **Admin Virtual Photoshoot:** Instantly generate new product catalog images using WooCommerce products or raw garment uploads. 23 * **AI Models & Ghost Mannequins:** Choose to use the original model, upload your own, generate a random AI model, or create invisible ghost mannequins. 24 * **Custom Backgrounds:** Describe any scene or background to place your models in. 25 * **Magic Edit:** Alter the pose, aspect ratio, resolution, and photography style of your generated images. 26 * **Usage Controls:** Set daily try-on limits to protect your API budget. 27 * **Privacy-First Design:** Built-in legal consent checkboxes for both shoppers and admins, ensuring no data is sent to the AI engine without explicit user agreement. 19 = A virtual fitting room for your shoppers = 20 21 A customizable **"Try It On" button** appears on your product pages, right next to Add to Cart. Shoppers upload a photo (or reuse their saved Digital Mannequin) and see exactly how the garment looks on their own body — not on a model who looks nothing like them. 22 23 * **1-click try-ons across your whole store** — logged-in shoppers securely save face and body photos once, then try on anything. 24 * **True-to-size fit** — pair try-ons with real garment measurements so shoppers see (or get warned about) sizes that run tight or loose. Fewer sizing mistakes, fewer returns, less bracketing. 25 * **Works with your theme** — hooks into standard WooCommerce templates automatically. 26 27 = An AI photo studio for store owners = 28 29 The **Virtual Photoshoot dashboard** turns a flat lay, hanger shot, or plain packshot into studio-quality on-model imagery in minutes: 30 31 * Generate catalog images from your existing WooCommerce product photos or raw garment uploads. 32 * Choose an **AI model**, upload your own model, or create clean **ghost mannequin** shots. 33 * Describe any **custom background** or scene in plain words. 34 * **Magic Edit** — change pose, aspect ratio, resolution, and photography style after generation. 35 36 No studio rental, no model fees, no reshoots when your catalog changes. 37 38 = Built-in size charts = 39 40 Create reusable size charts (tops, bottoms, dresses, outerwear, swimwear, eyewear, headwear) with real garment measurements per size. Assign them to products in bulk, override values per product, and let shoppers optionally add their own measurements (cm or inches) for a realistic fit. 41 42 = Privacy-first by design = 43 44 * Explicit consent checkboxes for both shoppers and admins — no data reaches the AI engine without agreement. 45 * Shopper photos live on **your own server**, auto-deleted after 7 days; shoppers can delete their data anytime from My Account. 46 * Images are processed in memory and discarded by the API immediately after generation — never used to train AI models. 47 48 = Perfect for = 49 50 * [Apparel & clothing stores](https://anydress.ai/use-cases/apparel/) 51 * [Eyewear & glasses](https://anydress.ai/use-cases/eyewear/) 52 * [Baby & kids clothes](https://anydress.ai/use-cases/baby-clothes/) 53 * [Jewelry](https://anydress.ai/use-cases/jewelry/) 54 * [Other use cases](https://anydress.ai/use-cases/) 55 56 = All features = 57 58 * **Shopper Virtual Try-On:** customizable "Try It On" button on WooCommerce product pages. 59 * **Digital Mannequin:** shoppers securely save photos for 1-click try-ons store-wide. 60 * **Admin Virtual Photoshoot:** generate product catalog images from products or raw garment uploads. 61 * **AI Models & Ghost Mannequins:** original model, your own model, a generated AI model, or invisible ghost mannequins. 62 * **Custom Backgrounds:** describe any scene to place your models in. 63 * **Magic Edit:** alter pose, aspect ratio, resolution, and photography style. 64 * **Size Charts & True-to-Size Fit:** reusable charts with per-size garment measurements, bulk assignment, per-product overrides. 65 * **Shopper Measurements (optional):** private body measurements for realistic fit rendering and sizing advice. 66 * **Usage Controls:** daily try-on limits to protect your API budget. 67 * **Privacy-First Design:** built-in legal consent flows for shoppers and admins. 68 69 = Learn more = 70 71 * [How AI virtual try-on works in WooCommerce](https://anydress.ai/virtual-try-on-for-woocommerce-guide/) 72 * [Creating product images without models](https://anydress.ai/ai-clothing-photography-without-a-model/) 73 * [Reducing clothing return rates](https://anydress.ai/reduce-clothing-return-rates-woocommerce/) 74 * [Plans and pricing](https://anydress.ai/#pricing) 28 75 29 76 == External Services == … … 32 79 33 80 * What the service is used for: The AnyDress AI API provides the artificial intelligence engine required to "clothe" a digital mannequin with a product image and to generate studio-quality catalog photoshoots. 34 * What data is sent and when: 81 * What data is sent and when: 35 82 1. Shopper Try-On: Only after a logged-in user gives explicit consent via a checkbox, their uploaded photos (Face/Body) and the WooCommerce product image are converted to Base64 and sent to the AnyDress AI endpoint. 36 2. Admin Photoshoot: Only after the administrator confirms rights to the images, the selected garment and model photos are sent to the API. 37 3. Verification: The store domain and license key are sent to verify subscription status and usage limits. 83 2. Optional Fit Data: If the shopper voluntarily enters body measurements, those numbers (together with the store's garment measurements for the selected size) are sent alongside the try-on request to render a realistic fit. Measurements are never required. 84 3. Admin Photoshoot: Only after the administrator confirms rights to the images, the selected garment and model photos are sent to the API. 85 4. Verification & Task Status: The store domain, license key, and generation task IDs are sent to verify subscription status, usage limits, and to retrieve finished images. 38 86 * Data Handling & Privacy: Images are processed in secure server memory to generate the result and are immediately discarded by the API upon completion. User data is confidential and never used to train AI models. 39 87 * **Terms & Privacy Links:** … … 53 101 == Frequently Asked Questions == 54 102 103 = How does the AI virtual try-on work for shoppers? = 104 A "Try It On" button appears on your product pages. The shopper uploads a photo of themselves (or reuses their saved Digital Mannequin), and the AI renders the garment on their own body in seconds — including realistic fit if measurements and size charts are set up. 105 106 = Can I really create product images without hiring models or a photographer? = 107 Yes. The Virtual Photoshoot dashboard turns a flat lay, hanger shot, ghost mannequin shot, or plain packshot into on-model, studio-quality catalog images. You choose the model, background, pose, and style. 108 109 = Does virtual try-on reduce return rates? = 110 Sizing and "it looked different online" are the top reasons clothing gets returned. Letting shoppers see garments on their own body — with optional true-to-size fit warnings — addresses both causes directly. See our [guide to reducing WooCommerce return rates](https://anydress.ai/reduce-clothing-return-rates-woocommerce/). 111 55 112 = Do I need an AnyDress AI account to use this plugin? = 56 Yes, you need an active API License Key from AnyDress AI to connect your WooCommerce store to the AI generation engine. 113 Yes, you need an active API License Key from AnyDress AI to connect your WooCommerce store to the AI generation engine. Plans are listed at https://anydress.ai/#pricing. 114 115 = Does it work for eyewear, accessories, or kids' clothes? = 116 Yes — size charts cover tops, bottoms, dresses, outerwear, swimwear, eyewear, and headwear, and stores use AnyDress for apparel, glasses, baby and kids' clothes, and jewelry. 57 117 58 118 = Where are shopper photos saved? = 59 119 Shopper photos are securely saved in a hidden, local folder on your own WordPress server (`wp-content/uploads/anydress-mannequins/`). To protect privacy and save disk space, a cron job automatically deletes shopper photos 7 days after upload. Shoppers can also manually delete their data at any time via the "Digital Mannequin" tab in their My Account page. 120 121 = Are shopper body measurements required? Where are they stored? = 122 No — measurements are entirely optional; without them the try-on works exactly as before. If provided, they are stored on your own WordPress server as user meta, editable and deletable by the shopper in My Account, and automatically removed by the same 7-day cleanup that deletes mannequin photos (and on plugin uninstall). 123 124 = How do I set up sizing for my products? = 125 Go to **AnyDress AI → Size Charts**, create a chart with your garment measurements per size, and assign it to products either from the chart itself or in the product editor under **Product data → AnyDress Sizing** (where you can also override individual values per product). 60 126 61 127 = Does this work with any theme? = … … 64 130 == Screenshots == 65 131 66 1. The Virtual Try-On button on a WooCommerce product page.67 2. The shopper upload modal .68 3. The shopper Virtual Try-On result.69 4. The Admin Virtual Photoshoot dashboard .132 1. The "Try It On" button on a WooCommerce product page — shoppers start a virtual try-on next to Add to Cart. 133 2. The shopper upload modal with consent and Digital Mannequin options. 134 3. A finished virtual try-on: the garment rendered on the shopper's own photo. 135 4. The Admin Virtual Photoshoot dashboard — AI product images without models. 70 136 5. AnyDress plugin settings and API dashboard. 71 137 72 138 == Changelog == 73 139 140 = 1.3.1 = 141 * Hardened input sanitization and declared compatibility with WordPress 7.0 (Plugin Check compliance). 142 143 = 1.3.0 = 144 * New settings: choose the Virtual Try-On aspect ratio (2:3, 3:4, 4:5) and resolution (1K or 2K) so every shopper result has a consistent, predictable format. 145 * Friendlier sizing feedback: when a selected size doesn't fit the shopper's measurements, the try-on now shows a helpful sizing-advice notice (instead of an error) and opens the size picker so they can switch and retry. 146 147 = 1.2.0 = 148 * Faster, more reliable generations: try-ons are now processed asynchronously — the page shows progress and delivers the image the moment it is ready, with no long-hanging requests. 149 * Improved reliability on slow hosts: generation no longer depends on a single long HTTP request. 150 151 = 1.1.1 = 152 * Size Charts: assign a chart to many products at once with a searchable product picker, and duplicate charts for product models with different measurements. 153 154 = 1.1.0 = 155 * New Size Charts system: reusable per-size garment measurements for tops, bottoms, dresses, outerwear, swimwear, eyewear, and headwear. 156 * Optional shopper body measurements (cm/inches) for a true-to-size virtual try-on, with a store setting to block or realistically render ill-fitting sizes. 157 * Per-product measurement overrides and automatic matching with WooCommerce size attributes. 158 * Fixed an issue where the face photo was uploaded twice, which could exceed server upload limits. 159 74 160 = 1.0.0 = 75 161 * Initial release. -
anydress-ai-virtual-try-on/tags/1.3.1/uninstall.php
r3530062 r3612360 23 23 'anydress_checkbox_tick_color', 24 24 'anydress_privacy_page_id', // Legacy setting 25 'anydress_plugin_version' 25 'anydress_plugin_version', 26 'anydress_privacy_policy_url', 27 'anydress_fit_mode', 28 'anydress_size_charts' 26 29 ); 27 30 … … 42 45 'anydress_mannequin_upload_time', 43 46 'anydress_usage_date', 44 'anydress_usage_count' 47 'anydress_usage_count', 48 'anydress_shopper_measurements', 49 'anydress_shopper_unit' 45 50 ); 46 51 … … 50 55 delete_metadata( 'user', 0, $anydress_meta_key, '', true ); 51 56 } 57 58 // 2b. Delete product sizing meta for ALL products 59 delete_metadata( 'post', 0, '_anydress_size_chart_id', '', true ); 60 delete_metadata( 'post', 0, '_anydress_size_overrides', '', true ); 52 61 53 62 // 3. Delete the custom mannequins folder and all images inside it -
anydress-ai-virtual-try-on/trunk/anydress-ai-virtual-try-on.php
r3530062 r3612360 4 4 * Plugin URI: https://anydress.ai/plugin/ 5 5 * Description: Elevate your digital boutique with seamless AI virtual try-on. 6 * Version: 1. 0.06 * Version: 1.3.1 7 7 * Author: AnyDress.ai 8 8 * Author URI: https://anydress.ai … … 20 20 21 21 // Define plugin constants for easy reference later 22 define( 'ANYDRESS_VERSION', '1. 0.0' ); // Added version for cache-busting CSS/JS22 define( 'ANYDRESS_VERSION', '1.3.1' ); // Added version for cache-busting CSS/JS 23 23 define( 'ANYDRESS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 24 24 define( 'ANYDRESS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 50 50 } 51 51 52 // 0. Load the shared Size Config (needed by Admin UI, AJAX, and Frontend alike) 53 require_once ANYDRESS_PLUGIN_DIR . 'includes/anydress-size-config.php'; 54 52 55 // 1. Load Admin UI (Only in WP Admin, but NOT during AJAX) 53 56 if ( is_admin() && ! wp_doing_ajax() ) { 54 57 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-settings.php'; 55 58 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-photoshoot.php'; 59 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-size-charts.php'; 60 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-product-sizes.php'; 56 61 } 57 62 58 63 // 2. Load AJAX Handlers (Only when an AJAX request is firing) 59 64 if ( wp_doing_ajax() ) { 65 // Async task status relay (shared by frontend + admin) 66 require_once ANYDRESS_PLUGIN_DIR . 'includes/anydress-task-client.php'; 60 67 // Admin AJAX 61 68 require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/ajax/ajax-generate-photoshoot.php'; -
anydress-ai-virtual-try-on/trunk/assets/css/frontend-tryon.css
r3530062 r3612360 127 127 border-color: #222; /* Default border */ 128 128 } 129 /* ========================================================================== 130 Sizing & Fit section (optional accurate-fit inputs) 131 ========================================================================== */ 132 .anydress-fit-section { 133 margin: 18px 0 5px; 134 text-align: left; 135 } 136 137 .anydress-fit-section details { 138 border: 1px solid #e2e2e2; 139 border-radius: 8px; 140 background: rgba(255, 255, 255, 0.6); 141 } 142 143 .anydress-fit-section summary { 144 padding: 12px 15px; 145 cursor: pointer; 146 font-weight: 600; 147 font-size: 14px; 148 user-select: none; 149 } 150 151 .anydress-fit-body { 152 padding: 5px 15px 15px; 153 border-top: 1px solid #eee; 154 } 155 156 .anydress-fit-row { 157 display: flex; 158 align-items: center; 159 gap: 12px; 160 margin: 10px 0; 161 font-size: 13px; 162 } 163 164 .anydress-fit-row > label { 165 min-width: 140px; 166 font-weight: 600; 167 } 168 169 .anydress-fit-row select { 170 padding: 6px 10px; 171 border: 1px solid #ccc; 172 border-radius: 6px; 173 max-width: 180px; 174 } 175 176 .anydress-fit-unit-opt { 177 margin-right: 12px; 178 cursor: pointer; 179 } 180 181 .anydress-fit-hint { 182 font-size: 12px; 183 color: #777; 184 margin: 8px 0 10px; 185 line-height: 1.4; 186 } 187 188 .anydress-fit-fields { 189 display: grid; 190 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 191 gap: 10px; 192 } 193 194 .anydress-fit-field label { 195 display: block; 196 font-size: 12px; 197 font-weight: 600; 198 margin-bottom: 3px; 199 line-height: 1.3; 200 } 201 202 .anydress-fit-field small { 203 font-weight: normal; 204 color: #888; 205 } 206 207 .anydress-fit-field input { 208 width: 100%; 209 padding: 6px 8px; 210 border: 1px solid #ccc; 211 border-radius: 6px; 212 font-size: 13px; 213 box-sizing: border-box; 214 } 215 216 .anydress-fit-warning { 217 background: #fff8e5; 218 border-left: 4px solid #dba617; 219 padding: 10px 14px; 220 margin: 12px 0; 221 font-size: 13px; 222 text-align: left; 223 border-radius: 0 6px 6px 0; 224 line-height: 1.4; 225 } -
anydress-ai-virtual-try-on/trunk/assets/js/admin-photoshoot.js
r3530062 r3612360 15 15 }); 16 16 } 17 // ====================================================================== 18 // ASYNC TASK RESOLUTION 19 // The server accepts jobs in seconds and returns a task_id; we poll a 20 // lightweight status action until the image is ready (or the task fails). 21 // ====================================================================== 22 async function anydressResolveTask(data) { 23 if (!data || !data.success || !data.data || !data.data.task_id) { 24 return data; // not a task response — pass through unchanged 25 } 26 const taskId = data.data.task_id; 27 28 for (let i = 0; i < 130; i++) { // ~9 minutes max (server times tasks out at 8) 29 await new Promise(r => setTimeout(r, i === 0 ? 6000 : 4000)); 30 try { 31 const fd = new FormData(); 32 fd.append('action', 'anydress_admin_check_task'); 33 fd.append('task_id', taskId); 34 fd.append('nonce', document.getElementById('anydress_nonce').value); 35 36 const resp = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: fd }); 37 const status = await resp.json(); 38 39 if (status && status.success && status.data && status.data.status === 'pending') { 40 continue; // still generating 41 } 42 return status; // completed (has image_url) or failed (error shape) 43 } catch (e) { 44 // transient network hiccup — keep polling 45 } 46 } 47 return { success: false, data: { message: 'Generation timed out. Please try again.' } }; 48 } 49 17 50 let originalImageUrl = ""; 18 51 let currentIsGhost = false; … … 436 469 let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 437 470 let data = await response.json(); 471 data = await anydressResolveTask(data); 438 472 439 473 // Fallback Interceptor … … 441 475 const loadingText = loadingBox.querySelector('p'); 442 476 if(loadingText) loadingText.innerText = "Optimizing... Please wait another 20-70 seconds"; 443 477 444 478 formData.set('force_fallback', 'true'); 445 479 response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 446 480 data = await response.json(); 481 data = await anydressResolveTask(data); 447 482 } 448 483 … … 662 697 let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 663 698 let data = await response.json(); 699 data = await anydressResolveTask(data); 664 700 665 701 // Fallback Interceptor 666 702 if (!data.success && data.data && data.data.error_code === 'require_fallback') { 667 703 loadingText.innerText = "Optimizing... Please wait another 20-70 seconds"; 668 704 669 705 formData.set('force_fallback', 'true'); 670 706 response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 671 707 data = await response.json(); 708 data = await anydressResolveTask(data); 672 709 } 673 710 … … 762 799 let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 763 800 let data = await response.json(); 801 data = await anydressResolveTask(data); 764 802 765 803 // Fallback Interceptor 766 804 if (!data.success && data.data && data.data.error_code === 'require_fallback') { 767 805 loadingText.innerText = "Optimizing... Please wait another 20-70 seconds"; 768 806 769 807 formData.set('force_fallback', 'true'); 770 808 response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData }); 771 809 data = await response.json(); 810 data = await anydressResolveTask(data); 772 811 } 773 812 -
anydress-ai-virtual-try-on/trunk/assets/js/frontend-tryon.js
r3530062 r3612360 75 75 } 76 76 77 // ====================================================================== 78 // SIZING & FIT (optional accurate-fit section) 79 // ====================================================================== 80 var sizing = anydressFrontend.sizing || null; 81 var fitSizeSelect = document.getElementById("anydress-fit-size"); 82 83 function fitActiveUnit() { 84 var checked = document.querySelector('input[name="anydress_fit_unit"]:checked'); 85 return checked ? checked.value : "cm"; 86 } 87 88 function fitUpdateUnitLabels() { 89 var unit = fitActiveUnit(); 90 document.querySelectorAll(".anydress-fit-unit-label").forEach(function (el) { 91 el.textContent = "(" + unit + ")"; 92 }); 93 } 94 95 function fitInitInputs() { 96 if (!sizing) { return; } 97 var unit = fitActiveUnit(); 98 var saved = sizing.saved || {}; 99 document.querySelectorAll(".anydress-fit-input").forEach(function (input) { 100 var key = input.getAttribute("data-key"); 101 var metric = saved[key]; // stored in cm (body) or mm (face) 102 if (typeof metric === "undefined" || metric === null || metric === "") { return; } 103 metric = parseFloat(metric); 104 if (!(metric > 0)) { return; } 105 if (input.getAttribute("data-fixed-unit") === "mm") { 106 input.value = metric; 107 } else { 108 input.value = (unit === "in") ? Math.round((metric / 2.54) * 10) / 10 : metric; 109 } 110 }); 111 fitUpdateUnitLabels(); 112 } 113 114 // Unit toggle: convert already-entered values between cm and in 115 document.querySelectorAll('input[name="anydress_fit_unit"]').forEach(function (radio) { 116 radio.addEventListener("change", function () { 117 var toUnit = radio.value; 118 document.querySelectorAll(".anydress-fit-input").forEach(function (input) { 119 if (input.getAttribute("data-fixed-unit") === "mm") { return; } 120 var v = parseFloat(input.value); 121 if (!(v > 0)) { return; } 122 input.value = (toUnit === "in") 123 ? Math.round((v / 2.54) * 10) / 10 124 : Math.round(v * 2.54 * 10) / 10; 125 }); 126 fitUpdateUnitLabels(); 127 }); 128 }); 129 130 // Sync the size picker with the WooCommerce variation dropdown (if one exists) 131 function fitSyncFromVariation() { 132 if (!sizing || !sizing.sizeAttrField || !fitSizeSelect) { return; } 133 var variationSelect = document.querySelector('select[name="' + sizing.sizeAttrField + '"]'); 134 if (!variationSelect || !variationSelect.value) { return; } 135 var chosen = variationSelect.selectedOptions.length 136 ? variationSelect.selectedOptions[0].textContent.trim() 137 : variationSelect.value; 138 var candidates = [variationSelect.value.toLowerCase(), chosen.toLowerCase()]; 139 for (var i = 0; i < fitSizeSelect.options.length; i++) { 140 var optVal = fitSizeSelect.options[i].value.toLowerCase().trim(); 141 if (optVal !== "" && candidates.indexOf(optVal) !== -1) { 142 fitSizeSelect.value = fitSizeSelect.options[i].value; 143 return; 144 } 145 } 146 } 147 148 if (sizing && sizing.sizeAttrField) { 149 var wooSizeSelect = document.querySelector('select[name="' + sizing.sizeAttrField + '"]'); 150 if (wooSizeSelect) { 151 wooSizeSelect.addEventListener("change", fitSyncFromVariation); 152 } 153 } 154 155 if (sizing) { 156 fitInitInputs(); 157 fitSyncFromVariation(); 158 } 159 160 // Append the shopper's sizing choices to an outgoing request 161 function fitAppendToForm(formData) { 162 if (!sizing) { return; } 163 164 if (fitSizeSelect && fitSizeSelect.value) { 165 formData.append("size_label", fitSizeSelect.value); 166 } 167 formData.append("measure_unit", fitActiveUnit()); 168 169 var any = false; 170 document.querySelectorAll(".anydress-fit-input").forEach(function (input) { 171 var v = parseFloat(input.value); 172 if (v > 0) { 173 formData.append("measurements[" + input.getAttribute("data-key") + "]", v); 174 any = true; 175 } 176 }); 177 178 if (any || (fitSizeSelect && fitSizeSelect.value)) { 179 formData.append("sizing_active", "true"); 180 } 181 } 182 183 // ====================================================================== 184 // ASYNC TASK RESOLUTION 185 // The server now accepts the job in seconds and returns a task_id; we poll 186 // a lightweight status action until the image is ready (or the task fails). 187 // ====================================================================== 188 async function anydressResolveTask(data) { 189 if (!data || !data.success || !data.data || !data.data.task_id) { 190 return data; // not a task response — pass through unchanged 191 } 192 var taskId = data.data.task_id; 193 194 for (var i = 0; i < 130; i++) { // ~9 minutes max (server times tasks out at 8) 195 await new Promise(function (r) { setTimeout(r, i === 0 ? 6000 : 4000); }); 196 try { 197 var fd = new FormData(); 198 fd.append("action", "anydress_check_task"); 199 fd.append("task_id", taskId); 200 fd.append("nonce", anydressFrontend.nonce); 201 202 var resp = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: fd }); 203 var status = await resp.json(); 204 205 if (status && status.success && status.data && status.data.status === "pending") { 206 continue; // still generating 207 } 208 return status; // completed (has image_url) or failed (error shape) 209 } catch (e) { 210 // transient network hiccup — keep polling 211 } 212 } 213 return { success: false, data: { message: "Generation timed out. Please try again." } }; 214 } 215 77 216 // Central function to send AJAX request 78 217 async function sendToEngine(formData, buttonToDisable) { … … 80 219 formData.append("force_fallback", "false"); 81 220 } 221 fitAppendToForm(formData); 82 222 83 223 buttonToDisable.innerText = "Processing... (This takes 20-70 seconds)"; … … 95 235 let response = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: formData }); 96 236 let data = await response.json(); 237 data = await anydressResolveTask(data); 97 238 98 239 if (!data.success && data.data && data.data.error_code === 'require_fallback') { 99 240 resultArea.innerHTML = "<p><i>Optimizing... Please wait another 20-70 seconds</i></p>"; 100 241 buttonToDisable.innerText = "Optimizing..."; 101 242 102 243 formData.set("force_fallback", "true"); 103 244 response = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: formData }); 104 245 data = await response.json(); 246 data = await anydressResolveTask(data); 105 247 } 106 248 … … 128 270 } 129 271 272 // Optional fit warning coming back from the sizing engine 273 var fitWarningHtml = ""; 274 if (data.data.fit_warning) { 275 var warnText = document.createElement("div"); 276 warnText.textContent = data.data.fit_warning; // XSS-safe 277 fitWarningHtml = "<div class='anydress-fit-warning'>⚠️ <b>" + (anydressFrontend.i18n.fitWarning || "Fit note:") + "</b> " + warnText.innerHTML + "</div>"; 278 } 279 130 280 // Display the image 131 resultArea.innerHTML = "<h3>Your Virtual Try-On:</h3> <img src='" + data.data.image_url + "' style='max-width:100%; max-height:60vh; object-fit:contain; display:block; margin: 15px auto 0; border-radius:8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);' />";281 resultArea.innerHTML = "<h3>Your Virtual Try-On:</h3>" + fitWarningHtml + "<img src='" + data.data.image_url + "' style='max-width:100%; max-height:60vh; object-fit:contain; display:block; margin: 15px auto 0; border-radius:8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);' />"; 132 282 } else { 133 283 // Error: Re-enable button and SHOW the link again 134 buttonToDisable.innerText = "Try Again";135 284 buttonToDisable.disabled = false; 136 285 if (showUploadLink) showUploadLink.style.display = ""; 137 138 // Handle object vs string error messages cleanly 286 287 // Handle object vs string error messages cleanly (XSS-safe) 139 288 let errorMsg = typeof data.data === 'string' ? data.data : (data.data.message || 'Unknown error'); 140 resultArea.innerHTML = "<p style='color:red; font-weight:bold;'>Error: " + errorMsg + "</p>"; 289 var safeMsg = document.createElement("div"); 290 safeMsg.textContent = errorMsg; 291 292 // Fit-advice blocks are helpful sizing guidance, not real errors — 293 // render them as a friendly amber notice instead of red error text. 294 var fitCodes = ["size_too_small", "size_too_large", "size_length_issue", 295 "frame_too_narrow", "frame_too_wide", "bridge_too_narrow", "bridge_too_wide", 296 "arms_too_short", "arms_too_long", "headwear_too_small", "headwear_too_large"]; 297 var errCode = (data.data && data.data.error_code) ? data.data.error_code : ""; 298 299 if (fitCodes.indexOf(errCode) !== -1) { 300 buttonToDisable.innerText = "Try Another Size"; 301 resultArea.innerHTML = "<div class='anydress-fit-warning'>📏 <b>Sizing advice:</b> " + safeMsg.innerHTML + "</div>"; 302 // Draw the shopper's eye to the size picker so they can switch and retry 303 var fitDetails = document.getElementById("anydress-fit-details"); 304 if (fitDetails) { fitDetails.open = true; } 305 } else { 306 buttonToDisable.innerText = "Try Again"; 307 resultArea.innerHTML = "<p style='color:red; font-weight:bold;'>Error: " + safeMsg.innerHTML + "</p>"; 308 } 141 309 } 142 310 … … 201 369 formData.append("use_saved_mannequin", "false"); 202 370 formData.append("face_image", compressedFace); 203 formData.append("face_image", compressedFace);204 371 formData.append("consent_given", "true"); 205 372 -
anydress-ai-virtual-try-on/trunk/includes/admin/ajax/ajax-generate-edit.php
r3530062 r3612360 75 75 'headers' => array( 'Content-Type' => 'application/json' ), 76 76 'body' => wp_json_encode( $payload ), 77 'timeout' => 12077 'timeout' => 60 78 78 )); 79 79 … … 91 91 wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) ); 92 92 } 93 if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits') {93 if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) { 94 94 wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) ); 95 95 } … … 97 97 } 98 98 99 if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 99 // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task' 100 if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 100 101 101 wp_send_json_success( array( ' image_url' => $data['image_url']) );102 wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) ); 102 103 } -
anydress-ai-virtual-try-on/trunk/includes/admin/ajax/ajax-generate-photoshoot.php
r3530062 r3612360 189 189 'headers' => array( 'Content-Type' => 'application/json' ), 190 190 'body' => wp_json_encode( $payload ), 191 'timeout' => 120191 'timeout' => 60 192 192 )); 193 193 … … 206 206 } 207 207 // NEW: Gracefully catch the out of credits error so the frontend can display an upgrade prompt 208 if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits') {208 if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) { 209 209 wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) ); 210 210 } … … 212 212 } 213 213 214 if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 215 216 wp_send_json_success( array( 'image_url' => $data['image_url'] ) ); 214 // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task' 215 if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 216 217 wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) ); 217 218 } 218 219 -
anydress-ai-virtual-try-on/trunk/includes/admin/ajax/ajax-magic-edit.php
r3530062 r3612360 75 75 'headers' => array( 'Content-Type' => 'application/json' ), 76 76 'body' => wp_json_encode( $payload ), 77 'timeout' => 12077 'timeout' => 60 78 78 )); 79 79 … … 91 91 wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) ); 92 92 } 93 if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits') {93 if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) { 94 94 wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) ); 95 95 } … … 97 97 } 98 98 99 if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 99 // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task' 100 if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 100 101 101 wp_send_json_success( array( ' image_url' => $data['image_url']) );102 wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) ); 102 103 } -
anydress-ai-virtual-try-on/trunk/includes/admin/class-anydress-photoshoot.php
r3530062 r3612360 242 242 <option value="4K"><?php esc_html_e( '4K (Ultra High Definition)', 'anydress-ai-virtual-try-on' ); ?></option> 243 243 </select> 244 <p style="font-size: 11px; color: #888; margin: 6px 0 0;"><?php esc_html_e( 'Note: swimwear, lingerie and similar items are processed by a dedicated engine that tops out at 2K — a 4K request for those items is generated and billed at 2K automatically.', 'anydress-ai-virtual-try-on' ); ?></p> 244 245 </div> 245 246 -
anydress-ai-virtual-try-on/trunk/includes/admin/class-anydress-settings.php
r3530062 r3612360 279 279 ); 280 280 281 register_setting( 'anydress_settings_group', 'anydress_fit_mode', array( $this, 'sanitize_fit_mode' ) ); 282 283 add_settings_field( 284 'anydress_fit_mode_field', 285 __( 'Sizing: Bad Fit Behavior', 'anydress-ai-virtual-try-on' ), 286 array( $this, 'fit_mode_render' ), 287 'anydress-settings', 288 'anydress_settings_section' 289 ); 290 291 register_setting( 'anydress_settings_group', 'anydress_vto_aspect', array( $this, 'sanitize_vto_aspect' ) ); 292 register_setting( 'anydress_settings_group', 'anydress_vto_resolution', array( $this, 'sanitize_vto_resolution' ) ); 293 294 add_settings_field( 295 'anydress_vto_aspect_field', 296 __( 'Try-On Image Aspect Ratio', 'anydress-ai-virtual-try-on' ), 297 array( $this, 'vto_aspect_render' ), 298 'anydress-settings', 299 'anydress_settings_section' 300 ); 301 302 add_settings_field( 303 'anydress_vto_resolution_field', 304 __( 'Try-On Image Resolution', 'anydress-ai-virtual-try-on' ), 305 array( $this, 'vto_resolution_render' ), 306 'anydress-settings', 307 'anydress_settings_section' 308 ); 309 281 310 // ========================================== 282 311 // 2. APPEARANCE SETTINGS … … 375 404 } 376 405 406 public function sanitize_fit_mode( $value ) { 407 return in_array( $value, array( 'block', 'warn' ), true ) ? $value : 'block'; 408 } 409 410 public function sanitize_vto_aspect( $value ) { 411 return in_array( $value, array( '2:3', '3:4', '4:5' ), true ) ? $value : '3:4'; 412 } 413 414 public function sanitize_vto_resolution( $value ) { 415 return in_array( $value, array( '1K', '2K' ), true ) ? $value : '1K'; 416 } 417 418 public function vto_aspect_render() { 419 $val = get_option( 'anydress_vto_aspect', '3:4' ); 420 $options = array( 421 '2:3' => __( '2:3 — Classic fashion editorial (tallest)', 'anydress-ai-virtual-try-on' ), 422 '3:4' => __( '3:4 — Balanced portrait (default)', 'anydress-ai-virtual-try-on' ), 423 '4:5' => __( '4:5 — Ecommerce / Instagram standard', 'anydress-ai-virtual-try-on' ), 424 ); 425 echo '<select name="anydress_vto_aspect">'; 426 foreach ( $options as $key => $label ) { 427 echo '<option value="' . esc_attr( $key ) . '" ' . selected( $val, $key, false ) . '>' . esc_html( $label ) . '</option>'; 428 } 429 echo '</select>'; 430 echo '<p class="description">' . esc_html__( 'Every shopper try-on image is generated in this fixed portrait format, so results always look consistent across your store.', 'anydress-ai-virtual-try-on' ) . '</p>'; 431 } 432 433 public function vto_resolution_render() { 434 $val = get_option( 'anydress_vto_resolution', '1K' ); 435 echo '<label style="margin-right: 15px;"><input type="radio" name="anydress_vto_resolution" value="1K" ' . checked( $val, '1K', false ) . ' /> ' . esc_html__( '1K — Standard (1 credit rate)', 'anydress-ai-virtual-try-on' ) . '</label>'; 436 echo '<label><input type="radio" name="anydress_vto_resolution" value="2K" ' . checked( $val, '2K', false ) . ' /> ' . esc_html__( '2K — High detail (1.5× credit rate)', 'anydress-ai-virtual-try-on' ) . '</label>'; 437 echo '<p class="description">' . esc_html__( 'Resolution of shopper try-on images. 2K looks sharper on zoom but each try-on costs 1.5× the credits and loads slightly slower on mobile.', 'anydress-ai-virtual-try-on' ) . '</p>'; 438 } 439 440 public function fit_mode_render() { 441 $val = get_option( 'anydress_fit_mode', 'block' ); 442 443 echo '<label style="display: block; margin-bottom: 8px;">'; 444 echo '<input type="radio" name="anydress_fit_mode" value="block" ' . checked( 'block', $val, false ) . ' /> '; 445 echo '<strong>' . esc_html__( 'Block generation', 'anydress-ai-virtual-try-on' ) . '</strong> — ' . esc_html__( 'if the shopper\'s measurements say the selected size won\'t fit, skip the image and suggest a better size. Saves credits.', 'anydress-ai-virtual-try-on' ); 446 echo '</label>'; 447 448 echo '<label style="display: block;">'; 449 echo '<input type="radio" name="anydress_fit_mode" value="warn" ' . checked( 'warn', $val, false ) . ' /> '; 450 echo '<strong>' . esc_html__( 'Warn but generate', 'anydress-ai-virtual-try-on' ) . '</strong> — ' . esc_html__( 'show a fit warning but still generate the image with a realistically tight or loose fit. Uses credits even for bad fits.', 'anydress-ai-virtual-try-on' ); 451 echo '</label>'; 452 453 echo '<p class="description">' . wp_kses_post( __( 'Only applies when a product has a <strong>Size Chart</strong> assigned and the shopper provides their body measurements. Manage charts under <strong>AnyDress AI → Size Charts</strong>.', 'anydress-ai-virtual-try-on' ) ) . '</p>'; 454 } 455 377 456 public function privacy_policy_url_render() { 378 457 $val = get_option( 'anydress_privacy_policy_url', '' ); -
anydress-ai-virtual-try-on/trunk/includes/frontend/ajax/ajax-frontend-tryon.php
r3530062 r3612360 244 244 245 245 // ==================================================================== 246 // SIZING & FIT: Build size_data from the store's chart + shopper input 247 // ==================================================================== 248 $size_data = null; 249 250 $sizing_active = isset( $_POST['sizing_active'] ) && $_POST['sizing_active'] === 'true'; 251 252 if ( $sizing_active && function_exists( 'anydress_size_resolve_garment_data' ) ) { 253 254 $size_label = isset( $_POST['size_label'] ) ? sanitize_text_field( wp_unslash( $_POST['size_label'] ) ) : ''; 255 $unit = ( isset( $_POST['measure_unit'] ) && sanitize_text_field( wp_unslash( $_POST['measure_unit'] ) ) === 'in' ) ? 'in' : 'cm'; 256 257 // 1. Garment measurements: resolved server-side from the product's size chart. 258 // The browser only ever sends the size LABEL — measurements are trusted store data. 259 $size_data = anydress_size_resolve_garment_data( $product_id, $size_label ); 260 261 if ( $size_data ) { 262 // 2. Shopper measurements (optional). Sanitized against the product type's 263 // known field list and converted to metric. 264 $raw_measurements = array(); 265 if ( isset( $_POST['measurements'] ) && is_array( $_POST['measurements'] ) ) { 266 // Values are numeric strings; numeric validation happens field-by-field 267 // in anydress_size_sanitize_shopper_measurements() right after. 268 $raw_measurements = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['measurements'] ) ); 269 } 270 271 $shopper = anydress_size_sanitize_shopper_measurements( $raw_measurements, $size_data['product_type'], $unit ); 272 273 if ( ! empty( $shopper ) ) { 274 // Persist for reuse (same lifecycle as the mannequin: My Account deletion, 275 // 7-day cron, uninstall). Merge so switching product types keeps other fields. 276 $existing = get_user_meta( $user_id, 'anydress_shopper_measurements', true ); 277 if ( ! is_array( $existing ) ) { 278 $existing = array(); 279 } 280 update_user_meta( $user_id, 'anydress_shopper_measurements', array_merge( $existing, $shopper ) ); 281 update_user_meta( $user_id, 'anydress_shopper_unit', $unit ); 282 283 $size_data = array_merge( $size_data, $shopper ); 284 $size_data['fit_mode'] = anydress_size_get_fit_mode(); 285 } else { 286 // Size chosen but no body measurements: fit validation is impossible. 287 // Send nothing so the try-on behaves exactly like the standard flow. 288 $size_data = null; 289 } 290 } 291 } 292 293 // ==================================================================== 246 294 // SAAS ROUTING: Send to AnyDress.ai Server 247 295 // ==================================================================== … … 255 303 256 304 $payload = array( 257 'license_key' => $api_key, 305 'license_key' => $api_key, 258 306 'client_domain' => $safe_domain, 259 'user_hash' => $user_hash, 307 'user_hash' => $user_hash, 260 308 'force_fallback' => $force_fallback, 309 'aspect_ratio' => get_option( 'anydress_vto_aspect', '3:4' ), 310 'resolution' => get_option( 'anydress_vto_resolution', '1K' ), 261 311 'face_base64' => $face_base64, 262 312 'face_mime' => $face_mime, … … 267 317 ); 268 318 319 // Attach sizing payload only when fully resolved (garment + shopper data) 320 if ( ! empty( $size_data ) ) { 321 $payload['size_data'] = $size_data; 322 } 323 269 324 $response = wp_remote_post( $saas_endpoint, array( 270 325 'headers' => array( 'Content-Type' => 'application/json' ), 271 326 'body' => wp_json_encode( $payload ), 272 'timeout' => 120 // 120 seconds to allow for image generation327 'timeout' => 60 // Submission returns a task_id in seconds; polling handles the rest 273 328 )); 274 329 … … 284 339 // Handle errors returned by the SaaS Server 285 340 if ( $status_code !== 200 ) { 286 // Propagate the specific fallback error code to frontend shopper 287 if ( isset( $data['code'] ) && $data['code'] === 'require_fallback' ) { 288 wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) ); 289 } 290 $error_msg = isset( $data['message'] ) ? $data['message'] : __( 'Unknown Server Error.', 'anydress-ai-virtual-try-on' ); 291 wp_send_json_error( $error_msg ); 292 } 293 294 if ( empty( $data['image_url'] ) ) { 295 wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 296 } 341 // Always relay the server's error code so the JS can style fit-advice 342 // blocks (size_too_small etc.) differently from real errors. 343 $error_code = isset( $data['code'] ) ? sanitize_key( $data['code'] ) : 'server_error'; 344 $error_msg = isset( $data['message'] ) ? $data['message'] : __( 'Unknown Server Error.', 'anydress-ai-virtual-try-on' ); 345 wp_send_json_error( array( 'error_code' => $error_code, 'message' => $error_msg ) ); 346 } 347 348 // ASYNC: the server now returns a task_id immediately. The browser polls 349 // the 'anydress_check_task' action until the image is ready. 350 if ( empty( $data['task_id'] ) ) { 351 wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) ); 352 } 353 354 // Count the daily-limit slot NOW (prevents bypassing the limit with parallel 355 // submissions). If the task terminally fails, the status relay refunds it. 297 356 update_user_meta( $user_id, 'anydress_usage_count', $usage_count + 1 ); 298 357 299 // Success! Send it to the frontend. 300 wp_send_json_success( array( 'image_url' => $data['image_url'] ) ); 358 wp_send_json_success( array( 359 'task_id' => sanitize_text_field( $data['task_id'] ), 360 'status' => 'pending', 361 ) ); 301 362 } -
anydress-ai-virtual-try-on/trunk/includes/frontend/anydress-cron.php
r3530062 r3612360 56 56 // 4. Remove timestamp 57 57 delete_user_meta( $user_id, 'anydress_mannequin_upload_time' ); 58 59 // 5. Delete saved body measurements (same privacy lifecycle as photos) 60 delete_user_meta( $user_id, 'anydress_shopper_measurements' ); 61 delete_user_meta( $user_id, 'anydress_shopper_unit' ); 58 62 } 59 63 } -
anydress-ai-virtual-try-on/trunk/includes/frontend/class-anydress-my-account.php
r3530062 r3612360 16 16 add_filter( 'woocommerce_account_menu_items', array( $this, 'add_my_account_menu_item' ) ); 17 17 add_action( 'template_redirect', array( $this, 'handle_mannequin_deletion' ) ); 18 add_action( 'template_redirect', array( $this, 'handle_measurements_save' ) ); 18 19 add_action( 'woocommerce_account_digital-mannequin_endpoint', array( $this, 'my_account_endpoint_content' ) ); 20 } 21 22 /** 23 * All body-measurement fields a shopper can store, across every product type. 24 * Returns array of payload_key => array( label, unit ). 25 */ 26 private function get_all_shopper_fields() { 27 $fields = array(); 28 if ( ! function_exists( 'anydress_size_get_product_types' ) ) { 29 return $fields; 30 } 31 foreach ( anydress_size_get_product_types() as $type ) { 32 foreach ( $type['shopper_fields'] as $key => $label ) { 33 if ( ! isset( $fields[ $key ] ) ) { 34 $fields[ $key ] = array( 'label' => $label, 'unit' => $type['unit'] ); 35 } 36 } 37 } 38 return $fields; 19 39 } 20 40 … … 84 104 85 105 // 4. Remove timestamp 86 delete_user_meta( $user_id, 'anydress_mannequin_upload_time' ); 106 delete_user_meta( $user_id, 'anydress_mannequin_upload_time' ); 107 108 // 5. Delete saved body measurements 109 delete_user_meta( $user_id, 'anydress_shopper_measurements' ); 110 delete_user_meta( $user_id, 'anydress_shopper_unit' ); 87 111 88 112 // Show a success message in WooCommerce … … 90 114 91 115 // Redirect back to the tab to refresh the page safely 116 wp_safe_redirect( wc_get_endpoint_url( 'digital-mannequin', '', wc_get_page_permalink( 'myaccount' ) ) ); 117 exit; 118 } 119 } 120 121 /** 122 * Handle saving of body measurements from the My Account tab 123 */ 124 public function handle_measurements_save() { 125 if ( is_account_page() && isset( $_POST['anydress_save_measurements'] ) && is_user_logged_in() ) { 126 if ( ! isset( $_POST['anydress_measurements_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['anydress_measurements_nonce'] ) ), 'save_measurements_action' ) ) { 127 wc_add_notice( __( 'Security check failed. Please try again.', 'anydress-ai-virtual-try-on' ), 'error' ); 128 return; 129 } 130 131 $user_id = get_current_user_id(); 132 $fields = $this->get_all_shopper_fields(); 133 $unit = ( isset( $_POST['anydress_unit'] ) && sanitize_text_field( wp_unslash( $_POST['anydress_unit'] ) ) === 'in' ) ? 'in' : 'cm'; 134 135 $clean = array(); 136 if ( isset( $_POST['anydress_m'] ) && is_array( $_POST['anydress_m'] ) ) { 137 // Values are numeric strings; validated numerically field-by-field below 138 $raw = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['anydress_m'] ) ); 139 foreach ( $fields as $key => $def ) { 140 if ( isset( $raw[ $key ] ) && $raw[ $key ] !== '' && is_numeric( $raw[ $key ] ) ) { 141 $val = (float) $raw[ $key ]; 142 if ( $val <= 0 ) { 143 continue; 144 } 145 // mm fields (face) are always entered in mm; cm fields follow the chosen unit 146 if ( $def['unit'] === 'mm' ) { 147 if ( $val <= 400 ) { 148 $clean[ $key ] = round( $val, 1 ); 149 } 150 } else { 151 $metric = ( $unit === 'in' ) ? round( $val * 2.54, 1 ) : round( $val, 1 ); 152 if ( $metric <= 350 ) { 153 $clean[ $key ] = $metric; 154 } 155 } 156 } 157 } 158 } 159 160 if ( ! empty( $clean ) ) { 161 update_user_meta( $user_id, 'anydress_shopper_measurements', $clean ); 162 update_user_meta( $user_id, 'anydress_shopper_unit', $unit ); 163 wc_add_notice( __( 'Your body measurements have been saved.', 'anydress-ai-virtual-try-on' ), 'success' ); 164 } else { 165 delete_user_meta( $user_id, 'anydress_shopper_measurements' ); 166 delete_user_meta( $user_id, 'anydress_shopper_unit' ); 167 wc_add_notice( __( 'Your body measurements have been removed.', 'anydress-ai-virtual-try-on' ), 'success' ); 168 } 169 92 170 wp_safe_redirect( wc_get_endpoint_url( 'digital-mannequin', '', wc_get_page_permalink( 'myaccount' ) ) ); 93 171 exit; … … 152 230 echo '<a href="' . esc_url( $shop_url ) . '" style="display: inline-block; background: #222; color: #fff; padding: 10px 20px; text-decoration: none; border-radius: 4px;">' . esc_html__( 'Go to Shop', 'anydress-ai-virtual-try-on' ) . '</a>'; 153 231 echo '</div>'; 154 155 echo '</div>'; 156 } 232 233 echo '</div>'; 234 } 235 236 $this->render_measurements_section( $user_id ); 237 } 238 239 /** 240 * The "My Body Measurements" edit form inside the My Account tab. 241 */ 242 private function render_measurements_section( $user_id ) { 243 $fields = $this->get_all_shopper_fields(); 244 if ( empty( $fields ) ) { 245 return; 246 } 247 248 $saved = get_user_meta( $user_id, 'anydress_shopper_measurements', true ); 249 if ( ! is_array( $saved ) ) { 250 $saved = array(); 251 } 252 $unit = get_user_meta( $user_id, 'anydress_shopper_unit', true ); 253 if ( ! in_array( $unit, array( 'cm', 'in' ), true ) ) { 254 $unit = 'cm'; 255 } 256 257 echo '<h3 style="margin-top: 40px;">' . esc_html__( '📏 My Body Measurements', 'anydress-ai-virtual-try-on' ) . '</h3>'; 258 echo '<p style="color: #666; font-size: 14px;">' . esc_html__( 'Used to make your Virtual Try-On fit as realistic as possible. All fields are optional. Like your mannequin photos, measurements are automatically deleted 7 days after your last mannequin upload.', 'anydress-ai-virtual-try-on' ) . '</p>'; 259 260 echo '<form method="post" style="background: #fff; padding: 20px; border: 1px solid #e2e2e2; border-radius: 8px;">'; 261 wp_nonce_field( 'save_measurements_action', 'anydress_measurements_nonce' ); 262 263 // Unit selector 264 echo '<p style="margin-top: 0;"><strong>' . esc_html__( 'Units:', 'anydress-ai-virtual-try-on' ) . '</strong> '; 265 echo '<label style="margin-right: 12px;"><input type="radio" name="anydress_unit" value="cm" ' . checked( $unit, 'cm', false ) . ' /> cm</label>'; 266 echo '<label><input type="radio" name="anydress_unit" value="in" ' . checked( $unit, 'in', false ) . ' /> in</label>'; 267 echo ' <small style="color: #888;">' . esc_html__( '(face measurements for eyewear are always in mm)', 'anydress-ai-virtual-try-on' ) . '</small></p>'; 268 269 echo '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px;">'; 270 foreach ( $fields as $key => $def ) { 271 $metric_val = isset( $saved[ $key ] ) ? (float) $saved[ $key ] : ''; 272 $display_val = $metric_val; 273 $unit_label = $def['unit']; 274 275 if ( $def['unit'] === 'cm' ) { 276 $unit_label = $unit; 277 if ( $metric_val !== '' && $unit === 'in' ) { 278 $display_val = round( $metric_val / 2.54, 1 ); 279 } 280 } 281 282 echo '<div>'; 283 echo '<label style="display: block; font-size: 13px; font-weight: 600; margin-bottom: 3px;">' . esc_html( $def['label'] ) . ' <small style="font-weight: normal; color: #888;">(' . esc_html( $unit_label ) . ')</small></label>'; 284 echo '<input type="number" step="0.1" min="0" name="anydress_m[' . esc_attr( $key ) . ']" value="' . esc_attr( $display_val ) . '" style="width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box;" />'; 285 echo '</div>'; 286 } 287 echo '</div>'; 288 289 echo '<div style="margin-top: 15px;">'; 290 echo '<button type="submit" name="anydress_save_measurements" value="1" style="background-color: #222; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: bold;">' . esc_html__( 'Save Measurements', 'anydress-ai-virtual-try-on' ) . '</button>'; 291 echo ' <small style="color: #888; margin-left: 10px;">' . esc_html__( 'Clear all fields and save to delete your measurements.', 'anydress-ai-virtual-try-on' ) . '</small>'; 292 echo '</div>'; 293 294 echo '</form>'; 157 295 } 158 296 } -
anydress-ai-virtual-try-on/trunk/includes/frontend/class-anydress-widget.php
r3530062 r3612360 70 70 'productId' => $product_id, 71 71 'nonce' => wp_create_nonce( 'anydress_frontend_action' ), 72 'sizing' => $this->get_sizing_context( $product_id ), 72 73 'i18n' => array( 73 74 'uploadFace' => __( 'Please upload your face photo.', 'anydress-ai-virtual-try-on' ), 74 75 'faceCompressed' => __( '⚠️ Your face photo could not be compressed enough. Please try a different image.', 'anydress-ai-virtual-try-on' ), 75 76 'bodyCompressed' => __( '⚠️ Your body photo could not be compressed enough. Please try a different image.', 'anydress-ai-virtual-try-on' ), 76 'errorProcess' => __( 'Error processing images. Please try different photos.', 'anydress-ai-virtual-try-on' ) 77 'errorProcess' => __( 'Error processing images. Please try different photos.', 'anydress-ai-virtual-try-on' ), 78 'fitWarning' => __( 'Fit note:', 'anydress-ai-virtual-try-on' ) 77 79 ) 78 80 )); 81 } 82 83 /** 84 * Build the sizing context for the frontend JS. 85 * Returns null when the product has no size chart assigned (plugin behaves exactly as before). 86 * NOTE: garment measurements themselves never leave the server — only size labels go to the browser. 87 */ 88 private function get_sizing_context( $product_id ) { 89 if ( ! function_exists( 'anydress_size_get_product_chart' ) ) { 90 return null; 91 } 92 93 $chart = anydress_size_get_product_chart( $product_id ); 94 if ( ! $chart ) { 95 return null; 96 } 97 98 $type_def = anydress_size_get_type( $chart['product_type'] ); 99 if ( ! $type_def ) { 100 return null; 101 } 102 103 $size_labels = array(); 104 foreach ( $chart['rows'] as $row ) { 105 $size_labels[] = $row['label']; 106 } 107 108 // Locate the size attribute so JS can sync with the variation dropdown 109 $size_attr_field = ''; 110 $product = wc_get_product( $product_id ); 111 $size_attr = anydress_size_get_product_size_attribute( $product ); 112 if ( $size_attr ) { 113 $size_attr_field = 'attribute_' . sanitize_title( $size_attr['attribute_name'] ); 114 } 115 116 // Saved shopper measurements (stored in metric) 117 $user_id = get_current_user_id(); 118 $saved = $user_id ? get_user_meta( $user_id, 'anydress_shopper_measurements', true ) : array(); 119 if ( ! is_array( $saved ) ) { 120 $saved = array(); 121 } 122 $saved_unit = $user_id ? get_user_meta( $user_id, 'anydress_shopper_unit', true ) : ''; 123 if ( ! in_array( $saved_unit, array( 'cm', 'in' ), true ) ) { 124 $saved_unit = 'cm'; 125 } 126 127 return array( 128 'productType' => $chart['product_type'], 129 'metricUnit' => $type_def['unit'], // what the server expects (cm or mm) 130 'allowImperial' => (bool) $type_def['allow_imperial'], 131 'preferredUnit' => $saved_unit, 132 'sizeLabels' => $size_labels, 133 'sizeAttrField' => $size_attr_field, 134 'shopperFields' => $type_def['shopper_fields'], 135 'saved' => (object) $saved, 136 ); 79 137 } 80 138 … … 193 251 echo ' </div>'; 194 252 253 // --- SIZING & FIT SECTION (only when the product has a size chart) --- 254 // Note: get_queried_object_id() is reliable in wp_footer, unlike get_the_ID() after custom loops 255 $sizing = $this->get_sizing_context( get_queried_object_id() ); 256 if ( $sizing ) { 257 $this->render_fit_section( $sizing ); 258 } 259 195 260 echo ' <div id="anydress-result-area"></div>'; 196 261 … … 198 263 echo '</div>'; 199 264 } 265 266 /** 267 * Render the optional "Get an accurate fit" block inside the modal. 268 * Works for both the saved-mannequin flow and the new-upload flow. 269 */ 270 private function render_fit_section( $sizing ) { 271 $is_mm = ( $sizing['metricUnit'] === 'mm' ); 272 273 echo '<div id="anydress-fit-section" class="anydress-fit-section">'; 274 echo ' <details id="anydress-fit-details">'; 275 echo ' <summary>' . esc_html__( '📏 Get an accurate fit (optional)', 'anydress-ai-virtual-try-on' ) . '</summary>'; 276 echo ' <div class="anydress-fit-body">'; 277 278 // 1. Size picker (synced with the variation dropdown when one exists) 279 echo ' <div class="anydress-fit-row">'; 280 echo ' <label for="anydress-fit-size">' . esc_html__( 'Size you\'re trying on', 'anydress-ai-virtual-try-on' ) . '</label>'; 281 echo ' <select id="anydress-fit-size">'; 282 echo ' <option value="">' . esc_html__( '— Select size —', 'anydress-ai-virtual-try-on' ) . '</option>'; 283 foreach ( $sizing['sizeLabels'] as $label ) { 284 echo ' <option value="' . esc_attr( $label ) . '">' . esc_html( $label ) . '</option>'; 285 } 286 echo ' </select>'; 287 echo ' </div>'; 288 289 // 2. Unit toggle (hidden for mm-based products like eyewear) 290 if ( ! $is_mm && $sizing['allowImperial'] ) { 291 echo ' <div class="anydress-fit-row anydress-fit-unit-row">'; 292 echo ' <label>' . esc_html__( 'Units', 'anydress-ai-virtual-try-on' ) . '</label>'; 293 echo ' <span>'; 294 echo ' <label class="anydress-fit-unit-opt"><input type="radio" name="anydress_fit_unit" value="cm" ' . checked( $sizing['preferredUnit'], 'cm', false ) . ' /> cm</label>'; 295 echo ' <label class="anydress-fit-unit-opt"><input type="radio" name="anydress_fit_unit" value="in" ' . checked( $sizing['preferredUnit'], 'in', false ) . ' /> in</label>'; 296 echo ' </span>'; 297 echo ' </div>'; 298 } 299 300 // 3. Measurement inputs (values injected by JS from saved data, converted to the active unit) 301 echo ' <p class="anydress-fit-hint">' . esc_html__( 'Your measurements are optional — the more you fill in, the more realistic the generated fit. They are saved with your digital mannequin and automatically deleted after 7 days.', 'anydress-ai-virtual-try-on' ) . '</p>'; 302 echo ' <div class="anydress-fit-fields">'; 303 foreach ( $sizing['shopperFields'] as $key => $label ) { 304 $unit_suffix = $is_mm ? 'mm' : ''; // body-field suffix is drawn by JS (follows the toggle) 305 echo ' <div class="anydress-fit-field">'; 306 echo ' <label for="anydress-fit-' . esc_attr( $key ) . '">' . esc_html( $label ) . ( $unit_suffix ? ' <small>(' . esc_html( $unit_suffix ) . ')</small>' : ' <small class="anydress-fit-unit-label"></small>' ); 307 echo ' </label>'; 308 echo ' <input type="number" step="0.1" min="0" inputmode="decimal" id="anydress-fit-' . esc_attr( $key ) . '" class="anydress-fit-input" data-key="' . esc_attr( $key ) . '" data-fixed-unit="' . esc_attr( $is_mm ? 'mm' : '' ) . '" />'; 309 echo ' </div>'; 310 } 311 echo ' </div>'; 312 313 echo ' </div>'; 314 echo ' </details>'; 315 echo '</div>'; 316 } 200 317 } 201 318 -
anydress-ai-virtual-try-on/trunk/readme.txt
r3530142 r3612360 1 === AnyDress AI Virtual Try-On===1 === AnyDress – AI Virtual Try-On for WooCommerce & AI Product Photos === 2 2 Contributors: anydress 3 Tags: woocommerce, virtual try on, ai, fashion, ecommerce3 Tags: virtual try on, woocommerce, ai, product images, clothing 4 4 Requires at least: 5.8 5 Tested up to: 6.96 Stable tag: 1. 0.05 Tested up to: 7.0 6 Stable tag: 1.3.1 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Elevate your digital boutique with seamless AI virtual try-on and instant studio-quality catalog photoshoots.11 AI virtual try-on for WooCommerce: shoppers try on clothes on their own photo, and you generate studio-quality product images without models. 12 12 13 13 == Description == 14 14 15 AnyDress AI Virtual Try-On revolutionizes your WooCommerce store by allowing shoppers to visually "try on" your clothing using artificial intelligence. Increase conversion rates, boost buyer confidence, and reduce return rates by letting customers see exactly how your garments will look on their own bodies.15 **AnyDress adds an AI virtual fitting room to your WooCommerce store — and an AI photo studio to your dashboard.** Shoppers can virtually try on clothes using their own photos before they buy, and store owners can generate professional product images without models, photographers, or physical photoshoots. 16 16 17 Additionally, store owners can use the Virtual Photoshoot dashboard to generate professional, studio-quality catalog images of their garments using AI models or ghost mannequins, saving thousands of dollars on physicalphotography.17 One plugin, two revenue levers: higher conversions from confident shoppers, and lower costs from AI-generated catalog photography. 18 18 19 = Features = 20 * **Shopper Virtual Try-On:** Adds a customizable "Try It On" button to WooCommerce product pages. 21 * **Digital Mannequin:** Logged-in shoppers can securely save their face and body photos for 1-click try-ons across your whole store. 22 * **Admin Virtual Photoshoot:** Instantly generate new product catalog images using WooCommerce products or raw garment uploads. 23 * **AI Models & Ghost Mannequins:** Choose to use the original model, upload your own, generate a random AI model, or create invisible ghost mannequins. 24 * **Custom Backgrounds:** Describe any scene or background to place your models in. 25 * **Magic Edit:** Alter the pose, aspect ratio, resolution, and photography style of your generated images. 26 * **Usage Controls:** Set daily try-on limits to protect your API budget. 27 * **Privacy-First Design:** Built-in legal consent checkboxes for both shoppers and admins, ensuring no data is sent to the AI engine without explicit user agreement. 19 = A virtual fitting room for your shoppers = 20 21 A customizable **"Try It On" button** appears on your product pages, right next to Add to Cart. Shoppers upload a photo (or reuse their saved Digital Mannequin) and see exactly how the garment looks on their own body — not on a model who looks nothing like them. 22 23 * **1-click try-ons across your whole store** — logged-in shoppers securely save face and body photos once, then try on anything. 24 * **True-to-size fit** — pair try-ons with real garment measurements so shoppers see (or get warned about) sizes that run tight or loose. Fewer sizing mistakes, fewer returns, less bracketing. 25 * **Works with your theme** — hooks into standard WooCommerce templates automatically. 26 27 = An AI photo studio for store owners = 28 29 The **Virtual Photoshoot dashboard** turns a flat lay, hanger shot, or plain packshot into studio-quality on-model imagery in minutes: 30 31 * Generate catalog images from your existing WooCommerce product photos or raw garment uploads. 32 * Choose an **AI model**, upload your own model, or create clean **ghost mannequin** shots. 33 * Describe any **custom background** or scene in plain words. 34 * **Magic Edit** — change pose, aspect ratio, resolution, and photography style after generation. 35 36 No studio rental, no model fees, no reshoots when your catalog changes. 37 38 = Built-in size charts = 39 40 Create reusable size charts (tops, bottoms, dresses, outerwear, swimwear, eyewear, headwear) with real garment measurements per size. Assign them to products in bulk, override values per product, and let shoppers optionally add their own measurements (cm or inches) for a realistic fit. 41 42 = Privacy-first by design = 43 44 * Explicit consent checkboxes for both shoppers and admins — no data reaches the AI engine without agreement. 45 * Shopper photos live on **your own server**, auto-deleted after 7 days; shoppers can delete their data anytime from My Account. 46 * Images are processed in memory and discarded by the API immediately after generation — never used to train AI models. 47 48 = Perfect for = 49 50 * [Apparel & clothing stores](https://anydress.ai/use-cases/apparel/) 51 * [Eyewear & glasses](https://anydress.ai/use-cases/eyewear/) 52 * [Baby & kids clothes](https://anydress.ai/use-cases/baby-clothes/) 53 * [Jewelry](https://anydress.ai/use-cases/jewelry/) 54 * [Other use cases](https://anydress.ai/use-cases/) 55 56 = All features = 57 58 * **Shopper Virtual Try-On:** customizable "Try It On" button on WooCommerce product pages. 59 * **Digital Mannequin:** shoppers securely save photos for 1-click try-ons store-wide. 60 * **Admin Virtual Photoshoot:** generate product catalog images from products or raw garment uploads. 61 * **AI Models & Ghost Mannequins:** original model, your own model, a generated AI model, or invisible ghost mannequins. 62 * **Custom Backgrounds:** describe any scene to place your models in. 63 * **Magic Edit:** alter pose, aspect ratio, resolution, and photography style. 64 * **Size Charts & True-to-Size Fit:** reusable charts with per-size garment measurements, bulk assignment, per-product overrides. 65 * **Shopper Measurements (optional):** private body measurements for realistic fit rendering and sizing advice. 66 * **Usage Controls:** daily try-on limits to protect your API budget. 67 * **Privacy-First Design:** built-in legal consent flows for shoppers and admins. 68 69 = Learn more = 70 71 * [How AI virtual try-on works in WooCommerce](https://anydress.ai/virtual-try-on-for-woocommerce-guide/) 72 * [Creating product images without models](https://anydress.ai/ai-clothing-photography-without-a-model/) 73 * [Reducing clothing return rates](https://anydress.ai/reduce-clothing-return-rates-woocommerce/) 74 * [Plans and pricing](https://anydress.ai/#pricing) 28 75 29 76 == External Services == … … 32 79 33 80 * What the service is used for: The AnyDress AI API provides the artificial intelligence engine required to "clothe" a digital mannequin with a product image and to generate studio-quality catalog photoshoots. 34 * What data is sent and when: 81 * What data is sent and when: 35 82 1. Shopper Try-On: Only after a logged-in user gives explicit consent via a checkbox, their uploaded photos (Face/Body) and the WooCommerce product image are converted to Base64 and sent to the AnyDress AI endpoint. 36 2. Admin Photoshoot: Only after the administrator confirms rights to the images, the selected garment and model photos are sent to the API. 37 3. Verification: The store domain and license key are sent to verify subscription status and usage limits. 83 2. Optional Fit Data: If the shopper voluntarily enters body measurements, those numbers (together with the store's garment measurements for the selected size) are sent alongside the try-on request to render a realistic fit. Measurements are never required. 84 3. Admin Photoshoot: Only after the administrator confirms rights to the images, the selected garment and model photos are sent to the API. 85 4. Verification & Task Status: The store domain, license key, and generation task IDs are sent to verify subscription status, usage limits, and to retrieve finished images. 38 86 * Data Handling & Privacy: Images are processed in secure server memory to generate the result and are immediately discarded by the API upon completion. User data is confidential and never used to train AI models. 39 87 * **Terms & Privacy Links:** … … 53 101 == Frequently Asked Questions == 54 102 103 = How does the AI virtual try-on work for shoppers? = 104 A "Try It On" button appears on your product pages. The shopper uploads a photo of themselves (or reuses their saved Digital Mannequin), and the AI renders the garment on their own body in seconds — including realistic fit if measurements and size charts are set up. 105 106 = Can I really create product images without hiring models or a photographer? = 107 Yes. The Virtual Photoshoot dashboard turns a flat lay, hanger shot, ghost mannequin shot, or plain packshot into on-model, studio-quality catalog images. You choose the model, background, pose, and style. 108 109 = Does virtual try-on reduce return rates? = 110 Sizing and "it looked different online" are the top reasons clothing gets returned. Letting shoppers see garments on their own body — with optional true-to-size fit warnings — addresses both causes directly. See our [guide to reducing WooCommerce return rates](https://anydress.ai/reduce-clothing-return-rates-woocommerce/). 111 55 112 = Do I need an AnyDress AI account to use this plugin? = 56 Yes, you need an active API License Key from AnyDress AI to connect your WooCommerce store to the AI generation engine. 113 Yes, you need an active API License Key from AnyDress AI to connect your WooCommerce store to the AI generation engine. Plans are listed at https://anydress.ai/#pricing. 114 115 = Does it work for eyewear, accessories, or kids' clothes? = 116 Yes — size charts cover tops, bottoms, dresses, outerwear, swimwear, eyewear, and headwear, and stores use AnyDress for apparel, glasses, baby and kids' clothes, and jewelry. 57 117 58 118 = Where are shopper photos saved? = 59 119 Shopper photos are securely saved in a hidden, local folder on your own WordPress server (`wp-content/uploads/anydress-mannequins/`). To protect privacy and save disk space, a cron job automatically deletes shopper photos 7 days after upload. Shoppers can also manually delete their data at any time via the "Digital Mannequin" tab in their My Account page. 120 121 = Are shopper body measurements required? Where are they stored? = 122 No — measurements are entirely optional; without them the try-on works exactly as before. If provided, they are stored on your own WordPress server as user meta, editable and deletable by the shopper in My Account, and automatically removed by the same 7-day cleanup that deletes mannequin photos (and on plugin uninstall). 123 124 = How do I set up sizing for my products? = 125 Go to **AnyDress AI → Size Charts**, create a chart with your garment measurements per size, and assign it to products either from the chart itself or in the product editor under **Product data → AnyDress Sizing** (where you can also override individual values per product). 60 126 61 127 = Does this work with any theme? = … … 64 130 == Screenshots == 65 131 66 1. The Virtual Try-On button on a WooCommerce product page.67 2. The shopper upload modal .68 3. The shopper Virtual Try-On result.69 4. The Admin Virtual Photoshoot dashboard .132 1. The "Try It On" button on a WooCommerce product page — shoppers start a virtual try-on next to Add to Cart. 133 2. The shopper upload modal with consent and Digital Mannequin options. 134 3. A finished virtual try-on: the garment rendered on the shopper's own photo. 135 4. The Admin Virtual Photoshoot dashboard — AI product images without models. 70 136 5. AnyDress plugin settings and API dashboard. 71 137 72 138 == Changelog == 73 139 140 = 1.3.1 = 141 * Hardened input sanitization and declared compatibility with WordPress 7.0 (Plugin Check compliance). 142 143 = 1.3.0 = 144 * New settings: choose the Virtual Try-On aspect ratio (2:3, 3:4, 4:5) and resolution (1K or 2K) so every shopper result has a consistent, predictable format. 145 * Friendlier sizing feedback: when a selected size doesn't fit the shopper's measurements, the try-on now shows a helpful sizing-advice notice (instead of an error) and opens the size picker so they can switch and retry. 146 147 = 1.2.0 = 148 * Faster, more reliable generations: try-ons are now processed asynchronously — the page shows progress and delivers the image the moment it is ready, with no long-hanging requests. 149 * Improved reliability on slow hosts: generation no longer depends on a single long HTTP request. 150 151 = 1.1.1 = 152 * Size Charts: assign a chart to many products at once with a searchable product picker, and duplicate charts for product models with different measurements. 153 154 = 1.1.0 = 155 * New Size Charts system: reusable per-size garment measurements for tops, bottoms, dresses, outerwear, swimwear, eyewear, and headwear. 156 * Optional shopper body measurements (cm/inches) for a true-to-size virtual try-on, with a store setting to block or realistically render ill-fitting sizes. 157 * Per-product measurement overrides and automatic matching with WooCommerce size attributes. 158 * Fixed an issue where the face photo was uploaded twice, which could exceed server upload limits. 159 74 160 = 1.0.0 = 75 161 * Initial release. -
anydress-ai-virtual-try-on/trunk/uninstall.php
r3530062 r3612360 23 23 'anydress_checkbox_tick_color', 24 24 'anydress_privacy_page_id', // Legacy setting 25 'anydress_plugin_version' 25 'anydress_plugin_version', 26 'anydress_privacy_policy_url', 27 'anydress_fit_mode', 28 'anydress_size_charts' 26 29 ); 27 30 … … 42 45 'anydress_mannequin_upload_time', 43 46 'anydress_usage_date', 44 'anydress_usage_count' 47 'anydress_usage_count', 48 'anydress_shopper_measurements', 49 'anydress_shopper_unit' 45 50 ); 46 51 … … 50 55 delete_metadata( 'user', 0, $anydress_meta_key, '', true ); 51 56 } 57 58 // 2b. Delete product sizing meta for ALL products 59 delete_metadata( 'post', 0, '_anydress_size_chart_id', '', true ); 60 delete_metadata( 'post', 0, '_anydress_size_overrides', '', true ); 52 61 53 62 // 3. Delete the custom mannequins folder and all images inside it
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)