← 提示词库 Meta/muse-agent/skills/generate_podcast/SKILL.md 原文 md
🌐 中英双语对照

name: "generate_podcast"
description: "Compose and deliver audio content: a podcast episode, briefing, or narrated summary, with one or more voices, as an MP3. For reading supplied text aloud verbatim, use tts."
metadata: { "includeInPrompt": true }

Generate Podcast / 生成播客

Purpose / 目的

Generate audio content — podcasts, audio briefings, narrated summaries, or any spoken audio. Use this skill for all generated audio, not just podcasts. The podcast-helper script handles synthesis, catalog management, cover art generation, and publishing.

生成音频内容——播客、音频简报、旁白摘要或任何口播音频。所有生成的音频都使用本技能,而不仅仅是播客。podcast-helper 脚本负责合成、目录管理、封面生成与发布。

Workflow / 工作流

1. Plan / 规划

2. Write the script / 撰写脚本

Write the full dialogue. Label each turn with the speaker name followed by a colon:
写出完整对话。每句话以说话人名加冒号标注:

Alex: Welcome to the podcast. Today we're diving into async Rust.
Jordan: Great topic. Let's start with why async matters.
Alex: The big advantage is zero-cost abstractions...

Write the script to a file (e.g. /tmp/script.txt).

把脚本写入一个文件(如 /tmp/script.txt)。

The script goes directly to a TTS model — write it in spoken form. The text will be read aloud exactly as written. Follow these guidelines:

**脚本会直接送入 TTS 模型——请以口语形式书写。**文本将按原样逐字朗读。请遵循以下准则:

Write complete, conversational sentences — not headlines. Every line a host speaks should be a full grammatical sentence with a subject and a verb, the way people actually talk out loud. Avoid telegraphic, verbless fragments and stat-ticker read-outs; this is a conversation, not a wire report or a scoreboard. An occasional short line for emphasis ("Unbelievable.") is fine, but never string clipped fragments together.

**写出完整的、对话式的句子——而不是标题式短语。**主持人说的每一句都应当是有主语有谓语的完整语法句,就像人们真正开口说话那样。避免电报式的无谓语片段和数据播报式的罗列;这是对话,不是通讯社快讯,也不是记分牌。偶尔来一句强调用的短句("Unbelievable.")没问题,但绝不要把残缺片段串在一起。

When you present a stat or a scoreline, fold it into a spoken sentence ("England had just forty-four percent of the ball") rather than dropping it in as a bare fragment ("England, forty-four percent possession").

呈现数据或比分时,把它融进口语句子("England had just forty-four percent of the ball"),而不是作为干巴巴的片段丢出来("England, forty-four percent possession")。

3. Generate / 生成

Write a topics summary for dedup. Before generating, write a short bulleted summary of the major topics and key points this episode covered to a file (e.g. /tmp/topics.md) and pass it with --topics-file. Keep it concise — a handful of bullets naming the subjects and any specific stories, guests, or angles, not a full transcript. It is persisted alongside the episode and read by future generations (via the topics field in manifest read) to avoid repeating material. Example:
**写一份用于去重的主题摘要。**生成之前,把本期覆盖的主要话题与要点写成简短的要点列表,存入一个文件(如 /tmp/topics.md),并用 --topics-file 传入。保持简洁——几条要点,点出主题及具体的故事、嘉宾或角度即可,不要写成完整逐字稿。它会随节目一起持久化,供未来的生成读取(通过 manifest read 的 topics 字段)以避免重复素材。示例:

- Async Rust fundamentals: futures, executors, zero-cost abstractions
- Tokio vs async-std tradeoffs
- Common pitfalls: blocking in async contexts, .await forgetting

Default: generate only, do not publish. Only add --publish when the user explicitly asked to publish or when the podcast catalog already has an RSS feed — a "feed" object with a non-empty feed_url (meaning they've published to a feed before and want new episodes added). A "feed" object that has only a spotify_show_url (from a personal Save to Spotify) is not an RSS feed and must not trigger --publish; publishing is public and requires explicit consent.

**默认:只生成,不发布。**只有当用户明确要求发布、或播客目录中已有 RSS 订阅源——即带非空 feed_url 的 "feed" 对象(说明他们此前发布过订阅源、希望追加新节目)——时才加 --publish。只含 spotify_show_url 的 "feed" 对象(来自个人的 Save to Spotify)不是 RSS 订阅源,不得触发 --publish;发布是公开行为,需要明确同意。

【评论】该条款把"个人保存到 Spotify 的节目链接"明确排除在 RSS 订阅源判定之外,避免把私密保存误当作公开发布的授权。

Always pass --cover-prompt with a short description of the episode's theme so podcast-helper can generate cover art automatically.

始终传入 --cover-prompt 并附一句对本期主题的简短描述,让 podcast-helper 能自动生成封面。

Do not pass --cover-image. Publishing accepts only generated cover art or the bundled default, so an episode published with --cover-image fails. Use --cover-prompt instead; if the user hands you an image file, generate a cover from a prompt describing it and say you did. The flag still parses and will be reconnected later — it just cannot reach the feed today.

**不要传 --cover-image。**发布只接受生成的封面或内置默认封面,因此带 --cover-image 发布的节目会失败。改用 --cover-prompt;如果用户递给你一个图片文件,就用一段描述它的提示词生成封面,并说明你这样做了。该标志仍可解析,以后会重新接上——只是今天到不了订阅源。

Language: unless the user asks for another language, write the title, description, feed copy, and script in the language of the current conversation. podcast-helper defaults synthesis to the request's JARVIS_PRESENTATION_LOCALE; pass --language only to honor an explicit user choice (e.g. --language es, --language pt, or a locale like pt_BR). Quality varies significantly across languages — English is highest quality; other languages can be rough (mispronunciations, accent), and some voices handle a given language better than others (try a different --speaker voice from voice_source.json if one sounds wrong). Tell the user non-English audio may be imperfect. See the tts skill's Language section for details.

**语言:**除非用户要求其他语言,标题、描述、订阅源文案与脚本都用当前对话的语言书写。podcast-helper 默认按请求的 JARVIS_PRESENTATION_LOCALE 合成;只有为尊重用户明确选择时才传 --language(如 --language es、--language pt,或 pt_BR 这样的区域设置)。各语言的合成质量差异明显——英语质量最高;其他语言可能比较粗糙(发音错误、口音),且不同嗓音对某一语言的表现有好有坏(若某个嗓音听起来不对,就从 voice_source.json 换一个 --speaker 嗓音)。要告诉用户非英语音频可能不完美。详见 tts 技能的 Language 一节。

One-off episode:
一次性节目:

podcast-helper generate \
  --script /tmp/script.txt \
  --title "Deep Dive into Async Rust" \
  --description "Alex and Jordan explore async patterns in Rust" \
  --speaker Alex=avocado_v2:MAI_03 \
  --speaker Jordan=avocado_v2:MAI_01 \
  --cover-prompt "async Rust programming, gears and lightning bolts" \
  --topics-file /tmp/topics.md

Cron/recurring episode — pass --series-id matching the cron job ID so episodes in the same series share cover art:
cron/常设节目——传入与 cron 任务 ID 一致的 --series-id,让同一系列的节目共用封面:

podcast-helper generate \
  --script /tmp/script.txt \
  --title "Morning News - June 2, 2026" \
  --description "Today's top stories" \
  --speaker Alex=avocado_v2:MAI_03 \
  --speaker Jordan=avocado_v2:MAI_01 \
  --series-id daily-news \
  --cover-prompt "morning news briefing, sunrise and newspaper" \
  --topics-file /tmp/topics.md

With --series-id, cover art is generated once for the first episode and reused for all subsequent episodes in the series.

带 --series-id 时,封面只为第一期生成一次,之后该系列的所有节目都复用它。

If the user asked to publish, or the podcast catalog already has an RSS feed — run podcast-helper manifest read and check for a "feed" object with a non-empty feed_url (a feed with only spotify_show_url does not count):

如果用户要求发布,或播客目录中已有 RSS 订阅源——运行 podcast-helper manifest read,检查是否存在带非空 feed_url 的 "feed" 对象(只含 spotify_show_url 的 feed 不算):

podcast-helper generate \
  --script /tmp/script.txt \
  --title "Deep Dive into Async Rust" \
  --description "Alex and Jordan explore async patterns in Rust" \
  --speaker Alex=avocado_v2:MAI_03 \
  --speaker Jordan=avocado_v2:MAI_01 \
  --cover-prompt "async Rust programming" \
  --topics-file /tmp/topics.md \
  --publish --feed-title "Daily with Alex"

Returns JSON with path, duration_secs, slug, cover, and (if published) feed_url, episode_url, and subscription_links.

返回 JSON,含 path、duration_secs、slug、cover,以及(若已发布)feed_url、episode_url 和 subscription_links。

4. Deliver to chat / 在对话中交付

Always present the title as plain text above the playable link:
始终把标题以纯文本形式放在可播放链接上方:

{title}
[{title}](sandbox://workspace/podcasts/{slug}/{slug}.mp3)

The first time you mention publishing, a personal feed, or subscription in a conversation, briefly explain what the feed is: a personal RSS podcast feed they can add to a podcast app, and future published episodes will show up there automatically. After that first explanation, use shorter wording.

在对话中第一次提到发布、个人订阅源或订阅时,简要解释订阅源是什么:一个可加入播客客户端的个人 RSS 播客订阅源,今后发布的节目会自动出现在那里。第一次解释之后,改用更简短的说法。

If the episode was published, mention it was published and that it will appear in their feed. Note that published episodes are public. If the result has published: false with publish_error, the audio was generated locally but publication failed: relay publish_error to the user and do not claim it reached the feed. If it also has blocked: true, apply the content-review stop rule in step 5 and skip the publish follow-up below: do not retry, reword the script, or use another publish path. If it has retryable: true, content review could not run; tell the user publication can be retried later using the full step 5 podcast-helper publish command. Before retrying, run podcast-helper manifest read and reuse the target feed's existing feed_title exactly when one exists; never invent or alter a title for an existing feed.

如果节目已发布,说明已发布并会出现在其订阅源中。注意:已发布的节目是公开的。如果结果带 published: false 和 publish_error,说明音频已在本地生成但发布失败:把 publish_error 如实转达给用户,不要声称它已到达订阅源。如果还带 blocked: true,则执行第 5 步的内容审查停止规则,并跳过下文的发布后续追问:不要重试、不要改写脚本、也不要走其他发布途径。如果带 retryable: true,说明内容审查未能运行;告诉用户稍后可用第 5 步完整的 podcast-helper publish 命令重试发布。重试前,先运行 podcast-helper manifest read,若目标订阅源已有 feed_title,则原样复用;绝不为已存在的订阅源杜撰或更改标题。

After delivering, present applicable follow-ups:
交付之后,提出适用的后续选项:

  1. If not published and not blocked: "Want me to publish this to a personal podcast feed? It's an RSS feed you can add to Apple Podcasts, Overcast, Pocket Casts, or most other podcast players, and future published episodes will show up there automatically. Note: published episodes are public — anyone with the link can listen."
    若未发布且未被拦截:"Want me to publish this to a personal podcast feed? It's an RSS feed you can add to Apple Podcasts, Overcast, Pocket Casts, or most other podcast players, and future published episodes will show up there automatically. Note: published episodes are public — anyone with the link can listen."
  2. If no cron job exists: "Generate a new episode every day?"
    若尚无 cron 任务:"Generate a new episode every day?"

5. Publish (if not done in step 3) / 发布(若第 3 步未做)

podcast-helper publish \
  --slug {slug} \
  --feed-title "Daily with Alex" \
  --feed-description "Daily news and tech updates"

Returns JSON with feed_url, episode_url, and subscription_links.

返回 JSON,含 feed_url、episode_url 和 subscription_links。

Public publishing is content-reviewed. If the output has "blocked": true, the episode was not published: the audio and catalog entry still exist locally, but it cannot go to the public feed. Relay the returned error message to the user plainly and stop — do not retry, reword the script to get around it, or fall back to another publish path. Saving to the user's own Spotify show is unaffected by this review.

公开发布会经过内容审查。如果输出带 "blocked": true,该节目未发布:音频与目录条目仍在本地存在,但不能进入公开订阅源。把返回的 error 消息平实地转达给用户并停止——不要重试、不要为绕过审查而改写脚本、也不要退回其他发布途径。保存到用户自己的 Spotify show 不受此审查影响。

A publish that brings a new show cover unifies artwork across that show — its episodes' covers are replaced with it in the podcast catalog. A series show keeps its series cover, and an episode whose cover was changed with update-cover keeps the show's existing cover when published. Another feed's episodes keep their own artwork.

发布时若带来了新的节目封面,会把该节目的全部作品统一——目录中其各期节目的封面都被替换为它。系列节目保留其系列封面,而用 update-cover 改过封面的某期节目在发布时保留节目既有的封面。其他订阅源的节目保留各自的作品图。

After publishing, always re-present the listen link and confirm publication:
发布之后,始终重新出示收听链接并确认发布:

{title}
[{title}](sandbox://workspace/podcasts/{slug}/{slug}.mp3)

Published to your personal feed, which is the RSS feed you can subscribe to in your podcast app so future published episodes show up there automatically.
Published episodes are public — anyone with the feed link can listen.

6. Subscribe (after publishing) / 订阅(发布之后)

On the first episode published to a feed, present subscription links from the output JSON:

在向某个订阅源发布第一期节目时,出示输出 JSON 中的订阅链接:

Subscribe to your personal feed:

RSS feed URL (copy and paste into any podcast player):
`{subscription_links.rss}`

Or open directly in a podcast app:
- [Open in Apple Podcasts]({subscription_links.apple_podcasts})
- [Open in Overcast]({subscription_links.overcast})
- [Open in Pocket Casts]({subscription_links.pocket_casts})

This is the RSS feed for your generated episodes, and once you add it to a podcast app, future published episodes will appear there automatically.

Important — how to render the feed URL: The RSS feed URL is for the user to copy and paste, never to click. Always present any published https:// URL (feed URL, episode link) wrapped in backticks as code — either inline `https://…` or inside a fenced code block. Never emit it as a bare URL (a bare https://… auto-linkifies on native clients) and never as a [label](https://…) markdown link; either form may fail to open on native clients and invites a click instead of a copy. Do not offer direct episode download links. The only clickable links should be local sandbox:// listen links and the podcast-app protocol links (podcast://, overcast://, pktc://).

重要——订阅源 URL 的呈现方式:RSS 订阅源 URL 是供用户复制粘贴的,绝不是供点击的。任何已发布的 https:// URL(订阅源 URL、节目链接)一律用反引号包成代码呈现——要么行内 `https://…`,要么放进围栏代码块。绝不要以裸 URL 输出(裸 https://… 在原生客户端上会自动变成链接),也绝不要作为 [label](https://…) 的 markdown 链接;这两种形式在原生客户端上可能无法打开,且会诱导点击而非复制。不要提供节目直接下载链接。唯一可点击的应当是本地 sandbox:// 收听链接和播客客户端协议链接(podcast://、overcast://、pktc://)。

【评论】把订阅链接强制为代码样式,是针对聊天客户端自动把裸 URL 转成可点击链接的适配设计,目的是让用户复制而不是误点。

On subsequent episodes to the same feed, skip — the user is already subscribed.

对同一订阅源之后的节目,跳过此步——用户已订阅。

Feed Organization / 订阅源组织

Always use a single feed unless the user explicitly asks for a separate one. Before publishing, run podcast-helper manifest read. feeds lists every feed on this VM and feed is whichever was published to most recently; if either has a non-empty feed_url, reuse that feed's feed_title exactly.

始终只用一个订阅源,除非用户明确要求单独一个。发布前运行 podcast-helper manifest read。feeds 列出本 VM 上的每个订阅源,feed 是最近一次发布到的那个;若其中任一具有非空 feed_url,就逐字复用该订阅源的 feed_title。

When the user does keep more than one show, the title is what picks the feed: publishing with a --feed-title that matches an existing feed adds to it, and any other title creates a new one. So reuse a title character for character when adding to a show, and never reuse one for a different show.

当用户确实保留多个节目时,标题就是挑选订阅源的依据:用与既有订阅源匹配的 --feed-title 发布会追加到它,任何其他标题则创建新的订阅源。因此向某个节目追加时逐字符复用其标题,且绝不在另一个节目上复用同一标题。

On the first publish (no feed_url in the podcast catalog yet — a feed object that only carries a spotify_show_url still counts as no RSS feed), choose a personal feed title based on the user's Muse name — e.g. "Today with Alex", "News with Alex".

首次发布时(播客目录中尚无 feed_url——只带 spotify_show_url 的 feed 对象仍视为没有 RSS 订阅源),基于用户的 Muse 名字选一个个人订阅源标题——如 "Today with Alex"、"News with Alex"。

Add to your Spotify (personal, optional) / 添加到你的 Spotify(个人的,可选)

When the user explicitly asks to put a generated episode on their Spotify, use
podcast-helper save-to-spotify to add it to the user's own Spotify account. This is a personal
save to the user's own Spotify library/show — it is NOT the public/subscribable RSS feed publishing
described above, and does not use podcast-helper --publish. Only do this on an explicit request; do
not offer it proactively.

当用户明确要求把生成的某期节目放到其 Spotify 上时,使用 podcast-helper save-to-spotify 把它加入用户自己的 Spotify 账户。这是对用户自己的 Spotify 资料库/show 的个人保存——它不是上文所述的公开/可订阅的 RSS 订阅源发布,也不使用 podcast-helper --publish。只在明确请求时执行;不要主动提议。

Run the upload through podcast-helper (not the raw save-to-spotify CLI): the helper uploads via the
bundled save-to-spotify CLI and records the resulting Spotify show link into the podcast catalog so
it shows up on the library podcasts page. See references/save-to-spotify.md for the underlying CLI,
the connect flow, and rules.

通过 podcast-helper 执行上传(而不是直接用 save-to-spotify CLI):该助手经内置的 save-to-spotify CLI 上传,并把生成的 Spotify show 链接记入播客目录,使其显示在资料库播客页面上。底层 CLI、连接流程与规则见 references/save-to-spotify.md。

  1. List shows / check connection: run save-to-spotify --json shows — it lists existing shows and
    confirms Spotify is connected. If it fails with a token / "not connected" error, tell the user to
    connect Spotify in Settings → Connections → Spotify (the shared Spotify connection), then retry.
    This tool has no auth subcommands or connect link. Ask the user whether to reuse an existing show
    or create a new one; don't silently pick.
    **列出 show / 检查连接:**运行 save-to-spotify --json shows——它列出现有 show 并确认 Spotify 已连接。若因 token / "not connected" 错误失败,告诉用户在 Settings → Connections → Spotify(共享的 Spotify 连接)中连接 Spotify,然后重试。此工具没有 auth 子命令或连接链接。询问用户是复用既有 show 还是新建一个;不要擅自选择。
  2. Upload + record with podcast-helper (confirm title, target show, and summary with the user
    first — this writes to their account). The helper reads the audio/title/cover from the podcast catalog by
    slug; pass a cover only to override (must be JPEG/PNG ≤ 1 MB — convert .webp/other formats first):
    上传 + 记录用 podcast-helper(先与用户确认标题、目标 show 与简介——这会写入他们的账户)。助手按 slug 从播客目录读取音频/标题/封面;只有要覆盖时才传封面(必须为 JPEG/PNG 且 ≤ 1 MB——先把 .webp 等其他格式转换):
    podcast-helper save-to-spotify \
      --slug {slug} \
      [--show-id <id> | --new-show "<show title>"] \
      [--title "{title}"] [--summary "{description}"] [--image {cover}]
    
    Returns JSON with episode_id, episode_uri, spotify_show_id, and spotify_show_url.
    返回 JSON,含 episode_id、episode_uri、spotify_show_id 和 spotify_show_url。
  3. Wait for readiness: save-to-spotify --json episodes status <episode-id> --wait (use the
    episode_id from step 2). Processing is server-side and takes a few minutes; a returned
    episode_uri means it was accepted. If it stalls in NOT_READY (Spotify occasionally 503s), it's a
    Spotify-side delay — tell the user it's processing and retry later rather than polling indefinitely.
    等待就绪:save-to-spotify --json episodes status <episode-id> --wait(使用第 2 步的 episode_id)。处理在服务端进行,需要几分钟;返回 episode_uri 表示已被接受。若卡在 NOT_READY(Spotify 偶尔返回 503),那是 Spotify 侧的延迟——告诉用户正在处理、稍后重试,而不是无限轮询。
  4. Tell the user it's on their Spotify and may take a few minutes to appear in the app. Because the show
    link is now recorded, the Spotify option on the podcasts library page will link to their show. Refer
    to the show and episodes by title and summarize readiness in plain language. Spotify show IDs,
    episode IDs, and spotify:show: / spotify:episode: URIs are internal CLI handles: use them for
    subsequent commands, but never include them in a user-facing response.
    告诉用户它已在用户的 Spotify 上,可能需要几分钟才会出现在客户端中。由于 show 链接已被记录,播客资料库页面上的 Spotify 选项将链接到他们的 show。用标题指称 show 与各期节目,并用平实语言概述就绪状态。Spotify show ID、episode ID 以及 spotify:show: / spotify:episode: URI 是内部 CLI 句柄:后续命令中使用它们,但绝不出现在面向用户的回复中。

For deletion, use the raw save-to-spotify CLI, not podcast-helper, spotify-api,
podcasters.spotify.com, or creators.spotify.com. Start with save-to-spotify --json shows, resolve
the requested show by title, and inspect it with shows get <show-id>. Use
episodes --show-id <show-id> followed by episodes delete <episode-id> to remove one episode. To
remove the entire show, confirm that all its episodes will be deleted and run shows delete <show-id>;
that command removes the episodes too. Treat {"status":"deleted"} as accepted, then poll the relevant
shows or episodes --show-id inventory for up to 60 seconds. Confirm deletion only after the item is
absent; otherwise tell the user Spotify is still propagating the accepted deletion and do not repeat it.
Deleting the Spotify copy does not delete local audio or an RSS feed. See
references/save-to-spotify.md for the full command flow.

删除时使用原始的 save-to-spotify CLI,而不是 podcast-helper、spotify-api、podcasters.spotify.com 或 creators.spotify.com。先运行 save-to-spotify --json shows,按标题解析所请求的 show,并用 shows get <show-id> 查看。用 episodes --show-id <show-id> 加 episodes delete <episode-id> 删除单期节目。要删除整个 show,先确认其所有节目都将被删除,然后运行 shows delete <show-id>;该命令会一并删除各期节目。把 {"status":"deleted"} 视为已受理,然后对相关的 shows 或 episodes --show-id 清单轮询至多 60 秒。只有当条目确实消失后才确认删除;否则告诉用户 Spotify 仍在传播这条已受理的删除,并且不要重复执行。删除 Spotify 副本不会删除本地音频或 RSS 订阅源。完整命令流程见 references/save-to-spotify.md。

Cover Art / 封面

Cover art is handled automatically by podcast-helper during generation. You control it with two flags:

封面在生成过程中由 podcast-helper 自动处理。你用两个标志控制它:

How artwork flows:

作品图的流转方式:

Scenario Behavior
One-off episode Unique cover generated per episode from --cover-prompt
Recurring/cron episode First episode generates cover; subsequent episodes with same --series-id reuse it
Published episode A new show cover replaces that feed's episode covers; a series show, or an episode changed with update-cover, keeps the show's cover
场景 行为
一次性节目 每期由 --cover-prompt 生成独一无二的封面
常设/cron 节目 第一期生成封面;之后相同 --series-id 的各期复用它
已发布节目 新的节目封面会替换该订阅源各期节目的封面;系列节目、或用 update-cover 改过封面的某期,发布时保留节目既有封面

Fallback when no --cover-prompt is provided: the bundled default cover
(/opt/hatch/skills/generate_podcast/default-cover.jpg). The user's avatar is
also tried first, but an avatar cover cannot currently be published — one more
reason to always pass --cover-prompt on an episode headed for a feed.

未提供 --cover-prompt 时的兜底:内置默认封面(/opt/hatch/skills/generate_podcast/default-cover.jpg)。也会先尝试用户头像,但头像封面目前无法发布——这是面向订阅源的节目始终要传 --cover-prompt 的又一个理由。

You do NOT need to call media.generate_image yourself for cover art — podcast-helper handles it internally.

你不需要为封面亲自调用 media.generate_image——podcast-helper 内部处理。

--cover-image is not available right now: publishing takes only generated cover art or the bundled default. The flag is still accepted so it can be reconnected later, but an episode published with it fails, so do not use it.

--cover-image 目前不可用:发布只接受生成的封面或内置默认封面。该标志仍被接受以便日后重新接上,但带它发布的节目会失败,所以不要使用。

Changing the cover / 更换封面

When the user asks to change an episode's cover — a new prompt, a variation, or an image they hand you — regenerate through the helper so the catalog reference moves to a new file:

当用户要求更换某期节目的封面——新提示词、变体、或他们递来的图片——通过助手重新生成,让目录引用指向新文件:

podcast-helper update-cover \
  --slug {slug} \
  --cover-prompt "starry night sky over a mountain lake"

This generates new artwork to a fresh path, updates the episode's cover reference to it, and prints the new path as cover. Present that new path in chat.

它会生成新的作品图到一个全新路径,把该期节目的 cover 引用更新为它,并把新路径作为 cover 打印出来。在对话中出示这个新路径。

Hard rules:

硬性规则:

【评论】"封面写入新路径而非覆盖旧文件"是围绕客户端按路径缓存图片这一现实约束的设计;同理,发布拒绝重复条目是为了避免订阅源中出现重复节目。

Scheduling / 排期

When a user asks for a recurring podcast, audio briefing, or scheduled audio content:

当用户要求常设播客、音频简报或定时音频内容时:

  1. Generate a first episode now — don't just set up the cron and leave them with nothing to listen to. Generate and deliver the first episode immediately so the user has something right away.
    立即生成第一期——不要只搭好 cron 就让用户无节目可听。立刻生成并交付第一期,让用户马上有东西可听。
  2. Ask about publishing — offer to publish to a podcast feed so they can subscribe and listen in their preferred podcast app. If they agree, publish the first episode and present subscription links.
    询问是否发布——提议发布到播客订阅源,让他们能订阅并在自己喜欢的播客客户端中收听。若同意,发布第一期并出示订阅链接。
  3. Then create the cron job — set up the recurring schedule.
    然后再创建 cron 任务——设置常设排期。

For recurring podcasts, create a cron job using the cron tool. Always set timeout_secs: 1800.

对常设播客,用 cron 工具创建 cron 任务。始终设置 timeout_secs: 1800。

Example cron tool call:
cron 工具调用示例:

{
  "action": "add",
  "file_name": "daily-podcast__daily@13:00:00.md",
  "id": "daily-podcast",
  "enabled": true,
  "mode": "task",
  "timeout_secs": 1800,
  "schedule": {
    "kind": "daily",
    "timezone": "UTC",
    "time": "13:00:00"
  },
  "body": "Generate a new podcast episode on <topic>. Follow the generate_podcast skill workflow. Keep the same cast every episode: hosts Alex (--speaker Alex=avocado_v2:MAI_01) and Jordan (--speaker Jordan=avocado_v2:MAI_03). Use --series-id daily-podcast and --cover-prompt '<topic description>' when calling podcast-helper generate. Before choosing today's angle, run podcast-helper manifest read and review each recent episode's topics field to avoid repeating what was already covered. Write a short topics summary to /tmp/topics.md and pass it with --topics-file so future episodes can dedup against it. Publish to the feed after generating."
}

Keep cron task descriptions concrete — include the topic angle, the fixed cast (host names and their voice ids, so every episode uses the same hosts and voices), --series-id matching the cron ID, and explicit instructions to research live results at generation time.

保持 cron 任务描述具体——包含主题角度、固定班底(主持人姓名与其嗓音 id,让每期用相同的主持人与嗓音)、与 cron ID 匹配的 --series-id,以及"在生成时检索实时结果"的明确指示。

Utility Commands / 实用命令

For edge cases and manual operations, podcast-helper exposes sub-commands:

针对边界情况与手动操作,podcast-helper 暴露以下子命令:

Listening to Existing Episodes / 收听既有节目

If the user asks to listen to a podcast, hear their podcast, or asks about their episodes, read the podcast catalog with podcast-helper manifest read and present listen links for the relevant episodes:

如果用户要求收听某个播客、听听他们的播客,或询问其各期节目,用 podcast-helper manifest read 读取播客目录,并出示相关节目的收听链接:

[{title}](sandbox://workspace/podcasts/{slug}/{slug}.mp3)

If the feed is published, also include the subscription links (see section 6 format).

若订阅源已发布,同时附上订阅链接(格式见第 6 节)。

Operating Rules / 操作规则

  1. Use voices from /opt/hatch/skills/voice-selector/voice_source.json, referenced by their catalog id. Default to the Meta AI voices (catalog ids MAI_01 and MAI_03) most of the time, but pick another catalog voice when the topic, tone, or user request steers that way. Never say "MAI" to the user — call them the Meta AI voices or use display names.
    使用 /opt/hatch/skills/voice-selector/voice_source.json 中的嗓音,按其目录 id 引用。多数情况下默认使用 Meta AI 嗓音(目录 id MAI_01 与 MAI_03),但当主题、语气或用户请求指向别处时,可挑选目录中的其他嗓音。绝不对用户说 "MAI"——称之为 Meta AI voices 或使用展示名。
  2. If a chunk fails, tts synthesize-script stops — do not deliver a partial episode. Most failures are transient backend issues: retry the same generation later on a bounded backoff (~5m, ~10m, ~30m, ~1h), keeping the same speaker voices. Never swap in a different voice or a different TTS engine to work around a failure. Schedule the retry (a delayed wakeup or short cron) rather than blocking, and tell the user you'll deliver the episode once synthesis recovers. If it still fails after the ~1h retry, stop — cancel the scheduled retry, report the error, and suggest they try again later. (A ... not allowed to use voiceID ... error won't clear on retry — that voice id isn't permitted for this client; pick another from voice_source.json and regenerate. Auth or clear request errors likewise need a fix, not a retry.)
    若某个分块失败,tts synthesize-script 会停止——不要交付残缺的节目。多数失败是暂时性的后端问题:稍后重试同一生成,按有界的退避(约 5 分钟、10 分钟、30 分钟、1 小时),并保持相同的说话人嗓音。绝不为绕过失败而换用别的嗓音或别的 TTS 引擎。安排重试(延迟唤醒或短 cron)而不是阻塞等待,并告诉用户合成恢复后会交付节目。若约 1 小时重试后仍失败,停止——取消已安排的重试,报告错误,并建议用户稍后再试。(... not allowed to use voiceID ... 错误重试不会消除——该嗓音 id 未获此客户端许可;从 voice_source.json 换一个并重新生成。鉴权错误或明显的请求错误同样需要修复,而不是重试。)
  3. On first episode in a new feed, help the user subscribe. On subsequent episodes, skip.
    新订阅源的第一期时,协助用户订阅。之后的节目跳过。
  4. Cron runs: Follow whatever the cron task description says. If it says to publish, publish without confirmation.
    **cron 运行:**按 cron 任务描述执行。若描述要求发布,无需确认直接发布。
  5. Never present a published https:// feed or episode URL as a bare or clickable link — always as copyable code (wrapped in backticks). Only sandbox:// listen links and podcast-app protocol links (podcast://, overcast://, pktc://) may be clickable.
    绝不把已发布的 https:// 订阅源或节目 URL 以裸链接或可点击链接呈现——一律作为可复制的代码(用反引号包裹)。只有 sandbox:// 收听链接和播客客户端协议链接(podcast://、overcast://、pktc://)可以点击。