maribakulj commited on
Commit
2f88b62
·
unverified ·
2 Parent(s): 6633c7a90027d3

Merge pull request #41 from maribakulj/claude/fix-manifest-analysis-performance-BIMKl

Browse files
backend/app/services/ai/response_parser.py CHANGED
@@ -5,10 +5,15 @@ Comportement :
5
  - JSON non parseable → ParseError (toute la page échoue)
6
  - Région avec bbox invalide → région ignorée + log (la page continue)
7
  - OCR invalide → OCRResult() par défaut + log (la page continue)
 
 
 
 
8
  """
9
  # 1. stdlib
10
  import json
11
  import logging
 
12
 
13
  # 2. third-party
14
  from pydantic import ValidationError
@@ -23,6 +28,77 @@ class ParseError(Exception):
23
  """Levée si la réponse de l'IA est un JSON invalide ou structurellement incorrecte."""
24
 
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def parse_ai_response(raw_text: str) -> tuple[dict, OCRResult]:
27
  """Parse la réponse textuelle de l'IA en layout dict + OCRResult validés.
28
 
@@ -30,7 +106,8 @@ def parse_ai_response(raw_text: str) -> tuple[dict, OCRResult]:
30
  faire échouer toute la page. Un JSON non parseable lève ParseError.
31
 
32
  Gère les balises Markdown (```json ... ```) que certains modèles ajoutent
33
- malgré les instructions.
 
34
 
35
  Args:
36
  raw_text: texte brut retourné par l'IA (censé être du JSON strict).
@@ -48,12 +125,16 @@ def parse_ai_response(raw_text: str) -> tuple[dict, OCRResult]:
48
  end = len(lines) - 1 if lines[-1].strip() == "```" else len(lines)
49
  text = "\n".join(lines[1:end])
50
 
51
- try:
52
- data = json.loads(text)
53
- except json.JSONDecodeError as exc:
 
 
 
54
  raise ParseError(
55
- f"Réponse IA non parseable en JSON : {exc}"
56
- ) from exc
 
57
 
58
  if not isinstance(data, dict):
59
  raise ParseError(
 
5
  - JSON non parseable → ParseError (toute la page échoue)
6
  - Région avec bbox invalide → région ignorée + log (la page continue)
7
  - OCR invalide → OCRResult() par défaut + log (la page continue)
8
+
9
+ Le parser est tolérant : il extrait le premier objet JSON valide du texte
10
+ même si l'IA ajoute du texte autour, des balises Markdown, des virgules
11
+ en trop, ou des échappements Unicode cassés.
12
  """
13
  # 1. stdlib
14
  import json
15
  import logging
16
+ import re
17
 
18
  # 2. third-party
19
  from pydantic import ValidationError
 
28
  """Levée si la réponse de l'IA est un JSON invalide ou structurellement incorrecte."""
29
 
30
 
31
+ def _extract_json_object(text: str) -> str:
32
+ """Extrait le premier objet JSON { ... } complet du texte.
33
+
34
+ Les VLMs renvoient souvent du texte avant/après le JSON, ou plusieurs
35
+ blocs JSON concaténés. Cette fonction trouve le premier '{' et compte
36
+ les accolades pour trouver le '}' fermant correspondant, en ignorant
37
+ les accolades à l'intérieur des chaînes.
38
+ """
39
+ start = text.find("{")
40
+ if start == -1:
41
+ return text
42
+
43
+ depth = 0
44
+ in_string = False
45
+ escape = False
46
+
47
+ for i in range(start, len(text)):
48
+ c = text[i]
49
+ if escape:
50
+ escape = False
51
+ continue
52
+ if c == "\\":
53
+ escape = True
54
+ continue
55
+ if c == '"' and not escape:
56
+ in_string = not in_string
57
+ continue
58
+ if in_string:
59
+ continue
60
+ if c == "{":
61
+ depth += 1
62
+ elif c == "}":
63
+ depth -= 1
64
+ if depth == 0:
65
+ return text[start : i + 1]
66
+
67
+ # Pas de fermeture trouvée — retourner depuis le premier '{'
68
+ return text[start:]
69
+
70
+
71
+ def _fix_common_json_issues(text: str) -> str:
72
+ """Corrige les erreurs JSON courantes des VLMs.
73
+
74
+ - Trailing commas avant } ou ]
75
+ - Échappements Unicode invalides (\\uXXXX incomplets)
76
+ """
77
+ # Trailing commas : ,} ou ,]
78
+ text = re.sub(r",\s*([}\]])", r"\1", text)
79
+ # Échappements unicode invalides — remplacer par un espace
80
+ text = re.sub(r"\\u(?![0-9a-fA-F]{4})[^\"]*", " ", text)
81
+ return text
82
+
83
+
84
+ def _try_parse_json(text: str) -> dict | None:
85
+ """Tente de parser du JSON, avec nettoyage progressif en cas d'échec."""
86
+ # Tentative 1 : tel quel
87
+ try:
88
+ return json.loads(text)
89
+ except json.JSONDecodeError:
90
+ pass
91
+
92
+ # Tentative 2 : corrections courantes
93
+ fixed = _fix_common_json_issues(text)
94
+ try:
95
+ return json.loads(fixed)
96
+ except json.JSONDecodeError:
97
+ pass
98
+
99
+ return None
100
+
101
+
102
  def parse_ai_response(raw_text: str) -> tuple[dict, OCRResult]:
103
  """Parse la réponse textuelle de l'IA en layout dict + OCRResult validés.
104
 
 
106
  faire échouer toute la page. Un JSON non parseable lève ParseError.
107
 
108
  Gère les balises Markdown (```json ... ```) que certains modèles ajoutent
109
+ malgré les instructions, ainsi que le texte avant/après le JSON et les
110
+ erreurs de formatage courantes des VLMs.
111
 
112
  Args:
113
  raw_text: texte brut retourné par l'IA (censé être du JSON strict).
 
125
  end = len(lines) - 1 if lines[-1].strip() == "```" else len(lines)
126
  text = "\n".join(lines[1:end])
127
 
128
+ # Extraction du premier objet JSON (ignore le texte autour)
129
+ text = _extract_json_object(text)
130
+
131
+ data = _try_parse_json(text)
132
+
133
+ if data is None:
134
  raise ParseError(
135
+ f"Réponse IA non parseable en JSON après nettoyage. "
136
+ f"Début du texte brut : {raw_text[:300]!r}"
137
+ )
138
 
139
  if not isinstance(data, dict):
140
  raise ParseError(