六个 transcript 脚本串成一条流水线:微软生成式 AI 入门课第 8 课

2026-08-18

第 8 课的 README 里有一句很容易被略过的话:Embedding 索引已经给你准备好了,你不需要跑那些脚本就能完成这一课。于是绝大多数人做完 notebook 就走了。

但只要你动了念头——把公司的会议录像、把自己那套课程视频也做成一个能按语义搜到「第几分几秒」的检索,你就绕不开 08-building-search-applications/scripts/ 下面那六个 transcript_*.py。这六个脚本不是六个独立工具,是一条有严格先后顺序的流水线:后一个脚本读的正是前一个脚本写出来的那个文件,中间那份 JSON 每过一道就多长出几个字段。看不懂这条链,你换数据源的时候就会在某一步卡住却不知道是哪一步没落盘。

这篇就沿着仓库里的编排脚本,把这条链走一遍。

入口不是 Python,是那三个编排脚本

顺序不写在任何一个 .py 里,写在编排脚本的注释和调用行里。同一份逻辑仓库给了三个平台版本:prepare_transcripts_ai_show.ps1prepare_transcripts_ai_show.shprepare_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 另一个仓库、cdsrc/data_prep文档里的命令名和目录里的文件名对不上,README 里写的那两个文件名在这个目录下并不存在——以目录里实际有的文件名为准。

Windows 侧还有两处细节值得单独说:README 建议把 AZURE_OPENAI_API_KEYAZURE_OPENAI_ENDPOINTAZURE_OPENAI_MODEL_DEPLOYMENT_NAMEGOOGLE_DEVELOPER_API_KEY 加到用户环境变量Windows Start > Edit the system environment variables),虚拟环境激活写的是 .venv\Scripts\activate;Linux/macOS 侧则是写进 ~/.bashrc~/.zshrc,激活用 source .venv/bin/activate。另外三个编排脚本都在跑 Python 之前先建好了 output 子目录,而后面写文件的脚本自己并不建目录——所以别绕过编排脚本单独手跑中间某一步。

一份数据,六站流转

先把整条链摊平。下面这张表是这篇的骨架,后面每一节都是在解释表里的某一行:

顺序脚本读什么写什么
1transcript_download.pyYouTube 播放列表每个视频两个文件:<videoId>.json<videoId>.json.vtt
2transcript_enrich_speaker.py<videoId>.json 与对应 .vtt 开头一段回写同一个 <videoId>.json
3transcript_enrich_bucket.py目录下所有 *.json 及其 .vttoutput/master_transcriptions.json
4transcript_enrich_summaries.pyoutput/master_transcriptions.jsonoutput/master_enriched.json
5transcript_enrich_embeddings.pyoutput/master_enriched.jsonoutput/master_enriched.json(同名覆写)
6transcript_enrich_lite.pyoutput/master_enriched.jsonoutput/master_enriched_lite.json

跑完之后编排脚本再做一次改名:master_enriched.json 改成 embedding_index_full_<分钟数>m.jsonmaster_enriched_lite.json 改成 embedding_index_<分钟数>m.json课程目录里那份 embedding_index_3m.json,就是第 6 步的产物改名而来。

注意第 5 行:embeddings 那一步读写的是同一个路径。这不是笔误,代码里 input_fileoutput_file 拼出来的都是 master_enriched.json

第 1 站:下载,一个视频落两个文件

transcript_download.pygoogleapiclient 分页拉播放列表,把每个 item 塞进 queue.Queue,再开一批线程消费。每个视频落两份东西:

  • gen_metadata(playlist_item)<videoId>.json,字段只有四个:speaker(先置空字符串)、titlevideoIddescription
  • 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()(去换行、还原 &#39;、去掉 >>[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 下的键是 speakersrequired 里写的却是 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(原始秒数)。同一个视频的每一行都会重复携带 videoIdtitlespeakerdescription

最后两处小观察:这个脚本的模块 docstring 写的是「generate a master csv file」,实际写出的是 JSON;文件里定义的 gen_metadata_master() 在整个 08-building-search-applications/ 目录下没有调用点。

第 4、5、6 站:摘要、向量、瘦身

transcript_enrich_summaries.pymaster_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-minitemperature 会拿到参数不支持的错误。而这两个脚本里模型部署名的默认值正是 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
    ]

去掉 textdescription,只留 speakertitlevideoIdstartsecondssummaryada_v2。改名之后就是课程里那份 embedding_index_3m.json——检索阶段其实用不到原文:靠 summary 展示、靠 ada_v2 算相似度、靠 videoIdseconds 拼出跳转链接就够了。

以上片段均原样摘自仓库文件,我们没有跑过,以仓库最新代码为准。

回头看下游,链条才算闭合

python/oai-solution.ipynbload_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 这个键名、startseconds 这对时间字段、以及 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_ENDPOINTAZURE_INFERENCE_CREDENTIAL)。所以 githubmodels- 这个示例前缀和它实际接入的目标早已脱节,更别想着把那套变量直接搬到这几个 transcript_*.py 里——它们读的是 AZURE_OPENAI_*

最后提醒一句:第 2、4、5 站都会把你的文本发到云端 API,脚本里也确实带了并发线程与重试。密钥和数据边界请自己评估,别拿不能外发的内容直接跑这条链。


本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与生成质量的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。 安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。