关于 agency-docs-updater
Agency Docs Updater 是自动处理会议录像并发布相关视频与文档更新的技能。它依据配置的项目路径和依赖,协调转录稿处理、视频准备与上传、元数据、缩略图及会议页面更新。
主要功能
- 01查找转录稿与会议布局
- 02下载 Zoom 或 Fathom 录像
- 03剪辑并准备视频
- 04上传并验证 YouTube 视频
- 05生成摘要并更新元数据
适用场景
- 01发布实验室会议录像
- 02准备会议视频与缩略图
- 03生成基于转录稿的会议摘要
- 04更新 YouTube 信息和会议文档
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ glebis/ claude-skills --skill agency-docs-updater安装后在 Claude Code 中输入/agency-docs-updater确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.env.examplelabs.json.claude-plugin/plugin.jsonreferences/learnings.mdthumbnail-guide.mdworkflow-conversion-analysis.mdworkflows.mdyoutube-api.mdscripts/cut_spans.pydetect_lesson_start.pylab_layout.pypoll-and-publish.shpreflight.shrebuild_aggregations.pysafe_build.shtrim_leading_silence.shupdate_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_NUMBERVIDEO_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 完成整个工作流:
- 生成 Nano Banana 覆盖图(使用指南中提示词模式里的主题专属提示词)
- 读取/检查原始图像以确认背景颜色,然后将线条重新着色为橙色(#e85d04)
- 基于
${YOUTUBE_UPLOADER_DIR}/templates/images/lab-meeting.html编写一个临时 HTML 文件(例如/tmp/lab-meeting-${MEETING_NUMBER}.html)——更新会议编号、主题主视觉文字、项目符号说明和日期。不要直接修改原模板。 - 使用 Playwright 以 1280×720 渲染 →
${YOUTUBE_UPLOADER_DIR}/processed/thumbnails/${VIDEO_NAME}.jpg - 读取/检查渲染后的缩略图,在上传前确认布局
- 上传到 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 指定目标文件。
运行后:
- 移除附加的 Marp 内容(摘要结尾的
---之后、之前的所有内容)——MDX 遇到 HTML 注释()、未转义的<和裸{字符时会报错 - 检查是否存在演示文件:在
${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 中添加链接 - 替换 frontmatter 占位符(
[Название встречи]、[Краткое описание встречи]、[Дата встречи]) - 如果
TRANSCRIPT_LANG=en,请用英文标签彻底重写 MDX——脚本默认使用俄语,翻译回退机制会生成混杂语言的错误内容 - 验证:运行
bash ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/safe_build.sh(封装npm run build;如果出现reading 'hash'/ ENOSPC 错误,会自动清除损坏的.next缓存并重试一次) - 搜索生成的 MDX 中的操作员/流水线备注(录制来源、编辑/剪辑详情、编码、转录同步、上传方式),并在发布前将其移除
- 在 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),按需运行,与此单场会议流水线相互独立。