workspaces to dashboards, imap email sync, ffmpeg tool, tts fix, file browser refresh, automation sidebar reorder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,5 +175,6 @@ The `tool-loader` extension discovers tools from directories listed in the `PI_T
|
||||
| `browser` | Control a Chrome browser via Browser Relay |
|
||||
| `apify` | Run any Apify actor (web scraping, social media data) |
|
||||
| `convert_audio_to_mp3` | Convert audio files to MP3 via ffmpeg |
|
||||
| `ffmpeg` | Run ffmpeg/ffprobe commands for any audio/video processing |
|
||||
| `ocr` | Optical character recognition on images |
|
||||
| `email_db` | Query the synced email database |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: convert_audio_to_mp3
|
||||
label: Convert Audio to MP3
|
||||
description: Convert audio files to MP3 320kbps using ffmpeg, preserving metadata. Supports single file conversion with real-time percentage progress, and batch conversion of an entire artist directory with per-file progress. Use when the user wants to convert FLAC, WAV, OGG, or other audio formats to MP3.
|
||||
version: 2
|
||||
version: 3
|
||||
language: typescript
|
||||
inputs:
|
||||
mode:
|
||||
@@ -18,6 +18,16 @@ inputs:
|
||||
|
||||
Converts audio to MP3 320kbps CBR via libmp3lame, preserving all metadata tags.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This tool requires `ffmpeg` and `ffprobe`. If they are not installed, install them before running:
|
||||
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get install -y ffmpeg
|
||||
```
|
||||
|
||||
All sudo commands run without a password prompt in this environment.
|
||||
|
||||
## Modes
|
||||
|
||||
### single
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: ffmpeg
|
||||
label: FFmpeg
|
||||
description: Run ffmpeg and ffprobe commands for audio/video processing. Converts formats, extracts audio/video streams, trims, merges, adjusts volume, changes resolution, extracts frames, and more. Auto-installs ffmpeg if not available. Use when the user needs any audio or video manipulation.
|
||||
version: 1
|
||||
language: typescript
|
||||
inputs:
|
||||
command:
|
||||
type: enum
|
||||
values: ffmpeg,ffprobe
|
||||
description: "ffmpeg: process audio/video. ffprobe: inspect file metadata (duration, codecs, streams, etc.)"
|
||||
args:
|
||||
type: string
|
||||
description: "Command-line arguments as a single string. Do NOT include the ffmpeg/ffprobe binary name — only the arguments. Example: '-i input.mp4 -vn -codec:a libmp3lame -b:a 320k output.mp3'"
|
||||
---
|
||||
|
||||
# FFmpeg
|
||||
|
||||
General-purpose audio/video processing via ffmpeg and ffprobe.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This tool requires `ffmpeg` and `ffprobe`. It will automatically install them if not available:
|
||||
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get install -y ffmpeg
|
||||
```
|
||||
|
||||
All sudo commands run without a password prompt in this environment.
|
||||
|
||||
## Usage
|
||||
|
||||
### ffprobe — Inspect files
|
||||
|
||||
Get duration, codecs, streams, bitrate, and other metadata:
|
||||
|
||||
```
|
||||
command: ffprobe
|
||||
args: -v quiet -print_format json -show_format -show_streams input.mp4
|
||||
```
|
||||
|
||||
### ffmpeg — Process files
|
||||
|
||||
Always include `-y` to overwrite output files without prompting.
|
||||
|
||||
**Convert video to MP4:**
|
||||
```
|
||||
command: ffmpeg
|
||||
args: -i input.avi -codec:v libx264 -codec:a aac output.mp4 -y
|
||||
```
|
||||
|
||||
**Extract audio from video:**
|
||||
```
|
||||
command: ffmpeg
|
||||
args: -i video.mp4 -vn -codec:a libmp3lame -b:a 320k audio.mp3 -y
|
||||
```
|
||||
|
||||
**Trim a clip:**
|
||||
```
|
||||
command: ffmpeg
|
||||
args: -i input.mp4 -ss 00:01:30 -to 00:03:00 -codec copy clip.mp4 -y
|
||||
```
|
||||
|
||||
**Change resolution:**
|
||||
```
|
||||
command: ffmpeg
|
||||
args: -i input.mp4 -vf scale=1280:720 -codec:a copy output.mp4 -y
|
||||
```
|
||||
|
||||
**Extract a frame as image:**
|
||||
```
|
||||
command: ffmpeg
|
||||
args: -i video.mp4 -ss 00:00:10 -frames:v 1 frame.png -y
|
||||
```
|
||||
|
||||
**Merge audio and video:**
|
||||
```
|
||||
command: ffmpeg
|
||||
args: -i video.mp4 -i audio.mp3 -codec:v copy -codec:a aac -shortest merged.mp4 -y
|
||||
```
|
||||
|
||||
**Convert audio format:**
|
||||
```
|
||||
command: ffmpeg
|
||||
args: -i input.flac -codec:a libmp3lame -b:a 320k output.mp3 -y
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Use absolute paths for input and output files.
|
||||
- Always add `-y` to ffmpeg args to avoid interactive prompts.
|
||||
- For long operations, progress is streamed in real time.
|
||||
- stderr output from ffmpeg is captured and returned on failure.
|
||||
- The tool has a 10-minute timeout.
|
||||
@@ -0,0 +1,173 @@
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
|
||||
type ToolResult = {
|
||||
content: Array<{ type: string; text: string }>;
|
||||
isError?: boolean;
|
||||
};
|
||||
|
||||
type OnUpdate = (partial: { content: Array<{ type: string; text: string }> }) => void;
|
||||
|
||||
function update(onUpdate: OnUpdate | undefined, text: string): void {
|
||||
onUpdate?.({ content: [{ type: 'text', text }] });
|
||||
}
|
||||
|
||||
function ensureFfmpeg(): boolean {
|
||||
try {
|
||||
execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
execFileSync('sudo', ['apt-get', 'update'], { stdio: 'ignore', timeout: 60_000 });
|
||||
execFileSync('sudo', ['apt-get', 'install', '-y', 'ffmpeg'], { stdio: 'ignore', timeout: 120_000 });
|
||||
execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argsString: string): string[] {
|
||||
const args: string[] = [];
|
||||
let current = '';
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
|
||||
for (let i = 0; i < argsString.length; i++) {
|
||||
const ch = argsString[i]!;
|
||||
|
||||
if (ch === "'" && !inDouble) {
|
||||
inSingle = !inSingle;
|
||||
} else if (ch === '"' && !inSingle) {
|
||||
inDouble = !inDouble;
|
||||
} else if (ch === ' ' && !inSingle && !inDouble) {
|
||||
if (current.length > 0) {
|
||||
args.push(current);
|
||||
current = '';
|
||||
}
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
|
||||
if (current.length > 0) args.push(current);
|
||||
return args;
|
||||
}
|
||||
|
||||
export async function execute(
|
||||
_toolCallId: string,
|
||||
params: { command: 'ffmpeg' | 'ffprobe'; args: string },
|
||||
_signal: AbortSignal | undefined,
|
||||
onUpdate?: OnUpdate,
|
||||
): Promise<ToolResult> {
|
||||
const { command, args: argsString } = params;
|
||||
|
||||
if (!argsString || argsString.trim().length === 0) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'No arguments provided. See tool documentation for usage examples.' }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
update(onUpdate, 'Checking ffmpeg installation...');
|
||||
|
||||
if (!ensureFfmpeg()) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Failed to install ffmpeg. Try manually: sudo apt-get update && sudo apt-get install -y ffmpeg' }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const args = parseArgs(argsString);
|
||||
|
||||
// For ffprobe, run synchronously and return output
|
||||
if (command === 'ffprobe') {
|
||||
update(onUpdate, `Running ffprobe...`);
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('ffprobe', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
proc.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
|
||||
proc.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
resolve({
|
||||
content: [{ type: 'text', text: `ffprobe failed (exit ${code}):\n${stderr.trim()}` }],
|
||||
isError: true,
|
||||
});
|
||||
} else {
|
||||
// ffprobe prints info to stderr by default, stdout for -print_format
|
||||
const output = stdout.trim() || stderr.trim();
|
||||
resolve({
|
||||
content: [{ type: 'text', text: output || 'No output' }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
resolve({
|
||||
content: [{ type: 'text', text: `ffprobe error: ${err.message}` }],
|
||||
isError: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// For ffmpeg, stream progress
|
||||
update(onUpdate, `Running ffmpeg...`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let progressBuf = '';
|
||||
|
||||
proc.stdout.on('data', (chunk: Buffer) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (chunk: Buffer) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
|
||||
// Parse ffmpeg progress from stderr (time= field)
|
||||
progressBuf += text;
|
||||
const lines = progressBuf.split('\r');
|
||||
progressBuf = lines.pop() ?? '';
|
||||
for (const line of lines) {
|
||||
const timeMatch = line.match(/time=(\d{2}:\d{2}:\d{2}\.\d{2})/);
|
||||
const speedMatch = line.match(/speed=\s*([\d.]+x)/);
|
||||
if (timeMatch) {
|
||||
const progress = `Time: ${timeMatch[1]}${speedMatch ? ` | Speed: ${speedMatch[1]}` : ''}`;
|
||||
update(onUpdate, progress);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
// Extract the last meaningful error line from stderr
|
||||
const errLines = stderr.trim().split('\n');
|
||||
const lastLines = errLines.slice(-10).join('\n');
|
||||
resolve({
|
||||
content: [{ type: 'text', text: `ffmpeg failed (exit ${code}):\n${lastLines}` }],
|
||||
isError: true,
|
||||
});
|
||||
} else {
|
||||
const output = stdout.trim();
|
||||
resolve({
|
||||
content: [{ type: 'text', text: output ? `Done.\n\n${output}` : 'Done.' }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
resolve({
|
||||
content: [{ type: 'text', text: `ffmpeg error: ${err.message}` }],
|
||||
isError: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user