codeboosterstech commited on
Commit
943a060
·
verified ·
1 Parent(s): e7a235b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -67
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py (single-file, all-in-one)
2
  import os
3
  import json
4
  import tempfile
@@ -67,78 +67,78 @@ class SerpClient:
67
  return resp.json()
68
 
69
  # ---------------------------
70
- # Safe file text extraction (Gradio returns FileData dict: {"name", "size", "path"})
71
  # ---------------------------
72
- def extract_text_from_gradio_file(filedata):
73
  """
74
- Supports BOTH:
75
- 1. HF Spaces dict:
76
- {"name": "..", "path": "...", "size": ...}
77
- 2. HF NamedString:
78
- filedata = NamedString("/tmp/.../file.txt")
79
  """
80
  if not filedata:
81
  return ""
82
 
83
- # Case A: filedata is a dict
84
  if isinstance(filedata, dict):
85
  file_path = filedata.get("path") or filedata.get("name")
86
- if not file_path:
87
- return ""
88
- file_path = str(file_path)
89
-
90
- # Case B: filedata is NamedString (just a string)
91
  else:
92
- # Gradio NamedString gives direct file path
93
  file_path = str(filedata)
94
 
95
- lower = file_path.lower()
 
96
 
97
  try:
 
98
  if lower.endswith(".txt"):
99
- return open(file_path, "r", encoding="utf-8", errors="ignore").read()
100
-
101
  if lower.endswith(".pdf"):
102
- from pypdf import PdfReader
103
- reader = PdfReader(file_path)
104
- return "\n".join([p.extract_text() or "" for p in reader.pages])
105
-
 
 
 
106
  if lower.endswith(".docx"):
107
- import docx
108
- doc = docx.Document(file_path)
109
- return "\n".join([p.text for p in doc.paragraphs])
110
-
111
- # fallback
 
 
 
112
  with open(file_path, "rb") as f:
113
  return f.read().decode("utf-8", errors="ignore")
114
-
115
  except Exception:
116
  return ""
117
 
118
  # ---------------------------
119
- # Prompt Templates (CSE and Non-CSE) - use your exact doc content here if available
120
  # ---------------------------
121
  NONCSE_TEMPLATE = """
122
  Role: You are an expert academic content creator for Mechanical/Electrical/Electronics (Non-CSE).
123
  Task: Generate an internal/continuous-assessment question paper matching GATE style.
124
  Rules:
125
- - Part A: {partA} questions, approx 2 marks each (adjust if marks per question different).
126
- - Part B: {partB} questions, choice/either-or pairs (marks per question ~13; adapt per template).
127
  - Part C: {partC} questions, case/design (higher marks).
128
  - Tag each question at end like: (Bloom's Level: <level> | Unit: <n> | GATE Reference: <year>)
129
  - Provide even unit coverage across the syllabus, ensure ~20% real-world/case-based questions.
130
  - Maintain difficulty index between 1.8 and 2.5.
131
- - Produce two outputs: Human-readable printable QP, and VALID JSON labeled <<QP_JSON>> at the very end, containing "questions" list with fields:
132
- question_no, part, sub_no, marks, unit, course_outcome, bloom_level, tags, question_text
133
  """
134
 
135
  CSE_TEMPLATE = """
136
  Role: You are an expert academic content creator for Computer Science (CSE), aligned with MAANGO BIG15.
137
  Task: Generate an internal/continuous-assessment question paper aligned with industry standards.
138
  Rules:
139
- - Part A: {partA} questions (short-answer)
140
- - Part B: {partB} questions (Either/Or; marks per question ~16)
141
- - Part C: {partC} questions (case/design)
142
  - Tag each question like: (Bloom's Level: <level> | Unit: <n> | Company Tag: <Company, Year>)
143
  - 20% of questions must be industry/case-study oriented.
144
  - Provide printable QP and VALID JSON <<QP_JSON>> as described above.
@@ -147,7 +147,7 @@ Rules:
147
  def build_master_prompt(stream: str, subject: str, partA: int, partB: int, partC: int, syllabus_text: str, ref_qp_text: str, realtime_snippets: str) -> str:
148
  template = CSE_TEMPLATE if stream.lower().startswith("cse") else NONCSE_TEMPLATE
149
  prompt = template.format(partA=partA, partB=partB, partC=partC)
150
- prompt += f"\nSubject: {subject}\n\nSyllabus (first 15000 chars):\n{(syllabus_text or '')[:15000]}\n\nReference QP (first 8000 chars):\n{(ref_qp_text or '')[:8000]}\n\nRealtime evidence (from web):\n{(realtime_snippets or '')[:5000]}\n\nINSTRUCTIONS:\n1) First provide the printable Question Paper\n2) At the very end provide the JSON labeled <<QP_JSON>> containing 'questions' array with the schema described above. JSON must be valid.\n"
151
  return prompt
152
 
153
  # ---------------------------
@@ -163,14 +163,12 @@ def extract_json_from_text(text: str) -> Optional[dict]:
163
  try:
164
  return json.loads(candidate)
165
  except Exception:
166
- # try find last '{'
167
  try:
168
  start = text.rfind("{")
169
  return json.loads(text[start:])
170
  except Exception:
171
  return None
172
  else:
173
- # fallback: try parse last {...}
174
  try:
175
  start = text.rfind("{")
176
  return json.loads(text[start:])
@@ -190,7 +188,6 @@ class MultiAgentOrchestrator:
190
  q = f"{subject} recent developments 2024 2025"
191
  out = self.serp.search(q, num=n)
192
  snippets = []
193
- # serpapi returns organic_results usually
194
  for item in out.get("organic_results", [])[:n]:
195
  title = item.get("title", "")
196
  snippet = item.get("snippet", "") or item.get("snippet_highlighted_words", "")
@@ -200,7 +197,7 @@ class MultiAgentOrchestrator:
200
  if not snippets and "answer" in out:
201
  snippets.append(str(out.get("answer")))
202
  return "\n\n".join(snippets)
203
- except Exception as e:
204
  return ""
205
 
206
  def run_pipeline(self, subject: str, stream: str, partA: int, partB: int, partC: int, syllabus_text: str, ref_qp_text: str) -> Dict[str, Any]:
@@ -219,31 +216,37 @@ class MultiAgentOrchestrator:
219
  # Try extract JSON
220
  qp_json = extract_json_from_text(gen_out)
221
  if qp_json is None:
222
- # ask generator for JSON only
223
  json_only_prompt = prompt + "\n\nNow output ONLY the VALID JSON object 'questions' for the paper (no additional text)."
224
  gen_json_only = self.groq.generate_text(system="Return JSON only.", user=json_only_prompt, model=GENERATOR_MODEL, max_tokens=3000, temperature=0.0)
225
  try:
226
  qp_json = json.loads(gen_json_only)
227
  except Exception:
228
  qp_json = {"raw_text": gen_out}
229
-
230
  result["qp_json"] = qp_json
231
 
232
  # AGENT 2: VERIFIER
233
  try:
234
- verifier_prompt = f"You are an academic verifier. Verify the QP JSON below for:\n- Bloom's taxonomy correctness\n- Unit coverage and distribution\n- Correct number of questions per part\n- Tag completeness and Company/GATE tags\n- Difficulty index 1.8-2.5\n- Duplications or ambiguous statements\nReturn a JSON object: {{'corrections': [...], 'issues': [...]}}"
 
 
 
 
 
 
 
 
 
235
  verifier_input = json.dumps(qp_json)[:50000]
236
  ver_out = self.groq.generate_text(system="Verifier agent.", user=verifier_prompt + "\n\n" + verifier_input, model=VERIFIER_MODEL, max_tokens=2000, temperature=0.0)
237
  try:
238
  ver_json = json.loads(ver_out)
239
  except Exception:
240
- # If not valid JSON, return raw text under 'raw'
241
  ver_json = {"raw": ver_out}
242
  result["verifier"] = ver_json
243
  except Exception as e:
244
  result["verifier"] = {"error": str(e)}
245
 
246
- # AGENT 3: FORMATTER (apply corrections & produce final JSON)
247
  try:
248
  fmt_prompt = (
249
  "You are a formatter. Input QP JSON and corrections. Apply corrections, ensure valid JSON structure, "
@@ -261,32 +264,36 @@ class MultiAgentOrchestrator:
261
  result["final"] = final_json
262
  except Exception as e:
263
  result["final"] = {"error": str(e)}
264
- except Exception as e:
265
  result["errors"].append(traceback.format_exc())
266
  return result
267
 
268
  # ---------------------------
269
- # DOCX builder functions (inline)
270
  # ---------------------------
271
  def _add_paragraph(doc, text, bold=False):
272
- run = doc.add_paragraph().add_run(text)
 
273
  run.bold = bold
274
 
275
- def build_question_paper_docx(path: Path, final_json: dict, generator_raw: str, subject: str):
276
  from docx import Document
277
  doc = Document()
278
  doc.add_heading(f"SNS College of Technology — {subject}", level=1)
279
  doc.add_paragraph("Instructions: Answer as per marks. Each question is tagged with Bloom's level and Unit.")
280
  doc.add_paragraph("\nPrintable Question Paper:\n")
281
  if generator_raw:
282
- # limit to a large but safe size
283
  doc.add_paragraph(generator_raw[:20000])
284
- # If structured final_json contains final_qp.questions, create a table
285
  questions = []
286
- if isinstance(final_json, dict):
287
- fq = final_json.get("final_qp") or final_json.get("final") or final_json
288
- if isinstance(fq, dict):
289
- questions = fq.get("questions", []) or []
 
 
 
 
290
  if questions:
291
  table = doc.add_table(rows=1, cols=5)
292
  hdr = table.rows[0].cells
@@ -302,32 +309,50 @@ def build_question_paper_docx(path: Path, final_json: dict, generator_raw: str,
302
  row[2].text = str(q.get("question_text", "")).strip()
303
  row[3].text = str(q.get("course_outcome", ""))
304
  row[4].text = f"{q.get('bloom_level','')} | {q.get('tags','')}"
 
 
 
305
  doc.save(path)
306
 
307
- def build_answers_docx(path: Path, final_json: dict, subject: str):
308
  from docx import Document
309
  doc = Document()
310
  doc.add_heading(f"Answer Key — {subject}", level=1)
 
311
  answers = {}
312
  if isinstance(final_json, dict):
313
- answers = final_json.get("answers", {}) or final_json.get("final", {}).get("answers", {}) or {}
 
314
  if isinstance(answers, dict) and answers:
315
  for k, v in answers.items():
316
  p = doc.add_paragraph()
317
  p.add_run(f"{k}:\n").bold = True
318
  doc.add_paragraph(str(v))
319
  else:
320
- doc.add_paragraph(json.dumps(final_json.get("answers", final_json), indent=2)[:15000])
 
 
 
 
 
 
 
 
321
  doc.save(path)
322
 
323
- def build_obe_docx(path: Path, final_json: dict, subject: str):
324
  from docx import Document
325
  doc = Document()
326
  doc.add_heading(f"OBE Summary — {subject}", level=1)
 
327
  obe = {}
328
  if isinstance(final_json, dict):
329
- obe = final_json.get("obe", {}) or final_json.get("final", {}).get("obe", {}) or {}
330
- doc.add_paragraph(json.dumps(obe, indent=2)[:15000])
 
 
 
 
331
  doc.save(path)
332
 
333
  # ---------------------------
@@ -354,16 +379,27 @@ def run_system_ui(subject, stream, partA, partB, partC, syllabus_file, ref_file)
354
  syllabus_text = extract_text_from_gradio_file(syllabus_file)
355
  ref_text = extract_text_from_gradio_file(ref_file) if ref_file else ""
356
  if not syllabus_text:
357
- # If the user uploaded nothing or extraction failed, show helpful message referencing the sample file
358
  sample_path = "/mnt/data/cloud_computing_syllabus.txt"
359
  msg = ("Syllabus extraction failed or file empty. "
360
- f"If you want to test immediately, you can use the sample syllabus located at: {sample_path} "
361
- "Upload a .txt/.pdf/.docx file instead.")
362
  return None, None, None, msg
363
 
364
  # call orchestrator
365
  out = orchestrator.run_pipeline(subject=subject, stream=stream, partA=int(partA), partB=int(partB), partC=int(partC), syllabus_text=syllabus_text, ref_qp_text=ref_text)
366
- final_json = out.get("final", {})
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  gen_raw = out.get("generator_raw", "")
368
 
369
  # write docx files to temp dir
@@ -411,5 +447,4 @@ with gr.Blocks() as app:
411
 
412
  # Launch
413
  if __name__ == "__main__":
414
- # On Spaces, gradio will handle host/port; for local testing you can set share=True
415
  app.launch()
 
1
+ # app.py (patched final single-file)
2
  import os
3
  import json
4
  import tempfile
 
67
  return resp.json()
68
 
69
  # ---------------------------
70
+ # Safe file text extraction (handles dict and NamedString)
71
  # ---------------------------
72
+ def extract_text_from_gradio_file(filedata) -> str:
73
  """
74
+ Accepts either:
75
+ - HF Spaces FileData dict: {"name": "...", "path": "/tmp/..", "size": n}
76
+ - Gradio NamedString or plain string (e.g., "/tmp/..")
77
+ Returns extracted text for .txt, .pdf, .docx, or a text fallback.
 
78
  """
79
  if not filedata:
80
  return ""
81
 
82
+ # Determine file path
83
  if isinstance(filedata, dict):
84
  file_path = filedata.get("path") or filedata.get("name")
 
 
 
 
 
85
  else:
86
+ # NamedString or plain string
87
  file_path = str(filedata)
88
 
89
+ if not file_path:
90
+ return ""
91
 
92
  try:
93
+ lower = file_path.lower()
94
  if lower.endswith(".txt"):
95
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
96
+ return f.read()
97
  if lower.endswith(".pdf"):
98
+ try:
99
+ from pypdf import PdfReader
100
+ reader = PdfReader(file_path)
101
+ return "\n".join([p.extract_text() or "" for p in reader.pages])
102
+ except Exception:
103
+ with open(file_path, "rb") as f:
104
+ return f.read().decode("utf-8", errors="ignore")
105
  if lower.endswith(".docx"):
106
+ try:
107
+ import docx
108
+ doc = docx.Document(file_path)
109
+ return "\n".join([p.text for p in doc.paragraphs])
110
+ except Exception:
111
+ with open(file_path, "rb") as f:
112
+ return f.read().decode("utf-8", errors="ignore")
113
+ # fallback: read bytes
114
  with open(file_path, "rb") as f:
115
  return f.read().decode("utf-8", errors="ignore")
 
116
  except Exception:
117
  return ""
118
 
119
  # ---------------------------
120
+ # Prompt Templates (CSE and Non-CSE)
121
  # ---------------------------
122
  NONCSE_TEMPLATE = """
123
  Role: You are an expert academic content creator for Mechanical/Electrical/Electronics (Non-CSE).
124
  Task: Generate an internal/continuous-assessment question paper matching GATE style.
125
  Rules:
126
+ - Part A: {partA} questions, approx 2 marks each.
127
+ - Part B: {partB} questions, choice/either-or pairs.
128
  - Part C: {partC} questions, case/design (higher marks).
129
  - Tag each question at end like: (Bloom's Level: <level> | Unit: <n> | GATE Reference: <year>)
130
  - Provide even unit coverage across the syllabus, ensure ~20% real-world/case-based questions.
131
  - Maintain difficulty index between 1.8 and 2.5.
132
+ - Produce two outputs: Human-readable printable QP, and VALID JSON labeled <<QP_JSON>> at the very end containing "questions".
 
133
  """
134
 
135
  CSE_TEMPLATE = """
136
  Role: You are an expert academic content creator for Computer Science (CSE), aligned with MAANGO BIG15.
137
  Task: Generate an internal/continuous-assessment question paper aligned with industry standards.
138
  Rules:
139
+ - Part A: {partA} short-answer questions.
140
+ - Part B: {partB} questions (Either/Or pairs).
141
+ - Part C: {partC} questions (case/design).
142
  - Tag each question like: (Bloom's Level: <level> | Unit: <n> | Company Tag: <Company, Year>)
143
  - 20% of questions must be industry/case-study oriented.
144
  - Provide printable QP and VALID JSON <<QP_JSON>> as described above.
 
147
  def build_master_prompt(stream: str, subject: str, partA: int, partB: int, partC: int, syllabus_text: str, ref_qp_text: str, realtime_snippets: str) -> str:
148
  template = CSE_TEMPLATE if stream.lower().startswith("cse") else NONCSE_TEMPLATE
149
  prompt = template.format(partA=partA, partB=partB, partC=partC)
150
+ prompt += f"\nSubject: {subject}\n\nSyllabus (first 15000 chars):\n{(syllabus_text or '')[:15000]}\n\nReference QP (first 8000 chars):\n{(ref_qp_text or '')[:8000]}\n\nRealtime evidence (from web):\n{(realtime_snippets or '')[:5000]}\n\nINSTRUCTIONS:\n1) First provide the printable Question Paper\n2) At the very end provide the JSON labeled <<QP_JSON>> containing 'questions' array. JSON must be valid.\n"
151
  return prompt
152
 
153
  # ---------------------------
 
163
  try:
164
  return json.loads(candidate)
165
  except Exception:
 
166
  try:
167
  start = text.rfind("{")
168
  return json.loads(text[start:])
169
  except Exception:
170
  return None
171
  else:
 
172
  try:
173
  start = text.rfind("{")
174
  return json.loads(text[start:])
 
188
  q = f"{subject} recent developments 2024 2025"
189
  out = self.serp.search(q, num=n)
190
  snippets = []
 
191
  for item in out.get("organic_results", [])[:n]:
192
  title = item.get("title", "")
193
  snippet = item.get("snippet", "") or item.get("snippet_highlighted_words", "")
 
197
  if not snippets and "answer" in out:
198
  snippets.append(str(out.get("answer")))
199
  return "\n\n".join(snippets)
200
+ except Exception:
201
  return ""
202
 
203
  def run_pipeline(self, subject: str, stream: str, partA: int, partB: int, partC: int, syllabus_text: str, ref_qp_text: str) -> Dict[str, Any]:
 
216
  # Try extract JSON
217
  qp_json = extract_json_from_text(gen_out)
218
  if qp_json is None:
 
219
  json_only_prompt = prompt + "\n\nNow output ONLY the VALID JSON object 'questions' for the paper (no additional text)."
220
  gen_json_only = self.groq.generate_text(system="Return JSON only.", user=json_only_prompt, model=GENERATOR_MODEL, max_tokens=3000, temperature=0.0)
221
  try:
222
  qp_json = json.loads(gen_json_only)
223
  except Exception:
224
  qp_json = {"raw_text": gen_out}
 
225
  result["qp_json"] = qp_json
226
 
227
  # AGENT 2: VERIFIER
228
  try:
229
+ verifier_prompt = (
230
+ "You are an academic verifier. Verify the QP JSON below for:\n"
231
+ "- Bloom's taxonomy correctness\n"
232
+ "- Unit coverage and distribution\n"
233
+ "- Correct number of questions per part\n"
234
+ "- Tag completeness and Company/GATE tags\n"
235
+ "- Difficulty index 1.8-2.5\n"
236
+ "- Duplications or ambiguous statements\n"
237
+ "Return a JSON object: {'corrections': [...], 'issues': [...]}"
238
+ )
239
  verifier_input = json.dumps(qp_json)[:50000]
240
  ver_out = self.groq.generate_text(system="Verifier agent.", user=verifier_prompt + "\n\n" + verifier_input, model=VERIFIER_MODEL, max_tokens=2000, temperature=0.0)
241
  try:
242
  ver_json = json.loads(ver_out)
243
  except Exception:
 
244
  ver_json = {"raw": ver_out}
245
  result["verifier"] = ver_json
246
  except Exception as e:
247
  result["verifier"] = {"error": str(e)}
248
 
249
+ # AGENT 3: FORMATTER
250
  try:
251
  fmt_prompt = (
252
  "You are a formatter. Input QP JSON and corrections. Apply corrections, ensure valid JSON structure, "
 
264
  result["final"] = final_json
265
  except Exception as e:
266
  result["final"] = {"error": str(e)}
267
+ except Exception:
268
  result["errors"].append(traceback.format_exc())
269
  return result
270
 
271
  # ---------------------------
272
+ # DOCX builder functions (robust)
273
  # ---------------------------
274
  def _add_paragraph(doc, text, bold=False):
275
+ p = doc.add_paragraph()
276
+ run = p.add_run(text)
277
  run.bold = bold
278
 
279
+ def build_question_paper_docx(path: Path, final_json: Optional[dict], generator_raw: str, subject: str):
280
  from docx import Document
281
  doc = Document()
282
  doc.add_heading(f"SNS College of Technology — {subject}", level=1)
283
  doc.add_paragraph("Instructions: Answer as per marks. Each question is tagged with Bloom's level and Unit.")
284
  doc.add_paragraph("\nPrintable Question Paper:\n")
285
  if generator_raw:
 
286
  doc.add_paragraph(generator_raw[:20000])
287
+
288
  questions = []
289
+ try:
290
+ if isinstance(final_json, dict):
291
+ fq = final_json.get("final_qp") or final_json.get("final") or final_json
292
+ if isinstance(fq, dict):
293
+ questions = fq.get("questions", []) or []
294
+ except Exception:
295
+ questions = []
296
+
297
  if questions:
298
  table = doc.add_table(rows=1, cols=5)
299
  hdr = table.rows[0].cells
 
309
  row[2].text = str(q.get("question_text", "")).strip()
310
  row[3].text = str(q.get("course_outcome", ""))
311
  row[4].text = f"{q.get('bloom_level','')} | {q.get('tags','')}"
312
+ else:
313
+ doc.add_paragraph("No structured questions were produced by the formatter. See the raw generator output above.")
314
+
315
  doc.save(path)
316
 
317
+ def build_answers_docx(path: Path, final_json: Optional[dict], subject: str):
318
  from docx import Document
319
  doc = Document()
320
  doc.add_heading(f"Answer Key — {subject}", level=1)
321
+
322
  answers = {}
323
  if isinstance(final_json, dict):
324
+ # try multiple possible locations
325
+ answers = final_json.get("answers") or final_json.get("final", {}).get("answers", {}) or {}
326
  if isinstance(answers, dict) and answers:
327
  for k, v in answers.items():
328
  p = doc.add_paragraph()
329
  p.add_run(f"{k}:\n").bold = True
330
  doc.add_paragraph(str(v))
331
  else:
332
+ # fallback: safe dump
333
+ safe_dump = ""
334
+ try:
335
+ safe_dump = json.dumps(final_json or {"note": "No final JSON"}, indent=2)[:15000]
336
+ except Exception:
337
+ safe_dump = str(final_json)[:15000]
338
+ doc.add_paragraph("No structured answers provided by AI. Falling back to raw final JSON (truncated):")
339
+ doc.add_paragraph(safe_dump)
340
+
341
  doc.save(path)
342
 
343
+ def build_obe_docx(path: Path, final_json: Optional[dict], subject: str):
344
  from docx import Document
345
  doc = Document()
346
  doc.add_heading(f"OBE Summary — {subject}", level=1)
347
+
348
  obe = {}
349
  if isinstance(final_json, dict):
350
+ obe = final_json.get("obe") or final_json.get("final", {}).get("obe", {}) or {}
351
+ try:
352
+ doc.add_paragraph(json.dumps(obe or {"note": "No OBE produced"}, indent=2)[:15000])
353
+ except Exception:
354
+ doc.add_paragraph(str(obe)[:15000])
355
+
356
  doc.save(path)
357
 
358
  # ---------------------------
 
379
  syllabus_text = extract_text_from_gradio_file(syllabus_file)
380
  ref_text = extract_text_from_gradio_file(ref_file) if ref_file else ""
381
  if not syllabus_text:
 
382
  sample_path = "/mnt/data/cloud_computing_syllabus.txt"
383
  msg = ("Syllabus extraction failed or file empty. "
384
+ f"Use the sample syllabus for testing: {sample_path} or upload a .txt/.pdf/.docx.")
 
385
  return None, None, None, msg
386
 
387
  # call orchestrator
388
  out = orchestrator.run_pipeline(subject=subject, stream=stream, partA=int(partA), partB=int(partB), partC=int(partC), syllabus_text=syllabus_text, ref_qp_text=ref_text)
389
+
390
+ # Ensure final_json is always a dict (fallback if None or invalid)
391
+ raw_final = out.get("final")
392
+ if isinstance(raw_final, dict):
393
+ final_json = raw_final
394
+ else:
395
+ final_json = {
396
+ "final_qp": {"questions": []},
397
+ "answers": {},
398
+ "obe": {},
399
+ "error": "Formatter returned invalid JSON or None.",
400
+ "generator_raw_sample": (out.get("generator_raw") or "")[:5000]
401
+ }
402
+
403
  gen_raw = out.get("generator_raw", "")
404
 
405
  # write docx files to temp dir
 
447
 
448
  # Launch
449
  if __name__ == "__main__":
 
450
  app.launch()