@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.
- package/README.md +65 -99
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +123 -101
- package/dist/commands/init.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +91 -65
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +5 -5
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +5 -5
- package/dist/constants/paths.js.map +1 -1
- package/dist/templates/aim/index.d.ts +1 -0
- package/dist/templates/aim/index.d.ts.map +1 -1
- package/dist/templates/aim/index.js +2 -0
- package/dist/templates/aim/index.js.map +1 -1
- package/dist/templates/aim/scripts/common/developer.py +2 -1
- package/dist/templates/aim/scripts/common/paths.py +3 -2
- package/dist/templates/aim/scripts/export.py +718 -0
- package/dist/templates/aim/workflow.md +12 -13
- package/dist/templates/claude/commands/aim/export.md +258 -108
- package/dist/templates/claude/commands/aim/finish-work.md +23 -0
- package/dist/templates/claude/commands/aim/onboard.md +10 -8
- package/dist/templates/claude/commands/aim/start.md +104 -37
- package/dist/templates/claude/commands/aim/story.md +275 -67
- package/dist/templates/claude/hooks/inject-subagent-context.py +6 -10
- package/dist/templates/claude/hooks/session-start.py +134 -24
- package/dist/templates/markdown/index.d.ts +5 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +6 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/cli/directory-structure.md.txt +71 -0
- package/dist/templates/markdown/spec/cli/error-handling.md.txt +91 -0
- package/dist/templates/markdown/spec/cli/index.md.txt +37 -0
- package/dist/templates/markdown/spec/cli/options-flags.md.txt +71 -0
- package/dist/templates/markdown/spec/cli/output-formatting.md.txt +93 -0
- package/dist/utils/project-detector.d.ts +1 -1
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +20 -0
- package/dist/utils/project-detector.js.map +1 -1
- package/package.json +1 -1
- package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
- package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
- package/dist/templates/claude/commands/trellis/break-loop.md +0 -125
- package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
- package/dist/templates/claude/commands/trellis/check-cross-layer.md +0 -153
- package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
- package/dist/templates/claude/commands/trellis/create-command.md +0 -154
- package/dist/templates/claude/commands/trellis/finish-work.md +0 -129
- package/dist/templates/claude/commands/trellis/integrate-skill.md +0 -219
- package/dist/templates/claude/commands/trellis/onboard.md +0 -358
- package/dist/templates/claude/commands/trellis/parallel.md +0 -193
- package/dist/templates/claude/commands/trellis/record-session.md +0 -62
- package/dist/templates/claude/commands/trellis/start.md +0 -280
- package/dist/templates/claude/commands/trellis/update-spec.md +0 -285
- package/dist/templates/trellis/gitignore.txt +0 -29
- package/dist/templates/trellis/index.d.ts +0 -49
- package/dist/templates/trellis/index.d.ts.map +0 -1
- package/dist/templates/trellis/index.js +0 -92
- package/dist/templates/trellis/index.js.map +0 -1
- package/dist/templates/trellis/scripts/__init__.py +0 -5
- package/dist/templates/trellis/scripts/add_session.py +0 -392
- package/dist/templates/trellis/scripts/common/__init__.py +0 -80
- package/dist/templates/trellis/scripts/common/cli_adapter.py +0 -435
- package/dist/templates/trellis/scripts/common/developer.py +0 -190
- package/dist/templates/trellis/scripts/common/git_context.py +0 -383
- package/dist/templates/trellis/scripts/common/paths.py +0 -347
- package/dist/templates/trellis/scripts/common/phase.py +0 -253
- package/dist/templates/trellis/scripts/common/registry.py +0 -366
- package/dist/templates/trellis/scripts/common/task_queue.py +0 -255
- package/dist/templates/trellis/scripts/common/task_utils.py +0 -178
- package/dist/templates/trellis/scripts/common/worktree.py +0 -219
- package/dist/templates/trellis/scripts/create_bootstrap.py +0 -290
- package/dist/templates/trellis/scripts/get_context.py +0 -16
- package/dist/templates/trellis/scripts/get_developer.py +0 -26
- package/dist/templates/trellis/scripts/init_developer.py +0 -51
- package/dist/templates/trellis/scripts/multi_agent/__init__.py +0 -5
- package/dist/templates/trellis/scripts/multi_agent/cleanup.py +0 -403
- package/dist/templates/trellis/scripts/multi_agent/create_pr.py +0 -329
- package/dist/templates/trellis/scripts/multi_agent/plan.py +0 -233
- package/dist/templates/trellis/scripts/multi_agent/start.py +0 -461
- package/dist/templates/trellis/scripts/multi_agent/status.py +0 -817
- package/dist/templates/trellis/scripts/task.py +0 -1056
- package/dist/templates/trellis/scripts-shell-archive/add-session.sh +0 -384
- package/dist/templates/trellis/scripts-shell-archive/common/developer.sh +0 -129
- package/dist/templates/trellis/scripts-shell-archive/common/git-context.sh +0 -263
- package/dist/templates/trellis/scripts-shell-archive/common/paths.sh +0 -208
- package/dist/templates/trellis/scripts-shell-archive/common/phase.sh +0 -150
- package/dist/templates/trellis/scripts-shell-archive/common/registry.sh +0 -247
- package/dist/templates/trellis/scripts-shell-archive/common/task-queue.sh +0 -142
- package/dist/templates/trellis/scripts-shell-archive/common/task-utils.sh +0 -151
- package/dist/templates/trellis/scripts-shell-archive/common/worktree.sh +0 -128
- package/dist/templates/trellis/scripts-shell-archive/create-bootstrap.sh +0 -299
- package/dist/templates/trellis/scripts-shell-archive/get-context.sh +0 -7
- package/dist/templates/trellis/scripts-shell-archive/get-developer.sh +0 -15
- package/dist/templates/trellis/scripts-shell-archive/init-developer.sh +0 -34
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/cleanup.sh +0 -396
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/create-pr.sh +0 -241
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/plan.sh +0 -207
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/start.sh +0 -317
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/status.sh +0 -828
- package/dist/templates/trellis/scripts-shell-archive/task.sh +0 -1204
- package/dist/templates/trellis/tasks/.gitkeep +0 -0
- package/dist/templates/trellis/workflow.md +0 -416
- 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()
|