tyagiAdarsh commited on
Commit
00a2c6e
ยท
verified ยท
1 Parent(s): 22b1bed

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +74 -95
app.py CHANGED
@@ -13,6 +13,7 @@ from transformers import AutoTokenizer, AutoModelForCausalLM
13
  # CONFIG
14
  # =========================================================
15
  MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
 
16
 
17
  app = FastAPI(title="Japanese Corrector API")
18
 
@@ -34,23 +35,19 @@ class InferenceRequest(BaseModel):
34
  input: str
35
 
36
  # =========================================================
37
- # SYSTEM PROMPT
38
  # =========================================================
39
  SYSTEM_PROMPT = """
40
  You are a professional Japanese language teacher.
41
- Input:
42
- - Japanese written in ROMAJI.
43
- Tasks:
44
- 1. Correct the sentence into natural Japanese.
45
- 2. Use appropriate Kanji.
46
- 3. Provide full Hiragana reading.
47
- 4. Explain grammar changes briefly.
48
- Rules:
49
- - Do NOT change meaning or polarity.
50
- - Do NOT invent verb forms.
51
- - Output ONLY valid JSON object (single JSON object) and nothing else.
52
- - No markdown, no extra text.
53
- Output format:
54
  {
55
  "kanji": "...",
56
  "hiragana": "...",
@@ -59,16 +56,13 @@ Output format:
59
  """.strip()
60
 
61
  # =========================================================
62
- # JSON EXTRACTION & SAFE PARSING
63
  # =========================================================
64
  def _strip_code_fences(text: str) -> str:
65
- # remove triple backticks and single backticks
66
  text = re.sub(r"```(?:json)?", "", text, flags=re.IGNORECASE)
67
- text = text.replace("`", "")
68
- return text
69
 
70
  def _normalize_quotes(text: str) -> str:
71
- # convert smart quotes to ASCII quotes
72
  replacements = {
73
  "โ€œ": '"', "โ€": '"', "โ€˜": "'", "โ€™": "'",
74
  "๏ผˆ": "(", "๏ผ‰": ")", "๏ผš": ":", "๏ผŒ": ","
@@ -78,73 +72,68 @@ def _normalize_quotes(text: str) -> str:
78
  return text
79
 
80
  def _remove_trailing_commas(text: str) -> str:
81
- # remove trailing commas before } or ]
82
- text = re.sub(r",\s*(?=[}\]])", "", text)
83
- return text
84
 
85
  def _find_balanced_json(text: str) -> Optional[str]:
86
- """Find first balanced JSON object substring using brace counting."""
87
- text = _strip_code_fences(text)
88
- text = _normalize_quotes(text)
89
- # find first '{'
90
  for m in re.finditer(r"\{", text):
91
  start = m.start()
92
  depth = 0
93
  for i in range(start, len(text)):
94
- ch = text[i]
95
- if ch == "{":
96
  depth += 1
97
- elif ch == "}":
98
  depth -= 1
99
  if depth == 0:
100
- candidate = text[start:i + 1]
101
- return candidate
102
  return None
103
 
104
  def safe_parse_json(text: str) -> Dict:
105
- """
106
- Robustly extract and parse the first JSON object in `text`.
107
- Attempts:
108
- 1) Balanced-brace extraction + json.loads
109
- 2) Apply small repairs (remove trailing commas, normalize quotes) + json.loads
110
- 3) ast.literal_eval on candidate (accepts single quotes / python-style)
111
- Raises ValueError if parsing fails.
112
- """
113
- raw = text or ""
114
- candidate = _find_balanced_json(raw)
115
  if not candidate:
116
- raise ValueError("No JSON-looking substring found")
117
 
118
- # try direct json.loads
119
  try:
120
  return json.loads(candidate)
121
- except Exception as e1:
122
- # try small repair: remove trailing commas
123
  repaired = _remove_trailing_commas(candidate)
124
  try:
125
  return json.loads(repaired)
126
- except Exception as e2:
127
- # try to fallback to ast.literal_eval (accepts single quotes)
128
- try:
129
- obj = ast.literal_eval(repaired)
130
- if isinstance(obj, dict):
131
- # ensure converted to json-compatible (strings) using dumps/loads
132
- return json.loads(json.dumps(obj, ensure_ascii=False))
133
- except Exception as e3:
134
- logger.debug("JSON parse attempts failed: %s | %s | %s", e1, e2, e3)
135
- raise ValueError("JSON parsing failed") from e3
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  # =========================================================
138
  # API ENDPOINT
139
  # =========================================================
140
  @app.post("/infer")
141
  def infer(req: InferenceRequest):
 
142
  messages = [
143
  {"role": "system", "content": SYSTEM_PROMPT},
144
  {"role": "user", "content": req.input.strip()}
145
  ]
146
 
147
- # build the underlying prompt using tokenizer helper (keeps chat formatting)
148
  try:
149
  prompt = tokenizer.apply_chat_template(
150
  messages,
@@ -152,50 +141,40 @@ def infer(req: InferenceRequest):
152
  add_generation_prompt=True
153
  )
154
  except Exception:
155
- # fallback: simple concatenation if apply_chat_template isn't available
156
  prompt = SYSTEM_PROMPT + "\n\nUser: " + req.input.strip() + "\nAssistant:"
157
 
158
- inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
159
-
160
- with torch.no_grad():
161
- outputs = model.generate(
162
- **inputs,
163
- max_new_tokens=256,
164
- do_sample=False,
165
- temperature=0.0,
166
- repetition_penalty=1.05,
167
- eos_token_id=tokenizer.eos_token_id,
168
- pad_token_id=tokenizer.eos_token_id,
169
- # num_return_sequences=1 is default
170
- )
171
-
172
- generated = outputs[0][inputs.input_ids.shape[1]:]
173
- raw_text = tokenizer.decode(generated, skip_special_tokens=True).strip()
174
- logger.info("Raw model output: %s", raw_text[:1000])
175
 
176
- try:
177
- data = safe_parse_json(raw_text)
178
- return {
179
- "result": {
180
- "kanji": data.get("kanji", ""),
181
- "hiragana": data.get("hiragana", ""),
182
- "explanation": data.get("explanation", ""),
183
- "warnings": []
184
- }
185
- }
186
- except Exception as exc:
187
- # include raw_text as a debugging warning (helpful for diagnosing why parse failed)
188
- return {
189
- "result": {
190
- "kanji": "",
191
- "hiragana": "",
192
- "explanation": "ใƒขใƒ‡ใƒซใฎๅ‡บๅŠ›ใ‚’่งฃๆžใงใใพใ›ใ‚“ใงใ—ใŸใ€‚",
193
- "warnings": [
194
- "Unparseable model output",
195
- raw_text[:2000] # limit length
196
- ]
197
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  }
 
199
 
200
  # =========================================================
201
  # LOCAL RUN
 
13
  # CONFIG
14
  # =========================================================
15
  MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
16
+ MAX_NEW_TOKENS = 256
17
 
18
  app = FastAPI(title="Japanese Corrector API")
19
 
 
35
  input: str
36
 
37
  # =========================================================
38
+ # SYSTEM PROMPT (STRICT)
39
  # =========================================================
40
  SYSTEM_PROMPT = """
41
  You are a professional Japanese language teacher.
42
+
43
+ Return ONLY valid JSON.
44
+ No text before or after JSON.
45
+ No markdown.
46
+ No explanations outside JSON.
47
+
48
+ The JSON MUST be fully closed.
49
+
50
+ Schema:
 
 
 
 
51
  {
52
  "kanji": "...",
53
  "hiragana": "...",
 
56
  """.strip()
57
 
58
  # =========================================================
59
+ # JSON UTILITIES
60
  # =========================================================
61
  def _strip_code_fences(text: str) -> str:
 
62
  text = re.sub(r"```(?:json)?", "", text, flags=re.IGNORECASE)
63
+ return text.replace("`", "")
 
64
 
65
  def _normalize_quotes(text: str) -> str:
 
66
  replacements = {
67
  "โ€œ": '"', "โ€": '"', "โ€˜": "'", "โ€™": "'",
68
  "๏ผˆ": "(", "๏ผ‰": ")", "๏ผš": ":", "๏ผŒ": ","
 
72
  return text
73
 
74
  def _remove_trailing_commas(text: str) -> str:
75
+ return re.sub(r",\s*(?=[}\]])", "", text)
 
 
76
 
77
  def _find_balanced_json(text: str) -> Optional[str]:
78
+ text = _normalize_quotes(_strip_code_fences(text))
 
 
 
79
  for m in re.finditer(r"\{", text):
80
  start = m.start()
81
  depth = 0
82
  for i in range(start, len(text)):
83
+ if text[i] == "{":
 
84
  depth += 1
85
+ elif text[i] == "}":
86
  depth -= 1
87
  if depth == 0:
88
+ return text[start:i + 1]
 
89
  return None
90
 
91
  def safe_parse_json(text: str) -> Dict:
92
+ candidate = _find_balanced_json(text)
 
 
 
 
 
 
 
 
 
93
  if not candidate:
94
+ raise ValueError("No JSON found")
95
 
 
96
  try:
97
  return json.loads(candidate)
98
+ except Exception:
 
99
  repaired = _remove_trailing_commas(candidate)
100
  try:
101
  return json.loads(repaired)
102
+ except Exception:
103
+ obj = ast.literal_eval(repaired)
104
+ if isinstance(obj, dict):
105
+ return json.loads(json.dumps(obj, ensure_ascii=False))
106
+ raise ValueError("JSON parsing failed")
107
+
108
+ # =========================================================
109
+ # GENERATION (WITH RETRY)
110
+ # =========================================================
111
+ def generate_once(prompt: str) -> str:
112
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
113
+ with torch.no_grad():
114
+ output = model.generate(
115
+ **inputs,
116
+ max_new_tokens=MAX_NEW_TOKENS,
117
+ do_sample=False,
118
+ temperature=0.0,
119
+ repetition_penalty=1.05,
120
+ eos_token_id=tokenizer.eos_token_id,
121
+ pad_token_id=tokenizer.eos_token_id
122
+ )
123
+ gen = output[0][inputs.input_ids.shape[1]:]
124
+ return tokenizer.decode(gen, skip_special_tokens=True).strip()
125
 
126
  # =========================================================
127
  # API ENDPOINT
128
  # =========================================================
129
  @app.post("/infer")
130
  def infer(req: InferenceRequest):
131
+
132
  messages = [
133
  {"role": "system", "content": SYSTEM_PROMPT},
134
  {"role": "user", "content": req.input.strip()}
135
  ]
136
 
 
137
  try:
138
  prompt = tokenizer.apply_chat_template(
139
  messages,
 
141
  add_generation_prompt=True
142
  )
143
  except Exception:
 
144
  prompt = SYSTEM_PROMPT + "\n\nUser: " + req.input.strip() + "\nAssistant:"
145
 
146
+ # ๐Ÿ” TRY TWICE (important)
147
+ last_raw = ""
148
+ for attempt in range(2):
149
+ raw = generate_once(prompt)
150
+ last_raw = raw
151
+ logger.info("Raw model output (attempt %d): %s", attempt + 1, raw[:500])
 
 
 
 
 
 
 
 
 
 
 
152
 
153
+ try:
154
+ data = safe_parse_json(raw)
155
+ return {
156
+ "result": {
157
+ "kanji": data.get("kanji", ""),
158
+ "hiragana": data.get("hiragana", ""),
159
+ "explanation": data.get("explanation", ""),
160
+ "warnings": []
161
+ }
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
163
+ except Exception:
164
+ continue
165
+
166
+ # ๐Ÿšจ Guaranteed safe fallback
167
+ return {
168
+ "result": {
169
+ "kanji": "",
170
+ "hiragana": "",
171
+ "explanation": "ใƒขใƒ‡ใƒซใฎๅ‡บๅŠ›ใ‚’่งฃๆžใงใใพใ›ใ‚“ใงใ—ใŸใ€‚",
172
+ "warnings": [
173
+ "Unparseable model output",
174
+ last_raw[:1000]
175
+ ]
176
  }
177
+ }
178
 
179
  # =========================================================
180
  # LOCAL RUN