free-short-video 5.2.0 → 5.3.0

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 (49) hide show
  1. package/README.md +7 -0
  2. package/core/api/agnes_image.py +6 -3
  3. package/core/api/agnes_video.py +4 -1
  4. package/core/artifacts.py +83 -0
  5. package/core/audio/subtitle/__init__.py +35 -0
  6. package/core/audio/subtitle/generator.py +899 -0
  7. package/core/audio/subtitle/renderer.py +212 -0
  8. package/core/audio/subtitle.py +16 -1103
  9. package/core/compositor/concatenator/__init__.py +35 -0
  10. package/core/compositor/concatenator/audio_overlay.py +376 -0
  11. package/core/compositor/concatenator/concat.py +293 -0
  12. package/core/compositor/concatenator.py +16 -653
  13. package/core/config.py +71 -16
  14. package/core/pipelines/__init__.py +109 -1
  15. package/core/pipelines/anchor_video.py +40 -59
  16. package/core/pipelines/creative/__init__.py +18 -0
  17. package/core/pipelines/creative/pipeline.py +173 -0
  18. package/core/pipelines/creative/steps_audio.py +477 -0
  19. package/core/pipelines/creative/steps_frames.py +371 -0
  20. package/core/pipelines/creative/steps_script.py +472 -0
  21. package/core/pipelines/creative/steps_video.py +546 -0
  22. package/core/pipelines/creative_video.py +23 -1968
  23. package/core/pipelines/manuscript_video.py +34 -51
  24. package/core/pipelines/multi_scene.py +68 -61
  25. package/core/pipelines/poetry_video.py +34 -49
  26. package/core/pipelines/simple_video.py +17 -9
  27. package/core/screenwriter/__init__.py +220 -0
  28. package/core/screenwriter/characters.py +550 -0
  29. package/core/screenwriter/scenes.py +338 -0
  30. package/core/screenwriter/story.py +550 -0
  31. package/core/screenwriter/style.py +276 -0
  32. package/models/task.py +6 -9
  33. package/package.json +1 -1
  34. package/server.py +69 -1963
  35. package/static/index.html +137 -0
  36. package/web/__init__.py +10 -0
  37. package/web/app_state.py +156 -0
  38. package/web/deps.py +185 -0
  39. package/web/helpers.py +284 -0
  40. package/web/routes/__init__.py +4 -0
  41. package/web/routes/config_routes.py +147 -0
  42. package/web/routes/image_routes.py +124 -0
  43. package/web/routes/task_creation_routes.py +620 -0
  44. package/web/routes/task_routes.py +184 -0
  45. package/web/routes/utility_routes.py +121 -0
  46. package/web/routes/video_routes.py +194 -0
  47. package/web/routes/voice_routes.py +80 -0
  48. package/web/routes/workspace_routes.py +86 -0
  49. package/core/screenwriter.py +0 -1860
package/README.md CHANGED
@@ -33,6 +33,13 @@ free-short-video
33
33
 
34
34
  > **🖥️ Try it now — no install needed:** Visit [video.lichuanyang.top](https://video.lichuanyang.top) and experience **Simple Video** mode directly in your browser. Just enter a prompt and generate a free AI video instantly.
35
35
 
36
+ ## 🚀 Two Ways to Use — Both Completely Free
37
+
38
+ | Project | Run Where | Features | Link |
39
+ |---------|-----------|----------|------|
40
+ | **[Agnes Video Generator](https://github.com/lcy362/agnes-video-generator)** (this project) | **Download & run locally** | **More powerful** — TTS narration, auto subtitles, digital anchor, image-to-video, keyframes animation, manuscript-to-video, checkpoint resume & more | [GitHub](https://github.com/lcy362/agnes-video-generator) |
41
+ | **[FreeShortVideoStudio](https://github.com/lcy362/free-short-video-studio)** | **Fully online, in the browser** | Lightweight, zero install — no setup at all, **features under active construction** | [video.lichuanyang.top/studio](https://video.lichuanyang.top/studio) · [GitHub](https://github.com/lcy362/free-short-video-studio) |
42
+
36
43
  ## ⭐ Support & Contribute
37
44
 
38
45
  If you find this project useful, please **star the [GitHub repository](https://github.com/lcy362/agnes-video-generator)** ⭐ — your support helps more people discover this free and open-source AI video generator.
@@ -17,6 +17,9 @@ from utils.image import download_image
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
+ # 图像生成读超时基数(秒):timeout = 基数 * (attempt + 1),首次 120s 逐步放宽
21
+ _READ_TIMEOUT_BASE_SECONDS = 120
22
+
20
23
 
21
24
  class ImageOutput:
22
25
  def __init__(self, fmt: str, ext: str, data: str):
@@ -82,7 +85,7 @@ class AgnesImageAPI:
82
85
  prompt: str,
83
86
  reference_image_paths: List[str] = [],
84
87
  size: Optional[str] = None,
85
- max_retries: int = 3,
88
+ max_retries: int = 4,
86
89
  retry_base_delay: float = 20.0,
87
90
  **kwargs,
88
91
  ) -> ImageOutput:
@@ -114,8 +117,8 @@ class AgnesImageAPI:
114
117
  try:
115
118
  # 全局限速:在发起 HTTP 请求前获取令牌
116
119
  await asyncio.to_thread(get_rate_limiter().acquire)
117
- # 动态超时:第一次 60s,后续逐步增加
118
- read_timeout = 60 * (attempt + 1)
120
+ # 动态超时:第一次 120s,后续逐步增加(图像生成较慢,放宽读超时)
121
+ read_timeout = _READ_TIMEOUT_BASE_SECONDS * (attempt + 1)
119
122
  resp = await asyncio.to_thread(
120
123
  requests.post,
121
124
  f"{get_agnes_base_url()}/images/generations",
@@ -26,6 +26,9 @@ DURATION_PRESETS = {
26
26
  20: (409, 24), # capped at 409 (API max for 720p); actual ~17s
27
27
  }
28
28
 
29
+ # 图片上传 429 退避间隔基数(秒):delay = 基数 * (attempt + 1)
30
+ _UPLOAD_RETRY_BASE_DELAY_SECONDS = 30
31
+
29
32
 
30
33
  class VideoOutput:
31
34
  def __init__(self, fmt: str, ext: str, data: str):
@@ -144,7 +147,7 @@ class AgnesVideoAPI:
144
147
  timeout=(30, 120),
145
148
  )
146
149
  if resp.status_code == 429:
147
- delay = 30 * (attempt + 1)
150
+ delay = _UPLOAD_RETRY_BASE_DELAY_SECONDS * (attempt + 1)
148
151
  logger.warning(f"[AgnesVideo] Image upload 429, retry in {delay}s...")
149
152
  await asyncio.sleep(delay)
150
153
  continue
package/core/artifacts.py CHANGED
@@ -12,7 +12,11 @@ core/artifacts.py — 中间产物注册表与级联删除计划
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import json
16
+ import logging
15
17
  import os
18
+ import shutil
19
+ import time
16
20
  from dataclasses import dataclass, field
17
21
  from typing import Any, Optional
18
22
 
@@ -572,3 +576,82 @@ def apply_cascade_plan(state: BaseTaskState, plan: CascadePlan) -> dict:
572
576
  update_kwargs["status"] = StepStatus.PENDING
573
577
 
574
578
  return update_kwargs
579
+
580
+
581
+ # ═══════════════════════════════════════════════════════════════
582
+ # 僵尸任务磁盘清理(v5.0 Batch 5 / 5.1)
583
+ # ═══════════════════════════════════════════════════════════════
584
+
585
+ _DEFAULT_PROTECT_STATUSES = {StepStatus.RUNNING, StepStatus.QUEUED, StepStatus.PENDING}
586
+
587
+
588
+ def sweep_stale_tasks(age_days: int = 7,
589
+ protect_statuses: Optional[set] = None) -> dict:
590
+ """扫描并清理僵尸任务目录(状态文件超龄且任务非活跃)。
591
+
592
+ 清理条件(全部满足才删除):
593
+ 1. 工作区中存在 ``task_state.json`` 且可解析出 ``status`` 字段;
594
+ 2. ``status`` 不在保护集合中;
595
+ 3. ``task_state.json`` 的修改时间距今超过 ``age_days`` 天。
596
+
597
+ 默认保护 ``RUNNING``(运行中)/ ``QUEUED``(排队中)/ ``PENDING``(断点续传
598
+ 候选),保证活跃任务与可恢复任务永不误删;调用方可传 ``protect_statuses``
599
+ 显式覆盖(如仅保护活跃状态,放开 PENDING)。
600
+
601
+ 删除策略:整目录 ``shutil.rmtree``(等价于按级联计划删除全部产物 + 状态文件),
602
+ 删除前校验 realpath 必须位于工作区内,防符号链接逃逸。
603
+
604
+ Args:
605
+ age_days: 状态文件超龄阈值(天)
606
+ protect_statuses: 保护的状态集合;None 时使用默认保护集合
607
+
608
+ Returns:
609
+ {"swept": [目录名], "protected": [目录名], "errors": [描述]}
610
+ """
611
+ working_dir = get_working_dir()
612
+ protect = protect_statuses if protect_statuses is not None else _DEFAULT_PROTECT_STATUSES
613
+ swept: list[str] = []
614
+ protected: list[str] = []
615
+ errors: list[str] = []
616
+
617
+ if not os.path.isdir(working_dir):
618
+ return {"swept": swept, "protected": protected, "errors": errors}
619
+
620
+ real_working = os.path.realpath(working_dir)
621
+ cutoff = time.time() - age_days * 86400
622
+
623
+ for name in sorted(os.listdir(working_dir)):
624
+ task_dir = os.path.join(working_dir, name)
625
+ if not os.path.isdir(task_dir):
626
+ continue
627
+ task_file = os.path.join(task_dir, "task_state.json")
628
+ if not os.path.isfile(task_file):
629
+ continue # 非任务目录(如 uploads/ 或普通文件目录)
630
+ try:
631
+ with open(task_file, "r", encoding="utf-8") as f:
632
+ status_str = json.load(f).get("status")
633
+ except (OSError, json.JSONDecodeError):
634
+ errors.append(f"{name}: task_state.json 无法解析")
635
+ continue
636
+ if status_str in {s.value for s in protect}:
637
+ protected.append(name)
638
+ continue
639
+ try:
640
+ mtime = os.path.getmtime(task_file)
641
+ except OSError:
642
+ errors.append(f"{name}: 无法读取 task_state.json 修改时间")
643
+ continue
644
+ if mtime > cutoff:
645
+ protected.append(name) # 未超龄,保留
646
+ continue
647
+ # 路径穿越防护:任务目录 realpath 必须位于工作区内
648
+ if not os.path.realpath(task_dir).startswith(real_working + os.sep):
649
+ errors.append(f"{name}: 任务目录逃逸工作区,拒绝删除")
650
+ continue
651
+ try:
652
+ shutil.rmtree(task_dir)
653
+ swept.append(name)
654
+ except OSError as e:
655
+ errors.append(f"{name}: 删除失败 ({e})")
656
+
657
+ return {"swept": swept, "protected": protected, "errors": errors}
@@ -0,0 +1,35 @@
1
+ """core.audio.subtitle — SRT 字幕生成 + moviepy 叠加(v5.0 Batch 4 / 4.3 拆分后的包入口)
2
+
3
+ SubtitleGenerator 由两个职责 mixin(SubtitleSrtMixin / SubtitleRenderMixin)
4
+ 组合而成;对外保留全部原符号(subtitle.py 兼容 re-export)。"""
5
+
6
+ import logging
7
+
8
+ from .generator import SubtitleSrtMixin
9
+ from .renderer import SubtitleRenderMixin
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class SubtitleGenerator(
15
+ SubtitleSrtMixin,
16
+ SubtitleRenderMixin,
17
+ ):
18
+
19
+ """字幕生成器:cues → SRT + moviepy 叠加。"""
20
+
21
+
22
+ from . import generator as srt_generator
23
+ from . import renderer as subtitle_renderer
24
+
25
+ # 运行时注入:mixin 方法内显式 `SubtitleGenerator.xxx` 自引用
26
+ # (原 @staticmethod 风格)在模块级延迟解析到组合类。
27
+ srt_generator.SubtitleGenerator = SubtitleGenerator
28
+ subtitle_renderer.SubtitleGenerator = SubtitleGenerator
29
+
30
+ __all__ = [
31
+ "SubtitleGenerator",
32
+ "SubtitleSrtMixin",
33
+ "SubtitleRenderMixin",
34
+ ]
35
+