AliyunOCRDriver.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. <?php
  2. namespace App\Services\OCR\Drivers;
  3. use App\Services\OCR\OCRInterface;
  4. use AlibabaCloud\SDK\Ocrapi\V20210707\Ocrapi;
  5. use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
  6. use Darabonba\OpenApi\Models\Config;
  7. use AlibabaCloud\SDK\Ocrapi\V20210707\Models\RecognizeEduPaperOcrRequest;
  8. use AlibabaCloud\SDK\Ocrapi\V20210707\Models\RecognizeEduPaperCutRequest;
  9. use Illuminate\Support\Facades\Log;
  10. class AliyunOCRDriver implements OCRInterface
  11. {
  12. protected $client;
  13. public function __construct(array $config, $client = null)
  14. {
  15. if ($client) {
  16. $this->client = $client;
  17. return;
  18. }
  19. $apiConfig = new Config([
  20. 'accessKeyId' => $config['access_key_id'],
  21. 'accessKeySecret' => $config['access_key_secret'],
  22. 'endpoint' => $config['endpoint'],
  23. ]);
  24. $this->client = new Ocrapi($apiConfig);
  25. }
  26. public function recognize(string $imagePath, array $options = []): array
  27. {
  28. try {
  29. // Check if file exists
  30. if (!file_exists($imagePath)) {
  31. throw new \Exception("Image file not found: {$imagePath}");
  32. }
  33. // Get parameters from options
  34. $cutType = $options['cutType'] ?? 'question';
  35. $subject = $options['subject'] ?? 'Math';
  36. $ocrRecordId = $options['ocr_record_id'] ?? null;
  37. // Read file content
  38. $fileStream = fopen($imagePath, 'rb');
  39. $stream = \GuzzleHttp\Psr7\Utils::streamFor($fileStream);
  40. $request = new \AlibabaCloud\SDK\Ocrapi\V20210707\Models\RecognizeEduPaperCutRequest([
  41. 'body' => $stream,
  42. 'cutType' => $cutType,
  43. 'imageType' => 'photo',
  44. 'subject' => $subject,
  45. 'outputOricoord' => false
  46. ]);
  47. $runtime = new RuntimeOptions([]);
  48. // Call Aliyun API
  49. $response = $this->client->recognizeEduPaperCutWithOptions($request, $runtime);
  50. // Close stream
  51. if (is_resource($fileStream)) {
  52. fclose($fileStream);
  53. }
  54. // Parse response
  55. $body = json_decode(json_encode($response->body), true);
  56. // Detailed logging
  57. Log::info('Aliyun EduPaperCut Full Response', [
  58. 'cutType' => $cutType,
  59. 'has_data' => isset($body['data']),
  60. 'request_id' => $body['requestId'] ?? null,
  61. 'code' => $body['code'] ?? null,
  62. 'message' => $body['message'] ?? null,
  63. 'body_keys' => array_keys($body ?? [])
  64. ]);
  65. // Log raw data if exists
  66. if (isset($body['data'])) {
  67. $dataPreview = is_string($body['data'])
  68. ? substr($body['data'], 0, 500)
  69. : json_encode($body['data']);
  70. Log::info('Aliyun Data Preview', ['data' => $dataPreview]);
  71. }
  72. // Extract data from Aliyun response
  73. $questions = [];
  74. if (isset($body['data'])) {
  75. // The data field is a JSON string
  76. $data = is_string($body['data']) ? json_decode($body['data'], true) : $body['data'];
  77. // Extract page_list -> subject_list OR answer_list
  78. if (isset($data['page_list']) && is_array($data['page_list'])) {
  79. foreach ($data['page_list'] as $page) {
  80. // Determine which list to use based on cutType
  81. $itemList = null;
  82. if ($cutType === 'answer' && isset($page['answer_list'])) {
  83. $itemList = $page['answer_list'];
  84. } elseif (isset($page['subject_list'])) {
  85. $itemList = $page['subject_list'];
  86. }
  87. if ($itemList && is_array($itemList)) {
  88. foreach ($itemList as $item) {
  89. // Extract question/answer data
  90. $questionNumber = count($questions) + 1; // 默认使用索引
  91. if (isset($item['ids']) && is_array($item['ids']) && !empty($item['ids'])) {
  92. $idValue = $item['ids'][0];
  93. // 只有当 ids[0] 是数字时才使用它作为题号
  94. if (is_numeric($idValue)) {
  95. $questionNumber = (int) $idValue;
  96. }
  97. }
  98. // Get text - if not provided, build from prism_wordsInfo
  99. $text = $item['text'] ?? '';
  100. if (empty($text) && isset($item['prism_wordsInfo']) && is_array($item['prism_wordsInfo'])) {
  101. $words = [];
  102. foreach ($item['prism_wordsInfo'] as $wordInfo) {
  103. if (isset($wordInfo['word'])) {
  104. $words[] = $wordInfo['word'];
  105. }
  106. }
  107. $text = implode('', $words);
  108. }
  109. // Calculate confidence from prism_wordsInfo
  110. $confidence = 0.0;
  111. if (isset($item['prism_wordsInfo']) && is_array($item['prism_wordsInfo'])) {
  112. $totalProb = 0;
  113. $count = 0;
  114. foreach ($item['prism_wordsInfo'] as $wordInfo) {
  115. if (isset($wordInfo['prob'])) {
  116. $totalProb += $wordInfo['prob'];
  117. $count++;
  118. }
  119. }
  120. $confidence = $count > 0 ? ($totalProb / $count) / 100 : 0.0;
  121. }
  122. $questions[] = [
  123. 'question_number' => $questionNumber,
  124. 'content' => $text,
  125. 'cut_type' => $cutType,
  126. 'confidence' => $confidence,
  127. 'raw_data' => $item
  128. ];
  129. }
  130. }
  131. }
  132. }
  133. }
  134. // 保存完整的API响应到ocr_raw_data表
  135. if (isset($body['requestId']) && !empty($questions)) {
  136. try {
  137. \App\Models\OCRRawData::saveRawResponse($ocrRecordId ?? 0, $body);
  138. \Log::info('Aliyun OCR: 原始数据已保存', [
  139. 'request_id' => $body['requestId'],
  140. 'questions_count' => count($questions),
  141. 'ocr_record_id' => $ocrRecordId ?? null
  142. ]);
  143. } catch (\Exception $e) {
  144. \Log::error('Aliyun OCR: 保存原始数据失败', [
  145. 'error' => $e->getMessage(),
  146. 'request_id' => $body['requestId'] ?? null
  147. ]);
  148. }
  149. }
  150. return [
  151. 'raw' => $body,
  152. 'questions' => $questions,
  153. 'cut_type' => $cutType
  154. ];
  155. } catch (\Exception $e) {
  156. Log::error('Aliyun OCR Error', [
  157. 'message' => $e->getMessage(),
  158. 'trace' => $e->getTraceAsString(),
  159. ]);
  160. throw $e;
  161. }
  162. }
  163. /**
  164. * 识别手写内容(使用RecognizeEduPaperOcr接口)
  165. *
  166. * @param string $imagePath 图片路径
  167. * @param array $options 选项参数
  168. * @return array 识别结果
  169. */
  170. public function recognizeHandwriting(string $imagePath, array $options = []): array
  171. {
  172. try {
  173. // Check if file exists
  174. if (!file_exists($imagePath)) {
  175. throw new \Exception("Image file not found: {$imagePath}");
  176. }
  177. // Get parameters from options
  178. $subject = $options['subject'] ?? 'Math';
  179. $ocrRecordId = $options['ocr_record_id'] ?? null;
  180. // Read file content
  181. $fileStream = fopen($imagePath, 'rb');
  182. $stream = \GuzzleHttp\Psr7\Utils::streamFor($fileStream);
  183. $request = new RecognizeEduPaperOcrRequest([
  184. 'body' => $stream,
  185. 'imageType' => 'photo',
  186. 'subject' => $subject,
  187. 'textType' => '2', // 2 = 手写体
  188. 'outputOricoord' => true // 输出坐标信息
  189. ]);
  190. $runtime = new RuntimeOptions([]);
  191. // Call Aliyun API
  192. $response = $this->client->recognizeEduPaperOcrWithOptions($request, $runtime);
  193. // Close stream
  194. if (is_resource($fileStream)) {
  195. fclose($fileStream);
  196. }
  197. // Parse response
  198. $body = json_decode(json_encode($response->body), true);
  199. // Detailed logging
  200. Log::info('Aliyun EduPaperOcr (Handwriting) Response', [
  201. 'has_data' => isset($body['data']),
  202. 'request_id' => $body['requestId'] ?? null,
  203. 'code' => $body['code'] ?? null,
  204. 'message' => $body['message'] ?? null,
  205. 'body_keys' => array_keys($body ?? [])
  206. ]);
  207. // Extract recognized text
  208. $recognizedTexts = [];
  209. if (isset($body['data'])) {
  210. $data = is_string($body['data']) ? json_decode($body['data'], true) : $body['data'];
  211. // Extract content from data structure
  212. if (isset($data['content']) && is_string($data['content'])) {
  213. // Simple content string
  214. $recognizedTexts[] = [
  215. 'text' => $data['content'],
  216. 'confidence' => 1.0
  217. ];
  218. } elseif (isset($data['prism_wordsInfo']) && is_array($data['prism_wordsInfo'])) {
  219. // Detailed word-level information
  220. $allWords = [];
  221. $totalProb = 0;
  222. $count = 0;
  223. foreach ($data['prism_wordsInfo'] as $wordInfo) {
  224. if (isset($wordInfo['word'])) {
  225. $allWords[] = $wordInfo['word'];
  226. if (isset($wordInfo['prob'])) {
  227. $totalProb += $wordInfo['prob'];
  228. $count++;
  229. }
  230. }
  231. }
  232. if (!empty($allWords)) {
  233. $recognizedTexts[] = [
  234. 'text' => implode('', $allWords),
  235. 'confidence' => $count > 0 ? ($totalProb / $count) / 100 : 0.0
  236. ];
  237. }
  238. } elseif (isset($data['page_list']) && is_array($data['page_list'])) {
  239. // Page-based structure (similar to EduPaperCut)
  240. foreach ($data['page_list'] as $page) {
  241. if (isset($page['prism_wordsInfo']) && is_array($page['prism_wordsInfo'])) {
  242. $words = [];
  243. foreach ($page['prism_wordsInfo'] as $wordInfo) {
  244. if (isset($wordInfo['word'])) {
  245. $words[] = $wordInfo['word'];
  246. }
  247. }
  248. if (!empty($words)) {
  249. $recognizedTexts[] = [
  250. 'text' => implode('', $words),
  251. 'confidence' => 1.0
  252. ];
  253. }
  254. }
  255. }
  256. }
  257. }
  258. Log::info('Handwriting recognition result', [
  259. 'texts_count' => count($recognizedTexts),
  260. 'preview' => !empty($recognizedTexts) ? mb_substr($recognizedTexts[0]['text'], 0, 100) : 'N/A'
  261. ]);
  262. return [
  263. 'raw' => $body,
  264. 'texts' => $recognizedTexts,
  265. 'type' => 'handwriting'
  266. ];
  267. } catch (\Exception $e) {
  268. Log::error('Aliyun Handwriting OCR Error', [
  269. 'message' => $e->getMessage(),
  270. 'trace' => $e->getTraceAsString(),
  271. ]);
  272. throw $e;
  273. }
  274. }
  275. }