J Z commited on
Update app.py
Browse files
app.py
CHANGED
|
@@ -27,6 +27,9 @@ FLOAT_ENABLED = os.environ.get("FLOAT_ENABLED", "true").lower() == "true"
|
|
| 27 |
for d in [VIDEOS_DIR, TTS_DIR, LIPSYNC_DIR]:
|
| 28 |
d.mkdir(parents=True, exist_ok=True)
|
| 29 |
|
|
|
|
|
|
|
|
|
|
| 30 |
# In-memory store for chunked generation results
|
| 31 |
# session_id -> { "chunks": [...], "done": bool }
|
| 32 |
_sessions = {}
|
|
@@ -218,8 +221,7 @@ async def chat(request: Request):
|
|
| 218 |
async def chat_next(request: Request):
|
| 219 |
"""
|
| 220 |
Generate TTS + lipsync for the next sentence in the session.
|
| 221 |
-
|
| 222 |
-
Returns the audio URL, lipsync video URL, duration, and sentence text.
|
| 223 |
"""
|
| 224 |
body = await request.json()
|
| 225 |
session_id = body.get("session_id", "")
|
|
@@ -236,8 +238,34 @@ async def chat_next(request: Request):
|
|
| 236 |
sentence = session["sentences"][idx]
|
| 237 |
session["next_index"] = idx + 1
|
| 238 |
is_last = (idx + 1 >= session["total"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
| 241 |
t0 = _time.time()
|
| 242 |
|
| 243 |
# 1. TTS
|
|
@@ -258,16 +286,17 @@ async def chat_next(request: Request):
|
|
| 258 |
except Exception:
|
| 259 |
audio_duration = 3.0
|
| 260 |
|
| 261 |
-
# 2. Lipsync
|
| 262 |
lipsync_video_url = None
|
| 263 |
if FLOAT_ENABLED and audio_path and os.path.exists(audio_path):
|
| 264 |
try:
|
| 265 |
from float_lipsync import get_lipsync
|
| 266 |
lipsync = get_lipsync()
|
| 267 |
if lipsync.ready:
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
|
|
|
| 271 |
logger.info(f"[{_ts()}] [TIMING] Chunk {idx+1} FLOAT: {t4-t3:.2f}s")
|
| 272 |
if lp and os.path.exists(lp):
|
| 273 |
lipsync_video_url = f"/api/stream/{os.path.basename(lp)}"
|
|
@@ -277,24 +306,11 @@ async def chat_next(request: Request):
|
|
| 277 |
total_time = _time.time() - t0
|
| 278 |
logger.info(f"[{_ts()}] [TIMING] Chunk {idx+1} TOTAL: {total_time:.2f}s")
|
| 279 |
|
| 280 |
-
|
| 281 |
-
session["done"] = True
|
| 282 |
-
# Clean up old sessions (keep last 10)
|
| 283 |
-
if len(_sessions) > 10:
|
| 284 |
-
oldest = sorted(_sessions.keys(), key=lambda k: _sessions[k]["created"])
|
| 285 |
-
for k in oldest[:-10]:
|
| 286 |
-
del _sessions[k]
|
| 287 |
-
|
| 288 |
-
return JSONResponse({
|
| 289 |
-
"done": False,
|
| 290 |
-
"chunk_index": idx,
|
| 291 |
-
"total_chunks": session["total"],
|
| 292 |
-
"is_last": is_last,
|
| 293 |
-
"sentence": sentence,
|
| 294 |
"audio_url": audio_url,
|
| 295 |
"audio_duration": audio_duration,
|
| 296 |
"lipsync_video_url": lipsync_video_url,
|
| 297 |
-
}
|
| 298 |
|
| 299 |
|
| 300 |
@app.post("/api/reset")
|
|
|
|
| 27 |
for d in [VIDEOS_DIR, TTS_DIR, LIPSYNC_DIR]:
|
| 28 |
d.mkdir(parents=True, exist_ok=True)
|
| 29 |
|
| 30 |
+
import threading
|
| 31 |
+
_float_lock = threading.Lock()
|
| 32 |
+
|
| 33 |
# In-memory store for chunked generation results
|
| 34 |
# session_id -> { "chunks": [...], "done": bool }
|
| 35 |
_sessions = {}
|
|
|
|
| 221 |
async def chat_next(request: Request):
|
| 222 |
"""
|
| 223 |
Generate TTS + lipsync for the next sentence in the session.
|
| 224 |
+
Heavy work runs in a thread so the server stays responsive for prefetch requests.
|
|
|
|
| 225 |
"""
|
| 226 |
body = await request.json()
|
| 227 |
session_id = body.get("session_id", "")
|
|
|
|
| 238 |
sentence = session["sentences"][idx]
|
| 239 |
session["next_index"] = idx + 1
|
| 240 |
is_last = (idx + 1 >= session["total"])
|
| 241 |
+
total = session["total"]
|
| 242 |
+
|
| 243 |
+
# Run the heavy TTS + FLOAT work in a thread
|
| 244 |
+
import asyncio
|
| 245 |
+
result = await asyncio.to_thread(_generate_chunk, sentence, idx, total)
|
| 246 |
+
|
| 247 |
+
if is_last:
|
| 248 |
+
session["done"] = True
|
| 249 |
+
if len(_sessions) > 10:
|
| 250 |
+
oldest = sorted(_sessions.keys(), key=lambda k: _sessions[k]["created"])
|
| 251 |
+
for k in oldest[:-10]:
|
| 252 |
+
del _sessions[k]
|
| 253 |
+
|
| 254 |
+
return JSONResponse({
|
| 255 |
+
"done": False,
|
| 256 |
+
"chunk_index": idx,
|
| 257 |
+
"total_chunks": total,
|
| 258 |
+
"is_last": is_last,
|
| 259 |
+
"sentence": sentence,
|
| 260 |
+
"audio_url": result["audio_url"],
|
| 261 |
+
"audio_duration": result["audio_duration"],
|
| 262 |
+
"lipsync_video_url": result["lipsync_video_url"],
|
| 263 |
+
})
|
| 264 |
|
| 265 |
+
|
| 266 |
+
def _generate_chunk(sentence, idx, total):
|
| 267 |
+
"""Synchronous TTS + FLOAT generation (runs in thread pool)."""
|
| 268 |
+
logger.info(f"[{_ts()}] [CHUNK {idx+1}/{total}] {sentence[:60]}...")
|
| 269 |
t0 = _time.time()
|
| 270 |
|
| 271 |
# 1. TTS
|
|
|
|
| 286 |
except Exception:
|
| 287 |
audio_duration = 3.0
|
| 288 |
|
| 289 |
+
# 2. Lipsync (locked — GPU not thread-safe)
|
| 290 |
lipsync_video_url = None
|
| 291 |
if FLOAT_ENABLED and audio_path and os.path.exists(audio_path):
|
| 292 |
try:
|
| 293 |
from float_lipsync import get_lipsync
|
| 294 |
lipsync = get_lipsync()
|
| 295 |
if lipsync.ready:
|
| 296 |
+
with _float_lock:
|
| 297 |
+
t3 = _time.time()
|
| 298 |
+
lp = lipsync.generate(audio_path)
|
| 299 |
+
t4 = _time.time()
|
| 300 |
logger.info(f"[{_ts()}] [TIMING] Chunk {idx+1} FLOAT: {t4-t3:.2f}s")
|
| 301 |
if lp and os.path.exists(lp):
|
| 302 |
lipsync_video_url = f"/api/stream/{os.path.basename(lp)}"
|
|
|
|
| 306 |
total_time = _time.time() - t0
|
| 307 |
logger.info(f"[{_ts()}] [TIMING] Chunk {idx+1} TOTAL: {total_time:.2f}s")
|
| 308 |
|
| 309 |
+
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
"audio_url": audio_url,
|
| 311 |
"audio_duration": audio_duration,
|
| 312 |
"lipsync_video_url": lipsync_video_url,
|
| 313 |
+
}
|
| 314 |
|
| 315 |
|
| 316 |
@app.post("/api/reset")
|