convert_local.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. convert_local.py
  5. Reads a local Excel file and converts its contents into a structured prompt library
  6. under `prompt-library/` per the development guide. It generates:
  7. - prompts/<category>/ (one file per non-empty cell across columns for each prompt row)
  8. - prompts/index.json (summary + traceability)
  9. - prompts/<category>/index.md (table + version matrix)
  10. - docs/tools.md, docs/support.md, docs/excel-data.md
  11. - README.md (top-level for prompt-library)
  12. Usage:
  13. python prompt-library/scripts/convert_local.py \
  14. [--excel "/absolute/or/relative/path/to/prompt (2).xlsx"] \
  15. [--config prompt-library/scripts/config.yaml] \
  16. [--category-name prompt-category]
  17. If no arguments are provided, it will:
  18. - load config from prompt-library/scripts/config.yaml (if present)
  19. - resolve Excel path from config.source.excel_file relative to project root
  20. - default category to "prompt-category"
  21. Dependencies: pandas, openpyxl, PyYAML
  22. """
  23. from __future__ import annotations
  24. import argparse
  25. import json
  26. import re
  27. from dataclasses import dataclass
  28. from datetime import datetime
  29. from pathlib import Path
  30. from typing import Dict, List, Optional, Tuple
  31. import pandas as pd
  32. try:
  33. import yaml # type: ignore
  34. except Exception: # pragma: no cover
  35. yaml = None # Optional; script still works without YAML if no config provided
  36. @dataclass
  37. class RowClassification:
  38. row_index: int # zero-based excel index
  39. kind: str # prompt|tool|social|wallet_header|wallet|warning|other
  40. data: Dict
  41. class ExcelPromptConverter:
  42. def __init__(
  43. self,
  44. project_root: Path,
  45. prompt_library_dir: Path,
  46. excel_path: Path,
  47. category_name: str = "prompt-category",
  48. config_path: Optional[Path] = None,
  49. output_root: Optional[Path] = None,
  50. ) -> None:
  51. self.project_root = project_root
  52. self.prompt_library_dir = prompt_library_dir
  53. # If an output_root is provided, write into that snapshot directory
  54. # rather than the in-repo prompts/docs locations.
  55. if output_root is not None:
  56. self.output_root = output_root
  57. self.prompts_dir = output_root / "prompts"
  58. self.docs_dir = output_root / "docs"
  59. self.readme_target_root = output_root
  60. else:
  61. self.output_root = None
  62. self.prompts_dir = prompt_library_dir / "prompts"
  63. self.docs_dir = prompt_library_dir / "docs"
  64. self.readme_target_root = prompt_library_dir
  65. self.scripts_dir = prompt_library_dir / "scripts"
  66. self.category_name = category_name # fallback if single sheet
  67. self.category_dir = self.prompts_dir / self.category_name
  68. self.excel_path = excel_path
  69. self.config_path = config_path
  70. self.config = self._load_config(config_path)
  71. self.now = datetime.now()
  72. # Per-sheet prompts map: {sheet_name: {excel_row -> {title, versions{col->file}}}}
  73. self.prompts_info_by_sheet: Dict[str, Dict[int, Dict]] = {}
  74. self.tools: List[Dict] = []
  75. self.social: List[Dict] = []
  76. self.wallets: Dict[str, Dict] = {}
  77. self.misc: List[Dict] = []
  78. self.total_rows = 0
  79. self.total_cols = 0
  80. self.sheet_names_order: List[str] = []
  81. def _load_config(self, config_path: Optional[Path]) -> Dict:
  82. if config_path and config_path.exists() and yaml is not None:
  83. with config_path.open("r", encoding="utf-8") as f:
  84. return yaml.safe_load(f) or {}
  85. return {}
  86. def _sanitize_filename(self, text: str, max_length: int = 60) -> str:
  87. if not text:
  88. return "untitled"
  89. text = str(text).strip()
  90. text = re.sub(r"[\\/:*?\"<>|\r\n]+", "", text)
  91. text = text.replace(" ", "_")
  92. if len(text) > max_length:
  93. text = text[:max_length].rstrip("_-")
  94. return text or "untitled"
  95. def _extract_title(self, contents: List[str]) -> str:
  96. for c in contents:
  97. if c and c.strip():
  98. first_line = c.strip().splitlines()[0]
  99. words = first_line.split()
  100. candidate = " ".join(words[:6])
  101. return self._sanitize_filename(candidate)
  102. return "untitled"
  103. def _read_excel_sheets(self) -> Dict[str, pd.DataFrame]:
  104. # Read all sheets; if workbook has single sheet, still returns dict with one entry
  105. sheets: Dict[str, pd.DataFrame] = pd.read_excel(self.excel_path, header=None, engine="openpyxl", sheet_name=None) # type: ignore
  106. normalized: Dict[str, pd.DataFrame] = {}
  107. for sheet_name, df in sheets.items():
  108. try:
  109. df = df.map(lambda v: v.strip() if isinstance(v, str) else v) # pandas >=2.1
  110. except Exception:
  111. df = df.applymap(lambda v: v.strip() if isinstance(v, str) else v) # fallback
  112. normalized[sheet_name] = df
  113. # preserve order of sheets
  114. self.sheet_names_order = list(normalized.keys())
  115. # set global rows/cols to first sheet for summary; detailed per-sheet handled later
  116. if normalized:
  117. any_df = normalized[self.sheet_names_order[0]]
  118. self.total_rows, self.total_cols = any_df.shape
  119. return normalized
  120. def _classify_rows(self, df: pd.DataFrame) -> List[RowClassification]:
  121. classifications: List[RowClassification] = []
  122. wallet_mode = False
  123. for r in range(df.shape[0]):
  124. row_vals = [df.iloc[r, c] if c < df.shape[1] else None for c in range(df.shape[1])]
  125. non_empty = [v for v in row_vals if isinstance(v, str) and v.strip()]
  126. any_http = any(isinstance(v, str) and v.startswith("http") for v in row_vals)
  127. if not non_empty:
  128. classifications.append(RowClassification(r, "other", {"empty": True}))
  129. continue
  130. # Wallet header detection (e.g., contains "网络" and a label like "礼貌要饭地址")
  131. joined = " ".join([v for v in non_empty])
  132. if any(k in joined for k in ["网络", "网络名称"]) and any(
  133. k in joined for k in ["礼貌要饭地址", "钱包", "地址"]
  134. ):
  135. wallet_mode = True
  136. classifications.append(RowClassification(r, "wallet_header", {"raw": row_vals}))
  137. continue
  138. if wallet_mode:
  139. # If the row still looks like wallet data (two columns: network, address)
  140. first, second = row_vals[0] if len(row_vals) > 0 else None, row_vals[1] if len(row_vals) > 1 else None
  141. if (first and isinstance(first, str)) and (second and isinstance(second, str)):
  142. classifications.append(
  143. RowClassification(
  144. r,
  145. "wallet",
  146. {
  147. "network": first,
  148. "address": second,
  149. "raw": row_vals,
  150. },
  151. )
  152. )
  153. continue
  154. else:
  155. wallet_mode = False # end wallet section if pattern breaks
  156. # Tools and social heuristics
  157. if any_http:
  158. url = next(v for v in row_vals if isinstance(v, str) and v.startswith("http"))
  159. desc = None
  160. for v in row_vals:
  161. if v and isinstance(v, str) and not v.startswith("http"):
  162. desc = v
  163. break
  164. kind = "social" if ("x.com" in url or "twitter.com" in url) else "tool"
  165. classifications.append(RowClassification(r, kind, {"url": url, "description": desc or "", "raw": row_vals}))
  166. continue
  167. # Warnings or misc markers
  168. if any("广告位" in v for v in non_empty if isinstance(v, str)):
  169. classifications.append(RowClassification(r, "warning", {"content": joined, "raw": row_vals}))
  170. continue
  171. # Placeholder rows to ignore as prompts
  172. if any(v in {"...", "….", "...."} for v in non_empty):
  173. classifications.append(RowClassification(r, "other", {"placeholder": True, "raw": row_vals}))
  174. continue
  175. # Otherwise: treat as prompt row (one logical prompt per row with multiple versions across columns)
  176. prompt_versions: Dict[int, str] = {}
  177. for c in range(df.shape[1]):
  178. cell = df.iloc[r, c] if c < df.shape[1] else None
  179. if isinstance(cell, str) and cell.strip():
  180. prompt_versions[c + 1] = cell.strip()
  181. if prompt_versions:
  182. classifications.append(RowClassification(r, "prompt", {"versions": prompt_versions}))
  183. else:
  184. classifications.append(RowClassification(r, "other", {"raw": row_vals}))
  185. return classifications
  186. def _ensure_dirs(self) -> None:
  187. self.prompts_dir.mkdir(parents=True, exist_ok=True)
  188. self.category_dir.mkdir(parents=True, exist_ok=True)
  189. self.docs_dir.mkdir(parents=True, exist_ok=True)
  190. def _write_prompt_file(self, row_num: int, col_num: int, title: str, content: str, versions_in_row: List[int]) -> str:
  191. """Write a prompt file containing ONLY the prompt text, nothing else."""
  192. row_col = f"({row_num},{col_num})"
  193. filename = f"{row_col}_{title}.md"
  194. filepath = self.category_dir / filename
  195. # Ensure content ends with newline and contains no surrounding fences/headers added by us
  196. pure = (content or "").rstrip("\n") + "\n"
  197. filepath.write_text(pure, encoding="utf-8")
  198. return filename
  199. def _generate_category_index(self, sheet_name: str, category_dir: Path, prompts_info: Dict[int, Dict]) -> None:
  200. index_path = category_dir / "index.md"
  201. total_prompts = len(prompts_info)
  202. total_versions = sum(len(meta["versions"]) for meta in prompts_info.values())
  203. avg_versions = total_versions / total_prompts if total_prompts else 0
  204. lines: List[str] = []
  205. lines.append(f"# 📂 提示词分类 - {sheet_name}(基于Excel原始数据)\n")
  206. lines.append(f"最后同步: {self.now.strftime('%Y-%m-%d %H:%M:%S')}\n")
  207. lines.append("\n## 📊 统计\n")
  208. lines.append(f"- 提示词总数: {total_prompts}\n")
  209. lines.append(f"- 版本总数: {total_versions} \n")
  210. lines.append(f"- 平均版本数: {avg_versions:.1f}\n\n")
  211. lines.append("## 📋 提示词列表\n")
  212. lines.append("\n| 序号 | 标题 | 版本数 | 查看 |\n|------|------|--------|------|\n")
  213. for row in sorted(prompts_info.keys()):
  214. info = prompts_info[row]
  215. title = info["title"]
  216. versions = info["versions"]
  217. links = " / ".join([f"[v{v}](./({row},{v})_{title}.md)" for v in sorted(versions.keys())])
  218. lines.append(f"| {row} | {title} | {len(versions)} | {links} |\n")
  219. # Version matrix
  220. max_col = 0
  221. for info in prompts_info.values():
  222. if info["versions"]:
  223. max_col = max(max_col, max(info["versions"].keys()))
  224. lines.append("\n## 🗂️ 版本矩阵\n")
  225. header = ["行"] + [f"v{i}" for i in range(1, max_col + 1)] + ["备注"]
  226. lines.append("\n| " + " | ".join(header) + " |\n" + "|" + "---|" * len(header) + "\n")
  227. for row in sorted(prompts_info.keys()):
  228. info = prompts_info[row]
  229. row_cells = [str(row)]
  230. for c in range(1, max_col + 1):
  231. row_cells.append("✅" if c in info["versions"] else "—")
  232. row_cells.append("")
  233. lines.append("| " + " | ".join(row_cells) + " |\n")
  234. index_path.write_text("\n".join(lines), encoding="utf-8")
  235. def _generate_prompts_index_json(self) -> None:
  236. index_json_path = self.prompts_dir / "index.json"
  237. total_prompts = sum(len(p) for p in self.prompts_info_by_sheet.values())
  238. total_versions = sum(sum(len(meta["versions"]) for meta in p.values()) for p in self.prompts_info_by_sheet.values())
  239. stats = {
  240. "sheets": len(self.prompts_info_by_sheet),
  241. "prompts": total_prompts,
  242. "versions": total_versions,
  243. "tools": len(self.tools) if self.tools else 0,
  244. "social_accounts": len(self.social) if self.social else 0,
  245. "crypto_wallets": len(self.wallets) if self.wallets else 0,
  246. }
  247. categories = []
  248. for sheet_name in self.sheet_names_order:
  249. prompts_info = self.prompts_info_by_sheet.get(sheet_name, {})
  250. categories.append(
  251. {
  252. "name": sheet_name,
  253. "prompt_count": len(prompts_info),
  254. "version_count": sum(len(meta["versions"]) for meta in prompts_info.values()),
  255. "prompts": [
  256. {
  257. "row": row,
  258. "title": info["title"],
  259. "versions": sorted(list(info["versions"].keys())),
  260. "files": [info["versions"][v] for v in sorted(info["versions"].keys())],
  261. }
  262. for row, info in sorted(prompts_info.items())
  263. ],
  264. }
  265. )
  266. excel_data = {
  267. "total_rows": self.total_rows,
  268. "total_cols": self.total_cols,
  269. "sheets": list(self.prompts_info_by_sheet.keys()),
  270. }
  271. tools = {}
  272. if self.tools:
  273. for t in self.tools:
  274. name = t.get("name") or "tool"
  275. tools[name] = {k: v for k, v in t.items() if k != "name"}
  276. social_media = {}
  277. if self.social:
  278. for s in self.social:
  279. name = s.get("name") or "social"
  280. social_media[name] = {k: v for k, v in s.items() if k != "name"}
  281. support = {
  282. "description": "礼貌要饭地址",
  283. "crypto_wallets": self.wallets,
  284. }
  285. data = {
  286. "last_updated": self.now.strftime("%Y-%m-%dT%H:%M:%S"),
  287. "source": self.excel_path.name,
  288. "stats": stats,
  289. "categories": categories,
  290. "excel_data": excel_data,
  291. "tools": tools,
  292. "social_media": social_media,
  293. "support": support,
  294. "misc": self.misc,
  295. }
  296. index_json_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
  297. def _generate_docs(self, sheets: Dict[str, pd.DataFrame]) -> None:
  298. # docs/excel-data.md (full table)
  299. excel_doc_path = self.docs_dir / "excel-data.md"
  300. lines: List[str] = []
  301. lines.append("# 📊 Excel原始数据完整记录\n")
  302. lines.append("## 数据来源\n")
  303. lines.append(f"- **文件**: {self.excel_path.name}\n")
  304. lines.append(f"- **处理时间**: {self.now.strftime('%Y-%m-%d')}\n")
  305. lines.append(f"- **工作表数量**: {len(sheets)}\n\n")
  306. for sheet_name, df in sheets.items():
  307. rows, cols = df.shape
  308. lines.append(f"## 工作表: {sheet_name} ({rows}行×{cols}列)\n")
  309. lines.append("\n| 行号 | 列1 | 列2 | 列3 |\n|-----:|-----|-----|-----|\n")
  310. for r in range(rows):
  311. c1 = df.iloc[r, 0] if cols > 0 else ""
  312. c2 = df.iloc[r, 1] if cols > 1 else ""
  313. c3 = df.iloc[r, 2] if cols > 2 else ""
  314. def fmt(x) -> str:
  315. try:
  316. if x is None or (isinstance(x, float) and pd.isna(x)) or (hasattr(pd, 'isna') and pd.isna(x)):
  317. return ""
  318. except Exception:
  319. pass
  320. s = str(x)
  321. return s.replace("|", "\\|")
  322. lines.append(f"| {r} | {fmt(c1)} | {fmt(c2)} | {fmt(c3)} |\n")
  323. lines.append("\n")
  324. lines.append("\n---\n*完整数据提取自 {0}*\n".format(self.excel_path.name))
  325. excel_doc_path.write_text("\n".join(lines), encoding="utf-8")
  326. # docs/tools.md
  327. tools_path = self.docs_dir / "tools.md"
  328. t_lines: List[str] = []
  329. t_lines.append("# 🛠️ 工具与资源(从Excel提取)\n")
  330. if self.tools:
  331. t_lines.append("\n## AI优化工具\n")
  332. for t in self.tools:
  333. t_lines.append("\n### {0}\n- **URL**: {1}\n- **描述**: {2}\n- **数据来源**: Excel表格第{3}行\n".format(
  334. t.get("name") or "工具",
  335. t.get("url", ""),
  336. t.get("description", ""),
  337. (t.get("excel_row") or 0) + 1,
  338. ))
  339. if self.social:
  340. t_lines.append("\n## 社交媒体\n")
  341. for s in self.social:
  342. t_lines.append("\n### {0}\n- **URL**: {1}\n- **描述**: {2}\n- **数据来源**: Excel表格第{3}行\n".format(
  343. s.get("name") or "社交账号",
  344. s.get("url", ""),
  345. s.get("description", ""),
  346. (s.get("excel_row") or 0) + 1,
  347. ))
  348. t_lines.append("\n## 使用建议\n\n1. **OpenAI优化器**: 可以用来测试和改进本库中的提示词\n2. **社交媒体**: 关注获取项目更新和使用技巧\n3. **集成方式**: 可以将这些工具集成到自动化工作流中\n\n---\n*数据来源: {0}*\n".format(self.excel_path.name))
  349. tools_path.write_text("\n".join(t_lines), encoding="utf-8")
  350. # docs/support.md
  351. support_path = self.docs_dir / "support.md"
  352. s_lines: List[str] = []
  353. s_lines.append("# 💰 项目支持(从Excel提取)\n")
  354. s_lines.append("\n## 支持说明\n**礼貌要饭地址** - 如果这个项目对您有帮助,欢迎通过以下方式支持\n")
  355. if self.wallets:
  356. s_lines.append("\n## 加密货币钱包地址\n\n### 主流网络支持\n")
  357. s_lines.append("\n| 网络名称 | 钱包地址 | Excel行号 |\n|----------|----------|-----------|\n")
  358. for net, data in self.wallets.items():
  359. s_lines.append("| **{0}** | `{1}` | 第{2}行 |\n".format(net.upper(), data.get("address", ""), (data.get("excel_row") or 0) + 1))
  360. if self.misc:
  361. for m in self.misc:
  362. if m.get("type") == "warning" or "广告位" in m.get("content", ""):
  363. s_lines.append("\n⚠️ **重要提醒**: {0}\n".format(m.get("content")))
  364. s_lines.append("\n### 使用建议\n1. 请确认钱包地址的准确性\n2. 建议小额测试后再进行大额转账\n3. 不同网络的转账费用不同,请选择合适的网络\n\n---\n*钱包地址来源: {0}*\n".format(self.excel_path.name))
  365. support_path.write_text("\n".join(s_lines), encoding="utf-8")
  366. def _generate_readme(self) -> None:
  367. readme_path = self.readme_target_root / "README.md"
  368. total_prompts = sum(len(p) for p in self.prompts_info_by_sheet.values())
  369. total_versions = sum(sum(len(meta["versions"]) for meta in p.values()) for p in self.prompts_info_by_sheet.values())
  370. readme = []
  371. readme.append("# 📚 提示词库(Excel转换版)\n")
  372. readme.append("![同步状态](https://img.shields.io/badge/status-synced-green)")
  373. readme.append(f"![提示词数量](https://img.shields.io/badge/prompts-{total_prompts}-blue)")
  374. readme.append(f"![版本总数](https://img.shields.io/badge/versions-{total_versions}-orange)")
  375. readme.append(f"![数据来源](https://img.shields.io/badge/source-Excel-yellow)\n")
  376. readme.append(f"最后更新: {self.now.strftime('%Y-%m-%d %H:%M:%S')}\n")
  377. readme.append("\n## 📊 总览\n")
  378. readme.append(f"- **数据来源**: {self.excel_path.name}\n")
  379. readme.append(f"- **分类数量**: {len(self.prompts_info_by_sheet)} \n- **提示词总数**: {total_prompts}\n- **版本总数**: {total_versions}\n")
  380. readme.append("\n## 📂 分类导航\n")
  381. for i, sheet_name in enumerate(self.sheet_names_order, start=1):
  382. prompts_info = self.prompts_info_by_sheet.get(sheet_name, {})
  383. folder = f"({i})_{self._sanitize_filename(sheet_name)}"
  384. ver_count = sum(len(meta["versions"]) for meta in prompts_info.values())
  385. readme.append(f"- [{sheet_name}](./prompts/{folder}/) - {len(prompts_info)} 个提示词, {ver_count} 个版本\n")
  386. readme.append("\n## 🔄 同步信息\n")
  387. readme.append(f"- **数据源**: {self.excel_path.name}\n- **处理时间**: {self.now.strftime('%Y-%m-%d %H:%M:%S')}\n")
  388. readme.append("\n## 📝 许可证\n本项目采用 MIT 许可证\n")
  389. readme.append("\n---\n*完全基于 Excel 表格自动生成*\n")
  390. readme_path.write_text("\n".join(readme), encoding="utf-8")
  391. def convert(self) -> None:
  392. self._ensure_dirs()
  393. sheets = self._read_excel_sheets()
  394. # If no sheets returned (shouldn't happen), fallback to empty
  395. for idx, sheet_name in enumerate(self.sheet_names_order, start=1):
  396. df = sheets[sheet_name]
  397. # Prepare per-sheet folder
  398. folder_name = f"({idx})_{self._sanitize_filename(sheet_name)}"
  399. category_dir = self.prompts_dir / folder_name
  400. category_dir.mkdir(parents=True, exist_ok=True)
  401. # Classify rows
  402. rows = self._classify_rows(df)
  403. prompts_info: Dict[int, Dict] = {}
  404. # Build prompt files for this sheet
  405. for rc in rows:
  406. if rc.kind == "prompt":
  407. excel_row_number = rc.row_index + 1
  408. versions: Dict[int, str] = rc.data["versions"]
  409. title = self._extract_title(list(versions.values()))
  410. prompts_info[excel_row_number] = {"title": title, "versions": {}}
  411. # Rewrite files directly into category_dir
  412. for col_num, content in versions.items():
  413. row_col = f"({excel_row_number},{col_num})"
  414. filename = f"{row_col}_{title}.md"
  415. (category_dir / filename).write_text((content or "").rstrip("\n") + "\n", encoding="utf-8")
  416. prompts_info[excel_row_number]["versions"][col_num] = filename
  417. elif rc.kind == "tool":
  418. url = rc.data.get("url", "")
  419. self.tools.append({
  420. "name": "OpenAI 提示词优化平台" if "openai" in url else "工具",
  421. "url": url,
  422. "description": rc.data.get("description", ""),
  423. "excel_row": rc.row_index,
  424. "sheet": sheet_name,
  425. })
  426. elif rc.kind == "social":
  427. url = rc.data.get("url", "")
  428. name = "Twitter/X 账号" if ("x.com" in url or "twitter.com" in url) else "社交账号"
  429. self.social.append({
  430. "name": name,
  431. "url": url,
  432. "description": rc.data.get("description", ""),
  433. "excel_row": rc.row_index,
  434. "sheet": sheet_name,
  435. })
  436. elif rc.kind == "wallet":
  437. network = str(rc.data.get("network", "")).strip()
  438. address = str(rc.data.get("address", "")).strip()
  439. if network and address:
  440. self.wallets[network.lower()] = {
  441. "address": address,
  442. "excel_row": rc.row_index,
  443. "sheet": sheet_name,
  444. }
  445. elif rc.kind == "warning":
  446. self.misc.append({"type": "warning", "excel_row": rc.row_index, "content": rc.data.get("content", ""), "sheet": sheet_name})
  447. # Save per-sheet prompts map and index
  448. self.prompts_info_by_sheet[sheet_name] = prompts_info
  449. self._generate_category_index(sheet_name, category_dir, prompts_info)
  450. # Global indices and docs
  451. self._generate_prompts_index_json()
  452. self._generate_docs(sheets)
  453. self._generate_readme()
  454. def parse_args() -> argparse.Namespace:
  455. parser = argparse.ArgumentParser(description="Convert local Excel into prompt library structure")
  456. parser.add_argument("--excel", type=str, default=None, help="Path to the Excel file (default from config)")
  457. parser.add_argument("--config", type=str, default=None, help="Path to config.yaml (optional)")
  458. parser.add_argument("--category-name", type=str, default="prompt-category", help="Output category folder name")
  459. parser.add_argument("--out-dir", type=str, default=None, help="Optional snapshot output root. If set, writes to <out-dir>/prompts and <out-dir>/docs")
  460. return parser.parse_args()
  461. def main() -> None:
  462. args = parse_args()
  463. script_path = Path(__file__).resolve()
  464. prompt_library_dir = script_path.parent.parent
  465. project_root = prompt_library_dir.parent
  466. config_path = Path(args.config).resolve() if args.config else (prompt_library_dir / "scripts" / "config.yaml")
  467. # Resolve Excel path
  468. if args.excel:
  469. excel_path = Path(args.excel)
  470. if not excel_path.is_absolute():
  471. excel_path = (project_root / excel_path).resolve()
  472. else:
  473. # Try config
  474. cfg_excel = None
  475. if config_path.exists() and yaml is not None:
  476. with config_path.open("r", encoding="utf-8") as f:
  477. cfg = yaml.safe_load(f) or {}
  478. cfg_excel = ((cfg.get("source") or {}).get("excel_file") or None)
  479. excel_path = (project_root / cfg_excel).resolve() if cfg_excel else (project_root / "prompt (2).xlsx").resolve()
  480. if not excel_path.exists():
  481. raise FileNotFoundError(f"Excel file not found: {excel_path}")
  482. out_dir = Path(args.out_dir).resolve() if args.out_dir else None
  483. converter = ExcelPromptConverter(
  484. project_root=project_root,
  485. prompt_library_dir=prompt_library_dir,
  486. excel_path=excel_path,
  487. category_name=args.category_name,
  488. config_path=config_path if config_path.exists() else None,
  489. output_root=out_dir,
  490. )
  491. converter.convert()
  492. target = out_dir if out_dir else prompt_library_dir
  493. print(f"✅ Conversion complete. Output under: {target}")
  494. if __name__ == "__main__":
  495. main()