Stylique commited on
Commit
5962298
·
verified ·
1 Parent(s): 9b19b76

Upload 3 files

Browse files
Files changed (1) hide show
  1. app.py +65 -50
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import FastAPI, HTTPException
2
  from pydantic import BaseModel
3
  import os
4
  import requests
@@ -10,9 +10,14 @@ from pathlib import Path
10
  from supabase import create_client, Client
11
  from openai import OpenAI
12
  import time
 
13
 
14
  app = FastAPI()
15
 
 
 
 
 
16
  class ProcessRequest(BaseModel):
17
  videoUrl: str
18
  projectId: str
@@ -20,54 +25,56 @@ class ProcessRequest(BaseModel):
20
  supabaseKey: str
21
  openaiKey: str
22
 
 
 
 
 
 
 
 
 
23
  @app.get("/")
24
  def read_root():
25
- return {"status": "Avatar Worker is Online"}
26
 
27
- @app.post("/process")
28
- async def process_video(req: ProcessRequest):
 
 
 
 
 
29
  temp_dir = Path(f"/tmp/{uuid.uuid4()}")
30
  temp_dir.mkdir(parents=True, exist_ok=True)
31
 
32
  try:
33
  # 1. Download Video
 
34
  video_path = temp_dir / "input_video.mp4"
35
- print(f"Downloading video from {req.videoUrl}...")
36
  resp = requests.get(req.videoUrl, stream=True)
37
  if resp.status_code != 200:
38
- raise HTTPException(status_code=400, detail="Failed to download video from Supabase")
39
 
40
  with open(video_path, 'wb') as f:
41
  for chunk in resp.iter_content(chunk_size=8192):
42
  f.write(chunk)
43
 
44
- # 2. Extract Audio for STT (to save bandwidth/token costs if needed, but Whisper API handles video too)
45
- # Actually Whisper API accepts files up to 25MB. If video is larger, we MUST extract audio.
46
  audio_path = temp_dir / "audio.mp3"
47
- print("Extracting audio for STT...")
48
  subprocess.run([
49
  "ffmpeg", "-i", str(video_path),
50
  "-vn", "-acodec", "libmp3lame", "-ar", "16000", "-ac", "1",
51
  str(audio_path)
52
  ], check=True, capture_output=True)
53
 
54
- # 3. Initialize Supabase Client
55
- print("Initializing Supabase client...")
56
- try:
57
- supabase: Client = create_client(req.supabaseUrl, req.supabaseKey)
58
- except Exception as se:
59
- print(f"FAILED to initialize Supabase: {str(se)}")
60
- raise HTTPException(status_code=500, detail=f"Supabase Init Error: {str(se)}")
61
 
62
  # 4. Get Timestamps from OpenAI Whisper
63
- print("Initializing OpenAI client...")
64
- try:
65
- openai_client = OpenAI(api_key=req.openaiKey)
66
- except Exception as oe:
67
- print(f"FAILED to initialize OpenAI: {str(oe)}")
68
- raise HTTPException(status_code=500, detail=f"OpenAI Init Error: {str(oe)}")
69
-
70
- print("Calling OpenAI Whisper API...")
71
  with open(audio_path, "rb") as audio_file:
72
  transcript = openai_client.audio.transcriptions.create(
73
  file=audio_file,
@@ -78,10 +85,11 @@ async def process_video(req: ProcessRequest):
78
 
79
  segments = transcript.segments
80
  if not segments:
81
- raise HTTPException(status_code=400, detail="No speech detected in video")
82
 
83
  # 5. Slice Video and Upload
84
  processed_slices = []
 
85
 
86
  for i, segment in enumerate(segments):
87
  start = segment.start
@@ -89,14 +97,15 @@ async def process_video(req: ProcessRequest):
89
  text = segment.text.strip()
90
  duration = end - start
91
 
92
- if duration < 0.5: continue # Skip too short segments
93
 
 
 
 
 
94
  output_filename = f"slice_{i}.mp4"
95
  output_path = temp_dir / output_filename
96
 
97
- print(f"Slicing segment {i}: {start}s to {end}s...")
98
-
99
- # Re-encode to ensure clean timestamps and compatibility (matching our earlier fix)
100
  subprocess.run([
101
  "ffmpeg", "-ss", str(start), "-t", str(duration), "-i", str(video_path),
102
  "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28",
@@ -106,8 +115,6 @@ async def process_video(req: ProcessRequest):
106
 
107
  # Upload to Supabase
108
  storage_path = f"{req.projectId}/avatar_{int(time.time())}_{i}.mp4"
109
- print(f"Uploading slice {i} to Supabase: {storage_path}")
110
-
111
  with open(output_path, "rb") as f:
112
  supabase.storage.from_("projects").upload(
113
  path=storage_path,
@@ -115,30 +122,38 @@ async def process_video(req: ProcessRequest):
115
  file_options={"content-type": "video/mp4", "x-upsert": "true"}
116
  )
117
 
118
- # Get Public URL
119
  public_url = supabase.storage.from_("projects").get_public_url(storage_path)
120
-
121
- processed_slices.append({
122
- "text": text,
123
- "url": public_url,
124
- "duration": duration
125
- })
126
-
127
- return {
128
- "success": True,
129
- "slices": processed_slices
130
- }
131
-
132
- except subprocess.CalledProcessError as e:
133
- print(f"FFmpeg Error: {e.stderr.decode()}")
134
- raise HTTPException(status_code=500, detail=f"Video processing failed: {e.stderr.decode()}")
135
  except Exception as e:
136
- print(f"General Error: {str(e)}")
137
- raise HTTPException(status_code=500, detail=str(e))
138
  finally:
139
- # Cleanup
140
  shutil.rmtree(temp_dir, ignore_errors=True)
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  if __name__ == "__main__":
143
  import uvicorn
144
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
2
  from pydantic import BaseModel
3
  import os
4
  import requests
 
10
  from supabase import create_client, Client
11
  from openai import OpenAI
12
  import time
13
+ from typing import Dict, Optional
14
 
15
  app = FastAPI()
16
 
17
+ # Global state for background jobs
18
+ # In a production environment, this should be a DB or Redis, but for HF Space singleton, a dict works
19
+ jobs: Dict[str, dict] = {}
20
+
21
  class ProcessRequest(BaseModel):
22
  videoUrl: str
23
  projectId: str
 
25
  supabaseKey: str
26
  openaiKey: str
27
 
28
+ class JobStatus(BaseModel):
29
+ job_id: str
30
+ status: str
31
+ progress: int
32
+ message: str
33
+ result: Optional[dict] = None
34
+ error: Optional[str] = None
35
+
36
  @app.get("/")
37
  def read_root():
38
+ return {"status": "Avatar Worker is Online", "active_jobs": len(jobs)}
39
 
40
+ @app.get("/status/{job_id}", response_model=JobStatus)
41
+ async def get_status(job_id: str):
42
+ if job_id not in jobs:
43
+ raise HTTPException(status_code=404, detail="Job not found")
44
+ return jobs[job_id]
45
+
46
+ def background_process(job_id: str, req: ProcessRequest):
47
  temp_dir = Path(f"/tmp/{uuid.uuid4()}")
48
  temp_dir.mkdir(parents=True, exist_ok=True)
49
 
50
  try:
51
  # 1. Download Video
52
+ jobs[job_id].update({"status": "processing", "progress": 10, "message": "Downloading video..."})
53
  video_path = temp_dir / "input_video.mp4"
 
54
  resp = requests.get(req.videoUrl, stream=True)
55
  if resp.status_code != 200:
56
+ raise Exception("Failed to download video from Supabase")
57
 
58
  with open(video_path, 'wb') as f:
59
  for chunk in resp.iter_content(chunk_size=8192):
60
  f.write(chunk)
61
 
62
+ # 2. Extract Audio for STT
63
+ jobs[job_id].update({"progress": 20, "message": "Extracting audio for AI analysis..."})
64
  audio_path = temp_dir / "audio.mp3"
 
65
  subprocess.run([
66
  "ffmpeg", "-i", str(video_path),
67
  "-vn", "-acodec", "libmp3lame", "-ar", "16000", "-ac", "1",
68
  str(audio_path)
69
  ], check=True, capture_output=True)
70
 
71
+ # 3. Initialize Clients
72
+ jobs[job_id].update({"progress": 30, "message": "Preparing AI engines..."})
73
+ supabase: Client = create_client(req.supabaseUrl, req.supabaseKey)
74
+ openai_client = OpenAI(api_key=req.openaiKey)
 
 
 
75
 
76
  # 4. Get Timestamps from OpenAI Whisper
77
+ jobs[job_id].update({"progress": 40, "message": "Analyzing speech and timing..."})
 
 
 
 
 
 
 
78
  with open(audio_path, "rb") as audio_file:
79
  transcript = openai_client.audio.transcriptions.create(
80
  file=audio_file,
 
85
 
86
  segments = transcript.segments
87
  if not segments:
88
+ raise Exception("No speech detected in video")
89
 
90
  # 5. Slice Video and Upload
91
  processed_slices = []
92
+ total_segments = len(segments)
93
 
94
  for i, segment in enumerate(segments):
95
  start = segment.start
 
97
  text = segment.text.strip()
98
  duration = end - start
99
 
100
+ if duration < 0.5: continue
101
 
102
+ # Update progress within the slicing phase (40% to 90%)
103
+ step_progress = 40 + int((i / total_segments) * 50)
104
+ jobs[job_id].update({"progress": step_progress, "message": f"Slicing segment {i+1}/{total_segments}..."})
105
+
106
  output_filename = f"slice_{i}.mp4"
107
  output_path = temp_dir / output_filename
108
 
 
 
 
109
  subprocess.run([
110
  "ffmpeg", "-ss", str(start), "-t", str(duration), "-i", str(video_path),
111
  "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28",
 
115
 
116
  # Upload to Supabase
117
  storage_path = f"{req.projectId}/avatar_{int(time.time())}_{i}.mp4"
 
 
118
  with open(output_path, "rb") as f:
119
  supabase.storage.from_("projects").upload(
120
  path=storage_path,
 
122
  file_options={"content-type": "video/mp4", "x-upsert": "true"}
123
  )
124
 
 
125
  public_url = supabase.storage.from_("projects").get_public_url(storage_path)
126
+ processed_slices.append({"text": text, "url": public_url, "duration": duration})
127
+
128
+ jobs[job_id].update({
129
+ "status": "completed",
130
+ "progress": 100,
131
+ "message": "Processing complete!",
132
+ "result": {"slices": processed_slices}
133
+ })
134
+
 
 
 
 
 
 
135
  except Exception as e:
136
+ print(f"Error in background job {job_id}: {str(e)}")
137
+ jobs[job_id].update({"status": "failed", "error": str(e)})
138
  finally:
 
139
  shutil.rmtree(temp_dir, ignore_errors=True)
140
 
141
+ @app.post("/process")
142
+ async def process_video(req: ProcessRequest, background_tasks: BackgroundTasks):
143
+ job_id = str(uuid.uuid4())
144
+ jobs[job_id] = {
145
+ "job_id": job_id,
146
+ "status": "queued",
147
+ "progress": 0,
148
+ "message": "Job received and queued",
149
+ "result": None,
150
+ "error": None
151
+ }
152
+
153
+ background_tasks.add_task(background_process, job_id, req)
154
+
155
+ return {"job_id": job_id}
156
+
157
  if __name__ == "__main__":
158
  import uvicorn
159
  uvicorn.run(app, host="0.0.0.0", port=7860)