
Felo Youtube Subtitling
- 67 installs
- 241 repo stars
- Updated July 22, 2026
- felo-inc/felo-skills
Fetch YouTube video subtitles or captions by video ID or URL via the Felo YouTube Subtitling API, optionally with timestamps and a language code.
About
Retrieves YouTube video subtitles or transcripts by video ID or URL using the Felo YouTube Subtitling API, with optional language selection and per-segment timestamps. A developer uses it to pull a video transcript for analysis or translation.
- Accepts full YouTube links or a plain 11-character video ID
- Optional --with-time flag adds start/duration timestamps per segment; requires a FELO_API_KEY
Felo Youtube Subtitling by the numbers
- 67 all-time installs (skills.sh)
- Ranked #965 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/felo-inc/felo-skills --skill felo-youtube-subtitlingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 67 |
|---|---|
| repo stars | ★ 241 |
| Last updated | July 22, 2026 |
| Repository | felo-inc/felo-skills ↗ |
What it does
Fetch YouTube video subtitles or captions by video ID or URL via the Felo YouTube Subtitling API, optionally with timestamps and a language code.
Files
Felo YouTube Subtitling Skill
When to Use
Trigger this skill when the user wants to:
- Get subtitles or captions from a YouTube video
- Extract transcript by video ID or video URL
- Fetch subtitles in a specific language (e.g. en, zh-CN)
- Get subtitles with timestamps for analysis or translation
Trigger keywords (examples):
- YouTube subtitles, get captions, video transcript, extract subtitles, YouTube 字幕
- Explicit:
/felo-youtube-subtitling, "use felo youtube subtitling"
Do NOT use for:
- Real-time search (use
felo-search) - Web page content (use
felo-web-fetch) - Generating slides (use
felo-slides)
Setup
1. Get API key
1. Visit felo.ai 2. Open Settings -> API Keys 3. Create and copy your API key
2. Configure environment variable
Linux/macOS:
export FELO_API_KEY="your-api-key-here"Windows PowerShell:
$env:FELO_API_KEY="your-api-key-here"How to Execute
Option A: Use the bundled script or packaged CLI
Script (from repo):
node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs --video-code "dQw4w9WgXcQ" [options]Packaged CLI (after npm install -g felo-ai):
felo youtube-subtitling -v "dQw4w9WgXcQ" [options]
# Short forms: -v (video-code), -l (language), -j (json)Options:
| Option | Default | Description |
|---|---|---|
--video-code / -v | (required) | YouTube video URL or video ID (e.g. https://youtube.com/watch?v=ID or dQw4w9WgXcQ) |
--language / -l | - | Subtitle language code (e.g. en, zh-CN) |
--with-time | false | Include start/duration timestamps in each segment |
--json / -j | false | Print full API response as JSON |
You can pass either a full YouTube link or the 11-character video ID:
- Supported URLs:
https://www.youtube.com/watch?v=ID,https://youtu.be/ID,https://youtube.com/embed/ID - Or plain ID:
dQw4w9WgXcQ
Examples:
# With video URL
node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs --video-code "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
felo youtube-subtitling -v "https://youtu.be/dQw4w9WgXcQ"
# With video ID
node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs -v "dQw4w9WgXcQ" --language zh-CN
# With timestamps
node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs -v "dQw4w9WgXcQ" --with-time --jsonOption B: Call API with curl
curl -X GET "https://openapi.felo.ai/v2/youtube/subtitling?video_code=dQw4w9WgXcQ" \
-H "Authorization: Bearer $FELO_API_KEY"API Reference (summary)
- Endpoint:
GET /v2/youtube/subtitling - Base URL:
https://openapi.felo.ai. Override withFELO_API_BASEenv if needed. - Auth:
Authorization: Bearer YOUR_API_KEY
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| video_code | string | Yes | - | YouTube video ID (e.g. dQw4w9WgXcQ) |
| language | string | No | - | Language code (e.g. en, zh-CN) |
| with_time | boolean | No | false | Include start/duration per segment |
Response (200)
{
"code": 0,
"message": "success",
"data": {
"title": "Video title",
"contents": [
{ "start": 0.32, "duration": 14.26, "text": "Subtitle text" }
]
}
}With with_time=false, start/duration may be absent or zero. contents[].text is always present.
Error codes
| HTTP | Code | Description |
|---|---|---|
| 400 | - | Parameter validation failed (e.g. missing video_code) |
| 401 | INVALID_API_KEY | API key invalid or revoked |
| 500/502 | YOUTUBE_SUBTITLING_FAILED | Service error or subtitles unavailable for video |
Output Format
- Without
--json: print title and then each segment's text (one per line or concatenated). If--with-time, output includes timestamps. - With
--json: print full API response.
On failure (no subtitles, API error): stderr message and exit 1. Example:
YouTube subtitling failed for video dQw4w9WgXcQ: YOUTUBE_SUBTITLING_FAILEDImportant Notes
- Not all videos have subtitles; the API may return an error for some videos.
- Language code must match a subtitle track available for the video.
- Same
FELO_API_KEYas other Felo skills.
References
Felo YouTube Subtitling Skill
Fetch YouTube video subtitles/captions using the Felo YouTube Subtitling API.
Features
- Get subtitles by YouTube video URL or video ID (e.g.
https://youtube.com/watch?v=IDordQw4w9WgXcQ) - Optional language (e.g.
en,zh-CN) - Optional timestamps (
--with-time) for each segment - Same
FELO_API_KEYas other Felo skills
Quick Start
1) Configure API key
At felo.ai -> Settings -> API Keys, create a key, then:
# Linux/macOS
export FELO_API_KEY="your-api-key-here"# Windows PowerShell
$env:FELO_API_KEY="your-api-key-here"2) Run
# From repo: script (URL or video ID)
node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs --video-code "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
# After npm install -g felo-ai: CLI
felo youtube-subtitling -v "https://youtu.be/dQw4w9WgXcQ"
felo youtube-subtitling -v "dQw4w9WgXcQ"All parameters
| Parameter | Option | Example |
|---|---|---|
| Video URL or ID (required) | -v, --video-code | --video-code "https://youtube.com/watch?v=ID" or "dQw4w9WgXcQ" |
| Language | -l, --language | --language zh-CN |
| Include timestamps | --with-time | --with-time |
| Full JSON response | -j, --json | -j |
Examples
felo youtube-subtitling -v "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
felo youtube-subtitling -v "dQw4w9WgXcQ" --language en --with-time -jWhen to use (Agent)
Trigger keywords: YouTube subtitles, get captions, video transcript, extract subtitles, /felo-youtube-subtitling.
See SKILL.md for full agent instructions and API parameters.
#!/usr/bin/env node
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
const DEFAULT_TIMEOUT_MS = 30_000;
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const SPINNER_INTERVAL_MS = 80;
const STATUS_PAD = 52;
function startSpinner(message) {
if (!process.stderr.isTTY) return null;
const start = Date.now();
let i = 0;
const id = setInterval(() => {
const elapsed = Math.floor((Date.now() - start) / 1000);
const line = `${message} ${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${elapsed}s`;
process.stderr.write(`\r${line.padEnd(STATUS_PAD, ' ')}`);
i += 1;
}, SPINNER_INTERVAL_MS);
return id;
}
function stopSpinner(id) {
if (id != null) clearInterval(id);
if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
}
/** Extract video ID from YouTube URL or return string if plain ID. Returns null if invalid. */
function extractVideoId(urlOrId) {
const s = typeof urlOrId === 'string' ? urlOrId.trim() : '';
if (!s) return null;
try {
if (s.startsWith('http://') || s.startsWith('https://')) {
const u = new URL(s);
if (u.hostname === 'youtu.be') return u.pathname.slice(1).split('?')[0] || null;
if (u.hostname === 'www.youtube.com' || u.hostname === 'youtube.com') {
if (u.pathname === '/watch') return u.searchParams.get('v');
const m = u.pathname.match(/^\/(?:embed|v)\/([a-zA-Z0-9_-]{10,12})/);
if (m) return m[1];
return u.searchParams.get('v');
}
}
if (/^[a-zA-Z0-9_-]{10,12}$/.test(s)) return s;
return null;
} catch {
return null;
}
}
function usage() {
console.error(
[
'Usage:',
' node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs --video-code <url-or-id> [options]',
'',
'Options:',
' --video-code, -v <url-or-id> YouTube video URL or 11-char video ID (required)',
' --language, -l <code> Subtitle language (e.g. en, zh-CN)',
' --with-time Include start/duration per segment',
' --json, -j Print full API response as JSON',
' --help Show this help',
].join('\n')
);
}
function parseArgs(argv) {
const out = {
videoCode: '',
language: '',
withTime: false,
json: false,
};
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i];
if (a === '--help' || a === '-h') {
out.help = true;
} else if (a === '--json' || a === '-j') {
out.json = true;
} else if (a === '--with-time') {
out.withTime = true;
} else if (a === '--video-code' || a === '-v') {
const next = argv[i + 1];
if (next === undefined || next === null || String(next).trim() === '' || String(next).startsWith('-')) {
out.videoCode = '';
} else {
out.videoCode = String(next).trim();
}
i += 1;
} else if (a === '--language' || a === '-l') {
out.language = (argv[i + 1] ?? '').trim();
i += 1;
}
}
return out;
}
function getMessage(payload) {
return (
payload?.message ||
payload?.error ||
payload?.msg ||
payload?.code ||
'Unknown error'
);
}
async function fetchJson(url, init, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { ...init, signal: controller.signal });
let body = {};
try {
body = await res.json();
} catch {
body = {};
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
}
const code = body.code;
const hasData = body?.data != null;
const successCodes = [0, 200];
const hasSuccessCode =
successCodes.includes(Number(code)) ||
code === undefined ||
code === null ||
(hasData && res.ok);
if (!hasSuccessCode) {
throw new Error(getMessage(body));
}
return body;
} finally {
clearTimeout(timer);
}
}
function formatContents(contents, withTime) {
if (!Array.isArray(contents) || contents.length === 0) return '';
return contents
.map((c) => {
if (withTime && (c.start != null || c.duration != null)) {
const start = Number(c.start);
const dur = Number(c.duration);
const startSec = Number.isFinite(start) ? start : 0;
const durSec = Number.isFinite(dur) ? dur : 0;
return `[${startSec.toFixed(2)}s, +${durSec.toFixed(2)}s] ${c.text ?? ''}`;
}
return c.text ?? '';
})
.filter(Boolean)
.join('\n');
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
process.exit(0);
}
const videoCode = extractVideoId(args.videoCode);
if (!videoCode) {
if (!args.videoCode || String(args.videoCode).trim() === '') {
usage();
} else {
console.error('ERROR: Invalid YouTube URL or video ID. Use a link (e.g. https://youtube.com/watch?v=ID) or an 11-character video ID.');
}
process.exit(1);
}
const apiKey = process.env.FELO_API_KEY?.trim();
if (!apiKey) {
console.error('ERROR: FELO_API_KEY not set');
process.exit(1);
}
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
const spinnerId = startSpinner(`Fetching subtitles ${videoCode}`);
try {
const params = new URLSearchParams({ video_code: videoCode });
if (args.language) params.set('language', args.language);
if (args.withTime) params.set('with_time', 'true');
const url = `${apiBase}/v2/youtube/subtitling?${params.toString()}`;
const payload = await fetchJson(
url,
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
},
},
DEFAULT_TIMEOUT_MS
);
const data = payload?.data ?? {};
const title = data?.title ?? '';
const contents = data?.contents ?? [];
if (args.json) {
console.log(JSON.stringify(payload, null, 2));
return;
}
const text = formatContents(contents, args.withTime);
const isEmpty = !text || text.trim() === '';
if (isEmpty) {
stopSpinner(spinnerId);
process.stderr.write(
`No subtitles found for video ${videoCode}. The video may have no captions or the language is not available.\n`
);
process.exit(1);
}
if (title) {
console.log(`# ${title}\n`);
}
console.log(text);
} finally {
stopSpinner(spinnerId);
}
}
main().catch((err) => {
let videoCode = '';
const argv = process.argv.slice(2);
const i = argv.findIndex((a) => a === '--video-code' || a === '-v');
if (i >= 0 && argv[i + 1]) videoCode = argv[i + 1];
process.stderr.write(
`YouTube subtitling failed${videoCode ? ` for video ${videoCode}` : ''}: ${err?.message || err}\n`
);
process.exit(1);
});