Cleanup: MB 2.0 Gap Analysis előtti állapot (adatok kizárva)
This commit is contained in:
64
backend/app/services/ai_ocr_service.py
Normal file
64
backend/app/services/ai_ocr_service.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# app/services/ai_ocr_service.py
|
||||
import json
|
||||
import httpx
|
||||
import base64
|
||||
from app.schemas.evidence import RegistrationDocumentExtracted
|
||||
|
||||
class AiOcrService:
|
||||
OLLAMA_URL = "http://service_finder_ollama:11434/api/generate"
|
||||
MODEL_NAME = "llama3.2-vision"
|
||||
|
||||
@classmethod
|
||||
async def extract_registration_data(cls, clean_image_bytes: bytes) -> RegistrationDocumentExtracted:
|
||||
base64_image = base64.b64encode(clean_image_bytes).decode('utf-8')
|
||||
|
||||
prompt = """
|
||||
Te egy magyar hatósági okmány-szakértő AI vagy. A feladatod a mellékelt magyar forgalmi engedély (kép) összes adatának kinyerése.
|
||||
|
||||
Keresd meg és olvasd le az adatokat az alábbi hatósági kódok alapján:
|
||||
- A: Rendszám (kötőjellel, pl: ABC-123 vagy AA-BB-123)
|
||||
- B: Első nyilvántartásba vétel dátuma (YYYY.MM.DD)
|
||||
- C.1.1: Családi név vagy cégnév
|
||||
- C.1.2: Utónév
|
||||
- C.1.3: Teljes lakcím (Irsz, Város, Utca, Házszám)
|
||||
- C.4: Jogosultság (a = tulajdonos, b = üzembentartó)
|
||||
- D.1: Gyártmány (pl. TOYOTA, VOLKSWAGEN)
|
||||
- D.2: Jármű típusa
|
||||
- D.3: Kereskedelmi leírás (pl. COROLLA, GOLF)
|
||||
- E: Alvázszám (pontosan 17 karakter)
|
||||
- G: Saját tömeg (kg)
|
||||
- F.1: Együttes tömeg (kg)
|
||||
- P.1: Hengerűrtartalom (cm3)
|
||||
- P.2: Teljesítmény (kW)
|
||||
- P.3: Hajtóanyag (pl. Benzin, Gázolaj, Elektromos)
|
||||
- P.5: Motorkód
|
||||
- V.9: Környezetvédelmi osztály kódja
|
||||
- R: Szín
|
||||
- S.1: Ülések száma
|
||||
- H: Műszaki érvényesség vége (YYYY.MM.DD)
|
||||
- Sebességváltó: Keresd a 0, 1, 2, 3 kódokat (0=mechanikus, 2=automata).
|
||||
|
||||
VÁLASZ FORMÁTUMA: Kizárólag érvényes JSON. Ha egy adat nem olvasható, az értéke null legyen.
|
||||
"""
|
||||
|
||||
payload = {
|
||||
"model": cls.MODEL_NAME,
|
||||
"prompt": prompt,
|
||||
"images": [base64_image],
|
||||
"stream": False,
|
||||
"format": "json"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=90.0) as client:
|
||||
try:
|
||||
response = await client.post(cls.OLLAMA_URL, json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
ai_response_text = response.json().get("response", "{}")
|
||||
data_dict = json.loads(ai_response_text)
|
||||
|
||||
return RegistrationDocumentExtracted(**data_dict)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Robot 3 AI Hiba: {e}")
|
||||
raise ValueError(f"AI hiba az adatkivonás során: {str(e)}")
|
||||
@@ -3,24 +3,20 @@ import json
|
||||
import logging
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
import base64
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional, List
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models import SystemParameter
|
||||
from app.models.system import SystemParameter
|
||||
|
||||
logger = logging.getLogger("AI-Service")
|
||||
|
||||
class AIService:
|
||||
"""
|
||||
AI Service v1.2.5 - Final Integrated Edition
|
||||
- Robot 2: Technikai dúsítás (Search + Regex JSON parsing)
|
||||
- Robot 3: OCR (Controlled JSON generation)
|
||||
"""
|
||||
api_key = os.getenv("GEMINI_API_KEY")
|
||||
client = genai.Client(api_key=api_key) if api_key else None
|
||||
PRIMARY_MODEL = "gemini-2.0-flash"
|
||||
OLLAMA_BASE_URL = "http://ollama:11434/api/generate"
|
||||
TEXT_MODEL = "qwen2.5-coder:32b"
|
||||
VISION_MODEL = "llava:7b"
|
||||
DVLA_API_KEY = os.getenv("DVLA_API_KEY")
|
||||
|
||||
@classmethod
|
||||
async def get_config_delay(cls) -> float:
|
||||
@@ -29,83 +25,71 @@ class AIService:
|
||||
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
|
||||
res = await db.execute(stmt)
|
||||
param = res.scalar_one_or_none()
|
||||
return float(param.value) if param else 1.0
|
||||
except Exception: return 1.0
|
||||
return float(param.value) if param else 0.1
|
||||
except Exception:
|
||||
return 0.1
|
||||
|
||||
@classmethod
|
||||
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 2: Adatbányászat Google Search segítségével."""
|
||||
if not cls.client: return None
|
||||
async def get_gold_data_from_research(cls, make: str, model: str, raw_context: str) -> Optional[Dict[str, Any]]:
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
search_tool = types.Tool(google_search=types.GoogleSearch())
|
||||
|
||||
prompt = f"""
|
||||
KERESS RÁ az interneten: {make} {raw_model} ({v_type}) pontos gyári modellkódja és technikai adatai.
|
||||
Adj választ szigorúan csak egy JSON blokkban:
|
||||
FELADAT: A mellékelt kutatási adatokból állíts össze egy hiteles technikai adatlapot.
|
||||
JÁRMŰ: {make} {model}
|
||||
KUTATÁSI ADATOK (Szemetesláda tartalom):
|
||||
{raw_context}
|
||||
|
||||
SZIGORÚ SZABÁLYOK:
|
||||
1. Csak a megerősített adatokat töltsd ki.
|
||||
2. Ha lóerőt (hp/bhp) találsz, váltsd át kW-ra (hp * 0.745).
|
||||
3. A 'marketing_name' maradjon 50 karakter alatt.
|
||||
|
||||
VÁLASZ FORMÁTUM (Tiszta JSON):
|
||||
{{
|
||||
"marketing_name": "tiszta név",
|
||||
"synonyms": ["név1", "név2"],
|
||||
"technical_code": "gyári kód",
|
||||
"year_from": int,
|
||||
"year_to": int_vagy_null,
|
||||
"marketing_name": "string",
|
||||
"technical_code": "string",
|
||||
"ccm": int,
|
||||
"kw": int,
|
||||
"maintenance": {{ "oil_type": "string", "oil_qty": float, "spark_plug": "string", "coolant": "string" }}
|
||||
"maintenance": {{
|
||||
"oil_type": "string",
|
||||
"oil_qty_liters": float,
|
||||
"spark_plug": "string",
|
||||
"final_drive": "string"
|
||||
}},
|
||||
"tires": {{
|
||||
"front": "string",
|
||||
"rear": "string"
|
||||
}},
|
||||
"is_duplicate_potential": bool
|
||||
}}
|
||||
FONTOS: A 'technical_code' NEM lehet üres. Ha nem találod, adj 'N/A' értéket!
|
||||
"""
|
||||
|
||||
# Search tool használata esetén a response_mime_type tilos!
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi járműtechnikai adatbányász vagy. Csak tiszta JSON-t válaszolsz markdown kódblokk nélkül.",
|
||||
tools=[search_tool],
|
||||
temperature=0.1
|
||||
)
|
||||
return await cls._execute_ai_call(prompt, make, model)
|
||||
|
||||
@classmethod
|
||||
async def _execute_ai_call(cls, prompt: str, make: str, model: str) -> Optional[Dict[str, Any]]:
|
||||
payload = {
|
||||
"model": cls.TEXT_MODEL,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json",
|
||||
"options": {"temperature": 0.1}
|
||||
}
|
||||
try:
|
||||
response = cls.client.models.generate_content(model=cls.PRIMARY_MODEL, contents=prompt, config=config)
|
||||
text = response.text
|
||||
# Tisztítás: ha az AI mégis tenne bele markdown jeleket
|
||||
clean_json = re.sub(r'```json\s*|```', '', text).strip()
|
||||
res_json = json.loads(clean_json)
|
||||
if isinstance(res_json, list) and len(res_json) > 0: res_json = res_json[0]
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
response = await client.post(cls.OLLAMA_BASE_URL, json=payload)
|
||||
response.raise_for_status()
|
||||
res_json = response.json()
|
||||
return json.loads(res_json.get("response", "{}"))
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
|
||||
logger.error(f"❌ AI hiba ({make} {model}): {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 3: OCR funkció - Forgalmi, Személyi, Számla, Odometer."""
|
||||
if not cls.client: return None
|
||||
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str, sources: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
prompts = {
|
||||
"identity": "Személyes okmány adatok (név, szám, lejárat).",
|
||||
"vehicle_reg": "Forgalmi adatok (rendszám, alvázszám, kW, ccm).",
|
||||
"invoice": "Számla adatok (partner, végösszeg, dátum).",
|
||||
"odometer": "Csak a kilométeróra állása számként."
|
||||
}
|
||||
|
||||
# Itt maradhat a response_mime_type, mert nem használunk Search-öt
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi OCR dokumentum-elemző vagy. Csak tiszta JSON-t válaszolsz.",
|
||||
response_mime_type="application/json"
|
||||
)
|
||||
|
||||
try:
|
||||
response = cls.client.models.generate_content(
|
||||
model=cls.PRIMARY_MODEL,
|
||||
contents=[
|
||||
f"Elemezd ezt a képet ({doc_type}): {prompts.get(doc_type, 'OCR')}",
|
||||
types.Part.from_bytes(data=image_data, mime_type="image/jpeg")
|
||||
],
|
||||
config=config
|
||||
)
|
||||
res_json = json.loads(response.text)
|
||||
if isinstance(res_json, list) and len(res_json) > 0: res_json = res_json[0]
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ OCR hiba: {e}")
|
||||
return None
|
||||
prompt = f"""
|
||||
FELADAT: Normalizáld a jármű adatait.
|
||||
GYÁRTÓ: {make} | MODELL: {raw_model}
|
||||
ADATOK: {json.dumps(sources)}
|
||||
(JSON válasz marketing_name, synonyms, technical_code, ccm, kw, year_from, year_to)
|
||||
"""
|
||||
return await cls._execute_ai_call(prompt, make, raw_model)
|
||||
141
backend/app/services/ai_service1.1.0.py
Normal file
141
backend/app/services/ai_service1.1.0.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
import re
|
||||
import base64
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional, List
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models import SystemParameter
|
||||
|
||||
logger = logging.getLogger("AI-Service")
|
||||
|
||||
class AIService:
|
||||
"""
|
||||
AI Service v1.3.5 - Private High-Performance Edition
|
||||
- Engine: Local Ollama (GPU Accelerated)
|
||||
- Features: DVLA Integration, 50-char Normalization, Private OCR
|
||||
"""
|
||||
|
||||
# A Docker belső hálózatán a szerviznév 'ollama'
|
||||
OLLAMA_BASE_URL = "http://ollama:11434/api/generate"
|
||||
TEXT_MODEL = "vehicle-pro"
|
||||
VISION_MODEL = "llava:7b"
|
||||
DVLA_API_KEY = os.getenv("DVLA_API_KEY")
|
||||
|
||||
@classmethod
|
||||
async def get_config_delay(cls) -> float:
|
||||
"""Késleltetés lekérése az adatbázisból."""
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
|
||||
res = await db.execute(stmt)
|
||||
param = res.scalar_one_or_none()
|
||||
return float(param.value) if param else 0.1
|
||||
except Exception:
|
||||
return 0.1
|
||||
|
||||
@classmethod
|
||||
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str, sources: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 2: Adat-összefésülés és normalizálás."""
|
||||
# Várjunk egy kicsit a GPU kímélése érdekében
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
prompt = f"""
|
||||
FELADAT: Normalizáld a jármű adatait több forrás alapján.
|
||||
GYÁRTÓ: {make}
|
||||
NYERS MODELLNÉV: {raw_model}
|
||||
FORRÁSOK NYERS ADATAI: {json.dumps(sources, ensure_ascii=False)}
|
||||
|
||||
SZIGORÚ SZABÁLYOK:
|
||||
1. 'marketing_name': MAXIMUM 50 KARAKTER!
|
||||
2. 'synonyms': Gyűjtsd ide az összes többi névváltozatot.
|
||||
3. 'technical_code': Keresd meg a gyári kódokat.
|
||||
|
||||
VÁLASZ FORMÁTUM (Csak tiszta JSON):
|
||||
{{
|
||||
"marketing_name": "string (max 50)",
|
||||
"synonyms": ["string"],
|
||||
"technical_code": "string",
|
||||
"ccm": int,
|
||||
"kw": int,
|
||||
"euro_class": int,
|
||||
"year_from": int,
|
||||
"year_to": int vagy null,
|
||||
"maintenance": {{
|
||||
"oil_type": "string",
|
||||
"oil_qty": float,
|
||||
"spark_plug": "string"
|
||||
}},
|
||||
"is_duplicate_potential": bool
|
||||
}}
|
||||
"""
|
||||
|
||||
payload = {
|
||||
"model": cls.TEXT_MODEL,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json",
|
||||
"options": {"temperature": 0.1}
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=90.0) as client:
|
||||
logger.info(f"📡 AI kérés küldése: {make} {raw_model}...")
|
||||
response = await client.post(cls.OLLAMA_BASE_URL, json=payload)
|
||||
response.raise_for_status()
|
||||
res_json = response.json()
|
||||
clean_data = json.loads(res_json.get("response", "{}"))
|
||||
|
||||
if clean_data.get("marketing_name"):
|
||||
clean_data["marketing_name"] = clean_data["marketing_name"][:50].strip()
|
||||
|
||||
return clean_data
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def get_dvla_data(cls, vrm: str) -> Optional[Dict[str, Any]]:
|
||||
"""Brit rendszám alapú adatok lekérése."""
|
||||
if not cls.DVLA_API_KEY: return None
|
||||
url = "https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles"
|
||||
headers = {"x-api-key": cls.DVLA_API_KEY, "Content-Type": "application/json"}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(url, json={"registrationNumber": vrm}, headers=headers)
|
||||
return resp.json() if resp.status_code == 200 else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ DVLA API hiba: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 3: Helyi OCR és dokumentum elemzés (Llava:7b)."""
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
prompts = {
|
||||
"identity": "Extract ID card data (name, id_number, expiry) as JSON.",
|
||||
"vehicle_reg": "Extract vehicle registration (plate, VIN, power_kw, engine_ccm) as JSON.",
|
||||
"invoice": "Extract invoice details (vendor, total_amount, date) as JSON.",
|
||||
"odometer": "Identify the number on the odometer and return as JSON: {'value': int}."
|
||||
}
|
||||
img_b64 = base64.b64encode(image_data).decode('utf-8')
|
||||
payload = {
|
||||
"model": cls.VISION_MODEL,
|
||||
"prompt": prompts.get(doc_type, "Perform OCR and return JSON"),
|
||||
"images": [img_b64],
|
||||
"stream": False,
|
||||
"format": "json"
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
response = await client.post(cls.OLLAMA_BASE_URL, json=payload)
|
||||
res_data = response.json()
|
||||
clean_json = res_data.get("response", "{}")
|
||||
match = re.search(r'\{.*\}', clean_json, re.DOTALL)
|
||||
return json.loads(match.group()) if match else json.loads(clean_json)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Helyi OCR hiba: {e}")
|
||||
return None
|
||||
111
backend/app/services/ai_service_googleApi_old.py
Normal file
111
backend/app/services/ai_service_googleApi_old.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models import SystemParameter
|
||||
|
||||
logger = logging.getLogger("AI-Service")
|
||||
|
||||
class AIService:
|
||||
"""
|
||||
AI Service v1.2.5 - Final Integrated Edition
|
||||
- Robot 2: Technikai dúsítás (Search + Regex JSON parsing)
|
||||
- Robot 3: OCR (Controlled JSON generation)
|
||||
"""
|
||||
api_key = os.getenv("GEMINI_API_KEY")
|
||||
client = genai.Client(api_key=api_key) if api_key else None
|
||||
PRIMARY_MODEL = "gemini-2.0-flash"
|
||||
|
||||
@classmethod
|
||||
async def get_config_delay(cls) -> float:
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
|
||||
res = await db.execute(stmt)
|
||||
param = res.scalar_one_or_none()
|
||||
return float(param.value) if param else 1.0
|
||||
except Exception: return 1.0
|
||||
|
||||
@classmethod
|
||||
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 2: Adatbányászat Google Search segítségével."""
|
||||
if not cls.client: return None
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
search_tool = types.Tool(google_search=types.GoogleSearch())
|
||||
|
||||
prompt = f"""
|
||||
KERESS RÁ az interneten: {make} {raw_model} ({v_type}) pontos gyári modellkódja és technikai adatai.
|
||||
Adj választ szigorúan csak egy JSON blokkban:
|
||||
{{
|
||||
"marketing_name": "tiszta név",
|
||||
"synonyms": ["név1", "név2"],
|
||||
"technical_code": "gyári kód",
|
||||
"year_from": int,
|
||||
"year_to": int_vagy_null,
|
||||
"ccm": int,
|
||||
"kw": int,
|
||||
"maintenance": {{ "oil_type": "string", "oil_qty": float, "spark_plug": "string", "coolant": "string" }}
|
||||
}}
|
||||
FONTOS: A 'technical_code' NEM lehet üres. Ha nem találod, adj 'N/A' értéket!
|
||||
"""
|
||||
|
||||
# Search tool használata esetén a response_mime_type tilos!
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi járműtechnikai adatbányász vagy. Csak tiszta JSON-t válaszolsz markdown kódblokk nélkül.",
|
||||
tools=[search_tool],
|
||||
temperature=0.1
|
||||
)
|
||||
|
||||
try:
|
||||
response = cls.client.models.generate_content(model=cls.PRIMARY_MODEL, contents=prompt, config=config)
|
||||
text = response.text
|
||||
# Tisztítás: ha az AI mégis tenne bele markdown jeleket
|
||||
clean_json = re.sub(r'```json\s*|```', '', text).strip()
|
||||
res_json = json.loads(clean_json)
|
||||
if isinstance(res_json, list) and len(res_json) > 0: res_json = res_json[0]
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 3: OCR funkció - Forgalmi, Személyi, Számla, Odometer."""
|
||||
if not cls.client: return None
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
prompts = {
|
||||
"identity": "Személyes okmány adatok (név, szám, lejárat).",
|
||||
"vehicle_reg": "Forgalmi adatok (rendszám, alvázszám, kW, ccm).",
|
||||
"invoice": "Számla adatok (partner, végösszeg, dátum).",
|
||||
"odometer": "Csak a kilométeróra állása számként."
|
||||
}
|
||||
|
||||
# Itt maradhat a response_mime_type, mert nem használunk Search-öt
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi OCR dokumentum-elemző vagy. Csak tiszta JSON-t válaszolsz.",
|
||||
response_mime_type="application/json"
|
||||
)
|
||||
|
||||
try:
|
||||
response = cls.client.models.generate_content(
|
||||
model=cls.PRIMARY_MODEL,
|
||||
contents=[
|
||||
f"Elemezd ezt a képet ({doc_type}): {prompts.get(doc_type, 'OCR')}",
|
||||
types.Part.from_bytes(data=image_data, mime_type="image/jpeg")
|
||||
],
|
||||
config=config
|
||||
)
|
||||
res_json = json.loads(response.text)
|
||||
if isinstance(res_json, list) and len(res_json) > 0: res_json = res_json[0]
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ OCR hiba: {e}")
|
||||
return None
|
||||
27
backend/app/services/dvla_service.py
Normal file
27
backend/app/services/dvla_service.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import httpx
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("DVLA-Service")
|
||||
|
||||
class DVLAService:
|
||||
API_URL = "https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles"
|
||||
API_KEY = "IDE_MÁSOLD_BE_AZ_API_KULCSOT"
|
||||
|
||||
@classmethod
|
||||
async def get_vehicle_details(cls, vrm: str):
|
||||
"""VRM az angol rendszám (pl. AB12 CDE)"""
|
||||
headers = {
|
||||
"x-api-key": cls.API_KEY,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {"registrationNumber": vrm}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(cls.API_URL, json=payload, headers=headers)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ DVLA hiba: {e}")
|
||||
return None
|
||||
62
backend/app/services/image_processor.py
Normal file
62
backend/app/services/image_processor.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from typing import Optional
|
||||
|
||||
class DocumentImageProcessor:
|
||||
"""
|
||||
Saját fejlesztésű képtisztító pipeline OCR-hez.
|
||||
A nyers (mobillal fotózott) képekből kontrasztos, fekete-fehér, zajmentes változatot készít,
|
||||
amelyet az AI már közel 100%-os pontossággal tud olvasni.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def process_for_ocr(image_bytes: bytes) -> Optional[bytes]:
|
||||
try:
|
||||
# 1. Kép betöltése a memóriából (FastAPI UploadFile bytes-ból)
|
||||
# A képet nem mentjük a lemezre, villámgyorsan a RAM-ban dolgozzuk fel.
|
||||
nparr = np.frombuffer(image_bytes, np.uint8)
|
||||
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
||||
|
||||
if img is None:
|
||||
raise ValueError("A képet nem sikerült dekódolni.")
|
||||
|
||||
# 2. Szürkeárnyalatossá alakítás (A színek csak zavarják a szövegfelismerést)
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# 3. Kép átméretezése (Felskálázás)
|
||||
# Az AI és az OCR motorok a minimum 300 DPI körüli képeket szeretik.
|
||||
height, width = gray.shape
|
||||
if width < 1000 or height < 1000:
|
||||
gray = cv2.resize(gray, None, fx=2.0, fy=2.0, interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
# 4. Kontraszt növelése (CLAHE - Contrast Limited Adaptive Histogram Equalization)
|
||||
# Ez eltünteti a vaku okozta becsillanásokat és kiemeli a halvány betűket.
|
||||
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||
contrast = clahe.apply(gray)
|
||||
|
||||
# 5. Enyhe homályosítás (Denoising / Noise Reduction)
|
||||
# Eltünteti a papír textúráját (pl. a forgalmi engedély vízjelét vagy a blokk gyűrődéseit).
|
||||
blur = cv2.GaussianBlur(contrast, (5, 5), 0)
|
||||
|
||||
# 6. Adaptív Küszöbérték (Binarization)
|
||||
# Minden pixel környezetét külön vizsgálja. Ez küszöböli ki azt, amikor a fotó egyik
|
||||
# sarka sötét (pl. árnyékot vet a telefon), a másik meg világos.
|
||||
thresh = cv2.adaptiveThreshold(
|
||||
blur,
|
||||
255,
|
||||
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||
cv2.THRESH_BINARY,
|
||||
11, # Blokk méret (páratlan szám)
|
||||
2 # Konstans levonás
|
||||
)
|
||||
|
||||
# 7. Visszakódolás bájt formátumba (PNG), hogy átadhassuk az AI-nak
|
||||
success, encoded_image = cv2.imencode('.png', thresh)
|
||||
if not success:
|
||||
raise ValueError("Nem sikerült a feldolgozott képet PNG-be kódolni.")
|
||||
|
||||
return encoded_image.tobytes()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Hiba a képfeldolgozás során: {str(e)}")
|
||||
return None
|
||||
29
backend/app/services/storage_service.py
Normal file
29
backend/app/services/storage_service.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import uuid
|
||||
from minio import Minio
|
||||
from app.core.config import settings
|
||||
|
||||
class StorageService:
|
||||
client = Minio(
|
||||
settings.MINIO_ENDPOINT,
|
||||
access_key=settings.MINIO_ROOT_USER,
|
||||
secret_key=settings.MINIO_ROOT_PASSWORD,
|
||||
secure=settings.MINIO_SECURE
|
||||
)
|
||||
BUCKET_NAME = "vehicle-documents"
|
||||
|
||||
@classmethod
|
||||
async def upload_document(cls, file_bytes: bytes, file_name: str, folder: str) -> str:
|
||||
if not cls.client.bucket_exists(cls.BUCKET_NAME):
|
||||
cls.client.make_bucket(cls.BUCKET_NAME)
|
||||
|
||||
# Egyedi fájlnév generálása az ütközések elkerülésére
|
||||
unique_name = f"{folder}/{uuid.uuid4()}_{file_name}"
|
||||
|
||||
from io import BytesIO
|
||||
cls.client.put_object(
|
||||
cls.BUCKET_NAME,
|
||||
unique_name,
|
||||
BytesIO(file_bytes),
|
||||
len(file_bytes)
|
||||
)
|
||||
return f"{cls.BUCKET_NAME}/{unique_name}"
|
||||
16
backend/app/services/translation.py
Executable file
16
backend/app/services/translation.py
Executable file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, Boolean, UniqueConstraint
|
||||
# JAVÍTÁS: Közvetlenül a base_class-ból importálunk, hogy elkerüljük a körkörös importot
|
||||
from app.db.base_class import Base
|
||||
|
||||
class Translation(Base):
|
||||
__tablename__ = "translations"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("key", "lang_code", name="uq_translation_key_lang"),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(100), nullable=False, index=True)
|
||||
lang_code = Column(String(5), nullable=False, index=True)
|
||||
value = Column(Text, nullable=False)
|
||||
is_published = Column(Boolean, default=False)
|
||||
Reference in New Issue
Block a user