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,147 @@
1
+ ---
2
+ name: dwy-doubao-tts
3
+ description: >
4
+ 豆包配音:用火山引擎豆包声音复刻 2.0(seed-icl-2.0)HTTP Chunked 单向流式接口,
5
+ 为自媒体口播生成 MP3 音频。API Key / 音色 ID 只读用户全局 ~/.dwy/config.yaml,
6
+ 禁止写入 skill 或任何项目目录。触发:豆包配音、口播配音、TTS、语音合成、
7
+ 生成口播音频、/doubao-tts、/dwy-doubao-tts。
8
+ ---
9
+
10
+ # 豆包配音(dwy-doubao-tts)
11
+
12
+ 自媒体口播:读全局密钥与音色 → 收口播文案 → 按语境选音色 → 流式合成 MP3 → 回报路径。
13
+
14
+ `SKILL_DIR` = 本 `SKILL.md` 所在目录(`dwy` 同步后随平台落在 `.claude/skills/dwy-doubao-tts` 或 `.agents/skills/dwy-doubao-tts`)。脚本一律用 `$SKILL_DIR/scripts/`,禁止写 `~/.grok/skills`。
15
+
16
+ ## 硬约束
17
+
18
+ 1. **私人配置唯一位置**:`~/.dwy/config.yaml`(权限 `600`),豆包段为顶层 key `doubao_tts`
19
+ 2. **禁止**把 `api_key`、音色 ID 写入 skill、项目、commit;禁止回显完整 Key
20
+ 3. 输出音频默认 `doubao_tts.output_dir`(默认 `~/Movies/doubao-tts`),不落到仓库内
21
+ 4. 模型资源:`X-Api-Resource-Id: seed-icl-2.0`
22
+ 5. 接口:`POST https://openspeech.bytedance.com/api/v3/tts/unidirectional`
23
+
24
+ ## 全局配置约定(`~/.dwy/config.yaml`)
25
+
26
+ 用户级统一配置文件,多业务共用,各业务独立 section:
27
+
28
+ ```yaml
29
+ version: 1
30
+
31
+ doubao_tts:
32
+ api_key: "<仅本机>"
33
+ resource_id: seed-icl-2.0
34
+ endpoint: https://openspeech.bytedance.com/api/v3/tts/unidirectional
35
+ default_voice: young_female
36
+ voices:
37
+ finance:
38
+ id: "<音色ID>"
39
+ label: 金融财经
40
+ young_female:
41
+ id: "<音色ID>"
42
+ label: 年轻女性
43
+ audio:
44
+ format: mp3
45
+ sample_rate: 24000
46
+ output_dir: ~/Movies/doubao-tts
47
+
48
+ # 其它业务可继续追加顶层 key,互不覆盖
49
+ # other_service:
50
+ # ...
51
+ ```
52
+
53
+ 结构说明见 `references/config.example.yaml`(示例无真实密钥)。
54
+
55
+ ## 启动流程(每次触发必做)
56
+
57
+ ### 1. 检查全局配置
58
+
59
+ ```bash
60
+ python3 -c "
61
+ from pathlib import Path
62
+ import sys
63
+ sys.path.insert(0, str(Path(r'$SKILL_DIR') / 'scripts'))
64
+ from global_config import CONFIG_PATH, SECTION_DOUBAO_TTS, get_section
65
+ c = get_section(SECTION_DOUBAO_TTS)
66
+ assert c.get('api_key') and c.get('voices'), 'missing'
67
+ print('ok', CONFIG_PATH)
68
+ "
69
+ ```
70
+
71
+ - 失败 → 「首次配置 / 迁移」,**不要**继续合成
72
+ - 成功 → 步骤 2
73
+
74
+ ### 2. 收集用户输入
75
+
76
+ 消息里若无完整口播正文,必须提问补齐,禁止瞎编:
77
+
78
+ | 字段 | 必选 | 说明 |
79
+ |------|------|------|
80
+ | `text` | 是 | 口播全文 |
81
+ | `voice` | 否 | `finance` / `young_female` / 配置其它 key;不传则自动选 |
82
+ | `output` | 否 | 输出绝对路径 |
83
+ | `speech_rate` / `loudness_rate` | 否 | `[-50,100]` |
84
+ | `context_texts` | 否 | 语音指令 |
85
+
86
+ ### 3. 自动选音色
87
+
88
+ 音色 ID **只从** `~/.dwy/config.yaml` 的 `doubao_tts.voices` 读,skill 内不硬编码真实 ID。
89
+
90
+ | 角色 key | 适用语境 |
91
+ |----------|----------|
92
+ | `finance` | 财经、股市、基金、理财、利率、宏观、投研 |
93
+ | `young_female` | 生活、种草、日常、泛娱乐、默认兜底 |
94
+
95
+ 1. 用户指定 `voice` → 用该 key
96
+ 2. 否则文案命中财经关键词 → `finance`
97
+ 3. 否则 → `default_voice`
98
+ 4. 两可 → 先问用户
99
+
100
+ ### 4. 执行合成
101
+
102
+ ```bash
103
+ python3 "$SKILL_DIR/scripts/synthesize.py" \
104
+ --text-file /tmp/doubao_tts_input.txt \
105
+ --voice <finance|young_female|...> \
106
+ [--output /absolute/path/out.mp3]
107
+ ```
108
+
109
+ - 长文优先 `--text-file`;单次建议 ≤ 1000 汉字
110
+ - 成功 stdout JSON:`{"ok":true,"path":"...","voice":"...","bytes":N}`
111
+
112
+ ### 5. 回报用户
113
+
114
+ 绝对路径、音色 key/label、文件大小;可 `open "<path>"` 试听。**禁止**回显 API Key。
115
+
116
+ ## 首次配置 / 迁移
117
+
118
+ ```bash
119
+ # 从旧路径 ~/.config/doubao-tts/config.json 迁到 ~/.dwy/config.yaml
120
+ python3 "$SKILL_DIR/scripts/setup_config.py" --migrate-legacy
121
+
122
+ # 或写入 / 更新 Key(可同时补音色)
123
+ python3 "$SKILL_DIR/scripts/setup_config.py" \
124
+ --api-key '<USER_KEY>' \
125
+ --voice 'finance=<ID>,金融财经' \
126
+ --voice 'young_female=<ID>,年轻女性'
127
+ ```
128
+
129
+ - 写入:`~/.dwy/config.yaml` → `doubao_tts`
130
+ - 成功后删除旧 JSON,避免双份密钥
131
+ - **永不**写入当前 git 仓库或 skill 目录
132
+
133
+ 仅换 Key:
134
+
135
+ ```bash
136
+ python3 "$SKILL_DIR/scripts/setup_config.py" --api-key '<NEW>' --update-key-only
137
+ ```
138
+
139
+ ## 协议要点
140
+
141
+ 见 `references/api.md`。Header:`X-Api-Key` / `X-Api-Resource-Id` / `X-Api-Request-Id`;流式 `code==0` 拼 base64,结束码 `20000000`。
142
+
143
+ ## 安全
144
+
145
+ - 私人信息只在 `~/.dwy/config.yaml`
146
+ - 不在项目建 `.env` / 本地 config
147
+ - 用户要求拷进项目 → 拒绝
@@ -0,0 +1,43 @@
1
+ # 豆包单向流式 TTS(HTTP Chunked)摘要
2
+
3
+ 权威字段以火山引擎控制台文档为准。实现以 `scripts/synthesize.py` 为准。
4
+
5
+ ## 配置
6
+
7
+ 私人信息只读:`~/.dwy/config.yaml` → 顶层 key `doubao_tts`
8
+ 示例骨架:`config.example.yaml`(无真实密钥)
9
+
10
+ ## Endpoint
11
+
12
+ `POST https://openspeech.bytedance.com/api/v3/tts/unidirectional`
13
+
14
+ ## Headers
15
+
16
+ | Header | 必选 | 说明 |
17
+ |--------|------|------|
18
+ | `X-Api-Key` | 是 | `doubao_tts.api_key` |
19
+ | `X-Api-Resource-Id` | 是 | 默认 `seed-icl-2.0` |
20
+ | `X-Api-Request-Id` | 是 | 客户端 uuid |
21
+
22
+ ## Body
23
+
24
+ ```json
25
+ {
26
+ "req_params": {
27
+ "text": "口播正文",
28
+ "speaker": "<音色ID>",
29
+ "audio_params": {
30
+ "format": "mp3",
31
+ "sample_rate": 24000,
32
+ "speech_rate": 0,
33
+ "loudness_rate": 0
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## 响应(Chunked)
40
+
41
+ - `code == 0`:`data` 为 base64 音频片
42
+ - `code == 20000000`:结束
43
+ - Header `X-Tt-Logid` 排障用
@@ -0,0 +1,33 @@
1
+ # 示例:复制结构到 ~/.dwy/config.yaml(勿提交真实密钥)
2
+ # 路径:/Users/<you>/.dwy/config.yaml
3
+ # 权限:chmod 600 ~/.dwy/config.yaml
4
+
5
+ version: 1
6
+
7
+ # 豆包配音(声音复刻 2.0)
8
+ doubao_tts:
9
+ api_key: "<从火山控制台粘贴>"
10
+ resource_id: seed-icl-2.0
11
+ endpoint: https://openspeech.bytedance.com/api/v3/tts/unidirectional
12
+ default_voice: young_female
13
+ voices:
14
+ finance:
15
+ id: "<金融财经音色ID>"
16
+ label: 金融财经
17
+ desc: 财经、投研、商业分析口播
18
+ young_female:
19
+ id: "<年轻女性音色ID>"
20
+ label: 年轻女性
21
+ desc: 生活、种草、日常、泛娱乐口播
22
+ audio:
23
+ format: mp3
24
+ sample_rate: 24000
25
+ speech_rate: 0
26
+ loudness_rate: 0
27
+ disable_markdown_filter: true
28
+ disable_emoji_filter: true
29
+ output_dir: ~/Movies/doubao-tts
30
+
31
+ # 其它业务配置可并列追加,例如:
32
+ # lark:
33
+ # app_id: "..."
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """用户全局配置读写:~/.dwy/config.yaml
3
+
4
+ 私人信息(API Key、音色 ID 等)只放在此文件,不进 skill、不进项目仓库。
5
+ 多业务共用一个 YAML,各业务占独立顶层 key(如 doubao_tts)。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import stat
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import yaml
16
+
17
+ # 用户主目录全局配置根(非项目 .dwy)
18
+ DWY_HOME = Path.home() / ".dwy"
19
+ CONFIG_PATH = DWY_HOME / "config.yaml"
20
+
21
+ # 豆包配音在全局 YAML 中的 section 名
22
+ SECTION_DOUBAO_TTS = "doubao_tts"
23
+
24
+ # 旧路径:迁移用
25
+ LEGACY_JSON = Path.home() / ".config" / "doubao-tts" / "config.json"
26
+
27
+
28
+ def ensure_dwy_home() -> None:
29
+ """确保 ~/.dwy 存在且仅本人可访问。"""
30
+ DWY_HOME.mkdir(parents=True, exist_ok=True)
31
+ os.chmod(DWY_HOME, stat.S_IRWXU)
32
+
33
+
34
+ def load_root() -> dict[str, Any]:
35
+ """加载整份全局 YAML;不存在返回带 version 的空结构。"""
36
+ if not CONFIG_PATH.is_file():
37
+ return {"version": 1}
38
+ data = yaml.safe_load(CONFIG_PATH.read_text(encoding="utf-8"))
39
+ if data is None:
40
+ return {"version": 1}
41
+ if not isinstance(data, dict):
42
+ raise ValueError(f"全局配置格式错误(需 mapping): {CONFIG_PATH}")
43
+ data.setdefault("version", 1)
44
+ return data
45
+
46
+
47
+ def save_root(root: dict[str, Any]) -> Path:
48
+ """原子写入 ~/.dwy/config.yaml,chmod 600。"""
49
+ ensure_dwy_home()
50
+ root = dict(root)
51
+ root.setdefault("version", 1)
52
+ text = yaml.safe_dump(
53
+ root,
54
+ allow_unicode=True,
55
+ default_flow_style=False,
56
+ sort_keys=False,
57
+ )
58
+ tmp = CONFIG_PATH.with_suffix(".yaml.tmp")
59
+ tmp.write_text(text, encoding="utf-8")
60
+ tmp.replace(CONFIG_PATH)
61
+ os.chmod(CONFIG_PATH, stat.S_IRUSR | stat.S_IWUSR)
62
+ return CONFIG_PATH
63
+
64
+
65
+ def get_section(name: str) -> dict[str, Any]:
66
+ """读取某一业务 section;缺失返回 {}。"""
67
+ root = load_root()
68
+ sec = root.get(name)
69
+ if sec is None:
70
+ return {}
71
+ if not isinstance(sec, dict):
72
+ raise ValueError(f"section {name!r} 必须是 mapping: {CONFIG_PATH}")
73
+ return sec
74
+
75
+
76
+ def set_section(name: str, section: dict[str, Any]) -> Path:
77
+ """覆盖写入某一业务 section,保留其它业务配置。"""
78
+ root = load_root()
79
+ root[name] = section
80
+ return save_root(root)
81
+
82
+
83
+ def try_load_legacy_doubao_json() -> dict[str, Any]:
84
+ """读取旧版 ~/.config/doubao-tts/config.json(若存在)。"""
85
+ if not LEGACY_JSON.is_file():
86
+ return {}
87
+ import json
88
+
89
+ return json.loads(LEGACY_JSON.read_text(encoding="utf-8"))
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """初始化 / 更新豆包配音配置到用户全局 ~/.dwy/config.yaml(doubao_tts section)。
3
+
4
+ 禁止写入 skill 目录与任何 git 项目。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import copy
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ # 同目录模块
16
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
17
+
18
+ from global_config import ( # noqa: E402
19
+ LEGACY_JSON,
20
+ SECTION_DOUBAO_TTS,
21
+ get_section,
22
+ set_section,
23
+ try_load_legacy_doubao_json,
24
+ )
25
+
26
+ # 结构默认值:不含真实 api_key / 音色 ID(私人信息只由用户写入全局 YAML)
27
+ DEFAULT_SECTION: dict = {
28
+ "api_key": "",
29
+ "resource_id": "seed-icl-2.0",
30
+ "endpoint": "https://openspeech.bytedance.com/api/v3/tts/unidirectional",
31
+ "voices": {},
32
+ "default_voice": "young_female",
33
+ "audio": {
34
+ "format": "mp3",
35
+ "sample_rate": 24000,
36
+ "speech_rate": 0,
37
+ "loudness_rate": 0,
38
+ "disable_markdown_filter": True,
39
+ "disable_emoji_filter": True,
40
+ },
41
+ "output_dir": str(Path.home() / "Movies" / "doubao-tts"),
42
+ }
43
+
44
+
45
+ def deep_merge(base: dict, overlay: dict) -> dict:
46
+ """浅层 + 一层 dict 合并;overlay 优先。"""
47
+ out = copy.deepcopy(base)
48
+ for k, v in overlay.items():
49
+ if isinstance(v, dict) and isinstance(out.get(k), dict):
50
+ merged = dict(out[k])
51
+ merged.update(v)
52
+ out[k] = merged
53
+ else:
54
+ out[k] = copy.deepcopy(v)
55
+ return out
56
+
57
+
58
+ def load_doubao_section() -> dict:
59
+ """优先 ~/.dwy/config.yaml#doubao_tts,其次旧 JSON。"""
60
+ sec = get_section(SECTION_DOUBAO_TTS)
61
+ if sec:
62
+ return sec
63
+ return try_load_legacy_doubao_json()
64
+
65
+
66
+ def main() -> int:
67
+ parser = argparse.ArgumentParser(description="写入豆包配音到 ~/.dwy/config.yaml")
68
+ parser.add_argument("--api-key", help="火山引擎语音 API Key")
69
+ parser.add_argument(
70
+ "--update-key-only",
71
+ action="store_true",
72
+ help="仅更新 api_key",
73
+ )
74
+ parser.add_argument("--default-voice", help="覆盖 default_voice(如 finance / young_female)")
75
+ parser.add_argument("--output-dir", help="覆盖口播输出目录")
76
+ parser.add_argument(
77
+ "--voice",
78
+ action="append",
79
+ metavar="KEY=ID[,label]",
80
+ help="写入/更新音色,可重复。例: --voice finance=S_xxx,金融财经",
81
+ )
82
+ parser.add_argument(
83
+ "--migrate-legacy",
84
+ action="store_true",
85
+ help="强制从 ~/.config/doubao-tts/config.json 迁移到 ~/.dwy/config.yaml",
86
+ )
87
+ args = parser.parse_args()
88
+
89
+ existing = load_doubao_section()
90
+ legacy = try_load_legacy_doubao_json()
91
+
92
+ if args.migrate_legacy and legacy:
93
+ existing = deep_merge(existing or {}, legacy)
94
+
95
+ if args.update_key_only:
96
+ if not args.api_key:
97
+ print("error: --update-key-only 需要 --api-key", file=sys.stderr)
98
+ return 2
99
+ if not existing:
100
+ print("error: 尚无 doubao_tts 配置可更新", file=sys.stderr)
101
+ return 2
102
+ sec = copy.deepcopy(existing)
103
+ sec["api_key"] = args.api_key.strip()
104
+ else:
105
+ sec = deep_merge(DEFAULT_SECTION, existing or {})
106
+ # 首次且无 voices:若有 legacy 整段带入
107
+ if not sec.get("voices") and legacy.get("voices"):
108
+ sec = deep_merge(sec, legacy)
109
+ if args.api_key:
110
+ sec["api_key"] = args.api_key.strip()
111
+ elif not (sec.get("api_key") or "").strip():
112
+ if legacy.get("api_key"):
113
+ sec["api_key"] = legacy["api_key"]
114
+ else:
115
+ print("error: 需要 --api-key(或先有可迁移的旧配置)", file=sys.stderr)
116
+ return 2
117
+
118
+ if args.default_voice:
119
+ sec["default_voice"] = args.default_voice
120
+ if args.output_dir:
121
+ sec["output_dir"] = str(Path(args.output_dir).expanduser())
122
+
123
+ if args.voice:
124
+ voices = dict(sec.get("voices") or {})
125
+ for item in args.voice:
126
+ if "=" not in item:
127
+ print(f"error: 无效 --voice {item!r},需 KEY=ID[,label]", file=sys.stderr)
128
+ return 2
129
+ key, rest = item.split("=", 1)
130
+ key = key.strip()
131
+ if "," in rest:
132
+ vid, label = rest.split(",", 1)
133
+ voices[key] = {"id": vid.strip(), "label": label.strip()}
134
+ else:
135
+ prev = voices.get(key) if isinstance(voices.get(key), dict) else {}
136
+ voices[key] = {**prev, "id": rest.strip()}
137
+ sec["voices"] = voices
138
+
139
+ if not (sec.get("api_key") or "").strip():
140
+ print("error: api_key 为空", file=sys.stderr)
141
+ return 2
142
+
143
+ path = set_section(SECTION_DOUBAO_TTS, sec)
144
+
145
+ removed_legacy = False
146
+ if LEGACY_JSON.is_file() and (args.migrate_legacy or legacy):
147
+ try:
148
+ LEGACY_JSON.unlink()
149
+ removed_legacy = True
150
+ parent = LEGACY_JSON.parent
151
+ if parent.is_dir() and not any(parent.iterdir()):
152
+ parent.rmdir()
153
+ except OSError:
154
+ pass
155
+
156
+ print(
157
+ json.dumps(
158
+ {
159
+ "ok": True,
160
+ "path": str(path),
161
+ "section": SECTION_DOUBAO_TTS,
162
+ "voices": list((sec.get("voices") or {}).keys()),
163
+ "default_voice": sec.get("default_voice"),
164
+ "resource_id": sec.get("resource_id"),
165
+ "has_api_key": bool(sec.get("api_key")),
166
+ "removed_legacy": removed_legacy,
167
+ },
168
+ ensure_ascii=False,
169
+ )
170
+ )
171
+ return 0
172
+
173
+
174
+ if __name__ == "__main__":
175
+ raise SystemExit(main())