Thursday88 commited on
Commit
460dde8
·
verified ·
1 Parent(s): b428567

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +26 -0
  2. README.md +48 -6
  3. app.py +105 -0
  4. start.sh +25 -0
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive \
4
+ PIP_NO_CACHE_DIR=1 \
5
+ PYTHONDONTWRITEBYTECODE=1 \
6
+ PYTHONUNBUFFERED=1 \
7
+ PORT=7860 \
8
+ VOICES_DIR=/voices
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ curl ca-certificates espeak-ng \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ RUN pip install --no-cache-dir \
15
+ "piper-tts" \
16
+ "fastapi" \
17
+ "uvicorn[standard]"
18
+
19
+ WORKDIR /app
20
+ COPY app.py /app/app.py
21
+ COPY start.sh /app/start.sh
22
+ RUN chmod +x /app/start.sh && mkdir -p ${VOICES_DIR}
23
+
24
+ EXPOSE 7860
25
+
26
+ CMD ["/app/start.sh"]
README.md CHANGED
@@ -1,10 +1,52 @@
1
  ---
2
- title: Palmtts
3
- emoji: 🏃
4
- colorFrom: pink
5
- colorTo: pink
6
  sdk: docker
7
- pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Palm Tree TTS
3
+ emoji: 🌴
4
+ colorFrom: yellow
5
+ colorTo: red
6
  sdk: docker
7
+ app_port: 7860
8
+ pinned: true
9
  ---
10
 
11
+ # 🌴 Palm Tree TTS
12
+
13
+ Bilingual Piper text-to-speech for the **Under The Palm Tree** games.
14
+
15
+ - 🇬🇧 English voice: `en_GB-alan-low` (Alan, British)
16
+ - 🇯🇴 Arabic voice: `ar_JO-kareem-medium` (Kareem, Jordanian)
17
+
18
+ ## API
19
+
20
+ ```
21
+ POST /synthesize
22
+ Content-Type: application/json
23
+ { "text": "Welcome to Samail.", "lang": "en" }
24
+ ```
25
+
26
+ Returns `audio/wav` (16-bit PCM).
27
+
28
+ You can also pass `"voice": "en_GB-alan-low"` instead of `lang`.
29
+
30
+ ```
31
+ GET / -> {"status": "ready", "voices": [...]}
32
+ GET /healthz -> "ok"
33
+ POST /tts -> alias for /synthesize
34
+ ```
35
+
36
+ CORS is open so the games can call this Space from any origin.
37
+
38
+ ## How the voices load
39
+
40
+ At container start, `start.sh` downloads the two `.onnx` voice models and
41
+ their `.onnx.json` configs from the GitHub raw URLs at
42
+ `thuraya1988/123Learning-English-Under-The-Palm-Tree/main/public/tts-voices/`.
43
+
44
+ That keeps the Space in sync with whatever Thuraya last uploaded to the repo.
45
+
46
+ ## Deploying
47
+
48
+ 1. Create a Hugging Face Space named `palm-tree-tts` under owner `Thursday88`.
49
+ 2. **SDK: Docker** — required.
50
+ 3. Upload `Dockerfile`, `app.py`, `start.sh`, and this `README.md`.
51
+ 4. Wait 3–5 minutes for the build.
52
+ 5. The Space serves on `https://thursday88-palm-tree-tts.hf.space`.
app.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Piper TTS HTTP server for the Under-The-Palm-Tree games.
2
+
3
+ Exposes a tiny REST API that the games' piper-client.js can call directly
4
+ from the browser. CORS is wide open so any origin (Vercel, GitHub Pages,
5
+ Squarespace iframes) can use it.
6
+
7
+ POST /synthesize {"text": "...", "voice": "en_GB-alan-low" | "ar_JO-kareem-medium"}
8
+ POST /tts {"text": "...", "lang": "en" | "ar"}
9
+ GET /healthz -> "ok"
10
+ GET / -> {"status": "ready", "voices": [...]}
11
+
12
+ Each POST returns audio/wav (16-bit PCM).
13
+ """
14
+ import io
15
+ import os
16
+ import wave
17
+ from pathlib import Path
18
+
19
+ from fastapi import FastAPI, HTTPException
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from fastapi.responses import Response, JSONResponse
22
+ from pydantic import BaseModel
23
+ from piper import PiperVoice
24
+
25
+ VOICES_DIR = Path(os.environ.get("VOICES_DIR", "/voices"))
26
+ DEFAULT_VOICE = "en_GB-alan-low"
27
+
28
+ LANG_VOICE_MAP = {
29
+ "ar": "ar_JO-kareem-medium",
30
+ "en": "en_GB-alan-low",
31
+ }
32
+
33
+ app = FastAPI(title="Palm Tree TTS — Piper")
34
+
35
+ app.add_middleware(
36
+ CORSMiddleware,
37
+ allow_origins=["*"],
38
+ allow_methods=["GET", "POST", "OPTIONS"],
39
+ allow_headers=["*"],
40
+ )
41
+
42
+ _voice_cache: dict = {}
43
+
44
+
45
+ def load_voice(name: str):
46
+ if name in _voice_cache:
47
+ return _voice_cache[name]
48
+ onnx = VOICES_DIR / f"{name}.onnx"
49
+ cfg = VOICES_DIR / f"{name}.onnx.json"
50
+ if not onnx.exists() or not cfg.exists():
51
+ raise HTTPException(status_code=404, detail=f"voice '{name}' not found")
52
+ _voice_cache[name] = PiperVoice.load(str(onnx), config_path=str(cfg))
53
+ return _voice_cache[name]
54
+
55
+
56
+ def available_voices():
57
+ return sorted(p.stem for p in VOICES_DIR.glob("*.onnx"))
58
+
59
+
60
+ def synth_wav(text: str, voice_name: str) -> bytes:
61
+ voice = load_voice(voice_name)
62
+ buf = io.BytesIO()
63
+ with wave.open(buf, "wb") as wf:
64
+ # Newer piper-tts uses synthesize_wav; fall back to synthesize for older builds.
65
+ synth = getattr(voice, "synthesize_wav", None) or voice.synthesize
66
+ synth(text, wf)
67
+ return buf.getvalue()
68
+
69
+
70
+ class SynthRequest(BaseModel):
71
+ text: str
72
+ voice: str | None = None
73
+ lang: str | None = None
74
+
75
+
76
+ @app.get("/")
77
+ def root():
78
+ return JSONResponse({"status": "ready", "voices": available_voices()})
79
+
80
+
81
+ @app.get("/healthz")
82
+ def healthz():
83
+ return Response("ok", media_type="text/plain")
84
+
85
+
86
+ def resolve_voice(req: SynthRequest) -> str:
87
+ if req.voice:
88
+ return req.voice
89
+ if req.lang:
90
+ return LANG_VOICE_MAP.get(req.lang.lower(), DEFAULT_VOICE)
91
+ return DEFAULT_VOICE
92
+
93
+
94
+ @app.post("/synthesize")
95
+ def synthesize(req: SynthRequest):
96
+ text = (req.text or "").strip()
97
+ if not text:
98
+ raise HTTPException(status_code=400, detail="text is required")
99
+ audio = synth_wav(text, resolve_voice(req))
100
+ return Response(audio, media_type="audio/wav")
101
+
102
+
103
+ @app.post("/tts")
104
+ def tts(req: SynthRequest):
105
+ return synthesize(req)
start.sh ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ VOICES_DIR="${VOICES_DIR:-/voices}"
5
+ BASE_URL="https://raw.githubusercontent.com/thuraya1988/123Learning-English-Under-The-Palm-Tree/main/public/tts-voices"
6
+
7
+ mkdir -p "${VOICES_DIR}"
8
+
9
+ for f in \
10
+ "en_GB-alan-low.onnx" \
11
+ "en_GB-alan-low.onnx.json" \
12
+ "ar_JO-kareem-medium.onnx" \
13
+ "ar_JO-kareem-medium.onnx.json"
14
+ do
15
+ out="${VOICES_DIR}/${f}"
16
+ if [[ ! -s "${out}" ]]; then
17
+ echo "Downloading ${f}..."
18
+ curl -fsSL "${BASE_URL}/${f}" -o "${out}"
19
+ fi
20
+ done
21
+
22
+ echo "Voices ready in ${VOICES_DIR}:"
23
+ ls -lh "${VOICES_DIR}"
24
+
25
+ exec uvicorn app:app --host 0.0.0.0 --port "${PORT:-7860}"