Markdown → parse_md → { 预览 HTML | split → gen → (match) → merge → 配音 HTML }
Purpose
Export blog Markdown as line-by-line reveal HTML suitable for presentations
- Right arrow key reveals line by line, left arrow key goes back
- Initially only the title is shown and centered; when entering a new
## 标题the previous section is cleared, keeping only the title and the current section - With pre-recorded audio version:
Shiftmute,Ctrlre-read the current line
Mode selection
If the user has not explicitly stated whether they want "preview" or "TTS", you must use the Ask tool to ask before generating.
Dependencies
Python packages:
pip install esflow edge-tts
System binaries (audio decoding):
brew install ffmpeg # macOS
sudo apt install ffmpeg # Debian/Ubuntu
- Preview / split / merge: Python 3 standard library
gen:edge-ttsmatch(alternative alignment):onnxruntime、numpy, model downloaded on first use toscripts/models/
Images:(/pics/...) relative to cwd; can be overridden with BLOG_NARRATOR_IMAGE_BASE .
Quick start
# 轻量预览(无预录音,体积小,验证排版)
python3 scripts/run.py preview input.md output.html [--rate 1.15] [--open]
# TTS 分段配音(一次跑完 split→gen→(match)→merge)
python3 scripts/run.py tts input.md work_dir [--voice xiaoxiao|xiaoyi] [--rate 1.175] [--open]
Default behavior: preview writes output.html; TTS writes work_dir/{stem}_voice.html,--open and automatically opens the browser.
Artifact forms
| Mode | Audio source | Size | Use case |
|---|---|---|---|
| Preview HTML | Browser SpeechSynthesis reading (no pre-recorded audio), includes a voice panel that can be clicked to play | Small | Quickly verify layout, live presentation fallback |
| TTS HTML | edge-tts pre-recorded audio embedded as base64,work_dir/audio/N.mp3 as the segmentation source |
Large | Offline presentations, stable voice |
Both artifacts are self-contained single files, both with a voice panel (Shift mute,Ctrl re-read the current line). The difference is only in the audio source: preview relies on browser synthesis with zero dependencies, TTS embeds pre-recorded audio.
- Working directory(TTS mode):
work_dir/
src.md # 原始 Markdown 副本
srt.md # 分段清单(## [N] 标记)
audio/ # 1.mp3, 2.mp3, …
{stem}_voice.html # 合成产物
.esflow-jobs/ # esflow 运行状态(--from 续跑用)
Flow structure
esflow DAG orchestration (scripts/flow.py declaration,scripts/nodes/ each node):
parse_md ──→ export_preview (mode=preview)
└──→ split → gen → match → merge (mode=tts)
Single flow with two paths, the subcommand determines mode, and the node accept skips accordingly:
| Node | Responsibility |
|---|---|
parse_md |
strip frontmatter/divider → HTML + extract title/paragraph text |
export_preview |
Only mode=preview, generate lightweight HTML |
split |
Only mode=tts, write work_dir/src.md + srt.md + audio/ |
gen |
Edge TTS segmented synthesis audio/N.mp3 |
match |
Condition:audio/ when there are non- N.mp3 named files, ASR alignment and renaming |
merge |
Synthesize HTML with embedded base64 audio |
match when skipped merge still runs——merge directly reads split.audio_dir, does not depend on match artifacts. No TO_AGENT breakpoint, runs to completion in one go.
Debug a single node: in the skill root directory esflow run scripts/ --node <节点 id>(upstream reuses from job artifacts).
Resume: after replacing audio, run only match+merge
Standard TTS flow gen always produces N.mp3,match automatically skipped. Tomanually replace/supplement audio(e.g., use a custom voice for flowcharts, code snippets):
# 1. 先正常跑一次,拿到 job 目录
python3 scripts/run.py tts input.md work_dir
# 2. 手动改 audio/:替换某些 N.mp3,或按 srt.md 段号放入网页 TTS 下载的音频(可非 N.mp3 命名)
# 3. 从 match 续跑(上游 split/gen 从 job 目录加载,不重跑)
python3 scripts/run.py tts input.md work_dir --from match --job-dir work_dir/.esflow-jobs/blog-narrator/<job时间戳>
match uses ASR to recognize non-named audio and rename it to N.mp3,merge Re-synthesize HTML.--from You can also specify merge to only re-synthesize.
Parameters
preview subcommand
| Parameter | Description |
|---|---|
input |
Input Markdown |
output |
Output HTML path |
--rate |
Default speech rate, default 1.15 |
--open |
Open browser after generation |
tts subcommand
| Parameter | Description |
|---|---|
input |
Input Markdown |
work_dir |
Working directory (stores src.md/srt.md/audio/artifacts) |
--voice |
Voice xiaoxiao/xiaoyi, default xiaoxiao |
--rate |
Speech rate, default 1.175 |
--open |
Open browser after generation |
--from NODE |
Resume from the specified node for that node and downstream, upstream loaded from --job-dir loaded |
--job-dir DIR |
--from Required when resuming, points to the previous job directory |
Exception handling
| Situation | Behavior |
|---|---|
| Input file does not exist | Precheck failed, exit code 3 |
--voice Invalid |
bad_voice error, exit code 3 |
--rate Not a number |
bad_rate error, exit code 3 |
| edge-tts synthesis failed | Single segment failure skipped, log ⚠️ 失败, does not interrupt the whole |
match Missing onnxruntime |
no_onnxruntime error, exit code 1 |
--from Not configured --job-dir |
exit code 3 |
Agent usage guide
Use this skill when the user mentions "blog to presentation", "line-by-line reveal HTML", or "blog voiceover":
- First use Ask to confirm the mode: preview or TTS
- Preview:
python3 scripts/run.py preview <input.md> <output.html> --open - TTS:
python3 scripts/run.py tts <input.md> <work_dir> --open - If the user wants to replace certain audio: guide them to use
--from match --job-dir <上次job>resume - Confirm the product HTML path and report it back to the user
Modification rules
- Core library (MD→HTML, stage, ASR):
scripts/narrator_core.py - DAG declaration:
scripts/flow.py; node implementation:scripts/nodes/ - CLI and envelope:
scripts/run.py、scripts/common.py