Spaces:
Sleeping
Sleeping
Commit ·
beedba5
1
Parent(s): 412135f
refactor(backend): extract process_reel_url from extract_url handler
Browse filesPrereq for the Supabase queue worker — same pipeline, callable in-process.
- main.py +18 -25
- verify_pipeline.py +31 -0
main.py
CHANGED
|
@@ -600,17 +600,13 @@ def list_clusters():
|
|
| 600 |
logger.error(f"Failed to list clusters: {str(e)}")
|
| 601 |
raise HTTPException(status_code=500, detail=f"Failed to list clusters: {str(e)}")
|
| 602 |
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
"""
|
| 606 |
-
Accepts Instagram Reel URL. Downloads video, extracts audio, transcribes audio,
|
| 607 |
-
combines description with transcript, extracts JSON highlights, and saves to database.
|
| 608 |
-
"""
|
| 609 |
-
url = payload.get("url")
|
| 610 |
-
if not url:
|
| 611 |
-
raise HTTPException(status_code=400, detail="Missing required 'url' parameter.")
|
| 612 |
|
| 613 |
-
|
|
|
|
|
|
|
|
|
|
| 614 |
try:
|
| 615 |
conn = sqlite3.connect(DB_PATH)
|
| 616 |
cursor = conn.cursor()
|
|
@@ -626,41 +622,29 @@ async def extract_url(payload: dict):
|
|
| 626 |
"raw_transcript": row[3],
|
| 627 |
"post_caption": row[4],
|
| 628 |
"extracted_json": json.loads(row[5]),
|
| 629 |
-
"created_at": row[6]
|
| 630 |
}
|
| 631 |
except Exception as e:
|
| 632 |
logger.warning(f"Failed to check existing SQLite URL cache: {str(e)}")
|
| 633 |
|
| 634 |
mp3_path = None
|
| 635 |
try:
|
| 636 |
-
# 1. Download video and extract audio MP3 & caption description
|
| 637 |
mp3_path, post_caption, title = download_and_extract_audio(url)
|
| 638 |
-
|
| 639 |
-
# 2. Transcribe Audio
|
| 640 |
raw_transcript = ""
|
| 641 |
try:
|
| 642 |
raw_transcript = transcribe_audio(mp3_path)
|
| 643 |
except Exception as e:
|
| 644 |
logger.warning(f"Transcription failed: {str(e)}. Proceeding using metadata/caption only.")
|
| 645 |
-
|
| 646 |
-
# 3. Check for empty transcription/silent hook
|
| 647 |
guard_silent_hook(raw_transcript, post_caption)
|
| 648 |
-
|
| 649 |
-
# 4. Extract structured insights
|
| 650 |
extracted_data = extract_structured_json(raw_transcript, post_caption)
|
| 651 |
-
|
| 652 |
-
# 5. Commit record to database
|
| 653 |
-
db_record = save_to_database(
|
| 654 |
url=url,
|
| 655 |
title=title,
|
| 656 |
raw_transcript=raw_transcript,
|
| 657 |
post_caption=post_caption,
|
| 658 |
-
extracted=extracted_data
|
| 659 |
)
|
| 660 |
-
|
| 661 |
-
return db_record
|
| 662 |
finally:
|
| 663 |
-
# Cleanup temporary audio files
|
| 664 |
if mp3_path and os.path.exists(mp3_path):
|
| 665 |
try:
|
| 666 |
os.remove(mp3_path)
|
|
@@ -668,6 +652,15 @@ async def extract_url(payload: dict):
|
|
| 668 |
except Exception as ce:
|
| 669 |
logger.warning(f"Could not delete temp file {mp3_path}: {str(ce)}")
|
| 670 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
@app.post("/extract/file", response_model=ExtractionResponse)
|
| 672 |
async def extract_file(
|
| 673 |
file: UploadFile = File(...),
|
|
|
|
| 600 |
logger.error(f"Failed to list clusters: {str(e)}")
|
| 601 |
raise HTTPException(status_code=500, detail=f"Failed to list clusters: {str(e)}")
|
| 602 |
|
| 603 |
+
def process_reel_url(url: str) -> dict:
|
| 604 |
+
"""Full single-reel pipeline. Returns the saved DB record.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
|
| 606 |
+
Returns the cached row if this URL was already processed. Raises
|
| 607 |
+
HTTPException(400) on silent-hook reels (no spoken content). Always
|
| 608 |
+
cleans up the temp audio file. Used by /extract/url and the queue worker."""
|
| 609 |
+
# Cache: skip inference if we already have this URL
|
| 610 |
try:
|
| 611 |
conn = sqlite3.connect(DB_PATH)
|
| 612 |
cursor = conn.cursor()
|
|
|
|
| 622 |
"raw_transcript": row[3],
|
| 623 |
"post_caption": row[4],
|
| 624 |
"extracted_json": json.loads(row[5]),
|
| 625 |
+
"created_at": row[6],
|
| 626 |
}
|
| 627 |
except Exception as e:
|
| 628 |
logger.warning(f"Failed to check existing SQLite URL cache: {str(e)}")
|
| 629 |
|
| 630 |
mp3_path = None
|
| 631 |
try:
|
|
|
|
| 632 |
mp3_path, post_caption, title = download_and_extract_audio(url)
|
|
|
|
|
|
|
| 633 |
raw_transcript = ""
|
| 634 |
try:
|
| 635 |
raw_transcript = transcribe_audio(mp3_path)
|
| 636 |
except Exception as e:
|
| 637 |
logger.warning(f"Transcription failed: {str(e)}. Proceeding using metadata/caption only.")
|
|
|
|
|
|
|
| 638 |
guard_silent_hook(raw_transcript, post_caption)
|
|
|
|
|
|
|
| 639 |
extracted_data = extract_structured_json(raw_transcript, post_caption)
|
| 640 |
+
return save_to_database(
|
|
|
|
|
|
|
| 641 |
url=url,
|
| 642 |
title=title,
|
| 643 |
raw_transcript=raw_transcript,
|
| 644 |
post_caption=post_caption,
|
| 645 |
+
extracted=extracted_data,
|
| 646 |
)
|
|
|
|
|
|
|
| 647 |
finally:
|
|
|
|
| 648 |
if mp3_path and os.path.exists(mp3_path):
|
| 649 |
try:
|
| 650 |
os.remove(mp3_path)
|
|
|
|
| 652 |
except Exception as ce:
|
| 653 |
logger.warning(f"Could not delete temp file {mp3_path}: {str(ce)}")
|
| 654 |
|
| 655 |
+
|
| 656 |
+
@app.post("/extract/url", response_model=ExtractionResponse)
|
| 657 |
+
async def extract_url(payload: dict):
|
| 658 |
+
"""Accepts an Instagram Reel URL and runs the full extraction pipeline."""
|
| 659 |
+
url = payload.get("url")
|
| 660 |
+
if not url:
|
| 661 |
+
raise HTTPException(status_code=400, detail="Missing required 'url' parameter.")
|
| 662 |
+
return process_reel_url(url)
|
| 663 |
+
|
| 664 |
@app.post("/extract/file", response_model=ExtractionResponse)
|
| 665 |
async def extract_file(
|
| 666 |
file: UploadFile = File(...),
|
verify_pipeline.py
CHANGED
|
@@ -214,6 +214,36 @@ def test_cluster_merge_pass():
|
|
| 214 |
main.CLUSTER_CHUNK_DELAY = orig_delay
|
| 215 |
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
def test_delete_reel():
|
| 218 |
print("Testing DELETE /reels/{id}...")
|
| 219 |
import main, sqlite3, uuid, json
|
|
@@ -268,6 +298,7 @@ if __name__ == "__main__":
|
|
| 268 |
test_recompute_clusters_mock()
|
| 269 |
test_cluster_chunking()
|
| 270 |
test_cluster_merge_pass()
|
|
|
|
| 271 |
test_delete_reel()
|
| 272 |
test_list_clusters()
|
| 273 |
test_reels_include_cluster()
|
|
|
|
| 214 |
main.CLUSTER_CHUNK_DELAY = orig_delay
|
| 215 |
|
| 216 |
|
| 217 |
+
def test_extract_url_regression():
|
| 218 |
+
print("Testing POST /extract/url via process_reel_url (mocked)...")
|
| 219 |
+
import main
|
| 220 |
+
long_transcript = " ".join(["word"] * 30) # clears the 15-word silent-hook guard
|
| 221 |
+
mocked = ReelExtraction(
|
| 222 |
+
core_topic="Topic", key_takeaway="Takeaway",
|
| 223 |
+
action_items=["a"], tools_or_resources=["b"])
|
| 224 |
+
orig_dl = main.download_and_extract_audio
|
| 225 |
+
orig_tr = main.transcribe_audio
|
| 226 |
+
orig_ex = main.extract_structured_json
|
| 227 |
+
orig_save = main.save_to_database
|
| 228 |
+
main.download_and_extract_audio = lambda url: ("/tmp/does_not_exist.mp3", "cap", "Title")
|
| 229 |
+
main.transcribe_audio = lambda p: long_transcript
|
| 230 |
+
main.extract_structured_json = lambda t, c: mocked
|
| 231 |
+
main.save_to_database = lambda url, title, raw_transcript, post_caption, extracted: {
|
| 232 |
+
"id": "id-1", "url": url, "title": title,
|
| 233 |
+
"raw_transcript": raw_transcript, "post_caption": post_caption,
|
| 234 |
+
"extracted_json": extracted.model_dump(), "created_at": "2026-06-06T00:00:00Z"}
|
| 235 |
+
try:
|
| 236 |
+
r = client.post("/extract/url", json={"url": "https://www.instagram.com/reel/REGRESSION1/"})
|
| 237 |
+
assert r.status_code == 200, r.text
|
| 238 |
+
assert r.json()["extracted_json"]["core_topic"] == "Topic"
|
| 239 |
+
print("[OK] extract_url regression passed!")
|
| 240 |
+
finally:
|
| 241 |
+
main.download_and_extract_audio = orig_dl
|
| 242 |
+
main.transcribe_audio = orig_tr
|
| 243 |
+
main.extract_structured_json = orig_ex
|
| 244 |
+
main.save_to_database = orig_save
|
| 245 |
+
|
| 246 |
+
|
| 247 |
def test_delete_reel():
|
| 248 |
print("Testing DELETE /reels/{id}...")
|
| 249 |
import main, sqlite3, uuid, json
|
|
|
|
| 298 |
test_recompute_clusters_mock()
|
| 299 |
test_cluster_chunking()
|
| 300 |
test_cluster_merge_pass()
|
| 301 |
+
test_extract_url_regression()
|
| 302 |
test_delete_reel()
|
| 303 |
test_list_clusters()
|
| 304 |
test_reels_include_cluster()
|