@fifine/aim-studio 0.0.3 → 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.
@@ -10,11 +10,14 @@ Usage:
10
10
  python export.py --scene 1 # Export scene 1 of current episode
11
11
  python export.py --format seedance # Seedance format (default)
12
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
13
15
  """
14
16
 
15
17
  import argparse
16
18
  import json
17
19
  import os
20
+ import re
18
21
  import sys
19
22
  from pathlib import Path
20
23
  from datetime import datetime
@@ -24,6 +27,222 @@ sys.path.insert(0, str(Path(__file__).parent))
24
27
  from common.paths import get_project_root, get_tasks_dir
25
28
 
26
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
+
27
246
  def read_file(file_path: Path) -> str:
28
247
  """Read file content."""
29
248
  try:
@@ -118,6 +337,7 @@ def generate_seedance_prompt(
118
337
  world_prompt: str,
119
338
  scene_name: str,
120
339
  previous_context: str = "",
340
+ duration: str = "10",
121
341
  ) -> str:
122
342
  """
123
343
  Generate a clean, copy-paste ready prompt for Seedance.
@@ -134,6 +354,10 @@ def generate_seedance_prompt(
134
354
  output.append(f"=== {scene_name} ===")
135
355
  output.append("")
136
356
 
357
+ # Duration
358
+ output.append(f"[DURATION: {duration}s]")
359
+ output.append("")
360
+
137
361
  # Character prompts
138
362
  if character_prompts:
139
363
  output.append("[CHARACTERS]")
@@ -197,7 +421,7 @@ def generate_seedance_prompt(
197
421
  return "\n".join(output)
198
422
 
199
423
 
200
- def generate_simple_prompt(scene_content: str, scene_name: str) -> str:
424
+ def generate_simple_prompt(scene_content: str, scene_name: str, duration: str = "10") -> str:
201
425
  """
202
426
  Generate a simple, minimal prompt for quick copy.
203
427
  Just the essential scene description.
@@ -207,6 +431,7 @@ def generate_simple_prompt(scene_content: str, scene_name: str) -> str:
207
431
  output = []
208
432
 
209
433
  output.append(f"Scene: {scene_name}")
434
+ output.append(f"Duration: {duration}s")
210
435
  output.append("")
211
436
 
212
437
  # Extract just the dialogue and action
@@ -266,6 +491,8 @@ def export_episode(
266
491
  spec_dir: Path,
267
492
  output_dir: Path,
268
493
  format_type: str = "seedance",
494
+ duration: str = "10",
495
+ skip_violations: bool = False,
269
496
  ) -> list:
270
497
  """Export a single episode."""
271
498
 
@@ -281,6 +508,12 @@ def export_episode(
281
508
  scene_name = scene_file.stem
282
509
  content = read_file(scene_file)
283
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
+
284
517
  # Get character prompts for this scene (simplified - use all characters)
285
518
  character_prompts = {}
286
519
  character_file = spec_dir / "story" / "character.md"
@@ -297,13 +530,14 @@ def export_episode(
297
530
 
298
531
  # Generate prompt based on format
299
532
  if format_type == "simple":
300
- prompt = generate_simple_prompt(content, scene_name)
533
+ prompt = generate_simple_prompt(content, scene_name, duration)
301
534
  else:
302
535
  prompt = generate_seedance_prompt(
303
536
  content,
304
537
  character_prompts,
305
538
  world_prompt,
306
539
  scene_name,
540
+ duration=duration,
307
541
  )
308
542
 
309
543
  # Write to output directory
@@ -335,6 +569,13 @@ def main():
335
569
  choices=["seedance", "simple"],
336
570
  help="Export format: seedance (full) or simple (minimal)",
337
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
+ )
338
579
  parser.add_argument(
339
580
  "--output",
340
581
  type=str,
@@ -346,6 +587,16 @@ def main():
346
587
  action="store_true",
347
588
  help="Open output directory after export",
348
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
+ )
349
600
 
350
601
  args = parser.parse_args()
351
602
 
@@ -359,12 +610,43 @@ def main():
359
610
  print("Error: This is not a story project. No spec/story/ directory found.")
360
611
  sys.exit(1)
361
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
+
362
642
  # Create output directory
363
643
  output_dir = project_root / args.output
364
644
  output_dir.mkdir(exist_ok=True)
365
645
 
366
- print(f"Exporting to: {output_dir}")
367
- print(f"Format: {args.format}")
646
+ print(f"\n导出设置:")
647
+ print(f" - 格式: {args.format}")
648
+ print(f" - 时长: {args.duration}秒")
649
+ print(f" - 输出: {output_dir}")
368
650
  print("")
369
651
 
370
652
  exported_files = []
@@ -373,8 +655,11 @@ def main():
373
655
  if args.ep == "all":
374
656
  episodes = find_episodes(tasks_dir)
375
657
  for ep_name, ep_dir in episodes:
376
- print(f"Exporting {ep_name}...")
377
- files = export_episode(ep_name, ep_dir, spec_dir, output_dir, args.format)
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
+ )
378
663
  exported_files.extend(files)
379
664
  elif "-" in str(args.ep):
380
665
  # Range like 1-3
@@ -383,8 +668,11 @@ def main():
383
668
  ep_name = f"EP{ep_num:02d}"
384
669
  ep_dir = tasks_dir / ep_name
385
670
  if ep_dir.exists():
386
- print(f"Exporting {ep_name}...")
387
- files = export_episode(ep_name, ep_dir, spec_dir, output_dir, args.format)
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
+ )
388
676
  exported_files.extend(files)
389
677
  elif args.ep:
390
678
  # Single episode
@@ -393,18 +681,21 @@ def main():
393
681
  ep_dir = tasks_dir / ep_name
394
682
 
395
683
  if ep_dir.exists():
396
- print(f"Exporting {ep_name}...")
397
- exported_files = export_episode(ep_name, ep_dir, spec_dir, output_dir, args.format)
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
+ )
398
689
  else:
399
690
  print(f"Error: Episode {ep_name} not found in {tasks_dir}")
400
691
  sys.exit(1)
401
692
  else:
402
- print("Error: Please specify --ep or --all")
693
+ print("Error: 请指定 --ep --all")
403
694
  parser.print_help()
404
695
  sys.exit(1)
405
696
 
406
697
  print("")
407
- print(f"Exported {len(exported_files)} files:")
698
+ print(f" 已导出 {len(exported_files)} 个文件:")
408
699
  for f in exported_files:
409
700
  print(f" - {f.relative_to(project_root)}")
410
701
 
@@ -420,7 +711,7 @@ def main():
420
711
  subprocess.run(["xdg-open", str(output_dir)])
421
712
 
422
713
  print("")
423
- print("Done! Files are ready for Seedance.")
714
+ print("完成!文件已准备好用于 Seedance")
424
715
 
425
716
 
426
717
  if __name__ == "__main__":
@@ -67,11 +67,10 @@ aim init
67
67
  │ ├── script.md # 剧本与分镜规范
68
68
  │ ├── character.md # 角色设定规范
69
69
  │ └── world.md # 世界观设定规范
70
- ├── workspace/ # 工作区
71
- │ └── {项目名}/ # 每个项目一个目录
72
- │ ├── 全局上下文.txt
73
- ├── 项目配置.md
74
- │ └── 第1集_*/ # 剧集目录
70
+ ├── aim-workspace/ # 工作区(项目根目录下)
71
+ │ └── {开发者名}/ # 每个开发者一个目录
72
+ │ ├── journal-1.md # 工作日志
73
+ └── index.md # 工作索引
75
74
  ├── materials/ # 小说原文
76
75
  └── tasks/ # 任务追踪
77
76
  ```
@@ -168,18 +167,18 @@ spec/story/
168
167
  - 修复问题后需要补充规范
169
168
  - 建立新的约定
170
169
 
171
- ### 2. workspace/ - 工作区
170
+ ### 2. aim-workspace/ - 工作区
172
171
 
173
- **用途**:存放创作输出
172
+ **用途**:存放开发者的工作日志和会话记录
173
+
174
+ **位置**:直接在项目根目录下(不在 .aim-studio/ 中)
174
175
 
175
176
  **结构**:
176
177
  ```
177
- workspace/{项目名}/
178
- ├── 项目配置.md # 视频格式配置
179
- ├── 全局上下文.txt # 角色设定、世界观
180
- ├── 使用说明.md # 使用说明
181
- ├── 第1集_*/ # 剧集目录
182
- └── ...
178
+ aim-workspace/{开发者名}/
179
+ ├── journal-1.md # 工作日志(第1次会话)
180
+ ├── journal-2.md # 工作日志(第2次会话)
181
+ └── index.md # 工作索引
183
182
  ```
184
183
 
185
184
  ### 3. materials/ - 小说原文