@visionengine/video-style-transfer 1.0.0

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.
package/README-zh.md ADDED
@@ -0,0 +1,24 @@
1
+ # VE 视频风格重绘 MCP
2
+
3
+ 通过 `ve-backend` 代理实现阿里云 DashScope 视频风格重绘的异步 MCP 服务。
4
+
5
+ ## 环境变量
6
+
7
+ - `API_URL`:后端代理地址,默认 `https://api.visionengine-tech.com/api/v1/video-style-transfer`
8
+ - `API_KEY`:VisionEngine 平台用户 API Key
9
+ - `WORKDIR`:本地工作目录根路径
10
+ - `DEFAULT_OUTPUT_DIR`:默认输出目录,默认 `public/videos`
11
+
12
+ ## 工具
13
+
14
+ - `video_style_transfer`
15
+
16
+ 当前版本 `videoPath` 支持两种输入:
17
+
18
+ - 公网可访问视频 URL
19
+ - 后端与 MCP 运行时共同挂载的 NAS 路径
20
+
21
+ 如果 DashScope 最终返回的是 NAS 文件路径而不是直接 URL,可由后端结合以下环境变量转换出下载地址:
22
+
23
+ - `VIDEO_STYLE_TRANSFER_SHARED_BASE_URL`
24
+ - `VIDEO_STYLE_TRANSFER_SHARED_MOUNT_ROOT`
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # VE Video Style Transfer MCP
2
+
3
+ Async MCP server for video style redraw via `ve-backend` proxy.
4
+
5
+ ## Environment
6
+
7
+ - `API_URL`: backend base url, default `https://api.visionengine-tech.com/api/v1/video-style-transfer`
8
+ - `API_KEY`: user API key from VisionEngine backend
9
+ - `WORKDIR`: local workspace root
10
+ - `DEFAULT_OUTPUT_DIR`: default relative output directory, default `public/videos`
11
+
12
+ ## Tool
13
+
14
+ - `video_style_transfer`
15
+
16
+ `videoPath` can be either:
17
+
18
+ - a public video URL, or
19
+ - a shared NAS path that both backend and MCP runtime can access.
20
+
21
+ If DashScope returns a shared file path instead of a direct URL, backend can expose it by configuring:
22
+
23
+ - `VIDEO_STYLE_TRANSFER_SHARED_BASE_URL`
24
+ - `VIDEO_STYLE_TRANSFER_SHARED_MOUNT_ROOT`
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { server } from "./server.js";
3
+ server.start({
4
+ transportType: "stdio",
5
+ });
package/dist/server.js ADDED
@@ -0,0 +1,185 @@
1
+ import { FastMCP } from "fastmcp";
2
+ import { z } from "zod";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import * as http from "http";
6
+ import * as https from "https";
7
+ import * as crypto from "crypto";
8
+ const server = new FastMCP({
9
+ name: "VE Video Style Transfer",
10
+ version: "1.0.0",
11
+ });
12
+ const API_URL = process.env.API_URL || "https://api.visionengine-tech.com/api/v1/video-style-transfer";
13
+ const API_KEY = process.env.API_KEY || "";
14
+ const WORKDIR = process.env.WORKDIR || "./";
15
+ const DEFAULT_OUTPUT_DIR = process.env.DEFAULT_OUTPUT_DIR || "public/videos";
16
+ function getWorkDir() {
17
+ return path.isAbsolute(WORKDIR) ? WORKDIR : path.resolve(process.cwd(), WORKDIR);
18
+ }
19
+ function ensureDir(dirPath) {
20
+ if (!fs.existsSync(dirPath)) {
21
+ fs.mkdirSync(dirPath, { recursive: true });
22
+ }
23
+ return dirPath;
24
+ }
25
+ function resolveInputPath(inputPath) {
26
+ return path.isAbsolute(inputPath) ? inputPath : path.resolve(getWorkDir(), inputPath);
27
+ }
28
+ function getDefaultOutputDir() {
29
+ const workDir = getWorkDir();
30
+ const outputDir = path.isAbsolute(DEFAULT_OUTPUT_DIR)
31
+ ? DEFAULT_OUTPUT_DIR
32
+ : path.join(workDir, DEFAULT_OUTPUT_DIR);
33
+ return ensureDir(outputDir);
34
+ }
35
+ function generateFilename(extension = ".mp4") {
36
+ return `styled_${Date.now()}_${crypto.randomBytes(4).toString("hex")}${extension}`;
37
+ }
38
+ function isHttpUrl(value) {
39
+ return /^https?:\/\//i.test(value);
40
+ }
41
+ function detectMediaUrl(input) {
42
+ if (isHttpUrl(input)) {
43
+ return input;
44
+ }
45
+ const fullPath = resolveInputPath(input);
46
+ if (!fs.existsSync(fullPath)) {
47
+ throw new Error(`Video file not found: ${fullPath}`);
48
+ }
49
+ return fullPath;
50
+ }
51
+ async function requestJson(url, init) {
52
+ if (!API_KEY) {
53
+ throw new Error("API_KEY environment variable is required");
54
+ }
55
+ const response = await fetch(url, {
56
+ method: init.method,
57
+ headers: {
58
+ Authorization: `Bearer ${API_KEY}`,
59
+ "Content-Type": "application/json",
60
+ },
61
+ body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
62
+ });
63
+ if (!response.ok) {
64
+ throw new Error(`Backend API error (${response.status}): ${await response.text()}`);
65
+ }
66
+ return (await response.json());
67
+ }
68
+ async function downloadFile(url, outputPath) {
69
+ const parsed = new URL(url);
70
+ const mod = parsed.protocol === "https:" ? https : http;
71
+ await new Promise((resolve, reject) => {
72
+ const request = mod.get(url, (response) => {
73
+ if (response.statusCode && response.statusCode >= 400) {
74
+ reject(new Error(`Failed to download file: HTTP ${response.statusCode}`));
75
+ return;
76
+ }
77
+ const stream = fs.createWriteStream(outputPath);
78
+ response.pipe(stream);
79
+ stream.on("finish", () => {
80
+ stream.close();
81
+ resolve();
82
+ });
83
+ stream.on("error", reject);
84
+ });
85
+ request.on("error", reject);
86
+ });
87
+ }
88
+ async function pollTask(taskId, pollIntervalMs, timeoutMs) {
89
+ const started = Date.now();
90
+ while (true) {
91
+ const result = await requestJson(`${API_URL}/query?task_id=${encodeURIComponent(taskId)}`, { method: "GET" });
92
+ if (["SUCCEEDED", "FAILED", "CANCELED", "CANCELLED", "UNKNOWN"].includes(result.task_status)) {
93
+ return result;
94
+ }
95
+ if (Date.now() - started > timeoutMs) {
96
+ throw new Error(`Video style transfer timed out after ${timeoutMs}ms`);
97
+ }
98
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
99
+ }
100
+ }
101
+ async function transferVideoStyle(options) {
102
+ const videoUrl = detectMediaUrl(options.videoPath);
103
+ const submitPayload = {
104
+ video_url: videoUrl,
105
+ parameters: {
106
+ style: options.style ?? 0,
107
+ video_fps: options.videoFps ?? 15,
108
+ animate_emotion: options.animateEmotion ?? true,
109
+ min_len: options.minLen ?? 720,
110
+ use_SR: options.useSR ?? false,
111
+ },
112
+ };
113
+ const submitResult = await requestJson(`${API_URL}/submit`, {
114
+ method: "POST",
115
+ body: submitPayload,
116
+ });
117
+ const queryResult = await pollTask(submitResult.task_id, options.pollIntervalMs ?? 5000, options.timeoutMs ?? 10 * 60 * 1000);
118
+ if (queryResult.task_status !== "SUCCEEDED") {
119
+ throw new Error(`Video style transfer failed with status ${queryResult.task_status}`);
120
+ }
121
+ if (!queryResult.output_video_url) {
122
+ throw new Error("Task succeeded but output_video_url is missing");
123
+ }
124
+ let resolvedOutputPath;
125
+ if (options.outputPath) {
126
+ resolvedOutputPath = path.isAbsolute(options.outputPath)
127
+ ? options.outputPath
128
+ : path.resolve(getWorkDir(), options.outputPath);
129
+ ensureDir(path.dirname(resolvedOutputPath));
130
+ }
131
+ else {
132
+ const outputDir = getDefaultOutputDir();
133
+ resolvedOutputPath = path.join(outputDir, generateFilename(".mp4"));
134
+ }
135
+ await downloadFile(queryResult.output_video_url, resolvedOutputPath);
136
+ return {
137
+ success: true,
138
+ taskId: submitResult.task_id,
139
+ taskStatus: queryResult.task_status,
140
+ style: options.style ?? 0,
141
+ outputVideoPath: resolvedOutputPath,
142
+ outputVideoUrl: queryResult.output_video_url,
143
+ usage: {
144
+ duration: queryResult.usage?.duration ?? null,
145
+ sr: queryResult.usage?.sr ?? null,
146
+ },
147
+ billingStatus: queryResult.billing_status,
148
+ message: "Video style transfer completed successfully",
149
+ };
150
+ }
151
+ server.addTool({
152
+ annotations: {
153
+ openWorldHint: true,
154
+ readOnlyHint: false,
155
+ title: "Video Style Transfer",
156
+ },
157
+ description: "Submit a video style redraw task to the backend proxy, poll until completion, then download the generated video locally.",
158
+ execute: async (args) => {
159
+ const result = await transferVideoStyle({
160
+ videoPath: args.videoPath,
161
+ style: args.style,
162
+ videoFps: args.videoFps,
163
+ animateEmotion: args.animateEmotion,
164
+ minLen: args.minLen,
165
+ useSR: args.useSR,
166
+ outputPath: args.outputPath,
167
+ pollIntervalMs: args.pollIntervalMs,
168
+ timeoutMs: args.timeoutMs,
169
+ });
170
+ return JSON.stringify(result, null, 2);
171
+ },
172
+ name: "video_style_transfer",
173
+ parameters: z.object({
174
+ videoPath: z.string().describe("Public video URL, or shared NAS path visible to both backend and MCP runtime."),
175
+ style: z.number().int().min(0).max(7).optional().describe("Style index: 0 日式漫画, 1 美式漫画, 2 清新漫画, 3 3D卡通, 4 国风卡通, 5 纸艺风格, 6 简易插画, 7 国风水墨."),
176
+ videoFps: z.number().int().min(15).max(25).optional().describe("Output FPS, 15~25, default 15."),
177
+ animateEmotion: z.boolean().optional().describe("Whether to enable animate emotion optimization. Default true."),
178
+ minLen: z.union([z.literal(540), z.literal(720)]).optional().describe("Output short side resolution, 540 or 720. Default 720."),
179
+ useSR: z.boolean().optional().describe("Whether to enable super-resolution. Default false."),
180
+ outputPath: z.string().optional().describe("Output path for the generated video. Relative to WORKDIR or absolute path."),
181
+ pollIntervalMs: z.number().int().positive().optional().describe("Polling interval in milliseconds. Default 5000."),
182
+ timeoutMs: z.number().int().positive().optional().describe("Overall timeout in milliseconds. Default 600000 (10 minutes)."),
183
+ }),
184
+ });
185
+ export { server };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@visionengine/video-style-transfer",
3
+ "version": "1.0.0",
4
+ "description": "VisionEngine Video Style Transfer MCP Server - Async video style redraw via backend proxy",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "ve-video-style-transfer": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "README-zh.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "clear": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "video-style-transfer",
25
+ "video-redraw",
26
+ "dashscope",
27
+ "visionengine"
28
+ ],
29
+ "author": "team@visionengine-tech.com",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/crazyyanchao/ve-mcp.git",
34
+ "directory": "packages/video-style-transfer"
35
+ },
36
+ "homepage": "https://visionengine-tech.com/mcp",
37
+ "dependencies": {
38
+ "fastmcp": "^3.26.8",
39
+ "zod": "^4.1.12"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^24.10.1",
43
+ "typescript": "^5.8.3",
44
+ "vitest": "^3.1.3"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ }
49
+ }