mcpField
A
agency-docs-updater

agency-docs-updater

github.com/glebis创意与媒体40164 次安装1 天前更新
代码仓库

关于 agency-docs-updater

Agency Docs Updater 是自动处理会议录像并发布相关视频与文档更新的技能。它依据配置的项目路径和依赖,协调转录稿处理、视频准备与上传、元数据、缩略图及会议页面更新。

主要功能

  1. 01查找转录稿与会议布局
  2. 02下载 Zoom 或 Fathom 录像
  3. 03剪辑并准备视频
  4. 04上传并验证 YouTube 视频
  5. 05生成摘要并更新元数据

适用场景

  1. 01发布实验室会议录像
  2. 02准备会议视频与缩略图
  3. 03生成基于转录稿的会议摘要
  4. 04更新 YouTube 信息和会议文档

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/glebis/claude-skills --skill agency-docs-updater

安装后在 Claude Code 中输入/agency-docs-updater确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.env.example
labs.json
.claude-plugin/
plugin.json
references/
learnings.md
thumbnail-guide.md
workflow-conversion-analysis.md
workflows.md
youtube-api.md
scripts/
cut_spans.py
detect_lesson_start.py
lab_layout.py
poll-and-publish.sh
preflight.sh
rebuild_aggregations.py
safe_build.sh
trim_leading_silence.sh
update_meeting_doc.py

详细介绍

Agency 文档更新器

按顺序自动执行所有步骤。只有在某一步失败且无法恢复时才暂停。开始前先阅读 references/learnings.md,了解已知问题。

配置:路径从技能根目录中的 .env 读取(参见 .env.example)。默认值适用于标准设置。关键环境变量:VAULT_DIR、DOCS_SITE_DIR、YOUTUBE_UPLOADER_DIR、PRESENTATIONS_DIR、SKILLS_REPO_DIR、SKILLS_LOCAL_DIR、ZOOM_CREDENTIALS_DIR、GITHUB_REPO、SITE_DOMAIN。

依赖项(运行前确认这些依赖项存在):

  • zoom — Zoom 录制内容下载(scripts/zoom_meetings.py)
  • fathom — Fathom 视频备用方案(scripts/download_video.py)
  • nano-banana — 缩略图叠加层生成(scripts/generate_image.sh)
  • calendar-sync — 仅本地使用,日历事件同步(sync.sh)
  • youtube-uploader — 视频处理、上传和 YouTube API 身份验证

步骤 0:解析参数并加载配置

从技能根目录加载 .env。然后按空白字符拆分 args:

  • 8 位令牌(YYYYMMDD)→ DATE
  • "yesterday" → DATE = $(date -v-1d +%Y%m%d)
  • "today" 或缺省 → DATE = $(date +%Y%m%d)
  • 2 位令牌(NN)或 lab-NN → LAB_FILTER
  • slug 令牌(例如 ai-design、claude-code)→ LAB_SLUG(覆盖环境变量;默认值为 claude-code)

展开后续步骤中使用的路径环境变量:

VAULT_DIR="${VAULT_DIR:-$HOME/Brains/brain}"
DOCS_SITE_DIR="${DOCS_SITE_DIR:-$HOME/Sites/agency-docs}"
YOUTUBE_UPLOADER_DIR="${YOUTUBE_UPLOADER_DIR:-$HOME/ai_projects/youtube-uploader}"
SKILLS_REPO_DIR="${SKILLS_REPO_DIR:-$HOME/ai_projects/claude-skills}"
SKILLS_LOCAL_DIR="${SKILLS_LOCAL_DIR:-$HOME/.claude/skills}"
ZOOM_CREDENTIALS_DIR="${ZOOM_CREDENTIALS_DIR:-$HOME/.zoom_credentials}"
PRESENTATIONS_DIR="${PRESENTATIONS_DIR:-$HOME/ai_projects/claude-code-lab}"
GITHUB_REPO="${GITHUB_REPO:-glebis/agency-docs}"
SITE_DOMAIN="${SITE_DOMAIN:-agency-lab.glebkalinin.com}"
LAB_SLUG="${LAB_SLUG:-claude-code}"   # e.g. ai-design for the AI Design Lab
LAB_TITLE="${LAB_TITLE:-$(echo $LAB_SLUG | tr '-' ' ' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1' | sed 's/^Ai /AI /')}"  # "Claude Code", "AI Design"
步骤 0a:预检(推荐)

运行预检诊断工具,提前发现三种常见的流水线中途失败原因(缺少 youtube-uploader Python 依赖、缺少 Playwright/chromium、Groq 密钥失效):

bash ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/preflight.sh

阻断性问题(依赖项/Playwright)会以非零状态码退出,并给出确切的修复命令——运行该命令,然后重新运行预检。Groq 密钥失效属于软警告:大模型元数据步骤会返回 401,因此应准备手动提供标题/描述/标签(构建一个 VideoConfig 并直接调用 upload.py,然后分别设置缩略图和播放列表)。

步骤 1:查找 Fathom 转录文本

如果设置了 LAB_FILTER:${VAULT_DIR}/${DATE}-${LAB_SLUG}-lab-${LAB_FILTER}.md
如果为空:匹配 ${VAULT_DIR}/${DATE}-${LAB_SLUG}-lab-*.md(按 mtime 选择最新文件)。如果没有匹配项且未显式提供 slug,则回退到 ${VAULT_DIR}/${DATE}-*-lab-*.md,并根据匹配结果推导 LAB_SLUG。

如果文件缺失:运行 ${SKILLS_LOCAL_DIR}/calendar-sync/sync.sh,重新检查;如果仍缺失,则停止。

从 YAML frontmatter 中提取并保存:

  • FATHOM_FILE、SHARE_URL、MEETING_TITLE、DATE、LAB_NUMBER
  • VIDEO_NAME = ${DATE}-${LAB_SLUG}-lab-${LAB_NUMBER}
  • TRANSCRIPT_LANG = 根据开头约 50 行自动检测(西里尔字母比例 > 0.3 → ru,否则为 en)

首先解析 lab 布局——不同 lab 的路径、页面 URL 和播放列表名称各不相同。切勿手动构建;应通过注册表解析:

python3 ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/lab_layout.py ${LAB_SLUG} --lab ${LAB_NUMBER} --meeting ${MEETING_NUMBER} --json
# → meetings_dir (relative to DOCS_SITE_DIR), page_url, playlist, lang, thumbnail_style,
#   preserve_placeholder_frontmatter, registered

注册表是技能根目录中的 labs.json(claude-code 旧版布局、GDD RU goal-driven-design-ru、GDD EN)。update_meeting_doc.py 和 rebuild_aggregations.py 会自动通过注册表解析。未注册的 slug 会回退到旧版 {slug}-internal-{lab} 方案,并发出警告——请将新 lab 添加到 labs.json,不要自行编造路径。步骤 4b 使用 playlist(创建前按此准确名称搜索现有播放列表);步骤 4b/8 使用 page_url;摘要/MDX 语言使用 lang;并遵循 preserve_placeholder_frontmatter(GDD 占位文件包含经过整理的 toolkit: frontmatter——应合并,切勿覆盖)。

确定 MEETING_NUMBER:检查 ${DOCS_SITE_DIR}/content/docs/${LAB_SLUG}-internal-${LAB_NUMBER}/meetings/ 中现有的 MDX 文件,查找日期为今天的占位文件。如果找到,则使用该编号。否则,检查文件内容大小,找到下一个空位。保存为两位数、前导零的字符串(例如 04)。此变量将在步骤 3b、4b、5、6 和 8 中使用。

步骤 2:下载视频

如果 ${VAULT_DIR}/${VIDEO_NAME}.mp4 已存在且大于 1 MB,则跳过。

注意:Zoom 录制内容可能需要在会议结束后约 15 分钟才能处理完成。如果 Zoom API 未返回录制内容,请等待并重试,然后再改用 Fathom。

首选 — Zoom:

python3 ${SKILLS_REPO_DIR}/zoom/scripts/zoom_meetings.py recordings \
  --start ${DATE:0:4}-${DATE:4:2}-${DATE:6:2} \
  --end $(date -j -v+1d -f %Y%m%d ${DATE} +%Y-%m-%d) \
  --show-downloads 2>&1

找到 MP4 URL,然后:

TOK=$(python3 -c "import json,pathlib; print(json.load(open(pathlib.Path('${ZOOM_CREDENTIALS_DIR}')/'oauth_token.json'))['access_token'])")
curl -L -H "Authorization: Bearer ${TOK}" -o ${VAULT_DIR}/${VIDEO_NAME}.mp4 "${MP4_DOWNLOAD_URL}"

备用 — Fathom(如果没有 Zoom 录制内容):

cd ${VAULT_DIR} && python3 ${SKILLS_LOCAL_DIR}/fathom/scripts/download_video.py \
  "${SHARE_URL}" --output-name "${VIDEO_NAME}"

步骤 3:上传到 YouTube

步骤 3-pre:剪掉开头的静音

Zoom 自动录制会从会议开启时开始,开头通常会有几分钟的空白时间。上传前:

bash ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/trim_leading_silence.sh ${VAULT_DIR}/${VIDEO_NAME}.mp4
# If it prints "trim: wrote …trimmed.mp4", upload the trimmed file instead of the original.

脚本仅在文件开头的静音超过 10 秒时才会剪辑,保留 2 秒的前导片段,拒绝剪掉超过 20 分钟的内容,并执行流复制(不重新编码)。“no leading silence detected” → 使用原文件。

通过转录文本进行更智能的剪辑(如果有带时间戳的转录文本,则优先使用 — Fathom JSON 或 Zoom VTT;避免使用合并后的发布版 .md,其中的区块时间戳较粗略):

python3 ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/detect_lesson_start.py <fathom.json|zoom.vtt> --json
# → {"lesson_start_s": 21.0, "lesson_phrase": "всем привет", "presentation_open_s": 915.0, ...}

它会查找 (a) 课程开场语(«всем привет»、«добро пожаловать»、«давайте начинать»、“let's start”等)和 (b) 开始演示的时刻(«открою презентацию»、“share my screen”等)。使用方式如下:

  • 剪辑点:max(silence_end, lesson_start_s − 5) — 保留问候语,剪掉前面的空白时间。请与静音检测结果进行合理性核对;如果两者相差很大,请检查后再剪辑。
  • 描述中的 YouTube 章节:0:00 Начало / MM:SS Презентация(取自 presentation_open_s,减去剪辑偏移量)。

如果未找到任一短语,则退回到普通静音剪辑。

技术问题片段。 同一检测器会输出 tech_check_spans — 屏幕共享过程中的操作(«видно презентацию?», «меня слышно?», «перешарю», «одну секундочку»,且附近提及 презентация/экран)。这些只是候选片段:先阅读每个片段的 context 行;真正关于是否看得见的问答可以剪掉,讲解过程中随口说的 «секундочку» 则不应剪掉。要剪掉已确认的片段:

python3 ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/cut_spans.py video.mp4 \
  --remove 1245-1270 --remove 781-821        # seconds, from tech_check_spans

cut_spans.py 会重新编码(帧精度;对于 1080p 的对话视频,大约实时),合并并限制片段范围,并拒绝移除超过总时长 15% 的内容。剪辑会使每个片段之后的内容时间发生偏移 — 请在完成所有剪辑后计算 YouTube 章节时间戳。对于单个不超过 30 秒的小卡顿,可以考虑跳过剪辑:对 2 小时的视频进行完整重新编码可能不值得。

cd ${YOUTUBE_UPLOADER_DIR} && \
python3 process_video.py \
  --video ${VAULT_DIR}/${VIDEO_NAME}.mp4 \
  --fathom-transcript ${FATHOM_FILE} \
  --title "${MEETING_TITLE}" \
  --upload

使用 run_in_background: true 运行(10-30 分钟)。如果失败:使用 --resume-from upload。

从标准输出(✓ YouTube video: ...)或 processed/metadata/${VIDEO_NAME}.json 中提取 YOUTUBE_URL。 从 URL 中提取 VIDEO_ID(?v= 之后的部分或最后一个路径段)。

步骤 3a:验证上传(必需)

提取 VIDEO_ID 后,请在继续之前验证该视频确实存在于 YouTube 上。视频可能会在处理中悄无声息地失败,也可能被 YouTube 的内容审核自动删除。

cd ${YOUTUBE_UPLOADER_DIR} && PYTHONPATH=. python3 -c "
from auth import get_authenticated_service
import sys, time

youtube = get_authenticated_service()
video_id = '${VIDEO_ID}'

# Poll up to 5 minutes for video to become available
for attempt in range(10):
    resp = youtube.videos().list(part='status,processingDetails', id=video_id).execute()
    if not resp['items']:
        if attempt < 9:
            print(f'Video not yet available (attempt {attempt+1}/10), waiting 30s...')
            time.sleep(30)
            continue
        print(f'FATAL: Video {video_id} not found after 5 minutes. Upload may have failed.')
        sys.exit(1)

    status = resp['items'][0]['status']
    processing = resp['items'][0].get('processingDetails', {})
    upload_status = status.get('uploadStatus', 'unknown')
    privacy = status.get('privacyStatus', 'unknown')
    rejection = status.get('rejectionReason', None)

    print(f'Upload status: {upload_status}, Privacy: {privacy}')
    if rejection:
        print(f'REJECTED: {rejection}')
        sys.exit(1)
    if upload_status in ('processed', 'uploaded'):
        print(f'✓ Video {video_id} verified OK')
        sys.exit(0)
    if upload_status == 'failed':
        print(f'FATAL: Upload failed — {status.get(\"failureReason\", \"unknown\")}')
        sys.exit(1)

    print(f'Status: {upload_status}, waiting 30s...')
    time.sleep(30)

print('FATAL: Video not ready after 5 minutes')
sys.exit(1)
"

如果验证失败:删除失败视频的元数据(rm processed/metadata/${VIDEO_NAME}.json),使用 --resume-from upload 重新上传,并再次验证。不要使用未经验证的 VIDEO_ID 继续 MDX 或缩略图步骤。

并行开始步骤 4 — 摘要不依赖 YouTube URL。

第 3b 步:实验室风格缩略图(必需)

始终执行此步骤——它会用品牌实验室模板替换 process_video.py 生成的通用缩略图。通用缩略图不符合发布要求。

前提条件:必须已知 VIDEO_ID(如有需要,请等待第 3 步完成)。

按照 references/thumbnail-guide.md 完成整个工作流:

  1. 生成 Nano Banana 覆盖图(使用指南中提示词模式里的主题专属提示词)
  2. 读取/检查原始图像以确认背景颜色,然后将线条重新着色为橙色(#e85d04)
  3. 基于 ${YOUTUBE_UPLOADER_DIR}/templates/images/lab-meeting.html 编写一个临时 HTML 文件(例如 /tmp/lab-meeting-${MEETING_NUMBER}.html)——更新会议编号、主题主视觉文字、项目符号说明和日期。不要直接修改原模板。
  4. 使用 Playwright 以 1280×720 渲染 → ${YOUTUBE_UPLOADER_DIR}/processed/thumbnails/${VIDEO_NAME}.jpg
  5. 读取/检查渲染后的缩略图,在上传前确认布局
  6. 上传到 YouTube:使用从第 3 步提取的 VIDEO_ID

不要跳过此步骤,也不要依赖 process_video.py 生成的缩略图。

第 4 步:生成经事实核查的摘要

读取 ${FATHOM_FILE}。使用 ${TRANSCRIPT_LANG} 生成结构化摘要:

  • 使用 ## 级标题、项目符号,并在适当时添加代码示例
  • 技术术语使用英文(MCP、Skills、Claude Code 等)
  • 排除个人日程安排详情
  • 排除操作员/流水线备注:录制来源、剪辑或裁切、静音/片头移除、重新封装或重新编码、转录文本同步、上传重试及其他制作操作,只能写入内部流水线报告——绝不能出现在已发布的摘要或会议页面中
  • 发布前,根据权威项目背景或用户明确更正,核实产品、公司和人名;不要相信 ASR/LLM 对专有名词的规范化结果
  • 对 MDX 进行处理:转义会导致 MDX 编译失败的 <、> 和裸 { 字符

如果可以使用 claude-code-guide 子 Agent,请用它核查 Claude Code 功能相关说法;如果无法使用该 Agent,则跳过事实核查。将更正后的摘要保存到暂存区,文件名为 summary.md。

第 4b 步:更新 YouTube 元数据

在第 3 步和第 4 步都完成后执行。 执行此步骤前,必须确定 VIDEO_ID、MEETING_NUMBER 和 LAB_NUMBER。阅读 references/youtube-api.md,了解说明格式和 API 代码片段。

根据摘要生成 YouTube 说明。使用与语言相符的模板:

  • 如果 TRANSCRIPT_LANG=en:使用英文标签("In this video:", "Course materials and session notes:")
  • 如果 TRANSCRIPT_LANG=ru:使用俄文标签("В этом видео:", "Материалы и конспект занятия:")

请勿在同一条说明中混用语言。

会议页面 URL:https://${SITE_DOMAIN}/${LAB_SLUG}-lab-${LAB_NUMBER}/meetings/${MEETING_NUMBER}

通过 YouTube API 更新标题、说明和标签,然后将视频添加到播放列表 "${LAB_TITLE} Lab ${LAB_NUMBER}"(如果播放列表不存在,则自动创建)。

第 5 步:生成 MDX

LAB_SLUG=${LAB_SLUG} python3 ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/update_meeting_doc.py \
  ${FATHOM_FILE} "${YOUTUBE_URL}" ${SCRATCHPAD}/summary.md

运行前:检查是否已存在今天日期的占位 MDX(在 meetings/ 中使用 grep -l)。如果存在,请使用 -n ${MEETING_NUMBER} --update 指定目标文件。

运行后:

  1. 移除附加的 Marp 内容(摘要结尾的 --- 之后、 之前的所有内容)——MDX 遇到 HTML 注释()、未转义的 < 和裸 { 字符时会报错
  2. 检查是否存在演示文件:在 ${PRESENTATIONS_DIR}/presentations/lab-${LAB_NUMBER}/(根据实验室设置 PRESENTATIONS_DIR;ai-design 实验室将演示文稿存放在其他位置——如果该 slug 未设置此变量,则跳过)和 ${PRESENTATIONS_DIR}/lesson-generator/ 中查找匹配 ${DATE} 的文件。如果找到,请复制到 ${DOCS_SITE_DIR}/public/${DATE}-${LAB_SLUG}-lab-${LAB_NUMBER}.html,并在 MDX 中添加链接
  3. 替换 frontmatter 占位符([Название встречи]、[Краткое описание встречи]、[Дата встречи])
  4. 如果 TRANSCRIPT_LANG=en,请用英文标签彻底重写 MDX——脚本默认使用俄语,翻译回退机制会生成混杂语言的错误内容
  5. 验证:运行 bash ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/safe_build.sh(封装 npm run build;如果出现 reading 'hash' / ENOSPC 错误,会自动清除损坏的 .next 缓存并重试一次)
  6. 搜索生成的 MDX 中的操作员/流水线备注(录制来源、编辑/剪辑详情、编码、转录同步、上传方式),并在发布前将其移除
  7. 在 MDX、公开转录稿、YouTube 元数据和缩略图文案中搜索已更正专有名词的已知 ASR 变体;确保所有公开内容都使用规范拼写

第 6 步:提交并推送

只暂存流水线文件——切勿运行 git add .:

cd ${DOCS_SITE_DIR}
git fetch origin main
BEHIND=$(git rev-list --count HEAD..origin/main)
if [ "$BEHIND" -gt 0 ]; then
  git stash push -m "agency-docs-updater: temp stash"
  git pull --rebase origin main
  git stash pop || true
fi
git add content/docs/${LAB_SLUG}-internal-${LAB_NUMBER}/meetings/${MEETING_NUMBER}.mdx
# Only stage presentation HTML if it was copied
[ -f public/${DATE}-${LAB_SLUG}-lab-${LAB_NUMBER}.html ] && git add public/${DATE}-${LAB_SLUG}-lab-${LAB_NUMBER}.html
git commit -m "Add ${LAB_TITLE} Lab ${LAB_NUMBER} Meeting ${MEETING_NUMBER}"
git push

保存 COMMIT_HASH=$(git rev-parse HEAD),供第 7 步使用。

第 7 步:等待 Vercel 部署

TIMEOUT=300; ELAPSED=0
until [ "$(gh api repos/${GITHUB_REPO}/commits/${COMMIT_HASH}/status --jq '.state' 2>/dev/null || echo 'pending')" != "pending" ]; do
  sleep 15; ELAPSED=$((ELAPSED+15))
  [ "$ELAPSED" -ge "$TIMEOUT" ] && echo "Deploy timeout after ${TIMEOUT}s" && break
done
DEPLOY_STATE=$(gh api repos/${GITHUB_REPO}/commits/${COMMIT_HASH}/status --jq '.state')
echo "Deploy state: ${DEPLOY_STATE}"

使用 run_in_background: true 运行。如果状态为 failure 或 error:检查 Vercel 日志(vercel logs),在本地修复后重新推送,并重新启动此步骤。

第 8 步:在浏览器中验证

在浏览器中打开 https://${SITE_DOMAIN}/${LAB_SLUG}-lab-${LAB_NUMBER}/meetings/${MEETING_NUMBER}(通过 chrome 自动化工具或手动打开)。确认 YouTube 嵌入内容可见。如果不可见:检查 VIDEO_ID,等待 YouTube 处理完成,或重新上传。

第 9 步:重新生成全站聚合内容

新会议提交后(第 6 步),从所有会议重新生成三项全站聚合内容,确保包含新会议:数据库(会议索引)、术语表和全局链接库。

python3 ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/rebuild_aggregations.py

脚本读取相同的 .env 路径并写入以下文件(路径可通过 AGG_* 环境变量配置):

  • content/docs/database.mdx + public/data/meetings.json — 所有会议的索引
  • content/docs/glossary.mdx(定义保存在 .agency-glossary.json 中)
  • content/docs/library.mdx — 所有会议中的去重外部链接

处理新的术语表词条:对于从未见过的词条,脚本会输出 → N NEW term(s) need definitions。请为每个词条在 ${DOCS_SITE_DIR}/.agency-glossary.json 中添加一行定义(技术术语保留英文;其他内容应与页面语言一致),然后重新运行脚本,以便重新生成包含这些定义的术语表 MDX。不要更改已有定义——存储文件是唯一可信来源。

然后:运行 bash ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/safe_build.sh,确认生成的 MDX 可以编译(会自动从损坏的 .next 缓存中恢复);暂存更改过的聚合文件(三个 MDX 页面、public/data/meetings.json 和 .agency-glossary.json——切勿运行 git add .),然后提交:

git add content/docs/database.mdx content/docs/glossary.mdx content/docs/library.mdx \
        public/data/meetings.json .agency-glossary.json
git commit -m "Rebuild aggregations after Lab ${LAB_NUMBER} Meeting ${MEETING_NUMBER}"
git push

如果希望只推送一次,可以将此提交合并到第 6 步的提交中;无论采用哪种方式,都必须在重新运行第 7 步的部署等待之前完成推送。

流水线报告

完成后,请报告:Fathom 路径、视频路径、YouTube URL、MDX 路径、提交哈希、部署状态、嵌入验证结果,以及聚合内容重建情况(会议数量、新定义的术语表词条)。

相关内容:扇出式维护工作流

如需对所有过往会议执行整个仓库范围的任务,例如审查每个页面中失效的嵌入内容或 MDX 缺陷,或回填/修复不完整的会议,请参阅 references/workflows.md。这些是扇出式动态工作流(每场会议对应一个 Agent),按需运行,与此单场会议流水线相互独立。

评论

支持 Markdown