@fifine/aim-studio 0.0.2 → 0.0.4

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 (104) hide show
  1. package/README.md +65 -99
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +123 -101
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/configurators/workflow.d.ts.map +1 -1
  6. package/dist/configurators/workflow.js +91 -65
  7. package/dist/configurators/workflow.js.map +1 -1
  8. package/dist/constants/paths.d.ts +5 -5
  9. package/dist/constants/paths.d.ts.map +1 -1
  10. package/dist/constants/paths.js +5 -5
  11. package/dist/constants/paths.js.map +1 -1
  12. package/dist/templates/aim/index.d.ts +1 -0
  13. package/dist/templates/aim/index.d.ts.map +1 -1
  14. package/dist/templates/aim/index.js +2 -0
  15. package/dist/templates/aim/index.js.map +1 -1
  16. package/dist/templates/aim/scripts/common/developer.py +2 -1
  17. package/dist/templates/aim/scripts/common/paths.py +3 -2
  18. package/dist/templates/aim/scripts/export.py +718 -0
  19. package/dist/templates/aim/workflow.md +12 -13
  20. package/dist/templates/claude/commands/aim/export.md +258 -108
  21. package/dist/templates/claude/commands/aim/finish-work.md +23 -0
  22. package/dist/templates/claude/commands/aim/onboard.md +10 -8
  23. package/dist/templates/claude/commands/aim/start.md +104 -37
  24. package/dist/templates/claude/commands/aim/story.md +275 -67
  25. package/dist/templates/claude/hooks/inject-subagent-context.py +6 -10
  26. package/dist/templates/claude/hooks/session-start.py +134 -24
  27. package/dist/templates/markdown/index.d.ts +5 -0
  28. package/dist/templates/markdown/index.d.ts.map +1 -1
  29. package/dist/templates/markdown/index.js +6 -0
  30. package/dist/templates/markdown/index.js.map +1 -1
  31. package/dist/templates/markdown/spec/cli/directory-structure.md.txt +71 -0
  32. package/dist/templates/markdown/spec/cli/error-handling.md.txt +91 -0
  33. package/dist/templates/markdown/spec/cli/index.md.txt +37 -0
  34. package/dist/templates/markdown/spec/cli/options-flags.md.txt +71 -0
  35. package/dist/templates/markdown/spec/cli/output-formatting.md.txt +93 -0
  36. package/dist/utils/project-detector.d.ts +1 -1
  37. package/dist/utils/project-detector.d.ts.map +1 -1
  38. package/dist/utils/project-detector.js +20 -0
  39. package/dist/utils/project-detector.js.map +1 -1
  40. package/package.json +1 -1
  41. package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
  42. package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
  43. package/dist/templates/claude/commands/trellis/break-loop.md +0 -125
  44. package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
  45. package/dist/templates/claude/commands/trellis/check-cross-layer.md +0 -153
  46. package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
  47. package/dist/templates/claude/commands/trellis/create-command.md +0 -154
  48. package/dist/templates/claude/commands/trellis/finish-work.md +0 -129
  49. package/dist/templates/claude/commands/trellis/integrate-skill.md +0 -219
  50. package/dist/templates/claude/commands/trellis/onboard.md +0 -358
  51. package/dist/templates/claude/commands/trellis/parallel.md +0 -193
  52. package/dist/templates/claude/commands/trellis/record-session.md +0 -62
  53. package/dist/templates/claude/commands/trellis/start.md +0 -280
  54. package/dist/templates/claude/commands/trellis/update-spec.md +0 -285
  55. package/dist/templates/trellis/gitignore.txt +0 -29
  56. package/dist/templates/trellis/index.d.ts +0 -49
  57. package/dist/templates/trellis/index.d.ts.map +0 -1
  58. package/dist/templates/trellis/index.js +0 -92
  59. package/dist/templates/trellis/index.js.map +0 -1
  60. package/dist/templates/trellis/scripts/__init__.py +0 -5
  61. package/dist/templates/trellis/scripts/add_session.py +0 -392
  62. package/dist/templates/trellis/scripts/common/__init__.py +0 -80
  63. package/dist/templates/trellis/scripts/common/cli_adapter.py +0 -435
  64. package/dist/templates/trellis/scripts/common/developer.py +0 -190
  65. package/dist/templates/trellis/scripts/common/git_context.py +0 -383
  66. package/dist/templates/trellis/scripts/common/paths.py +0 -347
  67. package/dist/templates/trellis/scripts/common/phase.py +0 -253
  68. package/dist/templates/trellis/scripts/common/registry.py +0 -366
  69. package/dist/templates/trellis/scripts/common/task_queue.py +0 -255
  70. package/dist/templates/trellis/scripts/common/task_utils.py +0 -178
  71. package/dist/templates/trellis/scripts/common/worktree.py +0 -219
  72. package/dist/templates/trellis/scripts/create_bootstrap.py +0 -290
  73. package/dist/templates/trellis/scripts/get_context.py +0 -16
  74. package/dist/templates/trellis/scripts/get_developer.py +0 -26
  75. package/dist/templates/trellis/scripts/init_developer.py +0 -51
  76. package/dist/templates/trellis/scripts/multi_agent/__init__.py +0 -5
  77. package/dist/templates/trellis/scripts/multi_agent/cleanup.py +0 -403
  78. package/dist/templates/trellis/scripts/multi_agent/create_pr.py +0 -329
  79. package/dist/templates/trellis/scripts/multi_agent/plan.py +0 -233
  80. package/dist/templates/trellis/scripts/multi_agent/start.py +0 -461
  81. package/dist/templates/trellis/scripts/multi_agent/status.py +0 -817
  82. package/dist/templates/trellis/scripts/task.py +0 -1056
  83. package/dist/templates/trellis/scripts-shell-archive/add-session.sh +0 -384
  84. package/dist/templates/trellis/scripts-shell-archive/common/developer.sh +0 -129
  85. package/dist/templates/trellis/scripts-shell-archive/common/git-context.sh +0 -263
  86. package/dist/templates/trellis/scripts-shell-archive/common/paths.sh +0 -208
  87. package/dist/templates/trellis/scripts-shell-archive/common/phase.sh +0 -150
  88. package/dist/templates/trellis/scripts-shell-archive/common/registry.sh +0 -247
  89. package/dist/templates/trellis/scripts-shell-archive/common/task-queue.sh +0 -142
  90. package/dist/templates/trellis/scripts-shell-archive/common/task-utils.sh +0 -151
  91. package/dist/templates/trellis/scripts-shell-archive/common/worktree.sh +0 -128
  92. package/dist/templates/trellis/scripts-shell-archive/create-bootstrap.sh +0 -299
  93. package/dist/templates/trellis/scripts-shell-archive/get-context.sh +0 -7
  94. package/dist/templates/trellis/scripts-shell-archive/get-developer.sh +0 -15
  95. package/dist/templates/trellis/scripts-shell-archive/init-developer.sh +0 -34
  96. package/dist/templates/trellis/scripts-shell-archive/multi-agent/cleanup.sh +0 -396
  97. package/dist/templates/trellis/scripts-shell-archive/multi-agent/create-pr.sh +0 -241
  98. package/dist/templates/trellis/scripts-shell-archive/multi-agent/plan.sh +0 -207
  99. package/dist/templates/trellis/scripts-shell-archive/multi-agent/start.sh +0 -317
  100. package/dist/templates/trellis/scripts-shell-archive/multi-agent/status.sh +0 -828
  101. package/dist/templates/trellis/scripts-shell-archive/task.sh +0 -1204
  102. package/dist/templates/trellis/tasks/.gitkeep +0 -0
  103. package/dist/templates/trellis/workflow.md +0 -416
  104. package/dist/templates/trellis/worktree.yaml +0 -47
@@ -0,0 +1,718 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Export script for generating Seedance-ready prompts
5
+
6
+ Usage:
7
+ python export.py --ep 1 # Export episode 1
8
+ python export.py --ep 1-3 # Export episodes 1-3
9
+ python export.py --all # Export all episodes
10
+ python export.py --scene 1 # Export scene 1 of current episode
11
+ python export.py --format seedance # Seedance format (default)
12
+ python export.py --format simple # Simple format for quick copy
13
+ python export.py --duration 10 # Set video duration (5/10/15/30/45/60 seconds)
14
+ python export.py --check # Check for violations before export
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+ from datetime import datetime
24
+
25
+ # Add parent directory to path for imports
26
+ sys.path.insert(0, str(Path(__file__).parent))
27
+ from common.paths import get_project_root, get_tasks_dir
28
+
29
+
30
+ # =============================================================================
31
+ # Duration Options
32
+ # =============================================================================
33
+
34
+ DURATION_OPTIONS = ["5", "10", "15", "30", "45", "60"]
35
+
36
+ # =============================================================================
37
+ # Violation Detection
38
+ # =============================================================================
39
+
40
+ # Keywords that may indicate real person references (celebrities, public figures)
41
+ REAL_PERSON_KEYWORDS = [
42
+ # Chinese celebrities
43
+ "明星", "演员", "歌手", "主持人", "网红", "名人", "偶像",
44
+ "刘德华", "周杰伦", "成龙", "李连杰", "甄子丹", "吴京",
45
+ "赵丽颖", "杨幂", "范冰冰", "李冰冰", "Angelababy", "迪丽热巴",
46
+ "肖战", "王一博", "蔡徐坤", "李易峰", "吴亦凡", "鹿晗",
47
+ "特朗普", "拜登", "奥巴马", "普京", "马斯克", "乔布斯",
48
+ # International celebrities
49
+ "Leonardo DiCaprio", "Brad Pitt", "Tom Cruise", "Jennifer Lawrence",
50
+ "Taylor Swift", "Beyonce", "K-pop", "BTS", "Blackpink",
51
+ "周杰伦", "王菲", "张学友", "刘德华", "郭富城", "黎明",
52
+ # General real person terms
53
+ "真人", "照片", "写真", "肖像", "脸", "颜值",
54
+ "真实照片", "本人", "本人照片", "自拍照",
55
+ ]
56
+
57
+ # Keywords that may indicate copyright issues
58
+ COPYRIGHT_KEYWORDS = [
59
+ # Copyright protected terms
60
+ "版权", "侵权", "盗版", "抄袭", "原创",
61
+ "小说改编", "影视改编", "动漫改编", "游戏改编",
62
+ "IP", "知识产权", "授权", "许可",
63
+ "哈利波特", "漫威", "DC", "迪士尼", "皮克斯",
64
+ " Batman", "Spider-Man", "Superman", "Iron Man",
65
+ "金庸", "古龙", "琼瑶", "JK罗琳", "马丁",
66
+ "腾讯", "网易", "字节", "B站",
67
+ ]
68
+
69
+ # Keywords that may indicate sensitive content
70
+ SENSITIVE_KEYWORDS = [
71
+ "暴力", "血腥", "色情", "裸露", "赌博", "毒品",
72
+ "政治", "宗教", "邪教", "恐怖", "自杀", "自残",
73
+ "未成年人", "儿童", "小孩", "幼儿",
74
+ ]
75
+
76
+
77
+ def detect_violations(content: str) -> dict:
78
+ """
79
+ Detect potential violations in content.
80
+
81
+ Returns:
82
+ dict with 'has_violation', 'real_person', 'copyright', 'sensitive' flags
83
+ """
84
+ result = {
85
+ "has_violation": False,
86
+ "real_person": [],
87
+ "copyright": [],
88
+ "sensitive": [],
89
+ }
90
+
91
+ # Check for real person references
92
+ for keyword in REAL_PERSON_KEYWORDS:
93
+ if keyword.lower() in content.lower():
94
+ result["real_person"].append(keyword)
95
+ result["has_violation"] = True
96
+
97
+ # Check for copyright issues
98
+ for keyword in COPYRIGHT_KEYWORDS:
99
+ if keyword in content:
100
+ result["copyright"].append(keyword)
101
+ result["has_violation"] = True
102
+
103
+ # Check for sensitive content
104
+ for keyword in SENSITIVE_KEYWORDS:
105
+ if keyword in content:
106
+ result["sensitive"].append(keyword)
107
+ result["has_violation"] = True
108
+
109
+ return result
110
+
111
+
112
+ def check_violations_in_project(project_root: Path) -> dict:
113
+ """Check all project files for potential violations."""
114
+ spec_dir = project_root / ".aim-studio" / "spec"
115
+
116
+ all_violations = {
117
+ "characters": [],
118
+ "world": [],
119
+ "scenes": [],
120
+ "has_violation": False,
121
+ }
122
+
123
+ # Check character file
124
+ char_file = spec_dir / "story" / "character.md"
125
+ if char_file.exists():
126
+ content = read_file(char_file)
127
+ violations = detect_violations(content)
128
+ if violations["has_violation"]:
129
+ all_violations["characters"] = violations
130
+ all_violations["has_violation"] = True
131
+
132
+ # Check world file
133
+ world_file = spec_dir / "story" / "world.md"
134
+ if world_file.exists():
135
+ content = read_file(world_file)
136
+ violations = detect_violations(content)
137
+ if violations["has_violation"]:
138
+ all_violations["world"] = violations
139
+ all_violations["has_violation"] = True
140
+
141
+ # Check scenes
142
+ tasks_dir = get_tasks_dir(project_root)
143
+ if tasks_dir.exists():
144
+ for ep_dir in tasks_dir.iterdir():
145
+ if ep_dir.is_dir() and "EP" in ep_dir.name.upper():
146
+ for scene_file in ep_dir.glob("*.md"):
147
+ content = read_file(scene_file)
148
+ violations = detect_violations(content)
149
+ if violations["has_violation"]:
150
+ all_violations["scenes"].append({
151
+ "file": str(scene_file.relative_to(project_root)),
152
+ "violations": violations,
153
+ })
154
+ all_violations["has_violation"] = True
155
+
156
+ return all_violations
157
+
158
+
159
+ def print_violation_report(violations: dict) -> None:
160
+ """Print a detailed violation report."""
161
+ print("\n" + "=" * 60)
162
+ print("🚨 违规检测报告")
163
+ print("=" * 60)
164
+
165
+ if not violations["has_violation"]:
166
+ print("\n✅ 未检测到违规内容,可以继续导出!")
167
+ return
168
+
169
+ print("\n⚠️ 检测到以下潜在违规内容:\n")
170
+
171
+ # Real person violations
172
+ if violations.get("characters") and violations["characters"].get("real_person"):
173
+ print("【真人素材风险】")
174
+ for keyword in violations["characters"]["real_person"]:
175
+ print(f" - 关键词: {keyword}")
176
+ print()
177
+
178
+ if violations.get("world") and violations["world"].get("real_person"):
179
+ print("【世界观真人素材风险】")
180
+ for keyword in violations["world"]["real_person"]:
181
+ print(f" - 关键词: {keyword}")
182
+ print()
183
+
184
+ # Copyright violations
185
+ if violations.get("characters") and violations["characters"].get("copyright"):
186
+ print("【版权风险】")
187
+ for keyword in violations["characters"]["copyright"]:
188
+ print(f" - 关键词: {keyword}")
189
+ print()
190
+
191
+ if violations.get("world") and violations["world"].get("copyright"):
192
+ print("【世界观版权风险】")
193
+ for keyword in violations["world"]["copyright"]:
194
+ print(f" - 关键词: {keyword}")
195
+ print()
196
+
197
+ # Sensitive content
198
+ if violations.get("characters") and violations["characters"].get("sensitive"):
199
+ print("【敏感内容风险】")
200
+ for keyword in violations["characters"]["sensitive"]:
201
+ print(f" - 关键词: {keyword}")
202
+ print()
203
+
204
+ # Scene violations
205
+ if violations.get("scenes"):
206
+ print("【场景违规详情】")
207
+ for scene in violations["scenes"][:5]: # Show first 5
208
+ print(f" - {scene['file']}")
209
+ if scene["violations"].get("real_person"):
210
+ print(f" 真人: {', '.join(scene['violations']['real_person'][:3])}")
211
+ if scene["violations"].get("copyright"):
212
+ print(f" 版权: {', '.join(scene['violations']['copyright'][:3])}")
213
+ if len(violations["scenes"]) > 5:
214
+ print(f" ... 还有 {len(violations['scenes']) - 5} 个场景")
215
+ print()
216
+
217
+ print("=" * 60)
218
+ print("💡 建议处理方式:")
219
+ print(" 1. 修改角色设定 - 使用虚构角色替代真人")
220
+ print(" 2. 替换版权内容 - 使用原创元素替代受版权保护的内容")
221
+ print(" 3. 简化敏感描述 - 移除可能引发争议的描述")
222
+ print(" 4. 强制导出 - 仍要导出(风险自负)")
223
+ print("=" * 60)
224
+
225
+
226
+ def get_user_choice() -> str:
227
+ """Get user's choice for handling violations."""
228
+ print("\n请选择处理方式(输入数字):")
229
+ print(" 1. 修改角色设定 - 手动修改角色文件")
230
+ print(" 2. 替换版权内容 - 手动修改版权相关内容")
231
+ print(" 3. 简化敏感描述 - 移除敏感描述后重试")
232
+ print(" 4. 强制导出 - 仍要导出(风险自负)")
233
+ print(" 5. 退出 - 不导出")
234
+
235
+ while True:
236
+ choice = input("\n请输入选项 (1-5): ").strip()
237
+ if choice in ["1", "2", "3", "4", "5"]:
238
+ return choice
239
+ print("无效选择,请重新输入。")
240
+
241
+
242
+ # =============================================================================
243
+ # File Operations
244
+ # =============================================================================
245
+
246
+ def read_file(file_path: Path) -> str:
247
+ """Read file content."""
248
+ try:
249
+ return file_path.read_text(encoding="utf-8")
250
+ except (FileNotFoundError, PermissionError):
251
+ return ""
252
+
253
+
254
+ def get_character_prompt(character_name: str, spec_dir: Path) -> str:
255
+ """Generate character prompt for Seedance."""
256
+ character_file = spec_dir / "story" / "character.md"
257
+ if not character_file.exists():
258
+ return ""
259
+
260
+ content = read_file(character_file)
261
+
262
+ # Extract character section
263
+ lines = content.split("\n")
264
+ in_character = False
265
+ character_lines = []
266
+
267
+ for line in lines:
268
+ if f"## {character_name}" in line or f"## {character_name}:" in line:
269
+ in_character = True
270
+ continue
271
+ if in_character:
272
+ if line.startswith("## "):
273
+ break
274
+ character_lines.append(line)
275
+
276
+ if character_lines:
277
+ # Extract key information: appearance, personality, clothing
278
+ prompt_parts = []
279
+
280
+ # Extract appearance
281
+ for line in character_lines:
282
+ if "外观" in line or "外貌" in line or "Appearance" in line:
283
+ prompt_parts.append(line.split(":", 1)[-1].strip())
284
+
285
+ if "服装" in line or "Clothing" in line:
286
+ prompt_parts.append(line.split(":", 1)[-1].strip())
287
+
288
+ return ", ".join(prompt_parts) if prompt_parts else ""
289
+
290
+ return ""
291
+
292
+
293
+ def get_world_prompt(spec_dir: Path) -> str:
294
+ """Generate world prompt for Seedance."""
295
+ world_file = spec_dir / "story" / "world.md"
296
+ if not world_file.exists():
297
+ return ""
298
+
299
+ content = read_file(world_file)
300
+
301
+ # Extract style keywords
302
+ keywords = []
303
+
304
+ # Look for style/tone keywords
305
+ for line in content.split("\n"):
306
+ if "风格" in line or "Style" in line or "视觉" in line or "Visual" in line:
307
+ # Extract keywords after colon
308
+ if ":" in line:
309
+ keywords.append(line.split(":", 1)[-1].strip())
310
+
311
+ return ", ".join(keywords)[:200] if keywords else "cinematic, high quality"
312
+
313
+
314
+ def parse_scene_file(scene_path: Path) -> dict:
315
+ """Parse scene file and extract Seedance-relevant content."""
316
+ content = read_file(scene_path)
317
+
318
+ if not content:
319
+ return {}
320
+
321
+ scene_data = {
322
+ "content": content,
323
+ "has_seedance_format": False,
324
+ }
325
+
326
+ # Check for Seedance 2.0 format (六要素)
327
+ required_elements = ["主体描述", "动作", "环境", "镜头", "音频", "风格"]
328
+ if any(elem in content for elem in required_elements):
329
+ scene_data["has_seedance_format"] = True
330
+
331
+ return scene_data
332
+
333
+
334
+ def generate_seedance_prompt(
335
+ scene_content: str,
336
+ character_prompts: dict,
337
+ world_prompt: str,
338
+ scene_name: str,
339
+ previous_context: str = "",
340
+ duration: str = "10",
341
+ ) -> str:
342
+ """
343
+ Generate a clean, copy-paste ready prompt for Seedance.
344
+
345
+ Format: Pure text, no markdown, ready for video generation.
346
+ """
347
+
348
+ # Extract key information from scene content
349
+ lines = scene_content.split("\n")
350
+
351
+ output = []
352
+
353
+ # Scene header
354
+ output.append(f"=== {scene_name} ===")
355
+ output.append("")
356
+
357
+ # Duration
358
+ output.append(f"[DURATION: {duration}s]")
359
+ output.append("")
360
+
361
+ # Character prompts
362
+ if character_prompts:
363
+ output.append("[CHARACTERS]")
364
+ for name, prompt in character_prompts.items():
365
+ if prompt:
366
+ output.append(f"{name}: {prompt}")
367
+ output.append("")
368
+
369
+ # Environment
370
+ output.append("[ENVIRONMENT]")
371
+ # Extract environment description
372
+ for line in lines:
373
+ if "环境" in line or "场景" in line:
374
+ env_text = line.split(":", 1)[-1].strip() if ":" in line else line
375
+ if env_text:
376
+ output.append(env_text)
377
+ break
378
+ output.append("")
379
+
380
+ # Previous context
381
+ if previous_context:
382
+ output.append("[PREVIOUS CONTEXT]")
383
+ output.append(previous_context[:200])
384
+ output.append("")
385
+
386
+ # Main content - extract the actual scene description
387
+ output.append("[SCENE]")
388
+
389
+ # Extract main scene content (skip headers and metadata)
390
+ in_main_content = False
391
+ for line in lines:
392
+ # Skip metadata lines
393
+ if any(marker in line for marker in ["#", "##", "---", "**", "日期", "时间", "时长"]):
394
+ continue
395
+
396
+ # Skip empty lines at start
397
+ if not in_main_content and not line.strip():
398
+ continue
399
+
400
+ # Start main content
401
+ if line.strip() and not in_main_content:
402
+ in_main_content = True
403
+
404
+ if in_main_content:
405
+ # Clean the line - remove markdown
406
+ cleaned_line = line.strip()
407
+ cleaned_line = cleaned_line.replace("**", "").replace("*", "")
408
+ cleaned_line = cleaned_line.replace("[", "").replace("]", "")
409
+
410
+ if cleaned_line:
411
+ output.append(cleaned_line)
412
+
413
+ output.append("")
414
+
415
+ # Style prompt
416
+ if world_prompt:
417
+ output.append("[STYLE]")
418
+ output.append(world_prompt)
419
+
420
+ # Return as pure text
421
+ return "\n".join(output)
422
+
423
+
424
+ def generate_simple_prompt(scene_content: str, scene_name: str, duration: str = "10") -> str:
425
+ """
426
+ Generate a simple, minimal prompt for quick copy.
427
+ Just the essential scene description.
428
+ """
429
+
430
+ lines = scene_content.split("\n")
431
+ output = []
432
+
433
+ output.append(f"Scene: {scene_name}")
434
+ output.append(f"Duration: {duration}s")
435
+ output.append("")
436
+
437
+ # Extract just the dialogue and action
438
+ for line in lines:
439
+ # Skip headers and metadata
440
+ if line.startswith("#") or line.startswith("##"):
441
+ continue
442
+ if "日期" in line or "时间" in line or "时长" in line:
443
+ continue
444
+ if "主体描述" in line or "动作" in line:
445
+ continue
446
+
447
+ # Clean and add
448
+ cleaned = line.strip().replace("**", "").replace("*", "")
449
+ if cleaned:
450
+ output.append(cleaned)
451
+
452
+ return "\n".join(output)
453
+
454
+
455
+ def find_episodes(tasks_dir: Path) -> list:
456
+ """Find all episode directories."""
457
+ episodes = []
458
+
459
+ if not tasks_dir.exists():
460
+ return episodes
461
+
462
+ for item in tasks_dir.iterdir():
463
+ if item.is_dir() and "EP" in item.name.upper():
464
+ episodes.append((item.name, item))
465
+
466
+ # Sort by episode number
467
+ episodes.sort(key=lambda x: x[0])
468
+ return episodes
469
+
470
+
471
+ def find_scenes(episode_dir: Path) -> list:
472
+ """Find all scene files in an episode directory."""
473
+ scenes = []
474
+
475
+ if not episode_dir.exists():
476
+ return scenes
477
+
478
+ for item in episode_dir.iterdir():
479
+ if item.is_file() and (item.suffix == ".md" or item.suffix == ".txt"):
480
+ if "task.json" in item.name or "prd" in item.name:
481
+ continue
482
+ scenes.append(item)
483
+
484
+ scenes.sort(key=lambda x: x.name)
485
+ return scenes
486
+
487
+
488
+ def export_episode(
489
+ episode_name: str,
490
+ episode_dir: Path,
491
+ spec_dir: Path,
492
+ output_dir: Path,
493
+ format_type: str = "seedance",
494
+ duration: str = "10",
495
+ skip_violations: bool = False,
496
+ ) -> list:
497
+ """Export a single episode."""
498
+
499
+ # Get character and world prompts
500
+ world_prompt = get_world_prompt(spec_dir)
501
+
502
+ # Find scenes
503
+ scenes = find_scenes(episode_dir)
504
+
505
+ output_files = []
506
+
507
+ for scene_file in scenes:
508
+ scene_name = scene_file.stem
509
+ content = read_file(scene_file)
510
+
511
+ # Check violations if not skipped
512
+ if not skip_violations:
513
+ violations = detect_violations(content)
514
+ if violations["has_violation"]:
515
+ print(f" ⚠️ 场景 {scene_name} 包含潜在违规内容")
516
+
517
+ # Get character prompts for this scene (simplified - use all characters)
518
+ character_prompts = {}
519
+ character_file = spec_dir / "story" / "character.md"
520
+ if character_file.exists():
521
+ char_content = read_file(character_file)
522
+ # Extract character names from ## headers
523
+ for line in char_content.split("\n"):
524
+ if line.startswith("## ") and not line.startswith("###"):
525
+ char_name = line.replace("##", "").strip()
526
+ # Generate prompt for this character
527
+ char_prompt = get_character_prompt(char_name, spec_dir)
528
+ if char_prompt:
529
+ character_prompts[char_name] = char_prompt
530
+
531
+ # Generate prompt based on format
532
+ if format_type == "simple":
533
+ prompt = generate_simple_prompt(content, scene_name, duration)
534
+ else:
535
+ prompt = generate_seedance_prompt(
536
+ content,
537
+ character_prompts,
538
+ world_prompt,
539
+ scene_name,
540
+ duration=duration,
541
+ )
542
+
543
+ # Write to output directory
544
+ output_file = output_dir / f"{episode_name}_{scene_name}.txt"
545
+ output_file.write_text(prompt, encoding="utf-8")
546
+ output_files.append(output_file)
547
+
548
+ return output_files
549
+
550
+
551
+ def main():
552
+ parser = argparse.ArgumentParser(
553
+ description="Export script for Seedance video generation"
554
+ )
555
+ parser.add_argument(
556
+ "--ep",
557
+ type=str,
558
+ help="Episode number(s). Examples: 1, 1-3, all",
559
+ )
560
+ parser.add_argument(
561
+ "--scene",
562
+ type=int,
563
+ help="Scene number within episode",
564
+ )
565
+ parser.add_argument(
566
+ "--format",
567
+ type=str,
568
+ default="seedance",
569
+ choices=["seedance", "simple"],
570
+ help="Export format: seedance (full) or simple (minimal)",
571
+ )
572
+ parser.add_argument(
573
+ "--duration",
574
+ type=str,
575
+ default="10",
576
+ choices=DURATION_OPTIONS,
577
+ help="Video duration in seconds (5/10/15/30/45/60)",
578
+ )
579
+ parser.add_argument(
580
+ "--output",
581
+ type=str,
582
+ default="export",
583
+ help="Output directory name",
584
+ )
585
+ parser.add_argument(
586
+ "--open",
587
+ action="store_true",
588
+ help="Open output directory after export",
589
+ )
590
+ parser.add_argument(
591
+ "--check",
592
+ action="store_true",
593
+ help="Only check for violations, do not export",
594
+ )
595
+ parser.add_argument(
596
+ "--force",
597
+ action="store_true",
598
+ help="Force export even with violations",
599
+ )
600
+
601
+ args = parser.parse_args()
602
+
603
+ # Get project paths
604
+ project_root = get_project_root()
605
+ tasks_dir = get_tasks_dir(project_root)
606
+ spec_dir = project_root / ".aim-studio" / "spec"
607
+
608
+ # Check if story project
609
+ if not (spec_dir / "story").exists():
610
+ print("Error: This is not a story project. No spec/story/ directory found.")
611
+ sys.exit(1)
612
+
613
+ # Check violations first
614
+ print("🔍 正在检查违规内容...")
615
+ violations = check_violations_in_project(project_root)
616
+ print_violation_report(violations)
617
+
618
+ # If --check only, exit here
619
+ if args.check:
620
+ sys.exit(0 if not violations["has_violation"] else 1)
621
+
622
+ # Handle violations
623
+ if violations["has_violation"] and not args.force:
624
+ choice = get_user_choice()
625
+
626
+ if choice == "1":
627
+ print("\n请手动修改角色设定文件后重试:")
628
+ print(f" {spec_dir / 'story' / 'character.md'}")
629
+ sys.exit(1)
630
+ elif choice == "2":
631
+ print("\n请手动修改版权相关内容后重试。")
632
+ sys.exit(1)
633
+ elif choice == "3":
634
+ print("\n请移除敏感描述后重试。")
635
+ sys.exit(1)
636
+ elif choice == "4":
637
+ print("\n⚠️ 强制导出已启用,风险自负!")
638
+ elif choice == "5":
639
+ print("\n已退出导出。")
640
+ sys.exit(0)
641
+
642
+ # Create output directory
643
+ output_dir = project_root / args.output
644
+ output_dir.mkdir(exist_ok=True)
645
+
646
+ print(f"\n导出设置:")
647
+ print(f" - 格式: {args.format}")
648
+ print(f" - 时长: {args.duration}秒")
649
+ print(f" - 输出: {output_dir}")
650
+ print("")
651
+
652
+ exported_files = []
653
+
654
+ # Determine which episodes to export
655
+ if args.ep == "all":
656
+ episodes = find_episodes(tasks_dir)
657
+ for ep_name, ep_dir in episodes:
658
+ print(f"导出 {ep_name}...")
659
+ files = export_episode(
660
+ ep_name, ep_dir, spec_dir, output_dir,
661
+ args.format, args.duration, args.force
662
+ )
663
+ exported_files.extend(files)
664
+ elif "-" in str(args.ep):
665
+ # Range like 1-3
666
+ start, end = map(int, args.ep.split("-"))
667
+ for ep_num in range(start, end + 1):
668
+ ep_name = f"EP{ep_num:02d}"
669
+ ep_dir = tasks_dir / ep_name
670
+ if ep_dir.exists():
671
+ print(f"导出 {ep_name}...")
672
+ files = export_episode(
673
+ ep_name, ep_dir, spec_dir, output_dir,
674
+ args.format, args.duration, args.force
675
+ )
676
+ exported_files.extend(files)
677
+ elif args.ep:
678
+ # Single episode
679
+ ep_num = int(args.ep)
680
+ ep_name = f"EP{ep_num:02d}"
681
+ ep_dir = tasks_dir / ep_name
682
+
683
+ if ep_dir.exists():
684
+ print(f"导出 {ep_name}...")
685
+ exported_files = export_episode(
686
+ ep_name, ep_dir, spec_dir, output_dir,
687
+ args.format, args.duration, args.force
688
+ )
689
+ else:
690
+ print(f"Error: Episode {ep_name} not found in {tasks_dir}")
691
+ sys.exit(1)
692
+ else:
693
+ print("Error: 请指定 --ep 或 --all")
694
+ parser.print_help()
695
+ sys.exit(1)
696
+
697
+ print("")
698
+ print(f"✅ 已导出 {len(exported_files)} 个文件:")
699
+ for f in exported_files:
700
+ print(f" - {f.relative_to(project_root)}")
701
+
702
+ # Optionally open output directory
703
+ if args.open:
704
+ import subprocess
705
+
706
+ if sys.platform == "win32":
707
+ os.startfile(output_dir)
708
+ elif sys.platform == "darwin":
709
+ subprocess.run(["open", str(output_dir)])
710
+ else:
711
+ subprocess.run(["xdg-open", str(output_dir)])
712
+
713
+ print("")
714
+ print("完成!文件已准备好用于 Seedance。")
715
+
716
+
717
+ if __name__ == "__main__":
718
+ main()