六个 transcript 脚本串成一条流水线:微软生成式 AI 入门课第 8 课
第 8 课的 README 里有一句很容易被略过的话:Embedding 索引已经给你准备好了,你不需要跑那些脚本就能完成这一课。于是绝大多数人做完 notebook 就走了。
但只要你动了念头——把公司的会议录像、把自己那套课程视频也做成一个能按语义搜到「第几分几秒」的检索,你就绕不开 08-building-search-applications/scripts/ 下面那六个 transcript_*.py。这六个脚本不是六个独立工具,是一条有严格先后顺序的流水线:后一个脚本读的正是前一个脚本写出来的那个文件,中间那份 JSON 每过一道就多长出几个字段。看不懂这条链,你换数据源的时候就会在某一步卡住却不知道是哪一步没落盘。
这篇就沿着仓库里的编排脚本,把这条链走一遍。
入口不是 Python,是那三个编排脚本
顺序不写在任何一个 .py 里,写在编排脚本的注释和调用行里。同一份逻辑仓库给了三个平台版本:prepare_transcripts_ai_show.ps1、prepare_transcripts_ai_show.sh、prepare_transcripts_ai_show.bat。PowerShell 那版的调用段原样是这样:
python transcript_download.py -f $TRANSCRIPT_FOLDER -p "PLlrxD0HtieHi0mwteKBOfEeOYf0LJU4O1"
python transcript_enrich_speaker.py -f $TRANSCRIPT_FOLDER
python transcript_enrich_bucket.py -f $TRANSCRIPT_FOLDER -m $TRANSCRIPT_BUCKET_MINUTES
python transcript_enrich_summaries.py -f $TRANSCRIPT_FOLDER
python transcript_enrich_embeddings.py -f $TRANSCRIPT_FOLDER
python transcript_enrich_lite.py -f $TRANSCRIPT_FOLDER
这里先记一个坑:scripts/README.md 的「Run the YouTube transcription data prep scripts」一节写的是 .\transcripts_prepare.ps1 和 ./transcripts_prepare.sh,而目录里实际存在的文件叫 prepare_transcripts_ai_show.*;同一份 README 前面还让你去 clone 另一个仓库、cd 到 src/data_prep。文档里的命令名和目录里的文件名对不上,README 里写的那两个文件名在这个目录下并不存在——以目录里实际有的文件名为准。
Windows 侧还有两处细节值得单独说:README 建议把 AZURE_OPENAI_API_KEY、AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_MODEL_DEPLOYMENT_NAME、GOOGLE_DEVELOPER_API_KEY 加到用户环境变量(Windows Start > Edit the system environment variables),虚拟环境激活写的是 .venv\Scripts\activate;Linux/macOS 侧则是写进 ~/.bashrc 或 ~/.zshrc,激活用 source .venv/bin/activate。另外三个编排脚本都在跑 Python 之前先建好了 output 子目录,而后面写文件的脚本自己并不建目录——所以别绕过编排脚本单独手跑中间某一步。
一份数据,六站流转
先把整条链摊平。下面这张表是这篇的骨架,后面每一节都是在解释表里的某一行:
| 顺序 | 脚本 | 读什么 | 写什么 |
|---|---|---|---|
| 1 | transcript_download.py | YouTube 播放列表 | 每个视频两个文件:<videoId>.json、<videoId>.json.vtt |
| 2 | transcript_enrich_speaker.py | <videoId>.json 与对应 .vtt 开头一段 | 回写同一个 <videoId>.json |
| 3 | transcript_enrich_bucket.py | 目录下所有 *.json 及其 .vtt | output/master_transcriptions.json |
| 4 | transcript_enrich_summaries.py | output/master_transcriptions.json | output/master_enriched.json |
| 5 | transcript_enrich_embeddings.py | output/master_enriched.json | output/master_enriched.json(同名覆写) |
| 6 | transcript_enrich_lite.py | output/master_enriched.json | output/master_enriched_lite.json |
跑完之后编排脚本再做一次改名:master_enriched.json 改成 embedding_index_full_<分钟数>m.json,master_enriched_lite.json 改成 embedding_index_<分钟数>m.json。课程目录里那份 embedding_index_3m.json,就是第 6 步的产物改名而来。
注意第 5 行:embeddings 那一步读写的是同一个路径。这不是笔误,代码里 input_file 与 output_file 拼出来的都是 master_enriched.json。
第 1 站:下载,一个视频落两个文件
transcript_download.py 用 googleapiclient 分页拉播放列表,把每个 item 塞进 queue.Queue,再开一批线程消费。每个视频落两份东西:
gen_metadata(playlist_item)写<videoId>.json,字段只有四个:speaker(先置空字符串)、title、videoId、description。get_transcript(playlist_item, counter_id)写<videoId>.json.vtt,内容是YouTubeTranscriptApi.get_transcript(video_id)的返回,逐条把\n换成空格后 dump 成 JSON。
两个命名细节别看漏:字幕文件后缀是 .json.vtt 而不是 .vtt,所以后面用 *.json 通配符扫目录时只会扫到元数据文件、不会扫到字幕文件——这条链的多处 glob 都依赖这个命名。另外 get_transcript 里有一句「文件已存在就跳过」,而 gen_metadata 只在 get_transcript 返回 True 时才被调用。
还有一处工程习惯上的差别:这个脚本用 os.environ["GOOGLE_DEVELOPER_API_KEY"] 直接取键,缺变量时是 KeyError;而后面几个脚本取模型部署名用的是 os.getenv(..., 默认值),缺了会静默走默认。
第 2 站:抽讲者,用工具调用回填一个字段
transcript_enrich_speaker.py 干的事很窄——把 <videoId>.json 里那个空的 speaker 填上,原地覆写同一个文件,不产生新文件。
素材是 'The title is: ' + metadata['title'] + " " + metadata["description"] + " " + get_first_segment(filename),其中 get_first_segment() 读 .vtt,按 SEGMENT_MIN_LENGTH_MINUTES 截取开头一段并做 clean_text()(去换行、还原 '、去掉 >> 与 [inaudible])。
抽取走的是工具调用。函数定义原样是:
get_speaker_name = {
"name": "get_speaker_name",
"description": "Get the speaker names for the session.",
"parameters": {
"type": "object",
"properties": {
"speakers": {
"type": "string",
"description": "The speaker names.",
}
},
"required": ["speaker_name"],
},
}
同一段里有个对不上的地方:properties 下的键是 speakers,required 里写的却是 speaker_name;而下游取值用的是 arguments.get("speakers", "")。两处摆在一起看,required 那个名字和实际被读取的键并不是同一个。
调用侧用的是 Responses 接口,代码上方那行注释写明「Responses API uses a flat tool format」,所以工具是 [{"type": "function", **get_speaker_name}] 这样平铺出来的,并用 tool_choice={"type": "function", "name": "get_speaker_name"} 强制走这个函数,同时带了 store=False。
一个跨脚本的耦合点:SEGMENT_MIN_LENGTH_MINUTES 是这个脚本内部的模块常量(当前值是 3,属于仓库当前代码里的取值,随版本可能变动),和 transcript_enrich_bucket.py 那个 -m 参数是两回事。你把编排脚本里的分桶时长改成别的值,抽讲者这一步截取的仍然是这个常量对应的开头一段。
第 3 站:分桶,数据形态在这里变了
前两站的数据是「每个视频一份」,transcript_enrich_bucket.py 之后变成「一个扁平的 segment 列表」——这是整条链形态变化最大的一步,也是最值得读源码的一步。
切分是双重边界:既看时间,也看 token。
if current_seconds < seg_finish_seconds and total_tokens < MAX_TOKENS:
时间边界由 -m 传进来(编排脚本传的是 3,脚本里 SEGMENT_LENGTH_MINUTES 的兜底值是 5);token 边界是 MAX_TOKENS = 2048,旁边的注释写明留出余量是为了下一步的摘要请求。这些都是仓库当前代码里的默认值,随版本可能变动。计数用的是 tiktoken.encoding_for_model("gpt-4o-mini")。
重叠的实现方向和直觉相反:
def append_text_to_previous_segment(text):
if len(segments) > 0:
words = text.split(" ")
word_count = len(words)
if word_count > 0:
append_text = " ".join(words[0 : int(word_count * PERCENTAGE_OVERLAP)])
segments[-1]["text"] += append_text
它取的是新一段开头的一小截,贴到已经存进列表的上一段末尾。所以重叠是往回长的。这里再记一处文档与代码的差异:第 8 课 README 说重叠约二十个词,而代码里是按 PERCENTAGE_OVERLAP(当前值 0.05)算的比例,段落长短不同则重叠出来的长度也不同。
还有一个只有读代码才看得到的结构:讲者名、标题、描述这三样是在进循环之前拼进 text 的("The speaker's name is " + ...),而每次切段之后 text 被重置为 text = current_text + " "。也就是说,每个视频只有第一段的正文里带着这段元信息前缀,后面各段的 text 是纯字幕。你要是打算改写这套脚本、让每段都带上下文,得自己在 add_new_segment() 附近动手。
add_new_segment() 把 metadata.copy() 塞进列表,并补上两个时间字段:start(%H:%M:%S 格式的字符串)和 seconds(原始秒数)。同一个视频的每一行都会重复携带 videoId、title、speaker、description。
最后两处小观察:这个脚本的模块 docstring 写的是「generate a master csv file」,实际写出的是 JSON;文件里定义的 gen_metadata_master() 在整个 08-building-search-applications/ 目录下没有调用点。
第 4、5、6 站:摘要、向量、瘦身
transcript_enrich_summaries.py 读 master_transcriptions.json,给每个 segment 加一个 summary 字段,写出 master_enriched.json。system 提示词原样是「You’re an AI Assistant for video, write an authoritative 60 word summary.Avoid starting sentences with ‘This video’.」调用同样走 client.responses.create(...),传的是 max_output_tokens 而不是 max_tokens,并用 response.status 是否等于 "completed" 来判断是否被截断。
这个写法和课程别处是对得上的:06-text-generation-apps/README.md 明确写着,Foundry 上当前未废弃的是 reasoning 模型(GPT-5 家族、o 系列),不支持 temperature / top_p,也不支持 max_tokens(改用 max_output_tokens),给 gpt-5-mini 传 temperature 会拿到参数不支持的错误。而这两个脚本里模型部署名的默认值正是 gpt-5-mini(同样是仓库当前代码里的默认值,随版本可能变动),全程没有出现 temperature。你如果照着老教程往这里补采样参数,就撞上了这条。
transcript_enrich_embeddings.py 是第 5 站,给每个 segment 加 ada_v2 字段(取的是 response.data[0].embedding)。这一步有三处和上一步不一样,值得留意:
- 客户端类换了。摘要那步用的是
OpenAI(api_key=..., base_url=f"{RESOURCE_ENDPOINT.rstrip('/')}/openai/v1/"),这一步用的是AzureOpenAI(api_key=..., azure_endpoint=..., api_version="2024-10-21")。同一条流水线里两种客户端写法并存。 - tokenizer 换了。分桶那步用
encoding_for_model("gpt-4o-mini"),这一步用get_encoding("cl100k_base"),并对超长文本直接continue跳过。 - 部署名来自
AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT,而scripts/README.md的环境变量清单里没有列这一条,只列了对话模型那个。它在代码里有默认值text-embedding-ada-002(仓库当前代码里的默认值,随版本可能变动),所以缺变量时不会报错、会直接按默认部署名去调。
顺带一提,errors 这个计数器在 speaker 与 summaries 两个脚本里都只被声明和判断,文件里没有任何地方给它加过一。
第 6 站 transcript_enrich_lite.py 最短,核心就一个字典推导:
def remove_text(video_segments):
"""This function removes the text from each dictionary in the list."""
return [
{k: v for k, v in seg.items() if k != "text" and k != "description"}
for seg in video_segments
]
去掉 text 与 description,只留 speaker、title、videoId、start、seconds、summary、ada_v2。改名之后就是课程里那份 embedding_index_3m.json——检索阶段其实用不到原文:靠 summary 展示、靠 ada_v2 算相似度、靠 videoId 加 seconds 拼出跳转链接就够了。
以上片段均原样摘自仓库文件,我们没有跑过,以仓库最新代码为准。
回头看下游,链条才算闭合
python/oai-solution.ipynb 里 load_dataset() 做的是 pd.read_json(source) 之后 .drop(columns=["text"], errors="ignore")——那个 errors="ignore" 正是为了兼容两种索引:full 版有 text 列,lite 版没有。display_results() 用 row["videoId"] 和 row["seconds"] 拼 https://youtu.be/{video_id}?t={seconds},这就解释了第 3 站为什么要专门存一个数值型的 seconds,而不是只留 00:00:00 那个字符串。
所以要换成自己的数据,真正被绑死的是这几处:字幕文件的 .json.vtt 命名、videoId 这个键名、start 与 seconds 这对时间字段、以及 ada_v2 这个向量字段名。第 1 站可以整个换掉(它只和 YouTube 有关),但产出的两个文件必须长成上面那个样子,后面五站才接得上。
还有一条容易踩的:这几个脚本的环境变量全是 AZURE_OPENAI_*,它们绑的是 Azure OpenAI 这条路线。课程里另外两条路线在 00-course-setup/03-providers.md 里说明,其中第三条是 Microsoft Foundry Models(原 GitHub Models 路线)——该文件写明 GitHub Models 于 2026 年 7 月底退役、Microsoft Foundry Models 是它的直接替代者(按今天的日历,这个时点已经过去),要配的是 Foundry 的 endpoint 与 key(AZURE_INFERENCE_ENDPOINT 与 AZURE_INFERENCE_CREDENTIAL)。所以 githubmodels- 这个示例前缀和它实际接入的目标早已脱节,更别想着把那套变量直接搬到这几个 transcript_*.py 里——它们读的是 AZURE_OPENAI_*。
最后提醒一句:第 2、4、5 站都会把你的文本发到云端 API,脚本里也确实带了并发线程与重试。密钥和数据边界请自己评估,别拿不能外发的内容直接跑这条链。
本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与生成质量的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。