'bg_remove']; // Manual-run bounded poll, admin-ajax runs on the WP server (not behind the // API's 30s ceiling), but we still cap it so a click never hangs a worker. const POLL_CAP_SECONDS = 18; const POLL_INTERVAL = 2; public static function backend_feature($feature) { return isset(self::BACKEND_FEATURE[$feature]) ? self::BACKEND_FEATURE[$feature] : $feature; } // Build the POST /process body for a feature + attachment. public static function build_payload($feature, $attachment_id, $extra, $tier) { $bf = self::backend_feature($feature); $opts = get_option('acpimg_settings', []); // site + image_id + email travel with every request so the backend can // key quota by site and record the per-site fixes log. $payload = [ 'feature' => $bf, 'tier' => $tier === 'premium' ? 'premium' : 'default', 'site' => home_url(), 'image_id' => (int) $attachment_id, 'email' => isset($opts['xpay_email']) ? (string) $opts['xpay_email'] : '', ]; if ($bf === 'scene_gen') { $payload['prompt'] = isset($extra['prompt']) ? (string) $extra['prompt'] : ''; } else { $url = wp_get_attachment_url($attachment_id); if (!$url) return new WP_Error('acpimg_no_attachment', 'attachment not found'); $payload['image_url'] = $url; } if ($bf === 'enhance') { $payload['scale'] = isset($extra['scale']) ? (int) $extra['scale'] : 2; } return $payload; } // Start a prediction. Returns prediction_id (string) or WP_Error. public static function start($feature, $attachment_id, $extra, $tier) { $payload = self::build_payload($feature, $attachment_id, $extra, $tier); if (is_wp_error($payload)) return $payload; $res = Acpimg_Provider_Xpay_Managed::post('/product-images/process', $payload); if (is_wp_error($res)) return $res; if (empty($res['prediction_id'])) return new WP_Error('acpimg_no_prediction', 'no prediction id returned'); return (string) $res['prediction_id']; } // Poll a prediction. Returns ['status'=>..., 'output'=>...] or WP_Error. public static function poll($prediction_id) { return Acpimg_Provider_Xpay_Managed::status($prediction_id); } // Route a succeeded prediction's output to the feature's write-back. public static function finish($feature, $attachment_id, $output, $tier, $extra = []) { return apply_filters( 'acpimg_finish_job', new WP_Error('acpimg_no_finisher', 'no finisher for ' . $feature), $feature, $attachment_id, $output, $tier, $extra ); } // Manual single-action path: start + bounded poll. Finishes inline if the // prediction lands within POLL_CAP_SECONDS; otherwise defers to the cron // queue and returns a processing marker. public static function run($feature, $attachment_id, $extra = [], $tier = 'default') { if (Acpimg_Plugin::auth_mode() !== 'managed') { return new WP_Error('acpimg_no_auth', 'Sign in to xpay to use AI features.'); } $pid = self::start($feature, $attachment_id, $extra, $tier); if (is_wp_error($pid)) return $pid; $deadline = time() + self::POLL_CAP_SECONDS; while (time() < $deadline) { sleep(self::POLL_INTERVAL); $st = self::poll($pid); if (is_wp_error($st)) continue; $status = isset($st['status']) ? $st['status'] : ''; if ($status === 'succeeded') { return self::finish($feature, $attachment_id, isset($st['output']) ? $st['output'] : null, $tier, $extra); } if ($status === 'failed') { return new WP_Error('acpimg_failed', isset($st['error']) ? $st['error'] : 'prediction failed'); } } // Still running, hand to the local queue so it completes on a later tick. Acpimg_Job_Queue::enqueue_awaiting($attachment_id, $feature, $pid, array_merge(['tier' => $tier], $extra)); return ['status' => 'processing', 'prediction_id' => $pid]; } }