| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- <?php
- namespace app\common\library\ps;
- use think\Db;
- use think\Exception;
- /**
- * 管理后台:双图色块识版入库。
- */
- class TemplateIngest
- {
- /**
- * @param array $input name,factory_id,cover,mark_cover,paper_size,page_w,page_h
- * @return array {template_id, cells, unlabeled}
- * @throws Exception
- */
- public static function ingest(array $input)
- {
- @set_time_limit(180);
- @ini_set('memory_limit', '512M');
- $name = trim((string)($input['name'] ?? ''));
- $factoryId = (int)($input['factory_id'] ?? 0);
- $cover = AiRecognizer::localFileUrl((string)($input['cover'] ?? ''));
- $mark = AiRecognizer::localFileUrl((string)($input['mark_cover'] ?? ''));
- if ($cover === '' || $cover === '/') {
- throw new Exception('请上传标准展示图');
- }
- if ($mark === '' || $mark === '/') {
- throw new Exception('请上传色块标注图');
- }
- $paper = PaperSize::resolve(
- (string)($input['paper_size'] ?? 'A4'),
- (float)($input['page_w'] ?? 0),
- (float)($input['page_h'] ?? 0)
- );
- $cleanPath = self::resolvePath($cover);
- $markPath = self::resolvePath($mark);
- $probe = LayoutProbe::inspect($cleanPath);
- $imgW = (int)$probe['width'];
- $imgH = (int)$probe['height'];
- if ($imgW < 20 || $imgH < 20) {
- throw new Exception('标准图无法读取尺寸');
- }
- if (!PaperSize::ratioMatches($imgW, $imgH, $paper['w'], $paper['h'], 0.05)) {
- throw new Exception('标准图像素比与纸张不符。当前图 '
- . $imgW . '×' . $imgH . ','
- . $paper['name'] . ' 应为约 '
- . $paper['px_w'] . '×' . $paper['px_h'] . ' px(300DPI)或同等比例');
- }
- $detected = MarkerDetector::detect($cleanPath, $markPath);
- $cells = $detected['cells'];
- if (!$cells) {
- throw new Exception('没有识别到色块。请确认色块图是在标准图副本上涂了明显颜色,且两张图像素一致');
- }
- $cells = self::labelCells($cells, $markPath);
- $elements = [];
- foreach ($cells as $idx => $cell) {
- $key = (string)($cell['key'] ?? '');
- $label = (string)($cell['label'] ?? $cell['text'] ?? '');
- $elements[] = PrintPlanner::buildElement([
- 'id' => $cell['id'] ?? ('c' . ($idx + 1)),
- 'type' => 'field',
- 'role' => 'fill',
- 'key' => $key,
- 'label' => $label,
- 'text' => '',
- 'x' => $cell['x'],
- 'y' => $cell['y'],
- 'w' => $cell['w'],
- 'h' => $cell['h'],
- 'border' => false,
- ], 1);
- }
- $fillCells = [];
- foreach ($elements as $el) {
- $fillCells[] = [
- 'id' => $el['id'],
- 'key' => $el['key'],
- 'label' => $el['label'],
- 'x' => $el['x'],
- 'y' => $el['y'],
- 'w' => $el['w'],
- 'h' => $el['h'],
- ];
- }
- if ($name === '') {
- $name = ($factoryId === 0 ? '官方模板 ' : '工厂模板 ') . date('Y-m-d H:i');
- }
- $dpi = PaperSize::dpiOf($imgW, $paper['w']);
- $schema = [
- 'name' => mb_substr($name, 0, 100),
- 'title' => mb_substr($name, 0, 100),
- 'mode' => 'overlay',
- 'paper_size' => $paper['name'],
- 'page' => [
- 'w' => $paper['w'],
- 'h' => $paper['h'],
- 'background' => $probe['background'] ?: '#ffffff',
- 'image_width' => $imgW,
- 'image_height' => $imgH,
- 'dpi' => $dpi,
- ],
- 'background' => $cover,
- 'file_url' => $cover,
- 'mark_image' => $mark,
- 'elements' => $elements,
- 'cells' => $fillCells,
- 'fields' => $fillCells,
- 'source' => 'marker',
- ];
- $now = time();
- $isSystem = $factoryId === 0 ? 1 : 0;
- $templateId = (int)Db::name('ps_template')->insertGetId([
- 'factory_id' => $factoryId,
- 'creator_user_id' => 0,
- 'name' => $schema['name'],
- 'cover' => $cover,
- 'paper_size' => $paper['name'],
- 'page_w' => $paper['w'],
- 'page_h' => $paper['h'],
- 'mark_cover' => $mark,
- 'source' => $isSystem ? 'system' : 'ai',
- 'license_type' => $isSystem ? 'self' : 'self',
- 'locked' => 0,
- 'source_template_id' => 0,
- 'source_version_id' => 0,
- 'current_version_id' => 0,
- 'is_system' => $isSystem,
- 'status' => 'normal',
- 'createtime' => $now,
- 'updatetime' => $now,
- ]);
- $versionId = (int)Db::name('ps_template_version')->insertGetId([
- 'template_id' => $templateId,
- 'factory_id' => $factoryId,
- 'version' => 1,
- 'schema_json' => json_encode($schema, JSON_UNESCAPED_UNICODE),
- 'remark' => '色块识版入库',
- 'createtime' => $now,
- ]);
- Db::name('ps_template')->where('id', $templateId)->update([
- 'current_version_id' => $versionId,
- 'updatetime' => $now,
- ]);
- $unlabeled = 0;
- foreach ($fillCells as $cell) {
- if (trim((string)$cell['key']) === '') {
- $unlabeled++;
- }
- }
- return [
- 'template_id' => $templateId,
- 'cells' => count($fillCells),
- 'unlabeled' => $unlabeled,
- 'dpi' => $dpi,
- 'image' => $imgW . '×' . $imgH,
- 'paper' => $paper['name'] . ' ' . $paper['w'] . '×' . $paper['h'] . 'mm',
- ];
- }
- /**
- * 用当前标准图和色块图重新识版,更新已有模板的格子。
- * @param int $templateId
- * @return array
- * @throws Exception
- */
- public static function rebuild($templateId)
- {
- $row = Db::name('ps_template')->where('id', (int)$templateId)->find();
- if (!$row) {
- throw new Exception('模板不存在');
- }
- $cover = AiRecognizer::localFileUrl((string)$row['cover']);
- $mark = AiRecognizer::localFileUrl((string)$row['mark_cover']);
- $cleanPath = self::resolvePath($cover);
- $markPath = self::resolvePath($mark);
- $detected = MarkerDetector::detect($cleanPath, $markPath);
- $cells = $detected['cells'];
- if (!$cells) {
- throw new Exception('没有识别到色块');
- }
- $cells = self::labelCells($cells, $markPath);
- $elements = [];
- foreach ($cells as $idx => $cell) {
- $elements[] = PrintPlanner::buildElement([
- 'id' => $cell['id'] ?? ('c' . ($idx + 1)),
- 'type' => 'field',
- 'role' => 'fill',
- 'key' => (string)($cell['key'] ?? ''),
- 'label' => (string)($cell['label'] ?? ''),
- 'text' => '',
- 'x' => $cell['x'],
- 'y' => $cell['y'],
- 'w' => $cell['w'],
- 'h' => $cell['h'],
- 'border' => false,
- ], 1);
- }
- $schema = PrintPlanner::decodeSchema(json_encode([
- 'name' => $row['name'],
- 'title' => $row['name'],
- 'mode' => 'overlay',
- 'paper_size' => $row['paper_size'],
- 'page' => [
- 'w' => (float)$row['page_w'],
- 'h' => (float)$row['page_h'],
- 'image_width' => $detected['width'],
- 'image_height' => $detected['height'],
- ],
- 'background' => $cover,
- 'file_url' => $cover,
- 'mark_image' => $mark,
- 'elements' => $elements,
- 'cells' => $elements,
- 'source' => 'marker',
- ], JSON_UNESCAPED_UNICODE), $cover);
- $versionId = (int)$row['current_version_id'];
- $now = time();
- $payload = json_encode($schema, JSON_UNESCAPED_UNICODE);
- if ($versionId > 0) {
- Db::name('ps_template_version')->where('id', $versionId)->update([
- 'schema_json' => $payload,
- ]);
- } else {
- $versionId = (int)Db::name('ps_template_version')->insertGetId([
- 'template_id' => (int)$templateId,
- 'factory_id' => (int)$row['factory_id'],
- 'version' => 1,
- 'schema_json' => $payload,
- 'remark' => '重新识版',
- 'createtime' => $now,
- ]);
- }
- Db::name('ps_template')->where('id', (int)$templateId)->update([
- 'current_version_id' => $versionId,
- 'updatetime' => $now,
- ]);
- $unlabeled = 0;
- foreach ($schema['cells'] as $cell) {
- if (trim((string)($cell['key'] ?? '')) === '') {
- $unlabeled++;
- }
- }
- return [
- 'template_id' => (int)$templateId,
- 'cells' => count($schema['cells']),
- 'unlabeled' => $unlabeled,
- ];
- }
- /**
- * 用色块上的文字对照字段目录。视觉模型只认字,不再猜格子位置。
- * @param array $cells
- * @param string $markPath
- * @return array
- */
- protected static function labelCells(array $cells, $markPath)
- {
- foreach ($cells as &$cell) {
- $cell['key'] = isset($cell['key']) ? (string)$cell['key'] : '';
- $cell['label'] = isset($cell['label']) ? (string)$cell['label'] : '';
- }
- unset($cell);
- $catalog = PrintPlanner::fieldCatalog();
- $lines = [];
- foreach ($catalog as $item) {
- $lines[] = $item['key'] . ' = ' . $item['label'];
- }
- $boxLines = [];
- foreach ($cells as $idx => $cell) {
- $boxLines[] = ($idx + 1) . ': x=' . $cell['x'] . ' y=' . $cell['y'] . ' w=' . $cell['w'] . ' h=' . $cell['h'];
- }
- $prompt = "这是一张流程单的色块标注图。每个彩色色块覆盖一个待填写格子,色块上写了字段名称。\n"
- . "色块位置已经用百分比给出(相对整张图 0-100),请只读取每个色块上的文字,不要改坐标。\n"
- . "可选字段:\n" . implode("\n", $lines) . "\n"
- . "色块列表:\n" . implode("\n", $boxLines) . "\n"
- . "只返回 JSON:{\"items\":[{\"i\":1,\"text\":\"产品名称\",\"key\":\"product.name\"}]}\n"
- . "text 用色块上看到的原文;key 必须是上面的字段名,看不清就留空字符串。";
- try {
- $client = LlmClient::forRecognize();
- if ($client->enabled() || $client->visionModel()) {
- $raw = $client->visionRaw($prompt, $markPath, $client->defaultModel('image'), true);
- $json = LlmClient::extractJson($raw);
- $items = [];
- if (isset($json['items']) && is_array($json['items'])) {
- $items = $json['items'];
- } elseif (isset($json[0])) {
- $items = $json;
- }
- $map = [];
- foreach ($items as $item) {
- if (!is_array($item)) {
- continue;
- }
- $i = (int)($item['i'] ?? $item['index'] ?? 0);
- if ($i < 1) {
- continue;
- }
- $text = trim((string)($item['text'] ?? $item['label'] ?? ''));
- $key = PrintPlanner::mapFieldKey((string)($item['key'] ?? ''));
- if ($key === '' && $text !== '') {
- $key = PrintPlanner::mapFieldKey($text);
- }
- $map[$i] = ['text' => $text, 'key' => $key];
- }
- foreach ($cells as $idx => &$cell) {
- $hit = $map[$idx + 1] ?? null;
- $cell['label'] = $hit ? $hit['text'] : '';
- $cell['key'] = $hit ? $hit['key'] : '';
- if ($cell['key'] === '' && $cell['label'] !== '') {
- $cell['key'] = PrintPlanner::mapFieldKey($cell['label']);
- }
- }
- unset($cell);
- }
- } catch (\Exception $e) {
- foreach ($cells as &$cell) {
- if (!isset($cell['key'])) {
- $cell['key'] = '';
- }
- if (!isset($cell['label'])) {
- $cell['label'] = '';
- }
- }
- unset($cell);
- }
- return $cells;
- }
- /**
- * @param string $fileUrl
- * @return string
- * @throws Exception
- */
- public static function resolvePath($fileUrl)
- {
- $fileUrl = AiRecognizer::localFileUrl($fileUrl);
- if ($fileUrl === '' || $fileUrl === '/') {
- throw new Exception('文件路径为空');
- }
- $relative = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, ltrim($fileUrl, '/\\'));
- $candidates = [
- ROOT_PATH . 'public' . DIRECTORY_SEPARATOR . $relative,
- ROOT_PATH . $relative,
- ];
- foreach ($candidates as $path) {
- if (is_file($path)) {
- return $path;
- }
- }
- throw new Exception('找不到已上传文件:' . $fileUrl);
- }
- }
|