Skill: sxliuyu/video-transcribe-pro (version 1.0.0)

# video-to-text - 免费视频转文字 API ## 概述 免费视频转文字 API - 用户需要免费、无需本地下载、直接使用 API 将视频/音频转为文字。 ## 触发词 当用户提到以下内容时激活此技能： - "视频转文字" - "音频转文字" - "语音转文字" - "video to text" - "audio to text" - "speech to text" - "字幕提取" - "提取字幕" - "视频转字幕" - "免费转文字" - "API转文字" ## 工具 此技能提供以下工具： ### video_to_text 将视频或音频文件转换为文字（字幕/ transcripts）。 **参数：** - `url` (必填): 视频/音频文件的 URL 地址，支持 mp4, wav, mp3, m4a, webm, ogg 等格式 - `language` (可选): 语言代码，如 "zh" (中文)、"en" (英语)、"ja" (日语)，默认自动检测 - `output_format` (可选): 输出格式，"text" (纯文字) 或 "srt" (字幕格式)，默认 "text" ## 使用示例 ``` 用户: 帮我把这个视频转成文字 AI: (使用 video_to_text 工具，传入视频URL) 用户: 提取这个音频的字幕 AI: (使用 video_to_text 工具，设置 output_format="srt") 用户: 把这个mp3转成文字 AI: (使用 video_to_text 工具) ``` ## 实现说明 此技能使用免费的 Whisper API 服务进行语音识别，无需 API Key，直接调用即可使用。 支持的免费 API 端点： - https://api.myshell.ai/v1/audio/transcriptions (MyShell Whisper) - https://api.openai.com/v1/audio/transcriptions (OpenAI，需要Key) 如果主要API不可用，会自动尝试备用方案。 ## 注意事项 - 支持的输入格式：mp4, wav, mp3, m4a, webm, ogg, flac - 最大文件大小：25MB - 处理时间取决于文件长度，通常需要等待 - 建议使用直接文件URL，避免需要认证的链接

path: _meta.json
content: {
  "version": "1.0.0",
  "openclaw": {
    "emoji": "🎬",
    "triggers": [
      "视频转文字",
      "音频转文字",
      "语音转文字",
      "字幕提取",
      "提取字幕",
      "视频转字幕",
      "免费转文字",
      "video to text",
      "audio to text", 
      "speech to text"
    ],
    "requires": {
      "bins": ["node"],
      "pip": []
    },
    "install": []
  },
  "tools": [
    {
      "name": "video_to_text",
      "description": "将视频或音频文件转换为文字（字幕/transcripts）。使用免费 Whisper API，无需 API Key。",
      "parameters": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "视频/音频文件的 URL 地址，支持 mp4, wav, mp3, m4a, webm, ogg 等格式"
          },
          "language": {
            "type": "string",
            "description": "语言代码，如 'zh' (中文)、'en' (英语)、'ja' (日语)，默认自动检测",
            "default": "zh"
          },
          "output_format": {
            "type": "string",
            "description": "输出格式：'text' (纯文字) 或 'srt' (字幕格式)",
            "default": "text",
            "enum": ["text", "srt"]
          }
        },
        "required": ["url"]
      }
    }
  ]
}


path: index.js
content: #!/usr/bin/env node

/**
 * video-to-text - 免费视频转文字 API 工具
 * 使用免费的 Whisper API 将视频/音频转为文字
 */

const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
const os = require('os');

// 配置
const CONFIG = {
  // 首选免费 API (MyShell)
  primaryApi: 'https://api.myshell.ai/v1/audio/transcriptions',
  // 备用 API (OpenAI，需要Key时可配置)
  backupApi: 'https://api.openai.com/v1/audio/transcriptions',
  maxFileSize: 25 * 1024 * 1024, // 25MB
};

/**
 * 下载文件到临时目录
 */
async function downloadFile(url) {
  return new Promise((resolve, reject) => {
    const protocol = url.startsWith('https') ? https : http;
    
    console.log(`📥 正在下载文件: ${url}`);
    
    protocol.get(url, (response) => {
      // 检查 Content-Length
      const contentLength = parseInt(response.headers['content-length'] || '0', 10);
      if (contentLength > CONFIG.maxFileSize) {
        reject(new Error(`文件太大: ${(contentLength / 1024 / 1024).toFixed(2)}MB，最大支持 ${CONFIG.maxFileSize / 1024 / 1024}MB`));
        return;
      }

      const tmpDir = os.tmpdir();
      const ext = path.extname(url.split('?')[0]) || '.tmp';
      const tempFile = path.join(tmpDir, `video-to-text-${Date.now()}${ext}`);
      const fileStream = fs.createWriteStream(tempFile);

      let downloaded = 0;
      response.on('data', (chunk) => {
        downloaded += chunk.length;
        if (contentLength) {
          const percent = ((downloaded / contentLength) * 100).toFixed(1);
          process.stdout.write(`\r📥 下载进度: ${percent}%`);
        }
      });

      response.pipe(fileStream);

      fileStream.on('finish', () => {
        console.log(`\n✅ 文件已保存到: ${tempFile}`);
        resolve(tempFile);
      });

      fileStream.on('error', (err) => {
        fs.unlink(tempFile, () => {});
        reject(err);
      });
    }).on('error', reject);
  });
}

/**
 * 使用 MyShell API 转写
 */
async function transcribeWithMyShell(filePath, language = 'zh') {
  const boundary = '----FormBoundary' + Date.now();
  
  const filename = path.basename(filePath);
  const fileContent = fs.readFileSync(filePath);

  const header = Buffer.from(
    `--${boundary}\r\n` +
    `Content-Disposition: form-data; name="file"; filename="${filename}"\r\n` +
    `Content-Type: audio/mpeg\r\n\r\n`
  );
  const footer = Buffer.from(`\r\n--${boundary}\r\n--${boundary}--\r\n`);
  
  const languagePart = Buffer.from(
    `--${boundary}\r\n` +
    `Content-Disposition: form-data; name="language"\r\n\r\n` +
    `${language}\r\n`
  );
  
  const modelPart = Buffer.from(
    `--${boundary}\r\n` +
    `Content-Disposition: form-data; name="model"\r\n\r\n` +
    `whisper-1\r\n`
  );

  const body = Buffer.concat([header, fileContent, languagePart, modelPart, footer]);

  return new Promise((resolve, reject) => {
    const url = new URL(CONFIG.primaryApi);
    
    const options = {
      hostname: url.hostname,
      port: url.port || 443,
      path: url.pathname,
      method: 'POST',
      headers: {
        'Content-Type': `multipart/form-data; boundary=${boundary}`,
        'Content-Length': body.length,
      }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try {
          const result = JSON.parse(data);
          if (result.text) {
            resolve(result.text);
          } else if (result.error) {
            reject(new Error(result.error.message || result.error));
          } else {
            reject(new Error(`API返回异常: ${data}`));
          }
        } catch (e) {
          resolve(data); // 有些API直接返回文本
        }
      });
    });

    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

/**
 * 主函数
 */
async function main() {
  const args = process.argv.slice(2);
  
  // 解析参数
  let url = null;
  let language = 'zh';
  let outputFormat = 'text';
  
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--url' && args[i + 1]) {
      url = args[i + 1];
      i++;
    } else if (args[i] === '--language' && args[i + 1]) {
      language = args[i + 1];
      i++;
    } else if (args[i] === '--format' && args[i + 1]) {
      outputFormat = args[i + 1];
      i++;
    } else if (!url && args[i].startsWith('http')) {
      url = args[i];
    }
  }

  if (!url) {
    console.error('❌ 请提供视频/音频文件的URL');
    console.log('用法: video-to-text <url> [--language zh] [--format text|srt]');
    console.log('示例: video-to-text https://example.com/audio.mp3 --language zh');
    process.exit(1);
  }

  try {
    console.log('🎬 开始视频转文字处理...');
    console.log(`📎 输入文件: ${url}`);
    console.log(`🌐 语言: ${language}`);
    console.log(`📝 输出格式: ${outputFormat}`);
    
    // 下载文件
    const filePath = await downloadFile(url);
    
    // 转写
    console.log('🔄 正在识别语音...');
    const text = await transcribeWithMyShell(filePath, language);
    
    // 清理临时文件
    fs.unlink(filePath, () => {});
    
    // 输出结果
    if (outputFormat === 'srt') {
      // 转换为 SRT 格式
      const srt = convertToSRT(text);
      console.log('\n📄 字幕内容 (SRT格式):\n');
      console.log(srt);
    } else {
      console.log('\n📄 转写结果:\n');
      console.log(text);
    }
    
  } catch (error) {
    console.error('❌ 错误:', error.message);
    
    // 如果首选API失败，提示备用方案
    if (error.message.includes('401') || error.message.includes('403')) {
      console.log('\n💡 提示: 主要API可能需要认证，尝试使用备用方案或配置OpenAI API Key');
    }
    process.exit(1);
  }
}

/**
 * 将纯文本转换为 SRT 字幕格式
 */
function convertToSRT(text) {
  // 简单按句分割，每句约10-15字为一条字幕
  const sentences = text.split(/[。！？\n]/).filter(s => s.trim());
  let srtContent = '';
  let startTime = 0;
  
  sentences.forEach((sentence, index) => {
    const trimmed = sentence.trim();
    if (!trimmed) return;
    
    // 每条字幕约3秒（根据字数调整）
    const duration = Math.max(2, Math.ceil(trimmed.length / 5));
    const endTime = startTime + duration;
    
    srtContent += `${index + 1}\n`;
    srtContent += `${formatSRTTime(startTime)} --> ${formatSRTTime(endTime)}\n`;
    srtContent += `${trimmed}\n\n`;
    
    startTime = endTime;
  });
  
  return srtContent;
}

/**
 * 格式化 SRT 时间
 */
function formatSRTTime(seconds) {
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')},000`;
}

// 导出供外部调用
module.exports = { transcribe: main };

// 直接运行
if (require.main === module) {
  main();
}


path: package.json
content: {
  "name": "video-to-text",
  "version": "1.0.0",
  "description": "免费视频转文字 API - 使用免费 Whisper API 将视频/音频转为文字",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "transcribe": "node index.js"
  },
  "keywords": [
    "video",
    "audio",
    "transcription",
    "whisper",
    "speech-to-text",
    "字幕"
  ],
  "author": "",
  "license": "MIT"
}


path: scripts/video2text.py
content: #!/usr/bin/env python3
"""
Video to Text - 视频转文字工具
支持本地处理和在线API
"""
import os
import sys
import argparse
import subprocess
import json

def extract_audio(video_path, output_path=None):
    """从视频提取音频"""
    if not output_path:
        output_path = video_path.rsplit('.', 1)[0] + '.wav'
    
    cmd = ['ffmpeg', '-i', video_path, '-vn', '-acodec', 'pcm_s16le', '-ar', '16000', '-ac', '1', output_path, '-y']
    try:
        subprocess.run(cmd, check=True)
        print(f"✅ 音频已提取: {output_path}")
        return output_path
    except subprocess.CalledProcessError:
        print("❌ ffmpeg 未安装，请先安装: sudo apt install ffmpeg")
        return None

def transcribe_with_local(video_path, model='base', language='zh'):
    """使用本地 Whisper 转写"""
    try:
        import whisper
    except ImportError:
        print("请安装 whisper: pip install openai-whisper")
        return None
    
    print(f"🔄 加载模型: {model}...")
    model = whisper.load_model(model)
    
    print("🔄 转写中...")
    result = model.transcribe(video_path, language=language)
    
    return result['text']

def transcribe_assemblyai(audio_path, api_key):
    """使用 AssemblyAI API"""
    import requests
    
    # 上传音频
    with open(audio_path, 'rb') as f:
        response = requests.post(
            'https://api.assemblyai.com/v2/upload',
            headers={'authorization': api_key},
            files={'file': f}
        )
    
    audio_url = response.json()['upload_url']
    
    # 转写
    response = requests.post(
        'https://api.assemblyai.com/v2/transcript',
        headers={'authorization': api_key},
        json={'audio_url': audio_url, 'language_code': 'zh'}
    )
    
    transcript_id = response.json()['id']
    
    # 等待结果
    import time
    while True:
        response = requests.get(
            f'https://api.assemblyai.com/v2/transcript/{transcript_id}',
            headers={'authorization': api_key}
        )
        status = response.json()['status']
        
        if status == 'completed':
            return response.json()['text']
        elif status == 'error':
            return None
        time.sleep(3)

def cmd_convert(args):
    video_path = args.video
    
    if not os.path.exists(video_path):
        print(f"❌ 文件不存在: {video_path}")
        return
    
    output = args.output or video_path.rsplit('.', 1)[0] + '.txt'
    lang = args.lang or 'zh'
    
    # 提取音频
    audio_path = video_path.rsplit('.', 1)[0] + '.wav'
    print("🔄 提取音频...")
    extract_audio(video_path, audio_path)
    
    # 转写
    if args.api == 'assemblyai' and args.[REDACTED_SECRET]"🔄 使用 AssemblyAI 转写...")
        text = transcribe_assemblyai(audio_path, args.api_key)
    else:
        print("🔄 使用本地 Whisper 转写...")
        text = transcribe_with_local(video_path, language=lang)
    
    if text:
        with open(output, 'w', encoding='utf-8') as f:
            f.write(text)
        print(f"✅ 已保存: {output}")
    else:
        print("❌ 转写失败")

def cmd_extract_audio(args):
    video_path = args.video
    output = args.output or video_path.rsplit('.', 1)[0] + '.wav'
    extract_audio(video_path, output)

def main():
    parser = argparse.ArgumentParser(description='Video to Text')
    subparsers = parser.add_subparsers()
    
    # convert
    p_conv = subparsers.add_parser('convert', help='视频转文字')
    p_conv.add_argument('video', help='视频文件路径')
    p_conv.add_argument('--lang', '-l', default='zh', help='语言: zh/en')
    p_conv.add_argument('--output', '-o', help='输出文件')
    p_conv.add_argument('--api', default='local', choices=['local', 'assemblyai'], help='转写方式')
    p_conv.add_argument('--api-key', help='API Key (AssemblyAI)')
    p_conv.set_defaults(func=cmd_convert)
    
    # extract-audio
    p_audio = subparsers.add_parser('extract-audio', help='提取音频')
    p_audio.add_argument('video', help='视频文件')
    p_audio.add_argument('--output', '-o', help='输出音频文件')
    p_audio.set_defaults(func=cmd_extract_audio)
    
    args = parser.parse_args()
    if hasattr(args, 'func'):
        args.func(args)
    else:
        parser.print_help()

if __name__ == '__main__':
    main()


path: tool.js
content: /**
 * video-to-text Skill - OpenClaw Tool Wrapper
 * 
 * 将视频/音频文件转换为文字
 */

const { execSync } = require('child_process');
const path = require('path');

/**
 * 视频转文字工具
 * @param {Object} params 参数
 * @param {string} params.url 视频/音频文件URL
 * @param {string} params.language 语言代码 (zh/en/ja)
 * @param {string} params.output_format 输出格式 (text/srt)
 * @returns {Object} 转写结果
 */
async function video_to_text(params) {
  const { url, language = 'zh', output_format = 'text' } = params;
  
  if (!url) {
    return {
      success: false,
      error: '请提供视频/音频文件的URL'
    };
  }

  try {
    // 构建命令
    const scriptPath = path.join(__dirname, 'index.js');
    const args = [
      'node',
      scriptPath,
      '--url', url,
      '--language', language,
      '--format', output_format
    ];
    
    // 执行转写
    const output = execSync(args.join(' '), {
      encoding: 'utf-8',
      maxBuffer: 10 * 1024 * 1024, // 10MB
      timeout: 300000 // 5分钟超时
    });

    return {
      success: true,
      result: output,
      format: output_format
    };
  } catch (error) {
    return {
      success: false,
      error: error.message || '转写失败'
    };
  }
}

module.exports = {
  video_to_text
};

// 如果直接运行
if (require.main === module) {
  const url = process.argv[2];
  const language = process.argv[3] || 'zh';
  const format = process.argv[4] || 'text';
  
  video_to_text({ url, language, output_format: format })
    .then(result => {
      console.log(JSON.stringify(result, null, 2));
    })
    .catch(err => {
      console.error(JSON.stringify({ success: false, error: err.message }));
    });
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)