gfdggadg commited on
Commit
f578e50
·
verified ·
1 Parent(s): f93ac5a

Switch OCR to Cyrillic PaddleOCR

Browse files
CV-Lenta-product-main/pyproject.toml CHANGED
@@ -11,6 +11,8 @@ requires-python = ">=3.10"
11
  dependencies = [
12
  "numpy>=1.24",
13
  "opencv-python-headless>=4.10",
 
 
14
  "pillow>=10.0",
15
  "torch>=2.8",
16
  "torchvision>=0.23",
 
11
  dependencies = [
12
  "numpy>=1.24",
13
  "opencv-python-headless>=4.10",
14
+ "paddleocr>=3.5.0",
15
+ "paddlepaddle>=3.3.0",
16
  "pillow>=10.0",
17
  "torch>=2.8",
18
  "torchvision>=0.23",
CV-Lenta-product-main/requirements.txt CHANGED
@@ -1,5 +1,7 @@
1
  numpy>=1.24
2
  opencv-python-headless>=4.10
 
 
3
  pillow>=10.0
4
  torch>=2.8
5
  torchvision>=0.23
 
1
  numpy>=1.24
2
  opencv-python-headless>=4.10
3
+ paddleocr>=3.5.0
4
+ paddlepaddle>=3.3.0
5
  pillow>=10.0
6
  torch>=2.8
7
  torchvision>=0.23
CV-Lenta-product-main/src/lenta_price_tags/config.py CHANGED
@@ -62,9 +62,10 @@ class ImageEnhancementConfig:
62
 
63
  @dataclass(frozen=True, slots=True)
64
  class OCRConfig:
65
- provider: str = "docscope"
66
- model_name: str = "prithivMLmods/docscopeOCR-7B-050425-exp"
67
- device: str | None = None
 
68
  batch_size: int = 16
69
  temperature: float = 0.0
70
  max_retries: int = 1
@@ -76,6 +77,13 @@ class OCRConfig:
76
  precache_model: bool = True
77
  skip_when_code_has_price_and_identity: bool = True
78
  fallback_to_mock_on_error: bool = True
 
 
 
 
 
 
 
79
 
80
 
81
  @dataclass(frozen=True, slots=True)
 
62
 
63
  @dataclass(frozen=True, slots=True)
64
  class OCRConfig:
65
+ provider: str = "paddle"
66
+ model_name: str = "cyrillic_PP-OCRv5_mobile_rec"
67
+ docscope_model_name: str = "prithivMLmods/docscopeOCR-7B-050425-exp"
68
+ device: str | None = "cpu"
69
  batch_size: int = 16
70
  temperature: float = 0.0
71
  max_retries: int = 1
 
77
  precache_model: bool = True
78
  skip_when_code_has_price_and_identity: bool = True
79
  fallback_to_mock_on_error: bool = True
80
+ paddle_text_detection_model_name: str = "PP-OCRv5_mobile_det"
81
+ paddle_text_det_limit_side_len: int = 960
82
+ paddle_text_det_thresh: float = 0.30
83
+ paddle_text_det_box_thresh: float = 0.50
84
+ paddle_text_det_unclip_ratio: float = 1.8
85
+ paddle_text_rec_score_thresh: float = 0.0
86
+ paddle_cache_dir: str | None = "runs/paddlex_cache"
87
 
88
 
89
  @dataclass(frozen=True, slots=True)
CV-Lenta-product-main/src/lenta_price_tags/ocr/docscope_provider.py CHANGED
@@ -27,6 +27,10 @@ except ImportError:
27
  class DocscopeOCRProvider:
28
  def __init__(self, config: OCRConfig | None = None) -> None:
29
  self.config = config or OCRConfig()
 
 
 
 
30
 
31
  def recognize(self, image_path: str, prompt: str, metadata: dict | None = None) -> OCREvidence:
32
  return self.recognize_many([(image_path, prompt, metadata)])[0]
@@ -35,7 +39,7 @@ class DocscopeOCRProvider:
35
  if not requests:
36
  return []
37
  if self.config.precache_model:
38
- _ensure_model_snapshot_cached(self.config.model_name)
39
  output: list[OCREvidence] = []
40
  batch_size = max(1, int(self.config.batch_size))
41
  for batch in _chunks(requests, batch_size):
@@ -58,7 +62,7 @@ class DocscopeOCRProvider:
58
  metadatas = [metadata or {} for _, _, metadata in requests]
59
  started = time.perf_counter()
60
  raw_texts, error = _run_docscope_batch_inference(
61
- self.config.model_name,
62
  [image_path for image_path, _, _ in requests],
63
  [prompt for _, prompt, _ in requests],
64
  self.config.device if hasattr(self.config, "device") else None,
@@ -84,7 +88,7 @@ class DocscopeOCRProvider:
84
  raw_text_lines=lines,
85
  parsed_fields=parsed,
86
  field_confidence={key: 0.70 for key, value in parsed.items() if key in OCR_FIELD_NAMES and value},
87
- model_name=self.config.model_name,
88
  latency_ms=per_item_latency,
89
  error=final_error,
90
  )
@@ -93,7 +97,7 @@ class DocscopeOCRProvider:
93
 
94
 
95
  def preload_docscope_model(model_name: str | None = None, device: str = "cuda") -> None:
96
- _load_runner(model_name or OCRConfig().model_name, device)
97
 
98
 
99
  def _run_docscope_batch_inference(
 
27
  class DocscopeOCRProvider:
28
  def __init__(self, config: OCRConfig | None = None) -> None:
29
  self.config = config or OCRConfig()
30
+ if self.config.model_name == "cyrillic_PP-OCRv5_mobile_rec":
31
+ self.model_name = self.config.docscope_model_name
32
+ else:
33
+ self.model_name = self.config.model_name
34
 
35
  def recognize(self, image_path: str, prompt: str, metadata: dict | None = None) -> OCREvidence:
36
  return self.recognize_many([(image_path, prompt, metadata)])[0]
 
39
  if not requests:
40
  return []
41
  if self.config.precache_model:
42
+ _ensure_model_snapshot_cached(self.model_name)
43
  output: list[OCREvidence] = []
44
  batch_size = max(1, int(self.config.batch_size))
45
  for batch in _chunks(requests, batch_size):
 
62
  metadatas = [metadata or {} for _, _, metadata in requests]
63
  started = time.perf_counter()
64
  raw_texts, error = _run_docscope_batch_inference(
65
+ self.model_name,
66
  [image_path for image_path, _, _ in requests],
67
  [prompt for _, prompt, _ in requests],
68
  self.config.device if hasattr(self.config, "device") else None,
 
88
  raw_text_lines=lines,
89
  parsed_fields=parsed,
90
  field_confidence={key: 0.70 for key, value in parsed.items() if key in OCR_FIELD_NAMES and value},
91
+ model_name=self.model_name,
92
  latency_ms=per_item_latency,
93
  error=final_error,
94
  )
 
97
 
98
 
99
  def preload_docscope_model(model_name: str | None = None, device: str = "cuda") -> None:
100
+ _load_runner(model_name or OCRConfig().docscope_model_name, device)
101
 
102
 
103
  def _run_docscope_batch_inference(
CV-Lenta-product-main/src/lenta_price_tags/ocr/paddle_provider.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import time
7
+ from decimal import Decimal
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from lenta_price_tags.config import OCRConfig
13
+ from lenta_price_tags.matching.normalizers import normalize_barcode, normalize_discount, normalize_price, normalize_sku
14
+ from lenta_price_tags.ocr.parser import OCR_FIELD_NAMES
15
+ from lenta_price_tags.schemas import OCREvidence
16
+
17
+
18
+ _PRICE_KEYS = ("price_default", "price_card", "price_discount")
19
+
20
+
21
+ class PaddleOCRProvider:
22
+ def __init__(self, config: OCRConfig | None = None) -> None:
23
+ self.config = config or OCRConfig()
24
+ self.model_name = self.config.model_name or "cyrillic_PP-OCRv5_mobile_rec"
25
+
26
+ def recognize(self, image_path: str, prompt: str, metadata: dict | None = None) -> OCREvidence:
27
+ return self.recognize_many([(image_path, prompt, metadata)])[0]
28
+
29
+ def recognize_many(self, requests: list[tuple[str, str, dict | None]]) -> list[OCREvidence]:
30
+ if not requests:
31
+ return []
32
+ recognizer = _load_paddle_ocr(
33
+ self.model_name,
34
+ self.config.paddle_text_detection_model_name,
35
+ self.config.device or "cpu",
36
+ self.config.batch_size,
37
+ self.config.paddle_text_det_limit_side_len,
38
+ self.config.paddle_text_det_thresh,
39
+ self.config.paddle_text_det_box_thresh,
40
+ self.config.paddle_text_det_unclip_ratio,
41
+ self.config.paddle_text_rec_score_thresh,
42
+ self.config.paddle_cache_dir,
43
+ )
44
+ return self._recognize_batch(recognizer, requests)
45
+
46
+ def _recognize_batch(self, recognizer, requests: list[tuple[str, str, dict | None]]) -> list[OCREvidence]:
47
+ started = time.perf_counter()
48
+ image_paths = [image_path for image_path, _, _ in requests]
49
+ try:
50
+ raw_results = recognizer.predict(image_paths)
51
+ except Exception:
52
+ return [self._recognize_one(recognizer, image_path, metadata or {}) for image_path, _, metadata in requests]
53
+ elapsed_ms = int((time.perf_counter() - started) * 1000)
54
+ if len(raw_results) != len(requests):
55
+ return [self._recognize_one(recognizer, image_path, metadata or {}) for image_path, _, metadata in requests]
56
+ per_item_latency = int(elapsed_ms / max(1, len(requests)))
57
+ return [
58
+ self._evidence_from_raw_result(raw_result, metadata or {}, per_item_latency)
59
+ for raw_result, (_, _, metadata) in zip(raw_results, requests)
60
+ ]
61
+
62
+ def _recognize_one(self, recognizer, image_path: str, metadata: dict[str, Any]) -> OCREvidence:
63
+ started = time.perf_counter()
64
+ try:
65
+ raw_result = recognizer.predict(str(image_path))
66
+ except Exception as exc:
67
+ return self._evidence_from_error(metadata, f"paddle_error: {exc}", int((time.perf_counter() - started) * 1000))
68
+ latency_ms = int((time.perf_counter() - started) * 1000)
69
+ return self._evidence_from_raw_result(raw_result, metadata, latency_ms)
70
+
71
+ def _evidence_from_raw_result(self, raw_result: Any, metadata: dict[str, Any], latency_ms: int) -> OCREvidence:
72
+ lines, scores = _extract_lines_and_scores(raw_result)
73
+ fields = parse_paddle_text_lines(lines)
74
+ error = None if lines else "paddle_no_text"
75
+ raw_text = json.dumps({"raw_text_lines": lines, **fields}, ensure_ascii=False)
76
+ confidence = _field_confidence(fields, scores)
77
+ return OCREvidence(
78
+ group_id=str(metadata.get("group_id", "")),
79
+ crop_id=str(metadata.get("crop_id", Path(image_path).stem)),
80
+ image_variant=str(metadata.get("image_variant", "original")),
81
+ raw_text=raw_text,
82
+ raw_text_lines=lines,
83
+ parsed_fields=fields,
84
+ field_confidence=confidence,
85
+ model_name=self.model_name,
86
+ latency_ms=latency_ms,
87
+ error=error,
88
+ )
89
+
90
+ def _evidence_from_error(self, metadata: dict[str, Any], error: str, latency_ms: int) -> OCREvidence:
91
+ return OCREvidence(
92
+ group_id=str(metadata.get("group_id", "")),
93
+ crop_id=str(metadata.get("crop_id", "")),
94
+ image_variant=str(metadata.get("image_variant", "original")),
95
+ raw_text="",
96
+ raw_text_lines=[],
97
+ parsed_fields={},
98
+ field_confidence={},
99
+ model_name=self.model_name,
100
+ latency_ms=latency_ms,
101
+ error=error,
102
+ )
103
+
104
+
105
+ def parse_paddle_text_lines(lines: list[str]) -> dict[str, str]:
106
+ clean_lines = [_clean_line(line) for line in lines]
107
+ clean_lines = [line for line in clean_lines if line]
108
+ prices = _extract_prices(clean_lines)
109
+ fields: dict[str, str] = {key: "" for key in OCR_FIELD_NAMES}
110
+ fields["barcode"] = _extract_barcode(clean_lines)
111
+ fields["discount_amount"] = _extract_discount(clean_lines)
112
+ fields["id_sku"] = _extract_sku(clean_lines, fields["barcode"])
113
+ fields["print_datetime"] = _extract_datetime(clean_lines)
114
+ if prices:
115
+ unique_prices = sorted(set(prices), key=lambda value: Decimal(value), reverse=True)
116
+ fields["price_default"] = unique_prices[0]
117
+ fields["price_card"] = unique_prices[-1] if len(unique_prices) > 1 else unique_prices[0]
118
+ fields["product_name"] = _extract_product_name(clean_lines)
119
+ return {key: value for key, value in fields.items() if value}
120
+
121
+
122
+ @lru_cache(maxsize=2)
123
+ def _load_paddle_ocr(
124
+ text_recognition_model_name: str,
125
+ text_detection_model_name: str,
126
+ device: str,
127
+ text_recognition_batch_size: int,
128
+ text_det_limit_side_len: int,
129
+ text_det_thresh: float,
130
+ text_det_box_thresh: float,
131
+ text_det_unclip_ratio: float,
132
+ text_rec_score_thresh: float,
133
+ cache_dir: str | None,
134
+ ):
135
+ _configure_paddle_cache(cache_dir)
136
+ from paddleocr import PaddleOCR
137
+
138
+ return PaddleOCR(
139
+ text_detection_model_name=text_detection_model_name,
140
+ text_recognition_model_name=text_recognition_model_name,
141
+ text_recognition_batch_size=max(1, int(text_recognition_batch_size)),
142
+ use_doc_orientation_classify=False,
143
+ use_doc_unwarping=False,
144
+ use_textline_orientation=False,
145
+ text_det_limit_side_len=text_det_limit_side_len,
146
+ text_det_thresh=text_det_thresh,
147
+ text_det_box_thresh=text_det_box_thresh,
148
+ text_det_unclip_ratio=text_det_unclip_ratio,
149
+ text_rec_score_thresh=text_rec_score_thresh,
150
+ device=device,
151
+ )
152
+
153
+
154
+ def _configure_paddle_cache(cache_dir: str | None) -> None:
155
+ if cache_dir:
156
+ path = Path(cache_dir)
157
+ if not path.is_absolute():
158
+ path = Path.cwd() / path
159
+ path.mkdir(parents=True, exist_ok=True)
160
+ os.environ.setdefault("PADDLE_PDX_CACHE_HOME", str(path))
161
+ os.environ.setdefault("PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK", "True")
162
+
163
+
164
+ def _extract_lines_and_scores(raw_result: Any) -> tuple[list[str], list[float]]:
165
+ lines: list[str] = []
166
+ scores: list[float] = []
167
+ for item in _iter_result_items(raw_result):
168
+ payload = _payload(item)
169
+ rec_texts = payload.get("rec_texts") or payload.get("texts") or []
170
+ rec_scores = payload.get("rec_scores") or payload.get("scores") or []
171
+ for text in rec_texts:
172
+ clean = _clean_line(text)
173
+ if clean:
174
+ lines.append(clean)
175
+ for score in rec_scores:
176
+ try:
177
+ scores.append(float(score))
178
+ except (TypeError, ValueError):
179
+ continue
180
+ if not rec_texts:
181
+ for text, score in _legacy_lines(item):
182
+ clean = _clean_line(text)
183
+ if clean:
184
+ lines.append(clean)
185
+ scores.append(score)
186
+ return lines, scores
187
+
188
+
189
+ def _iter_result_items(raw_result: Any) -> list[Any]:
190
+ if raw_result is None:
191
+ return []
192
+ if isinstance(raw_result, dict) or any(hasattr(raw_result, attr) for attr in ("to_dict", "dict", "res")):
193
+ return [raw_result]
194
+ if isinstance(raw_result, (list, tuple)):
195
+ if raw_result and _is_legacy_line(raw_result[0]):
196
+ return [raw_result]
197
+ return list(raw_result)
198
+ return [raw_result]
199
+
200
+
201
+ def _is_legacy_line(entry: Any) -> bool:
202
+ if not isinstance(entry, (list, tuple)) or len(entry) < 2:
203
+ return False
204
+ candidate = entry[1]
205
+ return isinstance(candidate, (list, tuple)) and bool(candidate)
206
+
207
+
208
+ def _payload(item: Any) -> dict[str, Any]:
209
+ if isinstance(item, dict):
210
+ return item.get("res", item)
211
+ for attr in ("to_dict", "dict"):
212
+ method = getattr(item, attr, None)
213
+ if callable(method):
214
+ try:
215
+ value = method()
216
+ if isinstance(value, dict):
217
+ return value.get("res", value)
218
+ except Exception:
219
+ pass
220
+ value = getattr(item, "json", None)
221
+ if isinstance(value, dict):
222
+ return value.get("res", value)
223
+ value = getattr(item, "res", None)
224
+ if isinstance(value, dict):
225
+ return value
226
+ return {}
227
+
228
+
229
+ def _legacy_lines(item: Any) -> list[tuple[str, float]]:
230
+ output: list[tuple[str, float]] = []
231
+ if not isinstance(item, list):
232
+ return output
233
+ for entry in item:
234
+ if not isinstance(entry, (list, tuple)) or len(entry) < 2:
235
+ continue
236
+ candidate = entry[1]
237
+ if isinstance(candidate, (list, tuple)) and candidate:
238
+ text = str(candidate[0])
239
+ score = float(candidate[1]) if len(candidate) > 1 else 0.7
240
+ output.append((text, score))
241
+ return output
242
+
243
+
244
+ def _extract_prices(lines: list[str]) -> list[str]:
245
+ prices: list[str] = []
246
+ for line in lines:
247
+ if "%" in line or _looks_like_barcode(line) or _looks_like_datetime(line):
248
+ continue
249
+ explicit = re.findall(r"(?<!\d)(\d{1,5})\s*[,.\-]\s*(\d{2})(?!\d)", line)
250
+ for rub, kop in explicit:
251
+ price = normalize_price(f"{rub}.{kop}")
252
+ if _valid_price(price):
253
+ prices.append(price)
254
+ if explicit:
255
+ continue
256
+ if _price_like_short_line(line):
257
+ match = re.search(r"(?<!\d)(\d{1,5})\s+(\d{2})(?!\d)", line)
258
+ if match:
259
+ price = normalize_price(f"{match.group(1)}.{match.group(2)}")
260
+ if _valid_price(price):
261
+ prices.append(price)
262
+ return prices
263
+
264
+
265
+ def _extract_barcode(lines: list[str]) -> str:
266
+ candidates: list[str] = []
267
+ for line in lines:
268
+ digits = normalize_barcode(line)
269
+ if len(digits) in {8, 12, 13, 14}:
270
+ candidates.append(digits)
271
+ for preferred_length in (13, 14, 8, 12):
272
+ for digits in candidates:
273
+ if len(digits) == preferred_length:
274
+ return digits
275
+ return ""
276
+
277
+
278
+ def _extract_discount(lines: list[str]) -> str:
279
+ for line in lines:
280
+ discount = normalize_discount(line)
281
+ if discount:
282
+ return discount
283
+ return ""
284
+
285
+
286
+ def _extract_sku(lines: list[str], barcode: str) -> str:
287
+ for line in lines:
288
+ digits = normalize_barcode(line)
289
+ if not digits or digits == barcode:
290
+ continue
291
+ if 9 <= len(digits) <= 12:
292
+ return normalize_sku(digits)
293
+ return ""
294
+
295
+
296
+ def _extract_datetime(lines: list[str]) -> str:
297
+ for line in lines:
298
+ match = re.search(r"\b\d{2}[.\/-]\d{2}[.\/-]\d{4}(?:\s+\d{1,2}:\d{2})?\b", line)
299
+ if match:
300
+ return match.group(0)
301
+ return ""
302
+
303
+
304
+ def _extract_product_name(lines: list[str]) -> str:
305
+ candidates: list[str] = []
306
+ for line in lines:
307
+ if _skip_name_line(line):
308
+ continue
309
+ if len(re.findall(r"[A-Za-zА-Яа-яЁё]", line)) < 3:
310
+ continue
311
+ candidates.append(line)
312
+ return " ".join(candidates[:3])[:220]
313
+
314
+
315
+ def _field_confidence(fields: dict[str, str], scores: list[float]) -> dict[str, float]:
316
+ base = max(0.35, min(0.99, sum(scores) / len(scores))) if scores else 0.65
317
+ return {key: base for key, value in fields.items() if key in OCR_FIELD_NAMES and value}
318
+
319
+
320
+ def _clean_line(value: Any) -> str:
321
+ return " ".join(str(value).replace("\u00a0", " ").split())
322
+
323
+
324
+ def _valid_price(value: str) -> bool:
325
+ try:
326
+ price = Decimal(value)
327
+ except Exception:
328
+ return False
329
+ return Decimal("0.01") <= price <= Decimal("999999.99")
330
+
331
+
332
+ def _looks_like_barcode(line: str) -> bool:
333
+ return len(normalize_barcode(line)) >= 8 and len(re.sub(r"\D", "", line)) / max(1, len(line)) > 0.65
334
+
335
+
336
+ def _looks_like_datetime(line: str) -> bool:
337
+ return bool(re.search(r"\b\d{2}[.\/-]\d{2}[.\/-]\d{4}\b", line))
338
+
339
+
340
+ def _price_like_short_line(line: str) -> bool:
341
+ if len(line) > 24:
342
+ return False
343
+ if re.search(r"[A-Za-zА-Яа-яЁё]{4,}", line):
344
+ return False
345
+ return bool(re.search(r"(?<!\d)\d{1,5}\s+\d{2}(?!\d)", line))
346
+
347
+
348
+ def _skip_name_line(line: str) -> bool:
349
+ lower = line.casefold()
350
+ if any(word in lower for word in ("цена", "скид", "руб", "коп", "карта", "штрих", "barcode")):
351
+ return True
352
+ if "%" in line or _looks_like_barcode(line) or _looks_like_datetime(line):
353
+ return True
354
+ return bool(_extract_prices([line]))
CV-Lenta-product-main/src/lenta_price_tags/pipeline.py CHANGED
@@ -18,6 +18,7 @@ from lenta_price_tags.cv.quality import CropQualitySelector
18
  from lenta_price_tags.matching.matcher import ProductMatcher
19
  from lenta_price_tags.ocr.docscope_provider import DocscopeOCRProvider
20
  from lenta_price_tags.ocr.mock_provider import MockOCRProvider
 
21
  from lenta_price_tags.ocr.prompts import PRICE_TAG_OCR_PROMPT
22
  from lenta_price_tags.output.csv_writer import CsvWriter, compose_final_row
23
  from lenta_price_tags.output.debug_writer import DebugWriter
@@ -155,6 +156,10 @@ class PriceTagRecognitionPipeline:
155
  ocr_config = replace(ocr_config, device=self.config.runtime.device)
156
  if ocr_config.provider.casefold() == "mock":
157
  return MockOCRProvider()
 
 
 
 
158
  return DocscopeOCRProvider(ocr_config)
159
 
160
  def _group_video(self, video: Path, run_dir: Path) -> list[PriceTagGroup]:
 
18
  from lenta_price_tags.matching.matcher import ProductMatcher
19
  from lenta_price_tags.ocr.docscope_provider import DocscopeOCRProvider
20
  from lenta_price_tags.ocr.mock_provider import MockOCRProvider
21
+ from lenta_price_tags.ocr.paddle_provider import PaddleOCRProvider
22
  from lenta_price_tags.ocr.prompts import PRICE_TAG_OCR_PROMPT
23
  from lenta_price_tags.output.csv_writer import CsvWriter, compose_final_row
24
  from lenta_price_tags.output.debug_writer import DebugWriter
 
156
  ocr_config = replace(ocr_config, device=self.config.runtime.device)
157
  if ocr_config.provider.casefold() == "mock":
158
  return MockOCRProvider()
159
+ if ocr_config.provider.casefold() == "docscope":
160
+ return DocscopeOCRProvider(ocr_config)
161
+ if ocr_config.provider.casefold() in {"paddle", "paddleocr", "ppocr"}:
162
+ return PaddleOCRProvider(ocr_config)
163
  return DocscopeOCRProvider(ocr_config)
164
 
165
  def _group_video(self, video: Path, run_dir: Path) -> list[PriceTagGroup]:
CV-Lenta-product-main/src/lenta_price_tags/ui/gradio_app.py CHANGED
@@ -50,7 +50,8 @@ def build_demo():
50
  "image_enhancement": {"variants": ["original"]},
51
  "ocr": {
52
  "provider": ocr_provider,
53
- "device": "cuda",
 
54
  "batch_size": int(ocr_batch_size),
55
  "max_new_tokens": 256,
56
  "zerogpu_duration_sec": 180,
@@ -78,7 +79,7 @@ def build_demo():
78
  video = gr.File(label="Video", file_types=[".mp4", ".mov", ".avi", ".mkv", ".webm"], type="filepath")
79
  db = gr.File(label="Product DB CSV", file_types=[".csv"], type="filepath")
80
  with gr.Row():
81
- ocr = gr.Radio(["docscope", "mock"], value="docscope", label="OCR")
82
  input_rotate = gr.Dropdown([0, 90, 180, 270], value=270, label="Video rotation")
83
  ocr_batch_size = gr.Dropdown([1, 2, 4, 8, 16, 32], value=16, label="OCR batch size")
84
  button = gr.Button("Run", variant="primary")
 
50
  "image_enhancement": {"variants": ["original"]},
51
  "ocr": {
52
  "provider": ocr_provider,
53
+ "device": "cuda" if ocr_provider == "docscope" else "cpu",
54
+ "model_name": "cyrillic_PP-OCRv5_mobile_rec",
55
  "batch_size": int(ocr_batch_size),
56
  "max_new_tokens": 256,
57
  "zerogpu_duration_sec": 180,
 
79
  video = gr.File(label="Video", file_types=[".mp4", ".mov", ".avi", ".mkv", ".webm"], type="filepath")
80
  db = gr.File(label="Product DB CSV", file_types=[".csv"], type="filepath")
81
  with gr.Row():
82
+ ocr = gr.Radio(["paddle", "docscope", "mock"], value="paddle", label="OCR")
83
  input_rotate = gr.Dropdown([0, 90, 180, 270], value=270, label="Video rotation")
84
  ocr_batch_size = gr.Dropdown([1, 2, 4, 8, 16, 32], value=16, label="OCR batch size")
85
  button = gr.Button("Run", variant="primary")
CV-Lenta-product-main/tests/test_paddle_ocr_parser.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from lenta_price_tags.ocr.paddle_provider import parse_paddle_text_lines
4
+
5
+
6
+ def test_parse_paddle_lines_extracts_price_barcode_and_name() -> None:
7
+ fields = parse_paddle_text_lines(
8
+ [
9
+ "Мед ПОТАПЫЧ натуральный липовый",
10
+ "415,79",
11
+ "316 99",
12
+ "-23%",
13
+ "4603552017456",
14
+ "370204501518",
15
+ ]
16
+ )
17
+
18
+ assert fields["product_name"] == "Мед ПОТАПЫЧ натуральный липовый"
19
+ assert fields["price_default"] == "415.79"
20
+ assert fields["price_card"] == "316.99"
21
+ assert fields["discount_amount"] == "-23%"
22
+ assert fields["barcode"] == "4603552017456"
23
+ assert fields["id_sku"] == "370204501518"
24
+
25
+
26
+ def test_parse_paddle_lines_does_not_turn_sku_into_price() -> None:
27
+ fields = parse_paddle_text_lines(
28
+ [
29
+ "Вино HAUT MARIN Colombard Ugni-blanc",
30
+ "270 108 726 573",
31
+ "3760094282559",
32
+ "1104.99",
33
+ ]
34
+ )
35
+
36
+ assert fields["price_default"] == "1104.99"
37
+ assert fields["barcode"] == "3760094282559"
38
+ assert fields["id_sku"] == "270108726573"
requirements.txt CHANGED
@@ -2,6 +2,8 @@ accelerate>=0.34
2
  gradio>=4.44
3
  numpy>=1.24
4
  opencv-python-headless>=4.10
 
 
5
  pillow>=10.0
6
  pydantic>=2.0
7
  qwen-vl-utils>=0.0.8
 
2
  gradio>=4.44
3
  numpy>=1.24
4
  opencv-python-headless>=4.10
5
+ paddleocr>=3.5.0
6
+ paddlepaddle>=3.3.0
7
  pillow>=10.0
8
  pydantic>=2.0
9
  qwen-vl-utils>=0.0.8