
Transcribe
- 3 installs
- 4 repo stars
- Updated March 11, 2026
- aviz85/ai-music-video-maker
transcribe is a Claude Code skill that transcribes audio or video to SRT subtitles and readable text using ElevenLabs Scribe v2.
About
A Claude Code skill that transcribes audio or video to SRT subtitles and readable text using ElevenLabs Scribe v2. Its script produces an SRT file, a Markdown transcript with speaker labels and timestamps, and optional word-level JSON. A developer uses it to caption media or get word-level timing for downstream lyric and caption overlays.
- Transcribes audio/video to SRT subtitles using ElevenLabs Scribe v2
- Outputs SRT plus readable Markdown with speaker diarization and word-level JSON
- Supports many language codes and semantic subtitle refinement rules
Transcribe by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,155 of 1,337 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
transcribe capabilities & compatibility
Requires ELEVENLABS_API_KEY in scripts/.env.
- Capabilities
- transcription · translation
- Use cases
- transcription · translation
- Pricing
- Bring your own API key
What transcribe says it does
Generate SRT subtitle files + readable text from audio/video using ElevenLabs Scribe v2.
The script **always** generates: 1. `.srt` - Standard subtitle file (for embedding)
npx skills add https://github.com/aviz85/ai-music-video-maker --skill transcribeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 4 |
| Last updated | March 11, 2026 |
| Repository | aviz85/ai-music-video-maker ↗ |
What it does
Transcribe audio or video into SRT subtitles, readable text, and word-level JSON with ElevenLabs Scribe v2.
Who is it for?
Producing SRT captions, readable transcripts, and word-level timing from audio or video.
Skip if: Offline use without an ElevenLabs API key.
When should I use this skill?
You need transcription, subtitles, captions, or SRT generation from media.
What you get
An SRT subtitle file plus readable Markdown transcript and optional word-level JSON.
- SRT subtitle file
- Readable Markdown transcript
- Optional word-level JSON
By the numbers
- default max 5 words per subtitle entry
- default max 70 chars per entry
- timestamps every ~5 minutes
Files
Transcribe
Generate SRT subtitle files + readable text from audio/video using ElevenLabs Scribe v2.
Quick Start
cd .claude/skills/transcribe/scripts
# Basic transcription (generates both SRT + readable .md)
npx ts-node transcribe.ts -i /path/to/video.mp4 -o /path/to/output.srt
# Specify language
npx ts-node transcribe.ts -i /path/to/video.mp4 -o /path/to/output.srt -l he
# Only readable text (skip SRT)
npx ts-node transcribe.ts -i /path/to/meeting.m4a -o /path/to/output.srt --no-srt
# Shorter timestamp intervals (every 2 minutes)
npx ts-node transcribe.ts -i /path/to/video.mp4 -o /path/to/output.srt --timestamp-interval 120
# Disable speaker diarization
npx ts-node transcribe.ts -i /path/to/video.mp4 -o /path/to/output.srt --no-speakersOutput Files
The script always generates: 1. .srt - Standard subtitle file (for embedding) 2. .md - Readable text with speakers + timestamps every ~5 min
Optional: 3. _transcript.json - Raw word-level data (with --json)
Options
| Option | Short | Default | Description |
|---|---|---|---|
--input | -i | (required) | Input audio/video file |
--output | -o | (required) | Output SRT file path |
--language | -l | auto | Language code (en, he, ar, etc.) |
--max-words | 5 | Max words per subtitle entry | |
--max-duration | 3.0 | Max seconds per subtitle entry | |
--max-chars | 70 | Max characters per subtitle entry | |
--timing-offset | 0.25 | Timing offset in seconds | |
--json | false | Also output raw transcript JSON | |
--no-srt | false | Skip SRT, only generate text | |
--no-speakers | false | Disable speaker diarization | |
--timestamp-interval | 300 | Seconds between timestamps in text (5 min) |
Language Codes
en- Englishhe- Hebrewar- Arabices- Spanishfr- Frenchde- Germanru- Russianzh- Chineseja- Japanese- (or omit for auto-detection)
Readable Text Format
The .md file includes:
- Header with filename, date, duration
- Speaker labels (דובר 1, דובר 2...) when diarization detects speaker changes
- Timestamps every ~5 minutes (configurable)
- Paragraphs broken naturally at sentence boundaries
Example:
# תמלול: meeting.m4a
**תאריך:** 27.1.2026
**משך:** 45:32
---
**דובר 1:**
שלום לכולם, אני רוצה להתחיל את הפגישה...
**דובר 2:**
בסדר, בוא נדבר על הנושא הראשון...
**[5:00]**
**דובר 1:**
עכשיו נעבור לחלק השני...Translation
If target language differs from audio language:
1. Transcribe first (get original language SRT) 2. Read the SRT, translate each entry preserving timestamps 3. Write translated SRT file
No API needed - translate directly.
Post-Transcription Refinement (Claude's Job)
After generating the raw SRT, always refine it semantically. The transcription API chunks by time intervals, not meaning - this creates awkward splits like:
1
00:00:00,500 --> 00:00:01,922
anything a human being can
2
00:00:02,002 --> 00:00:03,320
do, your Claude bot canThe word "do" belongs with the previous sentence! Always regroup by semantic meaning:
Refinement Rules
1. Complete sentences/clauses together - Never split subject from verb, verb from object 2. Move orphaned words - If a line starts with a word that completes the previous thought, merge it 3. Punctuation guides splits - Periods, question marks, exclamations are natural break points 4. Preserve parallel structure - Keep matching phrases together (e.g., "if X can do it, Y can do it") 5. Emphasis stays standalone - Repeated words for emphasis ("Anything. Anything!") get their own line 6. Action chains can split - Lists of actions can be separate lines if they're complete thoughts
Refinement Process
1. Read the raw SRT 2. Concatenate all text to see full content 3. Identify natural sentence/clause boundaries 4. Redistribute words into semantic groups 5. Adjust timestamps: line starts at first word's time, ends at last word's time + small buffer 6. Write refined SRT
Example Before/After
Before (raw):
1
00:00:00,500 --> 00:00:01,922
anything a human being can
2
00:00:02,002 --> 00:00:03,320
do, your Claude bot can
3
00:00:03,341 --> 00:00:06,282
do. Anything. Anything!After (refined):
1
00:00:00,500 --> 00:00:02,100
anything a human being can do,
2
00:00:02,100 --> 00:00:03,500
your Claude bot can do.
3
00:00:03,600 --> 00:00:06,100
Anything. Anything!Always apply this refinement after transcription. The user expects semantically coherent subtitles.
Environment
API key stored in scripts/.env:
ELEVENLABS_API_KEY=your_key_here#!/usr/bin/env npx ts-node
/**
* Transcribe audio/video to SRT + readable text using ElevenLabs Scribe v2
*/
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
import { program } from 'commander';
dotenv.config();
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;
const ELEVENLABS_BASE_URL = 'https://api.elevenlabs.io';
interface Word {
word: string;
start: number;
end: number;
speaker_id?: string;
}
interface TranscriptResult {
text: string;
words: Word[];
duration: number;
language: string;
}
interface DubbingStatus {
dubbing_id: string;
name: string;
status: string;
target_languages: string[];
error?: string;
}
interface SubtitleEntry {
index: number;
start: number;
end: number;
text: string;
}
function secondsToSrtTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const millis = Math.floor((seconds % 1) * 1000);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')},${millis.toString().padStart(3, '0')}`;
}
async function transcribe(inputPath: string, language?: string, enableSpeakers: boolean = true): Promise<TranscriptResult> {
if (!ELEVENLABS_API_KEY) {
throw new Error('ELEVENLABS_API_KEY not set in .env');
}
if (!fs.existsSync(inputPath)) {
throw new Error(`Input file not found: ${inputPath}`);
}
console.log(`Transcribing: ${path.basename(inputPath)}`);
console.log(`Language: ${language || 'auto-detect'}`);
console.log(`Speaker diarization: ${enableSpeakers ? 'enabled' : 'disabled'}`);
const formData = new FormData();
const fileBuffer = fs.readFileSync(inputPath);
const blob = new Blob([fileBuffer]);
formData.append('file', blob, path.basename(inputPath));
formData.append('model_id', 'scribe_v2');
formData.append('tag_audio_events', 'false');
// Enable speaker diarization
if (enableSpeakers) {
formData.append('diarize', 'true');
}
if (language) {
formData.append('language_code', language);
}
const response = await fetch(`${ELEVENLABS_BASE_URL}/v1/speech-to-text`, {
method: 'POST',
headers: {
'xi-api-key': ELEVENLABS_API_KEY,
},
body: formData,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Transcription failed: ${response.status} - ${errorText}`);
}
const result = await response.json() as any;
const words: Word[] = (result.words || []).map((w: any) => ({
word: w.text || w.word || '',
start: w.start || 0,
end: w.end || 0,
speaker_id: w.speaker_id || undefined,
}));
const duration = words.length > 0 ? Math.max(...words.map(w => w.end)) : 0;
console.log(`Done: ${words.length} words, ${duration.toFixed(1)}s`);
return {
text: result.text || '',
words,
duration,
language: result.language_code || language || 'unknown',
};
}
function generateSrt(
words: Word[],
options: {
maxWords: number;
maxDuration: number;
maxChars: number;
timingOffset: number;
}
): SubtitleEntry[] {
const entries: SubtitleEntry[] = [];
let currentWords: Word[] = [];
let currentStart: number | null = null;
for (const word of words) {
const wordText = word.word.trim();
if (!wordText) continue;
if (currentStart === null) {
currentStart = word.start;
}
const testText = [...currentWords.map(w => w.word), wordText].join(' ');
const entryDuration = word.end - currentStart;
const shouldBreak =
testText.length > options.maxChars ||
entryDuration > options.maxDuration ||
currentWords.length >= options.maxWords;
if (shouldBreak && currentWords.length > 0) {
const lastWord = currentWords[currentWords.length - 1];
const entryEnd = lastWord.end;
const text = currentWords.map(w => w.word).join(' ');
entries.push({
index: entries.length + 1,
start: currentStart + options.timingOffset,
end: entryEnd + options.timingOffset,
text,
});
currentWords = [word];
currentStart = word.start;
} else {
currentWords.push(word);
}
}
// Add final entry
if (currentWords.length > 0 && currentStart !== null) {
const lastWord = currentWords[currentWords.length - 1];
const text = currentWords.map(w => w.word).join(' ');
entries.push({
index: entries.length + 1,
start: currentStart + options.timingOffset,
end: lastWord.end + options.timingOffset,
text,
});
}
return entries;
}
function writeSrt(entries: SubtitleEntry[], outputPath: string): void {
const lines: string[] = [];
for (const entry of entries) {
lines.push(entry.index.toString());
lines.push(`${secondsToSrtTime(entry.start)} --> ${secondsToSrtTime(entry.end)}`);
lines.push(entry.text);
lines.push('');
}
fs.writeFileSync(outputPath, lines.join('\n'), 'utf-8');
console.log(`Written: ${outputPath} (${entries.length} entries)`);
}
function formatTimeSimple(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
function generateReadableText(
words: Word[],
options: {
timestampInterval: number; // seconds between timestamps (default 300 = 5 min)
inputFileName: string;
}
): string {
const lines: string[] = [];
let currentSpeaker: string | null = null;
let currentParagraph: string[] = [];
let paragraphStart: number | null = null;
let lastTimestamp = 0;
// Header
lines.push(`# תמלול: ${options.inputFileName}`);
lines.push(`**תאריך:** ${new Date().toLocaleDateString('he-IL')}`);
const totalDuration = words.length > 0 ? Math.max(...words.map(w => w.end)) : 0;
lines.push(`**משך:** ${formatTimeSimple(totalDuration)}`);
lines.push('');
lines.push('---');
lines.push('');
for (let i = 0; i < words.length; i++) {
const word = words[i];
const wordText = word.word.trim();
if (!wordText) continue;
// Check if we need a timestamp marker (every ~5 minutes)
if (word.start - lastTimestamp >= options.timestampInterval) {
// Flush current paragraph
if (currentParagraph.length > 0) {
lines.push(currentParagraph.join(' '));
lines.push('');
currentParagraph = [];
}
// Add timestamp marker
lines.push(`**[${formatTimeSimple(word.start)}]**`);
lines.push('');
lastTimestamp = word.start;
paragraphStart = word.start;
}
// Check for speaker change
const speakerId = word.speaker_id || 'unknown';
if (speakerId !== currentSpeaker) {
// Flush current paragraph
if (currentParagraph.length > 0) {
lines.push(currentParagraph.join(' '));
lines.push('');
currentParagraph = [];
}
currentSpeaker = speakerId;
// Add speaker label (only if we have actual speaker info)
if (word.speaker_id) {
const speakerLabel = `דובר ${speakerId.replace('speaker_', '')}`;
lines.push(`**${speakerLabel}:**`);
}
paragraphStart = word.start;
}
if (paragraphStart === null) {
paragraphStart = word.start;
}
currentParagraph.push(wordText);
// Natural paragraph breaks: after sentence-ending punctuation + some length
const endsWithPunctuation = /[.!?،]$/.test(wordText);
const isParagraphLongEnough = currentParagraph.join(' ').length > 300;
if (endsWithPunctuation && isParagraphLongEnough) {
lines.push(currentParagraph.join(' '));
lines.push('');
currentParagraph = [];
paragraphStart = null;
}
}
// Flush remaining paragraph
if (currentParagraph.length > 0) {
lines.push(currentParagraph.join(' '));
lines.push('');
}
// Footer
lines.push('---');
lines.push('');
lines.push('*תמלול אוטומטי - ElevenLabs Scribe v2*');
return lines.join('\n');
}
function writeReadableText(words: Word[], outputPath: string, inputFileName: string, timestampInterval: number = 300): void {
const content = generateReadableText(words, {
timestampInterval,
inputFileName,
});
fs.writeFileSync(outputPath, content, 'utf-8');
console.log(`Written: ${outputPath} (readable text)`);
}
async function main() {
program
.requiredOption('-i, --input <path>', 'Input audio/video file')
.requiredOption('-o, --output <path>', 'Output SRT file path')
.option('-l, --language <code>', 'Language code (en, he, ar, etc.)')
.option('--max-words <n>', 'Max words per subtitle', '5')
.option('--max-duration <s>', 'Max duration per subtitle in seconds', '3.0')
.option('--max-chars <n>', 'Max characters per subtitle', '70')
.option('--timing-offset <s>', 'Timing offset in seconds', '0.25')
.option('--json', 'Also output raw transcript JSON')
.option('--text', 'Also output readable text file (with speakers + timestamps)')
.option('--no-srt', 'Skip SRT generation (only generate text/json)')
.option('--no-speakers', 'Disable speaker diarization')
.option('--timestamp-interval <s>', 'Interval for timestamps in readable text (seconds)', '300')
.parse();
const opts = program.opts();
try {
const enableSpeakers = opts.speakers !== false;
const transcript = await transcribe(opts.input, opts.language, enableSpeakers);
// Generate SRT (unless --no-srt)
if (opts.srt !== false) {
const entries = generateSrt(transcript.words, {
maxWords: parseInt(opts.maxWords),
maxDuration: parseFloat(opts.maxDuration),
maxChars: parseInt(opts.maxChars),
timingOffset: parseFloat(opts.timingOffset),
});
writeSrt(entries, opts.output);
}
// Generate readable text (if --text or always by default now)
const textPath = opts.output.replace(/\.srt$/i, '.md');
const inputFileName = path.basename(opts.input);
writeReadableText(
transcript.words,
textPath,
inputFileName,
parseInt(opts.timestampInterval)
);
// Generate JSON (if --json)
if (opts.json) {
const jsonPath = opts.output.replace(/\.srt$/i, '_transcript.json');
fs.writeFileSync(jsonPath, JSON.stringify(transcript, null, 2), 'utf-8');
console.log(`Written: ${jsonPath}`);
}
console.log('\n✓ Transcription complete!');
} catch (error) {
console.error('Error:', (error as Error).message);
process.exit(1);
}
}
main();
Related skills
FAQ
What model does it use?
ElevenLabs Scribe v2 via the speech-to-text API.
What files does it output?
Always an .srt and a readable .md; optionally _transcript.json with word-level data.