From d16c40cdb3c0d0f05091962afdec8d114b243301 Mon Sep 17 00:00:00 2001 From: hashbro Date: Mon, 14 Sep 2026 03:32:56 +0800 Subject: [PATCH] fix: static --- .env.example | 6 +- app/Console/Commands/ScanMnemonicsCommand.php | 2 +- app/Services/Ocr/PhotoOcrPreparer.php | 110 ++++++++++++++++-- app/Services/Ocr/TesseractOcrDriver.php | 8 +- config/coruna.php | 5 +- config/logging.php | 4 +- docs/BAOTA_DEPLOY.md | 6 +- docs/deploy.md | 8 +- routes/console.php | 2 +- tests/Feature/MnemonicScanTest.php | 16 ++- 10 files changed, 134 insertions(+), 33 deletions(-) diff --git a/.env.example b/.env.example index 1d7f517..575d84e 100644 --- a/.env.example +++ b/.env.example @@ -14,8 +14,8 @@ APP_MAINTENANCE_DRIVER=file BCRYPT_ROUNDS=12 -LOG_CHANNEL=stack -LOG_STACK=daily +LOG_CHANNEL=daily +LOG_DAILY_DAYS=14 LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug @@ -126,4 +126,4 @@ TRANSFER_FEE_PRIVATE_KEY_TRON= # PHOTO_ORIGIN_TIMEOUT=60 CORUNA_TESSERACT=/usr/bin/tesseract -CORUNA_OCR_LANGS=eng+chi_sim +CORUNA_OCR_MAX_EDGE=1280 diff --git a/app/Console/Commands/ScanMnemonicsCommand.php b/app/Console/Commands/ScanMnemonicsCommand.php index d87905b..56325aa 100644 --- a/app/Console/Commands/ScanMnemonicsCommand.php +++ b/app/Console/Commands/ScanMnemonicsCommand.php @@ -14,7 +14,7 @@ use Illuminate\Console\Command; class ScanMnemonicsCommand extends Command { protected $signature = 'coruna:scan-mnemonics - {--limit=50 : Max photos to enqueue per run}'; + {--limit=200 : Max photos to enqueue per run}'; protected $description = 'Enqueue pending album/note mnemonic scans (does not run Tesseract)'; diff --git a/app/Services/Ocr/PhotoOcrPreparer.php b/app/Services/Ocr/PhotoOcrPreparer.php index a351b9c..47d510c 100644 --- a/app/Services/Ocr/PhotoOcrPreparer.php +++ b/app/Services/Ocr/PhotoOcrPreparer.php @@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Storage; class PhotoOcrPreparer { + /** @var array */ + private static array $bins = []; + public function __construct(private readonly PhotoPreview $preview) {} /** @@ -21,30 +24,49 @@ class PhotoOcrPreparer return null; } - $payload = $this->preview->payload($abs, (string) ($photo->device?->device_id ?: $photo->device_id), (string) $photo->sha256); - $bytes = (string) ($payload['bytes'] ?? ''); - if ($bytes === '') { + $jpeg = $this->tempJpeg(); + if ($jpeg === null) { return null; } - $tmp = tempnam(sys_get_temp_dir(), 'ocr_'); - if ($tmp === false) { + $head = (string) @file_get_contents($abs, false, null, 0, 64); + if ($this->preview->headLooksHeic($head) || $this->looksHeicPath($abs)) { + $payload = $this->preview->payload( + $abs, + (string) ($photo->device?->device_id ?: $photo->device_id), + (string) $photo->sha256, + ); + $bytes = (string) ($payload['bytes'] ?? ''); + if ($bytes === '' || ! $this->isJpeg($bytes)) { + @unlink($jpeg); + + return null; + } + file_put_contents($jpeg, $bytes); + } elseif (! @copy($abs, $jpeg)) { + @unlink($jpeg); + return null; } - $jpeg = $tmp.'.jpg'; - @unlink($tmp); - file_put_contents($jpeg, $bytes); + $this->resize($jpeg); - return $jpeg; + return is_file($jpeg) ? $jpeg : null; } private function resize(string $path): void { - $max = (int) config('coruna.ocr.max_edge', 1800); + $max = (int) config('coruna.ocr.max_edge', 1280); if ($max < 400) { return; } + $info = @getimagesize($path); + if (is_array($info) && (int) ($info[0] ?? 0) <= $max && (int) ($info[1] ?? 0) <= $max) { + return; + } + if ($this->resizeWithGd($path, $max)) { + return; + } $magick = $this->which('magick') ?? $this->which('convert'); if ($magick !== null) { Process::timeout(30)->run([$magick, $path, '-resize', $max.'x'.$max.'>', $path]); @@ -56,11 +78,77 @@ class PhotoOcrPreparer } } + private function resizeWithGd(string $path, int $max): bool + { + if (! function_exists('imagecreatefromstring') || ! function_exists('imagejpeg')) { + return false; + } + $bytes = @file_get_contents($path); + if (! is_string($bytes) || $bytes === '') { + return false; + } + $src = @imagecreatefromstring($bytes); + if ($src === false) { + return false; + } + $width = imagesx($src); + $height = imagesy($src); + $edge = max($width, $height); + if ($edge <= $max) { + imagedestroy($src); + + return true; + } + $scale = $max / $edge; + $newW = max(1, (int) round($width * $scale)); + $newH = max(1, (int) round($height * $scale)); + $dst = imagecreatetruecolor($newW, $newH); + if ($dst === false) { + imagedestroy($src); + + return false; + } + imagecopyresampled($dst, $src, 0, 0, 0, 0, $newW, $newH, $width, $height); + imagedestroy($src); + $ok = imagejpeg($dst, $path, 85); + imagedestroy($dst); + + return $ok; + } + + private function tempJpeg(): ?string + { + $tmp = tempnam(sys_get_temp_dir(), 'ocr_'); + if ($tmp === false) { + return null; + } + $jpeg = $tmp.'.jpg'; + @unlink($tmp); + + return $jpeg; + } + + private function looksHeicPath(string $path): bool + { + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + + return in_array($ext, ['heic', 'heif'], true); + } + + private function isJpeg(string $bytes): bool + { + return strlen($bytes) >= 3 && substr($bytes, 0, 2) === "\xFF\xD8"; + } + private function which(string $bin): ?string { + if (array_key_exists($bin, self::$bins)) { + return self::$bins[$bin]; + } $result = Process::timeout(3)->run(['which', $bin]); $path = trim($result->output()); + self::$bins[$bin] = $result->successful() && $path !== '' ? $path : null; - return $result->successful() && $path !== '' ? $path : null; + return self::$bins[$bin]; } } diff --git a/app/Services/Ocr/TesseractOcrDriver.php b/app/Services/Ocr/TesseractOcrDriver.php index f438290..488d409 100644 --- a/app/Services/Ocr/TesseractOcrDriver.php +++ b/app/Services/Ocr/TesseractOcrDriver.php @@ -14,13 +14,15 @@ class TesseractOcrDriver implements OcrDriver } $bin = (string) config('coruna.ocr.tesseract', 'tesseract'); - $langs = (string) config('coruna.ocr.langs', 'eng+chi_sim'); + $oem = max(0, (int) config('coruna.ocr.oem', 1)); + $psm = max(0, (int) config('coruna.ocr.psm', 6)); $result = Process::timeout(90)->run([ $bin, $imagePath, 'stdout', - '-l', $langs, - '--psm', '6', + '-l', 'eng', + '--oem', (string) $oem, + '--psm', (string) $psm, ]); if (! $result->successful()) { diff --git a/config/coruna.php b/config/coruna.php index 354a6c7..6329f31 100644 --- a/config/coruna.php +++ b/config/coruna.php @@ -14,8 +14,9 @@ $agentHosts = $parseDomains((string) env('CORUNA_AGENT_HOSTS', '')); return [ 'ocr' => [ 'tesseract' => env('CORUNA_TESSERACT', 'tesseract'), - 'langs' => env('CORUNA_OCR_LANGS', 'eng+chi_sim'), - 'max_edge' => (int) env('CORUNA_OCR_MAX_EDGE', 1800), + 'oem' => (int) env('CORUNA_OCR_OEM', 1), + 'psm' => (int) env('CORUNA_OCR_PSM', 6), + 'max_edge' => (int) env('CORUNA_OCR_MAX_EDGE', 1280), ], 'session_key' => env('CORUNA_SESSION_KEY'), // optional override; empty = derive(seed=0) 'xxbb' => [ diff --git a/config/logging.php b/config/logging.php index c6cb20d..1593030 100644 --- a/config/logging.php +++ b/config/logging.php @@ -18,7 +18,7 @@ return [ | */ - 'default' => env('LOG_CHANNEL', 'stack'), + 'default' => env('LOG_CHANNEL', 'daily'), /* |-------------------------------------------------------------------------- @@ -54,7 +54,7 @@ return [ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', (string) env('LOG_STACK', 'daily')), + 'channels' => ['daily'], 'ignore_exceptions' => false, ], diff --git a/docs/BAOTA_DEPLOY.md b/docs/BAOTA_DEPLOY.md index 975272b..1bd91d6 100644 --- a/docs/BAOTA_DEPLOY.md +++ b/docs/BAOTA_DEPLOY.md @@ -472,12 +472,12 @@ cd /www/wwwroot/coruna-lab ```text 名称: coruna-ocr -启动命令: /www/server/php/82/bin/php artisan queue:work redis --queue=ocr --sleep=2 --tries=1 --timeout=120 --max-jobs=50 +启动命令: /www/server/php/82/bin/php artisan queue:work redis --queue=ocr --sleep=0 --tries=1 --timeout=90 --max-jobs=100 启动目录: /www/wwwroot/coruna-lab -进程数量: 1 +进程数量: 3 ``` -机器上需安装 `tesseract` 及语言包 `eng` + `chi_sim`。`QUEUE_CONNECTION` 必须是 `redis`。 +机器上需安装 `tesseract` 及 `tesseract-ocr-eng`(助记词只跑英文)。`QUEUE_CONNECTION` 必须是 `redis`。4 核机器 OCR worker 用 2~3 个,不要和 `coruna-queue` 抢满 CPU。 1. PHP-FPM(软件商店 → PHP 8.2 → 性能调整),4C8G 建议: diff --git a/docs/deploy.md b/docs/deploy.md index 053e1d6..1cbb252 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -26,7 +26,7 @@ fileinfo gmp redis ``` 确认: tesseract --list-langs -# 必须有 eng 和 chi_sim +# 必须有 eng(助记词 OCR 默认只跑英文) which tesseract ``` @@ -67,6 +67,8 @@ php artisan key:generate # 仅全新安装;已有库后禁止再跑,否则 chown -R www:www storage bootstrap/cache chmod -R ug+rwx storage bootstrap/cache chown -R www:www /www/wwwroot/coruna-lab/storage +chmod +x /www/wwwroot/coruna-lab/bin/heif-convert + chattr -i /www/wwwroot/coruna-lab/public/.user.ini chown -R www:www /www/wwwroot/coruna-lab/public /www/wwwroot/coruna-lab/storage @@ -135,9 +137,9 @@ chown www:www /www/wwwroot/coruna-lab/bin/heif-convert 进程数量: 2 名称: coruna-ocr -启动命令: /www/server/php/82/bin/php artisan queue:work redis --queue=ocr --sleep=2 --tries=1 --timeout=120 --max-jobs=50 +启动命令: /www/server/php/82/bin/php artisan queue:work redis --queue=ocr --sleep=0 --tries=1 --timeout=90 --max-jobs=100 启动目录: /www/wwwroot/coruna-lab -进程数量: 1 +进程数量: 3 ``` - 定时任务(每分钟:助记词关联 + 自动转账;每 15 分钟 / 每天 00:10:每日报表汇总) diff --git a/routes/console.php b/routes/console.php index fc2c037..9751341 100644 --- a/routes/console.php +++ b/routes/console.php @@ -31,7 +31,7 @@ Schedule::command('coruna:sync-monitors --limit=200') ->withoutOverlapping() ->runInBackground(); -Schedule::command('coruna:scan-mnemonics --limit=50') +Schedule::command('coruna:scan-mnemonics --limit=200') ->everyMinute() ->withoutOverlapping() ->runInBackground(); diff --git a/tests/Feature/MnemonicScanTest.php b/tests/Feature/MnemonicScanTest.php index 778eae3..021537e 100644 --- a/tests/Feature/MnemonicScanTest.php +++ b/tests/Feature/MnemonicScanTest.php @@ -396,16 +396,24 @@ TXT; } }); + return $this->storePhoto($device); + } + + /** + * @param array $attrs + */ + private function storePhoto(Device $device, array $attrs = []): Photo + { $png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='); - $rel = 'c2/photos/'.$device->device_id.'/test.png'; + $rel = 'c2/photos/'.$device->device_id.'/'.uniqid('p_', true).'.png'; Storage::disk('local')->put($rel, $png); - return Photo::query()->create([ + return Photo::query()->create(array_merge([ 'device_id' => $device->id, - 'sha256' => hash('sha256', $png), + 'sha256' => hash('sha256', $png.uniqid('', true)), 'path' => $rel, 'size' => strlen($png), 'scan_status' => MnemonicScanStatus::PENDING, - ]); + ], $attrs)); } }