deveos commited on
Commit
28b96f2
·
verified ·
1 Parent(s): 9237ea3

Upload 7 files

Browse files
Files changed (3) hide show
  1. app/google_ai_mode.py +4 -0
  2. app/jobs.py +34 -1
  3. app/main.py +52 -1
app/google_ai_mode.py CHANGED
@@ -13,6 +13,7 @@ class PageRequest(NamedTuple):
13
  page_number: int
14
  image_path: Path
15
  prompt: str
 
16
 
17
 
18
  class GoogleAiModeClient:
@@ -405,6 +406,9 @@ async def _run_single_page_async(
405
  await _enter_prompt_async(page, request.prompt)
406
  await _submit_async(page)
407
  await page.wait_for_timeout(5_000)
 
 
 
408
  return await _wait_for_json_response_async(page, response_timeout_seconds)
409
  finally:
410
  await page.close()
 
13
  page_number: int
14
  image_path: Path
15
  prompt: str
16
+ screenshot_path: Path | None = None
17
 
18
 
19
  class GoogleAiModeClient:
 
406
  await _enter_prompt_async(page, request.prompt)
407
  await _submit_async(page)
408
  await page.wait_for_timeout(5_000)
409
+ if request.screenshot_path:
410
+ request.screenshot_path.parent.mkdir(parents=True, exist_ok=True)
411
+ await page.screenshot(path=str(request.screenshot_path), full_page=False)
412
  return await _wait_for_json_response_async(page, response_timeout_seconds)
413
  finally:
414
  await page.close()
app/jobs.py CHANGED
@@ -22,6 +22,10 @@ class JobState:
22
  total_batches: int = 0
23
  completed_batches: int = 0
24
  current_pages: str | None = None
 
 
 
 
25
  output_file: str | None = None
26
  error: str | None = None
27
  run_dir: str | None = None
@@ -88,9 +92,11 @@ def _run_job(
88
  image_dir = run_dir / "pages"
89
  batch_dir = run_dir / "batch-json"
90
  raw_dir = run_dir / "raw-output"
 
91
  browser_profile_dir = (run_dir / "browser-profile").resolve()
92
  batch_dir.mkdir(parents=True, exist_ok=True)
93
  raw_dir.mkdir(parents=True, exist_ok=True)
 
94
 
95
  jobs.update(
96
  job_id,
@@ -119,12 +125,15 @@ def _run_job(
119
  message=f"Wave {index}/{len(batches)}: opening AI Mode tabs for pages {current_pages}",
120
  completed_batches=index - 1,
121
  current_pages=current_pages,
 
122
  progress=15 + int(((index - 1) / max(len(batches), 1)) * 70),
123
  )
124
 
125
  requests = []
 
126
  for image_path in batch:
127
  page_number = images.index(image_path) + 1
 
128
  prompt = (
129
  f"{BANK_STATEMENT_PROMPT}\n\n"
130
  "The JSON block above is only the required output format. "
@@ -134,7 +143,15 @@ def _run_job(
134
  f"The uploaded image is PDF page {page_number}. "
135
  "Extract transactions only from this uploaded page image."
136
  )
137
- requests.append(PageRequest(page_number, image_path, prompt))
 
 
 
 
 
 
 
 
138
 
139
  wave_profile_dir = (browser_profile_dir / f"wave-{index:03d}").resolve()
140
  with GoogleAiModeClient(ai_mode_url, wave_profile_dir) as client:
@@ -146,6 +163,13 @@ def _run_job(
146
  progress=15 + int(((index - 0.25) / max(len(batches), 1)) * 70),
147
  )
148
  for page_number in sorted(responses):
 
 
 
 
 
 
 
149
  raw_response = responses[page_number]
150
  raw_file = raw_dir / f"page-{page_number:04d}-raw.txt"
151
  raw_file.write_text(raw_response, encoding="utf-8")
@@ -156,9 +180,17 @@ def _run_job(
156
  encoding="utf-8",
157
  )
158
  page_json_files[page_number] = page_file
 
 
 
 
 
 
 
159
  jobs.update(
160
  job_id,
161
  completed_batches=index,
 
162
  message=f"Saved page wave {index} of {len(batches)}",
163
  progress=15 + int((index / max(len(batches), 1)) * 70),
164
  )
@@ -173,6 +205,7 @@ def _run_job(
173
  progress=100,
174
  output_file=str(final_json),
175
  current_pages=None,
 
176
  data_preview=combined["data"][:20],
177
  )
178
  except Exception as exc:
 
22
  total_batches: int = 0
23
  completed_batches: int = 0
24
  current_pages: str | None = None
25
+ active_tabs: int = 0
26
+ completed_pages: int = 0
27
+ page_statuses: dict[str, str] = field(default_factory=dict)
28
+ latest_screenshot: str | None = None
29
  output_file: str | None = None
30
  error: str | None = None
31
  run_dir: str | None = None
 
92
  image_dir = run_dir / "pages"
93
  batch_dir = run_dir / "batch-json"
94
  raw_dir = run_dir / "raw-output"
95
+ browser_view_dir = run_dir / "browser-view"
96
  browser_profile_dir = (run_dir / "browser-profile").resolve()
97
  batch_dir.mkdir(parents=True, exist_ok=True)
98
  raw_dir.mkdir(parents=True, exist_ok=True)
99
+ browser_view_dir.mkdir(parents=True, exist_ok=True)
100
 
101
  jobs.update(
102
  job_id,
 
125
  message=f"Wave {index}/{len(batches)}: opening AI Mode tabs for pages {current_pages}",
126
  completed_batches=index - 1,
127
  current_pages=current_pages,
128
+ active_tabs=len(batch),
129
  progress=15 + int(((index - 1) / max(len(batches), 1)) * 70),
130
  )
131
 
132
  requests = []
133
+ page_statuses = dict(jobs.get(job_id).page_statuses if jobs.get(job_id) else {})
134
  for image_path in batch:
135
  page_number = images.index(image_path) + 1
136
+ page_statuses[str(page_number)] = "queued"
137
  prompt = (
138
  f"{BANK_STATEMENT_PROMPT}\n\n"
139
  "The JSON block above is only the required output format. "
 
143
  f"The uploaded image is PDF page {page_number}. "
144
  "Extract transactions only from this uploaded page image."
145
  )
146
+ requests.append(
147
+ PageRequest(
148
+ page_number,
149
+ image_path,
150
+ prompt,
151
+ browser_view_dir / f"page-{page_number:04d}.png",
152
+ )
153
+ )
154
+ jobs.update(job_id, page_statuses=page_statuses)
155
 
156
  wave_profile_dir = (browser_profile_dir / f"wave-{index:03d}").resolve()
157
  with GoogleAiModeClient(ai_mode_url, wave_profile_dir) as client:
 
163
  progress=15 + int(((index - 0.25) / max(len(batches), 1)) * 70),
164
  )
165
  for page_number in sorted(responses):
166
+ page_statuses[str(page_number)] = "parsing"
167
+ screenshot_path = browser_view_dir / f"page-{page_number:04d}.png"
168
+ jobs.update(
169
+ job_id,
170
+ page_statuses=page_statuses,
171
+ latest_screenshot=str(screenshot_path) if screenshot_path.exists() else None,
172
+ )
173
  raw_response = responses[page_number]
174
  raw_file = raw_dir / f"page-{page_number:04d}-raw.txt"
175
  raw_file.write_text(raw_response, encoding="utf-8")
 
180
  encoding="utf-8",
181
  )
182
  page_json_files[page_number] = page_file
183
+ page_statuses[str(page_number)] = "done"
184
+ jobs.update(
185
+ job_id,
186
+ completed_pages=len(page_json_files),
187
+ page_statuses=page_statuses,
188
+ latest_screenshot=str(screenshot_path) if screenshot_path.exists() else None,
189
+ )
190
  jobs.update(
191
  job_id,
192
  completed_batches=index,
193
+ active_tabs=0,
194
  message=f"Saved page wave {index} of {len(batches)}",
195
  progress=15 + int((index / max(len(batches), 1)) * 70),
196
  )
 
205
  progress=100,
206
  output_file=str(final_json),
207
  current_pages=None,
208
+ active_tabs=0,
209
  data_preview=combined["data"][:20],
210
  )
211
  except Exception as exc:
app/main.py CHANGED
@@ -6,6 +6,7 @@ from pathlib import Path
6
  from tempfile import NamedTemporaryFile
7
 
8
  from fastapi import FastAPI, File, Form, HTTPException, UploadFile
 
9
  from fastapi.responses import FileResponse
10
  from fastapi.staticfiles import StaticFiles
11
 
@@ -21,14 +22,45 @@ DEFAULT_BATCH_SIZE = int(os.getenv("PARALLEL_TABS", "10"))
21
  RUNS_DIR.mkdir(exist_ok=True)
22
 
23
  app = FastAPI(title="Bank Statement AI Mode Extractor")
 
 
 
 
 
 
 
 
 
 
 
24
  app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
25
 
26
 
27
  @app.get("/")
28
- def index() -> FileResponse:
 
 
29
  return FileResponse(STATIC_DIR / "index.html")
30
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  @app.post("/api/jobs")
33
  async def create_job(
34
  pdf: UploadFile = File(...),
@@ -59,6 +91,25 @@ def get_job(job_id: str) -> dict:
59
  return asdict(job)
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  @app.get("/api/jobs/{job_id}/download")
63
  def download_job(job_id: str) -> FileResponse:
64
  job = jobs.get(job_id)
 
6
  from tempfile import NamedTemporaryFile
7
 
8
  from fastapi import FastAPI, File, Form, HTTPException, UploadFile
9
+ from fastapi.middleware.cors import CORSMiddleware
10
  from fastapi.responses import FileResponse
11
  from fastapi.staticfiles import StaticFiles
12
 
 
22
  RUNS_DIR.mkdir(exist_ok=True)
23
 
24
  app = FastAPI(title="Bank Statement AI Mode Extractor")
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=[
28
+ origin.strip()
29
+ for origin in os.getenv("CORS_ORIGINS", "*").split(",")
30
+ if origin.strip()
31
+ ],
32
+ allow_credentials=False,
33
+ allow_methods=["*"],
34
+ allow_headers=["*"],
35
+ )
36
  app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
37
 
38
 
39
  @app.get("/")
40
+ def index():
41
+ if os.getenv("API_ONLY", "false").strip().lower() in {"1", "true", "yes"}:
42
+ return FileResponse(STATIC_DIR / "backend.html")
43
  return FileResponse(STATIC_DIR / "index.html")
44
 
45
 
46
+ @app.get("/api/info")
47
+ def api_info() -> dict[str, str]:
48
+ return {
49
+ "name": "Bank Statement AI Mode Extractor API",
50
+ "health": "/api/health",
51
+ "create_job": "POST /api/jobs with multipart fields pdf and optional pwd",
52
+ "job_status": "/api/jobs/{job_id}",
53
+ "latest_browser_view": "/api/jobs/{job_id}/browser-view/latest",
54
+ "download_json": "/api/jobs/{job_id}/download",
55
+ "download_csv": "/api/jobs/{job_id}/download.csv",
56
+ }
57
+
58
+
59
+ @app.get("/api/health")
60
+ def health() -> dict[str, str]:
61
+ return {"status": "ok"}
62
+
63
+
64
  @app.post("/api/jobs")
65
  async def create_job(
66
  pdf: UploadFile = File(...),
 
91
  return asdict(job)
92
 
93
 
94
+ @app.get("/api/jobs/{job_id}/browser-view/latest")
95
+ def latest_browser_view(job_id: str) -> FileResponse:
96
+ job = jobs.get(job_id)
97
+ if not job or not job.latest_screenshot:
98
+ raise HTTPException(status_code=404, detail="Browser screenshot is not ready.")
99
+ return FileResponse(job.latest_screenshot, media_type="image/png")
100
+
101
+
102
+ @app.get("/api/jobs/{job_id}/browser-view/page/{page_number}")
103
+ def page_browser_view(job_id: str, page_number: int) -> FileResponse:
104
+ job = jobs.get(job_id)
105
+ if not job or not job.run_dir:
106
+ raise HTTPException(status_code=404, detail="Job not found.")
107
+ screenshot = Path(job.run_dir) / "browser-view" / f"page-{page_number:04d}.png"
108
+ if not screenshot.exists():
109
+ raise HTTPException(status_code=404, detail="Browser screenshot is not ready.")
110
+ return FileResponse(screenshot, media_type="image/png")
111
+
112
+
113
  @app.get("/api/jobs/{job_id}/download")
114
  def download_job(job_id: str) -> FileResponse:
115
  job = jobs.get(job_id)