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 ADDED
@@ -0,0 +1,3 @@
1
+ from .engine import TTS
2
+
3
+ __all__ = ["TTS"]
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()
@@ -0,0 +1,3 @@
1
+ from .client import Communicate, ToolnetError
2
+
3
+ __all__ = ["Communicate", "ToolnetError"]
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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ toolnet-tts = toolnet_tts.cli:main
@@ -0,0 +1,2 @@
1
+ toolnet
2
+ toolnet_tts