toolnet-tts 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- toolnet/__init__.py +3 -0
- toolnet/engine.py +11 -0
- toolnet/text.py +50 -0
- toolnet_tts/__init__.py +3 -0
- toolnet_tts/cli.py +27 -0
- toolnet_tts/client.py +51 -0
- toolnet_tts/server.py +51 -0
- toolnet_tts-0.2.0.dist-info/METADATA +64 -0
- toolnet_tts-0.2.0.dist-info/RECORD +12 -0
- toolnet_tts-0.2.0.dist-info/WHEEL +5 -0
- toolnet_tts-0.2.0.dist-info/entry_points.txt +2 -0
- toolnet_tts-0.2.0.dist-info/top_level.txt +2 -0
toolnet/__init__.py
ADDED
toolnet/engine.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class TTS:
|
|
2
|
+
def __init__(self, voice="giong1", device="auto"):
|
|
3
|
+
from vieneu import Vieneu
|
|
4
|
+
|
|
5
|
+
self.voice = "Thiện Minh" if voice == "giong1" else voice
|
|
6
|
+
self.engine = Vieneu(mode="v3turbo", device=device)
|
|
7
|
+
|
|
8
|
+
def say(self, text, path="out.wav"):
|
|
9
|
+
audio = self.engine.infer(text, voice=self.voice)
|
|
10
|
+
self.engine.save(audio, str(path))
|
|
11
|
+
return path
|
toolnet/text.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
_D = ["không", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín"]
|
|
4
|
+
_ABBR = {"TP.HCM": "thành phố Hồ Chí Minh", "UBND": "uỷ ban nhân dân", "VND": "đồng"}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _read_3(n, full=False):
|
|
8
|
+
h, t, u = n // 100, (n % 100) // 10, n % 10
|
|
9
|
+
out = []
|
|
10
|
+
if h or full:
|
|
11
|
+
out += [_D[h], "trăm"]
|
|
12
|
+
if t > 1:
|
|
13
|
+
out += [_D[t], "mươi"]
|
|
14
|
+
elif t == 1:
|
|
15
|
+
out.append("mười")
|
|
16
|
+
elif (h or full) and u:
|
|
17
|
+
out.append("lẻ")
|
|
18
|
+
if u:
|
|
19
|
+
if t > 1 and u == 1:
|
|
20
|
+
out.append("mốt")
|
|
21
|
+
elif t >= 1 and u == 5:
|
|
22
|
+
out.append("lăm")
|
|
23
|
+
elif t > 1 and u == 4:
|
|
24
|
+
out.append("tư")
|
|
25
|
+
else:
|
|
26
|
+
out.append(_D[u])
|
|
27
|
+
return " ".join(out)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def number_to_words(n):
|
|
31
|
+
if n == 0:
|
|
32
|
+
return "không"
|
|
33
|
+
parts, units = [], ["", "nghìn", "triệu", "tỷ"]
|
|
34
|
+
groups = []
|
|
35
|
+
while n:
|
|
36
|
+
groups.append(n % 1000)
|
|
37
|
+
n //= 1000
|
|
38
|
+
for i in range(len(groups) - 1, -1, -1):
|
|
39
|
+
g = groups[i]
|
|
40
|
+
if g:
|
|
41
|
+
parts.append(_read_3(g, full=(i < len(groups) - 1)))
|
|
42
|
+
parts.append(units[i])
|
|
43
|
+
return " ".join(p for p in parts if p)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def clean(text):
|
|
47
|
+
for k, v in _ABBR.items():
|
|
48
|
+
text = text.replace(k, v)
|
|
49
|
+
text = re.sub(r"\d+", lambda m: number_to_words(int(m.group())), text)
|
|
50
|
+
return re.sub(r"\s+", " ", text).strip().lower()
|
toolnet_tts/__init__.py
ADDED
toolnet_tts/cli.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from .client import Communicate, ToolnetError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(description="Chuyển văn bản tiếng Việt thành giọng nói ToolNet.")
|
|
7
|
+
source = parser.add_mutually_exclusive_group(required=True)
|
|
8
|
+
source.add_argument("--text", help="Văn bản cần đọc")
|
|
9
|
+
source.add_argument("--file", help="File văn bản UTF-8")
|
|
10
|
+
parser.add_argument("--write-media", required=True, help="Đường dẫn file WAV")
|
|
11
|
+
parser.add_argument("--server", help="URL máy chủ (hoặc biến TOOLNET_TTS_SERVER)")
|
|
12
|
+
parser.add_argument("--voice", default="Thiện Minh")
|
|
13
|
+
args = parser.parse_args()
|
|
14
|
+
try:
|
|
15
|
+
if args.file:
|
|
16
|
+
with open(args.file, encoding="utf-8-sig") as file:
|
|
17
|
+
text = file.read()
|
|
18
|
+
else:
|
|
19
|
+
text = args.text
|
|
20
|
+
Communicate(text, args.voice, server=args.server).save_sync(args.write_media)
|
|
21
|
+
except (ToolnetError, ValueError, OSError) as exc:
|
|
22
|
+
parser.exit(1, f"toolnet-tts: {exc}\n")
|
|
23
|
+
print(f"Đã tạo {args.write_media}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
main()
|
toolnet_tts/client.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""A small HTTP client for a ToolNet TTS server."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.error import HTTPError, URLError
|
|
7
|
+
from urllib.request import Request, urlopen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolnetError(RuntimeError):
|
|
11
|
+
"""The speech service could not fulfil a request."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Communicate:
|
|
15
|
+
def __init__(self, text, voice="Thiện Minh", *, server=None, api_key=None, timeout=180):
|
|
16
|
+
if not isinstance(text, str) or not text.strip():
|
|
17
|
+
raise ValueError("text must be a non-empty string")
|
|
18
|
+
self.text = text
|
|
19
|
+
self.voice = voice
|
|
20
|
+
self.server = (server or os.getenv("TOOLNET_TTS_SERVER", "")).rstrip("/")
|
|
21
|
+
if not self.server.startswith(("https://", "http://")):
|
|
22
|
+
raise ValueError("Set server= or TOOLNET_TTS_SERVER to the speech server URL")
|
|
23
|
+
self.api_key = api_key if api_key is not None else os.getenv("TOOLNET_TTS_API_KEY")
|
|
24
|
+
self.timeout = timeout
|
|
25
|
+
|
|
26
|
+
def save_sync(self, path):
|
|
27
|
+
path = Path(path)
|
|
28
|
+
if path.suffix.lower() != ".wav":
|
|
29
|
+
raise ValueError("Output must have a .wav extension")
|
|
30
|
+
payload = json.dumps({"text": self.text, "voice": self.voice}, ensure_ascii=False).encode("utf-8")
|
|
31
|
+
headers = {"Content-Type": "application/json", "Accept": "audio/wav"}
|
|
32
|
+
if self.api_key:
|
|
33
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
34
|
+
request = Request(f"{self.server}/v1/speech", data=payload, headers=headers, method="POST")
|
|
35
|
+
try:
|
|
36
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
37
|
+
content_type = response.headers.get_content_type()
|
|
38
|
+
if content_type not in ("audio/wav", "audio/x-wav"):
|
|
39
|
+
raise ToolnetError(f"Unexpected response type: {content_type}")
|
|
40
|
+
audio = response.read()
|
|
41
|
+
except HTTPError as exc:
|
|
42
|
+
raise ToolnetError(f"Speech server returned HTTP {exc.code}") from exc
|
|
43
|
+
except (URLError, TimeoutError, OSError) as exc:
|
|
44
|
+
raise ToolnetError(f"Cannot reach speech server: {exc}") from exc
|
|
45
|
+
if len(audio) < 44 or audio[:4] != b"RIFF" or audio[8:12] != b"WAVE":
|
|
46
|
+
raise ToolnetError("Speech server returned invalid WAV data")
|
|
47
|
+
path.write_bytes(audio)
|
|
48
|
+
return str(path)
|
|
49
|
+
|
|
50
|
+
async def save(self, path):
|
|
51
|
+
return await asyncio.to_thread(self.save_sync, path)
|
toolnet_tts/server.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Self-hosted VieNeu inference service. Install with toolnet-tts[server]."""
|
|
2
|
+
import hmac
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, Header, HTTPException
|
|
8
|
+
from fastapi.responses import Response
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
from starlette.concurrency import run_in_threadpool
|
|
11
|
+
|
|
12
|
+
app = FastAPI(title="ToolNet TTS")
|
|
13
|
+
_engine = None
|
|
14
|
+
_lock = threading.Lock()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SpeechRequest(BaseModel):
|
|
18
|
+
text: str = Field(min_length=1, max_length=10000)
|
|
19
|
+
voice: str = "Thiện Minh"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _synthesize(text: str, voice: str) -> bytes:
|
|
23
|
+
global _engine
|
|
24
|
+
import soundfile as sf
|
|
25
|
+
from vieneu import Vieneu
|
|
26
|
+
|
|
27
|
+
with _lock:
|
|
28
|
+
if _engine is None:
|
|
29
|
+
_engine = Vieneu(mode="v3turbo", device=os.getenv("TOOLNET_TTS_DEVICE", "auto"))
|
|
30
|
+
audio = _engine.infer(text, voice=voice)
|
|
31
|
+
output = io.BytesIO()
|
|
32
|
+
sf.write(output, audio, _engine.sample_rate, format="WAV", subtype="PCM_16")
|
|
33
|
+
return output.getvalue()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.get("/health")
|
|
37
|
+
def health():
|
|
38
|
+
return {"status": "ok", "model_loaded": _engine is not None}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.post("/v1/speech")
|
|
42
|
+
async def speech(request: SpeechRequest, authorization: str = Header(default="")):
|
|
43
|
+
key = os.getenv("TOOLNET_TTS_API_KEY")
|
|
44
|
+
if key and not hmac.compare_digest(authorization, f"Bearer {key}"):
|
|
45
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
46
|
+
if not request.text.strip():
|
|
47
|
+
raise HTTPException(status_code=422, detail="Text is empty")
|
|
48
|
+
if request.voice not in ("Thiện Minh", "giong1"):
|
|
49
|
+
raise HTTPException(status_code=400, detail="Voice is not supported")
|
|
50
|
+
audio = await run_in_threadpool(_synthesize, request.text, "Thiện Minh")
|
|
51
|
+
return Response(audio, media_type="audio/wav")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: toolnet-tts
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Lightweight client for ToolNet Vietnamese text-to-speech
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: server
|
|
8
|
+
Requires-Dist: fastapi<1,>=0.115; extra == "server"
|
|
9
|
+
Requires-Dist: uvicorn<1,>=0.30; extra == "server"
|
|
10
|
+
Requires-Dist: vieneu==3.8.4; extra == "server"
|
|
11
|
+
Requires-Dist: onnxruntime<1.23,>=1.20; extra == "server"
|
|
12
|
+
|
|
13
|
+
# toolnet-tts
|
|
14
|
+
|
|
15
|
+
Client Python và Node.js gửi văn bản đến máy chủ ToolNet TTS để tạo WAV bằng giọng Thiện Minh của VieNeu v3 Turbo. Client không tải model; máy chủ cần cài model và các phụ thuộc riêng. Cần có URL máy chủ để sử dụng.
|
|
16
|
+
|
|
17
|
+
## Python
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install toolnet-tts
|
|
21
|
+
export TOOLNET_TTS_SERVER="https://your-server.example"
|
|
22
|
+
# Nếu máy chủ yêu cầu khóa:
|
|
23
|
+
export TOOLNET_TTS_API_KEY="your-api-key"
|
|
24
|
+
toolnet-tts --text "Xin chào các bạn" --write-media hello.wav
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import asyncio
|
|
29
|
+
from toolnet_tts import Communicate
|
|
30
|
+
|
|
31
|
+
asyncio.run(Communicate("Xin chào các bạn").save("hello.wav"))
|
|
32
|
+
# Hoặc đồng bộ: Communicate("Xin chào").save_sync("hello.wav")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Có thể đặt URL bằng `server="https://your-server.example"`. Chỉ hỗ trợ WAV; hiện không hỗ trợ MP3, word boundaries hoặc phát trực tiếp.
|
|
36
|
+
|
|
37
|
+
## Node.js
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install toolnet-tts
|
|
41
|
+
export TOOLNET_TTS_SERVER="https://your-server.example"
|
|
42
|
+
npx toolnet-tts --text "Xin chào các bạn" --write-media hello.wav
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { Communicate } from 'toolnet-tts';
|
|
47
|
+
await new Communicate('Xin chào các bạn').save('hello.wav');
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Chạy máy chủ riêng
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install 'toolnet-tts[server]'
|
|
54
|
+
export TOOLNET_TTS_API_KEY="replace-with-a-long-random-key"
|
|
55
|
+
uvicorn toolnet_tts.server:app --host 127.0.0.1 --port 8000
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Đặt `TOOLNET_TTS_DEVICE=cpu` để chạy CPU. Lần đầu cần tải model VieNeu; nếu dùng GPU cần thêm các phụ thuộc CUDA tương thích. `/health` báo tiến trình và trạng thái nạp model; `POST /v1/speech` nhận JSON `{"text":"...","voice":"Thiện Minh"}` và trả WAV. Đặt `TOOLNET_TTS_API_KEY` ở cả client và server để xác thực Bearer. Nếu mở máy chủ cho client ở xa, cần cấu hình HTTPS và mạng phù hợp; lệnh trên chỉ lắng nghe tại localhost.
|
|
59
|
+
|
|
60
|
+
Giọng và model: [VieNeu-TTS v3 Turbo](https://huggingface.co/pnnbao-ump/VieNeu-TTS-v3-Turbo), Apache-2.0. Dự án này sử dụng model gốc; không đưa checkpoint Piper thử nghiệm vào gói client.
|
|
61
|
+
|
|
62
|
+
## Chạy cục bộ kiểu cũ
|
|
63
|
+
|
|
64
|
+
`from toolnet import TTS` vẫn có thể dùng sau khi cài `toolnet-tts[server]`. API này tải và chạy VieNeu trên chính máy gọi, không dùng máy chủ.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
toolnet/__init__.py,sha256=rGUnFxpGA_sOO8efCNjNFOPyk3ipp4_nGMlqX_znkjE,43
|
|
2
|
+
toolnet/engine.py,sha256=5INBZY2u_eXc7w6zIuUAVoJ80-utpHv1qEWyhMXWIHw,392
|
|
3
|
+
toolnet/text.py,sha256=xqiom_AlLmmBufUdKq0NFPERa7eUGC8t715Eqj4EBk4,1391
|
|
4
|
+
toolnet_tts/__init__.py,sha256=-jZGmk2Xf2cZ16aD101001U5X89Eb0J5Uk2zogJB8ME,89
|
|
5
|
+
toolnet_tts/cli.py,sha256=RCpcDQ7-Nef8ktVOq0wtLCPTfQEMP_NlDd9zqLJA_uU,1105
|
|
6
|
+
toolnet_tts/client.py,sha256=PD_0YXSc6CvNsJzOXLn0FsgWicVMJvlm3xSlsXaHB_8,2382
|
|
7
|
+
toolnet_tts/server.py,sha256=QzKFSIfFjmLbNGMmQFZx77ySXwPQU2dRKa_FGPESHK8,1725
|
|
8
|
+
toolnet_tts-0.2.0.dist-info/METADATA,sha256=iE1YIhf9RxzK5AgUucssa0gAZ4RGMPaF-g-zjumqXus,2745
|
|
9
|
+
toolnet_tts-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
toolnet_tts-0.2.0.dist-info/entry_points.txt,sha256=n1ZK89GzaXROtCodI2Br3RcMHEy_cphJ92Ew3tROcV4,53
|
|
11
|
+
toolnet_tts-0.2.0.dist-info/top_level.txt,sha256=jhs3eUnPhSz_aMq3JQHiW-Cc6ZNUZSv44mD6WkUXqSM,20
|
|
12
|
+
toolnet_tts-0.2.0.dist-info/RECORD,,
|