WhoIsLuwah commited on
Commit
beedba5
·
1 Parent(s): 412135f

refactor(backend): extract process_reel_url from extract_url handler

Browse files

Prereq for the Supabase queue worker — same pipeline, callable in-process.

Files changed (2) hide show
  1. main.py +18 -25
  2. 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
- @app.post("/extract/url", response_model=ExtractionResponse)
604
- async def extract_url(payload: dict):
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
- # Check if we already processed this URL to save API tokens
 
 
 
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()