Skill: guorui303/bilibili-transcriber (version 1.0.0)

--- name: bilibili-transcriber version: 1.0.0 description: Bilibili视频转文字摘要专家。支持云端（阿里云Paraformer）和本地（faster-whisper）双引擎转录。当用户提供B站视频URL时，自动下载音频、转录成文字、生成结构化摘要。支持BV号和完整URL。 author: 郭锐 license: MIT tags: - bilibili - transcribe - whisper - video - chinese tools: WebFetch, WebSearch, Bash, Write, Read model: performance --- 你是Bilibili视频内容处理专家。你的任务是将B站视频转换为文字并生成高质量摘要。 ## 工作流程 ### 步骤1：解析视频信息 - 从URL中提取BV号（如 BV1xx411c7mD） - 如果用户提供的是短链接，先解析获取完整URL - 调用B站API获取视频基本信息（标题、UP主、时长、简介等） ### 步骤2：获取视频字幕/文字内容 **优先方案：获取CC字幕** ```bash # 调用B站API检查是否有官方字幕 curl "https://api.bilibili.com/x/player/wbi/v2?cid={cid}&bvid={bvid}" ``` **备选方案A（推荐）：阿里云 Paraformer 云端转写** 如果视频没有字幕，优先使用云端转写（速度快、方言准、不依赖GPU）： 1. **下载音频** ```bash python -m yt_dlp -f "bestaudio" --extract-audio --audio-format m4a -o "{output_path}.%(ext)s" "{video_url}" ``` 2. **云端转写** ```python from cloud_transcriber import cloud_transcribe # 上传音频 → Paraformer 转写 → 返回带时间戳的结果 segments = cloud_transcribe("audio.m4a") for seg in segments: print(f"[{seg['start']:.1f}s] {seg['text']}") ``` 需要设置环境变量 `DASHSCOPE_API_KEY` 或 `OPENAI_API_KEY`（阿里云百炼 API Key）。 依赖安装：`pip install dashscope requests` **备选方案B：本地 faster-whisper 转录（离线/无API Key时使用）** 如果没有 API Key 或需要离线使用，回退到本地转录： 1. **下载音频** ```bash python -m yt_dlp -f "bestaudio" --extract-audio --audio-format m4a -o "{output_path}.%(ext)s" "{video_url}" ``` 2. **音频格式处理（使用ffmpeg）** ```bash # 将m4a转换为wav格式（whisper推荐格式） ffmpeg -i input.m4a -ar 16000 -ac 1 -c:a pcm_s16le output.wav ``` 3. **语音转文字（faster-whisper + 模型缓存）** **使用预置的 transcriber 模块（推荐）：** ```python from transcriber import transcribe_audio # 首次调用会加载模型（约2-5秒），后续调用直接使用缓存模型 text = transcribe_audio("audio.wav", language="zh") print(text) ``` **如果需要批量处理多个视频：** ```python from transcriber import batch_transcribe audio_files = ["video1.wav", "video2.wav", "video3.wav"] results = batch_transcribe(audio_files, language="zh") for path, text in results.items(): print(f"{path}: {text[:100]}...") ``` ### 步骤3：生成结构化摘要 基于转录文本生成以下内容的摘要： - **视频标题**：原始标题 - **UP主**：创作者名称 - **视频时长**：总时长 - **核心观点**（3-5条）：视频传达的主要观点 - **详细摘要**（300-500字）：按时间线或主题组织的内容概述 - **关键时间节点**：重要内容的时间戳标记（格式：`[02:15] 讲解OpenClaw安装步骤`） - **适用人群**：这个视频适合谁看 ### 步骤4：输出格式 - 使用Markdown格式输出 - 保持条理清晰，层次分明 - 如果内容较长，保存为`.md`文件到用户工作目录 ## transcriber.py 模块说明 该 skill 包含 `transcriber.py` 模块，提供以下特性： ### 核心功能 - **模型单例缓存**：首次加载后常驻内存，后续调用零延迟 - **自动设备检测**：优先使用 GPU（CUDA），自动回退到 CPU - **量化优化**：默认使用 int8 量化，速度提升 4-5 倍 - **批量处理支持**：一次性处理多个文件，只加载一次模型 ### API 说明 ```python from transcriber import transcribe_audio, batch_transcribe, get_model_info # 转录单个文件（首次加载模型约2-5秒，后续<100ms） text = transcribe_audio("audio.wav", language="zh") # 批量转录（共享模型实例） results = batch_transcribe(["a.wav", "b.wav"], language="zh") # 查看模型信息 info = get_model_info() print(info) ``` ### 性能对比 | 方案 | 首次调用 | 后续调用 | 内存占用 | 准确率 | |------|---------|---------|---------|--------| | 原 whisper | 5-10s | 5-10s | ~1GB | 高 | | faster-whisper (本方案) | 2-5s | <100ms | ~500MB | 高 | ## 依赖安装 首次使用前需要安装依赖： ```bash pip install faster-whisper yt-dlp # ffmpeg 需要单独安装 # Windows: winget install ffmpeg # macOS: brew install ffmpeg # Linux: sudo apt install ffmpeg ``` ## 工具说明 ### ffmpeg 用途 - **格式转换**：m4a → wav/mp3 - **音频处理**：调整采样率、声道数 - **提取音频**：从视频文件中提取音轨 **常用命令：** ```bash # 查看音频信息 ffmpeg -i audio.m4a # 转换格式（whisper推荐：16kHz, 单声道, 16bit） ffmpeg -i input.m4a -ar 16000 -ac 1 -c:a pcm_s16le output.wav # 提取视频音频 ffmpeg -i video.mp4 -vn -acodec copy output.aac ``` ## 错误处理 - 如果视频无字幕，自动进入音频转录流程 - 如果 faster-whisper 未安装，提示用户安装：`pip install faster-whisper` - 如果 ffmpeg 未安装，提供安装指南 - 模型加载失败时自动清理缓存重试 - 始终尊重版权，仅用于个人学习和研究 ## 最佳实践 - **批量处理**：如需处理多个视频，使用 `batch_transcribe()` 函数避免重复加载模型 - **模型选择**：默认使用 "small" 模型（中文效果好），如需更快可使用 "tiny"，如需更准可使用 "base" - **文件格式**：优先使用 wav 格式（16kHz, 单声道），兼容性最好 - 摘要要准确反映原视频内容，不添加个人偏见 - 时间节点标记要精确到秒 - 核心观点要用简洁的语言概括 - 对于技术教程类视频，要突出关键步骤和命令

path: cloud_transcriber.py
content: """
阿里云百炼 Paraformer 云端语音转写模块
上传本地音频 → 获取临时 oss:// URL → Paraformer 异步转写 → 返回带时间戳的结果

优势：比本地 whisper 快 10-20 倍，不依赖 GPU，方言识别更准
依赖：pip install dashscope requests
"""

import os
import time
import json
import requests
from pathlib import Path
from typing import Optional, List, Dict


# 支持的音频格式
SUPPORTED_FORMATS = {
    ".aac", ".amr", ".avi", ".flac", ".flv", ".m4a",
    ".mkv", ".mov", ".mp3", ".mp4", ".mpeg", ".ogg",
    ".opus", ".wav", ".webm", ".wma", ".wmv"
}


def _get_api_key() -> str:
    """获取 API Key（优先 DASHSCOPE_API_KEY，其次 OPENAI_API_KEY）"""
    key = os.environ.get("DASHSCOPE_API_KEY") or os.environ.get("OPENAI_API_KEY")
    if not key:
        raise ValueError(
            "未找到 API Key，请设置环境变量 DASHSCOPE_API_KEY 或 OPENAI_API_KEY\n"
            "例如: export DASHSCOPE_API_KEY=sk-xxxxxxxx"
        )
    return key


def upload_audio_to_dashscope(
    file_path: str,
    [REDACTED_SECRET] = None,
    model: str = "paraformer-v2"
) -> str:
    """
    上传本地音频文件到阿里云百炼临时存储，获取 oss:// URL

    Args:
        file_path: 本地音频文件路径
        api_key: 百炼 API Key
        model: 模型名称（用于绑定文件访问权限）

    Returns:
        oss:// 格式的临时 URL（有效期 48 小时）
    """
    [REDACTED_SECRET] or _get_api_key()

    if not os.path.exists(file_path):
        raise FileNotFoundError(f"音频文件不存在: {file_path}")

    suffix = Path(file_path).suffix.lower()
    if suffix not in SUPPORTED_FORMATS:
        raise ValueError(f"不支持的音频格式: {suffix}，支持: {', '.join(sorted(SUPPORTED_FORMATS))}")

    file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
    print(f"[CloudASR] 上传音频: {Path(file_path).name} ({file_size_mb:.1f} MB)")

    # 步骤 1: 获取上传凭证
    policy_url = "https://dashscope.aliyuncs.com/api/v1/uploads"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    params = {
        "action": "getPolicy",
        "model": model
    }

    resp = requests.get(policy_url, headers=headers, params=params, timeout=30)
    if resp.status_code != 200:
        raise Exception(f"获取上传凭证失败 (HTTP {resp.status_code}): {resp.text}")

    policy_data = resp.json().get("data", {})
    if not policy_data:
        raise Exception(f"上传凭证为空: {resp.json()}")

    # 步骤 2: 上传文件到 OSS
    file_name = Path(file_path).name
    key = f"{policy_data['upload_dir']}/{file_name}"

    with open(file_path, 'rb') as f:
        files = {
            'OSSAccessKeyId': (None, policy_data['oss_access_key_id']),
            'Signature': (None, policy_data['signature']),
            'policy': (None, policy_data['policy']),
            'x-oss-object-acl': (None, policy_data['x_oss_object_acl']),
            'x-oss-forbid-overwrite': (None, policy_data['x_oss_forbid_overwrite']),
            'key': (None, key),
            'success_action_status': (None, '200'),
            'file': (file_name, f)
        }

        upload_resp = requests.post(policy_data['upload_host'], files=files, timeout=120)
        if upload_resp.status_code != 200:
            raise Exception(f"文件上传失败 (HTTP {upload_resp.status_code}): {upload_resp.text}")

    oss_url = f"oss://{key}"
    print(f"[CloudASR] 上传成功: {oss_url}")
    return oss_url


def cloud_transcribe(
    file_path: str,
    [REDACTED_SECRET] = None,
    model: str = "paraformer-v2",
    language_hints: Optional[List[str]] = None,
    poll_interval: float = 1.0,
    max_wait: int = 300
) -> List[Dict]:
    """
    使用阿里云百炼 Paraformer 进行云端语音转写

    Args:
        file_path: 本地音频文件路径
        api_key: 百炼 API Key
        model: ASR 模型名称（paraformer-v2, paraformer-v2-8k 等）
        language_hints: 语言提示（如 ["zh", "en"]）
        poll_interval: 轮询间隔（秒）
        max_wait: 最大等待时间（秒）

    Returns:
        带时间戳的片段列表: [{"start": float, "end": float, "text": str}, ...]
    """
    [REDACTED_SECRET] or _get_api_key()
    total_start = time.time()

    # 步骤 1: 上传音频
    t0 = time.time()
    oss_url = upload_audio_to_dashscope(file_path, api_key, model)
    upload_time = time.time() - t0

    # 步骤 2: 提交转写任务
    t1 = time.time()
    submit_url = "https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "X-DashScope-Async": "enable",
        "X-DashScope-OssResourceResolve": "enable"
    }

    request_body = {
        "model": model,
        "input": {
            "file_urls": [oss_url]
        },
        "parameters": {
            "language_hints": language_hints or ["zh"]
        }
    }

    print(f"[CloudASR] 提交转写任务 (模型: {model})...")
    resp = requests.post(submit_url, headers=headers, json=request_body, timeout=30)

    if resp.status_code != 200:
        raise Exception(f"提交任务失败 (HTTP {resp.status_code}): {resp.text}")

    resp_data = resp.json()
    task_id = resp_data.get("output", {}).get("task_id")
    if not task_id:
        raise Exception(f"未获取到 task_id: {resp_data}")

    submit_time = time.time() - t1
    print(f"[CloudASR] 任务已提交: {task_id}")

    # 步骤 3: 轮询等待结果
    t2 = time.time()
    query_url = f"https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}"
    query_headers = {
        "Authorization": f"Bearer {api_key}"
    }

    elapsed = 0
    while elapsed < max_wait:
        time.sleep(poll_interval)
        elapsed = time.time() - t2

        query_resp = requests.get(query_url, headers=query_headers, timeout=30)
        if query_resp.status_code != 200:
            print(f"[CloudASR] 查询失败，重试中...")
            continue

        query_data = query_resp.json()
        status = query_data.get("output", {}).get("task_status", "UNKNOWN")

        if status == "SUCCEEDED":
            wait_time = time.time() - t2
            print(f"[CloudASR] 转写完成! 等待耗时: {wait_time:.1f}s")
            break
        elif status == "FAILED":
            error_msg = query_data.get("output", {}).get("message", "未知错误")
            raise Exception(f"转写失败: {error_msg}")
        elif status == "PENDING" or status == "RUNNING":
            if int(elapsed) % 5 == 0 and elapsed > 1:
                print(f"[CloudASR] 转写中... ({elapsed:.0f}s)")
    else:
        raise TimeoutError(f"转写超时（{max_wait}秒）")

    # 步骤 4: 解析结果
    t3 = time.time()
    results_list = query_data.get("output", {}).get("results", [])
    if not results_list:
        raise Exception("未获取到转写结果")

    # 获取转写结果 URL
    transcription_url = results_list[0].get("transcription_url")
    if not transcription_url:
        raise Exception("未获取到 transcription_url")

    # 下载转写结果
    trans_resp = requests.get(transcription_url, timeout=30)
    if trans_resp.status_code != 200:
        raise Exception(f"下载转写结果失败 (HTTP {trans_resp.status_code})")

    trans_data = trans_resp.json()

    # 解析带时间戳的结果
    segments = []
    transcripts = trans_data.get("transcripts", [])

    for transcript in transcripts:
        # 逐句解析
        sentences = transcript.get("sentences", [])
        for sentence in sentences:
            start_ms = sentence.get("begin_time", 0)
            end_ms = sentence.get("end_time", 0)
            text = sentence.get("text", "").strip()
            if text:
                segments.append({
                    "start": start_ms / 1000.0,
                    "end": end_ms / 1000.0,
                    "text": text
                })

        # 如果没有 sentences，尝试用 paragraphs
        if not sentences:
            paragraphs = transcript.get("paragraphs", [])
            for para in paragraphs:
                for sentence in para.get("sentences", []):
                    start_ms = sentence.get("begin_time", 0)
                    end_ms = sentence.get("end_time", 0)
                    text = sentence.get("text", "").strip()
                    if text:
                        segments.append({
                            "start": start_ms / 1000.0,
                            "end": end_ms / 1000.0,
                            "text": text
                        })

    # 如果还是没有分句结果，提取纯文本
    if not segments:
        full_text = ""
        for transcript in transcripts:
            full_text += transcript.get("text", "")
        if full_text:
            segments.append({
                "start": 0.0,
                "end": 0.0,
                "text": full_text.strip()
            })

    parse_time = time.time() - t3
    total_time = time.time() - total_start

    print(f"\n[CloudASR] === 性能统计 ===")
    print(f"  上传耗时:   {upload_time:.1f}s")
    print(f"  提交耗时:   {submit_time:.1f}s")
    print(f"  转写等待:   {wait_time:.1f}s")
    print(f"  解析耗时:   {parse_time:.1f}s")
    print(f"  总耗时:     {total_time:.1f}s")
    print(f"  片段数量:   {len(segments)}")

    return segments


def cloud_transcribe_from_url(
    audio_url: str,
    [REDACTED_SECRET] = None,
    model: str = "paraformer-v2",
    language_hints: Optional[List[str]] = None,
    poll_interval: float = 1.0,
    max_wait: int = 300
) -> List[Dict]:
    """
    直接从公网 URL 进行云端转写（无需上传）

    Args:
        audio_url: 公网可访问的音频 URL
        api_key: 百炼 API Key
        model: ASR 模型名称
        language_hints: 语言提示
        poll_interval: 轮询间隔
        max_wait: 最大等待时间

    Returns:
        带时间戳的片段列表
    """
    [REDACTED_SECRET] or _get_api_key()
    total_start = time.time()

    # 提交转写任务
    submit_url = "https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "X-DashScope-Async": "enable"
    }

    request_body = {
        "model": model,
        "input": {
            "file_urls": [audio_url]
        },
        "parameters": {
            "language_hints": language_hints or ["zh"]
        }
    }

    print(f"[CloudASR] 提交转写任务 (URL模式, 模型: {model})...")
    resp = requests.post(submit_url, headers=headers, json=request_body, timeout=30)

    if resp.status_code != 200:
        raise Exception(f"提交任务失败 (HTTP {resp.status_code}): {resp.text}")

    resp_data = resp.json()
    task_id = resp_data.get("output", {}).get("task_id")
    if not task_id:
        raise Exception(f"未获取到 task_id: {resp_data}")

    print(f"[CloudASR] 任务已提交: {task_id}")

    # 轮询等待
    query_url = f"https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}"
    query_headers = {"Authorization": f"Bearer {api_key}"}

    t_wait = time.time()
    elapsed = 0
    while elapsed < max_wait:
        time.sleep(poll_interval)
        elapsed = time.time() - t_wait

        query_resp = requests.get(query_url, headers=query_headers, timeout=30)
        if query_resp.status_code != 200:
            continue

        query_data = query_resp.json()
        status = query_data.get("output", {}).get("task_status", "UNKNOWN")

        if status == "SUCCEEDED":
            print(f"[CloudASR] 转写完成! 耗时: {elapsed:.1f}s")
            break
        elif status == "FAILED":
            error_msg = query_data.get("output", {}).get("message", "未知错误")
            raise Exception(f"转写失败: {error_msg}")
    else:
        raise TimeoutError(f"转写超时（{max_wait}秒）")

    # 解析结果（复用相同逻辑）
    results_list = query_data.get("output", {}).get("results", [])
    transcription_url = results_list[0].get("transcription_url") if results_list else None
    if not transcription_url:
        raise Exception("未获取到 transcription_url")

    trans_resp = requests.get(transcription_url, timeout=30)
    trans_data = trans_resp.json()

    segments = []
    for transcript in trans_data.get("transcripts", []):
        sentences = transcript.get("sentences", [])
        for sentence in sentences:
            text = sentence.get("text", "").strip()
            if text:
                segments.append({
                    "start": sentence.get("begin_time", 0) / 1000.0,
                    "end": sentence.get("end_time", 0) / 1000.0,
                    "text": text
                })
        if not sentences:
            for para in transcript.get("paragraphs", []):
                for sentence in para.get("sentences", []):
                    text = sentence.get("text", "").strip()
                    if text:
                        segments.append({
                            "start": sentence.get("begin_time", 0) / 1000.0,
                            "end": sentence.get("end_time", 0) / 1000.0,
                            "text": text
                        })

    total_time = time.time() - total_start
    print(f"[CloudASR] 总耗时: {total_time:.1f}s, 片段数: {len(segments)}")

    return segments


if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("用法: python cloud_transcriber.py <音频文件路径或URL>")
        print("示例: python cloud_transcriber.py audio.m4a")
        sys.exit(1)

    target = sys.argv[1]

    if target.startswith("http://") or target.startswith("https://"):
        results = cloud_transcribe_from_url(target)
    else:
        results = cloud_transcribe(target)

    for seg in results:
        start_min = int(seg['start'] // 60)
        start_sec = int(seg['start'] % 60)
        print(f"[{start_min:02d}:{start_sec:02d}] {seg['text']}")


path: README.md
content: # bilibili-transcriber

B站视频转文字摘要工具。贴个链接，自动完成下载、转录、摘要，全程本地处理。

## 功能特点

- **官方字幕优先**：自动检测B站CC字幕（含AI字幕），有字幕直接用，秒级完成
- **Whisper 本地兜底**：无字幕时自动下载音频，使用 faster-whisper 本地转录，不依赖任何外部转录服务
- **智能模型选择**：根据视频时长、内容类型、系统内存自动选择最优 whisper 模型
- **流式内存处理**：小文件直接在内存中处理，跳过磁盘 I/O
- **模型单例缓存**：首次加载后常驻内存，批量处理多个视频时只加载一次模型
- **国内模型加速**：集成 ModelScope / HF-Mirror，国内下载 whisper 模型无需翻墙
- **结构化输出**：生成带时间戳的转录文本和 Markdown 格式摘要

## 使用方式

直接告诉 AI agent 一个B站视频链接即可，例如：

```
帮我摘要这个视频 https://www.bilibili.com/video/BV1xxxxxx
```

支持的输入格式：
- 完整 URL：`https://www.bilibili.com/video/BV1xxxxxx`
- BV 号：`BV1xxxxxx`
- 短链接：`https://b23.tv/xxxxxx`

## 依赖安装

```bash
pip install faster-whisper yt-dlp
```

ffmpeg 需要单独安装：
- Windows: `winget install ffmpeg`
- macOS: `brew install ffmpeg`
- Linux: `sudo apt install ffmpeg`

云端转写还需要：`pip install dashscope requests`

## API Key 配置（云端转写）

本技能支持两种转录方式，**无 API Key 也能用**——会自动回退到本地 faster-whisper 转录。如果你想使用速度更快、方言更准的云端转写（阿里云 Paraformer），需要配置 API Key：

**1. 申请 Key**

前往 [阿里云百炼控制台](https://bailian.console.aliyun.com/) 注册并创建 API Key（即 DashScope Key），新用户有免费额度。

**2. 设置环境变量**

Windows (PowerShell)：
```powershell
[System.Environment]::SetEnvironmentVariable("DASHSCOPE_API_KEY", "sk-你的key", "User")
```

Windows (CMD)：
```cmd
setx DASHSCOPE_API_KEY "sk-你的key"
```

macOS / Linux：
```bash
echo 'export DASHSCOPE_API_KEY=sk-你的key' >> ~/.bashrc
source ~/.bashrc
```

**3. 优先级**

技能会按以下顺序查找 Key：`DASHSCOPE_API_KEY` → `OPENAI_API_KEY`（兼容百炼的 OpenAI 兼容接口）。两个都没设置时，自动使用本地 faster-whisper，无需联网。

## 处理流程

```
输入URL → 解析BV号 → 获取视频信息 → 检查官方字幕
                                          ├─ 有字幕 → 直接使用 → 生成摘要
                                          └─ 无字幕 → 下载音频 → whisper转录 → 生成摘要
```

## 模型选择参考

| 模型 | 速度 | 精度 | 内存占用 | 适用场景 |
|------|------|------|----------|----------|
| tiny | 最快 | 一般 | ~400MB | 短视频、快速预览 |
| base | 快 | 较好 | ~600MB | 中等时长、日常使用 |
| small | 中等 | 好 | ~1GB | 长视频、技术内容（默认） |
| medium | 较慢 | 很好 | ~1.5GB | 专业内容、多语言混合 |
| large | 最慢 | 最佳 | ~2.5GB | 最高质量需求 |

## 许可证

MIT License


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