create-dwy 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/index.js +1 -1
  3. package/package.json +3 -2
  4. package/src/searchable-select.js +419 -0
  5. package/src/sync-all.js +313 -65
  6. package/src/sync-global-skills.js +167 -0
  7. package/src/sync.js +59 -43
  8. package/templates/ai-tools/CHANGELOG.md +210 -0
  9. package/templates/ai-tools/VERSION +1 -0
  10. package/templates/ai-tools/hook-manifests/hooks.json +7 -0
  11. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
  12. package/templates/ai-tools/rules/Android/dwy-android-core.md +280 -5
  13. package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
  14. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
  15. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
  16. package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
  17. package/templates/ai-tools/rules/Python/dwy-python-core.md +47 -0
  18. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +55 -0
  19. package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
  20. package/templates/ai-tools/rules/iOS/dwy-swift-core.md +46 -0
  21. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
  22. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +128 -0
  23. package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
  24. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +65 -0
  25. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/assets/appstore-submission.yaml +43 -0
  26. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/api-workflow.md +17 -0
  27. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/in-app-purchase-review.md +16 -0
  28. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-workflow.md +16 -0
  29. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/scripts/appstore_config.sh +140 -0
  30. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +11 -8
  31. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -1
  32. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +2 -1
  33. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +24 -4
  34. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +2 -1
  35. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +2 -1
  36. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +82 -0
  37. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -1
  38. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -2
  39. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/monitor-notify.md +2 -2
  40. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +1 -1
  41. package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md +2 -2
  42. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
  43. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
  44. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
  45. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
  46. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/README.md +57 -0
  47. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +147 -0
  48. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +43 -0
  49. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/config.example.yaml +33 -0
  50. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/global_config.py +89 -0
  51. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/setup_config.py +175 -0
  52. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +339 -0
  53. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +127 -0
  54. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/assets/cover-style-master-v1.png +0 -0
  55. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +37 -0
  56. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-posting-times.md +193 -0
  57. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/SKILL.md +42 -0
  58. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates/topic-backlog.template.md +50 -0
  59. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates//351/200/211/351/242/230/346/261/240.template.md +16 -0
  60. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/SKILL.md +106 -0
  61. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates/channel-profile.template.md +52 -0
  62. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates//344/272/247/345/223/201/345/215/241.template.md +132 -0
  63. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/SKILL.md +60 -0
  64. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/build_facts_html.py +430 -0
  65. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_html.py +1287 -0
  66. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_report.py +189 -0
  67. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/templates//344/272/213/345/256/236/350/265/204/346/226/231/345/272/223.template.md +55 -0
  68. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/SKILL.md +36 -0
  69. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates/README.md +10 -0
  70. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//344/272/213/345/256/236.template.md +82 -0
  71. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//345/215/225/351/233/206/346/226/207/346/241/243.template.md +175 -0
  72. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//350/204/232/346/234/254.template.md +69 -0
  73. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//351/252/250/346/236/266-v1.1.md +174 -0
  74. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0050-design/SKILL.md +55 -0
  75. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0060-assets/SKILL.md +29 -0
  76. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/SKILL.md +42 -0
  77. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/scripts/validate_shots.py +207 -0
  78. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/shots.schema.json +186 -0
  79. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/345/214/205.template.md +1 -0
  80. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/350/257/264/346/230/216.md +15 -0
  81. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//346/210/220/347/211/207/345/214/205.template.md +49 -0
  82. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0080-factory/SKILL.md +58 -0
  83. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/SKILL.md +424 -0
  84. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/evals.md +41 -0
  85. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/CONTRACT.md +123 -0
  86. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/SKILL.md +81 -0
  87. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow//346/211/247/350/241/214/346/270/205/345/215/225.template.md +60 -0
  88. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/SKILL.md +78 -0
  89. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/agents/openai.yaml +4 -0
  90. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/knowledge-town-paper-film-v1.json +80 -0
  91. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/AudioTracks.tsx +20 -0
  92. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CaptionLayer.tsx +18 -0
  93. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CueTimeline.ts +24 -0
  94. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/KnowledgeTownComposition.tsx +21 -0
  95. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/cinematic-direction.md +73 -0
  96. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/sticker-asset-production.md +30 -0
  97. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/style-guide.md +43 -0
  98. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/build_cue_timeline.py +75 -0
  99. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_cue_timeline.py +68 -0
  100. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_style_preset.py +22 -0
  101. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
  102. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/references/chapter-01-mirrors.md +112 -0
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env python3
2
+ """豆包声音复刻 2.0:HTTP Chunked 单向流式 TTS,写出完整音频文件。
3
+
4
+ 配置只从 ~/.dwy/config.yaml 的 doubao_tts section 读取。
5
+ 禁止向 skill / 项目目录写密钥或音色 ID。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import base64
12
+ import json
13
+ import re
14
+ import sys
15
+ import uuid
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import Any
19
+ from urllib.error import HTTPError, URLError
20
+ from urllib.request import Request, urlopen
21
+
22
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
23
+
24
+ from global_config import CONFIG_PATH, SECTION_DOUBAO_TTS, get_section # noqa: E402
25
+
26
+ # 流式帧:业务成功片;文档约定结束码 20000000
27
+ CODE_AUDIO = 0
28
+ CODE_DONE = 20000000
29
+
30
+ # 财经语境关键词(小写匹配前会保留中文原文)
31
+ FINANCE_KEYWORDS = (
32
+ "股市",
33
+ "基金",
34
+ "理财",
35
+ "财经",
36
+ "利率",
37
+ "涨跌",
38
+ "a股",
39
+ "港股",
40
+ "美股",
41
+ "纳斯达克",
42
+ "投资",
43
+ "证券",
44
+ "债券",
45
+ "期货",
46
+ "外汇",
47
+ "央行",
48
+ "gdp",
49
+ "cpi",
50
+ "通胀",
51
+ "宏观",
52
+ "财报",
53
+ "估值",
54
+ "牛市",
55
+ "熊市",
56
+ "板块",
57
+ "市盈率",
58
+ "分红",
59
+ "持仓",
60
+ "量化",
61
+ "对冲",
62
+ "ipo",
63
+ "美联储",
64
+ "加息",
65
+ "降息",
66
+ )
67
+
68
+
69
+ def load_config() -> dict[str, Any]:
70
+ """加载 ~/.dwy/config.yaml#doubao_tts;缺文件或缺 api_key 直接失败。"""
71
+ if not CONFIG_PATH.is_file():
72
+ raise FileNotFoundError(
73
+ f"配置不存在: {CONFIG_PATH}。请先运行 setup_config.py --api-key <KEY>"
74
+ )
75
+ cfg = get_section(SECTION_DOUBAO_TTS)
76
+ if not cfg:
77
+ raise ValueError(
78
+ f"缺少 section `{SECTION_DOUBAO_TTS}`: {CONFIG_PATH}。"
79
+ "请先运行 setup_config.py --migrate-legacy 或 --api-key <KEY>"
80
+ )
81
+ if not (cfg.get("api_key") or "").strip():
82
+ raise ValueError("doubao_tts.api_key 为空,请先 setup_config.py --api-key <KEY>")
83
+ if not (cfg.get("voices") or {}):
84
+ raise ValueError("doubao_tts.voices 为空,请用 setup_config.py --voice KEY=ID 写入")
85
+ return cfg
86
+
87
+
88
+ def resolve_voice_key(cfg: dict[str, Any], voice: str | None, text: str) -> str:
89
+ """用户指定优先;否则财经关键词 → finance;否则 default_voice。"""
90
+ voices = cfg.get("voices") or {}
91
+ if voice:
92
+ if voice not in voices:
93
+ raise KeyError(f"未知音色 key: {voice};可选: {', '.join(voices)}")
94
+ return voice
95
+
96
+ lower = text.lower()
97
+ if any(k in text or k in lower for k in FINANCE_KEYWORDS):
98
+ if "finance" in voices:
99
+ return "finance"
100
+
101
+ default = cfg.get("default_voice") or "young_female"
102
+ if default not in voices:
103
+ # 兜底:取第一个
104
+ if not voices:
105
+ raise KeyError("config.voices 为空")
106
+ return next(iter(voices))
107
+ return default
108
+
109
+
110
+ def speaker_id(cfg: dict[str, Any], voice_key: str) -> str:
111
+ """从全局 config 取 speaker 音色 ID。"""
112
+ meta = (cfg.get("voices") or {})[voice_key]
113
+ sid = meta.get("id") if isinstance(meta, dict) else meta
114
+ if not sid:
115
+ raise KeyError(f"音色 {voice_key} 缺少 id")
116
+ return str(sid)
117
+
118
+
119
+ def default_output_path(cfg: dict[str, Any], fmt: str) -> Path:
120
+ """按配置 output_dir 生成带时间戳的文件路径。"""
121
+ out_dir = Path(cfg.get("output_dir") or (Path.home() / "Movies" / "doubao-tts")).expanduser()
122
+ out_dir.mkdir(parents=True, exist_ok=True)
123
+ stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
124
+ return out_dir / f"口播_{stamp}.{fmt}"
125
+
126
+
127
+ def build_body(
128
+ text: str,
129
+ speaker: str,
130
+ audio: dict[str, Any],
131
+ speech_rate: int | None,
132
+ loudness_rate: int | None,
133
+ context_texts: list[str] | None,
134
+ model: str | None = None,
135
+ ) -> dict[str, Any]:
136
+ """组装 req_params;additions 需为 JSON 字符串(接口约定)。"""
137
+ params: dict[str, Any] = {
138
+ "text": text,
139
+ "speaker": speaker,
140
+ "audio_params": {
141
+ "format": audio.get("format") or "mp3",
142
+ "sample_rate": int(audio.get("sample_rate") or 24000),
143
+ "speech_rate": int(speech_rate if speech_rate is not None else audio.get("speech_rate") or 0),
144
+ "loudness_rate": int(
145
+ loudness_rate if loudness_rate is not None else audio.get("loudness_rate") or 0
146
+ ),
147
+ },
148
+ }
149
+ # 仅配置显式给出 model 时下发(复刻场景按控制台要求填写)
150
+ if model:
151
+ params["model"] = model
152
+ additions: dict[str, Any] = {}
153
+ if audio.get("disable_markdown_filter") is not None:
154
+ additions["disable_markdown_filter"] = bool(audio["disable_markdown_filter"])
155
+ if audio.get("disable_emoji_filter") is not None:
156
+ additions["disable_emoji_filter"] = bool(audio["disable_emoji_filter"])
157
+ if additions:
158
+ params["additions"] = json.dumps(additions, ensure_ascii=False)
159
+ if context_texts:
160
+ params["context_texts"] = context_texts
161
+ return {"req_params": params}
162
+
163
+
164
+ def parse_stream_lines(raw: bytes) -> list[dict[str, Any]]:
165
+ """把 chunked 响应体拆成 JSON 对象列表。
166
+
167
+ 兼容:每行一个 JSON;或粘连的 }{ 形态。
168
+ """
169
+ text = raw.decode("utf-8", errors="replace").strip()
170
+ if not text:
171
+ return []
172
+ objs: list[dict[str, Any]] = []
173
+ # 先按行
174
+ for line in text.splitlines():
175
+ line = line.strip()
176
+ if not line:
177
+ continue
178
+ try:
179
+ objs.append(json.loads(line))
180
+ continue
181
+ except json.JSONDecodeError:
182
+ pass
183
+ # 粘连 JSON:用 }{ 切
184
+ parts = re.split(r"\}\s*\{", line)
185
+ if len(parts) == 1:
186
+ raise ValueError(f"无法解析响应行: {line[:200]}")
187
+ for i, part in enumerate(parts):
188
+ if i == 0:
189
+ s = part + "}"
190
+ elif i == len(parts) - 1:
191
+ s = "{" + part
192
+ else:
193
+ s = "{" + part + "}"
194
+ objs.append(json.loads(s))
195
+ return objs
196
+
197
+
198
+ def synthesize(
199
+ cfg: dict[str, Any],
200
+ text: str,
201
+ voice_key: str,
202
+ output: Path,
203
+ speech_rate: int | None,
204
+ loudness_rate: int | None,
205
+ context_texts: list[str] | None,
206
+ timeout: int,
207
+ ) -> dict[str, Any]:
208
+ """发单向流式请求,拼接 base64 音频并落盘。"""
209
+ endpoint = cfg.get("endpoint") or "https://openspeech.bytedance.com/api/v3/tts/unidirectional"
210
+ resource_id = cfg.get("resource_id") or "seed-icl-2.0"
211
+ speaker = speaker_id(cfg, voice_key)
212
+ audio = cfg.get("audio") or {}
213
+ model = cfg.get("model") or audio.get("model")
214
+ body = build_body(text, speaker, audio, speech_rate, loudness_rate, context_texts, model)
215
+ req_id = str(uuid.uuid4())
216
+
217
+ headers = {
218
+ "Content-Type": "application/json",
219
+ "X-Api-Key": cfg["api_key"].strip(),
220
+ "X-Api-Resource-Id": resource_id,
221
+ "X-Api-Request-Id": req_id,
222
+ }
223
+ data = json.dumps(body, ensure_ascii=False).encode("utf-8")
224
+ request = Request(endpoint, data=data, headers=headers, method="POST")
225
+
226
+ try:
227
+ with urlopen(request, timeout=timeout) as resp:
228
+ raw = resp.read()
229
+ logid = resp.headers.get("X-Tt-Logid") or resp.headers.get("x-tt-logid") or ""
230
+ except HTTPError as e:
231
+ err_body = e.read().decode("utf-8", errors="replace")[:500]
232
+ raise RuntimeError(f"HTTP {e.code}: {err_body}") from e
233
+ except URLError as e:
234
+ raise RuntimeError(f"网络错误: {e.reason}") from e
235
+
236
+ chunks: list[bytes] = []
237
+ last_msg = ""
238
+ done = False
239
+ for obj in parse_stream_lines(raw):
240
+ code = obj.get("code")
241
+ msg = obj.get("message") or ""
242
+ if msg:
243
+ last_msg = msg
244
+ if code == CODE_AUDIO:
245
+ b64 = obj.get("data") or ""
246
+ if b64:
247
+ chunks.append(base64.b64decode(b64))
248
+ elif code == CODE_DONE:
249
+ done = True
250
+ break
251
+ elif code not in (None, CODE_AUDIO, CODE_DONE):
252
+ # 非 0 且非结束:业务错误
253
+ if code != 0:
254
+ raise RuntimeError(f"合成失败 code={code} message={msg} logid={logid}")
255
+
256
+ if not chunks:
257
+ raise RuntimeError(f"未收到音频数据 message={last_msg} logid={logid} done={done}")
258
+
259
+ output = output.expanduser().resolve()
260
+ output.parent.mkdir(parents=True, exist_ok=True)
261
+ payload = b"".join(chunks)
262
+ output.write_bytes(payload)
263
+
264
+ voice_meta = (cfg.get("voices") or {}).get(voice_key) or {}
265
+ return {
266
+ "ok": True,
267
+ "path": str(output),
268
+ "voice": voice_key,
269
+ "label": voice_meta.get("label") if isinstance(voice_meta, dict) else voice_key,
270
+ "bytes": len(payload),
271
+ "request_id": req_id,
272
+ "logid": logid,
273
+ "done": done,
274
+ }
275
+
276
+
277
+ def read_text(args: argparse.Namespace) -> str:
278
+ """从 --text / --text-file / stdin 取文案。"""
279
+ if args.text_file:
280
+ return Path(args.text_file).expanduser().read_text(encoding="utf-8").strip()
281
+ if args.text is not None:
282
+ return args.text.strip()
283
+ if not sys.stdin.isatty():
284
+ return sys.stdin.read().strip()
285
+ raise SystemExit("error: 请提供 --text 或 --text-file 或 stdin 管道")
286
+
287
+
288
+ def main() -> int:
289
+ parser = argparse.ArgumentParser(description="豆包配音流式合成")
290
+ parser.add_argument("--text", help="口播正文")
291
+ parser.add_argument("--text-file", help="从文件读口播正文(推荐长文)")
292
+ parser.add_argument("--voice", help="音色 key:finance / young_female / 配置自定义")
293
+ parser.add_argument("--output", help="输出音频绝对路径")
294
+ parser.add_argument("--speech-rate", type=int, help="语速 [-50,100]")
295
+ parser.add_argument("--loudness-rate", type=int, help="音量 [-50,100]")
296
+ parser.add_argument(
297
+ "--context-text",
298
+ action="append",
299
+ dest="context_texts",
300
+ help="语音指令,可重复传入",
301
+ )
302
+ parser.add_argument("--timeout", type=int, default=120, help="HTTP 超时秒数")
303
+ args = parser.parse_args()
304
+
305
+ try:
306
+ text = read_text(args)
307
+ if not text:
308
+ print("error: 文案为空", file=sys.stderr)
309
+ return 2
310
+ # 口播合理上限;超长仍允许但告警(由调用方拆段更佳)
311
+ if len(text) > 5000:
312
+ print("warning: 文案超过 5000 字,建议分段合成", file=sys.stderr)
313
+
314
+ cfg = load_config()
315
+ voice_key = resolve_voice_key(cfg, args.voice, text)
316
+ fmt = ((cfg.get("audio") or {}).get("format") or "mp3").lstrip(".")
317
+ out = Path(args.output) if args.output else default_output_path(cfg, fmt)
318
+
319
+ result = synthesize(
320
+ cfg=cfg,
321
+ text=text,
322
+ voice_key=voice_key,
323
+ output=out,
324
+ speech_rate=args.speech_rate,
325
+ loudness_rate=args.loudness_rate,
326
+ context_texts=args.context_texts,
327
+ timeout=args.timeout,
328
+ )
329
+ print(json.dumps(result, ensure_ascii=False))
330
+ return 0
331
+ except Exception as e:
332
+ # 永不打印 config 全文 / api_key
333
+ print(f"error: {e}", file=sys.stderr)
334
+ print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
335
+ return 1
336
+
337
+
338
+ if __name__ == "__main__":
339
+ raise SystemExit(main())
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: media-platform-packaging
3
+ description: |
4
+ 多平台包装(uc-media 可选 0090):B站/抖音/小红书/视频号/YouTube 标题简介标签、
5
+ 16:9/4:3/3:4 封面、建议发布时间;写入 0070-package 包装清单。
6
+ 触发词:包装、封面、标题简介、发布时间、0090、packaging、要发了。
7
+ ---
8
+
9
+ # 多平台自媒体包装与封面
10
+
11
+ 把内容的核心承诺变成可点击的标题和手机缩略图。目标不是把大纲塞进封面,而是让观众先读到「谁能看懂 + 这集讲什么」。
12
+
13
+ ## 在 uc-media 流水线中的位置
14
+
15
+ | 项 | 内容 |
16
+ |----|------|
17
+ | **stage** | `packaging`(**0090,可选**) |
18
+ | **何时** | 0070 成片包就绪后,或用户说「要发了 / 写标题封面」;可与 0080 并行或在 render 后 |
19
+ | **上游** | 脚本/成片承诺、`0060-assets` 视觉、频道档案默认分发 |
20
+ | **产出目录** | `{work_dir}/0070-package/`(包装清单 + `cover/`) |
21
+ | **不替代** | 0020 标题候选(可吸收 ★ 标题再按平台改写) |
22
+
23
+ ## 输入与输出
24
+
25
+ 先阅读用户提供的文章/视频稿和项目内的视觉资产、角色设定。按以下优先级找视觉资产:当前集 `media/0060-assets/` → 当前集 `remotion/public/characters/` → 仓库 `shared/characters/`、`shared/backgrounds/`。若素材不足,基于正文作合理假设,不为一般创作阻塞追问。
26
+
27
+ 默认交付:
28
+
29
+ 1. B站、抖音、小红书各一组标题、简介、标签。
30
+ 2. 三种画幅的封面:B站 16:9、B站首页 4:3、小红书/抖音 3:4;用户只指定部分平台时只做所需画幅。
31
+ 3. 各平台**建议发布时间**(UTC+8):默认按 `references/platform-posting-times.md`;有账号后台「粉丝活跃」数据时以后台为准。
32
+ 4. 一份写入 `media/0070-package/` 的包装清单,列出文案、建议发布时间与资产路径。
33
+
34
+ 若用户只要求“文案”“标题”“封面方案”或“几点发/发布时间”,只交付对应内容;不生成图片、不要求 `imagegen`,也不创建包装清单,除非用户明确要求保存。
35
+
36
+ ## 平台文案规则
37
+
38
+ | 平台 | 标题策略 | 简介与标签 |
39
+ |---|---|---|
40
+ | B站 | 主题 + 明确收益;长度不强行压缩 | 用开场问题、内容范围、一个边界或收获;4–7 个 `#标签` |
41
+ | YouTube | 主题 + 明确收益;长度不强行压缩 | 用开场问题、内容范围、一个边界或收获;标签字段使用多个关键词,并以**英文逗号**分隔,例如 `AI education, large language models, Transformer` |
42
+ | 抖音 | 问题或反常识钩子优先 | 1–2 句,强调观看收益;3–6 个标签 |
43
+ | 小红书 | **必须不超过 20 个汉字(标点计入)**;先写“小白也能懂”或直接收益 | 2–4 句,口语、可保存;5–8 个标签 |
44
+ | 微信视频号 | **必须不超过 16 个字符(中文、英文、数字与标点均计入)**;优先“人群 + 主题/收益” | 1–2 句,直接说明看完能获得什么;3–5 个标签 |
45
+
46
+ 避免“保姆级”“秒变专家”“全网最全”等空泛承诺。标题、封面主字和简介可以分工,不能机械重复。
47
+
48
+ ## 最佳发布时间(提高播放量)
49
+
50
+ 完整规则与测试方法见 `references/platform-posting-times.md`。包装交付时必须给出建议时间表;原则摘要:
51
+
52
+ | 平台 | 工作日首选(UTC+8) | 周末首选 | 备注 |
53
+ |---|---|---|---|
54
+ | B站 | 17:00–18:30、20:00–21:30 | 10:00–12:00、20:00–22:00 | 高峰前 30–60 分钟发;知识区偏晚间 |
55
+ | 抖音 | 07:00–08:30、18:00–20:00、21:00–22:30 | 10:00–12:00、20:00–23:00 | 小号可避 19 点整,改 12:15 / 21:00 |
56
+ | 小红书 | 07:00–09:00、11:30–13:00、19:00–22:00 | 09:00–11:00、20:00–21:30 | 万粉以下慎挤 19–21 大号墙 |
57
+ | 微信视频号 | 07:00–09:00、12:00–13:30、18:30–21:00 | 10:00 前后、20:00–22:30 | 发后 15 分钟内互动,喂社交裂变 |
58
+ | YouTube | 按目标观众时区 | 目标市场白天 | 中文受众可用北京 19:00–22:00 |
59
+
60
+ 约束:
61
+
62
+ - 时区默认中国 UTC+8;YouTube 必须先确认目标市场。
63
+ - **本号后台粉丝活跃曲线 > 本表默认值**。
64
+ - 多平台同日分发时错开 15 分钟以上,优先保 1–2 个主平台黄金窗。
65
+ - 包装清单中写清「建议时间 + 一句话理由」;无后台数据时标注「默认窗,待校准」。
66
+
67
+ ## 封面版式:先缩略图,后信息密度
68
+
69
+ 每张封面只允许一个绝对主标题;看不清时删减,而不是加字。
70
+
71
+ ### 固定视觉母版
72
+
73
+ `assets/cover-style-master-v1.png` 是本 Skill 的封面视觉母版。生成封面时应先将它作为 **style reference**,并保留以下不可变特征:
74
+
75
+ - 深海军蓝底、亮黄与白色超大主字、红色漫画爆炸框、青蓝/紫色发光概念装置;整体必须高对比。
76
+ - 主标题占据左侧或上方 40% 左右,粗深蓝描边 + 白色外轮廓 + 明确投影;手机缩略图中先读到主标题。
77
+ - 小圆以右侧或下侧大脸近景出现,黄色雨衣、蓝绿背包、惊讶/恍然表情,动作指向核心概念装置。
78
+ - 画面仅有:一个主标题、一句钩子、一个概念装置、最多三枚底部承诺标签;不加入密集流程图、细小图标或说明文字。
79
+
80
+ 这是“强标题科技漫画”风格,不是普通扁平信息图或复杂技术海报。生成后必须以约 25% 缩放预览检查:标题仍清楚、人物表情仍可辨、核心概念仍一眼可见,才可交付。
81
+
82
+ 1. **主标题**:占画面 35%–50%,两行以内;用“`小白看懂` + `主题`”或“`主题 + 关键问题`”。
83
+ 2. **概念装置**:只选择一个可以看懂的核心隐喻,不画完整课程流程图。
84
+ 3. **系列人格**:使用项目规定角色;脸部或动作要大,服务主标题而非抢字。
85
+ 4. **内容承诺**:最多三个小标签,例如“Transformer|训练三步|会聊天”。
86
+ 5. **钩子**:可加一个顶部短问句/爆炸框;仅一句。
87
+
88
+ 画幅重排,不裁切:
89
+
90
+ | 画幅 | 优先构图 |
91
+ |---|---|
92
+ | 16:9 | 左侧大标题,右侧角色 + 概念装置;底部可放三枚标签 |
93
+ | 4:3 | 标题与角色更靠中央,角色脸部放大,四边保留 7% 安全区 |
94
+ | 3:4 | 上方问题 + 巨大两行标题;下方概念装置 + 角色近景;底部标签不超过三枚 |
95
+
96
+ 推荐最小导出尺寸:16:9 为 1920×1080;4:3 为 1600×1200;3:4 为 1080×1440。生成后必须核对实际像素比例。
97
+
98
+ ## 生图流程
99
+
100
+ **REQUIRED SUB-SKILL:** 使用 `imagegen` 生成或编辑封面;先读取它的 `SKILL.md`。
101
+
102
+ 1. 检查本集/项目已有角色和风格图;有既定角色时,把它作为 reference image。没有时,不擅自创造长期角色设定。
103
+ 2. 为每个画幅分别生成,提示词中明确 `true 16:9`、`true 4:3` 或 `true 3:4`,禁止“裁切横图”。
104
+ 3. 生成前写清楚所有需要出现的文字;文字过多时把复杂信息移进小标签。若生成文字失真,重新生成或留出干净文字槽位后用后期排版,不交付乱码。
105
+ 4. 生成后检查:画幅、标题可读性、角色一致性、手机尺寸下的第一视觉、无水印/品牌 Logo/多余文字。
106
+ 5. 不覆盖既有素材;保存到 `media/0070-package/cover/<主题>-<平台>-<画幅>-vN.png`。
107
+
108
+ ## 提示词骨架
109
+
110
+ ```text
111
+ 用途:<平台> <画幅> 视频封面。
112
+ 主题:<主题>,面向零基础观众。
113
+ 标题(逐字):"<主标题>";钩子:"<可选短问句>";内容承诺:"<标签1>" | "<标签2>" | "<标签3>"。
114
+ 构图:<按画幅选择布局>;<角色>以大脸/明确动作出现;一个大型概念装置“<核心隐喻>”。
115
+ 风格:以 `assets/cover-style-master-v1.png` 为 style reference;高对比、粗描边、手机缩略图可读的强标题科技漫画。
116
+ 约束:标题完整且安全区可见;无额外文字、无水印、无品牌 Logo、无密集小图、无写实照片。
117
+ ```
118
+
119
+ ## 交付前检查
120
+
121
+ - [ ] 小红书标题 ≤20 字。
122
+ - [ ] 微信视频号标题 ≤16 字(标点计入)。
123
+ - [ ] 每个封面只含一个主标题,且与缩略图尺寸匹配。
124
+ - [ ] 16:9、4:3、3:4 是独立构图,不是同图硬裁。
125
+ - [ ] 已核对并报告每张图片的像素比例与保存路径。
126
+ - [ ] 已给出各平台建议发布时间(UTC+8),并注明是否为本号后台校准。
127
+ - [ ] 文案、标签、建议发布时间、图片路径都记录在本集包装清单中。
@@ -0,0 +1,37 @@
1
+ # 平台文案示例
2
+
3
+ ## 小红书标题长度
4
+
5
+ 计算 Unicode 字符数,标点也计入。上限 20。
6
+
7
+ | 合格示例 | 字符数 |
8
+ |---|---:|
9
+ | 小白也能懂:大模型如何学会聊天 | 15 |
10
+ | 小白看懂大模型:它为什么会聊天? | 17 |
11
+
12
+ ## 微信视频号标题长度
13
+
14
+ 计算 Unicode 字符数,中文、英文、数字和标点均计入;上限 16。
15
+
16
+ | 合格示例 | 字符数 |
17
+ |---|---:|
18
+ | 小白看懂大模型 | 7 |
19
+ | 大模型为什么会聊天? | 10 |
20
+
21
+ ## YouTube 标签格式
22
+
23
+ YouTube 的标签字段不使用 `#`,多个标签必须用英文逗号分隔:
24
+
25
+ ```text
26
+ AI education, large language models, Transformer, generative AI
27
+ ```
28
+
29
+ ## 封面文字的优先级
30
+
31
+ ```text
32
+ 主标题(最大):小白看懂 / 大模型
33
+ 钩子(小):AI 到底怎么工作?
34
+ 承诺(最小):Transformer|训练三步|会聊天
35
+ ```
36
+
37
+ 若画幅或主题无法承受三层文字,先保留主标题,再保留一个钩子;不要缩小主标题。