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 +101 -0
- zvex-0.1.0/PKG-INFO +100 -0
- zvex-0.1.0/README.md +87 -0
- zvex-0.1.0/pyproject.toml +27 -0
- zvex-0.1.0/src/zvex/__init__.py +3 -0
- zvex-0.1.0/src/zvex/server.py +222 -0
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,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()
|