Реалізація AI-витягування даних із накладних і актів

Проектуємо та впроваджуємо системи штучного інтелекту: від прототипу до production-ready рішення. Наша команда поєднує експертизу в машинному навчанні, дата-інжинірингу та MLOps, щоб AI працював не в лабораторії, а в реальному бізнесі.
Показано 1 з 1Усі 1566 послуг
Реалізація AI-витягування даних із накладних і актів
Середній
~3-5 днів
Часті запитання

Напрямки AI-розробки

Етапи розробки AI-рішення

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1288
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    902
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1123
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    590
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    860

AI-видобуток даних з накладних та актів

Транспортні накладні (ТТН, CMR), товарні накладні (ТОРГ-12), акти виконаних робіт — документи з жорсткою структурою, але великою варіативністю заповнення: рукописні поля, печаті поверх тексту, скані низької якості, змішане заповнення (частина полів — машинопись, частина — від руки).

Завдання NER на layoutlm для накладних

Накладна — табличний документ: шапка (реквізити сторін), таблиця товарних позицій, підписи. LayoutLMv3 обробляє все це через token classification з урахуванням координат тексту.

from transformers import LayoutLMv3Processor, LayoutLMv3ForTokenClassification
from datasets import Dataset
import torch

# Повний набір міток для ТОРГ-12 / ТТН
WAYBILL_LABELS = [
    'O',
    'B-DOC_NUMBER', 'I-DOC_NUMBER',
    'B-DOC_DATE',   'I-DOC_DATE',
    'B-SENDER_NAME',    'I-SENDER_NAME',
    'B-SENDER_INN',     'I-SENDER_INN',
    'B-SENDER_ADDRESS', 'I-SENDER_ADDRESS',
    'B-RECEIVER_NAME',    'I-RECEIVER_NAME',
    'B-RECEIVER_INN',     'I-RECEIVER_INN',
    'B-RECEIVER_ADDRESS', 'I-RECEIVER_ADDRESS',
    'B-CARRIER_NAME',   'I-CARRIER_NAME',
    'B-VEHICLE_REG',    'I-VEHICLE_REG',     # гос. номер ТС
    'B-ITEM_NAME',      'I-ITEM_NAME',
    'B-ITEM_QTY',       'I-ITEM_QTY',
    'B-ITEM_UNIT',      'I-ITEM_UNIT',
    'B-ITEM_PRICE',     'I-ITEM_PRICE',
    'B-ITEM_TOTAL',     'I-ITEM_TOTAL',
    'B-TOTAL_QTY',      'I-TOTAL_QTY',
    'B-TOTAL_AMOUNT',   'I-TOTAL_AMOUNT',
    'B-DRIVER_NAME',    'I-DRIVER_NAME',
]

def prepare_waybill_dataset(
    image_paths: list,
    annotations: list,    # список dict з ключами: words, boxes, labels
    processor: LayoutLMv3Processor
) -> Dataset:
    """
    Підготовка датасету для fine-tuning.
    annotations[i]['boxes']: нормалізовані bbox [0..1000] для LayoutLM.
    """
    label2id = {l: i for i, l in enumerate(WAYBILL_LABELS)}

    features_list = []
    for img_path, ann in zip(image_paths, annotations):
        from PIL import Image as PILImage
        image = PILImage.open(img_path).convert('RGB')

        encoding = processor(
            image,
            text=ann['words'],
            boxes=ann['boxes'],
            word_labels=[label2id[l] for l in ann['labels']],
            truncation=True,
            padding='max_length',
            max_length=512,
            return_tensors='pt'
        )
        features_list.append({
            k: v.squeeze().tolist() for k, v in encoding.items()
        })

    return Dataset.from_list(features_list)

Обробка рукописних полів

Накладні часто мають рукописні підписи, дати, кількості. PaddleOCR або TrOCR для друкованого тексту на рукописних полях помиляються. Потрібен детектор рукописи + окремий рукописний OCR:

from transformers import TrOCRProcessor, VisionEncoderDecoderModel
from PIL import Image
import torch

class HandwritingOCR:
    def __init__(self):
        self.processor = TrOCRProcessor.from_pretrained(
            'microsoft/trocr-base-handwritten'
        )
        self.model = VisionEncoderDecoderModel.from_pretrained(
            'microsoft/trocr-base-handwritten'
        ).eval().cuda()

    @torch.no_grad()
    def recognize(self, image: Image.Image) -> str:
        pixel_values = self.processor(
            image, return_tensors='pt'
        ).pixel_values.to('cuda')

        generated_ids = self.model.generate(
            pixel_values,
            max_new_tokens=64,
            num_beams=4,
            early_stopping=True
        )
        return self.processor.batch_decode(
            generated_ids, skip_special_tokens=True
        )[0]

class HybridWaybillOCR:
    """
    Визначаємо тип тексту (друк / рукопись) → вибираємо OCR.
    Ознаки рукописи: велика дисперсія висоти символів, нема serif-паттернів.
    """
    def __init__(self):
        self.handwriting_ocr = HandwritingOCR()
        # PaddleOCR для друкованого
        from paddleocr import PaddleOCR
        self.printed_ocr = PaddleOCR(use_angle_cls=True, lang='uk')

    def is_handwritten(self, text_region: Image.Image) -> bool:
        """Проста евристика: variance of stroke width"""
        import numpy as np
        img_array = np.array(text_region.convert('L'))
        # Бінаризація
        _, binary = cv2.threshold(img_array, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        # Дисперсія ширини рядків як ознака рукописи
        col_density = (binary == 0).mean(axis=0)
        return float(col_density.std()) > 0.15   # емпіричний поріг

    def recognize_region(self, image: Image.Image) -> str:
        if self.is_handwritten(image):
            return self.handwriting_ocr.recognize(image)
        else:
            result = self.printed_ocr.ocr(np.array(image))
            return ' '.join([line[1][0] for line in result[0] or []])

Валідація реквізитів

import re

def validate_russian_inn(inn: str) -> bool:
    """Перевірка контрольної цифри ІНН (РФ)"""
    if not re.match(r'^\d{10}$|^\d{12}$', inn):
        return False
    digits = [int(d) for d in inn]
    if len(inn) == 10:
        check = sum(d * w for d, w in zip(digits[:9], [2,4,10,3,5,9,4,6,8])) % 11 % 10
        return digits[9] == check
    else:
        c1 = sum(d * w for d, w in zip(digits[:11], [7,2,4,10,3,5,9,4,6,8,0])) % 11 % 10
        c2 = sum(d * w for d, w in zip(digits[:11], [3,7,2,4,10,3,5,9,4,6,8])) % 11 % 10
        return digits[10] == c1 and digits[11] == c2

Сроки

Завдання Сроки
Видобуток полів ТОРГ-12 / ТТН (стандартні формати) 2–3 тижні
Fine-tuning LayoutLMv3 на корпоративні накладні 5–7 тижнів
Повна система з рукописю + валідацією + інтеграцією 1С 8–14 тижнів