@leejungkiin/awkit 1.3.8 → 1.4.2

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 (135) hide show
  1. package/bin/awk.js +630 -52
  2. package/bin/claude-generators.js +122 -0
  3. package/core/AGENTS.md +54 -0
  4. package/core/CLAUDE.md +155 -0
  5. package/core/GEMINI.md +44 -9
  6. package/core/GEMINI.md.bak +126 -199
  7. package/package.json +1 -1
  8. package/skills/ai-sprite-maker/SKILL.md +81 -0
  9. package/skills/ai-sprite-maker/scripts/animate_sprite.py +102 -0
  10. package/skills/ai-sprite-maker/scripts/process_sprites.py +140 -0
  11. package/skills/awf-session-restore/SKILL.md +12 -2
  12. package/skills/brainstorm-agent/SKILL.md +11 -8
  13. package/skills/code-review/SKILL.md +21 -33
  14. package/skills/gitnexus/gitnexus-cli/SKILL.md +82 -0
  15. package/skills/gitnexus/gitnexus-debugging/SKILL.md +89 -0
  16. package/skills/gitnexus/gitnexus-exploring/SKILL.md +78 -0
  17. package/skills/gitnexus/gitnexus-guide/SKILL.md +64 -0
  18. package/skills/gitnexus/gitnexus-impact-analysis/SKILL.md +97 -0
  19. package/skills/gitnexus/gitnexus-refactoring/SKILL.md +121 -0
  20. package/skills/lucylab-tts/SKILL.md +64 -0
  21. package/skills/lucylab-tts/resources/voices_library.json +908 -0
  22. package/skills/lucylab-tts/scripts/.env +1 -0
  23. package/skills/lucylab-tts/scripts/lucylab_tts.py +506 -0
  24. package/skills/nm-memory-sync/SKILL.md +14 -1
  25. package/skills/orchestrator/SKILL.md +5 -38
  26. package/skills/ship-to-code/SKILL.md +115 -0
  27. package/skills/short-maker/SKILL.md +150 -0
  28. package/skills/short-maker/_backup/storyboard.html +106 -0
  29. package/skills/short-maker/_backup/video_mixer.py +296 -0
  30. package/skills/short-maker/outputs/fitbite-promo/background.jpg +0 -0
  31. package/skills/short-maker/outputs/fitbite-promo/final/promo-final.mp4 +0 -0
  32. package/skills/short-maker/outputs/fitbite-promo/script.md +19 -0
  33. package/skills/short-maker/outputs/fitbite-promo/segments/scene-01.mp4 +0 -0
  34. package/skills/short-maker/outputs/fitbite-promo/segments/scene-02.mp4 +0 -0
  35. package/skills/short-maker/outputs/fitbite-promo/segments/scene-03.mp4 +0 -0
  36. package/skills/short-maker/outputs/fitbite-promo/segments/scene-04.mp4 +0 -0
  37. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-01.png +0 -0
  38. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-02.png +0 -0
  39. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-03.png +0 -0
  40. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-04.png +0 -0
  41. package/skills/short-maker/outputs/fitbite-promo/storyboard.html +133 -0
  42. package/skills/short-maker/outputs/fitbite-promo/storyboard.json +38 -0
  43. package/skills/short-maker/outputs/fitbite-promo/temp/merged_chroma.mp4 +0 -0
  44. package/skills/short-maker/outputs/fitbite-promo/temp/merged_crossfaded.mp4 +0 -0
  45. package/skills/short-maker/outputs/fitbite-promo/temp/ready_00.mp4 +0 -0
  46. package/skills/short-maker/outputs/fitbite-promo/temp/ready_01.mp4 +0 -0
  47. package/skills/short-maker/outputs/fitbite-promo/temp/ready_02.mp4 +0 -0
  48. package/skills/short-maker/outputs/fitbite-promo/temp/ready_03.mp4 +0 -0
  49. package/skills/short-maker/outputs/fitbite-promo/tts/manifest.json +31 -0
  50. package/skills/short-maker/outputs/fitbite-promo/tts/scene-01.wav +0 -0
  51. package/skills/short-maker/outputs/fitbite-promo/tts/scene-02.wav +0 -0
  52. package/skills/short-maker/outputs/fitbite-promo/tts/scene-03.wav +0 -0
  53. package/skills/short-maker/outputs/fitbite-promo/tts/scene-04.wav +0 -0
  54. package/skills/short-maker/outputs/fitbite-promo/tts_script.txt +11 -0
  55. package/skills/short-maker/scripts/google-flow-cli/.project-identity +41 -0
  56. package/skills/short-maker/scripts/google-flow-cli/.trae/rules/project_rules.md +52 -0
  57. package/skills/short-maker/scripts/google-flow-cli/CODEBASE.md +67 -0
  58. package/skills/short-maker/scripts/google-flow-cli/GoogleFlowCli.code-workspace +29 -0
  59. package/skills/short-maker/scripts/google-flow-cli/README.md +168 -0
  60. package/skills/short-maker/scripts/google-flow-cli/docs/specs/PROJECT.md +12 -0
  61. package/skills/short-maker/scripts/google-flow-cli/docs/specs/REQUIREMENTS.md +22 -0
  62. package/skills/short-maker/scripts/google-flow-cli/docs/specs/ROADMAP.md +16 -0
  63. package/skills/short-maker/scripts/google-flow-cli/docs/specs/TECH-SPEC.md +13 -0
  64. package/skills/short-maker/scripts/google-flow-cli/gflow/__init__.py +3 -0
  65. package/skills/short-maker/scripts/google-flow-cli/gflow/api/__init__.py +19 -0
  66. package/skills/short-maker/scripts/google-flow-cli/gflow/api/client.py +1921 -0
  67. package/skills/short-maker/scripts/google-flow-cli/gflow/api/models.py +64 -0
  68. package/skills/short-maker/scripts/google-flow-cli/gflow/api/rpc_ids.py +98 -0
  69. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/__init__.py +15 -0
  70. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/browser_auth.py +692 -0
  71. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/humanizer.py +417 -0
  72. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/proxy_ext.py +120 -0
  73. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/recaptcha.py +482 -0
  74. package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/__init__.py +5 -0
  75. package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/client.py +414 -0
  76. package/skills/short-maker/scripts/google-flow-cli/gflow/cli/__init__.py +1 -0
  77. package/skills/short-maker/scripts/google-flow-cli/gflow/cli/main.py +1075 -0
  78. package/skills/short-maker/scripts/google-flow-cli/pyproject.toml +36 -0
  79. package/skills/short-maker/scripts/google-flow-cli/script.txt +22 -0
  80. package/skills/short-maker/scripts/google-flow-cli/tests/__init__.py +0 -0
  81. package/skills/short-maker/scripts/google-flow-cli/tests/test_batchexecute.py +113 -0
  82. package/skills/short-maker/scripts/google-flow-cli/tests/test_client.py +190 -0
  83. package/skills/short-maker/templates/aida_script.md +40 -0
  84. package/skills/short-maker/templates/mimic_analyzer.md +29 -0
  85. package/skills/single-flow-task-execution/SKILL.md +412 -0
  86. package/skills/single-flow-task-execution/code-quality-reviewer-prompt.md +20 -0
  87. package/skills/single-flow-task-execution/implementer-prompt.md +78 -0
  88. package/skills/single-flow-task-execution/spec-reviewer-prompt.md +61 -0
  89. package/skills/skill-creator/SKILL.md +44 -0
  90. package/skills/spm-build-analysis/SKILL.md +92 -0
  91. package/skills/spm-build-analysis/references/build-optimization-sources.md +155 -0
  92. package/skills/spm-build-analysis/references/recommendation-format.md +85 -0
  93. package/skills/spm-build-analysis/references/spm-analysis-checks.md +105 -0
  94. package/skills/spm-build-analysis/scripts/check_spm_pins.py +118 -0
  95. package/skills/symphony-enforcer/SKILL.md +83 -97
  96. package/skills/symphony-orchestrator/SKILL.md +1 -1
  97. package/skills/trello-sync/SKILL.md +52 -45
  98. package/skills/verification-gate/SKILL.md +13 -2
  99. package/skills/xcode-build-benchmark/SKILL.md +88 -0
  100. package/skills/xcode-build-benchmark/references/benchmark-artifacts.md +94 -0
  101. package/skills/xcode-build-benchmark/references/benchmarking-workflow.md +67 -0
  102. package/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json +230 -0
  103. package/skills/xcode-build-benchmark/scripts/benchmark_builds.py +308 -0
  104. package/skills/xcode-build-fixer/SKILL.md +218 -0
  105. package/skills/xcode-build-fixer/references/build-settings-best-practices.md +216 -0
  106. package/skills/xcode-build-fixer/references/fix-patterns.md +290 -0
  107. package/skills/xcode-build-fixer/references/recommendation-format.md +85 -0
  108. package/skills/xcode-build-fixer/scripts/benchmark_builds.py +308 -0
  109. package/skills/xcode-build-orchestrator/SKILL.md +156 -0
  110. package/skills/xcode-build-orchestrator/references/benchmark-artifacts.md +94 -0
  111. package/skills/xcode-build-orchestrator/references/build-settings-best-practices.md +216 -0
  112. package/skills/xcode-build-orchestrator/references/orchestration-report-template.md +143 -0
  113. package/skills/xcode-build-orchestrator/references/recommendation-format.md +85 -0
  114. package/skills/xcode-build-orchestrator/scripts/benchmark_builds.py +308 -0
  115. package/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py +273 -0
  116. package/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py +533 -0
  117. package/skills/xcode-compilation-analyzer/SKILL.md +89 -0
  118. package/skills/xcode-compilation-analyzer/references/build-optimization-sources.md +155 -0
  119. package/skills/xcode-compilation-analyzer/references/code-compilation-checks.md +106 -0
  120. package/skills/xcode-compilation-analyzer/references/recommendation-format.md +85 -0
  121. package/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py +273 -0
  122. package/skills/xcode-project-analyzer/SKILL.md +76 -0
  123. package/skills/xcode-project-analyzer/references/build-optimization-sources.md +155 -0
  124. package/skills/xcode-project-analyzer/references/build-settings-best-practices.md +216 -0
  125. package/skills/xcode-project-analyzer/references/project-audit-checks.md +101 -0
  126. package/skills/xcode-project-analyzer/references/recommendation-format.md +85 -0
  127. package/templates/CODEBASE.md +26 -42
  128. package/templates/configs/trello-config.json +2 -2
  129. package/templates/workflow_dual_mode_template.md +5 -5
  130. package/workflows/_uncategorized/conductor-codex.md +125 -0
  131. package/workflows/_uncategorized/conductor.md +97 -0
  132. package/workflows/_uncategorized/ship-to-code.md +85 -0
  133. package/workflows/_uncategorized/trello-sync.md +52 -0
  134. package/workflows/context/codebase-sync.md +10 -87
  135. package/workflows/quality/visual-debug.md +66 -12
@@ -0,0 +1 @@
1
+ LUCYLAB_BEARER=eyJhbGciOiJSUzI1NiIsImtpZCI6IjM3MzAwNzY5YTA3ZTA1MTE2ZjdlNTEzOGZhOTA5MzY4NWVlYmMyNDAiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiTmd1eeG7hW4gVHXhuqVuIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FDZzhvY0lWNUR3X3dONnpNYmNzOEZkT3IwUWw5ZjlWU1VhMlhPbTMxdEkzc3VMMmI2MzJBUFk9czk2LWMiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbHVjeS1jNjU0MyIsImF1ZCI6Imx1Y3ktYzY1NDMiLCJhdXRoX3RpbWUiOjE3NzQ3MDMxNDMsInVzZXJfaWQiOiJzd1RuUHhicGxJT0F3N2Z6NWtTY3Y2S08wdFMyIiwic3ViIjoic3dUblB4YnBsSU9BdzdmejVrU2N2NktPMHRTMiIsImlhdCI6MTc3NDc2MDExOSwiZXhwIjoxNzc0NzYzNzE5LCJlbWFpbCI6InNreW5ldHgzM0BnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJnb29nbGUuY29tIjpbIjExODQyMTU3Mzg2NTk3NDU3OTQ4MCJdLCJlbWFpbCI6WyJza3luZXR4MzNAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoiZ29vZ2xlLmNvbSJ9fQ.nfeWlALjyxuuW5vefLX221BLbEsi9OqypL26fBIgQkiP19TqEuzW6upm-HRz64pcDXnLSOk2ocCvKNLu6RzxDjbjh5T39TAWj1cU-XkGyyPUKFoq7nd2UWyhuTL54_UtUijtYr6YYei_BRwFvPCJ8W9wjNYhbZ6jBypmqZY_vkMKbQK-j3cT_Xom9FzT0L3xCMB5VVzzZ3eST_qyIdyANCEWHc_KAKWlbmcRNWIVkSXkf0eGK2FYzWgViyqqBj59UazHLEvOkvGxlZ20XkGu76uGBIb6t6j1nkTrF3L_-efxO2e90j8E_KdnF9S-Jpu5A1tc9-d9e8VaqahGL3p-Gw
@@ -0,0 +1,506 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import base64
5
+ import json
6
+ import os
7
+ import re
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import requests
13
+
14
+
15
+ def _load_json_documents(path: Path) -> list[Any]:
16
+ text = path.read_text(encoding="utf-8")
17
+ decoder = json.JSONDecoder()
18
+ docs: list[Any] = []
19
+ i = 0
20
+ while i < len(text):
21
+ while i < len(text) and text[i].isspace():
22
+ i += 1
23
+ if i >= len(text):
24
+ break
25
+ doc, end = decoder.raw_decode(text, idx=i)
26
+ docs.append(doc)
27
+ i = end
28
+ return docs
29
+
30
+
31
+ def _sanitize_filename(value: str) -> str:
32
+ value = value.strip().lower()
33
+ value = re.sub(r"\s+", "-", value)
34
+ value = re.sub(r"[^a-z0-9._-]+", "", value)
35
+ return value or "voice"
36
+
37
+
38
+ def _load_dotenv_value(dotenv_path: Path, key: str) -> str | None:
39
+ if not dotenv_path.exists():
40
+ return None
41
+ for raw_line in dotenv_path.read_text(encoding="utf-8").splitlines():
42
+ line = raw_line.strip()
43
+ if not line or line.startswith("#"):
44
+ continue
45
+ if line.startswith("export "):
46
+ line = line[len("export ") :].strip()
47
+ if "=" not in line:
48
+ continue
49
+ k, v = line.split("=", 1)
50
+ if k.strip() != key:
51
+ continue
52
+ value = v.strip()
53
+ if not value:
54
+ return ""
55
+ if value[0] in ("'", '"') and len(value) >= 2 and value[-1] == value[0]:
56
+ return value[1:-1]
57
+ value = value.split(" #", 1)[0].split("\t#", 1)[0].strip()
58
+ return value
59
+ return None
60
+
61
+
62
+ def _extract_bearer_from_curl(curl_text: str) -> str | None:
63
+ m = re.search(r"-H\s+'authorization:\s*Bearer\s+([^']+)'", curl_text, flags=re.IGNORECASE)
64
+ if m:
65
+ return m.group(1).strip()
66
+ m = re.search(r'-H\s+"authorization:\s*Bearer\s+([^"]+)"', curl_text, flags=re.IGNORECASE)
67
+ if m:
68
+ return m.group(1).strip()
69
+ return None
70
+
71
+
72
+ def _extract_headers_from_curl(curl_text: str) -> dict[str, str]:
73
+ headers: dict[str, str] = {}
74
+ for m in re.finditer(r"-H\s+'([^']+)'", curl_text):
75
+ raw = m.group(1)
76
+ if ":" not in raw:
77
+ continue
78
+ k, v = raw.split(":", 1)
79
+ headers[k.strip()] = v.strip()
80
+ for m in re.finditer(r'-H\s+"([^"]+)"', curl_text):
81
+ raw = m.group(1)
82
+ if ":" not in raw:
83
+ continue
84
+ k, v = raw.split(":", 1)
85
+ headers[k.strip()] = v.strip()
86
+ return headers
87
+
88
+
89
+ def _extract_endpoint_from_curl(curl_text: str) -> str | None:
90
+ m = re.search(r"curl\s+'([^']+)'", curl_text)
91
+ if m:
92
+ return m.group(1).strip()
93
+ m = re.search(r'curl\s+"([^"]+)"', curl_text)
94
+ if m:
95
+ return m.group(1).strip()
96
+ return None
97
+
98
+
99
+ def _load_voices(voice_json_path: Path) -> list[dict[str, Any]]:
100
+ voices: list[dict[str, Any]] = []
101
+ for doc in _load_json_documents(voice_json_path):
102
+ if not isinstance(doc, dict):
103
+ continue
104
+ items: Any = doc.get("items")
105
+ if items is None:
106
+ items = doc.get("result", {}).get("items", [])
107
+ if not isinstance(items, list):
108
+ continue
109
+ for v in items:
110
+ if not isinstance(v, dict):
111
+ continue
112
+ if v.get("id") and v.get("name"):
113
+ voices.append(v)
114
+ return voices
115
+
116
+
117
+ def _select_voices(voices: list[dict[str, Any]], selectors: list[str], limit: int) -> list[dict[str, Any]]:
118
+ if not selectors:
119
+ return voices[:limit]
120
+
121
+ selected: list[dict[str, Any]] = []
122
+ used_ids: set[str] = set()
123
+ for sel in selectors:
124
+ sel_norm = sel.strip().lower()
125
+ for v in voices:
126
+ vid = str(v.get("id", ""))
127
+ if not vid or vid in used_ids:
128
+ continue
129
+ name = str(v.get("name", "")).lower()
130
+ slug = str(v.get("slug", "")).lower()
131
+ if sel_norm == vid.lower() or sel_norm in name or (slug and sel_norm in slug):
132
+ selected.append(v)
133
+ used_ids.add(vid)
134
+ return selected
135
+
136
+
137
+ def _extract_scene_speeches(script_text: str) -> list[tuple[int, str]]:
138
+ scenes: list[tuple[int, str]] = []
139
+ for m in re.finditer(
140
+ r"SCENE\s+(\d+):.*?tông giọng[^:]*:\s*'([^']+)'",
141
+ script_text,
142
+ flags=re.IGNORECASE | re.DOTALL,
143
+ ):
144
+ idx = int(m.group(1))
145
+ speech = m.group(2).strip()
146
+ scenes.append((idx, speech))
147
+ scenes.sort(key=lambda x: x[0])
148
+ return scenes
149
+
150
+
151
+ def _find_audio_url(obj: Any) -> str | None:
152
+ if isinstance(obj, dict):
153
+ for k in ("cdnUrl", "audioUrl", "url", "fileUrl", "downloadUrl"):
154
+ v = obj.get(k)
155
+ if isinstance(v, str) and v.startswith("http"):
156
+ return v
157
+ for v in obj.values():
158
+ found = _find_audio_url(v)
159
+ if found:
160
+ return found
161
+ if isinstance(obj, list):
162
+ for v in obj:
163
+ found = _find_audio_url(v)
164
+ if found:
165
+ return found
166
+ return None
167
+
168
+
169
+ def _find_audio_base64(obj: Any) -> str | None:
170
+ if isinstance(obj, dict):
171
+ for k in ("audioBase64", "base64", "dataBase64"):
172
+ v = obj.get(k)
173
+ if isinstance(v, str) and len(v) > 200:
174
+ return v
175
+ for v in obj.values():
176
+ found = _find_audio_base64(v)
177
+ if found:
178
+ return found
179
+ if isinstance(obj, list):
180
+ for v in obj:
181
+ found = _find_audio_base64(v)
182
+ if found:
183
+ return found
184
+ return None
185
+
186
+
187
+ def _post_tts(
188
+ session: requests.Session,
189
+ *,
190
+ endpoint: str,
191
+ headers: dict[str, str],
192
+ bearer_token: str,
193
+ text: str,
194
+ user_voice_id: str,
195
+ speed: float,
196
+ block_version: int,
197
+ timeout_s: float,
198
+ ) -> dict[str, Any]:
199
+ req_headers = dict(headers)
200
+ req_headers["authorization"] = f"Bearer {bearer_token}"
201
+ req_headers["content-type"] = "application/json"
202
+
203
+ payload = {
204
+ "method": "tts",
205
+ "input": {
206
+ "text": text,
207
+ "userVoiceId": user_voice_id,
208
+ "speed": speed,
209
+ "blockVersion": block_version,
210
+ },
211
+ }
212
+ resp = session.post(endpoint, headers=req_headers, json=payload, timeout=timeout_s)
213
+ resp.raise_for_status()
214
+ return resp.json()
215
+
216
+
217
+ def _write_audio_from_result(
218
+ session: requests.Session,
219
+ result: dict[str, Any],
220
+ out_path_base: Path,
221
+ timeout_s: float,
222
+ ) -> Path:
223
+ audio_url = _find_audio_url(result)
224
+ if audio_url:
225
+ suffix = Path(audio_url.split("?", 1)[0]).suffix.lower()
226
+ out_path = out_path_base.with_suffix(suffix if suffix else ".mp3")
227
+ with session.get(audio_url, stream=True, timeout=timeout_s) as r:
228
+ r.raise_for_status()
229
+ out_path.parent.mkdir(parents=True, exist_ok=True)
230
+ with out_path.open("wb") as f:
231
+ for chunk in r.iter_content(chunk_size=1024 * 128):
232
+ if chunk:
233
+ f.write(chunk)
234
+ return out_path
235
+
236
+ audio_b64 = _find_audio_base64(result)
237
+ if audio_b64:
238
+ out_path = out_path_base.with_suffix(".wav")
239
+ out_path.parent.mkdir(parents=True, exist_ok=True)
240
+ out_path.write_bytes(base64.b64decode(audio_b64))
241
+ return out_path
242
+
243
+ out_path = out_path_base.with_suffix(".json")
244
+ out_path.parent.mkdir(parents=True, exist_ok=True)
245
+ out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
246
+ return out_path
247
+
248
+
249
+ def main() -> int:
250
+ parser = argparse.ArgumentParser(prog="lucylab-tts")
251
+ parser.add_argument("--endpoint", default="https://api.lucylab.io/json-rpc")
252
+ parser.add_argument("--curl-file", default="")
253
+ parser.add_argument("--header", action="append", default=[])
254
+ parser.add_argument("--bearer", default="")
255
+ parser.add_argument("--voice-json", default="")
256
+ parser.add_argument("--export-voice-library", default="")
257
+ parser.add_argument("--out-dir", default="outputs/tts-lucylab")
258
+ parser.add_argument("--text", default="")
259
+ parser.add_argument("--text-file", default="")
260
+ parser.add_argument("--voices", action="append", default=[])
261
+ parser.add_argument("--voice", action="append", default=[])
262
+ parser.add_argument("--limit", type=int, default=5)
263
+ parser.add_argument("--speed", type=float, default=1.0)
264
+ parser.add_argument("--block-version", type=int, default=0)
265
+ parser.add_argument("--sleep", type=float, default=0.25)
266
+ parser.add_argument("--timeout", type=float, default=60.0)
267
+ parser.add_argument("--mode", choices=("auto", "plain", "script-scenes"), default="auto")
268
+ args = parser.parse_args()
269
+
270
+ if args.export_voice_library:
271
+ src = Path(args.voice_json) if args.voice_json else Path("voice.json")
272
+ if not src.exists():
273
+ raise SystemExit("Missing source voice json. Provide --voice-json or create voice.json.")
274
+ voices = _load_voices(src)
275
+ def normalize_desc(desc: str) -> str:
276
+ d = (desc or "").strip()
277
+ d_lower = d.lower()
278
+ if d_lower.startswith("đây là một giọng nói hay"):
279
+ return ""
280
+ return d
281
+
282
+ def compact_item(v: dict[str, Any]) -> dict[str, Any] | None:
283
+ vid = str(v.get("id", "")).strip()
284
+ name = str(v.get("name", "")).strip()
285
+ if not vid or not name:
286
+ return None
287
+ tags: Any = v.get("tag")
288
+ if not isinstance(tags, list):
289
+ tags = v.get("tags")
290
+ if not isinstance(tags, list):
291
+ tags = []
292
+ return {
293
+ "id": vid,
294
+ "name": name,
295
+ "description": normalize_desc(str(v.get("description") or "")),
296
+ "tag": tags,
297
+ }
298
+
299
+ def categorize(tags: list[str]) -> str:
300
+ t = {str(x).strip().lower() for x in tags if str(x).strip()}
301
+ region = "other"
302
+ if "miền bắc" in t:
303
+ region = "north"
304
+ elif "miền nam" in t:
305
+ region = "south"
306
+ gender = "other"
307
+ if "nam" in t:
308
+ gender = "male"
309
+ elif "nữ" in t:
310
+ gender = "female"
311
+ if region in ("north", "south") and gender in ("male", "female"):
312
+ return f"{region}_{gender}"
313
+ return "other"
314
+
315
+ out_path = Path(args.export_voice_library)
316
+ if out_path.suffix.lower() != ".json":
317
+ out_dir = out_path
318
+ out_dir.mkdir(parents=True, exist_ok=True)
319
+ by_cat: dict[str, list[dict[str, Any]]] = {
320
+ "north_male": [],
321
+ "north_female": [],
322
+ "south_male": [],
323
+ "south_female": [],
324
+ "other": [],
325
+ }
326
+ for v in voices:
327
+ if not isinstance(v, dict):
328
+ continue
329
+ it = compact_item(v)
330
+ if it is None:
331
+ continue
332
+ cid = categorize([str(x) for x in it.get("tag", [])])
333
+ by_cat[cid].append(it)
334
+
335
+ name_map = {
336
+ "north_male": "Nam miền Bắc",
337
+ "north_female": "Nữ miền Bắc",
338
+ "south_male": "Nam miền Nam",
339
+ "south_female": "Nữ miền Nam",
340
+ "other": "Khác / không rõ",
341
+ }
342
+ index: dict[str, Any] = {"version": 1, "categories": []}
343
+ for cid, cat_items in by_cat.items():
344
+ (out_dir / f"{cid}.json").write_text(
345
+ json.dumps({"version": 1, "items": cat_items}, ensure_ascii=False, indent=2) + "\n",
346
+ encoding="utf-8",
347
+ )
348
+ index["categories"].append(
349
+ {"id": cid, "name": name_map[cid], "file": f"{cid}.json", "count": len(cat_items)}
350
+ )
351
+ (out_dir / "index.json").write_text(json.dumps(index, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
352
+ print(f"OK: {out_dir}")
353
+ return 0
354
+
355
+ items: list[dict[str, Any]] = []
356
+ for v in voices:
357
+ if not isinstance(v, dict):
358
+ continue
359
+ it = compact_item(v)
360
+ if it is not None:
361
+ items.append(it)
362
+
363
+ out_path.write_text(json.dumps({"version": 1, "items": items}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
364
+ print(f"OK: {out_path}")
365
+ return 0
366
+
367
+ out_dir = Path(args.out_dir)
368
+
369
+ endpoint = args.endpoint.strip() or "https://api.lucylab.io/json-rpc"
370
+ headers: dict[str, str] = {"accept": "*/*"}
371
+
372
+ curl_text = ""
373
+ if args.curl_file:
374
+ curl_path = Path(args.curl_file)
375
+ if curl_path.exists():
376
+ curl_text = curl_path.read_text(encoding="utf-8")
377
+ endpoint = _extract_endpoint_from_curl(curl_text) or endpoint
378
+ headers.update(_extract_headers_from_curl(curl_text))
379
+
380
+ for h in args.header:
381
+ raw = str(h).strip()
382
+ if not raw or ":" not in raw:
383
+ raise SystemExit("Invalid --header. Expected format: 'Key: Value'")
384
+ k, v = raw.split(":", 1)
385
+ headers[k.strip()] = v.strip()
386
+
387
+ bearer_token = args.bearer.strip() or os.environ.get("LUCYLAB_BEARER", "").strip()
388
+ if not bearer_token:
389
+ bearer_token = _load_dotenv_value(Path.cwd() / ".env", "LUCYLAB_BEARER") or ""
390
+ if not bearer_token:
391
+ bearer_token = _load_dotenv_value(Path(__file__).resolve().with_name(".env"), "LUCYLAB_BEARER") or ""
392
+ if not bearer_token:
393
+ bearer_token = _extract_bearer_from_curl(curl_text) or ""
394
+ if not bearer_token:
395
+ raise SystemExit("Missing bearer token. Set LUCYLAB_BEARER or pass --bearer.")
396
+
397
+ voice_specs: list[dict[str, Any]] = []
398
+ for spec in args.voice:
399
+ raw = str(spec).strip()
400
+ if not raw:
401
+ continue
402
+ if ":" in raw:
403
+ voice_id, voice_name = raw.split(":", 1)
404
+ voice_id = voice_id.strip()
405
+ voice_name = voice_name.strip() or voice_id
406
+ else:
407
+ voice_id = raw
408
+ voice_name = voice_id
409
+ if voice_id:
410
+ voice_specs.append({"id": voice_id, "name": voice_name, "slug": _sanitize_filename(voice_name)})
411
+
412
+ selected_voices: list[dict[str, Any]] = []
413
+ if args.voice_json:
414
+ voice_path = Path(args.voice_json)
415
+ if voice_path.exists():
416
+ voices = _load_voices(voice_path)
417
+ selected_voices = _select_voices(voices, args.voices, args.limit)
418
+
419
+ if not selected_voices:
420
+ if voice_specs:
421
+ selected_voices = voice_specs
422
+ else:
423
+ raise SystemExit(
424
+ "No voices selected. Provide --voice-json + --voices, or pass explicit --voice <id>[:name]."
425
+ )
426
+
427
+ text = args.text.strip()
428
+ if args.text_file:
429
+ text = Path(args.text_file).read_text(encoding="utf-8").strip()
430
+ if not text:
431
+ raise SystemExit("Provide --text or --text-file.")
432
+
433
+ mode = args.mode
434
+ if mode == "auto":
435
+ mode = "script-scenes" if re.search(r"\bSCENE\s+\d+\b", text, flags=re.IGNORECASE) else "plain"
436
+
437
+ items: list[tuple[str, str]] = []
438
+ if mode == "plain":
439
+ items = [("full", text)]
440
+ else:
441
+ scenes = _extract_scene_speeches(text)
442
+ if not scenes:
443
+ raise SystemExit("No SCENE thoại found in text. Use --mode plain or check script format.")
444
+ items = [(f"scene-{idx:02d}", speech) for idx, speech in scenes]
445
+
446
+ session = requests.Session()
447
+
448
+ manifest: dict[str, Any] = {
449
+ "endpoint": endpoint,
450
+ "speed": args.speed,
451
+ "blockVersion": args.block_version,
452
+ "mode": mode,
453
+ "voices": [],
454
+ "items": [],
455
+ }
456
+
457
+ for voice in selected_voices:
458
+ voice_id = str(voice["id"])
459
+ voice_name = str(voice.get("name", voice_id))
460
+ voice_slug = _sanitize_filename(str(voice.get("slug", voice_name)))
461
+ manifest["voices"].append({"id": voice_id, "name": voice_name, "slug": voice_slug})
462
+
463
+ for label, speech in items:
464
+ manifest["items"].append({"label": label, "text": speech})
465
+
466
+ out_dir.mkdir(parents=True, exist_ok=True)
467
+ (out_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
468
+
469
+ for voice in selected_voices:
470
+ voice_id = str(voice["id"])
471
+ voice_name = str(voice.get("name", voice_id))
472
+ voice_slug = _sanitize_filename(str(voice.get("slug", voice_name)))
473
+
474
+ for label, speech in items:
475
+ base = out_dir / voice_slug / label
476
+ attempt = 0
477
+ while True:
478
+ attempt += 1
479
+ try:
480
+ result = _post_tts(
481
+ session,
482
+ endpoint=endpoint,
483
+ headers=headers,
484
+ bearer_token=bearer_token,
485
+ text=speech,
486
+ user_voice_id=voice_id,
487
+ speed=args.speed,
488
+ block_version=args.block_version,
489
+ timeout_s=args.timeout,
490
+ )
491
+ _write_audio_from_result(session, result, base, args.timeout)
492
+ break
493
+ except Exception:
494
+ if attempt >= 3:
495
+ raise
496
+ time.sleep(1.5 * attempt)
497
+
498
+ if args.sleep > 0:
499
+ time.sleep(args.sleep)
500
+
501
+ print(f"OK: {out_dir}")
502
+ return 0
503
+
504
+
505
+ if __name__ == "__main__":
506
+ raise SystemExit(main())
@@ -165,9 +165,22 @@ This surfaces memories that a keyword search would miss.
165
165
 
166
166
  ---
167
167
 
168
+ ## Auto-Install Protocol (New Machine)
169
+
170
+ If `nmem` CLI is not available:
171
+ ```
172
+ 1. AI detects: command not found: nmem
173
+ 2. AI auto-runs: pip install neural-memory (or python3 -m pip install neural-memory)
174
+ → Requires Python >= 3.11
175
+ 3. After install: nmem init (initialize brain DB)
176
+ 4. Verify: nmem --version
177
+ 5. If Python < 3.11 or pip fails:
178
+ → Show user: "⚠️ NeuralMemory requires Python >= 3.11. Install via: brew install python@3.12"
179
+ ```
180
+
168
181
  ## Fallback Mode (NeuralMemory Not Installed)
169
182
 
170
- If `nmem` is not available, falls back to flat-file behavior:
183
+ If `nmem` is not available AND auto-install fails, falls back to flat-file behavior:
171
184
  ```
172
185
  - Reads from brain/decisions/, brain/solutions/
173
186
  - Keyword overlap matching (legacy behavior)
@@ -48,49 +48,16 @@ If user request involves iOS-specific → Check if mobile-ios pack enabled
48
48
  If not enabled → Suggest: "awf enable-pack mobile-ios"
49
49
  ```
50
50
 
51
- ### 3.5. Gate 4 Three-Phase Routing (v12.3 — AUTO-ENFORCE)
52
-
53
- > ⚠️ AI PHẢI CHỦ ĐỘNG kích hoạt — KHÔNG chờ user gọi.
54
- > Khi detect COMPLEX + UI → TỰ ĐỘNG announce Phase Announcement Block.
55
-
56
- ```yaml
57
- gate4_triage:
58
- trigger: After Gate 3 (tasks created), before execution begins
59
- auto_activate: true # AI proactively triggers, no user command needed
60
-
61
- complex_with_ui:
62
- condition: complexity == COMPLEX AND task has UI components
63
- action: Enforce Three-Phase Execution
64
- phases:
65
- - Phase A: Infrastructure (dependencies, DI, navigation skeleton)
66
- → Must build successfully before Phase B
67
- - Phase B: UI Shell (all screens with mock data)
68
- → TRIGGER TP1.7: User Test Checkpoint (MANDATORY)
69
- → User must confirm UI OK before Phase C
70
- - Phase C: Logic Integration (per feature)
71
- → TRIGGER TP1.7: after each feature (batch small ones)
72
- task_ordering: UI tasks MUST be grouped before logic tasks in Symphony
73
-
74
- moderate_with_ui:
75
- condition: complexity == MODERATE AND task has UI components
76
- action: Phase A+C merged, Phase B optional (recommend for hardware features)
77
-
78
- trivial_or_backend:
79
- condition: complexity == TRIVIAL OR no UI components
80
- action: Skip phases, code straight through (no checkpoints)
81
-
82
- detect_ui_components:
83
- signals:
84
- - Task mentions: screen, view, layout, UI, button, form, navigation
85
- - Files include: *.xml (Android), *.swift (iOS views), *.compose, *.tsx
86
- - Spec references: wireframe, mockup, design, screenshot
87
- ```
88
-
89
51
  ### 4. Fallback
90
52
  ```
91
53
  No match → Ask clarifying question (max 2 times)
92
54
  Still unclear → Suggest `/help`
93
55
  ```
94
56
 
57
+ ### 5. Post-Action Rules
58
+ ```
59
+ Build hoàn tất thành công (không có lỗi) → Tự động chạy git commit.
60
+ ```
61
+
95
62
  ## Auto-Activation
96
63
  This skill is always active. It runs as the first layer before any other processing.