zvex 0.1.0__tar.gz

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.
zvex-0.1.0/.gitignore ADDED
@@ -0,0 +1,101 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
4
+ *.swp
5
+ .git/*
6
+ __pycache__/
7
+ *.pyc
8
+ .venv
9
+ *.o
10
+ *.a
11
+ /vendor
12
+ *.egg-info/
13
+ /.env
14
+ .env
15
+ .zcode/
16
+
17
+ # 视频文件
18
+ *.mp4
19
+ *.avi
20
+ *.mov
21
+ *.wmv
22
+ *.flv
23
+ *.mkv
24
+ *.webm
25
+ *.m4v
26
+ *.mpeg
27
+ *.mpg
28
+ *.3gp
29
+ *.f4v
30
+ *.rmvb
31
+ *.vob
32
+
33
+ # 音频文件
34
+ *.wav
35
+ *.mp3
36
+ *.flac
37
+ *.ogg
38
+
39
+ # 模型权重
40
+ models/
41
+
42
+ # 运行时输出
43
+ outputs/
44
+ uploads/
45
+ *.log
46
+
47
+ # 第三方源码
48
+ third_party/
49
+
50
+ # ASD 研究参考代码(4 个克隆仓库,含 demo 输出和大权重,可重新克隆)
51
+ research/
52
+
53
+ # Claude
54
+ .claude/
55
+
56
+ # 归档文件
57
+ *.iso
58
+ *.dmg
59
+ *.rar
60
+ *.zip
61
+ *.gz
62
+
63
+ # 文档类也默认加入
64
+ *.pdf
65
+ *.docx
66
+ *.doc
67
+ *.xlsx
68
+ *.xls
69
+ *.ppt
70
+ *.pptx
71
+ *.xlsx
72
+ *.csv
73
+
74
+ # Python venv variants
75
+ .venv-qwen3/
76
+ .venv-*/
77
+ .venv-*
78
+ server.pid
79
+
80
+ # OmniVoice 隔离 venv / 根治迁移备份
81
+ .venvs/
82
+ .venv-omnivoice/
83
+ .venv-kazakhtts/
84
+ .venv-kazemotts/
85
+ kazemotts_src/
86
+ .venv.ntfs-bak/
87
+ .secrets/
88
+ cert_*.zip
89
+ *.pem
90
+
91
+ # voice bank 本地资产(大体积 wav + 频繁变化的 embeddings,不入库)
92
+ voice_bank_profiles/
93
+ .venv-orpheus/
94
+ scripts/server_freezes/
95
+ .env.bak_*
96
+
97
+ # 会话规划/评审过程产物(Manus 式工作目录,不入库)
98
+ .planning/
99
+
100
+ # Codex isolated feature worktrees
101
+ .worktrees/
zvex-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.5
2
+ Name: zvex
3
+ Version: 0.1.0
4
+ Summary: zvex (声桥) MCP server — AI video dubbing: hand it a video URL and get back a dubbed video that keeps the original speakers' voices, billed via zvex credits
5
+ Project-URL: Homepage, https://tts.xalhar.top
6
+ Author: zvex
7
+ License: MIT
8
+ Keywords: dubbing,mcp,translation,tts,video
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx>=0.27.0
11
+ Requires-Dist: mcp>=2.2.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # zvex MCP server
15
+
16
+ AI video dubbing as an MCP tool: hand it a video URL, get back a fully dubbed
17
+ video in Russian, English or Spanish — keeping the original speakers' voices
18
+ through per-speaker voice cloning.
19
+
20
+ Powered by [zvex (声桥)](https://tts.xalhar.top).
21
+
22
+ The server is a thin stdio client over zvex's HTTP API, so it runs
23
+ anywhere Python does — no GPU, no local models.
24
+
25
+ ## Install
26
+
27
+ From a checkout:
28
+
29
+ ```bash
30
+ uv tool install ./mcp/zvex
31
+ # or: pip install ./mcp/zvex
32
+ ```
33
+
34
+ ## Get an API key
35
+
36
+ 1. Sign in at <https://tts.xalhar.top>
37
+ 2. Open **Account → API keys** and create one
38
+ 3. Copy the `zvex-…` value — it is shown **only once**
39
+
40
+ Jobs are billed from the same credit balance as the web app, at a flat
41
+ **10 credits per minute** of video. A failed job is refunded in full.
42
+
43
+ ## Configure your MCP client
44
+
45
+ Claude Desktop (`claude_desktop_config.json`) or Cursor (`.cursor/mcp.json`):
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "zvex": {
51
+ "command": "zvex",
52
+ "env": {
53
+ "ZVEX_API_KEY": "zvex-your-key-here"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ | Variable | Required | Default | Meaning |
61
+ |---|---|---|---|
62
+ | `ZVEX_API_KEY` | yes | — | `zvex-…` key from the account page |
63
+ | `ZVEX_BASE_URL` | no | `https://tts.xalhar.top` | API base URL (self-hosted deployments) |
64
+
65
+ ## Tools
66
+
67
+ | Tool | Purpose |
68
+ |---|---|
69
+ | `estimate_cost(minutes, tier)` | Credit cost and current balance |
70
+ | `submit_dubbing_job(video_url, target_language, tier)` | Queue a dubbing job, returns `job_id` |
71
+ | `get_job_status(job_id)` | Poll once; final states carry the output URLs |
72
+ | `wait_for_job(job_id, timeout_seconds)` | Block until the job finishes |
73
+
74
+ Typical flow:
75
+
76
+ ```
77
+ submit_dubbing_job("https://example.com/episode-01.mp4", target_language="ru")
78
+ → {"job_id": 42, "credits_cost": 240, "duration_sec": 1441.0, …}
79
+ wait_for_job(42)
80
+ → {"status": "completed", "final_video_url": "…", "subtitle_url": "…"}
81
+ ```
82
+
83
+ ### Notes
84
+
85
+ - `video_url` must be a publicly reachable `http(s)` link, up to 500 MB.
86
+ - `target_language` depends on the deployment (`ru`, `en`, `es` on the hosted
87
+ service).
88
+ - `tier` is `fast`, `standard` or `professional` — it selects which features
89
+ are available, not the price.
90
+ - One job per account runs at a time; a second submission returns HTTP 409.
91
+ - Output links are served from the zvex domain and require being signed
92
+ in there.
93
+
94
+ ## Development
95
+
96
+ ```bash
97
+ uv venv --python 3.11 .venv-test
98
+ uv pip install --python .venv-test/bin/python -e .
99
+ ZVEX_API_KEY=zvex-… .venv-test/bin/python -m zvex.server
100
+ ```
zvex-0.1.0/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # zvex MCP server
2
+
3
+ AI video dubbing as an MCP tool: hand it a video URL, get back a fully dubbed
4
+ video in Russian, English or Spanish — keeping the original speakers' voices
5
+ through per-speaker voice cloning.
6
+
7
+ Powered by [zvex (声桥)](https://tts.xalhar.top).
8
+
9
+ The server is a thin stdio client over zvex's HTTP API, so it runs
10
+ anywhere Python does — no GPU, no local models.
11
+
12
+ ## Install
13
+
14
+ From a checkout:
15
+
16
+ ```bash
17
+ uv tool install ./mcp/zvex
18
+ # or: pip install ./mcp/zvex
19
+ ```
20
+
21
+ ## Get an API key
22
+
23
+ 1. Sign in at <https://tts.xalhar.top>
24
+ 2. Open **Account → API keys** and create one
25
+ 3. Copy the `zvex-…` value — it is shown **only once**
26
+
27
+ Jobs are billed from the same credit balance as the web app, at a flat
28
+ **10 credits per minute** of video. A failed job is refunded in full.
29
+
30
+ ## Configure your MCP client
31
+
32
+ Claude Desktop (`claude_desktop_config.json`) or Cursor (`.cursor/mcp.json`):
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "zvex": {
38
+ "command": "zvex",
39
+ "env": {
40
+ "ZVEX_API_KEY": "zvex-your-key-here"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ | Variable | Required | Default | Meaning |
48
+ |---|---|---|---|
49
+ | `ZVEX_API_KEY` | yes | — | `zvex-…` key from the account page |
50
+ | `ZVEX_BASE_URL` | no | `https://tts.xalhar.top` | API base URL (self-hosted deployments) |
51
+
52
+ ## Tools
53
+
54
+ | Tool | Purpose |
55
+ |---|---|
56
+ | `estimate_cost(minutes, tier)` | Credit cost and current balance |
57
+ | `submit_dubbing_job(video_url, target_language, tier)` | Queue a dubbing job, returns `job_id` |
58
+ | `get_job_status(job_id)` | Poll once; final states carry the output URLs |
59
+ | `wait_for_job(job_id, timeout_seconds)` | Block until the job finishes |
60
+
61
+ Typical flow:
62
+
63
+ ```
64
+ submit_dubbing_job("https://example.com/episode-01.mp4", target_language="ru")
65
+ → {"job_id": 42, "credits_cost": 240, "duration_sec": 1441.0, …}
66
+ wait_for_job(42)
67
+ → {"status": "completed", "final_video_url": "…", "subtitle_url": "…"}
68
+ ```
69
+
70
+ ### Notes
71
+
72
+ - `video_url` must be a publicly reachable `http(s)` link, up to 500 MB.
73
+ - `target_language` depends on the deployment (`ru`, `en`, `es` on the hosted
74
+ service).
75
+ - `tier` is `fast`, `standard` or `professional` — it selects which features
76
+ are available, not the price.
77
+ - One job per account runs at a time; a second submission returns HTTP 409.
78
+ - Output links are served from the zvex domain and require being signed
79
+ in there.
80
+
81
+ ## Development
82
+
83
+ ```bash
84
+ uv venv --python 3.11 .venv-test
85
+ uv pip install --python .venv-test/bin/python -e .
86
+ ZVEX_API_KEY=zvex-… .venv-test/bin/python -m zvex.server
87
+ ```
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "zvex"
3
+ version = "0.1.0"
4
+ description = "zvex (声桥) MCP server — AI video dubbing: hand it a video URL and get back a dubbed video that keeps the original speakers' voices, billed via zvex credits"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "zvex" }]
9
+ keywords = ["mcp", "video", "dubbing", "translation", "tts"]
10
+ dependencies = [
11
+ # 2.x 起 FastMCP 更名为 MCPServer(mcp.server.mcpserver),本包按 2.x API 编写
12
+ "mcp>=2.2.0",
13
+ "httpx>=0.27.0",
14
+ ]
15
+
16
+ [project.scripts]
17
+ zvex = "zvex.server:main"
18
+
19
+ [project.urls]
20
+ Homepage = "https://tts.xalhar.top"
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/zvex"]
@@ -0,0 +1,3 @@
1
+ """zvex (声桥) MCP server package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,222 @@
1
+ """声桥( zvex / zvex ) MCP server。
2
+
3
+ 把声桥的 AI 视频译制能力暴露给任意 MCP 客户端(Claude Desktop / Cursor / …):
4
+ 给一个视频链接,拿回多语种配音成片。全自动——服务端一阶段(识别/翻译)完成后
5
+ 自动衔接二阶段(配音/合成),不需要人工校正环节。
6
+
7
+ 鉴权走声桥 API Key(网页「账户设置 → API 密钥」创建,zvex- 开头),与网页端
8
+ 共用同一个账户积分池;提交时按服务端 ffprobe 实测时长预扣,任务失败全额退款。
9
+
10
+ 环境变量:
11
+ ZVEX_API_KEY 必填,形如 zvex-xxxxxxxx
12
+ ZVEX_BASE_URL 可选,默认 https://tts.xalhar.top(自建部署改这里)
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import json
18
+ import logging
19
+ import os
20
+ import sys
21
+
22
+ import httpx
23
+ from mcp.server.mcpserver import MCPServer
24
+
25
+ DEFAULT_BASE_URL = "https://tts.xalhar.top"
26
+ _REQUEST_TIMEOUT = 120.0 # 提交含服务端下载视频,给足时间
27
+ _CONNECT_TIMEOUT = 15.0
28
+ _POLL_INTERVAL = 15.0
29
+
30
+ # HTTP 状态码 → 给调用方(通常是 LLM)的可执行提示
31
+ _STATUS_HINTS = {
32
+ 401: "API key is missing, invalid, or revoked. Create a new one at "
33
+ "https://tts.xalhar.top/app/settings (Account → API keys).",
34
+ 402: "Not enough credits. Top up at https://tts.xalhar.top/app/pricing.",
35
+ 409: "Another job of this account is still running — wait for it to finish "
36
+ "(one concurrent job per account).",
37
+ 400: "The request was rejected by the server; see the detail above.",
38
+ }
39
+
40
+ mcp = MCPServer("zvex")
41
+
42
+
43
+ def _api_key() -> str:
44
+ key = os.getenv("ZVEX_API_KEY", "").strip()
45
+ if not key:
46
+ raise RuntimeError(
47
+ "ZVEX_API_KEY is not set. Create a key at "
48
+ "https://tts.xalhar.top/app/settings and pass it via the MCP server env."
49
+ )
50
+ return key
51
+
52
+
53
+ def _base_url() -> str:
54
+ return os.getenv("ZVEX_BASE_URL", DEFAULT_BASE_URL).rstrip("/")
55
+
56
+
57
+ def _client() -> httpx.AsyncClient:
58
+ return httpx.AsyncClient(
59
+ base_url=_base_url(),
60
+ headers={"Authorization": f"Bearer {_api_key()}"},
61
+ timeout=httpx.Timeout(_REQUEST_TIMEOUT, connect=_CONNECT_TIMEOUT),
62
+ )
63
+
64
+
65
+ # 服务端返回的是同源相对路径(/uploads/xxx.mp4),调用方(通常是别的机器上的
66
+ # LLM)需要能直接点开的绝对 URL。
67
+ _ABSOLUTE_KEYS = ("final_video_url", "subtitle_url", "poll")
68
+
69
+
70
+ def _absolutize(data: dict) -> dict:
71
+ if not isinstance(data, dict):
72
+ return data
73
+ base = _base_url()
74
+ for key in _ABSOLUTE_KEYS:
75
+ value = data.get(key)
76
+ if isinstance(value, str) and value.startswith("/"):
77
+ data[key] = base + value
78
+ return data
79
+
80
+
81
+ def _error_text(resp: httpx.Response) -> str:
82
+ """把 HTTP 错误翻成调用方能据以行动的文本(而不是抛异常中断会话)。"""
83
+ try:
84
+ detail = resp.json().get("detail")
85
+ except Exception:
86
+ detail = resp.text[:300]
87
+ text = f"HTTP {resp.status_code}: {detail}"
88
+ hint = _STATUS_HINTS.get(resp.status_code)
89
+ return f"{text} — {hint}" if hint else text
90
+
91
+
92
+ def _dump(payload: dict) -> str:
93
+ return json.dumps(payload, ensure_ascii=False, indent=2)
94
+
95
+
96
+ @mcp.tool()
97
+ async def estimate_cost(minutes: float, tier: str = "standard") -> str:
98
+ """Estimate the credit cost of dubbing a video and check the credit balance.
99
+
100
+ Pricing is a flat 10 credits per minute for every tier (a tier only changes
101
+ which features are available, not the price).
102
+
103
+ Args:
104
+ minutes: Video length in minutes (0 < minutes <= 600).
105
+ tier: "fast", "standard" or "professional".
106
+
107
+ Returns:
108
+ JSON with credits, current balance and whether the balance is enough.
109
+ """
110
+ async with _client() as client:
111
+ resp = await client.get("/api/v1/estimate", params={"minutes": minutes, "tier": tier})
112
+ if resp.status_code >= 400:
113
+ return _error_text(resp)
114
+ return _dump(resp.json())
115
+
116
+
117
+ @mcp.tool()
118
+ async def submit_dubbing_job(
119
+ video_url: str,
120
+ target_language: str = "ru",
121
+ tier: str = "standard",
122
+ ) -> str:
123
+ """Submit a fully automatic dubbing job: a video URL in, a dubbed video out.
124
+
125
+ The server downloads the video, then runs speech recognition, speaker
126
+ separation, translation, voice cloning/TTS and composition — no manual
127
+ review step. Credits are charged up front based on the server-measured
128
+ duration and refunded in full if the job fails.
129
+
130
+ The job runs in the background: use get_job_status to poll it, or
131
+ wait_for_job to block until it finishes. Only one job per account may run
132
+ at a time (a second submission returns HTTP 409).
133
+
134
+ Args:
135
+ video_url: Publicly reachable http(s) video link (max 500 MB).
136
+ target_language: Dubbing language, e.g. "ru", "en", "es"
137
+ (depends on the deployment's supported set).
138
+ tier: "fast", "standard" or "professional".
139
+
140
+ Returns:
141
+ JSON with job_id, run_id, measured duration and the charged credits.
142
+ """
143
+ async with _client() as client:
144
+ resp = await client.post(
145
+ "/api/v1/jobs",
146
+ json={"video_url": video_url, "target_language": target_language, "tier": tier},
147
+ )
148
+ if resp.status_code >= 400:
149
+ return _error_text(resp)
150
+ return _dump(_absolutize(resp.json()))
151
+
152
+
153
+ @mcp.tool()
154
+ async def get_job_status(job_id: int) -> str:
155
+ """Check a dubbing job's status.
156
+
157
+ Once the status is "completed" the response also carries the dubbed video
158
+ and subtitle download URLs. "failed" carries the error and means the
159
+ credits were refunded.
160
+
161
+ Args:
162
+ job_id: The job_id returned by submit_dubbing_job.
163
+
164
+ Returns:
165
+ JSON with status ("processing" / "completed" / "failed" / "cancelled")
166
+ and, for a completed job, final_video_url and subtitle_url.
167
+ """
168
+ async with _client() as client:
169
+ resp = await client.get(f"/api/v1/jobs/{job_id}")
170
+ if resp.status_code >= 400:
171
+ return _error_text(resp)
172
+ return _dump(_absolutize(resp.json()))
173
+
174
+
175
+ @mcp.tool()
176
+ async def wait_for_job(job_id: int, timeout_seconds: int = 1800) -> str:
177
+ """Block until a dubbing job reaches a final state, then return its result.
178
+
179
+ Convenience wrapper around get_job_status for callers that just want the
180
+ finished video. Dubbing typically takes a small multiple of the video's own
181
+ length, so keep the timeout generous.
182
+
183
+ Args:
184
+ job_id: The job_id returned by submit_dubbing_job.
185
+ timeout_seconds: Give up after this long (default 1800, max 7200).
186
+
187
+ Returns:
188
+ JSON with the final status and, when completed, the output URLs.
189
+ """
190
+ timeout_seconds = max(30, min(int(timeout_seconds), 7200))
191
+ deadline = asyncio.get_running_loop().time() + timeout_seconds
192
+ last = ""
193
+ async with _client() as client:
194
+ while True:
195
+ resp = await client.get(f"/api/v1/jobs/{job_id}")
196
+ if resp.status_code >= 400:
197
+ return _error_text(resp)
198
+ data = resp.json()
199
+ last = data.get("status", "")
200
+ if last in ("completed", "failed", "cancelled"):
201
+ return _dump(_absolutize(data))
202
+ if asyncio.get_running_loop().time() >= deadline:
203
+ return _dump({
204
+ "job_id": job_id,
205
+ "status": last,
206
+ "note": f"Still {last} after {timeout_seconds}s. "
207
+ "Call wait_for_job or get_job_status again to keep waiting.",
208
+ })
209
+ await asyncio.sleep(_POLL_INTERVAL)
210
+
211
+
212
+ def main() -> None:
213
+ """Entry point declared in pyproject ([project.scripts])."""
214
+ # stdio 传输下 stdout 是 JSON-RPC 协议通道:任何多余输出都会破坏协议。
215
+ # 显式把日志钉到 stderr,并关掉 httpx 的逐请求输出。
216
+ logging.basicConfig(stream=sys.stderr, level=logging.WARNING)
217
+ logging.getLogger("httpx").setLevel(logging.WARNING)
218
+ mcp.run()
219
+
220
+
221
+ if __name__ == "__main__":
222
+ main()