BaiduOCRDriver.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. namespace App\Services\OCR\Drivers;
  3. use App\Services\OCR\OCRInterface;
  4. use Illuminate\Support\Facades\Http;
  5. use Illuminate\Support\Facades\Log;
  6. class BaiduOCRDriver implements OCRInterface
  7. {
  8. protected $appId;
  9. protected $apiKey;
  10. protected $secretKey;
  11. protected $aesKey;
  12. public function __construct(array $config, $client = null)
  13. {
  14. if ($client) {
  15. // 使用模拟客户端(用于测试)
  16. $this->appId = $config['app_id'] ?? '';
  17. $this->apiKey = $config['api_key'] ?? '';
  18. $this->secretKey = $config['secret_key'] ?? '';
  19. $this->aesKey = $config['aes_key'] ?? '';
  20. return;
  21. }
  22. $this->appId = $config['app_id'] ?? '';
  23. $this->apiKey = $config['api_key'] ?? '';
  24. $this->secretKey = $config['secret_key'] ?? '';
  25. $this->aesKey = $config['aes_key'] ?? '';
  26. }
  27. public function recognize(string $imagePath, array $options = []): array
  28. {
  29. try {
  30. // Check if file exists
  31. if (!file_exists($imagePath)) {
  32. throw new \Exception("Image file not found: {$imagePath}");
  33. }
  34. // Get cutType from options
  35. $cutType = $options['cutType'] ?? 'question';
  36. $subject = $options['subject'] ?? 'Math';
  37. // Get access token
  38. $accessToken = $this->getAccessToken();
  39. // Read image file
  40. $imageData = base64_encode(file_get_contents($imagePath));
  41. // Call Baidu OCR API
  42. $response = Http::post("https://aip.baidubce.com/rest/2.0/ocr/v1/edu_paper?access_token={$accessToken}", [
  43. 'image' => $imageData,
  44. 'cut_type' => $cutType,
  45. 'subject' => $subject,
  46. ]);
  47. if ($response->failed()) {
  48. throw new \Exception('Baidu OCR API failed: ' . $response->body());
  49. }
  50. $body = $response->json();
  51. // Log the response
  52. Log::info('Baidu OCR Full Response', [
  53. 'cutType' => $cutType,
  54. 'has_data' => isset($body['data']),
  55. 'request_id' => $body['request_id'] ?? null,
  56. 'error_code' => $body['error_code'] ?? null,
  57. 'error_msg' => $body['error_msg'] ?? null,
  58. 'body_keys' => array_keys($body ?? [])
  59. ]);
  60. // Log raw data if exists
  61. if (isset($body['data'])) {
  62. $dataPreview = is_string($body['data'])
  63. ? substr($body['data'], 0, 500)
  64. : json_encode($body['data']);
  65. Log::info('Baidu OCR Data Preview', ['data' => $dataPreview]);
  66. }
  67. // Parse Baidu OCR response
  68. $questions = [];
  69. if (isset($body['data'])) {
  70. $data = is_string($body['data']) ? json_decode($body['data'], true) : $body['data'];
  71. // Extract page_list -> subject_list OR answer_list
  72. if (isset($data['page_list']) && is_array($data['page_list'])) {
  73. foreach ($data['page_list'] as $page) {
  74. // Determine which list to use based on cutType
  75. $itemList = null;
  76. if ($cutType === 'answer' && isset($page['answer_list'])) {
  77. $itemList = $page['answer_list'];
  78. } elseif (isset($page['subject_list'])) {
  79. $itemList = $page['subject_list'];
  80. }
  81. if ($itemList && is_array($itemList)) {
  82. foreach ($itemList as $index => $item) {
  83. // Extract question/answer data
  84. $questionNumber = count($questions) + 1; // 默认使用索引
  85. // 百度OCR的题号可能在不同的字段中
  86. // 尝试从多个可能的字段获取题号
  87. $idValue = null;
  88. if (isset($item['question_id'])) {
  89. $idValue = $item['question_id'];
  90. } elseif (isset($item['id'])) {
  91. $idValue = $item['id'];
  92. } elseif (isset($item['index'])) {
  93. $idValue = $item['index'];
  94. }
  95. // 只有当 idValue 是数字时才使用它作为题号
  96. if ($idValue !== null && is_numeric($idValue)) {
  97. $questionNumber = (int) $idValue;
  98. }
  99. // Get text
  100. $text = $item['text'] ?? '';
  101. if (empty($text) && isset($item['words'])) {
  102. // 百度OCR使用words字段
  103. if (is_array($item['words'])) {
  104. $words = array_column($item['words'], 'word');
  105. $text = implode('', $words);
  106. } else {
  107. $text = (string) $item['words'];
  108. }
  109. }
  110. // Calculate confidence
  111. $confidence = 0.0;
  112. if (isset($item['confidence'])) {
  113. $confidence = (float) $item['confidence'];
  114. } elseif (isset($item['words']) && is_array($item['words'])) {
  115. // 计算words的平均置信度
  116. $totalProb = 0;
  117. $count = 0;
  118. foreach ($item['words'] as $word) {
  119. if (isset($word['confidence'])) {
  120. $totalProb += $word['confidence'];
  121. $count++;
  122. }
  123. }
  124. $confidence = $count > 0 ? ($totalProb / $count) / 100 : 0.0;
  125. }
  126. $questions[] = [
  127. 'question_number' => $questionNumber,
  128. 'content' => $text,
  129. 'cut_type' => $cutType,
  130. 'confidence' => $confidence,
  131. 'raw_data' => $item
  132. ];
  133. }
  134. }
  135. }
  136. }
  137. }
  138. return [
  139. 'raw' => $body,
  140. 'questions' => $questions,
  141. 'cut_type' => $cutType
  142. ];
  143. } catch (\Exception $e) {
  144. Log::error('Baidu OCR Error', [
  145. 'message' => $e->getMessage(),
  146. 'trace' => $e->getTraceAsString(),
  147. ]);
  148. throw $e;
  149. }
  150. }
  151. /**
  152. * 获取百度OCR的访问令牌
  153. */
  154. protected function getAccessToken(): string
  155. {
  156. // 缓存访问令牌以避免频繁请求
  157. $cacheKey = 'baidu_ocr_access_token';
  158. $cachedToken = cache($cacheKey);
  159. if ($cachedToken) {
  160. return $cachedToken;
  161. }
  162. // 获取新的访问令牌
  163. $response = Http::post('https://aip.baidubce.com/oauth/2.0/token', [
  164. 'grant_type' => 'client_credentials',
  165. 'client_id' => $this->apiKey,
  166. 'client_secret' => $this->secretKey,
  167. ]);
  168. if ($response->failed()) {
  169. throw new \Exception('Failed to get Baidu OCR access token: ' . $response->body());
  170. }
  171. $data = $response->json();
  172. if (!isset($data['access_token'])) {
  173. throw new \Exception('Invalid response from Baidu OCR token API');
  174. }
  175. $accessToken = $data['access_token'];
  176. // 缓存访问令牌(默认25分钟过期,提前5分钟刷新)
  177. $expiresIn = $data['expires_in'] ?? 3600;
  178. cache([$cacheKey => $accessToken], now()->addMinutes($expiresIn / 60 - 5));
  179. return $accessToken;
  180. }
  181. }