basyx commited on
Commit
a31916d
·
verified ·
1 Parent(s): e358d86

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +61 -47
main.py CHANGED
@@ -14,36 +14,35 @@ TEMP_DIR = "/tmp/whisper_jobs"
14
  CLEANUP_INTERVAL = 600 # 10 minutes
15
  FILE_MAX_AGE = 1800 # 30 minutes
16
 
17
- # --- LIFESPAN & AUTO-CLEANUP ---
18
 
19
- async def auto_cleanup():
20
- """Background task to ensure the Hugging Face Space disk doesn't fill up."""
21
  while True:
22
  try:
23
  now = time.time()
24
  if os.path.exists(TEMP_DIR):
25
  for f in os.listdir(TEMP_DIR):
26
  p = os.path.join(TEMP_DIR, f)
27
- # Delete if file is older than 30 minutes
28
  if os.path.isfile(p) and (now - os.path.getmtime(p) > FILE_MAX_AGE):
29
  os.remove(p)
30
- logger.info(f"Auto-cleaned stale file: {f}")
31
  except Exception as e:
32
- logger.error(f"Cleanup task error: {e}")
33
  await asyncio.sleep(CLEANUP_INTERVAL)
34
 
35
  @asynccontextmanager
36
  async def lifespan(app: FastAPI):
37
  os.makedirs(TEMP_DIR, exist_ok=True)
38
- cleanup_task = asyncio.create_task(auto_cleanup())
39
  yield
40
- cleanup_task.cancel()
41
 
42
- app = FastAPI(title="Basyx TikTok Video Factory", lifespan=lifespan)
43
 
44
- # --- GRADIO UI (Manual Interface) ---
45
 
46
- def gradio_interface(audio_path):
47
  if not audio_path: return "No file uploaded.", ""
48
  try:
49
  segments, _ = engine.run(audio_path, word_timestamps=False)
@@ -51,22 +50,54 @@ def gradio_interface(audio_path):
51
  return full_text, generate_srt(segments)
52
  except Exception as e: return f"Error: {str(e)}", ""
53
 
54
- io = gr.Interface(
55
- fn=gradio_interface,
56
- inputs=gr.Audio(type="filepath", label="Quick Test"),
57
- outputs=[gr.Textbox(label="Transcription"), gr.Textbox(label="SRT")],
58
- title="Basyx Whisper Orchestrator",
59
- description="Manual testing zone. Use /v1/render-tiktok for n8n automation."
60
- )
 
 
 
 
 
 
 
 
61
 
62
- # --- API ENDPOINTS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  @app.post("/v1/transcribe")
65
- async def transcribe(file: UploadFile = File(...), word_timestamps: bool = Form(True)):
66
- """Simple text-only transcription for n8n."""
67
  job_id = str(uuid.uuid4())
68
  input_path = os.path.join(TEMP_DIR, f"{job_id}_{file.filename}")
69
-
70
  with open(input_path, "wb") as buffer:
71
  shutil.copyfileobj(file.file, buffer)
72
 
@@ -74,7 +105,6 @@ async def transcribe(file: UploadFile = File(...), word_timestamps: bool = Form(
74
  segments, info = engine.run(input_path, word_timestamps=word_timestamps)
75
  return {
76
  "status": "success",
77
- "job_id": job_id,
78
  "full_text": " ".join([s.text.strip() for s in segments]),
79
  "srt": generate_srt(segments),
80
  "metadata": {"language": info.language, "duration": round(info.duration, 2)}
@@ -83,49 +113,33 @@ async def transcribe(file: UploadFile = File(...), word_timestamps: bool = Form(
83
  if os.path.exists(input_path): os.remove(input_path)
84
 
85
  @app.post("/v1/render-tiktok")
86
- async def render_tiktok(file: UploadFile = File(...)):
87
- """
88
- The TikTok Factory Endpoint:
89
- - High-precision transcription.
90
- - Pango-style word highlight chunking.
91
- - MoviePy rendering with Bounce/Pop effects.
92
- - Returns the finished .mp4 file.
93
- """
94
  job_id = str(uuid.uuid4())
95
  input_path = os.path.join(TEMP_DIR, f"{job_id}_{file.filename}")
96
- output_path = os.path.join(TEMP_DIR, f"render_{job_id}.mp4")
97
 
98
- # Save incoming video
99
  with open(input_path, "wb") as buffer:
100
  shutil.copyfileobj(file.file, buffer)
101
 
102
  try:
103
- logger.info(f"Processing TikTok Render: {job_id}")
104
-
105
- # 1. Inference with word-level precision
106
  segments, _ = engine.run(input_path, word_timestamps=True)
107
-
108
- # 2. Process words into highlight-ready frames (Pango Logic)
109
  highlight_frames = sub_engine.create_highlight_chunks(segments)
110
-
111
- # 3. Render video (This blocks until finished)
112
  render_tiktok_video(input_path, highlight_frames, output_path)
113
 
114
- # 4. Response
115
  return FileResponse(
116
  path=output_path,
117
- filename=f"captioned_{file.filename}",
118
  media_type="video/mp4"
119
  )
120
-
121
  except Exception as e:
122
- logger.error(f"TikTok Render Job {job_id} failed: {e}")
123
  raise HTTPException(status_code=500, detail=str(e))
124
- # FileResponse handles the stream. Cleanup task handles the disk later.
125
 
126
  @app.get("/health")
127
  async def health():
128
  return {"status": "ready"}
129
 
130
- # Mount Gradio to the root
131
- app = gr.mount_gradio_app(app, io, path="/")
 
14
  CLEANUP_INTERVAL = 600 # 10 minutes
15
  FILE_MAX_AGE = 1800 # 30 minutes
16
 
17
+ # --- BACKGROUND ASYNC TASKS ---
18
 
19
+ async def auto_cleanup_task():
20
+ """Safety net to prevent disk overflow on Hugging Face."""
21
  while True:
22
  try:
23
  now = time.time()
24
  if os.path.exists(TEMP_DIR):
25
  for f in os.listdir(TEMP_DIR):
26
  p = os.path.join(TEMP_DIR, f)
 
27
  if os.path.isfile(p) and (now - os.path.getmtime(p) > FILE_MAX_AGE):
28
  os.remove(p)
29
+ logger.info(f"Disk Cleanup: Removed stale file {f}")
30
  except Exception as e:
31
+ logger.error(f"Cleanup Error: {e}")
32
  await asyncio.sleep(CLEANUP_INTERVAL)
33
 
34
  @asynccontextmanager
35
  async def lifespan(app: FastAPI):
36
  os.makedirs(TEMP_DIR, exist_ok=True)
37
+ cleanup_loop = asyncio.create_task(auto_cleanup_task())
38
  yield
39
+ cleanup_loop.cancel()
40
 
41
+ app = FastAPI(title="Basyx Whisper Orchestrator", lifespan=lifespan)
42
 
43
+ # --- GRADIO UI LOGIC ---
44
 
45
+ def gradio_transcribe_only(audio_path):
46
  if not audio_path: return "No file uploaded.", ""
47
  try:
48
  segments, _ = engine.run(audio_path, word_timestamps=False)
 
50
  return full_text, generate_srt(segments)
51
  except Exception as e: return f"Error: {str(e)}", ""
52
 
53
+ def gradio_render_video(video_path):
54
+ if not video_path: return None
55
+ try:
56
+ job_id = f"manual_{uuid.uuid4()}"
57
+ out_path = os.path.join(TEMP_DIR, f"{job_id}.mp4")
58
+
59
+ # Process video for TikTok style
60
+ segments, _ = engine.run(video_path, word_timestamps=True)
61
+ highlight_frames = sub_engine.create_highlight_chunks(segments)
62
+ render_tiktok_video(video_path, highlight_frames, out_path)
63
+
64
+ return out_path
65
+ except Exception as e:
66
+ logger.error(f"UI Rendering Failed: {e}")
67
+ return None
68
 
69
+ # Build the Tabbed UI
70
+ with gr.Blocks(title="Basyx Whisper Orchestrator") as demo:
71
+ gr.Markdown("# 🎬 Basyx Whisper & TikTok Factory")
72
+ gr.Markdown("Manual testing zone. Use `/v1/render-tiktok` for n8n automation.")
73
+
74
+ with gr.Tabs():
75
+ with gr.TabItem("Quick Transcription"):
76
+ with gr.Row():
77
+ with gr.Column():
78
+ audio_input = gr.Audio(type="filepath", label="Upload Audio/Video")
79
+ transcribe_btn = gr.Button("Transcribe", variant="primary")
80
+ with gr.Column():
81
+ text_out = gr.Textbox(label="Transcription")
82
+ srt_out = gr.Textbox(label="SRT Format")
83
+ transcribe_btn.click(gradio_transcribe_only, inputs=audio_input, outputs=[text_out, srt_out])
84
+
85
+ with gr.TabItem("TikTok Video Renderer"):
86
+ with gr.Row():
87
+ with gr.Column():
88
+ video_input = gr.Video(label="Source Video")
89
+ render_btn = gr.Button("Render TikTok Style", variant="primary")
90
+ with gr.Column():
91
+ video_output = gr.Video(label="Finished Video")
92
+ render_btn.click(gradio_render_video, inputs=video_input, outputs=video_output)
93
+
94
+ # --- API ENDPOINTS FOR N8N ---
95
 
96
  @app.post("/v1/transcribe")
97
+ async def transcribe_api(file: UploadFile = File(...), word_timestamps: bool = Form(True)):
98
+ """Standard text-only transcription."""
99
  job_id = str(uuid.uuid4())
100
  input_path = os.path.join(TEMP_DIR, f"{job_id}_{file.filename}")
 
101
  with open(input_path, "wb") as buffer:
102
  shutil.copyfileobj(file.file, buffer)
103
 
 
105
  segments, info = engine.run(input_path, word_timestamps=word_timestamps)
106
  return {
107
  "status": "success",
 
108
  "full_text": " ".join([s.text.strip() for s in segments]),
109
  "srt": generate_srt(segments),
110
  "metadata": {"language": info.language, "duration": round(info.duration, 2)}
 
113
  if os.path.exists(input_path): os.remove(input_path)
114
 
115
  @app.post("/v1/render-tiktok")
116
+ async def render_tiktok_api(file: UploadFile = File(...)):
117
+ """Full TikTok video production factory."""
 
 
 
 
 
 
118
  job_id = str(uuid.uuid4())
119
  input_path = os.path.join(TEMP_DIR, f"{job_id}_{file.filename}")
120
+ output_path = os.path.join(TEMP_DIR, f"api_render_{job_id}.mp4")
121
 
 
122
  with open(input_path, "wb") as buffer:
123
  shutil.copyfileobj(file.file, buffer)
124
 
125
  try:
126
+ logger.info(f"API Render Start: {job_id}")
 
 
127
  segments, _ = engine.run(input_path, word_timestamps=True)
 
 
128
  highlight_frames = sub_engine.create_highlight_chunks(segments)
 
 
129
  render_tiktok_video(input_path, highlight_frames, output_path)
130
 
 
131
  return FileResponse(
132
  path=output_path,
133
+ filename=f"tiktok_{file.filename}",
134
  media_type="video/mp4"
135
  )
 
136
  except Exception as e:
137
+ logger.error(f"API Render Error: {e}")
138
  raise HTTPException(status_code=500, detail=str(e))
 
139
 
140
  @app.get("/health")
141
  async def health():
142
  return {"status": "ready"}
143
 
144
+ # Mount everything
145
+ app = gr.mount_gradio_app(app, demo, path="/")