free-short-video 6.4.0 → 6.4.1
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 +2 -23
- package/core/compositor/concatenator/audio_overlay.py +10 -9
- package/core/compositor/concatenator/concat.py +10 -3
- package/core/compositor/ffmpeg_tool.py +102 -0
- package/core/compositor/processor.py +6 -4
- package/core/compositor/watermark.py +2 -1
- package/core/config.py +1 -1
- package/package.json +1 -1
- package/server.py +15 -0
- package/static/assets/index-Cg4TCtV5.js +45 -0
- package/static/index.html +1 -1
- package/static/assets/index-DA1ElcyA.js +0 -45
package/README.md
CHANGED
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
|
|
3
|
-
# What's New in v6.4.
|
|
3
|
+
# What's New in v6.4.1
|
|
4
4
|
|
|
5
5
|
## What's New
|
|
6
6
|
|
|
7
|
-
### Features & Improvements
|
|
8
|
-
|
|
9
|
-
- **Preview ("dry-run") endpoints for creative and manuscript tasks** — `POST /api/creative/preview-script` and `POST /api/manuscript/preview-split` let you inspect the generated script or manuscript segments before submitting a full task. Synchronous, no task created. Preview calls share the Chat API rate limiter and have a lightweight in-process concurrency cap (429 + Retry-After) to prevent abuse. (Credit: @Khaled97Sho, PR #33)
|
|
10
|
-
- **Arabic tashkeel for TTS narration** — creative and manuscript pipelines now automatically apply Arabic diacritic marks (harakat) to narration text before TTS synthesis, improving pronunciation quality. Tashkeel only affects TTS input; subtitle text and `narration.txt` remain clean. (Credit: @Khaled97Sho, PR #33)
|
|
11
|
-
- **Manuscript reference images** — per-segment reference images for manuscript tasks are now supported via `reference_images` + `reference_images_map` parameters, matching the same feature used in creative pipelines. (Credit: @Khaled97Sho, PR #33)
|
|
12
|
-
- **Language-aware narration budget** — narration length for non-CJK languages now uses a language-specific characters-per-second rate (Arabic: 4.0, CJK: 5.0, others: 12.0), fixing the "narration too short for non-CJK text" issue. (Credit: @Khaled97Sho, PR #33)
|
|
13
|
-
- **Configurable CORS origins** — the `AGNES_CORS_ORIGINS` environment variable (comma-separated) allows any companion tool to call the core API cross-origin. `AGNES_CORS_ENABLED` can force-enable or disable; auto-detects when origins are set.
|
|
14
|
-
- **China domestic API domain** — the `cn` domain in `AGNES_DOMAIN_MAP` now correctly points to `api.agnes-ai.cn` (the official China service endpoint), updated from the incorrect `apihub.agnes-ai.cn` (international-site fallback). The frontend domain picker reflects the correct hostname.
|
|
15
|
-
|
|
16
|
-
### Refactoring & Optimizations
|
|
17
|
-
|
|
18
|
-
- **`split_manuscript_text()` extracted as a public utility** — the manuscript-splitting algorithm is now available as a standalone function, used by both the manuscript pipeline and the new preview-split endpoint.
|
|
19
|
-
|
|
20
7
|
### Bug Fixes
|
|
21
8
|
|
|
22
|
-
|
|
23
|
-
- **Fixed image save not persisting** — `POST /api/images/generations` now `await`s the image download, ensuring the generated image is actually written to disk before the response.
|
|
24
|
-
- **Fixed GitHub CodeQL alerts #43 and #47** — path-injection and information-disclosure security alerts resolved.
|
|
25
|
-
- **Fixed PBKDF2-HMAC-SHA256 for config key IDs** — config key IDs are now hashed with a proper key-derivation function instead of weaker hashing.
|
|
26
|
-
- **Fixed Docker Hub Cloudflare WAF blocking** — the Docker Hub overview update script now strips `<script>` blocks and escapes `<` in the JSON payload to bypass the WAF XSS rule.
|
|
27
|
-
- **Fixed China domestic site domain** — `AGNES_DOMAIN_MAP["cn"]` corrected to `api.agnes-ai.cn` per the official Agnes model catalog. (Issue #37)
|
|
28
|
-
|
|
29
|
-
---
|
|
9
|
+
* **Fixed "ffmpeg not found" failure (`FileNotFoundError`) during video composition** — on Windows or systems where `ffmpeg` is not on the `PATH`, creative / manuscript / anchor / poetry composition (concatenation, audio overlay, watermark) could fail at the composition step with `[WinError 2]`. The server now automatically falls back to the static ffmpeg binary bundled with `imageio-ffmpeg`, so no manual ffmpeg installation is required. A non-blocking startup check logs which ffmpeg source is being used. (Issues #35, #36)
|
|
30
10
|
|
|
31
|
-
**Compatibility notes**: The `core/audio/tashkeel.py` module adds a new optional dependency (`mishkal`). It is installed by default via `requirements.txt`; if missing, tashkeel silently falls back to the original text. Existing task state files are forward-compatible; no migration needed.
|
|
32
11
|
|
|
33
12
|
---
|
|
34
13
|
|
|
@@ -13,6 +13,7 @@ from typing import List, Optional, Tuple
|
|
|
13
13
|
import srt as srt_lib
|
|
14
14
|
from moviepy import AudioFileClip, CompositeVideoClip, VideoFileClip
|
|
15
15
|
|
|
16
|
+
from core.compositor.ffmpeg_tool import resolve_binary
|
|
16
17
|
from models.task import SubtitleStyle
|
|
17
18
|
|
|
18
19
|
from .concat import _AUDIO_BITRATE, _AUDIO_CODEC, _AUDIO_FPS, _VIDEO_FPS
|
|
@@ -137,7 +138,7 @@ class AudioOverlayMixin:
|
|
|
137
138
|
tmp_files.append(extend_path)
|
|
138
139
|
pad_dur = final_dur - video_dur
|
|
139
140
|
VideoConcatenator._run_ffmpeg(
|
|
140
|
-
["ffmpeg", "-y",
|
|
141
|
+
[resolve_binary("ffmpeg"), "-y",
|
|
141
142
|
"-i", silent_path,
|
|
142
143
|
"-vf", f"tpad=stop_mode=clone:stop_duration={pad_dur:.2f}",
|
|
143
144
|
"-c:v", "libx264", "-pix_fmt", "yuv420p",
|
|
@@ -154,7 +155,7 @@ class AudioOverlayMixin:
|
|
|
154
155
|
tmp_files.append(apad_path)
|
|
155
156
|
pad_dur = final_dur - audio_dur
|
|
156
157
|
VideoConcatenator._run_ffmpeg(
|
|
157
|
-
["ffmpeg", "-y",
|
|
158
|
+
[resolve_binary("ffmpeg"), "-y",
|
|
158
159
|
"-i", audio_path,
|
|
159
160
|
"-af", f"apad=pad_dur={pad_dur:.2f},volume=1.5",
|
|
160
161
|
"-c:a", "libmp3lame", "-q:a", "2",
|
|
@@ -167,7 +168,7 @@ class AudioOverlayMixin:
|
|
|
167
168
|
vol_path = audio_path.replace(".mp3", "_vol.mp3")
|
|
168
169
|
tmp_files.append(vol_path)
|
|
169
170
|
VideoConcatenator._run_ffmpeg(
|
|
170
|
-
["ffmpeg", "-y",
|
|
171
|
+
[resolve_binary("ffmpeg"), "-y",
|
|
171
172
|
"-i", audio_path,
|
|
172
173
|
"-af", "volume=1.5",
|
|
173
174
|
"-c:a", "libmp3lame", "-q:a", "2",
|
|
@@ -290,7 +291,7 @@ class AudioOverlayMixin:
|
|
|
290
291
|
else:
|
|
291
292
|
v_chain = f"[0:v]tpad=stop_mode=clone:stop_duration={final_dur:.2f}[v]"
|
|
292
293
|
cmd = [
|
|
293
|
-
"ffmpeg", "-y",
|
|
294
|
+
resolve_binary("ffmpeg"), "-y",
|
|
294
295
|
"-i", silent_path,
|
|
295
296
|
"-i", audio_path,
|
|
296
297
|
"-filter_complex",
|
|
@@ -315,7 +316,7 @@ class AudioOverlayMixin:
|
|
|
315
316
|
"""ffprobe 获取视频宽高,失败回退 (768, 1152)。"""
|
|
316
317
|
try:
|
|
317
318
|
r = subprocess.run(
|
|
318
|
-
["ffprobe", "-v", "error", "-select_streams", "v:0",
|
|
319
|
+
[resolve_binary("ffprobe"), "-v", "error", "-select_streams", "v:0",
|
|
319
320
|
"-show_entries", "stream=width,height",
|
|
320
321
|
"-of", "csv=s=x:p=0", video_path],
|
|
321
322
|
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=15,
|
|
@@ -706,7 +707,7 @@ class AudioOverlayMixin:
|
|
|
706
707
|
"""
|
|
707
708
|
try:
|
|
708
709
|
n = len(audio_paths)
|
|
709
|
-
cmd = ["ffmpeg", "-y"]
|
|
710
|
+
cmd = [resolve_binary("ffmpeg"), "-y"]
|
|
710
711
|
for ap in audio_paths:
|
|
711
712
|
cmd += ["-i", ap]
|
|
712
713
|
filters = []
|
|
@@ -811,7 +812,7 @@ class AudioOverlayMixin:
|
|
|
811
812
|
|
|
812
813
|
# Step 1: Get clip duration
|
|
813
814
|
probe = subprocess.run(
|
|
814
|
-
["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
|
815
|
+
[resolve_binary("ffprobe"), "-v", "error", "-show_entries", "format=duration",
|
|
815
816
|
"-of", "csv=p=0", clip_path],
|
|
816
817
|
stdin=subprocess.DEVNULL,
|
|
817
818
|
capture_output=True, text=True, timeout=15,
|
|
@@ -836,7 +837,7 @@ class AudioOverlayMixin:
|
|
|
836
837
|
# Step 4: Concatenate with xfade cross-fade transitions
|
|
837
838
|
try:
|
|
838
839
|
subprocess.run(
|
|
839
|
-
["ffmpeg", "-y", "-f", "concat", "-safe", "0",
|
|
840
|
+
[resolve_binary("ffmpeg"), "-y", "-f", "concat", "-safe", "0",
|
|
840
841
|
"-i", concat_file,
|
|
841
842
|
"-c", "copy",
|
|
842
843
|
"-t", str(needed),
|
|
@@ -850,7 +851,7 @@ class AudioOverlayMixin:
|
|
|
850
851
|
# xfade filter 构建已由上方 trim 循环拼接替代(死代码,3.3 清理)
|
|
851
852
|
|
|
852
853
|
subprocess.run(
|
|
853
|
-
["ffmpeg", "-y",
|
|
854
|
+
[resolve_binary("ffmpeg"), "-y",
|
|
854
855
|
"-stream_loop", str(n - 1), "-i", clip_path,
|
|
855
856
|
"-filter_complex",
|
|
856
857
|
f"[0:v]trim=duration={needed}[v]",
|
|
@@ -11,6 +11,7 @@ from typing import List, Optional
|
|
|
11
11
|
import srt as srt_lib
|
|
12
12
|
from moviepy import VideoFileClip, concatenate_videoclips
|
|
13
13
|
|
|
14
|
+
from core.compositor.ffmpeg_tool import resolve_binary
|
|
14
15
|
from models.task import SubtitleStyle
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
@@ -114,7 +115,7 @@ class ConcatMixin:
|
|
|
114
115
|
sigs = set()
|
|
115
116
|
for p in video_paths:
|
|
116
117
|
r = subprocess.run(
|
|
117
|
-
["ffprobe", "-v", "error", "-select_streams", "v:0",
|
|
118
|
+
[resolve_binary("ffprobe"), "-v", "error", "-select_streams", "v:0",
|
|
118
119
|
"-show_entries", "stream=width,height,avg_frame_rate",
|
|
119
120
|
"-of", "csv=s=x:p=0", p],
|
|
120
121
|
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=15,
|
|
@@ -137,7 +138,7 @@ class ConcatMixin:
|
|
|
137
138
|
f.write(f"file '{esc}'\n")
|
|
138
139
|
try:
|
|
139
140
|
r = subprocess.run(
|
|
140
|
-
["ffmpeg", "-y", "-f", "concat", "-safe", "0",
|
|
141
|
+
[resolve_binary("ffmpeg"), "-y", "-f", "concat", "-safe", "0",
|
|
141
142
|
"-i", concat_file, "-c", "copy", output_path],
|
|
142
143
|
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=600,
|
|
143
144
|
)
|
|
@@ -383,7 +384,7 @@ class ConcatMixin:
|
|
|
383
384
|
"""用 ffprobe 获取媒体文件时长(秒)。"""
|
|
384
385
|
try:
|
|
385
386
|
r = subprocess.run(
|
|
386
|
-
["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
|
387
|
+
[resolve_binary("ffprobe"), "-v", "error", "-show_entries", "format=duration",
|
|
387
388
|
"-of", "csv=p=0", path],
|
|
388
389
|
stdin=subprocess.DEVNULL,
|
|
389
390
|
capture_output=True, text=True, timeout=15,
|
|
@@ -396,6 +397,12 @@ class ConcatMixin:
|
|
|
396
397
|
def _run_ffmpeg(cmd: list, desc: str = "") -> None:
|
|
397
398
|
"""执行 ffmpeg 命令,失败时抛 RuntimeError。"""
|
|
398
399
|
logger.info(f"[Compositor] ffmpeg: {desc}")
|
|
400
|
+
if not cmd or cmd[0] is None:
|
|
401
|
+
raise RuntimeError(
|
|
402
|
+
"ffmpeg not available (no system PATH ffmpeg nor builtin "
|
|
403
|
+
"imageio-ffmpeg). Please install ffmpeg or verify imageio-ffmpeg "
|
|
404
|
+
"is installed."
|
|
405
|
+
)
|
|
399
406
|
try:
|
|
400
407
|
r = subprocess.run(
|
|
401
408
|
cmd, stdin=subprocess.DEVNULL,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""core.compositor.ffmpeg_tool — ffmpeg / ffprobe 可执行文件统一解析。
|
|
2
|
+
|
|
3
|
+
背景:此前所有 ffmpeg/ffprobe 调用都用裸命令字符串经 ``subprocess.run`` 执行,
|
|
4
|
+
依赖系统 PATH 搜索可执行文件。Windows 未安装 ffmpeg 时 ``CreateProcess`` 抛
|
|
5
|
+
``[WinError 2] The system cannot find the file specified``,且发生在任务运行到
|
|
6
|
+
拼接步骤而非启动时(见 Issue #36 的完整 traceback)。
|
|
7
|
+
|
|
8
|
+
解析优先级(推荐,但不强制用户安装系统 ffmpeg):
|
|
9
|
+
1. 环境变量显式指定:``FFMPEG_BINARY`` / ``FFPROBE_BINARY`` —— 尊重用户意图
|
|
10
|
+
2. 系统 PATH(``shutil.which``)—— 已安装则用之,绝不覆盖
|
|
11
|
+
3. ``imageio-ffmpeg`` 内置静态二进制兜底 —— wheel 自带,无需用户操作
|
|
12
|
+
|
|
13
|
+
结果带进程级缓存;全部不可用时返回 None,由启动检测 / 调用方给出清晰指引
|
|
14
|
+
而非裸的 ``[WinError 2]``。
|
|
15
|
+
"""
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# 二进制名 → 覆盖环境变量名
|
|
23
|
+
_EXE_OVERRIDE = {"ffmpeg": "FFMPEG_BINARY", "ffprobe": "FFPROBE_BINARY"}
|
|
24
|
+
_cache: "dict[str, str | None]" = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_binary(name: str) -> "str | None":
|
|
28
|
+
"""按优先级解析 ffmpeg/ffprobe 可执行文件绝对路径,进程内缓存。
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name: ``"ffmpeg"`` 或 ``"ffprobe"``。
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
可用可执行文件绝对路径;全部不可用时返回 None。
|
|
35
|
+
"""
|
|
36
|
+
if name not in _EXE_OVERRIDE:
|
|
37
|
+
raise ValueError(f"unknown binary: {name}")
|
|
38
|
+
if name in _cache:
|
|
39
|
+
return _cache[name]
|
|
40
|
+
path = _resolve(name)
|
|
41
|
+
_cache[name] = path
|
|
42
|
+
return path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_ffmpeg() -> "str | None":
|
|
46
|
+
"""便捷:解析 ffmpeg。"""
|
|
47
|
+
return resolve_binary("ffmpeg")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_ffprobe() -> "str | None":
|
|
51
|
+
"""便捷:解析 ffprobe(可能为 None,仅时长/尺寸探测用,调用方自带兜底)。"""
|
|
52
|
+
return resolve_binary("ffprobe")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve(name: str) -> "str | None":
|
|
56
|
+
# 1) 显式指定
|
|
57
|
+
override = os.environ.get(_EXE_OVERRIDE[name])
|
|
58
|
+
if override and os.path.exists(override):
|
|
59
|
+
logger.info(f"[Compositor] {name}: explicit {override}")
|
|
60
|
+
return override
|
|
61
|
+
|
|
62
|
+
# 2) 系统 PATH
|
|
63
|
+
found = shutil.which(name)
|
|
64
|
+
if found:
|
|
65
|
+
logger.info(f"[Compositor] {name}: system {found}")
|
|
66
|
+
return found
|
|
67
|
+
|
|
68
|
+
# 3) imageio-ffmpeg 内置(仅自带 ffmpeg;ffprobe 从同目录推导)
|
|
69
|
+
base = _cache.get("ffmpeg") or _resolve_builtin_ffmpeg()
|
|
70
|
+
if base:
|
|
71
|
+
if name == "ffmpeg":
|
|
72
|
+
return base
|
|
73
|
+
probe = _sibling(base, "ffprobe")
|
|
74
|
+
if probe:
|
|
75
|
+
logger.info(f"[Compositor] {name}: builtin {probe}")
|
|
76
|
+
return probe
|
|
77
|
+
|
|
78
|
+
logger.warning(f"[Compositor] {name}: not found (no system PATH nor builtin)")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _resolve_builtin_ffmpeg() -> "str | None":
|
|
83
|
+
"""取 imageio-ffmpeg 内置静态二进制;加载/失败时记录并返回 None。"""
|
|
84
|
+
try:
|
|
85
|
+
import imageio_ffmpeg
|
|
86
|
+
|
|
87
|
+
exe = imageio_ffmpeg.get_ffmpeg_exe()
|
|
88
|
+
except Exception as e: # 加载失败 / 内置缺失 / 首次下载失败
|
|
89
|
+
logger.warning(f"[Compositor] builtin ffmpeg unavailable: {e}")
|
|
90
|
+
return None
|
|
91
|
+
if exe and os.path.exists(exe):
|
|
92
|
+
logger.info(f"[Compositor] ffmpeg: builtin {exe}")
|
|
93
|
+
return exe
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _sibling(exe: str, stem: str) -> "str | None":
|
|
98
|
+
"""由可执行文件路径推导同目录兄弟程序(Windows 补 .exe)。"""
|
|
99
|
+
candidate = os.path.join(
|
|
100
|
+
os.path.dirname(exe), stem + (".exe" if os.name == "nt" else "")
|
|
101
|
+
)
|
|
102
|
+
return candidate if os.path.exists(candidate) else None
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
8
|
|
|
9
|
+
from core.compositor.ffmpeg_tool import resolve_binary
|
|
10
|
+
|
|
9
11
|
logger = logging.getLogger(__name__)
|
|
10
12
|
|
|
11
13
|
|
|
@@ -20,7 +22,7 @@ class VideoProcessor:
|
|
|
20
22
|
|
|
21
23
|
import subprocess
|
|
22
24
|
subprocess.run([
|
|
23
|
-
"ffmpeg", "-y", "-i", input_path,
|
|
25
|
+
resolve_binary("ffmpeg"), "-y", "-i", input_path,
|
|
24
26
|
"-vf", f"scale={width}:{height}:force_original_aspect_ratio=decrease,"
|
|
25
27
|
f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2",
|
|
26
28
|
"-c:v", "libx264", "-preset", "fast",
|
|
@@ -37,7 +39,7 @@ class VideoProcessor:
|
|
|
37
39
|
|
|
38
40
|
import subprocess
|
|
39
41
|
subprocess.run([
|
|
40
|
-
"ffmpeg", "-y",
|
|
42
|
+
resolve_binary("ffmpeg"), "-y",
|
|
41
43
|
"-sseof", "-1",
|
|
42
44
|
"-i", video_path,
|
|
43
45
|
"-frames:v", "1",
|
|
@@ -55,7 +57,7 @@ class VideoProcessor:
|
|
|
55
57
|
|
|
56
58
|
import subprocess
|
|
57
59
|
subprocess.run([
|
|
58
|
-
"ffmpeg", "-y",
|
|
60
|
+
resolve_binary("ffmpeg"), "-y",
|
|
59
61
|
"-f", "lavfi",
|
|
60
62
|
"-i", "anullsrc=r=44100:cl=mono",
|
|
61
63
|
"-t", str(duration_sec),
|
|
@@ -87,7 +89,7 @@ class VideoProcessor:
|
|
|
87
89
|
# 注意:勿再用 -t {freeze_duration},那会把整个输出截断成只剩冻结段。
|
|
88
90
|
try:
|
|
89
91
|
subprocess.run([
|
|
90
|
-
"ffmpeg", "-y",
|
|
92
|
+
resolve_binary("ffmpeg"), "-y",
|
|
91
93
|
"-i", video_path,
|
|
92
94
|
"-vf", f"tpad=stop_mode=clone:stop_duration={freeze_duration}",
|
|
93
95
|
"-c:v", "libx264", "-preset", "fast",
|
|
@@ -11,6 +11,7 @@ import subprocess
|
|
|
11
11
|
import tempfile
|
|
12
12
|
from typing import NamedTuple, Optional
|
|
13
13
|
|
|
14
|
+
from core.compositor.ffmpeg_tool import resolve_binary
|
|
14
15
|
from core.config import resolve_font_path
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
@@ -194,7 +195,7 @@ def add_watermark(
|
|
|
194
195
|
|
|
195
196
|
# ffmpeg overlay: 将水印 PNG 叠加到视频上,无损流式拷贝
|
|
196
197
|
cmd = [
|
|
197
|
-
"ffmpeg", "-y",
|
|
198
|
+
resolve_binary("ffmpeg"), "-y",
|
|
198
199
|
"-i", input_path,
|
|
199
200
|
"-i", wm_png_path,
|
|
200
201
|
"-filter_complex", f"[0:v][1:v]overlay={layout.pos_x}:{layout.pos_y}[v]",
|
package/core/config.py
CHANGED
|
@@ -18,7 +18,7 @@ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
|
|
18
18
|
# ═══════════════════════════════════════════════════
|
|
19
19
|
# 应用版本号(v6.1 新增:发版时同步更新,见 docs/dev/release_process.md)
|
|
20
20
|
# ═══════════════════════════════════════════════════
|
|
21
|
-
APP_VERSION = "6.4.
|
|
21
|
+
APP_VERSION = "6.4.1"
|
|
22
22
|
|
|
23
23
|
# 未配置 API Key 时的统一报错文案(含免费获取与在线体验兜底,全站路由共用)
|
|
24
24
|
API_KEY_MISSING_MSG = (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-short-video",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.1",
|
|
4
4
|
"description": "Free AI short-video generator — run the full free-short-video service locally with one command. Text-to-video, image generation, Edge-TTS narration, subtitles, and compositing, all free. Official site: https://video.lichuanyang.top",
|
|
5
5
|
"bin": {
|
|
6
6
|
"free-short-video": "bin/cli.js",
|
package/server.py
CHANGED
|
@@ -105,6 +105,21 @@ async def lifespan(app: FastAPI):
|
|
|
105
105
|
except Exception as e:
|
|
106
106
|
logger.warning(f"[Startup] Stale task sweep failed ({e})")
|
|
107
107
|
|
|
108
|
+
# ffmpeg 启动检测(非阻断):解析一次并打印来源;缺失仅告警、不阻止启动。
|
|
109
|
+
try:
|
|
110
|
+
from core.compositor.ffmpeg_tool import resolve_binary
|
|
111
|
+
ffmpeg_bin = resolve_binary("ffmpeg")
|
|
112
|
+
if ffmpeg_bin:
|
|
113
|
+
logger.info(f"[Compositor] ffmpeg resolved: {ffmpeg_bin}")
|
|
114
|
+
else:
|
|
115
|
+
logger.warning(
|
|
116
|
+
"[Compositor] ffmpeg NOT found (no system PATH nor builtin "
|
|
117
|
+
"imageio-ffmpeg). Video/audio composition will fail later; "
|
|
118
|
+
"install ffmpeg or verify imageio-ffmpeg dependency."
|
|
119
|
+
)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.warning(f"[Compositor] ffmpeg detection failed: {e}")
|
|
122
|
+
|
|
108
123
|
yield
|
|
109
124
|
|
|
110
125
|
|