tryworld-paper
Create branded 试界TryWorld AI-knowledge videos in the fixed "Paper Algorithm" (纸上算法) style with HyperFrames. Input is a Chinese voiceover script and optional images; output is a 16:9 horizontal video with Azure YunxiNeural (真·云希) voiceover, word-synced captions, ink/paper animatio
Install
npx skills add https://github.com/TryWorld2026/paper-algorithm/tree/main/skills/tryworld-paper
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tryworld2026-paper-algorithm@llmmart
git clone https://github.com/TryWorld2026/paper-algorithm.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tryworld2026/paper-algorithm collection as a plugin from our marketplace. Git is the plain clone.
README
试界-纸上算法 TryWorld-Paper Algorithm
一页会动的算法笔记 —— 把 AI 知识口播稿变成风格统一的横屏视频、横竖封面与平台标题。
这是什么
tryworld-paper(试界-纸上算法 TryWorld-Paper Algorithm)是试界TryWorld 的视频生产 Skill:输入一篇中文 AI 口播稿(可附图片),自动产出:
- 横屏主视频(16:9,口播最长约 10 分钟)—— 纸上算法风格,真云希配音,全程烧录字幕;超长稿提炼核心精华压缩
- 横版封面 + 竖版封面(1920x1440 横 4:3 / 1080x1440 竖 3:4)—— 深墨海报风(与视频浅纸面两套视觉语言),标题 + 信息块,适配 B站/抖音/小红书
- 3-5 个平台标题 —— 按 B站/抖音/小红书分别适配
- 右上角全程常驻的"试界原创"防伪印章
- 每期固定的默认结尾签名(试界TryWorld 标准收尾,口播+画面同步)
设计契约:纸上算法 Paper Algorithm
设计源头:科学手稿 + 中文印刷传统。每一期视频都是一页"会动的算法笔记",纸面是舞台,墨迹是文字,朱红是重点,印章是签名。
色板(4 色锁定,不新增)
| 色 | 值 | 用途 |
|---|---|---|
| 纸面 | #F4EFE4 |
背景主色 |
| 墨黑 | #1C1916 |
主文字、线条 |
| 朱红 | #C0452F |
唯一强调色:关键词、数字、印章 |
| 墨水蓝 | #2E5E8C |
次级批注、图表线 |
字体
- 主标题:思源宋体(Noto Serif SC)
- 批注/引用:ZCOOL XiaoWei(默认);霞鹜文楷(LXGW WenKai)如需使用须自带 woff2
- 数据/坐标:等宽字体(JetBrains Mono)
动效签名(全片统一)
| 签名 | 感觉 | 缓动 |
|---|---|---|
| 墨落纸 | 主入场 | power3.out |
| 笔写入 | 批注/小元素 | sine.inOut |
| 盖章 | 数字/结论/印章 | back.out(1.4) |
转场统一为墨染过渡:前场景被墨色浸染溶解,新场景从纸面浮现;章节间留纸面空场。
动态图表
数据/对比/流程用风格统一的动态图表表达:数字滚动盖章、柱状墨迹增长、折线一笔描画、进度环、对比卡片;等宽数字 + 朱红强调 + 墨水蓝网格,GSAP/SVG 实现,避免大段静态文字配解说。
配音质量保障
- 多音色:默认云希(Azure YunxiNeural,试界品牌音色),
--voice可换预设(xiaoxiao 晓晓 / yunjian 云健 / yunyang 云扬 / xiaoyi 晓伊 / yunxia 云夏 / xiaobei 小贝东北话 / xiaoni 小妮陕西话),也接受完整 edge-tts 音色 id;本地合成,无需 API Key - 默认语速 +8%,可按内容微调 +5%~+15%
- 防卡顿机制:文本自动规整(清除 markdown、合并句中换行),按句切分合成——停顿只允许出现在句号/问号/感叹号处,句子中间永不硬断
- 句级时间戳(
sentences.json)直接驱动字幕同步,不依赖 whisper
工作流
口播稿 → 读稿理解 → 第一性原理优化 → 活人感改稿七遍 → 净化写作标记 → 硬禁项检查(check_prose.py 清零) → 用户确认优化稿 → 句级切分 → 云希配音 → 句级时间轴
→ 章节/场景规划 → HyperFrames 构图 → lint/validate/inspect --strict
→ 渲染前核验 → 渲染 16:9 主视频 → 横竖封面 → 平台标题 → 交付核验(verify_output.py)→ outputs/ 交付
使用方式
在 Codex 会话中给出口播稿(纯文字或 .txt/.md,可附图片):
用 $tryworld-paper 把这篇口播稿做成视频,加上横竖封面和标题。
可选输入:图片目录(AI 概念图 / 截图 / 图表,会自动"纸面化"处理)。
目录结构
tryworld-paper/
├── SKILL.md # 主说明:品牌契约、工作流、质量门禁
├── agents/
│ └── openai.yaml # UI 元数据
├── references/
│ ├── style-system.md # 视觉契约(色板/字体/动效/封面)
│ ├── workflow.md # 生产流程与命令
│ └── titles.md # 平台标题规则
├── scripts/
│ ├── tts_yunxi.py # 云希配音管线(句级切分 + 拼接 + 时间轴)
│ ├── verify_output.py # 交付核验硬门禁(音轨/时长/双封面/标题/发布计划)
│ └── check_prose.py # 活人感硬禁项检查(源自 human-writing v1.1.0,MIT)
└── assets/
├── paper-grain.svg # 纸纹叠加层
└── seal.svg # 朱红"试界原创"印章
环境要求
- Node.js >= 22
- FFmpeg(含 ffprobe,加入 PATH;
winget install Gyan.FFmpeg) - Python 3.10+,
pip install edge-tts - HyperFrames CLI(
npx hyperframes)
运行 npx hyperframes doctor 可一键检查环境。
防伪标识
视频与封面统一携带:
- 右上角全程常驻朱红试界原创印章(保护区,内容不得覆盖)
- 不添加"试界TryWorld 纸上算法"文字水印,印章是唯一水印
许可
本仓库采用知识共享 署名-相同方式共享 4.0 国际(CC BY-SA 4.0)。其中 scripts/check_prose.py 源自 KKKKhazix/human-writing v1.1.0(禁令上移到修辞动作级,检测器补盲区降误伤),按 MIT License 使用与改造,版权归原作者所有,完整声明见 LICENSE-MIT。
Skill manifest
TryWorld-Paper Algorithm(试界-纸上算法)
按当前主题的品牌制作 AI 知识类视频(默认主题为试界TryWorld 的纸上算法)。所有产出必须遵守本文件与 references/style-system.md 锁定的设计系统,任何一条都不允许为了省事而让步。
流水线契约(不可配置)
以下机制是这套工作流的价值本身,任何主题都必须遵守,动了不交付:
- 优化稿必须经用户确认才能进入配音/构图/渲染(闸门);口播稿与平台标题必须过
scripts/check_prose.py硬禁项清零 - 字幕全程烧录在画面内并与配音同步;印章/标识右上角全程常驻(根层覆盖实现),是唯一水印,禁止文字水印;印章为保护区
- 画幅:横屏 1920x1080;口播时长最长约 10 分钟(短可到抖音 30 秒),超长稿先提炼压缩
- 交付物:主视频(烧录字幕)、横版封面(1920x1440,4:3)、竖版封面(1080x1440,3:4)、3-5 个平台标题、字幕/时间轴
- 交付核验(硬性,最后一步):交付前必须运行
python -X utf8 scripts/verify_output.py --dir <outputs目录>,逐项核验主视频音轨、时长对齐、双封面尺寸、标题与发布计划——任何一项 FAIL 都不交付,修复后重跑直到全部 PASS。禁止跳过或仅凭肉眼判断。 - 数据必须有来源;场景必须有入场动画与转场(除末场无退场)
品牌主题(从主题文件读取)
平台名、色板、字体、印章、签名语、封面风格、动效签名、默认配音音色,全部定义在主题文件 themes/paper-algorithm.json(默认主题,试界TryWorld 品牌)。出片前先确认要用的主题(默认 paper-algorithm,用户说「用 <主题名> 主题」或要求换品牌时切换),品牌值一律以主题文件为准,不在本文件中写死。
- 想用自己的品牌做频道:复制主题 JSON 并按
references/theme-guide.md修改(可改色板/印章/签名/字体/音色等;流水线契约不可配置) - 默认主题的视觉细则见
references/style-system.md(纸上算法视觉系统的完整定义)
配音默认音色来自主题文件:出片配音命令带 --theme <主题文件>,脚本读取主题 voice.preset 作为音色(默认主题为云希);--voice <预设名> 可覆盖(xiaoxiao 晓晓 / yunjian 云健 / yunyang 云扬 / xiaoyi 晓伊 / yunxia 云夏 / xiaobei 小贝东北话 / xiaoni 小妮陕西话),交互规则见「音色选择」章节。
启动前必读
外部依赖:渲染依赖 hyperframes 技能(
$hyperframes,或npx hyperframesCLI)与 Node.js >= 22;配音依赖 edge-tts(Python 3.10+)。$hyperframes不在本仓库内,需另行安装;未安装时出片流程无法执行。
- 读
themes/paper-algorithm.json(或用户指定的主题文件)——确认本次出片的品牌值。 - 读
references/style-system.md—— 视觉契约。编写任何 HTML/CSS 前必须先读,并作为 hyperframes 流程中的 DESIGN.md 使用。 - 读
references/workflow.md—— 生产流程与命令,按顺序执行。 - 编写构图时遵循 hyperframes skill(
$hyperframes)的全部规则。 - 生成标题前读
references/titles.md。
工作流
- 输入并通读理解:口播稿(文本或 .txt/.md 文件)与可选图片。先通读全文,理解主题、受众、核心结论与章节结构;图片缺失时跳过图片场景,不允许降级风格。
- 优化并净化脚本:按流量第一性原理优化口播稿(共鸣选题带流量、认可攒赞、槽点引评论、嘴替促转发、价值认同涨粉),再按"活人感改稿七遍"清模型腔与注水(看谁在说 → 检查推进/删注水 → 拆表演性中文 → 听中文节奏 → 清硬禁项 → 核现实 → 查结尾,固定签名保留),随后清除写作标记/结构标签(如"一、开场钩子"、"(插入截图)")——标记不得以原文出现在视频中(不朗读、不上字幕、不显示),按意图转化为实际表达。净化后正文与平台标题必须运行
scripts/check_prose.py清零硬禁项。规则见 workflow.md。 - 交付优化稿并等待确认:净化后正文先通过
$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py(硬禁项清零),再把优化后的口播稿完整展示给用户审阅,附简明优化说明(改了什么、为什么)、元素落点清单(钩子/干货/槽点/嘴替/价值收尾)与数据来源清单;未获用户确认前,禁止进入配音/构图/渲染。 - 分段:按净化后口播时长分段,每章 40-90 秒;不足 1 分钟不分章;口播目标不超过约 10 分钟,超长稿先提炼核心精华压缩再分段;标记章节标题、关键词、数据点、图片提示。
- 配音:
python "skills/tryworld-paper/scripts/tts_yunxi.py" <净化后的脚本> --out work/audio --theme "skills/tryworld-paper/themes/paper-algorithm.json"(或用户指定的主题文件)生成配音、分段时间与合并音轨。 - 时间轴:字幕时间轴来自
work/audio/sentences.json(edge-tts 句级时间戳,已带绝对时间);如需词级时间轴可用npx hyperframes transcribe(依赖 whisper,可选)。 - 场景规划:按章节规划场景与节奏(开场-讲解-数据-小结),数据/对比/流程优先规划为动态图表场景,避免大段静态文字;先声明节奏模式再写 HTML。
- 构图:把
assets/复制进项目;按 style-system.md 与 hyperframes 规则编写 16:9 主构图。每个场景必须有入场动画与转场;除末场外禁止退场动画。 - 检查:
npx hyperframes lint、npx hyperframes validate、npx hyperframes inspect --strict全部通过——错误与警告清零,禁止元素重叠、文字溢出/出画布/截断等排版错误;封面构图同样核验。 - 渲染前核验:渲染前必须按 workflow.md 的"渲染前核验清单"逐项对照本文件与 style-system.md 的全部要求,确认无误后才允许渲染;任一项不满足先修改再渲染,避免返工。
- 渲染:
npx hyperframes render --fps 30 --quality high输出主视频(先--quality draft预览确认,再 high 出片)。 - 封面:按 style-system.md 封面系统独立设计 4:3 与 3:4 静态构图(使用主题定义的封面视觉语言,与视频画面保持两套语言;禁止截取主视频画面),渲染后取帧为 PNG。
- 标题:按 titles.md 生成 3-5 个候选,标注平台推荐与命中的增长原则(共鸣/认可/槽点/嘴替/价值认同);候选标题同守硬禁词(禁冒号/破折号/翻案句/黑话/模型路标),生成后自检或跑
$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py清零。 - 交付:主视频(烧录字幕)、横竖封面、标题、字幕文件统一放入
outputs/。
质量门禁(不通过不交付)
lint/validate/inspect全部通过。- 排版:禁止元素重叠、文字溢出/出画布/截断等低级错误;
inspect --strict错误与警告清零才可交付,封面同样核验。 - 防伪间距:右上角印章与文字/内容/动画保持安全距离,禁止重叠或贴近;视频与封面均适用。
- 场景间必须转场,禁止硬切;每个场景元素必须有入场动画(hyperframes 硬性规则)。
- 动效丰富度:禁止连续 3 秒静止;每场 ≥1 ambient、≥3 层视觉、≥2 焦点;动效遵守安全护栏——不入侵字幕/印章区、同屏并发 ≤3、只用风格内变换、无诡异运动与重叠。
- 无文字水印:视频与封面禁止出现"平台名+主题名"或类似文字水印;印章是唯一水印。
- 印章常驻:印章必须以根层覆盖实现并全程可见,渲染后抽查首/中/尾帧确认。
- 对比度:正文 4.5:1,大字(24px+ 或 19px+ 粗体)3:1,只能在本风格色板内调整。
- 确定性:禁止
Math.random()/Date.now();动画 repeat 必须有限值。 - 配音:句子只允许在句号/问号/感叹号处停顿;
tts_yunxi.py会自动规整文本并按句切分,禁止句子中间产生停顿或卡顿。 - 时长:口播最长约 10 分钟;超长稿必须提炼核心精华压缩(保留主线/结论/数据亮点,砍掉重复铺垫与次要细节),时长与质量并重。
- 脚本净化:写作标记/结构标签(如"一、开场钩子")不得以原文出现在视频中——不朗读、不上字幕、不显示为画面文字,必须转化为实际表达。
- 口播优化:脚本须按流量第一性原理优化(共鸣选题/认可点赞/槽点评论/嘴替转发/价值认同涨粉),成片须具备钩子、干货、讨论点、嘴替句与价值收尾;结尾固定使用主题 JSON 的
brand.sign_off;数据须场景化"人话"解读、不冷冰冰堆数据;专业人认可价值、普通人觉得用得上看完;全程高价值,能留住观众。 - 数据真实性:口播稿所有数据点须有来源(原稿出处或标注"待核实"),确认时附数据来源清单;禁止编造数据。
- 用户确认闸门:优化后的口播稿必须先完整展示给用户审阅并获确认,未经确认禁止生成视频。
- 字幕:主视频烧录字幕并与配音同步;字幕与画面动画文字不得重复——画面展示核心内容时该句字幕可隐藏或精简,同一信息同屏只呈现一次;字幕文件同步交付。
- 数据可视化:数据/对比/流程必须用风格统一的动态图表表达(等宽数字 + 主题强调色 + 主题次级色网格,GSAP/SVG 实现),禁止大段静态文字或原图图表。
- 标题增长:每个候选标题必须命中至少一个增长原则(共鸣/认可/槽点/嘴替/价值认同),禁止为凑数生成无钩子标题。
- 渲染前核验:渲染前必须逐项对照本文件与 style-system.md/workflow.md 的全部规则并确认通过,未通过禁止渲染。
- 封面:必须独立构图设计,禁止从主视频截帧或裁切充当封面;封面采用主题定义的封面视觉,与视频画面保持两套语言,避免被平台判定为截图。
- 防 AI 味:禁止紫蓝霓虹、黑底光效、通用科技字体、机械匀速动画、空荡背景、每句整屏大字。
- 活人感:净化后口播稿正文与
titles.txt硬禁项清零——动作级禁令:翻案腔(先立误解再推翻抬价,含 9+ 变形)、三项以上同构排比、抒情借喻(抽象名词配具体动词)、动词名词化;标点:破折号全禁、冒号仅引出直接原话可用;硬停词"说白了/说穿了/先说结论";模型洞察路标;商业与模型黑话(绝对禁词 + 语境判断词两档,清单见scripts/check_prose.py);check_prose.py失败不交付。
音色选择(静默默认,按需试听)
- 默认流程不询问音色,直接用当前主题的默认音色(默认主题为云希);只有用户主动提到换声音(女声/男声/换个音色/有哪些音色等)时才进入选择。
- 进入选择时:用用户当前稿子的开头两句(约 30 字)对候选音色各合成 3-5 秒试听 mp3,列出文件让用户听完再定;禁止只用文字描述(「温暖」「活泼」)代替试听。
- 用户选定后,本次配音带
--voice <预设名>,交付物注明所用音色;用户说「以后都用这个」时记录偏好,后续流程默认该音色,不再询问。 - 推荐组合(供用户参考):资讯盘点
yunyang(播报感)、热血/体育向选题yunjian、知识讲解yunxi/xiaoxiao、方言玩梗xiaobei/xiaoni。
语音回退(按序)
- 默认:
scripts/tts_yunxi.py(edge-tts,默认云希,--voice可换预设音色,需联网)。 - 用户提供云希音频:直接导入并转写,跳过合成步骤。
- 离线兜底:
npx hyperframes tts --voice zm_yunxi(音色偏平,需告知用户差异)。
资源
themes/paper-algorithm.json:默认品牌主题(色板/印章/签名/字体/音色等全部品牌值)references/style-system.md:默认主题(纸上算法)的视觉契约(色板/字体/动效/转场/字幕/图片处理/防伪/封面)references/theme-guide.md:主题指南——如何创建自己的品牌主题references/workflow.md:详细生产流程与命令references/titles.md:平台标题规则scripts/tts_yunxi.py:配音管线(内置多音色预设,默认云希)scripts/verify_output.py:交付核验硬门禁(音轨/时长/双封面/标题/发布计划,全过才交付)scripts/check_prose.py:活人感硬禁项检查脚本(TryWorld 改造版,源自 KKKKhazix/human-writing v1.1.0,MIT;禁令上移到修辞动作级)assets/paper-grain.svg:纸纹叠加层assets/seal.svg:朱红"试界原创"印章
Files (paper-algorithm)
-
agents
-
openai.yaml 335 B
interface: display_name: "试界-纸上算法 TryWorld-Paper Algorithm" short_description: "纸上算法风格+云希配音,产出试界TryWorld AI科普视频、横竖封面与标题" default_prompt: "Use $tryworld-paper to turn this AI voiceover script into a 试界TryWorld branded horizontal video with covers and titles."
-
-
assets
-
paper-grain.svg 380 B · in bundle
-
seal.svg 945 B · in bundle
-
-
references
-
style-system.md 10 KB
# 纸上算法 Paper Algorithm —— 试界TryWorld 视觉契约 本文件是**默认主题(纸上算法)视觉细则的唯一来源**,其结构(色板四色制、字体三系统、三动效签名、转场、字幕、防伪、封面)对所有主题生效;非默认主题的品牌值以其主题文件为准。所有构图、封面、素材处理的配色、字体、动效、转场必须精确遵循,不允许临时换色、换字或添加装饰。 > **主题说明**:本文件描述的是**默认主题 `paper-algorithm`**(试界TryWorld 品牌)的视觉系统,品牌值以 `themes/paper-algorithm.json` 为准。想换自己的品牌:复制主题 JSON 并按 `references/theme-guide.md` 创建新主题——本文件的结构(色板四色制/字体三系统/三动效签名/转场/字幕/防伪/封面)是所有主题的模板,值随主题变,结构不变。 ## 设计源头 科学手稿 + 中文印刷传统(实验记录本、文献批注、古籍印章)。每一期视频都是一页"会动的算法笔记":纸面是舞台,墨迹是文字,朱红是重点,印章是签名。 ## 色板(全片锁定,4 色,禁止新增) | 色 | 值 | 用途与规则 | |---|---|---| | 纸面 | `#F4EFE4` | 背景主色;卡片、封面底色 | | 墨黑 | `#1C1916` | 主文字、线条、边框 | | 朱红 | `#C0452F` | 唯一强调色:关键词、关键数字、重点划线、印章、图表强调线 | | 墨水蓝 | `#2E5E8C` | 次级批注文字、图表辅助线;仅限这两处 | 规则:朱红一次只服务一个焦点;同一画面最多 2 处朱红。禁止渐变紫蓝、霓虹;视频画面禁止纯黑背景,封面深墨海报的深墨底色为例外(见封面系统)。 ## 字体(三系统) - 主标题/大字:思源宋体(Noto Serif SC,900/700 字重)——编译器原生支持,已实测。 - 批注/引用/小标签:ZCOOL XiaoWei(小薇宋体)或手写感字体,模拟笔注——编译器原生支持,已实测。 - 数据/坐标/来源:等宽字体(JetBrains Mono 等),用于 `Fig.1`、数据来源、编号。 - 字号:标题 60px+,正文 20px+,数据标签 16px+;display 字号 tracking -0.03~-0.05em。 - 数字一律 `font-variant-numeric: tabular-nums`。 - 禁止:Inter、Roboto、Open Sans、Noto Sans(含 SC)、Syne、Poppins、Playfair 等被 hyperframes 禁止的字体;禁止黑体默认感。 - 字体嵌入(实测结论 2026-08-03):Noto Serif SC ✅、ZCOOL XiaoWei ✅ 由编译器自动嵌入;LXGW WenKai(霞鹜文楷)❌ 无编译器映射,如确需使用必须自带 woff2 并以 `@font-face` 声明(可选,非默认)。 - 回退链:思源宋体 → ZCOOL XiaoWei(均原生可用),保持气质(衬线/楷体,不用黑体)。 ## 动效语法(全局三种签名,全片一致) 1. **墨落纸**:主入场。`gsap.from` y+40~80 + opacity 0→1,`power3.out`,0.5-0.8s。 2. **笔写入**:小元素/批注。scale 0.2→1 + rotation 3-6°,`sine.inOut`,0.4-0.6s,像一笔写出来。 3. **盖章**:数字、结论、印章。scale 1.4→1(印章 1.6→1)+ opacity,`back.out(1.4)`,0.3-0.45s,带轻微压印感。 节奏:教程型慢-中-重音;每 8-15 秒一个动效重音(数字、结论、强调句);每场景 5-15 秒;章节切换前留 0.3 秒纸面空场。同一场景内入场模式不得重复;每场至少使用 2 种签名缓动。禁止匀速线性动画、无意义的弹跳。 ## 转场(统一,不硬切) - 默认:**墨染过渡**(前场景被墨色浸染溶解,新场景从纸面浮现),可用 shader 或遮罩 + 纸纹实现。 - 轻量场景间:纸纹交叉溶解 0.4s + 墨线擦除。 - 章节级:墨染 + 0.3s 纸面空场 + 新章节标题盖印入场。 - 遵守 hyperframes 转场硬性规则:每场有入场动画;除末场外不做退场动画;转场即退场。 ## 字幕与文字强调 - 强制:主视频必须全程带字幕(烧录在画面内),不允许有无字幕的段落。 - 字幕底色:墨黑字(浅纸底上),当前句关键词染朱红。 - 逐词高亮只用于关键词(马克笔扫过/手绘圈/划线/涂鸦,用 hyperframes css-patterns),普通句子整句淡入,避免闪动疲劳。 - 数字/代码/专有名词:等宽字体 + 朱红,可盖章入场。 - 引用/注释:墨水蓝小字 + 手绘下划线,模拟批注。 - 正文 20px+,避免过小字号。 ## 图片处理(混合素材统一通道) 所有外部图片进入"纸面化"管线,按类型处理: - **截图**:嵌入米色纸面卡片(白底截图 + 纸纹 + 墨色描边 2-4px + 手绘感圆角/不规则边框),卡片可 0.5-1° 轻微旋转交错摆放。 - **AI 概念图**:整幅使用但必须叠纸纹 + 墨色 vignette,轻微降饱和。 - **图表**:重绘为本风格动态图表,规则见"动态图表与可视化"章节,不直接用原图。 - **照片**:同概念图处理,叠纸纹。 ## 动态图表与可视化(数据动起来) 原则:数据、对比、流程、时间线优先用**动态图表**表达,避免大段静态文字配解说;每个数据重音(8-15 秒一次)尽量配一个可视化。 风格(与全片一致): - 数字:等宽字体 + `tabular-nums`,朱红标出关键值/最高点。 - 网格/辅助线:墨水蓝细线,克制使用;主数据柱/线用墨黑描边 + 朱红强调。 - 载体:纸面浅色卡片/纸纹底、墨色细边框、手绘感。 动态模式(用 GSAP + SVG/CSS 实现,禁止图表库): 1. **数字滚动/盖章计数**:从 0 滚到目标值,落点时盖章强调。 2. **柱状增长**:墨迹从底部向上填充或线条生长(`power3.out`)。 3. **折线/趋势**:墨线一笔画出(笔写入签名)。 4. **进度环/比例条**:弧线或填充条表示占比,数字居中。 5. **对比卡片**:2-3 个指标并排,同一版式只变数值(视觉连续性)。 6. **流程/时间线**:步骤节点用印章/圆点 + 墨线连接,逐点亮起。 硬规则(来自 hyperframes data-in-motion): - 同一概念的多组数据放在同一视觉空间,只变数值;换视觉 = 换概念。 - 数字必须有视觉载体(填充条/环/形状),禁止孤零零一个数字。 - 禁止饼图、多轴图、6 宫格仪表盘、网格线/刻度/图例。 - 禁止 D3/Chart.js 等图表库输出;一律 GSAP + SVG/CSS。 - 图表必须重绘为本风格,禁止直接使用原图/截图。 - 数据动画出现时刻 = 口播讲到该数据的时刻,**必须**从 `sentences.json` 的句级 `start` 时间戳反推 `data-start`,禁止手动估时。所有场景的 `data-start` 同理——从 `sentences.json` 时间轴反推,不凭感觉排布;这是动画与口播衔接的唯一正确做法。 ## 防伪标识(视频与封面都必须有) - **朱红印章**:右上角**全程常驻显示**——朱红「试界原创」印章(`assets/seal.svg`)。主视频中印章用盖章动画入场(scale 1.6→1 + back.out);**封面中印章为静态可见**(无 `gsap.from`,CSS opacity 1),保证截帧 t=0 即完整画面。印章是防伪标识也是品牌记忆点。 - **保护区(硬性)**:印章(右上角)为固定保护区——文字、字幕、内容元素与动画不得进入或贴近,内容不得从印章上盖过。构图时先预留保护区,任何元素不得与印章重叠。 - 封面右上角同样放置印章(可缩小),遵守同一保护区规则。 - **实现要求(硬性)**:印章用根层常驻覆盖元素实现(根 index.html 中 `data-start="0"`、`data-duration` = 全片时长,右上角定位,z-index 高于墨染层),禁止只放在章节首幕;章节构图内不再重复放印章。 - **禁止文字水印**:视频与封面禁止出现"试界TryWorld 纸上算法"文字水印(右下角 `watermark.svg`、封面 `.wm` 文本等一律不要);印章是唯一水印。 ## 封面系统(横 4:3 + 竖 3:4,深墨海报) 封面必须与视频画面分开、独立设计构图;禁止从主视频截帧或裁切充当封面(截帧封面会降低平台流量)。 ### 封面视觉语言:深墨海报(与视频刻意区分) 封面与视频使用**两套视觉语言**:视频是浅纸面,封面是深墨海报。平台按"封面与视频画面相似度"判定截图,两套语言可避免封面被识别为截图;品牌一致性由固定元素承担(顶部栏目名小字、朱红印章、思源宋体),禁止把"试界TryWorld 纸上算法"作为水印文字叠加在封面上。 | 元素 | 规范 | |---|---| | 底色 | 深墨 `#12100D`(仅封面使用),可叠微纸纹 | | 主标题 | 纸面浅色大字 `#F4EFE4`,思源宋体;1-2 个关键词染朱红 `#C0452F` | | 朱红光晕 | 标题/数字背后用朱红径向光晕(低透明度 8-15%),禁止大面直线渐变 | | 信息卡片 | 深色卡片 `#221F1B` + 纸面色描边(透明度 15-25%),浅色文字 | | 批注小字 | 墨水蓝 `#2E5E8C` 或纸面色 70% | | 品牌元素 | 顶部"试界TryWorld · 栏目/期数"、右上角朱红印章,全部保留 | | 水印 | 只允许右上角朱红印章;禁止"试界TryWorld 纸上算法"文字水印(含 `.wm` 文本元素) | 禁止:纯黑 `#000`、霓虹紫蓝、蓝紫渐变。 ### 信息结构 三层信息结构,杜绝稀疏: 1. **顶部品牌条**:试界TryWorld · 栏目/期数,浅色小字 + 朱红点缀。 2. **中央主标题**:浅色大字 2-3 行,可 1-2 个关键词染朱红。 3. **周围 2-4 个小信息块**:深色卡片承载朱红数字、短结论、悬念句(如"5 分钟"、"3 个误区"),围绕主标题。 版式: - 横版:标题占左 60%,信息块列于右侧;右上角朱红印章。 - 竖版:标题上部 40%,信息块环绕左右;右上角朱红印章。 - 安全区:主要信息保持在画面中央约 85% 区域;横版避让右下角平台时长角标,竖版避让底部 15% 评论区/文案区。 - 封面标题短(6-12 字),细节交给信息块,避免封面文字堆叠。 -
theme-guide.md 5.2 KB
# 主题指南 · 如何把这套流水线换成你自己的品牌 本技能的视觉与品牌全部集中在**主题文件**里(`themes/<主题名>.json`)。默认主题 `paper-algorithm` 是试界TryWorld 的品牌。想用自己的品牌做频道,照本指南创建一个新主题即可——**流水线的所有质量机制(闸门、活人感门禁、字幕烧录、质量检查)原样生效,一个都不用改。** ## 一、两层契约 - **流水线契约(不可配置)**:优化稿确认闸门、活人感门禁清零、字幕全程烧录、印章/标识右上角全程常驻且为唯一水印、句级停顿规则、对比度要求、时长与画幅上限(横屏 1920x1080)、交付物规格(主视频/横竖封面/平台标题/字幕时间轴)、场景入场动画与转场要求、数据真实性。完整清单以 SKILL.md「流水线契约」章节为准。这些是这套工作流的价值本身,任何主题都必须遵守。 - **品牌主题(可配置)**:色板、字体、印章文字、平台名、签名语、封面风格、动效签名、配音音色。全部写在主题 JSON 里。 > **边界须知**:品牌参数化当前覆盖 **tryworld-paper(出片技能)**。tryworld-koubo(写稿方法论)与 tryworld-topics(选题规则)内置了试界频道的选题定位与受众画像;邮件通知主题、发布计划抬头也为试界文案。换品牌用户主要使用 paper 出片;如需 Koubo/Topics 完全贴合你的频道,需按你自己的频道定位调整其 SKILL.md 与 selection-rules.md(后续版本会参数化这些点)。 ## 二、创建你的主题(三步) 1. 复制 `themes/paper-algorithm.json` 为 `themes/<你的主题名>.json`。 2. 改里面的值(见下方可改清单)。 3. 出片时告诉 AI 使用你的主题(「用 <主题名> 主题出片」),或在会话中直接说想换品牌,AI 会引导你完成。 ## 三、可改的值 | 字段 | 说明 | 注意 | |---|---|---| | `theme` | 主题名(调用依据) | 必须改为你的主题名且与文件名一致,出片时按此名引用 | | `name` / `description` | 主题显示名与一句话描述 | 自由填写 | | `brand.platform_name` | 平台/频道名,出现在视频、封面、品牌条 | 全片统一 | | `brand.seal_text` | 印章文字(1-4 字最佳) | 需同步替换 `assets/seal.svg` 为你自己的印章 SVG | | `brand.sign_off` | 每期固定结尾签名(口播+画面同步) | 建议 50-70 字(默认主题即此长度),含召唤关注 | | `brand.cover_brand_line` | 封面顶部品牌条文案 | | | `palette.*` | 全部颜色值(background 背景 / text 主文字 / accent 唯一强调色 / secondary 次级色 / cover_background 封面深底 / cover_card 封面卡片) | 保持 4 色制;强调色一次只服务一个焦点的规则不变;`cover_background` 与 `cover.style` 使用同一深底色,两处需同步改 | | `fonts.*` | 主标题/批注/数据字体 | 只能选编译器原生支持的字体(见 style-system.md 字体嵌入实测表),非原生字体须自带 woff2 | | `assets.*` | 印章、纹理 SVG 路径 | 换成你自己的资产文件 | | `voice.engine / preset` | 默认配音引擎与音色(出片配音命令带 `--theme` 时自动生效,`--voice` 可覆盖) | 引擎当前支持 edge-tts;详见 SKILL.md 音色章节 | | `motion_signatures` | 三种动效签名(主入场/小元素/重音) | 形式可自定义,但必须是三种、全片一致、每场至少用两种 | | `transition` | 转场方式 | 场景间必须有转场,禁止硬切的规则不变 | | `cover.*` | 封面视觉语言 | 封面必须与视频画面两套语言(避免被平台判定截图)、禁止截帧当封面 | | `publish_plan.title` | 发布计划.txt 的标题行 | 替换为你自己的品牌名 | | `publish_plan.platforms[].name / .time` | 发布计划平台名与推荐时间(数组,逐条渲染) | 平台数量与顺序自定;脚本与文档自动读取 | ## 四、不可改的(动了不交付) - 任何「可配置」都不豁免流水线契约:**优化稿必须经你确认才能出片;口播稿与平台标题必须过活人感门禁清零;印章/标识右上角全程常驻且是唯一水印(不得加文字水印);字幕烧录且与配音同步;数据必须有来源**。 - 4 色制:色板值可以换,但「背景 + 主文字 + 唯一强调色 + 次级色」的四色结构与克制原则不能破坏(禁止渐变紫蓝、霓虹、大面渐变)。 - 印章保护区:右上角保护区规则、根层常驻覆盖实现方式,任何主题都必须遵守。 ## 五、质量自检(换主题后第一次出片前) - [ ] 新印章 SVG 右上角常驻、与内容保持安全距离 - [ ] 新色板下正文对比度 ≥ 4.5:1(大字 ≥ 3:1) - [ ] 新字体在编译器中正确嵌入(先跑一次 draft 渲染确认) - [ ] 签名语口播与画面同步、长度适中 - [ ] 封面与视频仍是两套视觉语言 ## 六、主题与开源 主题文件随仓库以 CC BY-SA 4.0 发布。欢迎提交你设计的新主题(PR 到 `skills/tryworld-paper/themes/` 目录,附一版 720p 示例),入选后会在 README 展示并标注作者。注意:主题里不得包含他人商标或未授权的音色克隆。 -
titles.md 3.8 KB
# 标题规则(试界TryWorld) 原则:主标题必须有**具体钩子**(数字、反常识结论、问题、悬念),禁止"AI 时代来了"这类空泛标题。封面标题要短,细节给信息块。 ## 增长第一性原理(标题与表达的核心依据) | 目标 | 第一性原理 | 在标题/表达中的落地 | |---|---|---| | 流量 | 有共鸣的选题内容 | 标题直指目标人群的真实困惑与好奇,开头即"与我有关" | | 点赞 | 内容认可 | 标题承诺明确价值,内容有"学到了/说得对"的落点,不夸大 | | 评论 | 制造槽点 | 标题留一个可争论、可补充的钩子(反常识、待反驳结论、开放问题) | | 转发 | 给人当嘴替 | 替观众说出他们想说而不会说的话,观点鲜明、立场清晰 | | 涨粉 | 价值认同 | 体现"这套方法/这个认知值得长期关注",有方法论或系列感 | 生成要求:每个候选标题至少命中一个原则,推荐命中 2 个(如流量+评论、转发+涨粉),并在候选下标注命中的原则。 ## 平台适配 | 平台 | 风格 | 字数 | 要点 | |---|---|---|---| | B站 | 专业 + 信息量 | 10-20 字 | 关键词前置,可带系列感("纸上算法"栏目) | | 抖音 | 前 5 字即钩子 | 8-15 字 | 口语化、悬念/反常识、可带情绪词 | | 小红书 | 清单/攻略句式 | 10-20 字 | 价值明确("3 个方法""1 个模型"),emoji 克制使用 | ## 生成流程 1. 从脚本提取:核心结论、最大反常识点、关键数字、目标人群。 2. 按平台各写 1-2 个候选(共 3-5 个),格式统一为 `<钩子>`(可选 `<副信息>`)。 3. 每个候选标注:推荐平台、理由、命中的增长原则。 输出到 `titles.txt` 时,候选标题行与说明行统一遵守硬禁词(见下节):说明行里的"命中"用空格或括号分隔(如 `命中 流量(共鸣)+ 涨粉(方法论感)`),不用冒号,保证整份 `titles.txt` 能一次过 `check_prose.py` 清零。 ## 公式示例 - 反常识:`大模型其实不会"思考",它只是在做下一词预测` - 数字:`5 分钟看懂 Transformer,一个数字拆穿它` - 悬念:`为什么 AI 越强,越需要"纸上算法"?` - 清单:`AI 科普 3 个误区,第 2 个几乎人人中招` ## 硬禁词(标题与正文同守) 平台标题与口播正文一样执行活人感硬禁项,动作级定义(完整清单见 `scripts/check_prose.py`): - **翻案腔**:禁先立误解再推翻抬价("不是……而是……""你以为……其实……""回头才发现"等 9+ 变形);标题空间小,判断从正面下。 - **同构排比**:三项以上整齐排比禁,两项为限。 - **抒情借喻**:不给抽象名词配具体动词("时间保管细节"类)。 - **动词名词化**:禁"实现了……的提升"式,还原成直接动词。 - **标点**:标题禁中文/英文冒号(`:` `:`)与破折号(`—` `——` `–`)。标题无"引出原话"场景,冒号一律不用(与正文的分级规则不同)。 - **硬停词**:"说白了/说穿了/先说结论"。 - **模型洞察路标**:"值得注意的是""还有一层""从某种意义上说"等。 - **商业/模型黑话**:绝对禁词 + 语境判断词两档(清单见 `scripts/check_prose.py`)。 - 改写方法:冒号换逗号或括号,破折号换逗号或句号,翻案腔拆回事实陈述。 - 标题生成后运行 `python -X utf8 "$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py" outputs/titles.txt`,失败项改写后清零。 - 说明行(推荐平台/理由/命中原则)同样避免冒号与破折号,推荐理由里的专有名词、数据与括号不受影响。 ## 反例(禁止) - `AI 时代来了`、`你必须要知道的 AI`、`震惊!`、`看完震惊了` - 无信息量、标题与内容脱节、夸大虚假承诺 -
workflow.md 21.6 KB
# 生产流程(按顺序执行) > **主题说明**:本文件以默认主题 `paper-algorithm` 描述生产流程。使用其他品牌主题时,流程步骤不变,品牌值(色板/印章/签名/字体/封面语言等)以主题文件为准,视觉细节按该主题的视觉定义执行。 环境前提:Node >= 22、FFmpeg(含 ffprobe,加入 PATH;`winget install Gyan.FFmpeg` 可安装,装后用 `npx hyperframes doctor` 验证)、Python 3.10+、`edge-tts`(`python -m pip install edge-tts`)、hyperframes CLI(`npx hyperframes`)。转写需要 whisper(`npx hyperframes transcribe` 首次使用会给出安装提示)。 ## 1. 输入、读稿理解、优化与用户确认 输入:口播稿文本(可直接粘贴或 .txt/.md 路径)+ 可选图片目录。 ### 1.1 读稿理解(强制,第一步) 先通读全稿,理解:主题是什么、讲给谁、核心结论、章节结构、每段要传达的意图。只有理解了意图,才能正确净化与补写。同时评估**选题共鸣点**:主题是否对准目标人群的真实困惑;共鸣弱时在开场钩子与标题上强化(增长原则见 titles.md)。 ### 1.2 口播稿优化(按流量第一性原理) 优化目标:让口播稿的每一部分都服务于平台行为。五项第一性原理及落地规则: 1. **流量 ← 有共鸣的选题内容**:开场 0-15 秒直接命中目标人群的共鸣点(痛点/好奇/反常识);确认选题切中受众真实处境,不做自嗨内容。 2. **点赞 ← 内容认可**:全片要有"值得认可"的干货——专业判断、清晰逻辑、具体数据;关键观点明确不模棱两可;认可点集中在数据与结论场景。 3. **评论 ← 制造槽点**:在合适位置埋 1-2 个"槽点"——可讨论、可反驳、有悬念的提问或观点(反常识结论、二选一、留白问题),引导观众发言;槽点贴合内容与品牌气质,不低俗引战。 4. **转发 ← 给人当嘴替**:写 1-2 句"观众想替自己说"的话——替观众说出痛点、表达立场或情绪;独立成句、便于截取,让人转发时能说"这就是我想说的"。 5. **涨粉 ← 价值认同**:结尾**必须使用当前主题的 `sign_off`**(默认主题为"我是试界TryWorld,持续把 AI 讲清楚,让每个普通人都看得懂、用得上。觉得这条视频有用,点个赞,也欢迎关注,谢谢大家,下期见。",见主题文件);让观众因认同价值而关注。 执行要求: - 优化后仍须通顺自然、符合口播习惯(句级停顿规则不变)。 - 优化可能引入"开场钩子""金句"等写作标记,最终统一按 1.3 净化规则清除。 - 不得编造数据:数据点必须来自原稿,或明确标注"需补充核实"。 - **时长控制**:口播目标最长约 10 分钟;原稿过长时提炼核心精华压缩——保留主线、结论、关键数据与案例,砍掉重复铺垫、次要细节与低信息量段落;压缩后的稿子同样要过双观众检验与用户确认闸门。 优化验收标准(提交确认前逐项自检): - **内容吸引人、全程高价值**:每段都有增量信息,删掉低信息量流水句;没有"为了凑时长"的内容。 - **不是冷冰冰的数据口述**:每个数据/概念都要有场景、"人话翻译"和"与观众的关系"(回答"所以呢?")。 - **有共鸣点**:对准目标人群的真实处境;专业人士认可其专业与判断,普通人觉得"以后用得上"或"值得看完"。 - **留住观众**:每 30-60 秒给一个继续看下去的理由(悬念、问题、案例、转折、钩子回收)。 - **口播自然**:像对朋友讲,不像念报告;长句拆短、术语配解释。 - **可核对交付**:优化稿须能逐句指认五类元素落点——钩子句、干货句、槽点句、嘴替句、价值收尾句(确认时附元素落点清单);压缩稿附"删除清单"(删了哪段、为什么删);每 30-60 秒的留人点须能在稿中指认。 ### 1.2.1 活人感改稿(七遍,强制) 目标:让稿子像一个见过事、查过材料、愿意把来龙去脉讲清楚的人在说话,而不是模型在完成写作任务。七遍按顺序走: 1. **看谁在说**:这段话凭什么知道这件事?哪句换一个模型也能原样写出?后者优先补信息来路、作者取舍或具体材料,没得补就删。 2. **检查有没有往前走**:给每段标一个主要作用(动作/事实/解释/例子/疑问/判断/背景/情绪),连续几段做同一件事就合并;后段只把前段换说法,删;删掉三分之一后事实与判断几乎没变,说明在注水。 3. **拆表演性中文**:删假深刻(单独截图好看、放回正文没用的句子)、假具体(无来源的时间/天气/神态/摆设/对白)、假口语("老铁""兄弟们""泡杯茶慢慢说")、抽象名词堆砌(问谁做了什么、影响了谁、钱和时间落在哪里)、比喻换场(短距离出现多套借喻先还原成本义)。 4. **听中文顺不顺**:主干早出现(先给做事的人与动作,再接时间/原因/条件);代词指向清楚;连续短句像敲桌子就合并,长句一口气没落脚处就拆开;动作/原话已经把情绪写出来,删掉后面的解释。 5. **清硬禁项(动作级)**:禁的是修辞动作不是字面。翻案腔按动作查(先立读者没有的误解再推翻抬价,外衣包括"不是……而是……""你以为……其实……""回头才发现""说到底"等 9+ 变形);三项以上同构排比;抒情借喻(抽象名词配具体动词);动词名词化("完成了对……的优化");标点:破折号全禁、冒号仅引出直接原话可用;硬停词"说白了/说穿了/先说结论";模型路标("值得注意的是""还有一层"等);黑话(绝对禁词 + 语境判断词,清单见 `scripts/check_prose.py`)。命中就拆回普通句子,不要换另一种漂亮句式。 6. **核现实**:时间、数字、身份、引语、因果与第一人称动作逐项核对;数据点无来源标"待核实",禁止编造。 7. **查结尾**:删掉最后两段各读一遍,删后更有力就提前结束;结尾不重新摘要全文;固定签名保留,正文在签名前自然收束。 检查命令(净化后正文与 `titles.txt` 各跑一次,清零才进入 1.4 闸门): ```powershell python -X utf8 "$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py" <净化后正文文件> python -X utf8 "$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py" <titles.txt> ``` `发布计划.txt`、邮件正文、文件名、数据来源清单不受标点禁令限制。 ### 1.3 脚本净化(强制,优化后执行) 识别**写作标记/结构标签**——这些内容**永远不得以原文出现在成片中**:不朗读、不上字幕、不显示为画面文字。常见类型: - 章节/结构标签:"一、开场钩子"、"二、痛点"、"三、案例"、"结论总结"、"小标题"等。 - 括号指令:"(此处插入截图)"、"(停顿)"、"(配数据)"、"【画面:...】"等。 - 写作提示词:稿子里提示"开场钩子""金句""过渡句""留悬念"等做法的文字。 净化规则(灵活处理,不机械套用): - 标签本身删除,保留其后真正的口播内容;如"一、开场钩子:今天我们来聊……"→ 只保留"今天我们来聊……"。 - 标签后没有内容时,根据对主题的理解补写实际表达(如"开场钩子"→ 写一个真正的钩子句)。 - 括号指令转化为对应内容:插数据→写出具体数据表达;配图→留给构图,不进入口播文本。 - 净化后的文本才是配音、字幕、画面文字的输入;成片中不得出现任何写作标签原文。 ### 1.4 用户确认闸门(强制) 优化与净化完成后,**先停下来**: 1. 先运行 `python -X utf8 "$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py" <净化后正文>` 清零硬禁项,再在对话中完整展示优化后的口播稿正文。 2. 附简明优化说明:改了什么、为什么(命中哪些第一性原理);附**元素落点清单**(钩子/干货/槽点/嘴替/价值收尾逐句指认)与**数据来源清单**(每个数据点 + 出处,无法核实标注"待核实")。 3. 交付前自检(双受众视角): - **专业人士**:干货密度、逻辑严谨、数据可靠、观点专业——看完觉得"内容不错、有价值"。 - **普通人**:有共鸣、听得懂、觉得"以后可能用得上"或"值得看完"——不是冷冰冰的数据口述。 - 全程无数据堆砌:每个数据/概念都要有"人话翻译"和"与观众的关系"。 4. **未获用户确认,禁止进入 ## 2 配音及后续所有制作步骤**;用户确认后再继续。 闸门的技术边界(必须知道):`.prose_pass` / `.confirmed` 是 `work/` 下绑定了 draft 内容 sha256 的普通 JSON 文件,由 `pipeline/state.py` 读写。它能防住"草稿改了但没重新过门禁"这类漂移与误操作,**但不能防住同一用户/同一进程伪造标记**——所以"停下来等用户确认"必须是真实发生在对话里的动作,不能由 agent 自己补一个 `--confirm` 或手写标记文件来代替。tests/test_gate_adversarial.py 固化了一组绕过尝试(跳过步骤 03、只传 `--confirm`、过期确认等),回归时一并跑。 口播稿约定(保证配音无生硬停顿/卡顿): - 句子以句号/问号/感叹号/省略号结束;句内不要为排版硬换行(脚本会自动把句子中间的换行合并,不会产生停顿)。 - 段落之间留空行;段落会在句号边界切分合成,停顿只出现在句子结束处。 - 脚本里的 markdown 标题行(`#`)是结构标记,不会被朗读;`**`、反引号、列表符等符号会自动去除,不要依赖它们表达语气。 - 若某句实际读起来仍不顺,检查该句标点(过长句可拆成两句)。 分段规则(口播最长约 10 分钟,按口播时长自适应): - 每章 40-90 秒口播(约 180-400 字)。 - 不足 1 分钟:不分章,单场景直接做。 - 1-3 分钟:1-3 个章节。 - 3-10 分钟:3-6 个章节。 - 原稿超过约 10 分钟:先按"提炼核心精华"压缩至 10 分钟以内(左右),再按压缩后时长分段;禁止带着超长原稿直接做片。 - 每章标记:章节标题、3-5 个关键词、2-4 个数据点、图片/图表提示、重音句。 - 章节结构建议:开场钩子(0-15s)→ 概念讲解 → 数据/示例 → 小结。每章结尾留一个可做转场的"结论句"。 产出:`plan.json`(或 Markdown 场景规划表),含每章标题、口播段落、场景数、节奏模式。 ## 2. 配音(默认云希,可选预设音色) ```powershell python "$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\tts_yunxi.py" script.txt --out work/audio ``` 输出: - `work/audio/segment-XXX.mp3`:每段落一轨 - `work/audio/narration.mp3`:合并全片 - `work/audio/segments.json`:每段 start/duration,用于章节时间轴 参数:默认 `--rate +8%`(语速偏快一档,可按内容微调 +5%~+15%)、`--voice yunxi`(默认云希;预设音色 xiaoxiao/xiaoyi/yunjian/yunyang/yunxia/xiaobei/xiaoni,也接受完整 edge-tts 音色 id)。 用户提供云希音频时:跳过本步,直接用其音频,记录章节切点。 ## 3. 字幕时间轴 字幕同步使用 `work/audio/sentences.json`(edge-tts 句级时间戳,脚本已按段落偏移换算为绝对时间)。每句含 start/end/text,用于字幕淡入与关键词高亮窗口。 可选:如需词级时间轴(更细的高亮),安装 whisper 后运行 `npx hyperframes transcribe work/audio/narration.mp3`。 **强制要求**:字幕必须烧录进最终视频画面(硬字幕),不是仅提供字幕文件;全片不允许有无字幕的段落。 ## 4. 场景规划 在写 HTML 前声明节奏,例如: `开场快-讲解慢-数据重音-小结慢-章节墨染` 每个章节 1-3 个场景:章节标题卡、讲解场景、数据卡/图片场景。数据点/对比/流程优先规划为**动态图表场景**(计数/柱状/折线/进度环/流程),避免大段静态文字配解说。规划动效丰富度:**每 3-5 秒一个可见动效、每 8-15 秒一个重音、每场 1 个 ambient、同屏并发元素 ≤3**,并标注动效禁区(字幕区/印章区)。**画面文字与字幕错位规划**:数字图表化/缩写、金句画面与字幕择一呈现,同一信息同屏只出现一次。 ## 5. 构图 - 项目结构:`npx hyperframes init <slug> --non-interactive`(或用现有项目)。 - 把 `assets/` 的纸纹、印章复制进项目。 - 主构图 `index.html`:1920x1080;多章节用子构图(sub-composition)组织。 - 先按 style-system.md 写静态布局(hero frame),再加 GSAP 入场;遵守 hyperframes 全部规则(时间轴注册、data-track-index、无 repeat:-1、无随机、场景转场等)。 - 布局以 hero frame(元素完全就位的那一帧)为准:禁止元素重叠、文字溢出/出画布/截断;入场动画的临时溢出需标 `data-layout-allow-overflow`,最终帧必须干净。 - 图表:用 GSAP + SVG/CSS 实现(禁图表库);同一概念数据保持同一版式只变数值;数字配视觉载体(填充条/环/形状);数据动画出现时刻与口播时间轴对齐。 - 字幕为强制元素:全片逐句烧录在画面内并与配音同步,但**不得与画面动画文字重复**——画面展示核心内容(标题/数据/结论/关键词)时,该句字幕可隐藏或精简;逐词时间轴驱动关键词高亮,不逐词闪动普通句子。 - 防伪:印章(右上角)全程常驻显示,用根 index.html 常驻覆盖层实现(id 如 `seal-overlay`,`data-start="0"`、`data-duration` = 全片总时长,右上角定位,z-index 高于墨染层);章节构图内禁止再放印章(避免重复)。 - 禁止文字水印:不得生成 `watermark.svg` 或 `#wm`/`.wm` 等"试界TryWorld 纸上算法"文字水印层;印章是唯一水印。 ## 6. 检查 ```powershell npx hyperframes lint npx hyperframes validate npx hyperframes inspect --strict --samples 15 ``` 规则:**错误与警告全部清零**才允许进入下一步;禁止元素重叠、文字溢出/出画布/截断等排版错误。对比度警告在色板内调整(正文 4.5:1,大字 3:1)。溢出问题优先用 max-width/字号/换行解决;装饰性元素用 `data-layout-ignore`。封面构图同样跑 lint/validate/inspect,取帧前用 `inspect --at <封面hero帧>` 复核版式。长视频建议跑 animation-map 检查节奏与死区。 ## 7. 渲染前核验(Render Gate) 最终渲染前,逐项核对以下清单,**全部通过才允许渲染**;任一项不满足,先修改再重新核验,避免返工浪费。 1. **脚本净化**:全稿无写作标记残留("一、开场钩子"、括号指令、写作提示等均不朗读、不上字幕、不显示为画面文字)。 2. **配音**:按 `--voice` 指定的音色(默认取主题 voice 字段,当前为真云希 Azure YunxiNeural)、语速默认 +8%、句级切分、无句中停顿/卡顿;`sentences.json` 时间轴与口播对得上;结尾为主题签名句(默认主题为试界TryWorld 标准收尾,见主题文件 sign_off)。 3. **字幕**:全片烧录在画面内、无字幕空段;样式为墨黑字 + 当前句关键词朱红,与配音句级同步;与画面动画文字无重复(同一信息同屏只呈现一次)。 4. **风格与动效**:4 色板无越界色、字体合规(无被禁字体)、三种签名动效;动效丰富度达标(无连续 3 秒静止、每场有 ambient、≥3 层视觉、≥2 焦点);安全护栏通过(无字幕/印章区入侵、同屏并发 ≤3、无诡异运动、无重叠);场景间有墨染转场、无硬切。**所有场景与数据动画的 `data-start` 必须从 `sentences.json` 句级 `start` 反推,禁止手动估时。** 5. **防伪**:右上角朱红"试界原创"印章全程常驻显示(抽查首/中/尾帧确认);与字幕/内容/动画保持安全距离,无重叠、不贴近;封面同样适用;视频与封面无"试界TryWorld 纸上算法"文字水印。 6. **图片与图表**:所有外部图片已纸面化处理,无原图直出;图表已重绘为本风格动态图表(GSAP/SVG、无原图截图、数字有视觉载体)。 7. **封面**:独立深墨海报构图、横 4:3 / 竖 3:4、素材与视频完全独立、不截取视频画面;封面 HTML 无 `gsap.from`(元素 CSS 默认 opacity 1,截帧 t=0 即完整画面)。 8. **工程**:`lint` / `validate` / `inspect --strict` 全部通过(错误与警告清零,无排版错误/元素重叠/文字溢出);封面构图同样核验;视频时长与口播 + 头尾留白对齐。 渲染策略:先 `--quality draft` 预览确认整体效果,再 `--quality high` 出片;draft 阶段发现问题直接修改,避免 high 渲染返工。 ## 8. 渲染主视频 ```powershell npx hyperframes render --fps 30 --quality high --output outputs/tryworld_<slug>.mp4 ``` 迭代用 `--quality draft`。确认总时长 ≈ 口播时长 + 头尾留白(通常 +2-4s)。 ## 9. 封面 封面必须独立制作,禁止从主视频截帧或裁切画面(截帧封面会降低平台流量);封面采用**深墨海报**视觉(深墨底 + 朱红光晕 + 浅色大字 + 深色卡片),与视频浅纸面画面刻意区分,避免被平台判定为截图。制作两个独立静态构图: - `covers/horizontal.html`:1920x1440(4:3) - `covers/vertical.html`:1080x1440(3:4) 构图规则见 style-system.md 封面系统,标题与信息块为封面单独设计,不复用视频画面;印章(右上角)为保护区,主标题与信息卡片不得与其重叠或贴近。封面 HTML 必须为独立构图(standalone composition,不可用 `<template>` 包裹)。推荐直接复制 `templates/cover-horizontal.html` 模板改占位符(`{{TITLE}}` / `{{SUBTITLE}}` / `{{CARDS}}` / `{{BRAND_LINE}}` / `{{DATE_LINE}}` / `{{SEAL_SVG_PATH}}`),模板已符合下方硬性规则。**硬性**:封面禁用 `gsap.from` 动画(from 初始态为透明,截帧可能得到全黑画面)——封面元素用 CSS 默认可见(opacity 1),如需入场预备态用 `gsap.set`;截帧在 t=0 即得完整画面。渲染时用 `--composition` 指定封面文件(否则默认渲染主视频 index.html)。渲染后取帧为 PNG: ```powershell npx hyperframes render --composition covers/horizontal.html --output covers/h.mp4 ffmpeg -y -i covers/h.mp4 -frames:v 1 outputs/cover_4x3.png npx hyperframes render --composition covers/vertical.html --output covers/v.mp4 ffmpeg -y -i covers/v.mp4 -frames:v 1 outputs/cover_3x4.png ``` 封面必须含:主标题、2-4 个信息块、品牌条、右上角印章。封面水印只允许印章,禁止"试界TryWorld 纸上算法"文字水印(包括 `.wm` 文本元素)。 ## 10. 标题 按 `references/titles.md` 生成 3-5 个候选,每个候选标注平台推荐与命中的增长原则(共鸣/认可/槽点/嘴替/价值认同),输出 `outputs/titles.txt`。候选标题同守硬禁词:禁冒号/破折号/翻案句/黑话/模型路标;生成后运行 `python -X utf8 "$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py" outputs/titles.txt`,失败项改写后清零。 ## 11. 交付清单与核验(硬性门禁) `outputs/` 下应有: - `tryworld_<slug>.mp4`(主视频) - `cover_4x3.png`、`cover_3x4.png` - `titles.txt`(含平台推荐) - `发布计划.txt`(四平台推荐发布时间与发布状态记录) - `narration.mp3` + `sentences.json`(配音与字幕时间轴,便于二次编辑) **交付核验(硬性,不通过不交付)**——整理完 `outputs/` 后必须运行: ```powershell python -X utf8 "$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\verify_output.py" --dir <outputs目录> ``` 逐项核验主视频音轨存在、时长与时间轴对齐、双封面尺寸(4:3 与 3:4)、标题与发布计划齐全。任何一项 FAIL:修复后重跑,直到输出「核验全部通过」才允许向用户交付。禁止跳过此步或仅凭肉眼判断(音轨缺失、封面漏渲染在画面抽帧中看不出来)。 - `plan.json`(场景规划,便于复用章节结构) ## 常见问题 - edge-tts 报错/无网络:用 `npx hyperframes tts --voice zm_yunxi` 兜底并告知用户音色差异,或改用用户提供的音频。 - 转写时间轴与章节对不上:用 segments.json 的 start/duration 校准,而不是逐句估算。 - 字体不支持:按 style-system.md 回退链换字,不换风格。 - 渲染时长异常:检查 data-duration 与 timeline 长度、音频时长;确认没有空 tween 撑时长。 - 配音出现不自然停顿/卡顿:检查脚本是否有句子中间换行或段落被硬切;脚本已按句切分,合成边界只在句号/问号/感叹号处。 - 口播稿里有"一、开场钩子"这类章节标签怎么办:不朗读、不上字幕、不显示,先按净化规则转成实际表达再进入配音与构图。 - check_prose.py 报失败项怎么办:按 1.2.1 第五遍处理——把冒号改成逗号或拆成两句、把破折号换成逗号/句号、把"不是……而是……"拆回事实陈述、把黑话与模型路标改成普通说法;改写后重跑直到清零,不要寻找另一种漂亮句式替换。 - 成片右下角出现"试界TryWorld 纸上算法"水印:检查根 index.html 是否残留 `#wm`/`assets/watermark.svg`,删除并改用印章常驻覆盖层后重新渲染。
-
-
scripts
-
check_prose.py 22.6 KB
#!/usr/bin/env python3 """检查中文成稿的硬禁令与常见模型化形状。只报警,不自动改文。 TryWorld 改造版:源自 KKKKhazix/human-writing v1.1.0 (https://github.com/KKKKhazix/human-writing, tag v1.1.0), Copyright (c) 2026 Human Writing Skill contributors,按 MIT License 使用, 完整许可文本见同目录 LICENSE-MIT。 禁令上移到修辞动作级:翻案腔按动作查(变形族列入警告层),新增同构排比/抒情借喻/ 名词化检查,冒号分级(引出原话放行、提示性冒号仍禁),新增句长变异系数与连词密度统计; 保留上游禁词清单与基础检查逻辑,检查逻辑在上游基础上扩展(见 LICENSE-MIT)。 """ from __future__ import annotations import argparse import collections import re import sys from dataclasses import dataclass from pathlib import Path HARD_STOPS = ( "说白了", "说穿了", "先说结论", ) HARD_JARGON = ( "赋能", "抓手", "商业闭环", "价值闭环", "能力沉淀", "拉通", "底层逻辑", "顶层设计", "认知跃迁", "价值释放", "能力建设", "降本增效", "内容矩阵", "全链路", "组合拳", "打开想象空间", "结构性机会", "关键命题", "深层逻辑", "技术底座", "公共底座", "技术主权", "单点风险", "主脊柱", "材料锚点", "认知增量", "迭代闭环", ) CONTEXT_JARGON = ( "沉淀", "颗粒度", "对齐", "协同", "链路", "生态位", "心智", "范式", "方法论", "核心变量", "打法", "想象空间", "闭环", "不丢", ) LYRIC_WORDS = ( "安放", "抵达", "微光", "褶皱", "丰盈", "滚烫", "轻盈", "赤裸", "剥开", ) ROAD_SIGNS = ( "更微妙的是", "还有一层", "只说对了一半", "值得注意的是", "需要指出的是", "从某种意义上说", ) ROAD_STRIP_CHARS = "。!?!? \n" FORBIDDEN_PUNCTUATION = { ":": "中文冒号", ":": "英文冒号", "—": "破折号", "–": "连接号式破折号", } PIVOT_PATTERNS = ( re.compile(r"(?:并)?不是[^。!?\n]{0,90}而是"), re.compile(r"并非[^。!?\n]{0,90}而是"), re.compile(r"不在于[^。!?\n]{0,90}而在于"), re.compile(r"与其说[^。!?\n]{0,90}(?:不如|毋宁|倒不如)"), re.compile(r"[。!?!?]\s*而是"), re.compile(r"表面(?:上)?[^。!?\n]{0,90}(?:其实|实际|实则)"), re.compile(r"看似[^。!?\n]{0,90}(?:其实|实际|实则)"), ) SEMANTIC_PIVOT_PATTERNS = ( re.compile(r"(?:总|一直|曾|都)?以为[^!?\n]{2,60}?(?:其实|才发现|才明白|才知道|后来才)"), re.compile(r"(?:总|都|一直)以为[^!?\n]{2,60}?[。,](?:可|但|其实)"), re.compile(r"回头(?:看|一看)?才(?:发现|明白|知道)"), re.compile(r"(?:并)?不是[^。!?\n]{1,40},(?:更|才)?是[^,。!?\n]"), re.compile(r"从来(?:都)?(?:不是|与[^。!?,\n]{1,12}无关)"), re.compile(r"答案(?:是否定的|恰恰相反)|恰恰相反"), re.compile(r"表面(?:上)?[^!?\n]{0,60}。[^!?\n]{0,12}(?:其实|实际|实则)"), re.compile(r"看似[^!?\n]{0,60}。[^!?\n]{0,12}(?:其实|实际|实则)"), re.compile(r"[^,。!?\n]{1,12}不重要,(?:重要|要紧)的是"), re.compile(r"真正[^,。!?\n]{0,16}的(?:,)?是"), re.compile(r"不只(?:是)?[^。!?\n]{0,90}(?:还|也)"), ) NOMINALIZATION_PATTERNS = ( re.compile(r"进行(?:了|一次|一场|着)?[^。,!?\n]{0,10}(?:调整|优化|升级|分析|讨论|沟通|梳理|复盘|迭代|探索|尝试|思考|规划|布局)"), re.compile(r"实现了?[^。,!?\n]{0,14}的?[^。,!?\n]{0,6}(?:提升|增长|突破|转变|跃升|落地)"), re.compile(r"完成了?对[^。,!?\n]{0,16}的"), re.compile(r"起到了?[^。,!?\n]{0,12}的?作用"), re.compile(r"具有[^。,!?\n]{0,10}(?:意义|价值)"), ) CONJUNCTIONS = ( "因为", "所以", "但是", "然而", "同时", "此外", "而且", "并且", "因此", "不仅", ) ROAD_SIGN_PATTERNS = ( re.compile( rf"(?:^|[。!?!?]\s*){re.escape(ROAD_SIGNS[0])}[^。!?!?\n]{{0,24}}", re.MULTILINE, ), re.compile( rf"(?:^|[。!?!?]\s*){re.escape(ROAD_SIGNS[1])}(?=(?:更|原因|问题|意思|考虑|变化|逻辑|价值|作用|风险|影响|值得|很少|不容易|常被|往往))[^。!?!?\n]{{0,24}}", re.MULTILINE, ), *( re.compile( rf"(?:^|[。!?!?]\s*){re.escape(phrase)}[^。!?!?\n]{{0,24}}", re.MULTILINE, ) for phrase in ROAD_SIGNS[2:] ), ) SOFT_MARKERS = ( "真正", "本质上", "更深层次", "归根结底", "换句话说", "不可否认", "核心是", "关键在于", "这意味着", ) REPEATED_OPENERS = ( "其实", "不过", "当然", "所以", "但是", "后来", "当时", "很多人", "问题是", "更重要的是", "说到这里", ) LEFT_BRANCH_PATTERNS = ( re.compile(r"(?:^|[。!?]\s*)在[^,。!?\n]{12,70}(?:以后|之后|之前|以前|过程中|情况下|背景下),"), re.compile(r"(?:^|[。!?]\s*)那些[^,。!?\n]{10,60}的[^,。!?\n]{2,30}[,。]"), re.compile(r"(?:^|[。!?]\s*)(?:真正|最终|最后)让[^,。!?\n]{8,70}的,是"), ) METAPHOR_FIELDS = { "温度": ("降温", "升温", "冷却", "余温", "温度最高"), "生死战争": ("杀死", "死因", "枪响", "开火", "战场", "引爆", "弹药"), "建筑灾害": ("坍塌", "崩塌", "地基", "砖头", "支柱", "废墟"), "仓储租赁": ("仓库", "库房", "租金", "取货", "入库", "库存"), "道路竞赛": ("赛道", "跑道", "岔路", "十字路口", "终点线", "门票"), "机器器官": ("齿轮", "引擎", "发动机", "血管", "骨架", "肌肉"), "海洋航行": ("蓝海", "浪潮", "潮水", "航船", "灯塔", "彼岸"), } @dataclass class Paragraph: position: int text: str han: int sentences: int def han_count(text: str) -> int: return len(re.findall(r"[\u4e00-\u9fff]", text)) def line_number(text: str, position: int) -> int: return text.count("\n", 0, position) + 1 def excerpt(value: str, width: int = 72) -> str: value = re.sub(r"\s+", " ", value).strip() return value if len(value) <= width else value[: width - 1] + "…" def mask_non_prose(text: str) -> str: """屏蔽代码、网址和机器元数据,同时保留字符位置与换行。""" def mask(match: re.Match[str]) -> str: return "".join("\n" if char == "\n" else " " for char in match.group()) patterns = ( re.compile(r"\A---\s*\n.*?\n---\s*(?:\n|\Z)", re.DOTALL), re.compile(r"```.*?```", re.DOTALL), re.compile(r"`[^`\n]*`"), re.compile(r"\]\([^\n)]*\)"), re.compile(r"https?://[^\s)>]+"), re.compile(r"<[^>\n]+>"), ) masked = text for pattern in patterns: masked = pattern.sub(mask, masked) return masked def non_overlapping_terms(text: str, terms: tuple[str, ...]): matches = [] occupied = [] for term in sorted(terms, key=len, reverse=True): for match in re.finditer(re.escape(term), text): start, end = match.span() if any(start < old_end and end > old_start for old_start, old_end in occupied): continue matches.append((start, term)) occupied.append((start, end)) return sorted(matches) def all_matches(text: str, patterns: tuple[re.Pattern[str], ...]): matches = [] for pattern in patterns: matches.extend(pattern.finditer(text)) return sorted(matches, key=lambda match: match.start()) def heavy_de_sentences(text: str): """找出主干可能被多个“的”压到后面的长句。""" matches = [] pattern = re.compile(r"[^。!?!?\n]+(?:[。!?!?]|$)") for match in pattern.finditer(text): value = match.group() if han_count(value) >= 38 and value.count("的") >= 4: matches.append(match) return matches def anaphora_runs(text: str, minimum: int = 3): """找出同一句里三个以上小句用同一个开头的排比。""" matches = [] for sentence in re.finditer(r"[^。!?!?\n]+(?:[。!?!?]|$)", text): clauses = [ clause.strip() for clause in re.split(r"[,、;,;]", sentence.group()) if han_count(clause) >= 3 ] if len(clauses) < minimum: continue run = 1 for previous, current in zip(clauses, clauses[1:]): if previous[:2] == current[:2] and re.match(r"[一-鿿]{2}", current): run += 1 if run >= minimum: matches.append(sentence) break else: run = 1 return matches def sentence_length_cv(text: str): """句长变异系数。人写的长短句差距大,模型的句长彼此接近。""" lengths = [ han_count(match.group()) for match in re.finditer(r"[^。!?!?\n]+[。!?!?]", text) if han_count(match.group()) >= 4 ] if len(lengths) < 12: return None mean = sum(lengths) / len(lengths) if mean == 0: return None variance = sum((value - mean) ** 2 for value in lengths) / len(lengths) return (variance ** 0.5) / mean, len(lengths) def bracket_highlights(text: str): """「」括起来的短语。太密说明在批量造金句。""" return list(re.finditer(r"[「『][^」』\n]{1,6}[」』]", text)) def prose_paragraphs(text: str) -> list[Paragraph]: paragraphs = [] cursor = 0 for block in re.split(r"\n\s*\n", text): position = text.find(block, cursor) cursor = max(position + len(block), cursor) clean = re.sub(r"[>*_`]", "", block).strip() if not clean or clean.startswith(("#", "http", "![", "```")): continue if re.match(r"^(?:[-+*]|\d+[.、])\s", clean): continue count = han_count(clean) if count < 4: continue sentences = max(1, len(re.findall(r"[。!?!?]", clean))) paragraphs.append(Paragraph(position, clean, count, sentences)) return paragraphs def metaphor_cluster(text: str, distance: int = 800): hits = [] for field, words in METAPHOR_FIELDS.items(): for word in words: for match in re.finditer(re.escape(word), text): hits.append((match.start(), field, word)) hits.sort() for index, (start, _, _) in enumerate(hits): window = [hit for hit in hits[index:] if hit[0] - start <= distance] fields = {hit[1] for hit in window} if len(fields) >= 3: return window, fields return None def short_streak(paragraphs: list[Paragraph], limit: int = 4): streak = [] for paragraph in paragraphs: if paragraph.han <= 24 and paragraph.sentences <= 1: streak.append(paragraph) if len(streak) >= limit: return streak else: streak = [] return None def opener_counts(paragraphs: list[Paragraph]): counts = collections.Counter() examples = {} for paragraph in paragraphs: value = paragraph.text.lstrip("“‘\"((") for opener in REPEATED_OPENERS: if value.startswith(opener): counts[opener] += 1 examples.setdefault(opener, paragraph.position) break return counts, examples def read_text(path: str) -> str: if path == "-": return sys.stdin.read() return Path(path).read_text(encoding="utf-8") def main() -> int: parser = argparse.ArgumentParser(description="检查中文成稿的硬禁令与模型化形状") parser.add_argument("path", help="Markdown 或文本文件路径。使用 - 从标准输入读取") parser.add_argument("--whitelist", type=str, default=None, help="Path to whitelist file. Each line is a regex pattern to strip from analysis.") args = parser.parse_args() try: text = read_text(args.path) except (OSError, UnicodeError) as error: print(f"无法读取稿件。{error}", file=sys.stderr) return 2 if args.whitelist: import re as _re try: wl_text = Path(args.whitelist).read_text(encoding="utf-8") wl_patterns = [line.strip() for line in wl_text.splitlines() if line.strip() and not line.startswith("#")] for pattern in wl_patterns: if pattern in (".*", ".+", ".*?", ".+?"): print("WARNING: whitelist pattern is too broad, this will clear the entire text.", file=sys.stderr) try: text = _re.sub(pattern, "", text) except _re.error as e: print(f"WARNING: invalid whitelist regex '{pattern}': {e}", file=sys.stderr) except (OSError, UnicodeError) as e: print(f"WARNING: cannot read whitelist file: {e}", file=sys.stderr) prose = mask_non_prose(text) total_han = han_count(prose) if total_han == 0: print("没有检测到汉字。", file=sys.stderr) return 2 failures = [] warnings = [] quote_colons = [] for symbol, label in FORBIDDEN_PUNCTUATION.items(): matches = list(re.finditer(re.escape(symbol), prose)) if symbol in (":", ":"): hard = [] for match in matches: tail = prose[match.end() : match.end() + 2].lstrip() if tail[:1] in ("「", "『", "“", "‘", '"'): quote_colons.append(match) else: hard.append(match) matches = hard if matches: lines = "、".join(str(line_number(text, match.start())) for match in matches[:8]) failures.append(f"{label}共 {len(matches)} 处,出现在第 {lines} 行。") if quote_colons: lines = "、".join( str(line_number(text, match.start())) for match in quote_colons[:8] ) warnings.append( f"引出原话的冒号 {len(quote_colons)} 处,第 {lines} 行。确认引号里确实是原话,且不是提示性用法。" ) stop_matches = non_overlapping_terms(prose, HARD_STOPS) for position, phrase in stop_matches: failures.append(f"硬停词,第 {line_number(text, position)} 行,{phrase}") jargon_matches = non_overlapping_terms(prose, HARD_JARGON) for position, phrase in jargon_matches: failures.append(f"黑话,第 {line_number(text, position)} 行,{phrase}") context_jargon_matches = non_overlapping_terms(prose, CONTEXT_JARGON) hard_spans = [ (position, position + len(phrase)) for position, phrase in jargon_matches ] context_jargon_matches = [ (position, phrase) for position, phrase in context_jargon_matches if not any( position < end and position + len(phrase) > start for start, end in hard_spans ) ] if context_jargon_matches: samples = "、".join( dict.fromkeys(phrase for _, phrase in context_jargon_matches) ) lines = "、".join( dict.fromkeys( str(line_number(text, position)) for position, _ in context_jargon_matches[:8] ) ) warnings.append( f"有 {len(context_jargon_matches)} 处词语需要结合语境判断。" f"第 {lines} 行出现 {samples}。本义准确时保留,用来抬价时改写。" ) road_signs = all_matches(prose, ROAD_SIGN_PATTERNS) for match in road_signs: failures.append( f"模型路标,第 {line_number(text, match.start())} 行," f"“{excerpt(match.group().lstrip(ROAD_STRIP_CHARS))}”" ) pivots = all_matches(prose, PIVOT_PATTERNS) for match in pivots: failures.append( f"禁用翻案句,第 {line_number(text, match.start())} 行," f"“{excerpt(match.group())}”" ) occupied_spans = [match.span() for match in pivots] semantic_pivots = [] for match in all_matches(prose, SEMANTIC_PIVOT_PATTERNS): if any( match.start() < end and match.end() > start for start, end in occupied_spans ): continue semantic_pivots.append(match) occupied_spans.append(match.span()) for match in semantic_pivots: warnings.append( f"疑似翻案腔变形,第 {line_number(text, match.start())} 行," f"“{excerpt(match.group(), 44)}”。先立误解再推翻就改成正面陈述,正常用法保留。" ) anaphoras = anaphora_runs(prose) for match in anaphoras[:4]: warnings.append( f"三连以上同构排比,第 {line_number(text, match.start())} 行," f"“{excerpt(match.group(), 44)}”。留两项,第三项换说法或删掉。" ) lyric_matches = non_overlapping_terms(prose, LYRIC_WORDS) if len(lyric_matches) >= 2: samples = "、".join(dict.fromkeys(term for _, term in lyric_matches)) warnings.append( f"模型偏爱的抒情词 {len(lyric_matches)} 处。{samples}。" "写具体事物时保留,给抽象概念穿衣服时删掉。" ) nominalizations = all_matches(prose, NOMINALIZATION_PATTERNS) for match in nominalizations[:4]: warnings.append( f"名词化句式,第 {line_number(text, match.start())} 行," f"“{excerpt(match.group(), 36)}”。还原成直接的动词。" ) conjunction_hits = non_overlapping_terms(prose, CONJUNCTIONS) if total_han >= 600 and len(conjunction_hits) * 1000 / total_han > 7: samples = "、".join( f"{term} {count} 次" for term, count in collections.Counter( term for _, term in conjunction_hits ).most_common(4) ) warnings.append( f"连词密度偏高,每千字 {len(conjunction_hits) * 1000 // total_han} 个。{samples}。" "中文小句靠语序和事理相接,删掉一半试试。" ) highlights = bracket_highlights(prose) highlight_limit = max(3, total_han // 700) if len(highlights) > highlight_limit: samples = "、".join(dict.fromkeys(match.group() for match in highlights[:6])) warnings.append( f"「」括起的短语共 {len(highlights)} 处。{samples}。太密说明在批量造金句。" ) cv_result = sentence_length_cv(prose) if cv_result and cv_result[0] < 0.42: warnings.append( f"全文 {cv_result[1]} 个句子长度过于接近(变异系数 {cv_result[0]:.2f})。" "人写的段落里十个字的句子会挨着四十个字的句子,放开几句,压短几句。" ) marker_matches = non_overlapping_terms(prose, SOFT_MARKERS) marker_limit = max(2, total_han // 900) if len(marker_matches) > marker_limit: samples = "、".join(dict.fromkeys(term for _, term in marker_matches)) warnings.append( f"洞察路标共 {len(marker_matches)} 处,当前提醒线为 {marker_limit} 处。" f"重点检查 {samples}。" ) left_branches = all_matches(prose, LEFT_BRANCH_PATTERNS) left_limit = max(2, total_han // 1200) if len(left_branches) > left_limit: samples = ";".join( f"第 {line_number(text, match.start())} 行“{excerpt(match.group(), 44)}”" for match in left_branches[:4] ) warnings.append( f"长前置成分共 {len(left_branches)} 处,可能让主干来得太晚。{samples}" ) dense_de = heavy_de_sentences(prose) dense_de_limit = max(1, total_han // 1500) if len(dense_de) > dense_de_limit: samples = ";".join( f"第 {line_number(text, match.start())} 行“{excerpt(match.group(), 44)}”" for match in dense_de[:4] ) warnings.append( f"有 {len(dense_de)} 个长句包含四个以上的“的”,可能要先交代人和动作。{samples}" ) paragraphs = prose_paragraphs(prose) if len(paragraphs) >= 10: one_sentence = sum(paragraph.sentences <= 1 for paragraph in paragraphs) ratio = one_sentence / len(paragraphs) if ratio >= 0.75: warnings.append( f"可识别段落中有 {ratio:.0%} 只有一句话,可能形成统一的短段鼓点。" ) streak = short_streak(paragraphs) if streak: first = streak[0] warnings.append( f"从第 {line_number(text, first.position)} 行起连续出现 {len(streak)} 个短促单句段," "检查是否在排队喊结论。" ) counts, opener_examples = opener_counts(paragraphs) repeated = [(opener, count) for opener, count in counts.items() if count >= 4] if repeated: details = "、".join(f"{opener} {count} 次" for opener, count in repeated) first_position = min(opener_examples[opener] for opener, _ in repeated) warnings.append( f"段落开场重复,从第 {line_number(text, first_position)} 行附近开始。{details}。" ) metaphors = metaphor_cluster(prose) if metaphors: window, fields = metaphors samples = "、".join(dict.fromkeys(hit[2] for hit in window)) warnings.append( f"八百字内出现 {len(fields)} 套借喻。{'、'.join(sorted(fields))}。" f"例词有 {samples}。" ) print(f"汉字数 {total_han}") print( f"翻案句 {len(pivots)},翻案腔变形 {len(semantic_pivots)}," f"同构排比 {len(anaphoras)},名词化 {len(nominalizations)}," f"黑话 {len(jargon_matches)},硬停词 {len(stop_matches)}," f"模型路标 {len(road_signs)},需辨语境词 {len(context_jargon_matches)}," f"抒情词 {len(lyric_matches)},洞察路标 {len(marker_matches)}," f"长前置成分 {len(left_branches)},重定语句 {len(dense_de)}" ) if failures: print("\n需要修改") for item in failures: print(f"- {item}") if warnings: print("\n需要人工判断") for item in warnings: print(f"- {item}") if not failures and not warnings: print("\n未发现这份检查器覆盖的问题。") return 1 if failures else 0 if __name__ == "__main__": raise SystemExit(main()) -
tts_yunxi.py 13.8 KB
#!/usr/bin/env python3 """Synthesize Chinese narration via edge-tts (multi-voice, default 真·云希). Usage: python tts_yunxi.py <script.txt> --out work/audio [--voice yunxi] [--rate +8%] [--volume +0%] [--pitch +0Hz] [--max-chars 700] Voice presets (--voice takes a preset name or any full edge-tts voice id): yunxi zh-CN-YunxiNeural male, sunny (default, brand voice) xiaoxiao zh-CN-XiaoxiaoNeural female, warm xiaoyi zh-CN-XiaoyiNeural female, lively yunjian zh-CN-YunjianNeural male, passionate yunyang zh-CN-YunyangNeural male, news anchor yunxia zh-CN-YunxiaNeural male, youthful xiaobei zh-CN-liaoning-XiaobeiNeural female, Northeastern dialect xiaoni zh-CN-shaanxi-XiaoniNeural female, Shaanxi dialect Input: UTF-8 text file. Blank-line separated paragraphs are preserved as groups. Mid-sentence line breaks are joined (no artificial pause). Segments are split ONLY at sentence endings (。!?…), so the narration never pauses or stutters in the middle of a sentence. Output: segment-XXX.mp3 one file per sentence group narration.mp3 concatenated full narration segments.json per-segment start/duration + total (for timeline math) sentences.json absolute sentence-level timings from edge-tts (caption sync) """ from __future__ import annotations import argparse import asyncio import json import os import re import shutil import subprocess import sys from pathlib import Path SENTENCE_END = "。!?…!?" def find_bin(name: str) -> Path | None: """Locate ffmpeg/ffprobe: PATH, env override, then common WinGet locations.""" which = shutil.which(name) if which: return Path(which) env_dir = os.environ.get("HYPERFRAMES_FFMPEG_DIR") or os.environ.get("FFMPEG_BIN") if env_dir: candidate = Path(env_dir) / (name + ".exe") if candidate.exists(): return candidate localappdata = os.environ.get("LOCALAPPDATA", "") winget = Path(localappdata) / "Microsoft" / "WinGet" / "Packages" if winget.exists(): for sub in sorted(winget.glob("Gyan.FFmpeg*/ffmpeg-*/bin")): candidate = sub / (name + ".exe") if candidate.exists(): return candidate return None async def synth( text: str, out_path: Path, voice: str, rate: str, volume: str, pitch: str ) -> list[dict]: """Synthesize one segment; return sentence boundaries relative to this segment.""" last_error: Exception | None = None for attempt in range(2): try: comm = _get_edge_tts().Communicate(text, voice, rate=rate, volume=volume, pitch=pitch) sentences: list[dict] = [] with open(out_path, "wb") as audio_file: async for chunk in comm.stream(): if chunk["type"] == "audio": audio_file.write(chunk["data"]) elif chunk["type"] == "SentenceBoundary": start_s = chunk["offset"] / 10_000_000 end_s = start_s + chunk["duration"] / 10_000_000 sentences.append( { "start": round(start_s, 3), "end": round(end_s, 3), "text": chunk["text"], } ) if not out_path.exists() or out_path.stat().st_size == 0: raise RuntimeError("edge-tts produced empty audio") return sentences except Exception as exc: # network hiccups are common; retry once last_error = exc await asyncio.sleep(1) raise RuntimeError(f"edge-tts failed for {out_path.name}: {last_error}") def duration_seconds(path: Path, text: str) -> tuple[float, str]: """Return (duration, source). source is ffprobe, mutagen, or estimate.""" ffprobe = find_bin("ffprobe") if ffprobe: try: result = subprocess.run( [ str(ffprobe), "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", str(path), ], capture_output=True, text=True, check=True, ) return round(float(result.stdout.strip()), 3), "ffprobe" except (subprocess.SubprocessError, ValueError): pass try: from mutagen.mp3 import MP3 return round(float(MP3(str(path)).info.length), 3), "mutagen" except ImportError: pass except Exception: pass estimate = max(0.5, round(len(text) / 3.5, 3)) print(f"warning: no ffprobe/mutagen, duration estimated for {path.name}", file=sys.stderr) return estimate, "estimate" def concat_mp3(segments: list[Path], out_path: Path) -> bool: ffmpeg = find_bin("ffmpeg") if ffmpeg is None: print( "warning: ffmpeg not found, skipping narration.mp3 concat; " "use segment files individually or install FFmpeg", file=sys.stderr, ) return False list_file = out_path.with_suffix(".list.txt") list_file.write_text( "".join(f"file '{s.resolve().as_posix()}'\n" for s in segments), encoding="utf-8", ) subprocess.run( [ str(ffmpeg), "-y", "-f", "concat", "-safe", "0", "-i", str(list_file), "-c", "copy", str(out_path), ], check=True, capture_output=True, ) return True MARKDOWN_PREFIX = re.compile(r"^\s*(#{1,6}\s*|[-*+]\s+|\d+[.、]\s*|>\s*)") def strip_markdown(line: str) -> str | None: """Strip markdown artifacts; return None for structural heading lines (not spoken).""" if re.match(r"^\s*#{1,6}\s+", line): return None line = MARKDOWN_PREFIX.sub("", line) for token in ("**", "__", "`", "~~"): line = line.replace(token, "") return line.strip() or None _edge_tts = None def _get_edge_tts(): """Lazy import edge_tts so the module can be imported without the dependency.""" global _edge_tts if _edge_tts is None: import edge_tts _edge_tts = edge_tts return _edge_tts def normalize_paragraph(paragraph: str) -> str: """Strip markdown, join mid-sentence line breaks, ensure sentence-ending punctuation.""" lines = [stripped for line in paragraph.split("\n") if (stripped := strip_markdown(line))] if not lines: return "" joined = lines[0] for line in lines[1:]: joined += line # Chinese needs no space; removes mid-sentence line-break pauses joined = re.sub(r"\s+", " ", joined).strip() if joined and joined[-1] not in SENTENCE_END: joined += "。" return joined def split_sentences(text: str) -> list[str]: parts = re.split(r"(?<=[。!?…!?])", text) return [part.strip() for part in parts if part.strip()] def build_segments(paragraphs: list[str], max_chars: int) -> list[tuple[int, str]]: """Group sentences into segments; boundaries only at paragraph or sentence ends.""" segments: list[tuple[int, str]] = [] for paragraph_index, paragraph in enumerate(paragraphs, start=1): buffer = "" for sentence in split_sentences(paragraph): if buffer and len(buffer) + len(sentence) > max_chars: segments.append((paragraph_index, buffer)) buffer = "" buffer += sentence if buffer: segments.append((paragraph_index, buffer)) return segments # 中文音色预设(实测于 edge-tts --list-voices,2026-08-28;--voice 也接受完整音色 id) VOICE_PRESETS = { "yunxi": "zh-CN-YunxiNeural", # 男 · 阳光(默认,试界品牌音色) "xiaoxiao": "zh-CN-XiaoxiaoNeural", # 女 · 温暖(新闻/小说) "xiaoyi": "zh-CN-XiaoyiNeural", # 女 · 活泼 "yunjian": "zh-CN-YunjianNeural", # 男 · 浑厚激情(体育/小说) "yunyang": "zh-CN-YunyangNeural", # 男 · 新闻播报,专业沉稳 "yunxia": "zh-CN-YunxiaNeural", # 男 · 少年感 "xiaobei": "zh-CN-liaoning-XiaobeiNeural", # 女 · 东北方言 "xiaoni": "zh-CN-shaanxi-XiaoniNeural", # 女 · 陕西方言 } DEFAULT_VOICE = "yunxi" def main() -> int: parser = argparse.ArgumentParser(description="Chinese narration pipeline (default 真·云希, multi-voice)") parser.add_argument("script", type=Path, help="UTF-8 script file (.txt/.md)") parser.add_argument("--out", type=Path, default=Path("work/audio"), help="output directory") parser.add_argument( "--voice", default=None, help="voice preset (yunxi/xiaoxiao/xiaoyi/yunjian/yunyang/yunxia/xiaobei/xiaoni) " "or a full edge-tts voice id; overrides --theme", ) parser.add_argument( "--theme", type=Path, default=None, help="theme JSON file; its voice.preset is used when --voice is omitted", ) parser.add_argument("--rate", default="+8%", help="speech rate, e.g. +5%% / +8%% / +15%%") parser.add_argument("--volume", default="+0%", help="volume, e.g. +0%%") parser.add_argument("--pitch", default="+0Hz", help="pitch, e.g. +0Hz") parser.add_argument("--max-chars", type=int, default=700, help="max chars per segment") args = parser.parse_args() theme_preset = None if args.theme is not None: if not args.theme.exists(): print(f"error: theme file not found: {args.theme}", file=sys.stderr) return 1 try: cfg = json.loads(args.theme.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: print(f"error: cannot read theme file: {exc}", file=sys.stderr) return 1 theme_voice = cfg.get("voice") or {} theme_preset = theme_voice.get("preset") engine = theme_voice.get("engine") if engine and engine != "edge-tts": print( f"warning: theme requests engine '{engine}' but this script only supports " "edge-tts; continuing with edge-tts.", file=sys.stderr, ) voice_name = args.voice or theme_preset or DEFAULT_VOICE voice = VOICE_PRESETS.get(voice_name, voice_name) if "-" not in voice: print( f"error: unknown voice '{voice_name}'. Available presets: " f"{'/'.join(VOICE_PRESETS)}; or pass a full edge-tts voice id " "(e.g. zh-CN-XiaoxiaoNeural).", file=sys.stderr, ) return 1 print(f"voice: {voice}") if not args.script.exists(): print(f"error: script file not found: {args.script}", file=sys.stderr) return 1 args.out.mkdir(parents=True, exist_ok=True) raw_text = args.script.read_text(encoding="utf-8") paragraphs = [ normalize_paragraph(p) for p in raw_text.replace("\r\n", "\n").split("\n\n") if p.strip() ] if not paragraphs: print("error: no paragraphs found (separate paragraphs with blank lines)", file=sys.stderr) return 1 segments = build_segments(paragraphs, args.max_chars) segments_meta: list[dict] = [] all_sentences: list[dict] = [] mp3s: list[Path] = [] offset = 0.0 for segment_index, (paragraph_index, text) in enumerate(segments, start=1): mp3 = args.out / f"segment-{segment_index:03d}.mp3" print(f"[{segment_index}/{len(segments)}] synthesizing {len(text)} chars ...") sentences = asyncio.run( synth(text, mp3, voice, args.rate, args.volume, args.pitch) ) duration, duration_source = duration_seconds(mp3, text) segments_meta.append( { "index": segment_index, "paragraph": paragraph_index, "file": mp3.name, "start": round(offset, 3), "duration": duration, "durationSource": duration_source, "text": text, } ) for sentence in sentences: all_sentences.append( { "segment": segment_index, "start": round(sentence["start"] + offset, 3), "end": round(sentence["end"] + offset, 3), "text": sentence["text"], } ) offset += duration mp3s.append(mp3) # edge-tts sentence boundaries can slightly overlap; clamp to avoid caption collisions. normalized_sentences: list[dict] = [] last_end = 0.0 for sentence in all_sentences: sentence["start"] = round(max(sentence["start"], last_end), 3) sentence["end"] = round(max(sentence["end"], sentence["start"]), 3) last_end = sentence["end"] normalized_sentences.append(sentence) narration = args.out / "narration.mp3" concatenated = concat_mp3(mp3s, narration) (args.out / "segments.json").write_text( json.dumps( { "segments": segments_meta, "totalDuration": round(offset, 3), "narrationConcat": concatenated, }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) (args.out / "sentences.json").write_text( json.dumps( { "sentences": normalized_sentences, "totalDuration": round(offset, 3), }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) print(json.dumps( { "segments": len(segments_meta), "totalDuration": round(offset, 3), "narration": str(narration) if concatenated else None, "warning": None if concatenated else "ffmpeg not found; narration.mp3 not concatenated", }, ensure_ascii=False, )) return 0 if __name__ == "__main__": sys.exit(main()) -
verify_output.py 7.7 KB
#!/usr/bin/env python3 """交付核验硬门禁:verify_output.py --dir <outputs 目录> 逐项检查交付物完整性,任何一项不通过则 exit 1(不交付)。 检查项:主视频存在且有音轨、时长>0 且与字幕时间轴一致、双封面存在且尺寸正确、 titles.txt 非空、发布计划.txt 存在、字幕时间轴存在。 """ import argparse import json import os import shutil import subprocess import sys from pathlib import Path def is_within_directory(path: Path, directory: Path) -> bool: """Return whether path resolves inside directory, including the directory itself.""" try: path.resolve().relative_to(directory.resolve()) return True except ValueError: return False def find_bin(name: str) -> str | None: """Locate ffmpeg/ffprobe: PATH first, then env override, then WinGet locations. Kept in sync with tts_yunxi.find_bin so both scripts resolve the same binaries. """ which = shutil.which(name) if which: return which env_dir = os.environ.get("HYPERFRAMES_FFMPEG_DIR") or os.environ.get("FFMPEG_BIN") if env_dir: candidate = Path(env_dir) / (name + ".exe") if candidate.exists(): return str(candidate) localappdata = os.environ.get("LOCALAPPDATA", "") winget = Path(localappdata) / "Microsoft" / "WinGet" / "Packages" if winget.exists(): for sub in sorted(winget.glob("Gyan.FFmpeg*/ffmpeg-*/bin")): candidate = sub / (name + ".exe") if candidate.exists(): return str(candidate) return None def ffprobe(path: Path, args: list[str]) -> str: binary = find_bin("ffprobe") if not binary: print("error: ffprobe 不可用(请安装 FFmpeg 并加入 PATH,或设置 HYPERFRAMES_FFMPEG_DIR / FFMPEG_BIN),无法执行核验。") sys.exit(2) try: r = subprocess.run( [binary, "-v", "error", *args, "-of", "default=noprint_wrappers=1:nokey=1", str(path)], capture_output=True, text=True, ) except FileNotFoundError: print("error: ffprobe 不可用(请安装 FFmpeg 并加入 PATH,或设置 HYPERFRAMES_FFMPEG_DIR / FFMPEG_BIN),无法执行核验。") sys.exit(2) return r.stdout.strip() if r.returncode == 0 else "" def _cover_brightness(path: Path) -> float | None: """Read first frame of image and return mean brightness 0-255. None if tools unavailable.""" try: from PIL import Image import statistics img = Image.open(path).convert("L").resize((64, 64)) return round(statistics.mean(img.getdata()), 1) except ImportError: pass except Exception: pass import tempfile ffmpeg = find_bin("ffmpeg") if not ffmpeg: return None try: with tempfile.NamedTemporaryFile(suffix=".gray", delete=False) as tmp: tmp_path = tmp.name subprocess.run([ffmpeg, "-y", "-i", str(path), "-vframes", "1", "-f", "rawvideo", "-pix_fmt", "gray", tmp_path], capture_output=True, check=True) data = Path(tmp_path).read_bytes() Path(tmp_path).unlink(missing_ok=True) if not data: return None sample = data[::16] return round(sum(sample) / len(sample), 1) except (subprocess.SubprocessError, OSError): return None def main() -> int: ap = argparse.ArgumentParser(description="交付核验硬门禁") ap.add_argument("--dir", type=Path, default=Path("outputs"), help="outputs 目录") ap.add_argument("--video", type=Path, default=None, help="主视频路径(outputs 中有多个 mp4 时用于指定)") args = ap.parse_args() out = args.dir failures: list[str] = [] def check(ok: bool, label: str, detail: str = "") -> None: mark = "PASS" if ok else "FAIL" print(f"[{mark}] {label}" + (f" ({detail})" if detail else "")) if not ok: failures.append(label) # 1. 主视频唯一、有视频流与音轨 videos = sorted(out.glob("*.mp4")) dur_f = 0.0 video = args.video if args.video else (videos[0] if len(videos) == 1 else None) if video is None: if len(videos) > 1: check(False, "outputs 中恰好一个主视频 mp4", f"发现 {len(videos)} 个:{'、'.join(v.name for v in videos)};删除多余文件,或用 --video 指定主视频") else: check(False, "主视频 *.mp4 存在") else: if not is_within_directory(video, out): check(False, "主视频位于 outputs 目录内", str(video)) elif not video.is_file(): check(False, "主视频存在(--video 指定的文件不存在)", str(video)) else: streams = ffprobe(video, ["-show_entries", "stream=codec_type"]) has_video = "video" in streams has_audio = "audio" in streams check(has_video, "主视频含视频流") check(has_audio, "主视频含音轨", "配音缺失即静音成片,禁止交付") dur = ffprobe(video, ["-show_entries", "format=duration"]) try: dur_f = float(dur) except ValueError: dur_f = 0.0 check(dur_f > 1, "主视频时长 > 1s", f"{dur_f:.1f}s") # 2. 字幕时间轴与主视频时长一致(±1s) tl_candidates = [out / "字幕时间轴_sentences.json", out / "sentences.json"] tl = next((f for f in tl_candidates if f.exists()), tl_candidates[0]) if tl.exists() and video: try: total = json.loads(tl.read_text(encoding="utf-8")).get("totalDuration", 0) check(abs(total - dur_f) <= 1.0, "时间轴与主视频时长对齐", f"{total:.1f}s vs {dur_f:.1f}s") except (ValueError, json.JSONDecodeError): failures.append("时间轴 JSON 无法解析") print("[FAIL] 时间轴 JSON 无法解析") else: check(tl.exists(), "字幕时间轴_sentences.json 存在") # 3. 双封面存在且尺寸正确(兼容文档约定 cover_* 与中文交付命名) cover_specs = [ (("封面_横版4x3.png", "cover_4x3.png"), (1920, 1440), "横版封面"), (("封面_竖版3x4.png", "cover_3x4.png"), (1080, 1440), "竖版封面"), ] for names, size, label in cover_specs: f = next((out / name for name in names if (out / name).exists()), out / names[0]) name = f.name if not f.exists(): check(False, f"{name} 存在") continue dim = ffprobe(f, ["-select_streams", "v:0", "-show_entries", "stream=width,height"]) nums = [int(x) for x in dim.split() if x.strip().isdigit()] ok = len(nums) >= 2 and (nums[0], nums[1]) == size check(ok, f"{name} 尺寸 {size[0]}x{size[1]}", "x".join(str(n) for n in nums[:2])) # 黑帧检测:封面第一帧不应全黑(gsap.from 竞态的兜底防线) brightness = _cover_brightness(f) if brightness is not None: check(brightness > 8.0, f"{name} 非全黑(均值亮度 {brightness:.1f})", "封面可能被截到透明帧(gsap.from 竞态);重新渲染封面" if brightness <= 8.0 else "") # 4. 标题与发布计划 titles = out / "titles.txt" check(titles.exists() and titles.stat().st_size > 50, "titles.txt 存在且非空(3-5 个标题)") check((out / "发布计划.txt").exists(), "发布计划.txt 存在") print() if failures: print(f"核验未通过:{len(failures)} 项失败 → {', '.join(failures)}") print("不满足交付条件,禁止交付。") return 1 print("核验全部通过,允许交付。") return 0 if __name__ == "__main__": sys.exit(main())
-
-
templates
-
cover-horizontal.html 3.2 KB · in bundle
-
-
themes
-
content-default.json 501 B
{ "domain": "AI", "audience": "对 AI 感兴趣的普通人", "topic_sources": ["aihot"], "topic_rules": "关注 AI 与普通人的交叉点,选题须能回答「所以呢」", "tone": "人话,不用术语堆砌,写给耳朵", "standard_length_chars": [2500, 2800], "short_length_seconds": [30, 60], "sign_off_key": "brand.sign_off", "platforms": ["小红书", "抖音", "B站", "微信视频号"], "topic_dedup_dir": "E:\\Codex口播视频", "writing_rules": "tryworld" } -
content-test-brand.json 510 B
{ "domain": "效率工具", "audience": "经常用电脑工作但效率不高的职场人", "topic_sources": ["manual"], "topic_rules": "关注工具的隐藏用法和效率提升技巧,选题须能回答「用了能省多少时间」", "tone": "实用、具体、不绕弯子", "standard_length_chars": [1800, 2200], "short_length_seconds": [30, 45], "sign_off_key": "brand.sign_off", "platforms": ["B站", "抖音"], "topic_dedup_dir": "E:\\TestVideos", "writing_rules": "tryworld" } -
paper-algorithm.json 1.8 KB
{ "theme": "paper-algorithm", "name": "纸上算法 Paper Algorithm", "description": "科学手稿 + 中文印刷传统。默认主题:纸面是舞台,墨迹是文字,朱红是重点,印章是签名。", "brand": { "platform_name": "试界TryWorld", "seal_text": "试界原创", "sign_off": "我是试界TryWorld,持续把 AI 讲清楚,让每个普通人都看得懂、用得上。觉得这条视频有用,点个赞,也欢迎关注,谢谢大家,下期见。", "cover_brand_line": "试界TryWorld · 栏目/期数" }, "palette": { "background": "#F4EFE4", "text": "#1C1916", "accent": "#C0452F", "secondary": "#2E5E8C", "cover_background": "#12100D", "cover_card": "#221F1B" }, "fonts": { "heading": "Noto Serif SC(思源宋体,900/700 字重)", "notes": "ZCOOL XiaoWei(小薇宋体)", "data": "JetBrains Mono / 等宽字体" }, "assets": { "seal": "assets/seal.svg", "grain": "assets/paper-grain.svg" }, "voice": { "engine": "edge-tts", "preset": "yunxi" }, "motion_signatures": [ "ink_drop 墨落纸(主入场)", "brush_write 笔写入(小元素/批注)", "seal_stamp 盖章(数字/结论/印章)" ], "transition": "ink_bleed 墨染过渡", "cover": { "style": "dark_poster 深墨海报(与视频浅纸面两套语言)", "background": "#12100D" }, "publish_plan": { "title": "试界TryWorld · 平台发布计划", "platforms": [ { "name": "小红书", "time": "中午 12:30" }, { "name": "抖音", "time": "晚上 19:30" }, { "name": "B站", "time": "晚上 20:30" }, { "name": "微信视频号", "time": "晚上 20:30" } ] } } -
test-brand.json 1.4 KB
{ "theme": "test-brand", "name": "Test Brand", "description": "Test theme for brand parameterization validation.", "brand": { "platform_name": "TestChannel", "seal_text": "测试原创", "sign_off": "我是TestChannel,每周拆解一个工具的隐藏用法。如果你也有效率困惑,点个赞,关注我不迷路,下期见。", "cover_brand_line": "TestChannel · 效率工具" }, "palette": { "background": "#FAFAFA", "text": "#222222", "accent": "#0066CC", "secondary": "#888888", "cover_background": "#111111", "cover_card": "#222222" }, "fonts": { "heading": "Noto Serif SC(思源宋体,900/700 字重)", "notes": "ZCOOL XiaoWei(小薇宋体)", "data": "JetBrains Mono / 等宽字体" }, "assets": { "seal": "assets/seal.svg", "grain": "assets/paper-grain.svg" }, "voice": { "engine": "edge-tts", "preset": "xiaoxiao" }, "motion_signatures": [ "ink_drop 墨落纸(主入场)", "brush_write 笔写入(小元素/批注)", "seal_stamp 盖章(数字/结论/印章)" ], "transition": "ink_bleed 墨染过渡", "cover": { "style": "dark_poster 深墨海报(与视频浅纸面两套语言)", "background": "#111111" }, "publish_plan": { "title": "TestChannel · 平台发布计划", "platforms": [ { "name": "B站", "time": "晚上 20:00" }, { "name": "抖音", "time": "晚上 18:00" } ] } }
-
-
.gitignore 19 B · in bundle
-
LICENSE-MIT 1.5 KB · in bundle
-
NOTICE.md 1005 B
# NOTICE 本技能是 [TryWorld2026/paper-algorithm](https://github.com/TryWorld2026/paper-algorithm) 仓库的一部分,采用 **CC BY-SA 4.0** 许可。 使用与二创时请保留本声明与上游链接,并同样以 CC BY-SA 4.0 发布衍生作品。 本技能内的 `scripts/check_prose.py` 源自 [KKKKhazix/human-writing](https://github.com/KKKKhazix/human-writing) v1.1.0(Copyright (c) 2026 Human Writing Skill contributors),按 **MIT License** 使用与改造,完整许可文本见 `LICENSE-MIT`。 This skill is part of the [TryWorld2026/paper-algorithm](https://github.com/TryWorld2026/paper-algorithm) repository, licensed under **CC BY-SA 4.0**. Derivative works must retain this notice, credit the upstream, and be released under the same license. `scripts/check_prose.py` is derived from [KKKKhazix/human-writing](https://github.com/KKKKhazix/human-writing) v1.1.0 (Copyright (c) 2026 Human Writing Skill contributors), used under the **MIT License** — see `LICENSE-MIT`. -
README.md 5.6 KB
# 试界-纸上算法 TryWorld-Paper Algorithm > 一页会动的算法笔记 —— 把 AI 知识口播稿变成风格统一的横屏视频、横竖封面与平台标题。     ## 这是什么 `tryworld-paper`(试界-纸上算法 TryWorld-Paper Algorithm)是试界TryWorld 的视频生产 Skill:输入一篇中文 AI 口播稿(可附图片),自动产出: - **横屏主视频**(16:9,口播最长约 10 分钟)—— 纸上算法风格,真云希配音,全程烧录字幕;超长稿提炼核心精华压缩 - **横版封面 + 竖版封面**(1920x1440 横 4:3 / 1080x1440 竖 3:4)—— 深墨海报风(与视频浅纸面两套视觉语言),标题 + 信息块,适配 B站/抖音/小红书 - **3-5 个平台标题** —— 按 B站/抖音/小红书分别适配 - 右上角全程常驻的"试界原创"防伪印章 - 每期固定的默认结尾签名(试界TryWorld 标准收尾,口播+画面同步) ## 设计契约:纸上算法 Paper Algorithm 设计源头:**科学手稿 + 中文印刷传统**。每一期视频都是一页"会动的算法笔记",纸面是舞台,墨迹是文字,朱红是重点,印章是签名。 ### 色板(4 色锁定,不新增) | 色 | 值 | 用途 | |---|---|---| | 纸面 | `#F4EFE4` | 背景主色 | | 墨黑 | `#1C1916` | 主文字、线条 | | 朱红 | `#C0452F` | 唯一强调色:关键词、数字、印章 | | 墨水蓝 | `#2E5E8C` | 次级批注、图表线 | ### 字体 - 主标题:思源宋体(Noto Serif SC) - 批注/引用:ZCOOL XiaoWei(默认);霞鹜文楷(LXGW WenKai)如需使用须自带 woff2 - 数据/坐标:等宽字体(JetBrains Mono) ### 动效签名(全片统一) | 签名 | 感觉 | 缓动 | |---|---|---| | 墨落纸 | 主入场 | `power3.out` | | 笔写入 | 批注/小元素 | `sine.inOut` | | 盖章 | 数字/结论/印章 | `back.out(1.4)` | 转场统一为**墨染过渡**:前场景被墨色浸染溶解,新场景从纸面浮现;章节间留纸面空场。 ### 动态图表 数据/对比/流程用风格统一的动态图表表达:数字滚动盖章、柱状墨迹增长、折线一笔描画、进度环、对比卡片;等宽数字 + 朱红强调 + 墨水蓝网格,GSAP/SVG 实现,避免大段静态文字配解说。 ## 配音质量保障 - **多音色**:默认云希(Azure YunxiNeural,试界品牌音色),`--voice` 可换预设(xiaoxiao 晓晓 / yunjian 云健 / yunyang 云扬 / xiaoyi 晓伊 / yunxia 云夏 / xiaobei 小贝东北话 / xiaoni 小妮陕西话),也接受完整 edge-tts 音色 id;本地合成,无需 API Key - **默认语速** +8%,可按内容微调 +5%~+15% - **防卡顿机制**:文本自动规整(清除 markdown、合并句中换行),按句切分合成——停顿只允许出现在句号/问号/感叹号处,句子中间永不硬断 - 句级时间戳(`sentences.json`)直接驱动字幕同步,不依赖 whisper ## 工作流 ```text 口播稿 → 读稿理解 → 第一性原理优化 → 活人感改稿七遍 → 净化写作标记 → 硬禁项检查(check_prose.py 清零) → 用户确认优化稿 → 句级切分 → 云希配音 → 句级时间轴 → 章节/场景规划 → HyperFrames 构图 → lint/validate/inspect --strict → 渲染前核验 → 渲染 16:9 主视频 → 横竖封面 → 平台标题 → 交付核验(verify_output.py)→ outputs/ 交付 ``` ## 使用方式 在 Codex 会话中给出口播稿(纯文字或 .txt/.md,可附图片): ```text 用 $tryworld-paper 把这篇口播稿做成视频,加上横竖封面和标题。 ``` 可选输入:图片目录(AI 概念图 / 截图 / 图表,会自动"纸面化"处理)。 ## 目录结构 ```text tryworld-paper/ ├── SKILL.md # 主说明:品牌契约、工作流、质量门禁 ├── agents/ │ └── openai.yaml # UI 元数据 ├── references/ │ ├── style-system.md # 视觉契约(色板/字体/动效/封面) │ ├── workflow.md # 生产流程与命令 │ └── titles.md # 平台标题规则 ├── scripts/ │ ├── tts_yunxi.py # 云希配音管线(句级切分 + 拼接 + 时间轴) │ ├── verify_output.py # 交付核验硬门禁(音轨/时长/双封面/标题/发布计划) │ └── check_prose.py # 活人感硬禁项检查(源自 human-writing v1.1.0,MIT) └── assets/ ├── paper-grain.svg # 纸纹叠加层 └── seal.svg # 朱红"试界原创"印章 ``` ## 环境要求 - Node.js >= 22 - FFmpeg(含 ffprobe,加入 PATH;`winget install Gyan.FFmpeg`) - Python 3.10+,`pip install edge-tts` - HyperFrames CLI(`npx hyperframes`) 运行 `npx hyperframes doctor` 可一键检查环境。 ## 防伪标识 视频与封面统一携带: - 右上角全程常驻朱红**试界原创**印章(保护区,内容不得覆盖) - 不添加"试界TryWorld 纸上算法"文字水印,印章是唯一水印 ## 许可 本仓库采用知识共享 **署名-相同方式共享 4.0 国际(CC BY-SA 4.0)**。其中 `scripts/check_prose.py` 源自 [KKKKhazix/human-writing](https://github.com/KKKKhazix/human-writing) v1.1.0(禁令上移到修辞动作级,检测器补盲区降误伤),按 **MIT License** 使用与改造,版权归原作者所有,完整声明见 [LICENSE-MIT](LICENSE-MIT)。 -
SKILL.md 13.7 KB
--- name: tryworld-paper description: Create branded 试界TryWorld AI-knowledge videos in the fixed "Paper Algorithm" (纸上算法) style with HyperFrames. Input is a Chinese voiceover script and optional images; output is a 16:9 horizontal video with Azure YunxiNeural (真·云希) voiceover, word-synced captions, ink/paper animations and unified transitions, an always-visible anti-counterfeit red seal, plus horizontal (4:3) and vertical (3:4) cover images and platform-optimized titles for Bilibili/Douyin/Xiaohongshu. Brand values are theme-driven (themes/paper-algorithm.json by default; custom brand themes supported — see references/theme-guide.md). Use when the user provides an AI 科普/教程/内容解读/知识分享 script and wants a TryWorld/试界 branded video, asks for the 纸上算法/Paper Algorithm style, wants a consistent script-to-video pipeline with covers and titles, or wants this pipeline with their own brand theme. --- # TryWorld-Paper Algorithm(试界-纸上算法) 按当前主题的品牌制作 AI 知识类视频(默认主题为试界TryWorld 的纸上算法)。所有产出必须遵守本文件与 `references/style-system.md` 锁定的设计系统,任何一条都不允许为了省事而让步。 ## 流水线契约(不可配置) 以下机制是这套工作流的价值本身,任何主题都必须遵守,动了不交付: - 优化稿必须经用户确认才能进入配音/构图/渲染(闸门);口播稿与平台标题必须过 `scripts/check_prose.py` 硬禁项清零 - 字幕全程烧录在画面内并与配音同步;印章/标识右上角全程常驻(根层覆盖实现),是唯一水印,禁止文字水印;印章为保护区 - 画幅:横屏 1920x1080;口播时长最长约 10 分钟(短可到抖音 30 秒),超长稿先提炼压缩 - 交付物:主视频(烧录字幕)、横版封面(1920x1440,4:3)、竖版封面(1080x1440,3:4)、3-5 个平台标题、字幕/时间轴 - **交付核验(硬性,最后一步)**:交付前必须运行 `python -X utf8 scripts/verify_output.py --dir <outputs目录>`,逐项核验主视频音轨、时长对齐、双封面尺寸、标题与发布计划——任何一项 FAIL 都不交付,修复后重跑直到全部 PASS。禁止跳过或仅凭肉眼判断。 - 数据必须有来源;场景必须有入场动画与转场(除末场无退场) ## 品牌主题(从主题文件读取) 平台名、色板、字体、印章、签名语、封面风格、动效签名、默认配音音色,全部定义在主题文件 **`themes/paper-algorithm.json`**(默认主题,试界TryWorld 品牌)。出片前先确认要用的主题(默认 `paper-algorithm`,用户说「用 <主题名> 主题」或要求换品牌时切换),品牌值一律以主题文件为准,不在本文件中写死。 - 想用自己的品牌做频道:复制主题 JSON 并按 `references/theme-guide.md` 修改(可改色板/印章/签名/字体/音色等;流水线契约不可配置) - 默认主题的视觉细则见 `references/style-system.md`(纸上算法视觉系统的完整定义) 配音默认音色来自主题文件:出片配音命令带 `--theme <主题文件>`,脚本读取主题 `voice.preset` 作为音色(默认主题为云希);`--voice <预设名>` 可覆盖(xiaoxiao 晓晓 / yunjian 云健 / yunyang 云扬 / xiaoyi 晓伊 / yunxia 云夏 / xiaobei 小贝东北话 / xiaoni 小妮陕西话),交互规则见「音色选择」章节。 ## 启动前必读 > **外部依赖**:渲染依赖 hyperframes 技能(`$hyperframes`,或 `npx hyperframes` CLI)与 Node.js >= 22;配音依赖 edge-tts(Python 3.10+)。`$hyperframes` 不在本仓库内,需另行安装;未安装时出片流程无法执行。 1. 读 `themes/paper-algorithm.json`(或用户指定的主题文件)——确认本次出片的品牌值。 2. 读 `references/style-system.md` —— 视觉契约。编写任何 HTML/CSS 前必须先读,并作为 hyperframes 流程中的 DESIGN.md 使用。 3. 读 `references/workflow.md` —— 生产流程与命令,按顺序执行。 4. 编写构图时遵循 hyperframes skill(`$hyperframes`)的全部规则。 5. 生成标题前读 `references/titles.md`。 ## 工作流 1. **输入并通读理解**:口播稿(文本或 .txt/.md 文件)与可选图片。先通读全文,理解主题、受众、核心结论与章节结构;图片缺失时跳过图片场景,不允许降级风格。 2. **优化并净化脚本**:按流量第一性原理优化口播稿(共鸣选题带流量、认可攒赞、槽点引评论、嘴替促转发、价值认同涨粉),再按"活人感改稿七遍"清模型腔与注水(看谁在说 → 检查推进/删注水 → 拆表演性中文 → 听中文节奏 → 清硬禁项 → 核现实 → 查结尾,固定签名保留),随后清除写作标记/结构标签(如"一、开场钩子"、"(插入截图)")——标记不得以原文出现在视频中(不朗读、不上字幕、不显示),按意图转化为实际表达。净化后正文与平台标题必须运行 `scripts/check_prose.py` 清零硬禁项。规则见 workflow.md。 3. **交付优化稿并等待确认**:净化后正文先通过 `$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py`(硬禁项清零),再把优化后的口播稿完整展示给用户审阅,附简明优化说明(改了什么、为什么)、元素落点清单(钩子/干货/槽点/嘴替/价值收尾)与数据来源清单;**未获用户确认前,禁止进入配音/构图/渲染**。 4. **分段**:按净化后口播时长分段,每章 40-90 秒;不足 1 分钟不分章;口播目标不超过约 10 分钟,超长稿先提炼核心精华压缩再分段;标记章节标题、关键词、数据点、图片提示。 5. **配音**:`python "skills/tryworld-paper/scripts/tts_yunxi.py" <净化后的脚本> --out work/audio --theme "skills/tryworld-paper/themes/paper-algorithm.json"`(或用户指定的主题文件)生成配音、分段时间与合并音轨。 6. **时间轴**:字幕时间轴来自 `work/audio/sentences.json`(edge-tts 句级时间戳,已带绝对时间);如需词级时间轴可用 `npx hyperframes transcribe`(依赖 whisper,可选)。 7. **场景规划**:按章节规划场景与节奏(开场-讲解-数据-小结),数据/对比/流程优先规划为动态图表场景,避免大段静态文字;先声明节奏模式再写 HTML。 8. **构图**:把 `assets/` 复制进项目;按 style-system.md 与 hyperframes 规则编写 16:9 主构图。每个场景必须有入场动画与转场;除末场外禁止退场动画。 9. **检查**:`npx hyperframes lint`、`npx hyperframes validate`、`npx hyperframes inspect --strict` 全部通过——错误与警告清零,禁止元素重叠、文字溢出/出画布/截断等排版错误;封面构图同样核验。 10. **渲染前核验**:渲染前必须按 workflow.md 的"渲染前核验清单"逐项对照本文件与 style-system.md 的全部要求,确认无误后才允许渲染;任一项不满足先修改再渲染,避免返工。 11. **渲染**:`npx hyperframes render --fps 30 --quality high` 输出主视频(先 `--quality draft` 预览确认,再 high 出片)。 12. **封面**:按 style-system.md 封面系统独立设计 4:3 与 3:4 静态构图(使用主题定义的封面视觉语言,与视频画面保持两套语言;禁止截取主视频画面),渲染后取帧为 PNG。 13. **标题**:按 titles.md 生成 3-5 个候选,标注平台推荐与命中的增长原则(共鸣/认可/槽点/嘴替/价值认同);候选标题同守硬禁词(禁冒号/破折号/翻案句/黑话/模型路标),生成后自检或跑 `$env:USERPROFILE\.agents\skills\tryworld-paper\scripts\check_prose.py` 清零。 14. **交付**:主视频(烧录字幕)、横竖封面、标题、字幕文件统一放入 `outputs/`。 ## 质量门禁(不通过不交付) - `lint` / `validate` / `inspect` 全部通过。 - 排版:禁止元素重叠、文字溢出/出画布/截断等低级错误;`inspect --strict` 错误与警告清零才可交付,封面同样核验。 - 防伪间距:右上角印章与文字/内容/动画保持安全距离,禁止重叠或贴近;视频与封面均适用。 - 场景间必须转场,禁止硬切;每个场景元素必须有入场动画(hyperframes 硬性规则)。 - 动效丰富度:禁止连续 3 秒静止;每场 ≥1 ambient、≥3 层视觉、≥2 焦点;动效遵守安全护栏——不入侵字幕/印章区、同屏并发 ≤3、只用风格内变换、无诡异运动与重叠。 - 无文字水印:视频与封面禁止出现"平台名+主题名"或类似文字水印;印章是唯一水印。 - 印章常驻:印章必须以根层覆盖实现并全程可见,渲染后抽查首/中/尾帧确认。 - 对比度:正文 4.5:1,大字(24px+ 或 19px+ 粗体)3:1,只能在本风格色板内调整。 - 确定性:禁止 `Math.random()` / `Date.now()`;动画 repeat 必须有限值。 - 配音:句子只允许在句号/问号/感叹号处停顿;`tts_yunxi.py` 会自动规整文本并按句切分,禁止句子中间产生停顿或卡顿。 - 时长:口播最长约 10 分钟;超长稿必须提炼核心精华压缩(保留主线/结论/数据亮点,砍掉重复铺垫与次要细节),时长与质量并重。 - 脚本净化:写作标记/结构标签(如"一、开场钩子")不得以原文出现在视频中——不朗读、不上字幕、不显示为画面文字,必须转化为实际表达。 - 口播优化:脚本须按流量第一性原理优化(共鸣选题/认可点赞/槽点评论/嘴替转发/价值认同涨粉),成片须具备钩子、干货、讨论点、嘴替句与价值收尾;结尾固定使用主题 JSON 的 `brand.sign_off`;数据须场景化"人话"解读、不冷冰冰堆数据;专业人认可价值、普通人觉得用得上看完;全程高价值,能留住观众。 - 数据真实性:口播稿所有数据点须有来源(原稿出处或标注"待核实"),确认时附数据来源清单;禁止编造数据。 - 用户确认闸门:优化后的口播稿必须先完整展示给用户审阅并获确认,未经确认禁止生成视频。 - 字幕:主视频烧录字幕并与配音同步;字幕与画面动画文字不得重复——画面展示核心内容时该句字幕可隐藏或精简,同一信息同屏只呈现一次;字幕文件同步交付。 - 数据可视化:数据/对比/流程必须用风格统一的动态图表表达(等宽数字 + 主题强调色 + 主题次级色网格,GSAP/SVG 实现),禁止大段静态文字或原图图表。 - 标题增长:每个候选标题必须命中至少一个增长原则(共鸣/认可/槽点/嘴替/价值认同),禁止为凑数生成无钩子标题。 - 渲染前核验:渲染前必须逐项对照本文件与 style-system.md/workflow.md 的全部规则并确认通过,未通过禁止渲染。 - 封面:必须独立构图设计,禁止从主视频截帧或裁切充当封面;封面采用主题定义的封面视觉,与视频画面保持两套语言,避免被平台判定为截图。 - 防 AI 味:禁止紫蓝霓虹、黑底光效、通用科技字体、机械匀速动画、空荡背景、每句整屏大字。 - 活人感:净化后口播稿正文与 `titles.txt` 硬禁项清零——动作级禁令:翻案腔(先立误解再推翻抬价,含 9+ 变形)、三项以上同构排比、抒情借喻(抽象名词配具体动词)、动词名词化;标点:破折号全禁、冒号仅引出直接原话可用;硬停词"说白了/说穿了/先说结论";模型洞察路标;商业与模型黑话(绝对禁词 + 语境判断词两档,清单见 `scripts/check_prose.py`);`check_prose.py` 失败不交付。 ## 音色选择(静默默认,按需试听) - 默认流程**不询问音色**,直接用当前主题的默认音色(默认主题为云希);只有用户主动提到换声音(女声/男声/换个音色/有哪些音色等)时才进入选择。 - 进入选择时:用用户当前稿子的开头两句(约 30 字)对候选音色各合成 3-5 秒试听 mp3,列出文件让用户听完再定;禁止只用文字描述(「温暖」「活泼」)代替试听。 - 用户选定后,本次配音带 `--voice <预设名>`,交付物注明所用音色;用户说「以后都用这个」时记录偏好,后续流程默认该音色,不再询问。 - 推荐组合(供用户参考):资讯盘点 `yunyang`(播报感)、热血/体育向选题 `yunjian`、知识讲解 `yunxi`/`xiaoxiao`、方言玩梗 `xiaobei`/`xiaoni`。 ## 语音回退(按序) 1. 默认:`scripts/tts_yunxi.py`(edge-tts,默认云希,`--voice` 可换预设音色,需联网)。 2. 用户提供云希音频:直接导入并转写,跳过合成步骤。 3. 离线兜底:`npx hyperframes tts --voice zm_yunxi`(音色偏平,需告知用户差异)。 ## 资源 - `themes/paper-algorithm.json`:默认品牌主题(色板/印章/签名/字体/音色等全部品牌值) - `references/style-system.md`:默认主题(纸上算法)的视觉契约(色板/字体/动效/转场/字幕/图片处理/防伪/封面) - `references/theme-guide.md`:主题指南——如何创建自己的品牌主题 - `references/workflow.md`:详细生产流程与命令 - `references/titles.md`:平台标题规则 - `scripts/tts_yunxi.py`:配音管线(内置多音色预设,默认云希) - `scripts/verify_output.py`:交付核验硬门禁(音轨/时长/双封面/标题/发布计划,全过才交付) - `scripts/check_prose.py`:活人感硬禁项检查脚本(TryWorld 改造版,源自 KKKKhazix/human-writing v1.1.0,MIT;禁令上移到修辞动作级) - `assets/paper-grain.svg`:纸纹叠加层 - `assets/seal.svg`:朱红"试界原创"印章
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.