Daemontatox commited on
Commit
521c1f0
·
verified ·
1 Parent(s): 40fcbb2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +84 -353
app.py CHANGED
@@ -1,26 +1,28 @@
1
- import openai
2
- import base64
3
  import io
4
  import time
 
5
  import logging
6
  import fitz # PyMuPDF
7
  from PIL import Image
8
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  # Set up logging
11
  logging.basicConfig(level=logging.INFO)
12
  logger = logging.getLogger(__name__)
13
 
14
- import os
15
- OPENROUTER_API_KEY = os.getenv("OPENAI_TOKEN")
16
- if not OPENROUTER_API_KEY:
17
- raise ValueError("OPENROUTER_API_KEY environment variable not set")
18
- openai.api_key = OPENROUTER_API_KEY
19
-
20
- # Configure the OpenAI API to use OpenRouter
21
- openai.api_base = "https://openrouter.ai/api/v1"
22
- openai.api_key = OPENROUTER_API_KEY
23
-
24
  # -------------------------------
25
  # Document State and File Processing
26
  # -------------------------------
@@ -38,7 +40,7 @@ class DocumentState:
38
  doc_state = DocumentState()
39
 
40
  def process_pdf_file(file_path):
41
- """Convert PDF to images and extract text using PyMuPDF."""
42
  try:
43
  doc = fitz.open(file_path)
44
  images = []
@@ -50,13 +52,12 @@ def process_pdf_file(file_path):
50
  if page_text.strip():
51
  text += f"Page {page_num + 1}:\n{page_text}\n\n"
52
 
53
- # Render page to an image
54
  zoom = 3
55
  mat = fitz.Matrix(zoom, zoom)
56
  pix = page.get_pixmap(matrix=mat, alpha=False)
57
  img_data = pix.tobytes("png")
58
- img = Image.open(io.BytesIO(img_data))
59
- img = img.convert("RGB")
60
 
61
  # Resize if image is too large
62
  max_size = 1600
@@ -64,7 +65,6 @@ def process_pdf_file(file_path):
64
  ratio = max_size / max(img.size)
65
  new_size = tuple(int(dim * ratio) for dim in img.size)
66
  img = img.resize(new_size, Image.Resampling.LANCZOS)
67
-
68
  images.append(img)
69
  except Exception as e:
70
  logger.error(f"Error processing page {page_num}: {str(e)}")
@@ -78,13 +78,13 @@ def process_pdf_file(file_path):
78
  raise
79
 
80
  def process_uploaded_file(file):
81
- """Process uploaded file and update document state."""
82
  try:
83
  doc_state.clear()
84
  if file is None:
85
  return "No file uploaded. Please upload a file."
86
 
87
- # Get the file path and extension
88
  if isinstance(file, dict):
89
  file_path = file["name"]
90
  else:
@@ -119,16 +119,17 @@ def process_uploaded_file(file):
119
  return "An error occurred while processing the file. Please try again."
120
 
121
  # -------------------------------
122
- # Bot Streaming Function Using OpenAI API
123
  # -------------------------------
124
- def bot_streaming(prompt_option, max_new_tokens=4096):
125
  """
126
- Generate a response using the OpenAI API.
127
-
128
- If an image is available, it is encoded in base64 and appended to the prompt.
 
129
  """
130
  try:
131
- # Define predetermined prompts
132
  prompts = {
133
  "NOC Timesheet": (
134
  """Extract structured information from the provided timesheet. The extracted details should include:
@@ -173,333 +174,90 @@ Noc representative's date approval_date
173
 
174
  Noc representative status as approval_status
175
 
176
- The output should be formatted as a JSON instance that conforms to the JSON schema below.
177
-
178
- As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]} the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted."""
179
  ),
180
  "NOC Basic": (
181
  "Based on the provided timesheet details, extract the following information:\n"
182
- " - Full name of the person\n"
183
- " - Position title of the person\n"
184
  " - Work location\n"
185
  " - Contractor's name\n"
186
  " - NOC ID\n"
187
- " - Month and year (in MM/YYYY format)"
188
  ),
189
  "Aramco Full structured": (
190
- """You are a document parsing assistant designed to extract structured data from various document types, including invoices, timesheets, purchase orders, and travel bookings. Your goal is to return highly accurate, properly formatted JSON for each document type.
191
- General Rules:
192
- 1. Always return ONLY valid JSON—no explanations, comments, or additional text.
193
- 2. Use null for any fields that are not present or cannot be extracted.
194
- 3. Ensure all JSON keys are enclosed in double quotes and properly formatted.
195
- 4. Validate financial, time tracking, and contract details carefully before output.
196
-
197
- Extraction Instructions:
198
-
199
- 1. Invoice:
200
- - Parse and extract financial and invoice-specific details.
201
- - JSON structure:
202
- ```json
203
- {
204
- "invoice": {
205
- "date": null,
206
- "dueDate": null,
207
- "accountNumber": null,
208
- "invoiceNumber": null,
209
- "customerContact": null,
210
- "kintecContact": null,
211
- "accountsContact": null,
212
- "periodEnd": null,
213
- "contractNo": null,
214
- "specialistsName": null,
215
- "rpoNumber": null,
216
- "assignmentProject": null,
217
- "workLocation": null,
218
- "expenses": null,
219
- "regularHours": null,
220
- "overtime": null,
221
- "mobilisationAllowance": null,
222
- "dailyHousing": null,
223
- "opPipTechnical": null,
224
- "code": null,
225
- "vatBasis": null,
226
- "vatRate": null,
227
- "vatAmount": null,
228
- "totalExclVat": null,
229
- "totalInclVat": null
230
- }
231
- }
232
- ```
233
-
234
- 2. Timesheet:
235
- - Extract time tracking, work details, and approvals.
236
- - JSON structure:
237
- ```json
238
- {
239
- "timesheet": {
240
- "Year": null,
241
- "RPO_Number": null,
242
- "PMC_Name": null,
243
- "Project_Location": null,
244
- "Project_and_Package": null,
245
- "Month": null,
246
- "Timesheet_Details": [
247
- {
248
- "Week": null,
249
- "Regular_Hours": null,
250
- "Overtime_Hours": null,
251
- "Total_Hours": null,
252
- "Comments": null
253
- },
254
- {
255
- "Week": null,
256
- "Regular_Hours": null,
257
- "Overtime_Hours": null,
258
- "Total_Hours": null,
259
- "Comments": null
260
- }
261
- ],
262
- "Monthly_Totals": {
263
- "Regular_Hours": null,
264
- "Overtime_Hours": null,
265
- "Total_Hours": null
266
- },
267
- "reviewedBy": {
268
- "name": null,
269
- "position": null,
270
- "date": null
271
- },
272
- "approvedBy": {
273
- "name": null,
274
- "position": null,
275
- "date": null
276
- }
277
- }
278
- }
279
- ```
280
-
281
- 3. Purchase Order:
282
- - Extract contract and pricing details with precision.
283
- - JSON structure:
284
- ```json
285
- {
286
- "purchaseOrder": {
287
- "contractNo": null,
288
- "relPoNo": null,
289
- "version": null,
290
- "title": null,
291
- "startDate": null,
292
- "endDate": null,
293
- "costCenter": null,
294
- "purchasingGroup": null,
295
- "contractor": null,
296
- "location": null,
297
- "workDescription": null,
298
- "pricing": {
299
- "regularRate": null,
300
- "overtimeRate": null,
301
- "totalBudget": null
302
- }
303
- }
304
- }
305
- ```
306
-
307
- 4. Travel Booking:
308
- - Parse travel-specific and employee information.
309
- - JSON structure:
310
- ```json
311
- {
312
- "travelBooking": {
313
- "requestId": null,
314
- "approvalStatus": null,
315
- "employee": {
316
- "name": null,
317
- "id": null,
318
- "email": null,
319
- "firstName": null,
320
- "lastName": null,
321
- "gradeCodeGroup": null
322
- },
323
- "defaultManager": {
324
- "name": null,
325
- "email": null
326
- },
327
- "sender": {
328
- "name": null,
329
- "email": null
330
- },
331
- "travel": {
332
- "startDate": null,
333
- "endDate": null,
334
- "requestPolicy": null,
335
- "requestType": null,
336
- "employeeType": null,
337
- "travelActivity": null,
338
- "tripType": null
339
- },
340
- "cost": {
341
- "companyCode": null,
342
- "costObject": null,
343
- "costObjectId": null
344
- },
345
- "transport": {
346
- "type": null,
347
- "comments": null
348
- },
349
- "changeRequired": null,
350
- "comments": null
351
- }
352
- }
353
- ```
354
-
355
- Use these structures for parsing documents and ensure compliance with the rules and instructions provided for each type."""
356
  ),
357
  "Aramco Timesheet only": (
358
  """Extract time tracking, work details, and approvals.
359
- - JSON structure:
360
- ```json
361
- {
362
- "timesheet": {
363
- "Year": null,
364
- "RPO_Number": null,
365
- "PMC_Name": null,
366
- "Project_Location": null,
367
- "Project_and_Package": null,
368
- "Month": null,
369
- "Timesheet_Details": [
370
- {
371
- "Week": null,
372
- "Regular_Hours": null,
373
- "Overtime_Hours": null,
374
- "Total_Hours": null,
375
- "Comments": null
376
- },
377
- {
378
- "Week": null,
379
- "Regular_Hours": null,
380
- "Overtime_Hours": null,
381
- "Total_Hours": null,
382
- "Comments": null
383
- }
384
- ],
385
- "Monthly_Totals": {
386
- "Regular_Hours": null,
387
- "Overtime_Hours": null,
388
- "Total_Hours": null
389
- },
390
- "reviewedBy": {
391
- "name": null,
392
- "position": null,
393
- "date": null
394
- },
395
- "approvedBy": {
396
- "name": null,
397
- "position": null,
398
- "date": null
399
- }
400
- }
401
- }
402
- ```"""
403
  ),
404
  "NOC Invoice": (
405
- """You are a highly accurate data extraction system. Your task is to analyze the provided image of an invoice and extract all data, paying close attention to the structure and formatting of the document. Organize the extracted data in a clear, structured format, such as JSON. Do not invent any information. If a field cannot be read with high confidence, indicate that with "UNCLEAR" or a similar designation. Be as specific as possible, and do not summarize or combine fields unless explicitly indicated.
406
-
407
- Here's the expected output format, in JSON, with all required fields:
408
-
409
- ```json
410
  {
411
- "invoiceDetails": {
412
- "pleaseQuote": "string",
413
- "invoiceNumber": "string",
414
- "workPeriod": "string",
415
- "invoiceDate": "string",
416
- "assignmentReference": "string"
417
- },
418
- "from": {
419
- "companyName": "string",
420
- "addressLine1": "string",
421
- "addressLine2": "string",
422
- "city": "string",
423
- "postalCode": "string",
424
- "country": "string"
425
- },
426
- "to": {
427
- "companyName": "string",
428
- "office": "string",
429
- "floor": "string",
430
- "building": "string",
431
- "addressLine1": "string",
432
- "poBox": "string",
433
- "city": "string"
434
- },
435
- "services": [
436
- {
437
- "serviceDetails": "string",
438
- "fromDate": "string",
439
- "toDate": "string",
440
- "currency": "string",
441
- "fx": "string",
442
- "noOfDays": "number or string (if range)",
443
- "rate": "number",
444
- "total": "number"
445
- }
446
- ],
447
- "totals": {
448
- "subTotal": "number",
449
- "tax": "number",
450
- "totalDue": "number"
451
- },
452
- "bankDetails": {
453
- "bankName": "string",
454
- "descriptionReferenceField": "string",
455
- "bankAddress": "string",
456
- "swiftBicCode": "string",
457
- "ibanNumber": "string",
458
- "accountNumber": "string",
459
- "beneficiaryName": "string",
460
- "accountCurrency": "string",
461
- "expectedAmount": "string"
462
- }
463
  }
464
- ```"""
465
  )
466
  }
467
 
468
- # Retrieve the selected prompt
469
  selected_prompt = prompts.get(prompt_option, "Invalid prompt selected.")
470
  context = ""
471
- if doc_state.current_doc_images:
472
- if doc_state.current_doc_text:
473
- context = f"\nDocument context:\n{doc_state.current_doc_text}"
474
  full_prompt = selected_prompt + context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
- # Create the messages list for the API call
477
- messages = [{"role": "user", "content": full_prompt}]
478
-
479
- # If an image is available, encode it in base64 and append to the prompt
480
  if doc_state.current_doc_images:
481
  buffered = io.BytesIO()
482
  doc_state.current_doc_images[0].save(buffered, format="PNG")
483
- img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
484
- messages[0]["content"] += f"\n[Image Data: {img_str}]"
 
 
 
 
 
485
 
486
- # Call the OpenAI API with streaming enabled.
487
- response = openai.ChatCompletion.create(
488
  model="qwen/qwen2.5-vl-72b-instruct:free",
489
  messages=messages,
490
  max_tokens=max_new_tokens,
491
- stream=True,
492
  )
493
 
494
  buffer = ""
495
- for chunk in response:
496
- if 'choices' in chunk:
497
- delta = chunk['choices'][0].get('delta', {})
498
- content = delta.get('content', '')
499
- buffer += content
500
- time.sleep(0.01)
501
- yield buffer
502
-
503
  except Exception as e:
504
  logger.error(f"Error in bot_streaming: {str(e)}")
505
  yield "An error occurred while processing your request. Please try again."
@@ -521,48 +279,21 @@ with gr.Blocks() as demo:
521
  label="Upload Document",
522
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"]
523
  )
524
- upload_status = gr.Textbox(
525
- label="Upload Status",
526
- interactive=False
527
- )
528
 
529
  with gr.Row():
530
  prompt_dropdown = gr.Dropdown(
531
  label="Select Prompt",
532
- choices=[
533
- "NOC Timesheet",
534
- "NOC Basic",
535
- "Aramco Full structured",
536
- "Aramco Timesheet only",
537
- "NOC Invoice"
538
- ],
539
  value="NOC Timesheet"
540
  )
541
  generate_btn = gr.Button("Generate")
542
 
543
  clear_btn = gr.Button("Clear Document Context")
 
544
 
545
- output_text = gr.Textbox(
546
- label="Output",
547
- interactive=False
548
- )
549
-
550
- file_upload.change(
551
- fn=process_uploaded_file,
552
- inputs=[file_upload],
553
- outputs=[upload_status]
554
- )
555
-
556
- generate_btn.click(
557
- fn=bot_streaming,
558
- inputs=[prompt_dropdown],
559
- outputs=[output_text]
560
- )
561
-
562
- clear_btn.click(
563
- fn=clear_context,
564
- outputs=[upload_status]
565
- )
566
 
567
- # Launch the interface
568
  demo.launch(debug=True)
 
1
+ import os
 
2
  import io
3
  import time
4
+ import base64
5
  import logging
6
  import fitz # PyMuPDF
7
  from PIL import Image
8
  import gradio as gr
9
+ from openai import OpenAI # Use the OpenAI client that supports multimodal messages
10
+
11
+ # Load API key from environment variable (secrets)
12
+ HF_API_KEY = os.getenv("OPENAI_TOKEN")
13
+ if not HF_API_KEY:
14
+ raise ValueError("HF_API_KEY environment variable not set")
15
+
16
+ # Create the client pointing to the Hugging Face Inference endpoint
17
+ client = OpenAI(
18
+ base_url="https://openrouter.ai/api/v1",
19
+ api_key=HF_API_KEY
20
+ )
21
 
22
  # Set up logging
23
  logging.basicConfig(level=logging.INFO)
24
  logger = logging.getLogger(__name__)
25
 
 
 
 
 
 
 
 
 
 
 
26
  # -------------------------------
27
  # Document State and File Processing
28
  # -------------------------------
 
40
  doc_state = DocumentState()
41
 
42
  def process_pdf_file(file_path):
43
+ """Convert PDF pages to images and extract text using PyMuPDF."""
44
  try:
45
  doc = fitz.open(file_path)
46
  images = []
 
52
  if page_text.strip():
53
  text += f"Page {page_num + 1}:\n{page_text}\n\n"
54
 
55
+ # Render page as an image with a zoom factor
56
  zoom = 3
57
  mat = fitz.Matrix(zoom, zoom)
58
  pix = page.get_pixmap(matrix=mat, alpha=False)
59
  img_data = pix.tobytes("png")
60
+ img = Image.open(io.BytesIO(img_data)).convert("RGB")
 
61
 
62
  # Resize if image is too large
63
  max_size = 1600
 
65
  ratio = max_size / max(img.size)
66
  new_size = tuple(int(dim * ratio) for dim in img.size)
67
  img = img.resize(new_size, Image.Resampling.LANCZOS)
 
68
  images.append(img)
69
  except Exception as e:
70
  logger.error(f"Error processing page {page_num}: {str(e)}")
 
78
  raise
79
 
80
  def process_uploaded_file(file):
81
+ """Process an uploaded file (PDF or image) and update document state."""
82
  try:
83
  doc_state.clear()
84
  if file is None:
85
  return "No file uploaded. Please upload a file."
86
 
87
+ # Get the file path from the Gradio upload (may be a dict or file-like object)
88
  if isinstance(file, dict):
89
  file_path = file["name"]
90
  else:
 
119
  return "An error occurred while processing the file. Please try again."
120
 
121
  # -------------------------------
122
+ # Bot Streaming Function Using the Multimodal API
123
  # -------------------------------
124
+ def bot_streaming(prompt_option, max_new_tokens=500):
125
  """
126
+ Build a multimodal message payload and call the inference API.
127
+ The payload includes:
128
+ - A text segment (the selected prompt and any document context).
129
+ - If available, an image as a data URI (using a base64-encoded PNG).
130
  """
131
  try:
132
+ # Predetermined prompts (you can adjust these as needed)
133
  prompts = {
134
  "NOC Timesheet": (
135
  """Extract structured information from the provided timesheet. The extracted details should include:
 
174
 
175
  Noc representative status as approval_status
176
 
177
+ Format the output as valid JSON.
178
+ """
 
179
  ),
180
  "NOC Basic": (
181
  "Based on the provided timesheet details, extract the following information:\n"
182
+ " - Full name\n"
183
+ " - Position title\n"
184
  " - Work location\n"
185
  " - Contractor's name\n"
186
  " - NOC ID\n"
187
+ " - Month and year (MM/YYYY)"
188
  ),
189
  "Aramco Full structured": (
190
+ """You are a document parsing assistant designed to extract structured data from various documents such as invoices, timesheets, purchase orders, and travel bookings. Return only valid JSON with no extra text.
191
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  ),
193
  "Aramco Timesheet only": (
194
  """Extract time tracking, work details, and approvals.
195
+ Return a JSON object following the specified structure.
196
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  ),
198
  "NOC Invoice": (
199
+ """You are a highly accurate data extraction system. Analyze the provided invoice image and extract all data into the following JSON format:
 
 
 
 
200
  {
201
+ "invoiceDetails": { ... },
202
+ "from": { ... },
203
+ "to": { ... },
204
+ "services": [ ... ],
205
+ "totals": { ... },
206
+ "bankDetails": { ... }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  }
208
+ """
209
  )
210
  }
211
 
212
+ # Select the appropriate prompt
213
  selected_prompt = prompts.get(prompt_option, "Invalid prompt selected.")
214
  context = ""
215
+ if doc_state.current_doc_images and doc_state.current_doc_text:
216
+ context = "\nDocument context:\n" + doc_state.current_doc_text
 
217
  full_prompt = selected_prompt + context
218
+
219
+ # Build the message payload in the expected format.
220
+ # The content field is a list of objects—one for text, and (if an image is available) one for the image.
221
+ messages = [
222
+ {
223
+ "role": "user",
224
+ "content": [
225
+ {
226
+ "type": "text",
227
+ "text": full_prompt
228
+ }
229
+ ]
230
+ }
231
+ ]
232
 
233
+ # If an image is available, encode it as a data URI and append it as an image_url message.
 
 
 
234
  if doc_state.current_doc_images:
235
  buffered = io.BytesIO()
236
  doc_state.current_doc_images[0].save(buffered, format="PNG")
237
+ img_b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
238
+ # Create a data URI (many APIs accept this format in place of a public URL)
239
+ data_uri = f"data:image/png;base64,{img_b64}"
240
+ messages[0]["content"].append({
241
+ "type": "image_url",
242
+ "image_url": {"url": data_uri}
243
+ })
244
 
245
+ # Call the inference API with streaming enabled.
246
+ stream = client.chat.completions.create(
247
  model="qwen/qwen2.5-vl-72b-instruct:free",
248
  messages=messages,
249
  max_tokens=max_new_tokens,
250
+ stream=True
251
  )
252
 
253
  buffer = ""
254
+ for chunk in stream:
255
+ # The response structure is similar to the reference: each chunk contains a delta.
256
+ delta = chunk.choices[0].delta.content
257
+ buffer += delta
258
+ time.sleep(0.01)
259
+ yield buffer
260
+
 
261
  except Exception as e:
262
  logger.error(f"Error in bot_streaming: {str(e)}")
263
  yield "An error occurred while processing your request. Please try again."
 
279
  label="Upload Document",
280
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"]
281
  )
282
+ upload_status = gr.Textbox(label="Upload Status", interactive=False)
 
 
 
283
 
284
  with gr.Row():
285
  prompt_dropdown = gr.Dropdown(
286
  label="Select Prompt",
287
+ choices=["NOC Timesheet", "NOC Basic", "Aramco Full structured", "Aramco Timesheet only", "NOC Invoice"],
 
 
 
 
 
 
288
  value="NOC Timesheet"
289
  )
290
  generate_btn = gr.Button("Generate")
291
 
292
  clear_btn = gr.Button("Clear Document Context")
293
+ output_text = gr.Textbox(label="Output", interactive=False)
294
 
295
+ file_upload.change(fn=process_uploaded_file, inputs=[file_upload], outputs=[upload_status])
296
+ generate_btn.click(fn=bot_streaming, inputs=[prompt_dropdown], outputs=[output_text])
297
+ clear_btn.click(fn=clear_context, outputs=[upload_status])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
 
299
  demo.launch(debug=True)