import_templates.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. """
  2. 从 daping_rec_online_api 项目的 write_jinja 文件中提取模板信息
  3. 生成完整的模板数据(包含 type_id、zone_ids、card_type 等字段)
  4. 注意:此脚本仅读取 daping_rec 作为参考,不修改其任何文件
  5. """
  6. import json
  7. import re
  8. from pathlib import Path
  9. from datetime import datetime
  10. from typing import Optional
  11. # ==================== 机型硬件配置(与 backend/main.py 保持一致)====================
  12. MACHINE_CONFIGS = {
  13. "x11": {
  14. "label": "X11",
  15. "type_ids": {
  16. "wash": "201c51890c31c308050100218008390000000000000000000000000000000040",
  17. "dry": "201c51890c31c308310100218003484748000000000000000000000000000040",
  18. },
  19. "zone_ids": {"wash": ["2", "3"], "dry": ["0"]},
  20. "card_type": "single_card",
  21. "has_template_body": True, # x11 有自己的模板正文
  22. },
  23. "hplus": {
  24. "label": "Hplus",
  25. "type_ids": {
  26. "wash": "201c51890c31c308050400218003484843000001000000000000000000000040",
  27. "dry": "201c51890c31c308050400218003484843000001000000000000000000000040",
  28. },
  29. "zone_ids": {"wash": ["3"], "dry": ["0"]},
  30. "card_type": "single_card",
  31. "has_template_body": True,
  32. },
  33. "f2_multi": {
  34. "label": "F2 Multi",
  35. "type_ids": {
  36. "wash": "201c51890c31c30805040021800",
  37. },
  38. "zone_ids": {"wash": ["1"]},
  39. "card_type": "multi_card",
  40. "has_template_body": True,
  41. },
  42. "ultra": {
  43. "label": "Ultra",
  44. "type_ids": {
  45. "wash": "201c51890c31c30805010021800805415000000000000000000000000000040",
  46. "dry": "201c51890c31c30805010021800805415000000000000000000000000000040",
  47. },
  48. "zone_ids": {"wash": ["1", "3", "41", "43", "61", "63"], "dry": ["1", "3", "41", "43", "61", "63"]},
  49. "card_type": "single_card",
  50. "has_template_body": False, # 复用 x11
  51. },
  52. "seeker": {
  53. "label": "Seeker",
  54. "type_ids": {
  55. "wash": "201c51890c31c308050100218008054251410000000000000000000000000040",
  56. "dry": "201c51890c31c308310100218008054251410003000000000000000000000040",
  57. },
  58. "zone_ids": {"wash": ["2", "3", "4"], "dry": ["0"]},
  59. "card_type": "single_card",
  60. "has_template_body": False, # 复用 x11
  61. },
  62. "seekerR2": {
  63. "label": "SeekerR2",
  64. "type_ids": {
  65. "wash": "201c51890c31c308050100218008054146000000000000000000000000000040",
  66. "dry": "201c51890c31c308310100218008054146000000000000000000000000000040",
  67. },
  68. "zone_ids": {"wash": ["1", "41", "61"], "dry": ["24"]},
  69. "card_type": "single_card",
  70. "has_template_body": False, # 复用 x11
  71. },
  72. "x11_ultra": {
  73. "label": "X11 Ultra",
  74. "type_ids": {
  75. "wash": "201c51890c31c308050400218003484446000000000000000000000000000040",
  76. "dry": "201c51890c31c308050400218003484446000000000000000000000000000040",
  77. },
  78. "zone_ids": {"wash": ["1", "7"], "dry": ["24"]},
  79. "card_type": "single_card",
  80. "has_template_body": False, # 复用 x11
  81. },
  82. }
  83. # 目录名到配置名的映射(处理大小写和下划线差异)
  84. DIR_TO_CONFIG_MAP = {
  85. "x11Ultra": "x11_ultra",
  86. }
  87. # 复用映射:哪些机型复用 x11 的模板
  88. # 值表示:当前机型的 device_type 复用 x11 的哪个 device_type
  89. REUSE_MAP = {
  90. "ultra": {"wash": "wash", "dry": "dry"},
  91. "seeker": {"wash": "wash", "dry": "dry"},
  92. "seekerR2": {"wash": "wash", "dry": "dry"},
  93. "x11_ultra": {"wash": "wash", "dry": "dry"},
  94. }
  95. # 模板类型标准化映射
  96. TEMPLATE_TYPE_MAP = {
  97. "popular": "time_popular",
  98. "made": "time_custom",
  99. "custom": "time_custom",
  100. "season": "time_season",
  101. "season_in": "time_season_in", # 季节上新(入季)
  102. "season_storage": "time_season_storage",
  103. "care": "care",
  104. "env": "environmental",
  105. "environmental": "environmental",
  106. "health": "health",
  107. "health2": "health2",
  108. "weather": "weather",
  109. "common": "common",
  110. }
  111. def extract_templates_from_py(py_file: Path, model_name: str) -> list:
  112. """从 Python write_jinja 文件中提取模板信息
  113. Returns:
  114. list of dict: 每个 dict 包含 template_type、template_body、second_key、device_type
  115. """
  116. content = py_file.read_text(encoding='utf-8')
  117. templates = []
  118. # 提取 zone_ids(从 zone_ids = [...] 或 zone_id = "...")
  119. zone_ids = []
  120. zone_ids_match = re.search(r'zone_ids\s*=\s*\[([^\]]+)\]', content)
  121. if zone_ids_match:
  122. zone_ids = [z.strip().strip('"\'') for z in zone_ids_match.group(1).split(',')]
  123. else:
  124. zone_id_match = re.search(r'zone_id\s*=\s*["\']([^"\']+)["\']', content)
  125. if zone_id_match:
  126. zone_ids = [zone_id_match.group(1)]
  127. # 判断设备类型(通过文件名或内容推断)
  128. device_type = "wash" # 默认
  129. file_name = py_file.name.lower()
  130. if any(kw in file_name for kw in ['_up', '_dry', 'dry']):
  131. device_type = "dry"
  132. elif any(kw in file_name for kw in ['_down', '_wash', 'wash']):
  133. device_type = "wash"
  134. # 提取模板正文 value_xxx = """..."""
  135. template_pattern = r'value_(\w+)\s*=\s*"""(.*?)"""'
  136. template_matches = re.findall(template_pattern, content, re.DOTALL)
  137. # 提取 second_key value_xxx_second_key = "..."
  138. second_key_pattern = r'value_(\w+)_second_key\s*=\s*"([^"]+)"'
  139. second_key_matches = dict(re.findall(second_key_pattern, content))
  140. has_body = len(template_matches) > 0
  141. if has_body:
  142. # 有模板正文的机型(x11/hplus/f2_multi)
  143. for template_type, template_body in template_matches:
  144. # 跳过基础模板
  145. if template_type in ['default', 'basic', 'w_basic', 'w_default', 'd_default']:
  146. continue
  147. second_key = second_key_matches.get(template_type, "")
  148. templates.append({
  149. "template_type": template_type,
  150. "template_body": template_body.strip(),
  151. "second_key": second_key,
  152. "device_type": device_type,
  153. "zone_ids": zone_ids,
  154. })
  155. else:
  156. # 无模板正文的机型(ultra/seeker/seekerR2/x11Ultra),仅记录 second_key
  157. for template_type, second_key in second_key_matches.items():
  158. if template_type in ['default', 'basic', 'w_basic', 'w_default', 'd_default']:
  159. continue
  160. templates.append({
  161. "template_type": template_type,
  162. "template_body": "", # 复用 x11,无正文
  163. "second_key": second_key,
  164. "device_type": device_type,
  165. "zone_ids": zone_ids,
  166. })
  167. return templates
  168. def extract_all_templates(daping_rec_dir: Path) -> dict:
  169. """从 daping_rec_online_api 的 pre_data 目录提取所有机型模板
  170. Returns:
  171. dict: {model_name: [template_info, ...]}
  172. """
  173. all_templates = {}
  174. pre_data_dir = daping_rec_dir / "app" / "pre_data"
  175. if not pre_data_dir.exists():
  176. print(f"❌ 未找到 daping_rec 目录: {pre_data_dir}")
  177. return all_templates
  178. print(f"\n📂 扫描 daping_rec 模板目录: {pre_data_dir}")
  179. # 遍历所有机型目录
  180. for model_dir in sorted(pre_data_dir.iterdir()):
  181. if not model_dir.is_dir():
  182. continue
  183. dir_name = model_dir.name
  184. # 转换目录名为配置名
  185. model_name = DIR_TO_CONFIG_MAP.get(dir_name, dir_name)
  186. if model_name not in MACHINE_CONFIGS:
  187. print(f" ⏭ 跳过未知机型: {dir_name}")
  188. continue
  189. print(f"\n🔍 处理机型: {dir_name} -> {model_name} ({MACHINE_CONFIGS[model_name]['label']})")
  190. # 查找 write_*.py 文件(排除参数文件)
  191. write_files = sorted([
  192. f for f in model_dir.glob("write_*.py")
  193. if not f.name.startswith("write_param")
  194. ])
  195. if not write_files:
  196. print(f" ⚠️ 未找到 write_*.py 文件")
  197. continue
  198. model_templates = []
  199. for py_file in write_files:
  200. print(f" 📄 {py_file.name}")
  201. templates = extract_templates_from_py(py_file, model_name)
  202. if templates:
  203. print(f" ✓ 提取 {len(templates)} 个模板")
  204. model_templates.extend(templates)
  205. if model_templates:
  206. all_templates[model_name] = model_templates
  207. print(f" 📊 机型 {model_name} 共 {len(model_templates)} 个模板")
  208. return all_templates
  209. def normalize_template_type(raw_type: str) -> str:
  210. """标准化模板类型名称
  211. Examples:
  212. popular → time_popular
  213. made → time_custom
  214. w_popular → time_popular
  215. """
  216. # 去除 w_/d_ 前缀
  217. clean_type = raw_type
  218. if raw_type.startswith('w_'):
  219. clean_type = raw_type[2:]
  220. elif raw_type.startswith('d_'):
  221. clean_type = raw_type[2:]
  222. # 映射到标准类型
  223. return TEMPLATE_TYPE_MAP.get(clean_type, clean_type)
  224. def generate_redis_keys(model_name: str, device_type: str, template_type: str, second_key: str) -> dict:
  225. """根据模板信息生成 Redis key
  226. Returns:
  227. dict: {second_key: str, first_keys: list}
  228. """
  229. config = MACHINE_CONFIGS[model_name]
  230. card_type = config["card_type"]
  231. # 二级 key(如果未提供则自动生成)
  232. if not second_key:
  233. second_key = f"broadcast-template-jinja:{card_type}:{device_type}:{template_type}"
  234. # 一级 key(为每个 zone_id 生成)
  235. first_keys = []
  236. type_id = config["type_ids"].get(device_type, "")
  237. zone_ids = config["zone_ids"].get(device_type, [])
  238. # 模板类型到 recommend_type 的映射
  239. recommend_type_map = {
  240. "common": ("common", None),
  241. "time_popular": ("time_made_popularity", "1"),
  242. "time_custom": ("time_made_popularity", "2"),
  243. "time_season": ("time_season_storage", None),
  244. "time_season_new": ("time_season_storage", None),
  245. "time_season_storage": ("time_season_storage", None),
  246. "care": ("care", None),
  247. "environmental": ("environmental", None),
  248. "health": ("health", None),
  249. "health2": ("health", None),
  250. "weather": ("weather", None),
  251. }
  252. rec_type, tag = recommend_type_map.get(template_type, (template_type, None))
  253. for zone_id in zone_ids:
  254. if tag:
  255. first_key = f"broadcast-template-jinja:{type_id}:{zone_id}:{card_type}:{rec_type}{tag}"
  256. else:
  257. first_key = f"broadcast-template-jinja:{type_id}:{zone_id}:{card_type}:{rec_type}"
  258. first_keys.append(first_key)
  259. return {"second": second_key, "first": first_keys}
  260. def generate_template_records(all_templates: dict) -> list:
  261. """生成完整的模板记录列表
  262. Args:
  263. all_templates: {model_name: [template_info, ...]}
  264. Returns:
  265. list: 模板记录列表
  266. """
  267. records = []
  268. seen_templates = set() # 去重:model_device_type
  269. # 模板类型描述
  270. type_descriptions = {
  271. "common": "通用模板",
  272. "time_popular": "人气模板",
  273. "time_custom": "定制模板",
  274. "time_season": "季节模板",
  275. "time_season_new": "季节上新模板",
  276. "time_season_storage": "季节收纳模板",
  277. "care": "关怀模板",
  278. "environmental": "环保模板",
  279. "health": "健康模板",
  280. "health2": "健康2-杀菌模板",
  281. "weather": "天气模板",
  282. }
  283. # 处理有模板正文的机型(x11/hplus/f2_multi)
  284. for model_name in ["x11", "hplus", "f2_multi"]:
  285. if model_name not in all_templates:
  286. continue
  287. config = MACHINE_CONFIGS[model_name]
  288. templates = all_templates[model_name]
  289. for tpl in templates:
  290. raw_type = tpl["template_type"]
  291. template_type = normalize_template_type(raw_type)
  292. device_type = tpl["device_type"]
  293. # 去重
  294. dedup_key = f"{model_name}_{device_type}_{template_type}"
  295. if dedup_key in seen_templates:
  296. continue
  297. seen_templates.add(dedup_key)
  298. # 生成 ID
  299. tpl_id = f"{model_name}_{device_type}_{template_type}"
  300. # 生成 Redis keys
  301. redis_keys = generate_redis_keys(model_name, device_type, template_type, tpl["second_key"])
  302. # 描述
  303. label = config["label"]
  304. device_label = "洗衣机" if device_type == "wash" else "干衣机"
  305. type_desc = type_descriptions.get(template_type, template_type)
  306. record = {
  307. "id": tpl_id,
  308. "name": f"{label}{device_label}-{type_desc}",
  309. "category": f"{'单卡' if config['card_type'] == 'single_card' else '多卡'}{device_label}",
  310. "device_type": device_type,
  311. "machine_model": model_name,
  312. "template_type": template_type,
  313. "type_id": config["type_ids"].get(device_type, ""),
  314. "zone_ids": config["zone_ids"].get(device_type, []),
  315. "card_type": config["card_type"],
  316. "reusable_from": "",
  317. "reuse_targets": [],
  318. "description": f"{label} {device_label} {type_desc}",
  319. "output_description": "语音播报脚本",
  320. "prerequisite": "",
  321. "call_method": "",
  322. "input_variables": [],
  323. "template_body": tpl["template_body"],
  324. "redis_key_second": redis_keys["second"],
  325. "redis_keys_first": redis_keys["first"],
  326. "created_at": datetime.now().astimezone().isoformat(),
  327. "updated_at": datetime.now().astimezone().isoformat(),
  328. }
  329. records.append(record)
  330. print(f" ✓ {record['name']} (ID: {tpl_id})")
  331. # 处理复用机型(ultra/seeker/seekerR2/x11_ultra)
  332. print(f"\n🔄 处理复用机型模板...")
  333. for model_name in ["ultra", "seeker", "seekerR2", "x11_ultra"]:
  334. if model_name not in REUSE_MAP:
  335. print(f" ⏭ {model_name} 不在 REUSE_MAP 中")
  336. continue
  337. print(f"\n 🔗 处理 {model_name} 复用模板...")
  338. config = MACHINE_CONFIGS[model_name]
  339. reuse_info = REUSE_MAP[model_name]
  340. source_model = "x11" # 所有复用机型都复用 x11
  341. # 从已生成的 x11 模板中找出对应的模板
  342. x11_templates = [r for r in records if r["machine_model"] == source_model]
  343. print(f" 找到 {len(x11_templates)} 个 x11 源模板")
  344. for device_type in ["wash", "dry"]:
  345. if device_type not in reuse_info:
  346. continue
  347. source_device = reuse_info[device_type] # 例如:ultra 的 wash 复用 x11 的 wash
  348. # 找到对应的 x11 模板
  349. source_templates = [
  350. r for r in x11_templates
  351. if r["device_type"] == source_device
  352. ]
  353. print(f" 设备类型 {device_type} -> 复用 x11 的 {source_device}: {len(source_templates)} 个模板")
  354. for source_tpl in source_templates:
  355. template_type = source_tpl["template_type"]
  356. tpl_id = f"{model_name}_{device_type}_{template_type}"
  357. # 去重
  358. dedup_key = f"{model_name}_{device_type}_{template_type}"
  359. if dedup_key in seen_templates:
  360. print(f" ⏭ 跳过重复: {dedup_key}")
  361. continue
  362. seen_templates.add(dedup_key)
  363. # 生成 Redis keys
  364. redis_keys = generate_redis_keys(model_name, device_type, template_type, "")
  365. label = config["label"]
  366. device_label = "洗衣机" if device_type == "wash" else "干衣机"
  367. type_desc = type_descriptions.get(template_type, template_type)
  368. # 复用源模板 ID
  369. source_tpl_id = f"{source_model}_{source_device}_{template_type}"
  370. record = {
  371. "id": tpl_id,
  372. "name": f"{label}{device_label}-{type_desc} (复用{source_model.upper()})",
  373. "category": f"单卡{device_label}",
  374. "device_type": device_type,
  375. "machine_model": model_name,
  376. "template_type": template_type,
  377. "type_id": config["type_ids"].get(device_type, ""),
  378. "zone_ids": config["zone_ids"].get(device_type, []),
  379. "card_type": config["card_type"],
  380. "reusable_from": source_tpl_id,
  381. "reuse_targets": [],
  382. "description": f"{label} {device_label} {type_desc},复用 {source_model.upper()} 模板",
  383. "output_description": "语音播报脚本",
  384. "prerequisite": "",
  385. "call_method": "",
  386. "input_variables": [],
  387. "template_body": "", # 复用模板无正文
  388. "redis_key_second": redis_keys["second"],
  389. "redis_keys_first": redis_keys["first"],
  390. "created_at": datetime.now().astimezone().isoformat(),
  391. "updated_at": datetime.now().astimezone().isoformat(),
  392. }
  393. records.append(record)
  394. print(f" ✓ {record['name']} (ID: {tpl_id}, 复用: {source_tpl_id})")
  395. # 更新源模板的 reuse_targets
  396. for r in records:
  397. if r["id"] == source_tpl_id:
  398. if "reuse_targets" not in r:
  399. r["reuse_targets"] = []
  400. r["reuse_targets"].append(tpl_id)
  401. return records
  402. def main():
  403. # daping_rec 路径(仅读取,不修改)
  404. daping_rec_dir = Path(__file__).parent.parent.parent / "daping_rec_online_api"
  405. # 输出文件
  406. output_file = Path(__file__).parent.parent / "backend" / "data" / "templates.json"
  407. print("=" * 80)
  408. print("🚀 语音播报模板导入工具")
  409. print("=" * 80)
  410. print(f"\n📖 参考项目: {daping_rec_dir}")
  411. print(f"📝 输出文件: {output_file}")
  412. if not daping_rec_dir.exists():
  413. print(f"\n❌ 错误: 未找到 daping_rec_online_api 项目")
  414. print(f" 请确保 {daping_rec_dir} 存在")
  415. return
  416. # 提取所有模板
  417. print("\n" + "=" * 80)
  418. print("📥 步骤 1: 从 daping_rec 提取模板信息")
  419. print("=" * 80)
  420. all_templates = extract_all_templates(daping_rec_dir)
  421. if not all_templates:
  422. print("\n❌ 未提取到任何模板,请检查 daping_rec 目录结构")
  423. return
  424. print(f"\n📊 提取完成:")
  425. for model, templates in all_templates.items():
  426. print(f" • {model}: {len(templates)} 个模板")
  427. # 生成模板记录
  428. print("\n" + "=" * 80)
  429. print("📦 步骤 2: 生成模板记录(含 Redis key 和复用关系)")
  430. print("=" * 80)
  431. records = generate_template_records(all_templates)
  432. # 保存到 JSON
  433. print("\n" + "=" * 80)
  434. print("💾 步骤 3: 保存到 templates.json")
  435. print("=" * 80)
  436. output_file.parent.mkdir(parents=True, exist_ok=True)
  437. output_file.write_text(
  438. json.dumps(records, ensure_ascii=False, indent=2),
  439. encoding='utf-8'
  440. )
  441. print(f"\n✅ 导入成功!")
  442. print(f" • 模板总数: {len(records)}")
  443. print(f" • 输出文件: {output_file}")
  444. print(f"\n💡 提示: 运行后端服务后可通过 API 查看和管理这些模板")
  445. print("=" * 80)
  446. if __name__ == "__main__":
  447. main()