Percy3822 commited on
Commit
3d86a9e
·
verified ·
1 Parent(s): 8df8ab9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +67 -91
app.py CHANGED
@@ -1,115 +1,91 @@
1
- import os, pathlib, tempfile, shutil, uuid
2
  from fastapi import FastAPI, HTTPException, Request
3
- from fastapi.responses import JSONResponse
4
- from fastapi.middleware.cors import CORSMiddleware
5
- from pydantic import BaseModel
6
  import subprocess
 
 
7
 
8
- # ----------------------------------
9
- # CONFIG & PATHS (robust writable base)
10
- # ----------------------------------
11
- def _first_writable(candidates):
12
- for p in candidates:
13
- try:
14
- path = pathlib.Path(p).resolve()
15
- path.mkdir(parents=True, exist_ok=True)
16
- test_file = path / ".write_test"
17
- test_file.write_text("ok", encoding="utf-8")
18
- test_file.unlink(missing_ok=True)
19
- return path
20
- except Exception:
21
- continue
22
- fallback = pathlib.Path(tempfile.gettempdir()) / "tts_app"
23
- fallback.mkdir(parents=True, exist_ok=True)
24
- return fallback.resolve()
25
 
26
- _env_base = os.environ.get("TTS_BASE_DIR", "").strip()
27
- _candidates = []
28
- if _env_base:
29
- _candidates.append(_env_base)
30
- _candidates += ["/data/tts_app", "/tmp/tts_app", "/home/user/tts_app"]
31
 
32
- BASE_DIR = _first_writable(_candidates)
33
- FILES_DIR = BASE_DIR / "files"
34
- VOICES_DIR = BASE_DIR / "voices"
35
- FILES_DIR.mkdir(parents=True, exist_ok=True)
36
- VOICES_DIR.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
37
 
38
- # ----------------------------------
39
- # FASTAPI SETUP
40
- # ----------------------------------
41
- app = FastAPI()
42
- app.add_middleware(
43
- CORSMiddleware,
44
- allow_origins=["*"],
45
- allow_credentials=True,
46
- allow_methods=["*"],
47
- allow_headers=["*"],
48
- )
49
 
50
- # ----------------------------------
51
- # UTILITIES
52
- # ----------------------------------
53
- class TTSRequest(BaseModel):
54
- text: str
55
- voice: str = "en_US-libritts-high"
56
- length_scale: float = 1.0
57
- noise_scale: float = 0.33
58
- noise_w: float = 0.8
59
 
60
  @app.get("/health")
61
  def health():
62
  return {
63
  "ok": True,
64
  "engine": "piper-tts (CLI, CPU)",
65
- "default_voice": None,
66
  "voice_dir": str(VOICES_DIR),
67
- "available_voices": sorted([v.name for v in VOICES_DIR.glob("/*.onnx")]),
68
  "files_dir": str(FILES_DIR),
 
 
69
  }
70
 
71
  @app.post("/speak")
72
- def speak(body: TTSRequest):
73
- if not body.text.strip():
74
- raise HTTPException(status_code=400, detail="Empty text")
75
-
76
- text_id = str(uuid.uuid4())[:8]
77
- txt_path = FILES_DIR / f"{text_id}.txt"
78
- wav_path = FILES_DIR / f"{text_id}.wav"
 
79
 
80
- txt_path.write_text(body.text.strip(), encoding="utf-8")
 
81
 
82
- # --------------------------
83
- # Run piper
84
- # --------------------------
85
- voice = body.voice
86
- voice_path = VOICES_DIR / voice / "model.onnx"
87
- if not voice_path.exists():
88
- return JSONResponse(content={"ok": False, "error": f"Voice not found: {voice}"})
 
 
 
89
 
90
- cmd = [
91
- "piper",
92
- "--model", str(voice_path),
93
- "--output_file", str(wav_path),
94
- "--length_scale", str(body.length_scale),
95
- "--noise_scale", str(body.noise_scale),
96
- "--noise_w", str(body.noise_w),
97
- ]
98
 
99
- try:
100
- with open(txt_path, "r", encoding="utf-8") as f:
101
- subprocess.run(cmd, stdin=f, check=True)
102
- except subprocess.CalledProcessError as e:
103
- return JSONResponse(content={"ok": False, "error": str(e)})
104
 
105
- return {
106
- "ok": True,
107
- "audio_url": f"/files/{wav_path.name}",
108
- }
109
 
110
- @app.get("/files/{filename}")
111
- def get_file(filename: str):
112
- f = FILES_DIR / filename
113
- if not f.exists():
114
- raise HTTPException(status_code=404, detail="File not found")
115
- return JSONResponse(content={"url": f"/files/{filename}"})
 
 
1
  from fastapi import FastAPI, HTTPException, Request
2
+ from fastapi.responses import FileResponse
3
+ from pathlib import Path
4
+ import uuid
5
  import subprocess
6
+ import os
7
+ import shutil
8
 
9
+ app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ # ========== Dynamic VOICES_DIR fix ==========
12
+ # Try /tmp/tts_app/voices first
13
+ base_dirs = [Path("/tmp/tts_app/voices"), Path("/data/voices"), Path("/home/user/voices"), Path.cwd() / "voices"]
 
 
14
 
15
+ for path in base_dirs:
16
+ try:
17
+ path.mkdir(parents=True, exist_ok=True)
18
+ test_file = path / ".write_test"
19
+ test_file.write_text("ok")
20
+ test_file.unlink()
21
+ VOICES_DIR = path
22
+ break
23
+ except Exception:
24
+ continue
25
+ else:
26
+ raise RuntimeError("❌ Could not find any writable directory for VOICES_DIR.")
27
 
28
+ FILES_DIR = VOICES_DIR.parent / "files"
29
+ FILES_DIR.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
30
 
31
+ # ========== Piper engine config ==========
32
+ VOICE_TAGS = [
33
+ "en_US-libritts-high", # Humanlike, CPU-friendly (VITS-based)
34
+ "en_US-amy-medium", # Classic Piper Amy
35
+ "en_US-lessac-high", # LJSpeech-derived VITS
36
+ ]
 
 
 
37
 
38
  @app.get("/health")
39
  def health():
40
  return {
41
  "ok": True,
42
  "engine": "piper-tts (CLI, CPU)",
 
43
  "voice_dir": str(VOICES_DIR),
 
44
  "files_dir": str(FILES_DIR),
45
+ "default_voice": None,
46
+ "available_voices": VOICE_TAGS,
47
  }
48
 
49
  @app.post("/speak")
50
+ async def speak(request: Request):
51
+ try:
52
+ payload = await request.json()
53
+ text = payload.get("text", "").strip()
54
+ voice = payload.get("voice", VOICE_TAGS[0])
55
+ length_scale = float(payload.get("length_scale", 1.08))
56
+ noise_scale = float(payload.get("noise_scale", 0.33))
57
+ noise_w = float(payload.get("noise_w", 0.8))
58
 
59
+ if not text:
60
+ raise HTTPException(status_code=400, detail="No text provided")
61
 
62
+ output_file = FILES_DIR / f"{uuid.uuid4().hex}.wav"
63
+ command = [
64
+ "piper",
65
+ "--model", f"voices/{voice}.onnx",
66
+ "--output_file", str(output_file),
67
+ "--text", text,
68
+ "--length_scale", str(length_scale),
69
+ "--noise_scale", str(noise_scale),
70
+ "--noise_w", str(noise_w),
71
+ ]
72
 
73
+ result = subprocess.run(command, capture_output=True, text=True)
74
+ if result.returncode != 0 or not output_file.exists():
75
+ raise HTTPException(status_code=500, detail=f"TTS failed: {result.stderr.strip()}")
 
 
 
 
 
76
 
77
+ return {
78
+ "ok": True,
79
+ "voice": voice,
80
+ "audio_url": f"/file/{output_file.name}"
81
+ }
82
 
83
+ except Exception as e:
84
+ raise HTTPException(status_code=500, detail=str(e))
 
 
85
 
86
+ @app.get("/file/{filename}")
87
+ async def get_file(filename: str):
88
+ path = FILES_DIR / filename
89
+ if path.exists():
90
+ return FileResponse(path)
91
+ raise HTTPException(status_code=404, detail="File not found")