config = new openAI_c(); } /** * @inheritDoc */ public function requireModuleEnabled(): void { if (!$this->config->enabled->isTrue()) { throw new Exception('OpenAI module is not enabled.'); } } /** * Send a structured JSON text task to the OpenAI Responses API. * * @throws Exception */ public function jsonTask(string $schemaName, string $prompt, array $payload, array $schema, float $temperature = 0.1): array { $this->requireModuleEnabled(); $data = [ 'model' => $this->model, 'input' => [ [ 'role' => 'user', 'content' => [ [ 'type' => 'input_text', 'text' => $prompt . "\n\nData:\n" . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ], ], ], ], 'text' => [ 'format' => [ 'type' => 'json_schema', 'name' => $schemaName, 'schema' => $schema, 'strict' => true, ], ], 'temperature' => $temperature, ]; $response = $this->sendRequest($data); $output = $response['output'][0]['content'][0]['text'] ?? null; if (!is_string($output) || $output === '') { throw new Exception('Invalid response format from OpenAI API. (Missing text field)'); } $decoded = json_decode($output, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) { throw new Exception('Error parsing JSON response: ' . json_last_error_msg()); } return $decoded; } protected function getLPRSchema(): array { return [ /** * "format": { * "type": "json_schema", * "name": "math_reasoning", * "schema": { * "type": "object", * "properties": { * "steps": { * "type": "array", * "items": { * "type": "object", * "properties": { * "explanation": { "type": "string" }, * "output": { "type": "string" } * }, * "required": ["explanation", "output"], * "additionalProperties": false * } * }, * "final_answer": { "type": "string" } * }, * "required": ["steps", "final_answer"], * "additionalProperties": false * }, * "strict": true * } */ 'format' => [ 'type' => 'json_schema', 'name' => 'lpr', 'schema' => [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean', 'description' => 'Indicates whether the license plate number was successfully extracted.' ], 'license_plate_number' => [ 'type' => 'string', 'description' => 'The extracted license plate number from the image.' ], ], 'required' => ['license_plate_number', 'success'], 'additionalProperties' => false ], 'strict' => true ] ]; } /** * @inheritDoc * @return array The extracted license plate number (If there is a valid response) * @throws Exception If the API response cannot be parsed * @throws Exception If the image base64 is not valid. * @throws Exception If the module is not enabled or if there is an error in the API request */ public function lpr(string $base64_image): array { $this->requireModuleEnabled(); if (empty($base64_image)) { throw new Exception('Image URL cannot be empty.'); } $prompt = $this->prompt_LPR; $data = [ 'model' => $this->model, 'input' => [ [ 'role' => 'user', 'content' => [ ['type' => 'input_text', 'text' => $prompt], [ 'type' => 'input_image', 'image_url' => $this->generateTemporaryDirectDownloadLink($base64_image) // Generate a temporary direct download link for the base64 image ] ] ] ], 'text' => [ ...self::getLPRSchema(), // Apply the LPR schema { success, license_plate_number } ], 'temperature' => (float)$this->temperature, ]; // Send the request to the OpenAI API $response = $this->sendRequest($data); // Check if the response contains the expected format return $this->parseLPRResponse($response); } /** * Generates a temporary direct download link for the image. * * @param string $image The image object key name * @return string The temporary direct download link for the image. */ private function generateTemporaryDirectDownloadLink(string $image): string { $uploads = new upload_store(); // Store the image temporarily and return the direct download link return $uploads->generateDirectDownloadUrl($image); } /** * Parses the response from the OpenAI API for the LPR task. * * @param mixed $response The response object from the OpenAI API. * @return array The extracted license plate number if successful, otherwise an empty string. * @throws Exception If the response does not contain the expected format or if parsing fails. */ private function parseLPRResponse(mixed $response): array { /** * {"success":true,"data":{"id":"resp_68a2e2d5eb608191a546ef0540797d3b0148dcae28786ad6","object":"response","created_at":1755505366,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-mini-2025-04-14","output":[{"id":"msg_68a2e2d91050819183cc2c1d9be252950148dcae28786ad6","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"success\":true,\"license_plate_number\":\"CU 61 297\"}"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":0.7,"text":{"format":{"type":"json_schema","description":null,"name":"lpr","schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Indicates whether the license plate number was successfully extracted."},"license_plate_number":{"type":"string","description":"The extracted license plate number from the image."}},"required":["license_plate_number","success"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1,"truncation":"disabled","usage":{"input_tokens":2470,"input_tokens_details":{"cached_tokens":0},"output_tokens":16,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":2486},"user":null,"metadata":[]},"meta":[],"includes":[]} */ // Check if the response contains the expected format if (!isset($response['output'][0]['content'][0]['text'])) { throw new Exception('Invalid response format from OpenAI API. (Missing text field)'); } $output = $response['output'][0]['content'][0]['text']; // Decode the JSON response $decodedOutput = json_decode($output, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Error parsing JSON response: ' . json_last_error_msg()); } // Check if the response contains the expected fields if (!isset($decodedOutput['success']) || !isset($decodedOutput['license_plate_number'])) { throw new Exception('Invalid response format from OpenAI API. (Missing success or license_plate_number field)'); } // Return the license plate number if successful if ($decodedOutput['success']) { // Remove any spaces or unwanted characters from the license plate number return [ 'success' => $decodedOutput['success'], 'license_plate_number' => $this->cleanLicensePlate($decodedOutput['license_plate_number']), ]; // return $decodedOutput['license_plate_number']); } else { throw new Exception('License plate extraction failed.'); } } /** * Cleans the license plate number by removing unwanted characters and spaces. * * @param string $license_plate The license plate number to clean. * @return string The cleaned license plate number. */ private function cleanLicensePlate(string $license_plate): string { // Remove any unwanted characters (e.g., spaces, special characters) $cleaned = preg_replace('/[^A-Za-z0-9]/', '', $license_plate); // Return the cleaned license plate number return strtoupper(trim($cleaned)); // Convert to uppercase and trim any whitespace } /** * Sends a request to the OpenAI API and returns the response. * * @param array $data The data to send in the request. * @return array The response from the API, typically an associative array containing the response data. * @throws Exception If the API request fails or if the response cannot be parsed. */ private function sendRequest(array $data): array { $this->requireModuleEnabled(); /** * curl https://api.openai.com/v1/responses \ * -H "Content-Type: application/json" \ * -H "Authorization: Bearer $OPENAI_API_KEY" \ * -d '{ * "model": "gpt-4.1-mini", * "input": [ * { * "role": "user", * "content": [ * {"type": "input_text", "text": "what is in this image?"}, * { * "type": "input_image", * "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" * } * ] * } * ] * }' */ $curl = curl_init($this->api_url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $this->config->api_key->getVariableValue() ]); curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); $response = curl_exec($curl); if (curl_errno($curl)) { throw new Exception('cURL error: ' . curl_error($curl)); } curl_close($curl); $responseData = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Error parsing JSON response: ' . json_last_error_msg()); } //print_r($responseData); return $responseData; } }