File size: 8,621 Bytes
8160245
 
 
 
166e4a6
8160245
 
 
 
 
 
 
 
 
 
 
 
 
 
f4c95fc
 
8160245
166e4a6
 
 
 
8160245
 
 
 
 
166e4a6
 
8160245
f4c95fc
8160245
 
 
 
 
 
 
 
166e4a6
f4c95fc
 
 
8160245
f4c95fc
8160245
 
f4c95fc
8160245
 
 
 
 
 
 
f4c95fc
8160245
 
f4c95fc
 
 
8160245
 
 
 
f4c95fc
8160245
f4c95fc
8160245
 
 
f4c95fc
 
 
8160245
 
 
 
f4c95fc
8160245
 
 
f4c95fc
 
 
8160245
 
 
 
f4c95fc
8160245
 
 
 
166e4a6
8160245
 
 
166e4a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8160245
f4c95fc
8160245
 
 
 
 
 
 
 
 
 
 
f4c95fc
 
8160245
 
f4c95fc
8160245
f4c95fc
8160245
 
 
f4c95fc
8160245
 
 
f4c95fc
8160245
 
f4c95fc
8160245
 
 
 
 
 
 
f4c95fc
8160245
f4c95fc
8160245
 
f4c95fc
8160245
 
 
 
 
166e4a6
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# app.py
import os
import io
import json
import time
import traceback
from typing import List
from io import BytesIO

from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv

import PyPDF2
import openai
import requests
from pdf2image import convert_from_bytes
from google.cloud import vision_v1 as vision

# ───── Load .env (locally) ────────────────────────────────────
# In HF Spaces you'll set these as Secrets under Settings β†’ Variables & secrets
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
gcv_api_key = os.getenv("GCV_API_KEY")
brightdata_api_key = os.getenv("BRIGHTDATA_API_KEY")
brightdata_dataset_id = "gd_l1viktl72bvl7bjuj0"

if not openai.api_key:
    raise RuntimeError("Missing OPENAI_API_KEY")
if not gcv_api_key:
    raise RuntimeError("Missing GCV_API_KEY")
if not brightdata_api_key:
    raise RuntimeError("Missing BRIGHTDATA_API_KEY")

# ───── FastAPI setup ──────────────────────────────────────────
app = FastAPI(title="Aliro Data Extraction API")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# ───── A simple root so GET / won't 404 ───────────────────────
@app.get("/")
def read_root():
    return {"message": "Aliro Data Extraction API – POST your files to /extract"}

# ───── PDF/OCR processing ─────────────────────────────────────
def process_buffers(buffers: List[BytesIO]) -> List[str]:
    client = vision.ImageAnnotatorClient(client_options={"api_key": gcv_api_key})
    results: List[str] = []

    for buf in buffers:
        name = getattr(buf, "name", "").lower()
        data = buf.getvalue()

        # PDF: try text β†’ fallback to page-by-page OCR
        if name.endswith(".pdf"):
            # 1) PyPDF2 text
            try:
                reader = PyPDF2.PdfReader(io.BytesIO(data))
                text = "".join(page.extract_text() or "" for page in reader.pages)
                if text.strip():
                    results.append(text)
                    continue
            except:
                pass

            # 2) Fallback: render pages β†’ OCR
            try:
                for img in convert_from_bytes(data):
                    img_buf = io.BytesIO()
                    img.save(img_buf, format="JPEG")
                    resp = client.text_detection(image=vision.Image(content=img_buf.getvalue()))
                    desc = resp.text_annotations[0].description if resp.text_annotations else ""
                    if desc:
                        results.append(desc)
                continue
            except:
                pass

        # Image β†’ always OCR
        if name.endswith((".png", ".jpg", ".jpeg")):
            try:
                resp = client.text_detection(image=vision.Image(content=data))
                desc = resp.text_annotations[0].description if resp.text_annotations else ""
                if desc:
                    results.append(desc)
                    continue
            except:
                pass

        # Otherwise unsupported
        results.append(f"[Unsupported file type: {name}]")

    return results

# ───── LinkedIn scrape via BrightData ─────────────────────────
def scrape_linkedin(url: str) -> str:
    if not url:
        return ""
        
    try:
        # Step 1: Trigger data collection
        trigger_url = "https://api.brightdata.com/datasets/v3/trigger"
        headers = {
            "Authorization": f"Bearer {brightdata_api_key}",
            "Content-Type": "application/json",
        }
        params = {
            "dataset_id": brightdata_dataset_id,
            "include_errors": "true",
        }
        
        trigger_response = requests.post(
            trigger_url,
            headers=headers,
            params=params,
            json=[{"url": url}],
            timeout=30
        )
        
        if not trigger_response.ok:
            raise Exception(f"BrightData trigger failed: {trigger_response.status_code} - {trigger_response.text}")
            
        trigger_data = trigger_response.json()
        snapshot_id = trigger_data.get('snapshot_id')
        
        if not snapshot_id:
            raise Exception("No snapshot_id received from BrightData")
            
        print(f"BrightData collection triggered, snapshot_id: {snapshot_id}")
        
        # Step 2: Poll for completion (max 5 minutes)
        max_attempts = 30
        for attempt in range(max_attempts):
            time.sleep(10)  # Wait 10 seconds between checks
            
            progress_url = f"https://api.brightdata.com/datasets/v3/progress/{snapshot_id}"
            progress_response = requests.get(progress_url, headers=headers, timeout=30)
            
            if progress_response.ok:
                progress_data = progress_response.json()
                status = progress_data.get('status')
                
                print(f"Progress check {attempt + 1}: status = {status}")
                
                if status == 'ready':
                    # Step 3: Fetch results
                    result_url = f"https://api.brightdata.com/datasets/v3/snapshot/{snapshot_id}"
                    result_params = {"format": "json"}
                    result_response = requests.get(result_url, headers=headers, params=result_params, timeout=30)
                    
                    if result_response.ok:
                        linkedin_data = result_response.json()
                        return json.dumps(linkedin_data, indent=2)
                    else:
                        raise Exception(f"Failed to fetch results: {result_response.status_code} - {result_response.text}")
                        
                elif status == 'failed':
                    raise Exception("BrightData collection failed")
                    
                # Continue polling if status is 'running'
            else:
                print(f"Progress check failed: {progress_response.status_code}")
                
        raise Exception("Timeout waiting for BrightData collection to complete")
        
    except Exception as e:
        raise Exception(f"LinkedIn scraping error: {str(e)}")

# ───── Summarize via OpenAI ─────────────────────────────────
def make_summary(chunks: List[str]) -> str:
    if not chunks:
        return "No data extracted."
    prompt = "\n\n---\n\n".join(chunks)
    resp = openai.ChatCompletion.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": (
                    "Extract and structure personal, educational, and professional details "
                    "into a clear, logical and relational hierarchy. Translate non-English "
                    "content, remove duplicates, and output in English."
                )
            },
            {"role": "user", "content": f"Summarize the following data:\n\n{prompt}"},
        ],
        max_tokens=1500,
    )
    return resp.choices[0].message.content.strip()

# ───── The POST /extract endpoint ─────────────────────────────
@app.post("/extract")
async def extract_endpoint(
    files: List[UploadFile] = File(default=[]),
    linkedin_url: str       = Form(default="")
):
    try:
        # Wrap each UploadFile in a BytesIO (so we can peek at .name & .getvalue())
        buffers: List[BytesIO] = []
        for f in files:
            data = await f.read()
            bio = BytesIO(data)
            bio.name = f.filename
            buffers.append(bio)

        # 1) OCR/PDF text
        texts = process_buffers(buffers)
        # 2) LinkedIn JSON
        if linkedin_url:
            texts.append(scrape_linkedin(linkedin_url))
        # 3) Summarize
        summary = make_summary(texts)
        return {"summary": summary}

    except requests.HTTPError as e:
        raise HTTPException(status_code=502, detail=f"LinkedIn scrape failed: {e}")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")