|
@@ -148,13 +148,15 @@ _playback_status: dict = {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+BRIDGE_INJECT_URL = "http://127.0.0.1:8002/inject"
|
|
|
|
|
+
|
|
|
async def _stream_file(filepath: Path, speed: float) -> None:
|
|
async def _stream_file(filepath: Path, speed: float) -> None:
|
|
|
global _playback_status
|
|
global _playback_status
|
|
|
try:
|
|
try:
|
|
|
import miniaudio
|
|
import miniaudio
|
|
|
- import bridge # import the running bridge module
|
|
|
|
|
|
|
+ import httpx
|
|
|
except ImportError as e:
|
|
except ImportError as e:
|
|
|
- _playback_status.update({"state": "error", "error": str(e)})
|
|
|
|
|
|
|
+ _playback_status.update({"state": "error", "error": f"Missing package: {e}"})
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
@@ -172,25 +174,21 @@ async def _stream_file(filepath: Path, speed: float) -> None:
|
|
|
|
|
|
|
|
stream = miniaudio.stream_file(
|
|
stream = miniaudio.stream_file(
|
|
|
str(filepath),
|
|
str(filepath),
|
|
|
- output_format=miniaudio.SampleFormat.SIGNED16, # back to s16le
|
|
|
|
|
|
|
+ output_format=miniaudio.SampleFormat.SIGNED16,
|
|
|
nchannels=1,
|
|
nchannels=1,
|
|
|
sample_rate=16000,
|
|
sample_rate=16000,
|
|
|
frames_to_read=chunk_frames,
|
|
frames_to_read=chunk_frames,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- for chunk in stream:
|
|
|
|
|
- # Wait until bridge has initialised its injection queue
|
|
|
|
|
- while bridge.test_audio_queue is None:
|
|
|
|
|
- await asyncio.sleep(0.1)
|
|
|
|
|
-
|
|
|
|
|
- chunk_bytes = bytes(chunk)
|
|
|
|
|
- await bridge.test_audio_queue.put(chunk_bytes)
|
|
|
|
|
- elapsed += chunk_secs
|
|
|
|
|
- _playback_status["elapsed"] = round(elapsed, 1)
|
|
|
|
|
- _playback_status["progress"] = (
|
|
|
|
|
- min(99, round(elapsed / duration * 100)) if duration else 0
|
|
|
|
|
- )
|
|
|
|
|
- await asyncio.sleep(chunk_secs / speed)
|
|
|
|
|
|
|
+ async with httpx.AsyncClient() as client:
|
|
|
|
|
+ for chunk in stream:
|
|
|
|
|
+ await client.post(BRIDGE_INJECT_URL, content=bytes(chunk))
|
|
|
|
|
+ elapsed += chunk_secs
|
|
|
|
|
+ _playback_status["elapsed"] = round(elapsed, 1)
|
|
|
|
|
+ _playback_status["progress"] = (
|
|
|
|
|
+ min(99, round(elapsed / duration * 100)) if duration else 0
|
|
|
|
|
+ )
|
|
|
|
|
+ await asyncio.sleep(chunk_secs / speed)
|
|
|
|
|
|
|
|
_playback_status.update({
|
|
_playback_status.update({
|
|
|
"state": "done", "progress": 100, "elapsed": round(duration, 1),
|
|
"state": "done", "progress": 100, "elapsed": round(duration, 1),
|
|
@@ -202,21 +200,22 @@ async def _stream_file(filepath: Path, speed: float) -> None:
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
_playback_status.update({"state": "error", "error": str(exc), "progress": 0})
|
|
_playback_status.update({"state": "error", "error": str(exc), "progress": 0})
|
|
|
print(f"[Playback] {exc}")
|
|
print(f"[Playback] {exc}")
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
# ── Test recording API ────────────────────────────────────────────────────────
|
|
# ── Test recording API ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
@app.post("/api/test/upload")
|
|
@app.post("/api/test/upload")
|
|
|
async def api_test_upload(file: UploadFile = File(...)):
|
|
async def api_test_upload(file: UploadFile = File(...)):
|
|
|
suffix = Path(file.filename or "recording.wav").suffix.lower()
|
|
suffix = Path(file.filename or "recording.wav").suffix.lower()
|
|
|
if suffix not in ALLOWED_AUDIO_EXTS:
|
|
if suffix not in ALLOWED_AUDIO_EXTS:
|
|
|
- raise HTTPException(400, f"Unsupported format '{suffix}'. Use WAV, MP3, FLAC, OGG, or M4A.")
|
|
|
|
|
- stem = Path(file.filename).stem[:80] # limit filename length
|
|
|
|
|
|
|
+ raise HTTPException(400, f"Unsupported format '{suffix}'")
|
|
|
|
|
+ # Sanitise filename — replace spaces with underscores
|
|
|
|
|
+ stem = Path(file.filename).stem[:80].replace(" ", "_")
|
|
|
out = TEST_RECORDINGS_DIR / f"{stem}{suffix}"
|
|
out = TEST_RECORDINGS_DIR / f"{stem}{suffix}"
|
|
|
- with out.open("wb") as f:
|
|
|
|
|
- shutil.copyfileobj(file.file, f)
|
|
|
|
|
- size_mb = round(out.stat().st_size / 1024 / 1024, 1)
|
|
|
|
|
- return {"ok": True, "filename": out.name, "mb": size_mb}
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ with out.open("wb") as f:
|
|
|
|
|
+ shutil.copyfileobj(file.file, f)
|
|
|
|
|
+ except OSError as e:
|
|
|
|
|
+ raise HTTPException(500, f"Could not save file: {e}")
|
|
|
|
|
+ return {"ok": True, "filename": out.name, "mb": round(out.stat().st_size / 1024 / 1024, 1)}
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/test/files")
|
|
@app.get("/api/test/files")
|
|
@@ -231,11 +230,14 @@ def api_test_list():
|
|
|
return {"files": files}
|
|
return {"files": files}
|
|
|
|
|
|
|
|
|
|
|
|
|
-@app.delete("/api/test/files/{filename}")
|
|
|
|
|
|
|
+@app.delete("/api/test/files/{filename:path}")
|
|
|
def api_test_delete(filename: str):
|
|
def api_test_delete(filename: str):
|
|
|
- p = TEST_RECORDINGS_DIR / Path(filename).name # sanitise — no path traversal
|
|
|
|
|
- if p.exists():
|
|
|
|
|
- p.unlink()
|
|
|
|
|
|
|
+ p = TEST_RECORDINGS_DIR / Path(filename).name
|
|
|
|
|
+ try:
|
|
|
|
|
+ if p.exists():
|
|
|
|
|
+ p.unlink()
|
|
|
|
|
+ except OSError as e:
|
|
|
|
|
+ raise HTTPException(500, f"Could not delete: {e}")
|
|
|
return {"ok": True}
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|