feat: 新增小宇宙播客转文字渠道 (#124)
* feat: add Xiaoyuzhou podcast transcription channel - New channel: 小宇宙播客 (xiaoyuzhoufm.com) → full text transcript - Uses Groq Whisper API (free, no credit card needed) - Auto-downloads audio, converts to low-bitrate MP3, splits by 25MB limit - Supports any length podcast with automatic chunking - Script installed to ~/.agent-reach/tools/xiaoyuzhou/transcribe.sh - User just needs: agent-reach configure groq-key gsk_xxxxx - Updated README (CN/EN), install.md, pyproject.toml * docs: clarify Xiaoyuzhou setup — free key, limitations, step-by-step --------- Co-authored-by: Panniantong <panniantong@users.noreply.github.com>
This commit is contained in:
@@ -80,6 +80,7 @@ AI Agent 已经能帮你写代码、改文档、管项目——但你让它去
|
||||
| 🏢 **Boss直聘** | Jina Reader 读职位页 | 搜索职位、向 HR 打招呼 | 告诉 Agent「帮我配 Boss直聘」 |
|
||||
| 💬 **微信公众号** | 搜索 + 阅读公众号文章(全文 Markdown) | — | 无需配置 |
|
||||
| 📰 **微博** | 热搜、搜索内容/用户/话题、用户动态、评论 | — | 无需配置 |
|
||||
| 🎙️ **小宇宙播客** | — | 播客音频转文字(Whisper 转录,免费 Key) | 告诉 Agent「帮我配小宇宙播客」 |
|
||||
|
||||
> **不知道怎么配?不用查文档。** 直接告诉 Agent「帮我配 XXX」,它知道需要什么、会一步一步引导你。
|
||||
>
|
||||
|
||||
@@ -21,6 +21,7 @@ from .linkedin import LinkedInChannel
|
||||
from .bosszhipin import BossZhipinChannel
|
||||
from .wechat import WeChatChannel
|
||||
from .weibo import WeiboChannel
|
||||
from .xiaoyuzhou import XiaoyuzhouChannel
|
||||
|
||||
|
||||
# Channel registry
|
||||
@@ -36,6 +37,7 @@ ALL_CHANNELS: List[Channel] = [
|
||||
BossZhipinChannel(),
|
||||
WeChatChannel(),
|
||||
WeiboChannel(),
|
||||
XiaoyuzhouChannel(),
|
||||
RSSChannel(),
|
||||
ExaSearchChannel(),
|
||||
WebChannel(),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Xiaoyuzhou Podcast (小宇宙播客) — transcribe podcasts via Groq Whisper API."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from .base import Channel
|
||||
|
||||
|
||||
class XiaoyuzhouChannel(Channel):
|
||||
name = "xiaoyuzhou"
|
||||
description = "小宇宙播客转文字"
|
||||
backends = ["groq-whisper", "ffmpeg"]
|
||||
tier = 1
|
||||
|
||||
def can_handle(self, url: str) -> bool:
|
||||
from urllib.parse import urlparse
|
||||
d = urlparse(url).netloc.lower()
|
||||
return "xiaoyuzhoufm.com" in d
|
||||
|
||||
def check(self, config=None):
|
||||
# Check ffmpeg
|
||||
if not shutil.which("ffmpeg"):
|
||||
return "off", (
|
||||
"需要 ffmpeg(音频转码和切片)。安装:\n"
|
||||
" Ubuntu/Debian: apt install -y ffmpeg\n"
|
||||
" macOS: brew install ffmpeg"
|
||||
)
|
||||
|
||||
# Check script exists
|
||||
script = os.path.expanduser("~/.agent-reach/tools/xiaoyuzhou/transcribe.sh")
|
||||
if not os.path.isfile(script):
|
||||
return "off", (
|
||||
"转录脚本未安装。运行:\n"
|
||||
" agent-reach install --env=auto\n"
|
||||
" 或手动复制 transcribe.sh 到 ~/.agent-reach/tools/xiaoyuzhou/"
|
||||
)
|
||||
|
||||
# Check GROQ_API_KEY
|
||||
if not os.environ.get("GROQ_API_KEY"):
|
||||
# Check if saved in config
|
||||
config_path = os.path.expanduser("~/.agent-reach/config.json")
|
||||
has_key = False
|
||||
if os.path.isfile(config_path):
|
||||
try:
|
||||
import json
|
||||
with open(config_path) as f:
|
||||
cfg = json.load(f)
|
||||
has_key = bool(cfg.get("groq_api_key"))
|
||||
except Exception:
|
||||
pass
|
||||
if not has_key:
|
||||
return "warn", (
|
||||
"需要配置 Groq API Key(免费)。步骤:\n"
|
||||
" 1. 注册 https://console.groq.com\n"
|
||||
" 2. 运行: agent-reach configure groq-api-key gsk_xxxxx"
|
||||
)
|
||||
|
||||
return "ok", "完整可用(播客下载 + Whisper 转录)"
|
||||
@@ -450,10 +450,60 @@ def _install_system_deps():
|
||||
# ── Weibo (mcp-server-weibo fork with visitor passport fix) ──
|
||||
_install_weibo_deps()
|
||||
|
||||
# ── Xiaoyuzhou Podcast (transcribe.sh + ffmpeg) ──
|
||||
_install_xiaoyuzhou_deps()
|
||||
|
||||
# ── WeChat Articles (miku_ai + camoufox + wechat-article-for-ai) ──
|
||||
_install_wechat_deps()
|
||||
|
||||
|
||||
def _install_xiaoyuzhou_deps():
|
||||
"""Install Xiaoyuzhou podcast transcription script."""
|
||||
print("Setting up Xiaoyuzhou podcast transcription...")
|
||||
|
||||
tools_dir = os.path.expanduser("~/.agent-reach/tools/xiaoyuzhou")
|
||||
script_dst = os.path.join(tools_dir, "transcribe.sh")
|
||||
|
||||
if os.path.isfile(script_dst):
|
||||
print(" ✅ Xiaoyuzhou transcription script already installed")
|
||||
else:
|
||||
# Copy script from package
|
||||
script_src = os.path.join(os.path.dirname(__file__), "scripts", "transcribe_xiaoyuzhou.sh")
|
||||
if os.path.isfile(script_src):
|
||||
try:
|
||||
os.makedirs(tools_dir, exist_ok=True)
|
||||
import shutil as _shutil
|
||||
_shutil.copy2(script_src, script_dst)
|
||||
os.chmod(script_dst, 0o755)
|
||||
print(" ✅ Xiaoyuzhou transcription script installed")
|
||||
except Exception as e:
|
||||
print(f" [!] Failed to install script: {e}")
|
||||
else:
|
||||
print(" [!] Script source not found in package")
|
||||
|
||||
# Check ffmpeg
|
||||
if shutil.which("ffmpeg"):
|
||||
print(" ✅ ffmpeg available")
|
||||
else:
|
||||
print(" -- ffmpeg not found. Install: apt install -y ffmpeg (or brew install ffmpeg)")
|
||||
|
||||
# Check GROQ_API_KEY
|
||||
config_path = os.path.expanduser("~/.agent-reach/config.json")
|
||||
has_key = bool(os.environ.get("GROQ_API_KEY"))
|
||||
if not has_key and os.path.isfile(config_path):
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
cfg = json.load(f)
|
||||
has_key = bool(cfg.get("groq_api_key"))
|
||||
except Exception:
|
||||
pass
|
||||
if has_key:
|
||||
print(" ✅ Groq API key configured")
|
||||
else:
|
||||
print(" -- Groq API key not set. Get free key at https://console.groq.com")
|
||||
print(" Then run: agent-reach configure groq-api-key gsk_xxxxx")
|
||||
|
||||
|
||||
def _install_weibo_deps():
|
||||
"""Install Weibo MCP server (Panniantong fork with visitor passport auth)."""
|
||||
import subprocess
|
||||
|
||||
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/bin/bash
|
||||
# 小宇宙播客转文字脚本
|
||||
# 用法: bash transcribe.sh <小宇宙链接> [输出文件路径]
|
||||
# 环境变量: GROQ_API_KEY (必须)
|
||||
|
||||
set -e
|
||||
|
||||
URL="${1:?用法: bash transcribe.sh <小宇宙链接> [输出文件路径]}"
|
||||
OUTPUT="${2:-/tmp/podcast_transcript.txt}"
|
||||
TMPDIR="/tmp/xiaoyuzhou_$$"
|
||||
|
||||
# Try env var first, then agent-reach config
|
||||
if [ -z "$GROQ_API_KEY" ]; then
|
||||
CONFIG_FILE="$HOME/.agent-reach/config.json"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
GROQ_API_KEY=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('groq_api_key',''))" 2>/dev/null || true)
|
||||
fi
|
||||
fi
|
||||
GROQ_API_KEY="${GROQ_API_KEY:?请设置 GROQ_API_KEY 环境变量或运行 agent-reach configure groq-key}"
|
||||
|
||||
# Groq API 限制: 25MB per file
|
||||
MAX_CHUNK_SIZE_MB=20
|
||||
AUDIO_BITRATE="64k"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMPDIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$TMPDIR"
|
||||
|
||||
echo "📻 小宇宙播客转文字"
|
||||
echo "===================="
|
||||
|
||||
# Step 1: 提取音频 URL 和标题
|
||||
echo "🔍 正在解析页面..."
|
||||
PAGE=$(curl -s "$URL")
|
||||
AUDIO_URL=$(echo "$PAGE" | grep -oP 'https://media\.xyzcdn\.net/[^"]*\.(m4a|mp3)' | head -1)
|
||||
TITLE=$(echo "$PAGE" | grep -oP '"title":"[^"]*"' | head -1 | sed 's/"title":"//;s/"//')
|
||||
|
||||
if [ -z "$AUDIO_URL" ]; then
|
||||
echo "❌ 无法从页面提取音频链接"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📝 标题: $TITLE"
|
||||
echo "🔗 音频: $AUDIO_URL"
|
||||
|
||||
# Step 2: 下载音频
|
||||
echo "⬇️ 正在下载音频..."
|
||||
EXT="${AUDIO_URL##*.}"
|
||||
curl -sL -o "$TMPDIR/original.$EXT" "$AUDIO_URL"
|
||||
FILE_SIZE=$(ls -lh "$TMPDIR/original.$EXT" | awk '{print $5}')
|
||||
echo "📦 文件大小: $FILE_SIZE"
|
||||
|
||||
# Step 3: 获取时长
|
||||
DURATION=$(ffprobe -v quiet -show_entries format=duration -of csv=p=0 "$TMPDIR/original.$EXT" 2>/dev/null | cut -d. -f1)
|
||||
DURATION_MIN=$((DURATION / 60))
|
||||
DURATION_SEC=$((DURATION % 60))
|
||||
echo "⏱️ 时长: ${DURATION_MIN}分${DURATION_SEC}秒"
|
||||
|
||||
# Step 4: 转为低码率单声道 MP3
|
||||
echo "🔄 正在转码..."
|
||||
ffmpeg -y -i "$TMPDIR/original.$EXT" -b:a "$AUDIO_BITRATE" -ac 1 "$TMPDIR/mono.mp3" 2>/dev/null
|
||||
MONO_SIZE=$(stat -c%s "$TMPDIR/mono.mp3" 2>/dev/null || stat -f%z "$TMPDIR/mono.mp3")
|
||||
echo "📦 转码后: $(echo "$MONO_SIZE / 1024 / 1024" | bc)MB"
|
||||
|
||||
# Step 5: 按大小切片
|
||||
MAX_BYTES=$((MAX_CHUNK_SIZE_MB * 1024 * 1024))
|
||||
|
||||
if [ "$MONO_SIZE" -le "$MAX_BYTES" ]; then
|
||||
# 不需要切片
|
||||
cp "$TMPDIR/mono.mp3" "$TMPDIR/chunk_0.mp3"
|
||||
NUM_CHUNKS=1
|
||||
echo "📎 无需切片"
|
||||
else
|
||||
# 计算需要几个 chunk
|
||||
NUM_CHUNKS=$(( (MONO_SIZE / MAX_BYTES) + 1 ))
|
||||
CHUNK_DURATION=$(( DURATION / NUM_CHUNKS + 10 )) # 加 10 秒缓冲
|
||||
echo "✂️ 切分为 $NUM_CHUNKS 段 (每段约 $((CHUNK_DURATION / 60)) 分钟)..."
|
||||
|
||||
for i in $(seq 0 $((NUM_CHUNKS - 1))); do
|
||||
START=$((i * CHUNK_DURATION))
|
||||
ffmpeg -y -i "$TMPDIR/mono.mp3" -ss "$START" -t "$CHUNK_DURATION" -c copy "$TMPDIR/chunk_${i}.mp3" 2>/dev/null
|
||||
CHUNK_SIZE=$(ls -lh "$TMPDIR/chunk_${i}.mp3" | awk '{print $5}')
|
||||
echo " 段 $((i+1))/$NUM_CHUNKS: $CHUNK_SIZE"
|
||||
done
|
||||
fi
|
||||
|
||||
# Step 6: 调用 Groq Whisper API 转录
|
||||
echo "🎙️ 正在转录 (Groq Whisper large-v3)..."
|
||||
|
||||
for i in $(seq 0 $((NUM_CHUNKS - 1))); do
|
||||
echo -n " 段 $((i+1))/$NUM_CHUNKS... "
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
||||
https://api.groq.com/openai/v1/audio/transcriptions \
|
||||
-H "Authorization: Bearer $GROQ_API_KEY" \
|
||||
-F file="@$TMPDIR/chunk_${i}.mp3" \
|
||||
-F model="whisper-large-v3" \
|
||||
-F language="zh" \
|
||||
-F response_format="text")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "❌ API 错误 (HTTP $HTTP_CODE)"
|
||||
echo "$BODY"
|
||||
|
||||
# 如果是速率限制,等待后重试
|
||||
if [ "$HTTP_CODE" = "429" ]; then
|
||||
# 从错误信息中提取等待时间,默认 120 秒
|
||||
WAIT_SEC=$(echo "$BODY" | grep -oP 'in \K[0-9]+m' | sed 's/m//' | head -1)
|
||||
WAIT_SEC=${WAIT_SEC:-2}
|
||||
WAIT_SEC=$((WAIT_SEC * 60 + 30))
|
||||
echo " ⏳ 速率限制,等待 ${WAIT_SEC} 秒后重试..."
|
||||
sleep "$WAIT_SEC"
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
||||
https://api.groq.com/openai/v1/audio/transcriptions \
|
||||
-H "Authorization: Bearer $GROQ_API_KEY" \
|
||||
-F file="@$TMPDIR/chunk_${i}.mp3" \
|
||||
-F model="whisper-large-v3" \
|
||||
-F language="zh" \
|
||||
-F response_format="text")
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo " ❌ 重试失败"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$BODY" > "$TMPDIR/transcript_${i}.txt"
|
||||
CHARS=$(wc -m < "$TMPDIR/transcript_${i}.txt")
|
||||
echo "✅ ($CHARS 字)"
|
||||
done
|
||||
|
||||
# Step 7: 合并输出
|
||||
echo "📄 正在合并文字稿..."
|
||||
|
||||
{
|
||||
echo "# $TITLE"
|
||||
echo ""
|
||||
echo "来源: $URL"
|
||||
echo "时长: ${DURATION_MIN}分${DURATION_SEC}秒"
|
||||
echo "转录时间: $(date '+%Y-%m-%d %H:%M')"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
|
||||
for i in $(seq 0 $((NUM_CHUNKS - 1))); do
|
||||
cat "$TMPDIR/transcript_${i}.txt"
|
||||
echo ""
|
||||
done
|
||||
} > "$OUTPUT"
|
||||
|
||||
TOTAL_CHARS=$(wc -m < "$OUTPUT")
|
||||
echo ""
|
||||
echo "✅ 完成!"
|
||||
echo "📄 输出: $OUTPUT"
|
||||
echo "📊 总字数: $TOTAL_CHARS"
|
||||
echo "===================="
|
||||
@@ -69,6 +69,7 @@ Update Agent Reach: https://raw.githubusercontent.com/Panniantong/agent-reach/ma
|
||||
| 🏢 **Boss直聘** | Jina Reader (job pages) | Job search, greet recruiters | Tell your Agent "help me set up Boss直聘" |
|
||||
| 💬 **WeChat Articles** | Search + Read | Zero config | Search + read WeChat Official Account articles (full Markdown) ([wechat-article-for-ai](https://github.com/bzd6661/wechat-article-for-ai) + [miku_ai](https://github.com/GobinFan/Miku_Spider)) |
|
||||
| 📰 **Weibo** | Trending · Search · Feeds · Comments | Zero config | Hot search, content/user/topic search, feeds, comments ([mcp-server-weibo](https://github.com/Panniantong/mcp-server-weibo)) |
|
||||
| 🎙️ **Xiaoyuzhou Podcast** | Transcription | Free API key | Podcast audio → full text transcript via Groq Whisper (free) |
|
||||
| 🔍 **Web Search** | Search | Auto-configured | Auto-configured during install, free, no API key ([Exa](https://exa.ai) via [mcporter](https://github.com/nicepkg/mcporter)) |
|
||||
| 📦 **GitHub** | Read · Search | Zero config | [gh CLI](https://cli.github.com) powered. Public repos work immediately. `gh auth login` unlocks Fork, Issue, PR |
|
||||
| 📺 **YouTube** | Read · **Search** | Zero config | Subtitles + search across 1800+ video sites ([yt-dlp](https://github.com/yt-dlp/yt-dlp) ⭐148K) |
|
||||
@@ -226,6 +227,7 @@ Each channel file only checks whether its upstream tool is installed and working
|
||||
| Boss直聘 | [mcp-bosszp](https://github.com/mucsbr/mcp-bosszp) | MCP server, job search + recruiter greeting |
|
||||
| WeChat Articles | [wechat-article-for-ai](https://github.com/bzd6661/wechat-article-for-ai) + [miku_ai](https://github.com/GobinFan/Miku_Spider) | Stealth browser for full article reading + Sogou search |
|
||||
| Weibo | `mcporter` | `mcporter call 'weibo.get_trendings(limit: 10)'` |
|
||||
| Xiaoyuzhou Podcast | `transcribe.sh` | `bash ~/.agent-reach/tools/xiaoyuzhou/transcribe.sh <URL>` |
|
||||
|
||||
> 📌 These are the *current* choices. Don't like one? Swap out the file. That's the whole point of scaffolding.
|
||||
|
||||
|
||||
@@ -174,6 +174,35 @@ mcporter config add weibo --command 'mcp-server-weibo'
|
||||
|
||||
> 无需登录、无需 Cookie、无需代理。海外服务器也可以直接访问。
|
||||
|
||||
**小宇宙播客 / Xiaoyuzhou Podcast (Groq Whisper):**
|
||||
> "小宇宙播客转文字已默认安装,只需要一个免费的 Groq API Key。"
|
||||
|
||||
脚本已随 Agent Reach 自动安装,用户只需提供 Key:
|
||||
|
||||
```bash
|
||||
agent-reach configure groq-key gsk_xxxxx
|
||||
```
|
||||
|
||||
> **获取 Groq API Key(免费、无需信用卡、30 秒搞定):**
|
||||
> 1. 打开 https://console.groq.com
|
||||
> 2. 用 Google/GitHub 账号登录(或注册)
|
||||
> 3. 左侧菜单 → API Keys → Create API Key
|
||||
> 4. 复制 Key(以 `gsk_` 开头),发给 Agent 即可
|
||||
>
|
||||
> **使用方式:**
|
||||
> 用户发一个小宇宙链接给 Agent,Agent 自动调用:
|
||||
> ```bash
|
||||
> bash ~/.agent-reach/tools/xiaoyuzhou/transcribe.sh https://www.xiaoyuzhoufm.com/episode/xxxxx
|
||||
> ```
|
||||
>
|
||||
> 自动下载音频 → 转码切片 → Groq Whisper 转录 → 输出完整中文文字稿。
|
||||
>
|
||||
> **免费额度和限制:**
|
||||
> - 每小时约 2 小时音频(7200 秒),超出后等 15 分钟自动恢复
|
||||
> - 日常听几期播客完全够用
|
||||
> - 转录质量高(Whisper large-v3),但不区分说话人
|
||||
> - 2 小时以上的播客建议分批处理
|
||||
|
||||
**抖音 / Douyin (douyin-mcp-server):**
|
||||
> "抖音视频解析需要一个 MCP 服务。安装 douyin-mcp-server 后即可解析视频、获取无水印下载链接。"
|
||||
|
||||
@@ -308,6 +337,7 @@ If the user wants a different agent to handle it, let them choose.
|
||||
| `agent-reach check-update` | Check for new versions |
|
||||
| `agent-reach configure twitter-cookies "..."` | Unlock Twitter search + posting |
|
||||
| `agent-reach configure proxy URL` | Unlock Reddit + Bilibili on servers |
|
||||
| `agent-reach configure groq-key gsk_xxx` | Unlock Xiaoyuzhou podcast transcription |
|
||||
|
||||
After installation, use upstream tools directly. See SKILL.md for the full command reference:
|
||||
|
||||
@@ -322,6 +352,7 @@ After installation, use upstream tools directly. See SKILL.md for the full comma
|
||||
| Exa Search | `mcporter` | `mcporter call 'exa.web_search_exa(...)'` |
|
||||
| 小红书 | `mcporter` | `mcporter call 'xiaohongshu.search_feeds(...)'` |
|
||||
| 微博 | `mcporter` | `mcporter call 'weibo.get_trendings(limit: 10)'` |
|
||||
| 小宇宙播客 | `transcribe.sh` | `bash ~/.agent-reach/tools/xiaoyuzhou/transcribe.sh <URL>` |
|
||||
| 抖音 | `mcporter` | `mcporter call 'douyin.parse_douyin_video_info(...)'` |
|
||||
| LinkedIn | `mcporter` | `mcporter call 'linkedin.get_person_profile(...)'` |
|
||||
| Boss直聘 | `mcporter` | `mcporter call 'bosszhipin.search_jobs_tool(...)'` |
|
||||
|
||||
@@ -67,6 +67,7 @@ packages = ["agent_reach"]
|
||||
[tool.hatch.build.targets.wheel.force-include]
|
||||
"agent_reach/guides" = "agent_reach/guides"
|
||||
"agent_reach/skill" = "agent_reach/skill"
|
||||
"agent_reach/scripts" = "agent_reach/scripts"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
|
||||
Reference in New Issue
Block a user