@twick/mcp-agent 0.14.15
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.md +194 -0
- package/claude_desktop_config.json +18 -0
- package/dist/stdio-server.d.ts +2 -0
- package/dist/stdio-server.d.ts.map +1 -0
- package/dist/stdio-server.js +94 -0
- package/dist/stdio-server.js.map +1 -0
- package/dist/transcriber.d.ts +21 -0
- package/dist/transcriber.d.ts.map +1 -0
- package/dist/transcriber.js +281 -0
- package/dist/transcriber.js.map +1 -0
- package/dist/utils.d.ts +81 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +142 -0
- package/dist/utils.js.map +1 -0
- package/install.js +145 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# MCP Twick Server
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server for Claude Desktop that generates video captions using Google Vertex AI (Gemini) and integrates with Twick Studio.
|
|
4
|
+
|
|
5
|
+
## š¦ Installation
|
|
6
|
+
|
|
7
|
+
### Quick Install (Recommended)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone <repository-url>
|
|
11
|
+
cd twick-mcp-agent
|
|
12
|
+
npm run install-claude
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This will automatically install dependencies, build the project, and configure Claude Desktop.
|
|
16
|
+
|
|
17
|
+
See [SHIPPING.md](./SHIPPING.md) for distribution options and release information.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- š¬ **Video Transcription**: Transcribe videos from public URLs using Google Vertex AI
|
|
22
|
+
- š **Multi-language Support**: Support for multiple languages and fonts
|
|
23
|
+
- š **Subtitle Generation**: Generate timed subtitle files in Twick Studio format
|
|
24
|
+
- š **Twick Studio Integration**: Direct upload and link generation for Twick Studio
|
|
25
|
+
- š¦ **Claude Desktop Extension**: Easy one-click installation
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
- Node.js 18+ and npm
|
|
30
|
+
- Google Cloud Platform account with:
|
|
31
|
+
- Vertex AI API enabled
|
|
32
|
+
- Service account key file (`gcp-sa-key.json`)
|
|
33
|
+
- Claude Desktop installed
|
|
34
|
+
|
|
35
|
+
## Quick Installation (One-Click)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run install-claude
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This will:
|
|
42
|
+
1. Install all dependencies
|
|
43
|
+
2. Build the TypeScript project
|
|
44
|
+
3. Automatically merge the MCP server config into your Claude Desktop configuration
|
|
45
|
+
|
|
46
|
+
## Manual Installation
|
|
47
|
+
|
|
48
|
+
1. **Clone and install dependencies:**
|
|
49
|
+
```bash
|
|
50
|
+
git clone <your-repo-url>
|
|
51
|
+
cd mcp-agent
|
|
52
|
+
npm install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
2. **Build the project:**
|
|
56
|
+
```bash
|
|
57
|
+
npm run build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
3. **Configure Claude Desktop:**
|
|
61
|
+
- Open Claude Desktop settings
|
|
62
|
+
- Go to "MCP Servers" section
|
|
63
|
+
- Click "Import config from file"
|
|
64
|
+
- Select `claude_desktop_config.json` from this project
|
|
65
|
+
- Edit the environment variables with your values (see Configuration below)
|
|
66
|
+
|
|
67
|
+
4. **Restart Claude Desktop**
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
Edit the `twick-mcp-agent` section in your Claude Desktop config file:
|
|
72
|
+
|
|
73
|
+
**Windows:**
|
|
74
|
+
```
|
|
75
|
+
%APPDATA%\Claude\claude_desktop_config.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**macOS:**
|
|
79
|
+
```
|
|
80
|
+
~/Library/Application Support/Claude/claude_desktop_config.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Linux:**
|
|
84
|
+
```
|
|
85
|
+
~/.config/Claude/claude_desktop_config.json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Required Environment Variables
|
|
89
|
+
|
|
90
|
+
- `GOOGLE_CLOUD_PROJECT`: Your GCP project ID
|
|
91
|
+
- `GOOGLE_CLOUD_LOCATION`: GCP location (default: "global")
|
|
92
|
+
- `GOOGLE_APPLICATION_CREDENTIALS`: Absolute path to your `gcp-sa-key.json` file
|
|
93
|
+
|
|
94
|
+
### Optional Environment Variables
|
|
95
|
+
|
|
96
|
+
- `GOOGLE_VERTEX_MODEL`: Vertex AI model name (default: "gemini-2.5-flash-lite")
|
|
97
|
+
- `UPLOAD_API_URL`: Your upload API endpoint for uploading project files
|
|
98
|
+
- `TWICK_STUDIO_URL`: Twick Studio URL with `$project` placeholder (e.g., `https://studio.example.com?project-file=$project`)
|
|
99
|
+
|
|
100
|
+
### Example Configuration
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"twick-mcp-agent": {
|
|
106
|
+
"command": "node",
|
|
107
|
+
"args": ["C:\\path\\to\\mcp-agent\\dist\\stdio-server.js"],
|
|
108
|
+
"env": {
|
|
109
|
+
"GOOGLE_CLOUD_PROJECT": "my-gcp-project",
|
|
110
|
+
"GOOGLE_CLOUD_LOCATION": "global",
|
|
111
|
+
"GOOGLE_APPLICATION_CREDENTIALS": "C:\\path\\to\\mcp-agent\\gcp-sa-key.json",
|
|
112
|
+
"GOOGLE_VERTEX_MODEL": "gemini-2.5-flash-lite",
|
|
113
|
+
"UPLOAD_API_URL": "https://api.example.com/upload",
|
|
114
|
+
"TWICK_STUDIO_URL": "https://studio.example.com?project-file=$project"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Usage
|
|
122
|
+
|
|
123
|
+
Once installed, the `generate-subtitles` tool will be available in Claude Desktop. You can use it like:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
Generate subtitless for this video: https://example.com/video.mp4
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or with specific language settings:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Generate subtitless for https://example.com/video.mp4 in Spanish with Spanish font
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Tool Parameters
|
|
136
|
+
|
|
137
|
+
- `videoUrl` (required): Publicly accessible video URL
|
|
138
|
+
- `language` (optional): Target language for transcription (default: "english")
|
|
139
|
+
- `language_font` (optional): Font/script for subtitles (default: "english")
|
|
140
|
+
|
|
141
|
+
## Project Structure
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
mcp-agent/
|
|
145
|
+
āāā dist/ # Compiled JavaScript (generated)
|
|
146
|
+
āāā src/ # TypeScript source files
|
|
147
|
+
āāā stdio-server.ts # Main MCP server entry point
|
|
148
|
+
āāā transcriber.ts # Video transcription logic
|
|
149
|
+
āāā utils.ts # Utility functions
|
|
150
|
+
āāā claude_desktop_config.json # Claude Desktop config template
|
|
151
|
+
āāā install.js # Installation script
|
|
152
|
+
āāā package.json
|
|
153
|
+
āāā README.md
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Install dependencies
|
|
160
|
+
npm install
|
|
161
|
+
|
|
162
|
+
# Build TypeScript
|
|
163
|
+
npm run build
|
|
164
|
+
|
|
165
|
+
# Run in development mode
|
|
166
|
+
npm run dev
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Troubleshooting
|
|
170
|
+
|
|
171
|
+
### Server not appearing in Claude Desktop
|
|
172
|
+
|
|
173
|
+
1. Check that the config file path is correct
|
|
174
|
+
2. Verify all environment variables are set
|
|
175
|
+
3. Check Claude Desktop logs for errors
|
|
176
|
+
4. Ensure `dist/stdio-server.js` exists (run `npm run build`)
|
|
177
|
+
|
|
178
|
+
### Transcription errors
|
|
179
|
+
|
|
180
|
+
1. Verify your GCP credentials are valid
|
|
181
|
+
2. Check that Vertex AI API is enabled in your GCP project
|
|
182
|
+
3. Ensure the video URL is publicly accessible
|
|
183
|
+
4. Check that you have sufficient GCP quota
|
|
184
|
+
|
|
185
|
+
### Upload/Studio link issues
|
|
186
|
+
|
|
187
|
+
- If `UPLOAD_API_URL` is not set, the tool will return the project as a downloadable JSON file
|
|
188
|
+
- If `TWICK_STUDIO_URL` is not set, only the upload result will be returned
|
|
189
|
+
- The `$project` placeholder in `TWICK_STUDIO_URL` will be automatically replaced with the encoded project URL
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
ISC
|
|
194
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"twick-mcp-agent": {
|
|
4
|
+
"command": "node",
|
|
5
|
+
"args": ["dist/stdio-server.js"],
|
|
6
|
+
"env": {
|
|
7
|
+
"GOOGLE_CLOUD_PROJECT": "your-gcp-project-id",
|
|
8
|
+
"GOOGLE_CLOUD_LOCATION": "global",
|
|
9
|
+
"GOOGLE_APPLICATION_CREDENTIALS": "gcp-sa-key.json",
|
|
10
|
+
"GOOGLE_VERTEX_MODEL": "gemini-2.5-flash-lite",
|
|
11
|
+
"UPLOAD_API_URL": "https://your-upload-api.example.com/upload",
|
|
12
|
+
"TWICK_STUDIO_URL": "https://development.d1vtsw7m0lx01h.amplifyapp.com?project-file=$project"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-server.d.ts","sourceRoot":"","sources":["../stdio-server.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { transcribeVideoUrl } from "./transcriber.js";
|
|
5
|
+
import { processSubtitlesToProject, returnProjectAsFile, returnProjectAsTwickStudioLink, } from "./utils.js";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
import dotenv from "dotenv";
|
|
9
|
+
// Get project root (go up from dist/ to project root)
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
13
|
+
// Load .env.local from project root, not cwd
|
|
14
|
+
dotenv.config({ path: resolve(PROJECT_ROOT, ".env.local") });
|
|
15
|
+
// Initialize MCP server
|
|
16
|
+
const server = new Server({
|
|
17
|
+
name: "twick-mcp-agent",
|
|
18
|
+
version: "1.0.0",
|
|
19
|
+
}, {
|
|
20
|
+
capabilities: {
|
|
21
|
+
tools: {},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
// Advertise tools so MCP clients can discover them
|
|
25
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
26
|
+
return {
|
|
27
|
+
tools: [
|
|
28
|
+
{
|
|
29
|
+
name: "generate-subtitles",
|
|
30
|
+
description: "Generate subtitles for an video URL using an external service",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
videoUrl: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Publicly accessible media URL (video)",
|
|
37
|
+
},
|
|
38
|
+
language: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Language for transcription (default: english)",
|
|
41
|
+
},
|
|
42
|
+
language_font: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Language font (default: english)",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ["videoUrl"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
54
|
+
try {
|
|
55
|
+
if (request.params.name === "generate-subtitles") {
|
|
56
|
+
const { videoUrl, language, language_font } = request.params
|
|
57
|
+
.arguments;
|
|
58
|
+
if (!videoUrl || typeof videoUrl !== "string") {
|
|
59
|
+
throw new Error("Invalid or missing videoUrl");
|
|
60
|
+
}
|
|
61
|
+
const data = await transcribeVideoUrl({
|
|
62
|
+
videoUrl,
|
|
63
|
+
language: language ?? "english",
|
|
64
|
+
languageFont: language_font ?? "english",
|
|
65
|
+
});
|
|
66
|
+
const project = await processSubtitlesToProject({
|
|
67
|
+
subtitles: data.subtitles,
|
|
68
|
+
videoUrl: data.videoUrl,
|
|
69
|
+
duration: data.duration,
|
|
70
|
+
videoSize: { width: 720, height: 1280 },
|
|
71
|
+
});
|
|
72
|
+
if (process.env.UPLOAD_API_URL && process.env.TWICK_STUDIO_URL) {
|
|
73
|
+
return await returnProjectAsTwickStudioLink(project);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
return returnProjectAsFile(project);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// Start the server with stdio transport
|
|
89
|
+
async function main() {
|
|
90
|
+
const transport = new StdioServerTransport();
|
|
91
|
+
await server.connect(transport);
|
|
92
|
+
}
|
|
93
|
+
main().catch(console.error);
|
|
94
|
+
//# sourceMappingURL=stdio-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-server.js","sourceRoot":"","sources":["../stdio-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcribe an audio URL to JSON subtitles using Google GenAI (Vertex AI),
|
|
3
|
+
* mirroring the Python implementation in `playground/vertex/transcript.py`.
|
|
4
|
+
*
|
|
5
|
+
* @param {Object} params
|
|
6
|
+
* @param {string} params.videoUrl - Publicly reachable video URL.
|
|
7
|
+
* @param {string} [params.language="english"] - Target transcription language (human-readable).
|
|
8
|
+
* @param {string} [params.languageFont="english"] - Target font/script for subtitles.
|
|
9
|
+
* @returns {Promise<{ subtitles: Array<{t: string, s: number, e: number}> }>} Subtitles array with text, start time, and end time.
|
|
10
|
+
* @throws {Error} When audioUrl is missing or downstream calls fail.
|
|
11
|
+
*/
|
|
12
|
+
export declare const transcribeVideoUrl: (params: {
|
|
13
|
+
videoUrl: string;
|
|
14
|
+
language?: string;
|
|
15
|
+
languageFont?: string;
|
|
16
|
+
}) => Promise<{
|
|
17
|
+
subtitles: any[];
|
|
18
|
+
duration: number;
|
|
19
|
+
videoUrl: string;
|
|
20
|
+
}>;
|
|
21
|
+
//# sourceMappingURL=transcriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcriber.d.ts","sourceRoot":"","sources":["../transcriber.ts"],"names":[],"mappings":"AAsMA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAU,QAAQ;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE;;;;EAwG9G,CAAC"}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { GoogleGenAI } from "@google/genai";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path, { join } from "path";
|
|
4
|
+
import { mkdtemp, readFile, rm } from "fs/promises";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
import { execFile } from "child_process";
|
|
7
|
+
import { promisify } from "util";
|
|
8
|
+
import { Readable, pipeline } from "stream";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
// These packages provide prebuilt ffmpeg/ffprobe binaries. Types are not bundled,
|
|
12
|
+
// so we import them as `any` to keep TypeScript satisfied.
|
|
13
|
+
import ffmpeg from "@ffmpeg-installer/ffmpeg";
|
|
14
|
+
import ffprobe from "@ffprobe-installer/ffprobe";
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
// Get project root (go up from dist/ to project root)
|
|
18
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
const pipelineAsync = promisify(pipeline);
|
|
21
|
+
const ffmpegPath = ffmpeg.path;
|
|
22
|
+
const ffprobePath = ffprobe.path;
|
|
23
|
+
/**
|
|
24
|
+
* Read a required environment variable, optionally falling back to a default.
|
|
25
|
+
* Throws if neither value is available, making configuration errors obvious.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} name - Environment variable to read.
|
|
28
|
+
* @param {string | undefined} defaultValue - Optional fallback value.
|
|
29
|
+
* @returns {string} The resolved value.
|
|
30
|
+
* @throws {Error} If no value is found.
|
|
31
|
+
*/
|
|
32
|
+
const ensureEnv = (name, defaultValue) => {
|
|
33
|
+
const value = process.env[name] ?? defaultValue;
|
|
34
|
+
if (!value) {
|
|
35
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
const ensureCredentials = () => {
|
|
40
|
+
// Resolve gcp-sa-key.json relative to project root (not cwd, which may be Claude Desktop's directory)
|
|
41
|
+
// First try an explicit env var, then fall back to project root
|
|
42
|
+
const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS
|
|
43
|
+
|| process.env.GCP_SA_KEY_PATH
|
|
44
|
+
|| path.join(PROJECT_ROOT, "gcp-sa-key.json");
|
|
45
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
46
|
+
throw new Error(`Credentials file not found at ${credentialsPath}. Set GCP_SA_KEY_PATH environment variable or place gcp-sa-key.json in the project root (${PROJECT_ROOT})`);
|
|
47
|
+
}
|
|
48
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Initialize a Google GenAI client configured for Vertex AI.
|
|
52
|
+
* Ensures credentials, project, and location are available before instantiating.
|
|
53
|
+
*
|
|
54
|
+
* @returns {Promise<GoogleGenAI>} Configured GenAI client instance.
|
|
55
|
+
* @throws {Error} When required environment variables are missing.
|
|
56
|
+
*/
|
|
57
|
+
const createGenAIClient = async () => {
|
|
58
|
+
ensureCredentials();
|
|
59
|
+
const project = ensureEnv("GOOGLE_CLOUD_PROJECT");
|
|
60
|
+
const location = ensureEnv("GOOGLE_CLOUD_LOCATION", "global");
|
|
61
|
+
const client = new GoogleGenAI({
|
|
62
|
+
vertexai: true,
|
|
63
|
+
project: project,
|
|
64
|
+
location: location,
|
|
65
|
+
});
|
|
66
|
+
return client;
|
|
67
|
+
};
|
|
68
|
+
const extractAudioBufferFromVideo = async (videoUrl) => {
|
|
69
|
+
const videoResponse = await fetch(videoUrl);
|
|
70
|
+
if (!videoResponse.ok) {
|
|
71
|
+
throw new Error(`Failed to download video: ${videoResponse.status} ${videoResponse.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
const tmpBase = await mkdtemp(join(tmpdir(), 'mcp-'));
|
|
74
|
+
const inputPath = join(tmpBase, 'input_video');
|
|
75
|
+
const outputPath = join(tmpBase, 'output_audio.mp3');
|
|
76
|
+
// Stream the video response directly to disk to avoid holding the full video in memory
|
|
77
|
+
if (!videoResponse.body) {
|
|
78
|
+
await rm(tmpBase, { recursive: true, force: true });
|
|
79
|
+
throw new Error("Video response has no body");
|
|
80
|
+
}
|
|
81
|
+
const videoStream = Readable.fromWeb(videoResponse.body);
|
|
82
|
+
const fileWriteStream = fs.createWriteStream(inputPath);
|
|
83
|
+
await pipelineAsync(videoStream, fileWriteStream);
|
|
84
|
+
// Get duration using bundled ffprobe
|
|
85
|
+
let duration = 0;
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await execFileAsync(ffprobePath, [
|
|
88
|
+
'-v', 'error',
|
|
89
|
+
'-show_entries', 'format=duration',
|
|
90
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
91
|
+
inputPath
|
|
92
|
+
]);
|
|
93
|
+
duration = parseFloat(stdout.toString().trim()) || 0;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.warn('Failed to get duration using ffprobe, duration will be 0');
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await execFileAsync(ffmpegPath, [
|
|
100
|
+
'-y',
|
|
101
|
+
'-i', inputPath,
|
|
102
|
+
'-vn',
|
|
103
|
+
'-acodec', 'libmp3lame',
|
|
104
|
+
'-q:a', '2',
|
|
105
|
+
outputPath
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
await rm(tmpBase, { recursive: true, force: true });
|
|
110
|
+
const stderr = err?.stderr?.toString?.().trim?.() || "";
|
|
111
|
+
const msg = stderr || (err instanceof Error ? err.message : String(err));
|
|
112
|
+
throw new Error(`ffmpeg execution failed: ${msg}`);
|
|
113
|
+
}
|
|
114
|
+
const audioBuffer = await readFile(outputPath);
|
|
115
|
+
await rm(tmpBase, { recursive: true, force: true });
|
|
116
|
+
return { audioBuffer, duration };
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Build the captioning prompt passed to the Gemini model.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} language - Human-readable target language.
|
|
122
|
+
* @param {string} languageFont - Desired script/font name.
|
|
123
|
+
* @returns {string} Instruction prompt for the model.
|
|
124
|
+
*/
|
|
125
|
+
const buildPrompt = (duration, language, languageFont) => {
|
|
126
|
+
const durationMs = Math.round(duration * 1000);
|
|
127
|
+
return `You are a professional subtitle and transcription engine.
|
|
128
|
+
|
|
129
|
+
## INPUT
|
|
130
|
+
- Audio duration: ${durationMs} milliseconds
|
|
131
|
+
- Target language: ${language}
|
|
132
|
+
- Subtitle font script: ${languageFont}
|
|
133
|
+
|
|
134
|
+
## OBJECTIVE
|
|
135
|
+
Transcribe the audio into clear, readable subtitles.
|
|
136
|
+
|
|
137
|
+
If the spoken audio is NOT in ${language}, translate it into ${language} before generating subtitles.
|
|
138
|
+
|
|
139
|
+
## SUBTITLE SEGMENTATION RULES
|
|
140
|
+
- Split speech into short, natural phrases.
|
|
141
|
+
- Each subtitle phrase MUST contain a maximum of 4 words.
|
|
142
|
+
- Do NOT split words across phrases.
|
|
143
|
+
- Avoid breaking phrases mid-sentence unless required by timing constraints.
|
|
144
|
+
|
|
145
|
+
## TIMING RULES (STRICT ā MUST FOLLOW)
|
|
146
|
+
- All timestamps are in **milliseconds**.
|
|
147
|
+
- Each subtitle object MUST include:
|
|
148
|
+
- 's': start timestamp
|
|
149
|
+
- 'e': end timestamp
|
|
150
|
+
- Duration of each phrase = 'e - s'
|
|
151
|
+
- Minimum phrase duration: **100 ms**
|
|
152
|
+
- 'e' MUST be greater than 's'
|
|
153
|
+
- 'e' MUST be **less than or equal to ${durationMs}**
|
|
154
|
+
- Subtitles MUST be sequential:
|
|
155
|
+
- 's' of the next phrase MUST be **greater than or equal to** the previous 'e'
|
|
156
|
+
- NO overlapping timestamps
|
|
157
|
+
- Prefer aligning timestamps with natural speech pauses.
|
|
158
|
+
|
|
159
|
+
## TEXT RULES
|
|
160
|
+
- 't' MUST be written using ${languageFont} characters.
|
|
161
|
+
- No emojis.
|
|
162
|
+
- No punctuation-only subtitles.
|
|
163
|
+
- Normalize casing according to the target language's writing system.
|
|
164
|
+
- Remove filler sounds (e.g., āumā, āuhā) unless semantically important.
|
|
165
|
+
|
|
166
|
+
## OUTPUT FORMAT (CRITICAL)
|
|
167
|
+
Return ONLY a valid JSON array.
|
|
168
|
+
- No markdown
|
|
169
|
+
- No code blocks
|
|
170
|
+
- No explanations
|
|
171
|
+
- No additional text
|
|
172
|
+
- Output MUST start with '[' and end with ']'
|
|
173
|
+
|
|
174
|
+
## OUTPUT SCHEMA
|
|
175
|
+
[
|
|
176
|
+
{
|
|
177
|
+
"t": "Subtitle text",
|
|
178
|
+
"s": 0,
|
|
179
|
+
"e": 1200
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
`.trim();
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Transcribe an audio URL to JSON subtitles using Google GenAI (Vertex AI),
|
|
186
|
+
* mirroring the Python implementation in `playground/vertex/transcript.py`.
|
|
187
|
+
*
|
|
188
|
+
* @param {Object} params
|
|
189
|
+
* @param {string} params.videoUrl - Publicly reachable video URL.
|
|
190
|
+
* @param {string} [params.language="english"] - Target transcription language (human-readable).
|
|
191
|
+
* @param {string} [params.languageFont="english"] - Target font/script for subtitles.
|
|
192
|
+
* @returns {Promise<{ subtitles: Array<{t: string, s: number, e: number}> }>} Subtitles array with text, start time, and end time.
|
|
193
|
+
* @throws {Error} When audioUrl is missing or downstream calls fail.
|
|
194
|
+
*/
|
|
195
|
+
export const transcribeVideoUrl = async (params) => {
|
|
196
|
+
const { videoUrl, language = "english", languageFont = "english", } = params || {};
|
|
197
|
+
if (!videoUrl) {
|
|
198
|
+
throw new Error("Missing required parameter: videoUrl");
|
|
199
|
+
}
|
|
200
|
+
const { audioBuffer, duration } = await extractAudioBufferFromVideo(videoUrl);
|
|
201
|
+
if (!duration) {
|
|
202
|
+
throw new Error("Failed to get duration of video");
|
|
203
|
+
}
|
|
204
|
+
const prompt = buildPrompt(duration, language, languageFont);
|
|
205
|
+
const client = await createGenAIClient();
|
|
206
|
+
const modelName = process.env.GOOGLE_VERTEX_MODEL || "gemini-2.5-flash-lite";
|
|
207
|
+
const generationConfig = {
|
|
208
|
+
maxOutputTokens: 65535,
|
|
209
|
+
temperature: 1,
|
|
210
|
+
topP: 0.95,
|
|
211
|
+
thinkingConfig: {
|
|
212
|
+
thinkingBudget: 0,
|
|
213
|
+
},
|
|
214
|
+
safetySettings: [
|
|
215
|
+
{
|
|
216
|
+
category: "HARM_CATEGORY_HATE_SPEECH",
|
|
217
|
+
threshold: "OFF",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
221
|
+
threshold: "OFF",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
225
|
+
threshold: "OFF",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
category: "HARM_CATEGORY_HARASSMENT",
|
|
229
|
+
threshold: "OFF",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
const req = {
|
|
234
|
+
model: modelName,
|
|
235
|
+
contents: [
|
|
236
|
+
{
|
|
237
|
+
role: "user",
|
|
238
|
+
parts: [
|
|
239
|
+
{
|
|
240
|
+
inlineData: {
|
|
241
|
+
data: audioBuffer.toString("base64"),
|
|
242
|
+
mimeType: "audio/mpeg",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{ text: prompt },
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
config: generationConfig,
|
|
250
|
+
};
|
|
251
|
+
//@ts-ignore
|
|
252
|
+
const response = await client.models.generateContent(req);
|
|
253
|
+
let textPart = response.text || "";
|
|
254
|
+
// Strip markdown code fences if present (```json ... ``` or ``` ... ```)
|
|
255
|
+
textPart = textPart
|
|
256
|
+
.replace(/^```json\s*/i, "") // Remove opening ```json
|
|
257
|
+
.replace(/^```\s*/i, "") // Remove opening ```
|
|
258
|
+
.replace(/\s*```$/i, "") // Remove closing ```
|
|
259
|
+
.trim();
|
|
260
|
+
let subtitles = [];
|
|
261
|
+
try {
|
|
262
|
+
// Try to find JSON array in the text (in case there's extra text)
|
|
263
|
+
const jsonMatch = textPart.match(/\[[\s\S]*\]/);
|
|
264
|
+
const jsonText = jsonMatch ? jsonMatch[0] : textPart;
|
|
265
|
+
subtitles = JSON.parse(jsonText);
|
|
266
|
+
if (!Array.isArray(subtitles)) {
|
|
267
|
+
throw new Error("Parsed subtitles are not an array");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
console.warn("Failed to parse model output as JSON subtitles, returning raw text", err);
|
|
272
|
+
console.warn("Raw response text:", textPart.substring(0, 500));
|
|
273
|
+
subtitles = [];
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
subtitles,
|
|
277
|
+
duration,
|
|
278
|
+
videoUrl
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=transcriber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcriber.js","sourceRoot":"","sources":["../transcriber.ts"],"names":[],"mappings":"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"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export declare const uploadDataToFile: (data: any) => Promise<unknown>;
|
|
2
|
+
export declare const processSubtitles: (project: any) => Promise<unknown>;
|
|
3
|
+
export declare const processSubtitlesToProject: ({ subtitles, videoUrl, duration, videoSize, }: {
|
|
4
|
+
subtitles: any;
|
|
5
|
+
videoUrl: string;
|
|
6
|
+
duration: number;
|
|
7
|
+
videoSize: {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
}) => Promise<{
|
|
12
|
+
properties: {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
tracks: ({
|
|
17
|
+
id: string;
|
|
18
|
+
type: string;
|
|
19
|
+
elements: {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
s: number;
|
|
23
|
+
e: number;
|
|
24
|
+
props: {
|
|
25
|
+
src: string;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
29
|
+
}[];
|
|
30
|
+
props?: never;
|
|
31
|
+
} | {
|
|
32
|
+
id: string;
|
|
33
|
+
type: string;
|
|
34
|
+
props: {
|
|
35
|
+
capStyle: string;
|
|
36
|
+
font: {
|
|
37
|
+
size: number;
|
|
38
|
+
weight: number;
|
|
39
|
+
family: string;
|
|
40
|
+
};
|
|
41
|
+
colors: {
|
|
42
|
+
text: string;
|
|
43
|
+
highlight: string;
|
|
44
|
+
bgColor: string;
|
|
45
|
+
};
|
|
46
|
+
lineWidth: number;
|
|
47
|
+
stroke: string;
|
|
48
|
+
fontWeight: number;
|
|
49
|
+
shadowOffset: number[];
|
|
50
|
+
shadowColor: string;
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
applyToAll: boolean;
|
|
54
|
+
};
|
|
55
|
+
elements: any;
|
|
56
|
+
})[];
|
|
57
|
+
version: number;
|
|
58
|
+
}>;
|
|
59
|
+
export declare const getTwickStudioLink: (uploadResult: any) => string | undefined;
|
|
60
|
+
export declare const returnProjectAsTwickStudioLink: (project: any) => Promise<{
|
|
61
|
+
content: {
|
|
62
|
+
type: string;
|
|
63
|
+
text: string;
|
|
64
|
+
}[];
|
|
65
|
+
} | {
|
|
66
|
+
content: {
|
|
67
|
+
type: string;
|
|
68
|
+
text: string;
|
|
69
|
+
}[];
|
|
70
|
+
structuredContent: {
|
|
71
|
+
success: boolean;
|
|
72
|
+
result: string;
|
|
73
|
+
};
|
|
74
|
+
}>;
|
|
75
|
+
export declare const returnProjectAsFile: (project: any) => {
|
|
76
|
+
content: {
|
|
77
|
+
type: string;
|
|
78
|
+
text: string;
|
|
79
|
+
}[];
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAU,MAAM,GAAG,qBAoB/C,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAU,SAAS,GAAG,qBAclD,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAU,+CAK7C;IACD,SAAS,EAAE,GAAG,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DA,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,cAAc,GAAG,uBAanD,CAAC;AAEF,eAAO,MAAM,8BAA8B,GAAU,SAAS,GAAG;;;;;;;;;;;;;;EAYhE,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,SAAS,GAAG;;;;;CAuB/C,CAAC"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export const uploadDataToFile = async (data) => {
|
|
2
|
+
const formData = new FormData();
|
|
3
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
|
4
|
+
type: "application/json",
|
|
5
|
+
});
|
|
6
|
+
formData.append("file", blob, "data.json");
|
|
7
|
+
const res = await fetch(process.env.UPLOAD_API_URL ?? "", {
|
|
8
|
+
method: "POST",
|
|
9
|
+
body: formData,
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
throw new Error(`Failed to upload data to file: ${res.status} ${res.statusText}`);
|
|
13
|
+
}
|
|
14
|
+
return res.json();
|
|
15
|
+
};
|
|
16
|
+
export const processSubtitles = async (project) => {
|
|
17
|
+
try {
|
|
18
|
+
const uploadResult = await uploadDataToFile(project);
|
|
19
|
+
// Use stderr for logs so stdio JSON-RPC traffic on stdout is not corrupted.
|
|
20
|
+
console.error("Upload Result", uploadResult);
|
|
21
|
+
return uploadResult;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error("Error processing subtitles", err);
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: "Error uploading project to file",
|
|
28
|
+
project: project,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export const processSubtitlesToProject = async ({ subtitles, videoUrl, duration, videoSize, }) => {
|
|
33
|
+
return {
|
|
34
|
+
properties: {
|
|
35
|
+
width: videoSize.width || 720,
|
|
36
|
+
height: videoSize.height || 1280,
|
|
37
|
+
},
|
|
38
|
+
tracks: [
|
|
39
|
+
{
|
|
40
|
+
id: "video",
|
|
41
|
+
type: "video",
|
|
42
|
+
elements: [
|
|
43
|
+
{
|
|
44
|
+
id: "video",
|
|
45
|
+
type: "video",
|
|
46
|
+
s: 0,
|
|
47
|
+
e: duration,
|
|
48
|
+
props: {
|
|
49
|
+
src: videoUrl,
|
|
50
|
+
width: videoSize.width || 720,
|
|
51
|
+
height: videoSize.height || 1280,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "subtitle",
|
|
58
|
+
type: "caption",
|
|
59
|
+
props: {
|
|
60
|
+
capStyle: "highlight_bg",
|
|
61
|
+
font: {
|
|
62
|
+
size: 50,
|
|
63
|
+
weight: 700,
|
|
64
|
+
family: "Bangers",
|
|
65
|
+
},
|
|
66
|
+
colors: {
|
|
67
|
+
text: "#ffffff",
|
|
68
|
+
highlight: "#ff4081",
|
|
69
|
+
bgColor: "#444444",
|
|
70
|
+
},
|
|
71
|
+
lineWidth: 0.35,
|
|
72
|
+
stroke: "#000000",
|
|
73
|
+
fontWeight: 700,
|
|
74
|
+
shadowOffset: [-3, 3],
|
|
75
|
+
shadowColor: "#000000",
|
|
76
|
+
x: 0,
|
|
77
|
+
y: 200,
|
|
78
|
+
applyToAll: true,
|
|
79
|
+
},
|
|
80
|
+
elements: subtitles.map((subtitle, index) => ({
|
|
81
|
+
id: `subtitle-${index}`,
|
|
82
|
+
type: "caption",
|
|
83
|
+
s: subtitle.s / 1000,
|
|
84
|
+
e: subtitle.e / 1000,
|
|
85
|
+
t: subtitle.t,
|
|
86
|
+
})),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
version: 1,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
export const getTwickStudioLink = (uploadResult) => {
|
|
93
|
+
if (!uploadResult?.success)
|
|
94
|
+
return;
|
|
95
|
+
const fileUrl = uploadResult.result?.[0]?.url;
|
|
96
|
+
if (!fileUrl)
|
|
97
|
+
return;
|
|
98
|
+
const template = process.env.TWICK_STUDIO_URL;
|
|
99
|
+
if (!template)
|
|
100
|
+
return;
|
|
101
|
+
// If TWICK_STUDIO_URL contains a $project placeholder, substitute it
|
|
102
|
+
if (template.includes("$project")) {
|
|
103
|
+
return template.replace("$project", encodeURIComponent(fileUrl));
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
export const returnProjectAsTwickStudioLink = async (project) => {
|
|
107
|
+
const uploadResult = await uploadDataToFile(project);
|
|
108
|
+
const twickStudioLink = getTwickStudioLink(uploadResult);
|
|
109
|
+
if (twickStudioLink) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{ type: "text", text: `Open link in browser: ${twickStudioLink}` },
|
|
113
|
+
],
|
|
114
|
+
structuredContent: { success: true, result: twickStudioLink },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return returnProjectAsFile(project);
|
|
118
|
+
};
|
|
119
|
+
export const returnProjectAsFile = (project) => {
|
|
120
|
+
const data = Buffer.from(JSON.stringify(project, null, 2)).toString("base64");
|
|
121
|
+
const result = {
|
|
122
|
+
file: {
|
|
123
|
+
name: "project.json",
|
|
124
|
+
content: data,
|
|
125
|
+
encoding: "base64",
|
|
126
|
+
mimeType: "application/json",
|
|
127
|
+
},
|
|
128
|
+
metadata: {
|
|
129
|
+
size: data.length,
|
|
130
|
+
created: new Date().toISOString(),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: JSON.stringify(result, null, 2),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,IAAS,EAAE,EAAE;IAClD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;QACrD,IAAI,EAAE,kBAAkB;KACzB,CAAC,CAAC;IAEH,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IAE3C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,EAAE;QACxD,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,QAAQ;KACf,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,kCAAkC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CACjE,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EAAE,OAAY,EAAE,EAAE;IACrD,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACrD,4EAA4E;QAC5E,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;QAC7C,OAAO,YAAY,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,GAAG,CAAC,CAAC;QACjD,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,iCAAiC;YACxC,OAAO,EAAE,OAAO;SACjB,CAAC;IACJ,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,KAAK,EAAE,EAC9C,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,SAAS,GAMV,EAAE,EAAE;IACH,OAAO;QACL,UAAU,EAAE;YACV,KAAK,EAAE,SAAS,CAAC,KAAK,IAAI,GAAG;YAC7B,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,IAAI;SACjC;QACD,MAAM,EAAE;YACN;gBACE,EAAE,EAAE,OAAO;gBACX,IAAI,EAAE,OAAO;gBACb,QAAQ,EAAE;oBACR;wBACE,EAAE,EAAE,OAAO;wBACX,IAAI,EAAE,OAAO;wBACb,CAAC,EAAE,CAAC;wBACJ,CAAC,EAAE,QAAQ;wBACX,KAAK,EAAE;4BACL,GAAG,EAAE,QAAQ;4BACb,KAAK,EAAE,SAAS,CAAC,KAAK,IAAI,GAAG;4BAC7B,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,IAAI;yBACjC;qBACF;iBACF;aACF;YACD;gBACE,EAAE,EAAE,UAAU;gBACd,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE;oBACL,QAAQ,EAAE,cAAc;oBACxB,IAAI,EAAE;wBACJ,IAAI,EAAE,EAAE;wBACR,MAAM,EAAE,GAAG;wBACX,MAAM,EAAE,SAAS;qBAClB;oBACD,MAAM,EAAE;wBACN,IAAI,EAAE,SAAS;wBACf,SAAS,EAAE,SAAS;wBACpB,OAAO,EAAE,SAAS;qBACnB;oBACD,SAAS,EAAE,IAAI;oBACf,MAAM,EAAE,SAAS;oBACjB,UAAU,EAAE,GAAG;oBACf,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;oBACrB,WAAW,EAAE,SAAS;oBACtB,CAAC,EAAE,CAAC;oBACJ,CAAC,EAAE,GAAG;oBACN,UAAU,EAAE,IAAI;iBACjB;gBACD,QAAQ,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,QAAa,EAAE,KAAa,EAAE,EAAE,CAAC,CAAC;oBACzD,EAAE,EAAE,YAAY,KAAK,EAAE;oBACvB,IAAI,EAAE,SAAS;oBACf,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI;oBACpB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI;oBACpB,CAAC,EAAE,QAAQ,CAAC,CAAC;iBACd,CAAC,CAAC;aACJ;SACF;QACD,OAAO,EAAE,CAAC;KACX,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,YAAiB,EAAE,EAAE;IACtD,IAAI,CAAC,YAAY,EAAE,OAAO;QAAE,OAAO;IAEnC,MAAM,OAAO,GAAuB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;IAClE,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC9C,IAAI,CAAC,QAAQ;QAAE,OAAO;IAEtB,qEAAqE;IACrE,IAAI,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,OAAO,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;IACnE,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,8BAA8B,GAAG,KAAK,EAAE,OAAY,EAAE,EAAE;IACnE,MAAM,YAAY,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACrD,MAAM,eAAe,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC;IACzD,IAAI,eAAe,EAAE,CAAC;QACpB,OAAO;YACL,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,eAAe,EAAE,EAAE;aACnE;YACD,iBAAiB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE;SAC9D,CAAC;IACJ,CAAC;IACD,OAAO,mBAAmB,CAAC,OAAO,CAAC,CAAC;AACtC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAY,EAAE,EAAE;IAClD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC9E,MAAM,MAAM,GAAG;QACb,IAAI,EAAE;YACJ,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,kBAAkB;SAC7B;QACD,QAAQ,EAAE;YACR,IAAI,EAAE,IAAI,CAAC,MAAM;YACjB,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SAClC;KACF,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC,CAAC"}
|
package/install.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Installation script for twick-mcp-agent Claude Desktop extension
|
|
5
|
+
* This script:
|
|
6
|
+
* 1. Installs npm dependencies
|
|
7
|
+
* 2. Builds the TypeScript project
|
|
8
|
+
* 3. Merges the MCP server config into Claude Desktop's config file
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
13
|
+
import { join, dirname, resolve } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
const PROJECT_ROOT = resolve(__dirname);
|
|
20
|
+
|
|
21
|
+
// Platform-specific Claude Desktop config paths
|
|
22
|
+
function getClaudeConfigPath() {
|
|
23
|
+
const platform = process.platform;
|
|
24
|
+
if (platform === "win32") {
|
|
25
|
+
// Windows: %APPDATA%\Claude\claude_desktop_config.json
|
|
26
|
+
return join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
|
|
27
|
+
} else if (platform === "darwin") {
|
|
28
|
+
// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
29
|
+
return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
30
|
+
} else {
|
|
31
|
+
// Linux: ~/.config/Claude/claude_desktop_config.json
|
|
32
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mergeConfig(existingConfig, newServerConfig) {
|
|
37
|
+
const config = existingConfig || { mcpServers: {} };
|
|
38
|
+
|
|
39
|
+
if (!config.mcpServers) {
|
|
40
|
+
config.mcpServers = {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Merge the new server config
|
|
44
|
+
Object.assign(config.mcpServers, newServerConfig.mcpServers);
|
|
45
|
+
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main() {
|
|
50
|
+
console.log("š Installing twick-mcp-agent for Claude Desktop...\n");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Step 1: Install dependencies
|
|
54
|
+
console.log("š¦ Installing npm dependencies...");
|
|
55
|
+
execSync("npm install", { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
56
|
+
console.log("ā
Dependencies installed\n");
|
|
57
|
+
|
|
58
|
+
// Step 2: Build the project
|
|
59
|
+
console.log("šØ Building TypeScript project...");
|
|
60
|
+
execSync("npm run build", { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
61
|
+
console.log("ā
Build completed\n");
|
|
62
|
+
|
|
63
|
+
// Step 3: Read the MCP server config template
|
|
64
|
+
const configTemplatePath = join(PROJECT_ROOT, "claude_desktop_config.json");
|
|
65
|
+
if (!existsSync(configTemplatePath)) {
|
|
66
|
+
throw new Error(`Config template not found: ${configTemplatePath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const configTemplate = JSON.parse(
|
|
70
|
+
readFileSync(configTemplatePath, "utf-8")
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Step 4: Update paths in config to be absolute
|
|
74
|
+
const serverConfig = { ...configTemplate };
|
|
75
|
+
const serverName = Object.keys(serverConfig.mcpServers)[0];
|
|
76
|
+
const serverEntry = serverConfig.mcpServers[serverName];
|
|
77
|
+
|
|
78
|
+
// Make the args path absolute
|
|
79
|
+
if (serverEntry.args && serverEntry.args[0]) {
|
|
80
|
+
serverEntry.args[0] = join(PROJECT_ROOT, "dist", "stdio-server.js");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Make GOOGLE_APPLICATION_CREDENTIALS path absolute if it's a relative path
|
|
84
|
+
if (serverEntry.env?.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
85
|
+
const credsPath = serverEntry.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
86
|
+
if (!credsPath.startsWith("/") && !credsPath.match(/^[A-Z]:/)) {
|
|
87
|
+
// Relative path, make it absolute
|
|
88
|
+
serverEntry.env.GOOGLE_APPLICATION_CREDENTIALS = join(
|
|
89
|
+
PROJECT_ROOT,
|
|
90
|
+
credsPath
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 5: Merge into Claude Desktop config
|
|
96
|
+
const claudeConfigPath = getClaudeConfigPath();
|
|
97
|
+
console.log(`š Updating Claude Desktop config: ${claudeConfigPath}`);
|
|
98
|
+
|
|
99
|
+
let existingConfig = null;
|
|
100
|
+
if (existsSync(claudeConfigPath)) {
|
|
101
|
+
try {
|
|
102
|
+
existingConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8"));
|
|
103
|
+
console.log("ā
Found existing Claude Desktop config");
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.warn(`ā ļø Could not parse existing config: ${err.message}`);
|
|
106
|
+
console.log("š Creating new config file");
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
console.log("š Creating new Claude Desktop config file");
|
|
110
|
+
// Ensure directory exists
|
|
111
|
+
mkdirSync(dirname(claudeConfigPath), { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const mergedConfig = mergeConfig(existingConfig, serverConfig);
|
|
115
|
+
writeFileSync(
|
|
116
|
+
claudeConfigPath,
|
|
117
|
+
JSON.stringify(mergedConfig, null, 2) + "\n",
|
|
118
|
+
"utf-8"
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
console.log("ā
Configuration merged successfully\n");
|
|
122
|
+
|
|
123
|
+
// Step 6: Summary
|
|
124
|
+
console.log("š Installation complete!\n");
|
|
125
|
+
console.log("š Next steps:");
|
|
126
|
+
console.log(" 1. Edit your Claude Desktop config to set your environment variables:");
|
|
127
|
+
console.log(` ${claudeConfigPath}`);
|
|
128
|
+
console.log(" 2. Update the following in the 'twick-mcp-agent' section:");
|
|
129
|
+
console.log(" - GOOGLE_CLOUD_PROJECT: Your GCP project ID");
|
|
130
|
+
console.log(" - GOOGLE_APPLICATION_CREDENTIALS: Path to your GCP service account key");
|
|
131
|
+
console.log(" - UPLOAD_API_URL: Your upload API endpoint (optional)");
|
|
132
|
+
console.log(" - TWICK_STUDIO_URL: Your Twick Studio URL (optional)");
|
|
133
|
+
console.log(" 3. Restart Claude Desktop to load the new MCP server");
|
|
134
|
+
console.log("\n⨠The 'generate-subtitles' tool will be available in Claude Desktop!\n");
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error("\nā Installation failed:");
|
|
137
|
+
console.error(error.message);
|
|
138
|
+
if (error.stdout) console.error(error.stdout.toString());
|
|
139
|
+
if (error.stderr) console.error(error.stderr.toString());
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main();
|
|
145
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twick/mcp-agent",
|
|
3
|
+
"version": "0.14.15",
|
|
4
|
+
"description": "MCP server for Claude Desktop that generates video captions using Google Vertex AI and integrates with Twick Studio",
|
|
5
|
+
"main": "dist/stdio-server.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"twick-mcp-agent": "./dist/stdio-server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*",
|
|
12
|
+
"claude_desktop_config.json",
|
|
13
|
+
"README.md",
|
|
14
|
+
"install.js"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"start": "node dist/stdio-server.js",
|
|
22
|
+
"dev": "tsc && node dist/stdio-server.js",
|
|
23
|
+
"install-claude": "node install.js",
|
|
24
|
+
"prepublishOnly": "pnpm run build",
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"claude-desktop",
|
|
31
|
+
"video-transcription",
|
|
32
|
+
"captions",
|
|
33
|
+
"subtitle",
|
|
34
|
+
"google-vertex-ai",
|
|
35
|
+
"gemini",
|
|
36
|
+
"twick-studio"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": ""
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.20.0",
|
|
49
|
+
"@google/genai": "^1.0.0",
|
|
50
|
+
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
51
|
+
"@ffprobe-installer/ffprobe": "^2.0.0",
|
|
52
|
+
"dotenv": "^16.4.5",
|
|
53
|
+
"express": "^5.1.0",
|
|
54
|
+
"zod": "^3.25.76"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/express": "^5.0.0",
|
|
58
|
+
"@types/node": "^24.7.2",
|
|
59
|
+
"typescript": "^5.9.3"
|
|
60
|
+
}
|
|
61
|
+
}
|