lecture-to-md · git:20260905.7dcad9c · 2026-09-05 · sha256 bbdf5ea7a5d369bc
lecture-to-md git:20260905.7dcad9cA
Immutable. This exact content is served forever at /api/v1/blob/bbdf5ea7a5d369bc.
---
name: lecture-to-md
description: 把课堂视频(本地或 B 站/YouTube)、文字稿、课件三者(任意组合)整理成一份详细的中文 Markdown 课堂笔记,输出按课程标题命名的 `{title_name}.md`(首行为 `# 文档标题`)+ 相对路径图片。Markdown 工作流,与上游 `lecture-to-notes` 的 LaTeX/PDF 输出并行存在;上游 skill 完全不动。触发词:markdown 笔记、md 笔记、视频转 markdown、Markdown 讲义、不要 LaTeX、不要 PDF、纯文本笔记。
---
# Lecture to Notes (Markdown)
把课堂视频(本地或 B 站/YouTube)、文字稿、课件三者(任意组合)整理成一份详细的中文 Markdown 课堂笔记,输出按课程标题命名的 `{title_name}.md`(首行为 `# 文档标题`)+ 相对路径图片。**作为上游 `lecture-to-notes` LaTeX/PDF 流的 Markdown 平行方案;本 skill 与上游 skill 完全独立,不修改也不依赖上游 skill 的脚本。**
> **子代理分工**:素材获取(下载/ASR/抽帧/课件转 PNG)委派 **S0** 子代理并行做;主代理通读文字稿定死一级标题结构后,切素材(文字稿切片 / 帧按时段分目录 / 课件标页码)委派 **S1** 子代理做;然后每个撰写子代理领 1–3 个 H1 并行撰写 → 主代理汇总 → **对照文字稿重排目录结构**(只改标题、不动正文)→ 交付。
> 切分与素材委派见 [`references/splitting.md`](references/splitting.md),派发与汇总见 [`references/subagent-workflow.md`](references/subagent-workflow.md),结构重排见 [`references/structure-reorder.md`](references/structure-reorder.md)。
## 与 LaTeX 版的区别
| | lecture-to-notes (LaTeX) | 本 skill (Markdown) |
| ----- | ------------------------ | --------------------- |
| 输出 | `.tex` + PDF | `{title_name}.md` + 图片 |
| 抽帧 | 每 15s(按章节) | 严格每 5s |
| 配图 | 全帧 + contact sheet 人工选 | LLM 多模态按 `###` 标题自动选图 |
| 配图优先级 | 视频帧 | 课件截图 > 视频帧 |
| 编译 | xelatex | 无需编译 |
## 子技能(asr 与云端转写)
本 skill 包内自带两个 ASR 子 skill,**默认走本地 ASR(见下「ASR 选型」一节)**:
| 子 skill 路径 | 何时用 |
| ---------------------------------------- | -------------------------------------------------------------------- |
| [`local-asr/`](../local-asr/SKILL.md) | **默认**。无字幕时全文转写 / 短片段核验。已在 **macOS Apple Silicon**、**Linux ARM64**(CPU)与 **Windows**(PowerShell 5.1,CPU)端到端跑通。 |
| [`volcengine-asr/`](../volcengine-asr/SKILL.md) | 备用。火山引擎豆包 BigASR / 豆包 2.0,需要 APP ID + Token;长音频首选异步模式 |
调用约定见各子 skill 的 SKILL.md,本 SKILL.md 不重复列参数。
## ASR 选型
- **首选 `local-asr/`**(sherpa-onnx X-ASR,跨平台路径齐备,int8 量化 + Apple Silicon AMX 矩阵加速 ~100× 实时);调用时默认带 `--timestamps`,以保留字幕时间轴。
- 若用户已自带文字稿(火山引擎豆包、飞书妙记等),**跳过 ASR**,直接走 Phase 1 的文字稿核验。
- 火山引擎走 `volcengine-asr/`,把产出的 `transcript.txt` 当作 S0-a 的产物继续。
## Dependencies
跨平台检查命令是否就位,缺则按平台提示用户安装(具体安装命令见各子 skill 的 setup 脚本):
| 平台 | 检查命令 | 备注 |
| ------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------- |
| macOS / Linux shell | `which <cmd>` 或 `command -v <cmd>` | 沙箱 PATH 可能不含 `/opt/homebrew/bin`(macOS)/ `/usr/local/bin`(Linux),见「已知坑」#1 |
| Windows PowerShell 5.1 / cmd | `where.exe <cmd>` | **`which` 不是 Windows 内建命令**,PowerShell 5.1 也没有该 alias;只有装了 Git for Windows / msys2 时才会有 `which.exe`(实测)。Windows 沙箱 PATH 可能不含 winget links / scoop shims / Chocolatey bin,见「已知坑」#1 |
最简做法:跑 `local-asr/scripts/common.py::require("<cmd>")`,它内部已经做了 `shutil.which` 兜底 + 按平台补齐常见 bin 目录(macOS `/opt/homebrew/bin`、Windows `winget links` / `%ProgramFiles%\ffmpeg\bin` / `~\scoop\shims` / `%ProgramData%\chocolatey\bin` 等),并返回**绝对路径**,避免子进程再吃 PATH 的亏。
| 工具 | 必需 | 用途 |
| ----------------------- | -- | ------------------------------------------------------------- |
| `yt-dlp` | ✓ | 视频/字幕/元数据下载(YouTube + Bilibili) |
| `ffmpeg` | ✓ | 5s 抽帧、音频提取(`ffprobe` 随 ffmpeg 一起装,Phase 4 判时长要用) |
| `python3` | ✓ | `clean_subs.py` |
| `pdftoppm` | △ | 课件 PDF → PNG |
| `soffice` (LibreOffice) | △ | 课件 PPT/PPTX → PDF |
| `local-asr/` | △ | 无字幕时全文转写 / 前 1min 核验;默认加 `--timestamps` 出 srt/vtt 时间戳 |
| `volcengine-asr/` | △ | 调用火山引擎豆包 ASR(需要凭据) |
| `sherpa-onnx` (pip 包) | △ | `local-asr/` 后端依赖,首次跑 `bash ../local-asr/scripts/setup.sh` 自动装 |
## 已知坑(务必遵守)
1. **PATH 兜底**:沙箱 PATH 可能丢以下常用 bin 目录——macOS `/opt/homebrew/bin`、`/usr/local/bin`、`/opt/local/bin`;Linux `/usr/local/bin`、`~/.local/bin`、`/snap/bin`;Windows `%LOCALAPPDATA%\Microsoft\WinGet\Links`、`%ProgramFiles%\ffmpeg\bin`、`~\scoop\shims`、`~\scoop\apps\<app>\current\bin`、`%ProgramData%\chocolatey\bin`。`yt-dlp`/`ffmpeg`/`pdftoppm`/`soffice` 一律用绝对路径,或先 `require()` 兜底(参考 `../local-asr/scripts/common.py::require`,Python 脚本首选;它内部已按平台列好候选目录)。
2. **帧序号 → 时间戳**:`verify_figures.py`(LaTeX 版脚本)硬编码 `×15`,本 skill 不复用该脚本。**不要硬编码 `帧时间 = (帧序号-1)×5`**——`fps=1/5` 的首帧可能不落在 0s、末尾帧可能被被丢、起始 PTS/掉帧都会让算术失真(实测 12s 片段只出 2 帧而非 3 帧)。帧时间一律由 ffmpeg 的 `showinfo` 记录真实 `pts_time`(见 Phase 1 的 S0-b)。
3. **ASR 时间戳选型**:`local-asr/` 默认 sherpa-onnx X-ASR 模型直接产出 token 级时间戳(transducer 模型自带)。S0-a 调用默认带 `--timestamps`(`transcribe.py --timestamps`,会写 srt/vtt);只有用户明确只要纯文本时才省略。若是已有稿子想补时间戳,跑一次 `transcribe.py 音频 --timestamps`,把生成的 srt 时间轴对齐到原文即可(sherpa-onnx 不需要独立的 ForcedAligner 模型)。
## 平台识别
| 匹配 | 平台 |
| --------------------------------- | -------- |
| `youtube.com`, `youtu.be` | YouTube |
| `bilibili.com/video/BV`, `b23.tv` | Bilibili |
B 站分 P(多 part)视频:先用 `yt-dlp --flat-playlist --dump-json "<URL>"` 列出所有 part,**先问用户处理哪几 P** 再下载。
## 输入矩阵(视频 × 文字稿 × 课件)
| 视频 | 文字稿 | 课件 | 处理动作 |
| :-: | :-: | :-: | -------------------------------------------- |
| ✓ | ✗ | ✗ | 取视频 → 先试官方字幕(CC→自动字幕),兜底 `local-asr/` → 5s 抽帧配图 |
| ✓ | ✓ | ✗ | 核验文字稿 → 5s 抽帧配图 |
| ✓ | ✗ | ✓ | 取视频 → 先试官方字幕,兜底 `local-asr/` → 课件截图优先 + 5s 抽帧(都要做) |
| ✓ | ✓ | ✓ | 核验文字稿 → 课件截图优先 + 5s 抽帧(都要做) |
| ✗ | ✓ | ✗ | 直接总结文字稿,无图 |
| ✗ | ✓ | ✓ | 总结文字稿 + 课件截图 |
| ✗ | ✗ | ✓ | 仅课件:**暂时禁止** → 提示用户补视频或文字稿 |
| ✗ | ✗ | ✗ | 询问用户至少提供一样 |
- **视频来源**:本地路径 → 直接用;B 站/YouTube URL → `yt-dlp` 下载。
- **文字稿来源**:路径/粘贴文本 → 直接用;否则下载官方字幕(CC → 自动字幕去重);再否则 `local-asr/` 全文转写(如果用户指定了火山引擎走 `volcengine-asr/`)。
- **课件来源**:PDF / PPT / PPTX 路径。
## 工作流
### 工作目录约定
默认输出到**当前仓库根目录**下,沿用 `<course_id>_<lecture>_<title>/` 子目录约定,例如 `nju_os_01_intro/`、`cs336_01_tokenization/`。
**CRITICAL**:后台/长命令一律用绝对路径(沙箱 shell 会重置 cwd,相对路径会写错地方)。
目录内布局(>30min 时 `outline.md` / `parts/` 才会出现):
```text
<outdir>/
{video_name}.mp4
frames/ ← 全课程 5s 帧 + times.txt(唯一时间依据)
slides/ ← 课件每页 PNG,共享不复制
outline.md ┐
parts/ │ 子代理模式(>30min)才出现
transcript_part_XX.txt│
frames_part_XX/ │ 本时段帧 + 局部 times.txt
part_XX.md ┘
{title_name}.md
assets/{title_name}/ ← 最终配图(pXX_ 前缀隔离)
```
### Phase 0 输入解析
按输入矩阵确定路径;缺输入先补齐,不空跑。三样都没有 → 用 AskUserQuestion 让用户至少提供一样。
### Phase 1 素材获取(委派 S0 子代理,主代理不亲自下载)
素材获取是机械操作,且过程嘈杂(yt-dlp 试 cookie / 换格式 / 应对 bot 验证、ASR 长转写重试、ffmpeg 抽帧日志),**一律委派给子代理**,主代理只拿结果——既隔离主代理上下文,又能与读稿并行。
按输入矩阵决定派哪些 S0(缺什么派什么,本地已提供的直接跳过)。`skills/lecture-to-md/` 是本 skill 的根目录,子代理用它定位脚本。
| 子代理 | 干什么 | 何时派 | 产物 |
| -------- | --------------------------------- | ------ | -------------------------------------------- |
| **S0-a** | 文字稿获取:官方字幕 → 自动字幕 → 去重清洗 → 兜底 ASR | 无文字稿输入 | `transcript.txt`(尽量带时间戳) |
| **S0-b** | 视频下载 + 严格 5s 抽帧 + times.txt | 有视频输入 | `video.mp4` + `frames/` + `frames/times.txt` |
| **S0-c** | 课件 PDF/PPT → 每页 PNG | 有课件输入 | `slides/slide-*.png` |
三个 S0 彼此独立,**在同一条消息里并行派发**(`subagent_type: general-purpose`),下载 / 转写 / 抽帧同时跑。主代理趁这个空档等文字稿产出、为读稿做准备。
#### S0-a:文字稿获取
```text
你是素材准备子代理,负责给一门课的视频/音频准备文字稿。
工作目录(绝对路径):{outdir}
仓库根目录:{repo_root}
## 任务
按下面优先级拿到文字稿,写到 {outdir}/transcript.txt(能带时间戳就带,保留时间戳):
1. 官方 CC 字幕:
yt-dlp --write-subs --sub-langs "zh.*,en.*" --convert-subs srt --skip-download "{URL}"
2. 无 CC 时自动字幕(YouTube):
yt-dlp --write-auto-subs --sub-langs "en" --convert-subs srt --skip-download "{URL}"
3. 自动字幕逐行重复 2-3 次,务必去重:
python3 {repo_root}/skills/lecture-to-md/lecture-to-md/scripts/clean_subs.py subs.en.srt --stats
4. 无任何字幕 → 本地 ASR 全文转写(sherpa-onnx X-ASR,默认中文):
python3 {repo_root}/skills/lecture-to-md/local-asr/scripts/transcribe.py "{abs_video_path}" --lang zh --timestamps
首次跑会提示执行 bash setup.sh 装 sherpa-onnx + 下载 X-ASR 模型(约 200MB)。
## 输出(返回给主代理,200 字内)
- 文字稿路径 + 行数/字数
- 来源:官方字幕 / 自动字幕 / local-asr
- 是否带时间戳
- ASR 转写时长(若走 ASR)
- 任何异常(下载失败、bot 验证、字幕语言缺失、ASR 模型未下载等)
```
#### S0-b:视频下载 + 抽帧
```text
你是素材准备子代理,负责下载视频并抽帧。
工作目录(绝对路径):{outdir}
仓库根目录:{repo_root}
## 任务
1. 下载视频(本地路径则跳过):
yt-dlp -f "bestvideo+bestaudio/best" --merge-output-format mp4 -o "video.mp4" "{URL}"
# YouTube 遇 bot 验证("Sign in to confirm you're not a bot")时追加:
# --cookies-from-browser chrome (或 safari / firefox / edge)
2. 严格 5s 抽帧 + 记录真实时间(禁止按帧号推算,见 SKILL.md「已知坑」#2):
mkdir -p frames
ffmpeg -i video.mp4 -vf "fps=1/5,showinfo" frames/f_%05d.png 2> frames/showinfo.log
3. 生成「帧文件 → 真实秒数」映射表 times.txt:
python3 - <<'PY'
import re, glob
pts = re.findall(r'pts_time:(\d+(?:\.\d+)?)',
open('frames/showinfo.log', encoding='utf-8', errors='ignore').read())
n_png = len(glob.glob('frames/f_*.png'))
assert len(pts) == n_png, f"帧数不匹配: showinfo {len(pts)} vs png {n_png}"
open('frames/times.txt', 'w', encoding='utf-8').write(
"".join(f"f_{i:05d}.png\t{t}\n" for i, t in enumerate(pts, 1)))
PY
4. 报时长:
ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 video.mp4
## 输出(返回给主代理,200 字内)
- video.mp4 路径 + 时长
- 抽帧张数 + times.txt 首末帧时间(首帧应≈0,末帧应≈时长)
- 任何异常(下载失败、bot 验证、抽帧中断等)
```
#### S0-c:课件转 PNG
```text
你是素材准备子代理,负责把课件转成每页 PNG。
工作目录(绝对路径):{outdir}
仓库根目录:{repo_root}
## 任务
- PDF 直接转:pdftoppm -png -r 120 deck.pdf slides/slide
- PPT/PPTX 先转 PDF 再转 PNG:
soffice --headless --convert-to pdf deck.pptx --outdir .
pdftoppm -png -r 120 deck.pdf slides/slide
## 输出(返回给主代理,200 字内)
- slides/ 路径 + 总页数
- 文件名命名规则(slide-01.png … slide-NNN.png,数字即 PDF 页码)
- 任何异常(转换失败、缺页、字体乱码等)
```
> **工具绝对路径**:沙箱 PATH 可能不含 `/opt/homebrew/bin`,子代理执行 `yt-dlp`/`ffmpeg`/`pdftoppm`/`soffice` 时同样要**先 `which` 或用绝对路径**(见「已知坑」#1)。
### Phase 2 文字稿核验(主代理,仅当「视频 + 用户另给的文字稿」同时提供)
S0-a 已经拿到官方字幕(或 ASR 转写),主代理用它核验用户额外提供的文字稿是否与视频对得上:
1. 有官方字幕 → 直接拿官方字幕比对用户文字稿;
2. 无官方字幕 → 用 S0-a 的 ASR 结果(或 `local-asr/` 只转写**前 1 分钟**)比对;
3. 比对前 1min + 随机抽几段:**主题/关键词明显对不上才算不一致**(措辞不同属正常),明显不一致 → 停下用 AskUserQuestion 问用户是否继续;
4. 都无法核验(网站访问不了 / 无官方字幕且 ASR 不可用)→ 记录「文字稿未核验」并继续,不阻塞。
### Phase 3 验收素材(主代理,S0 产物必须亲自验)
委派不等于闭眼收结果——子代理可能抽帧失败却报「完成」、字幕下错语言、课件页码随手给个范围。主代理**亲自**跑几条命令验收,成本极低:
```bash
# ① times.txt 行数 == frames/*.png 数量(抓抽帧中途失败)
[ "$(wc -l < frames/times.txt)" = "$(ls frames/f_*.png | wc -l)" ] && echo "✓ 帧数一致" || echo "✗ 帧数不一致"
# ② times.txt 首行 ≈ 0、末行 ≈ 视频时长(抓 pts 偏移)
head -1 frames/times.txt; tail -1 frames/times.txt
ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 video.mp4
# ③ 文字稿非空、首尾合理
wc -l transcript.txt; head -5 transcript.txt; tail -5 transcript.txt
# ④ 课件 PNG 张数 == 页数(有课件时)
ls slides/slide-*.png | wc -l
```
`frames/times.txt` 是后续所有时间溯源的**唯一依据**:配图标注「画面时间」、子代理按时间区间取帧、大纲锚点换算,全部查这张表。
课件:直接用 S0-c 转出的每页 PNG(`slides/slide-01.png` …)。
### Phase 4 结构与分片:决定单线程还是子代理
**4.1 判定课程时长**
```bash
ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 video.mp4
```
无视频(纯文字稿)时按字数估算:中文约 **250 字/分钟**、英文约 **150 词/分钟**。
- **≤ 30 分钟** → 走 `Phase 5-A`:主代理单线程直出,不必拆分子代理。
- **> 30 分钟** → **必须**走 `Phase 5-B`:先出大纲 → 按一级标题切分 → 派子代理并行撰写 → 主代理汇总。
- 切分侧(大纲格式、切分规则、切素材命令)见 **`references/splitting.md`**
- 派发侧(prompt 模板、汇总、校验)见 **`references/subagent-workflow.md`**
**4.2 配图跟随撰写者**:单线程时主代理自己挑图;子代理模式下选图下沉给各子代理——它们手上有本片段的完整上下文,选图比主代理隔空猜更准。
### Phase 5 生成 md
读取 `assets/notes-prompt.md` 作为写作规范,产出 `<outdir>/{title_name}.md`。
- **文件名与文档标题**:`{title_name}` 取自课程标题(如 `cs336_01_tokenization`);文件第一行是文档标题 `# <课程标题>`,与文件名对应,**不计入正文 H1 ≤ 10 的限额**;
- 图片用相对路径 `assets/{title_name}/xxx.png`;
- 视频帧图下用 `> 画面时间 00:12:31` 标注来源(时间查 `frames/times.txt`);
- 课件图标注页码(如 `> 课件第 12 页`)。
**5-A 单线程(≤ 30 分钟)**
用多模态模型按**每个三级标题(`###`)** 挑 1~多张最相关图:
- **优先课件截图**,课件缺失时用视频帧;无合适图则该 H3 不配图(不硬凑);
- 选出的图复制到 `assets/{title_name}/` 并统一命名(如 `h3_01.png`、`h3_02a.png`);
- 然后一气写完全文。
**5-B 子代理并行(> 30 分钟)**
| 步骤 | 谁做 | 产物 |
| ------------- | ------------------ | ----------------------------------------------------------------- |
| ① 通读文字稿,定结构 | **主代理**(不可跳过、不可委派) | `outline.md`(H1 + 稿子锚点 + 帧区间 + **课件页码区间**) |
| ② 按一级标题切分 | **主代理** | 切分方案(写进 outline.md) |
| ③ 切素材 | **S1 子代理** | `parts/transcript_part_XX.txt`、`parts/frames_part_XX/`(课件不搬,只标页码) |
| ④ 并行撰写 + 各自配图 | **再启动N 个子代理** | `parts/part_XX.md` + `assets/{title_name}/pXX_*.png` |
| ⑤ 按序汇总、统一风格 | **主代理** | `{title_name}.md` → 接着做 **Phase 6 结构重排** |
四条硬规则:
1. **主代理必须先读完整个文字稿再定大纲**——只有通读才能按课程真实结构划分,不能让子代理边读边自由发挥。
2. **切分单位是一级标题,且大纲里的 H1 就是最终 `{title_name}.md` 里的 H1**——子代理不得改名、增删、调序。每个子代理最多分 **1–3 个一级标题**。
3. **素材按片段隔离**:文字稿切进 `parts/`(相邻段**重叠** 30–60 秒保证衔接),视频帧按时段切进 `parts/frames_part_XX/`(帧**不重叠**,避免重复配图)。**课件不复制**——只在 prompt 里给路径 + 页码区间;课件常是整门课合订 PDF,不标页码子代理就会用到别的课时的页。
4. **并行不共享文件**:每个子代理只写 `parts/part_XX.md` 和自己前缀为 `pXX_` 的图片,互不覆盖。
切素材是机械操作(`sed` 按行号切、srt 按时间切、帧按时段 `awk`+`cp`、课件页码定位),委派给 **S1 子代理**执行、主代理验收——见 **`references/splitting.md` 第 4 节**;派发 prompt 模板与汇总校验见 **`references/subagent-workflow.md`**。
### Phase 6 目录结构重排(主代理,只改标题不动正文)
汇总出来的 H1 反映的是「**切分块**」,不一定等于「**课程结构**」——定大纲时可能会为了让各子代理工作量均衡,容易把大主题拆散、把小主题合并。这一环把失真修回来。
**必做**:5-B 子代理模式。**建议做**:5-A 单线程模式(同样可能切偏,跑一遍诊断代价很低)。
| | |
| -------- | ---------------------------------------- |
| **谁做** | 主代理,且**必须重新对照文字稿**,不能只看 `{title_name}.md` 自己改自己 |
| **改什么** | 只有标题行:调层级、改文字、删重复、在段落边界插新标题、同步序号 |
| **不改什么** | 正文段落、公式代码、图片引用与图注、「画面时间」「课件第 N 页」标注 |
| **判据** | **去掉所有标题行之后,重排前后的正文应逐字相同**(`diff` 可验) |
诊断信号(命中任一条就该重排):H1 带子层级编号(「2.3 …」「B2 …」)、H1 带续接词(「…(续)」「再谈…」)、相邻 H1 同属一个母题、各 H1 字数悬殊、H1 是具体技术点而非主题块。
完整诊断表、从文字稿识别真实母题的 5 类信号、执行步骤与自检命令见 **`references/structure-reorder.md`**。
### Phase 7 交付
报告 `{title_name}.md` 与 `assets/{title_name}/` 的绝对路径。(我们称这个 `{title_name}.md` 和 `assets/{title_name}/`)为成果,用户可能后续会要你移动这个到指定地方,你按这个粒度来理解成果或者产品)
## 复用资产
- `references/splitting.md`:**切分规范**——何时启用、outline.md 格式、切分规则、S0/S1 素材子代理委派(prompt 模板 + 主代理验收)
- `references/subagent-workflow.md`:**子代理规范**(派发与汇总)——prompt 模板、汇总校验、常见错误
- `references/structure-reorder.md`:**结构重排**(Phase 6)——诊断切分失真、对照文字稿定位真实母题、只改标题行不改正文的两级自检
- `scripts/clean_subs.py`:YouTube 自动字幕去重(✓ 复用,已复制到本 skill `scripts/`)
- `local-asr/`:**本地 ASR 子 skill**(sherpa-onnx X-ASR,跨平台)——见 [../local-asr/SKILL.md](../local-asr/SKILL.md)
- `volcengine-asr/`:**火山引擎 ASR 子 skill**(BigASR / 豆包 2.0,云端)——见 [../volcengine-asr/SKILL.md](../volcengine-asr/SKILL.md)