free-short-video 6.4.2 → 6.4.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 +8 -9
- package/core/api/agnes_chat.py +4 -3
- package/core/api/agnes_image.py +9 -5
- package/core/api/agnes_models.py +15 -3
- package/core/api/agnes_video.py +19 -10
- package/core/api/rate_limiter.py +12 -14
- package/core/async_io.py +62 -0
- package/core/audio/tts.py +4 -2
- package/core/config.py +3 -3
- package/core/pipelines/__init__.py +6 -9
- package/core/pipelines/anchor_video.py +2 -2
- package/core/pipelines/creative/pipeline.py +2 -2
- package/core/pipelines/creative/steps_audio.py +4 -6
- package/core/pipelines/creative/steps_script.py +11 -20
- package/core/pipelines/manuscript_video.py +5 -6
- package/core/pipelines/poetry_video.py +4 -7
- package/core/pipelines/simple_video.py +2 -2
- package/core/screenwriter/__init__.py +1 -1
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/server.py +3 -1
- package/sonar-project.properties +41 -0
- package/static/assets/ar-CQYg2nGj.js +1 -0
- package/static/assets/bn-GRmExguM.js +1 -0
- package/static/assets/de-BC7M_3b2.js +1 -0
- package/static/assets/es-B4nBYimU.js +1 -0
- package/static/assets/fa-BmlqC2rs.js +1 -0
- package/static/assets/fr-pidgrwQZ.js +1 -0
- package/static/assets/hi-BLey3CdD.js +1 -0
- package/static/assets/id-qgAFE4x8.js +1 -0
- package/static/assets/index-N-jLGcpC.js +45 -0
- package/static/assets/it-AkzVx0c6.js +1 -0
- package/static/assets/ja-5UwAsDwf.js +1 -0
- package/static/assets/ko-DKS32eFu.js +1 -0
- package/static/assets/ms-DSRBo61e.js +1 -0
- package/static/assets/nl-D90WoBj4.js +1 -0
- package/static/assets/pt-BF-gpCqr.js +1 -0
- package/static/assets/ru-BY7yjapB.js +1 -0
- package/static/assets/th-D1q4ZJcr.js +1 -0
- package/static/assets/tl-COEGFxts.js +1 -0
- package/static/assets/tr-EAmZYFuY.js +1 -0
- package/static/assets/ur-DeZ35cwW.js +1 -0
- package/static/assets/vi-2944d5L1.js +1 -0
- package/static/index.html +1 -1
- package/web/app_state.py +3 -1
- package/web/deps.py +3 -1
- package/web/helpers.py +6 -3
- package/web/log_safe.py +45 -0
- package/web/routes/config_routes.py +51 -21
- package/web/routes/image_routes.py +14 -5
- package/web/routes/task_creation_routes.py +23 -10
- package/web/routes/task_routes.py +15 -8
- package/web/routes/utility_routes.py +2 -2
- package/web/routes/video_routes.py +16 -12
- package/web/routes/voice_routes.py +2 -1
- package/static/assets/ar-DD2gvMKw.js +0 -1
- package/static/assets/bn-DPcCDLpk.js +0 -1
- package/static/assets/de-DFxaRKtY.js +0 -1
- package/static/assets/es-DoMCctKF.js +0 -1
- package/static/assets/fa-BzEKx0PN.js +0 -1
- package/static/assets/fr-C0bWeTQ6.js +0 -1
- package/static/assets/hi-C_hrAd50.js +0 -1
- package/static/assets/id-BlECjGv5.js +0 -1
- package/static/assets/index-DD5NtUvj.js +0 -45
- package/static/assets/it-BCF04BbE.js +0 -1
- package/static/assets/ja--ICvVBr-.js +0 -1
- package/static/assets/ko-DAjiyyWl.js +0 -1
- package/static/assets/ms-DamV6gQc.js +0 -1
- package/static/assets/nl-BOMKQzmH.js +0 -1
- package/static/assets/pt-CC05NXdy.js +0 -1
- package/static/assets/ru-DPWcNyU1.js +0 -1
- package/static/assets/th-9kl-UM3z.js +0 -1
- package/static/assets/tl-4mRVYM2I.js +0 -1
- package/static/assets/tr-dfD9cPiH.js +0 -1
- package/static/assets/ur-L0V50o2j.js +0 -1
- package/static/assets/vi-ChKtUMhj.js +0 -1
package/README.md
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
|
|
3
|
-
# What's New in v6.4.
|
|
3
|
+
# What's New in v6.4.4
|
|
4
4
|
|
|
5
5
|
## What's New
|
|
6
6
|
|
|
7
7
|
### Features & Improvements
|
|
8
8
|
|
|
9
|
-
* **
|
|
9
|
+
* **Image model selection enabled** — you can now choose the image model used for reference images, end frames, and standalone image generation directly in the Settings panel, instead of it being fixed to the built-in default.
|
|
10
10
|
|
|
11
|
-
* **
|
|
11
|
+
* **Agnes Image 2.5 Flash added and set as default** — the latest generation image model `agnes-image-2.5-flash` is now the default for both text-to-image and image-to-image workflows; the older `agnes-image-2.1-flash` and `agnes-image-2.0-flash` remain available as selectable options.
|
|
12
12
|
|
|
13
|
-
* **
|
|
13
|
+
* **LLM upgraded to Agnes 2.5 Flash** — the deprecated `agnes-2.0-flash` text model is removed from the selector and replaced by `agnes-2.5-flash` for story, script, and narration generation, following the official model deprecation notice.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* **Fixed multi-key authentication failures across domains** — after the Chinese domain migration, global-site keys could fail authentication against the strict domestic endpoint; domain is now resolved per key with the restored fallback, so mixed global/domestic keys authenticate successfully.
|
|
18
|
-
|
|
19
|
-
* **Domain mismatch hints in the config UI** — keys without a bound domain now show a clear "domain not set" hint, prompting users to pick or auto-detect the matching domain on their next visit.
|
|
15
|
+
---
|
|
20
16
|
|
|
17
|
+
This is a maintenance release focused on model flexibility: open image model selection, a newer default image model, and migration off the deprecated text model.
|
|
21
18
|
|
|
22
19
|
---
|
|
23
20
|
|
|
@@ -45,6 +42,8 @@ free-short-video
|
|
|
45
42
|
[](https://video.lichuanyang.top)
|
|
46
43
|
[](https://hub.docker.com/r/lcy362/free-short-video)
|
|
47
44
|
[](https://www.npmjs.com/package/free-short-video)
|
|
45
|
+
[](https://sonarcloud.io/project/overview?id=lcy362_agnes-video-generator)
|
|
46
|
+
[](https://sonarcloud.io/project/overview?id=lcy362_agnes-video-generator)
|
|
48
47
|
|
|
49
48
|
<p align="center">
|
|
50
49
|
<img src="images/home.png" alt="Agnes Video Generator — Free AI Video Generator" width="720">
|
package/core/api/agnes_chat.py
CHANGED
|
@@ -58,7 +58,7 @@ def strip_code_fence(text: str) -> str:
|
|
|
58
58
|
class AgnesChatAPI:
|
|
59
59
|
"""Agnes LLM Chat API 封装(text + multimodal)。"""
|
|
60
60
|
|
|
61
|
-
def __init__(self, api_key: str, model: str = "agnes-2.
|
|
61
|
+
def __init__(self, api_key: str, model: str = "agnes-2.5-flash"):
|
|
62
62
|
self.api_key = api_key
|
|
63
63
|
self.model = model
|
|
64
64
|
# 基础 headers(不含 Authorization):每次请求前经 _auth_headers() 注入当前 Key
|
|
@@ -217,16 +217,17 @@ class AgnesChatAPI:
|
|
|
217
217
|
# Step 1: 去围栏
|
|
218
218
|
cleaned = strip_code_fence(content)
|
|
219
219
|
# Step 2: 直接解析
|
|
220
|
+
# json.JSONDecodeError 是 ValueError 的子类,仅捕获 ValueError(S5713)
|
|
220
221
|
try:
|
|
221
222
|
return json.loads(cleaned)
|
|
222
|
-
except
|
|
223
|
+
except ValueError:
|
|
223
224
|
pass
|
|
224
225
|
# Step 3: 正则提取首个 {…} 块
|
|
225
226
|
match = _JSON_BLOCK_RE.search(cleaned)
|
|
226
227
|
if match:
|
|
227
228
|
try:
|
|
228
229
|
return json.loads(match.group())
|
|
229
|
-
except
|
|
230
|
+
except ValueError:
|
|
230
231
|
pass
|
|
231
232
|
# Step 3.5: json_repair 修复(可选依赖,修复 LLM 常见缺冒号/尾随逗号/单引号)
|
|
232
233
|
if repair_json is not None:
|
package/core/api/agnes_image.py
CHANGED
|
@@ -52,7 +52,7 @@ class AgnesImageAPI:
|
|
|
52
52
|
def __init__(
|
|
53
53
|
self,
|
|
54
54
|
api_key: str,
|
|
55
|
-
model: str = "agnes-image-2.
|
|
55
|
+
model: str = "agnes-image-2.5-flash",
|
|
56
56
|
i2i_model: Optional[str] = None,
|
|
57
57
|
):
|
|
58
58
|
"""初始化图片 API。
|
|
@@ -60,13 +60,13 @@ class AgnesImageAPI:
|
|
|
60
60
|
Args:
|
|
61
61
|
api_key: Agnes API Key。
|
|
62
62
|
model: t2i 默认模型。
|
|
63
|
-
i2i_model: i2i 默认模型。默认与 ``model`` 相同(官方 agnes-image-2.
|
|
64
|
-
同时支持 t2i 与 i2i
|
|
63
|
+
i2i_model: i2i 默认模型。默认与 ``model`` 相同(官方 agnes-image-2.5-flash
|
|
64
|
+
同时支持 t2i 与 i2i)。如需回退到旧版,可通过环境变量
|
|
65
65
|
``AGNES_IMAGE_I2I_MODEL`` 或显式传参覆盖。
|
|
66
66
|
"""
|
|
67
67
|
self.api_key = api_key
|
|
68
68
|
self.model = model
|
|
69
|
-
# i2i 默认与 t2i 同模型(官方 2.
|
|
69
|
+
# i2i 默认与 t2i 同模型(官方 2.5 同时支持 t2i/i2i);环境变量可回退到旧版。
|
|
70
70
|
from core.config import get_settings
|
|
71
71
|
env_i2i = get_settings().agnes_image_i2i_model
|
|
72
72
|
self.i2i_model = i2i_model or env_i2i or model
|
|
@@ -148,7 +148,11 @@ class AgnesImageAPI:
|
|
|
148
148
|
"image": resolved,
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
# S5145:不在日志中记录用户 prompt 内容(可能含敏感信息),只记模式与长度
|
|
152
|
+
logger.info(
|
|
153
|
+
f"[AgnesImage] Generating ({'i2i' if use_i2i else 't2i'}): "
|
|
154
|
+
f"prompt {len(prompt)} chars"
|
|
155
|
+
)
|
|
152
156
|
|
|
153
157
|
resp = None
|
|
154
158
|
attempt = 0
|
package/core/api/agnes_models.py
CHANGED
|
@@ -22,17 +22,20 @@ REQUEST_TIMEOUT = 20
|
|
|
22
22
|
# 分组失败时的兜底列表
|
|
23
23
|
_FALLBACK = {
|
|
24
24
|
"text": [DEFAULT_TEXT_MODEL],
|
|
25
|
-
"image": [DEFAULT_IMAGE_MODEL],
|
|
25
|
+
"image": [DEFAULT_IMAGE_MODEL, "agnes-image-2.1-flash", "agnes-image-2.0-flash"],
|
|
26
26
|
"video": [DEFAULT_VIDEO_MODEL],
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
# 已废弃、应从可选列表中剔除的模型 ID(如 agnes-2.0-flash,官方已迁移至 agnes-2.5-flash)
|
|
30
|
+
_DEPRECATED_MODELS = {"agnes-2.0-flash"}
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
def _classify(model_id: str) -> str:
|
|
31
34
|
"""根据模型 ID 前缀判断分组。
|
|
32
35
|
|
|
33
36
|
- ``agnes-image*`` → image
|
|
34
37
|
- ``agnes-video*`` → video
|
|
35
|
-
- 其余(如 ``agnes-2.
|
|
38
|
+
- 其余(如 ``agnes-2.5-flash``)→ text
|
|
36
39
|
"""
|
|
37
40
|
if model_id.startswith("agnes-image"):
|
|
38
41
|
return "image"
|
|
@@ -68,13 +71,22 @@ def fetch_available_models(api_key: str) -> dict:
|
|
|
68
71
|
grouped = {"text": [], "image": [], "video": []}
|
|
69
72
|
for item in data.get("data", []):
|
|
70
73
|
mid = item.get("id")
|
|
71
|
-
if not mid:
|
|
74
|
+
if not mid or mid in _DEPRECATED_MODELS:
|
|
72
75
|
continue
|
|
73
76
|
grouped[_classify(mid)].append(mid)
|
|
74
77
|
# 任一分类为空则补回默认,避免 UI 空下拉
|
|
75
78
|
for k, default in _FALLBACK.items():
|
|
76
79
|
if not grouped[k]:
|
|
77
80
|
grouped[k] = list(default)
|
|
81
|
+
# 确保各分类的默认模型始终在可选列表内(如服务端未返回时也能选择)
|
|
82
|
+
_defaults = {
|
|
83
|
+
"text": DEFAULT_TEXT_MODEL,
|
|
84
|
+
"image": DEFAULT_IMAGE_MODEL,
|
|
85
|
+
"video": DEFAULT_VIDEO_MODEL,
|
|
86
|
+
}
|
|
87
|
+
for k, d in _defaults.items():
|
|
88
|
+
if d not in grouped[k]:
|
|
89
|
+
grouped[k].append(d)
|
|
78
90
|
return grouped
|
|
79
91
|
except Exception as e: # noqa: BLE001
|
|
80
92
|
logger.warning("[AgnesModels] fetch failed (%s), using fallback", e)
|
package/core/api/agnes_video.py
CHANGED
|
@@ -71,6 +71,22 @@ def is_remote_video_failure(exc: BaseException) -> bool:
|
|
|
71
71
|
return "Video generation failed:" in str(exc)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def _read_json_cache(path: str) -> dict:
|
|
75
|
+
"""读取 JSON 缓存文件(线程池执行,避免阻塞事件循环,S7493)。"""
|
|
76
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
77
|
+
return json.loads(f.read())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _write_json_cache(path: str, data: dict) -> None:
|
|
81
|
+
"""原子写入 JSON 缓存文件(先写临时文件再 replace,线程池执行)。"""
|
|
82
|
+
tmp_file = path + ".tmp"
|
|
83
|
+
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
84
|
+
json.dump(data, f)
|
|
85
|
+
f.flush()
|
|
86
|
+
os.fsync(f.fileno())
|
|
87
|
+
os.replace(tmp_file, path)
|
|
88
|
+
|
|
89
|
+
|
|
74
90
|
class VideoOutput:
|
|
75
91
|
def __init__(self, fmt: str, ext: str, data: str):
|
|
76
92
|
self.fmt = fmt
|
|
@@ -147,8 +163,7 @@ class AgnesVideoAPI:
|
|
|
147
163
|
_URL_CACHE_MAX_AGE = 3600 # 1 小时
|
|
148
164
|
if os.path.exists(url_file):
|
|
149
165
|
try:
|
|
150
|
-
|
|
151
|
-
cache_data = json.loads(f.read())
|
|
166
|
+
cache_data = await asyncio.to_thread(_read_json_cache, url_file)
|
|
152
167
|
cached_url = cache_data.get("url", "")
|
|
153
168
|
cached_ts = cache_data.get("ts", 0)
|
|
154
169
|
age = time.time() - cached_ts
|
|
@@ -170,13 +185,7 @@ class AgnesVideoAPI:
|
|
|
170
185
|
url = await self._upload_image_to_url(ref)
|
|
171
186
|
if url:
|
|
172
187
|
try:
|
|
173
|
-
|
|
174
|
-
tmp_file = url_file + ".tmp"
|
|
175
|
-
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
176
|
-
json.dump(cache_data, f)
|
|
177
|
-
f.flush()
|
|
178
|
-
os.fsync(f.fileno())
|
|
179
|
-
os.replace(tmp_file, url_file)
|
|
188
|
+
await asyncio.to_thread(_write_json_cache, url_file, {"url": url, "ts": time.time()})
|
|
180
189
|
except Exception as e:
|
|
181
190
|
logger.debug(f"[AgnesVideo] Failed to cache URL: {e}")
|
|
182
191
|
return url
|
|
@@ -196,7 +205,7 @@ class AgnesVideoAPI:
|
|
|
196
205
|
try:
|
|
197
206
|
b64_data = self._path_to_b64(image_path)
|
|
198
207
|
payload = {
|
|
199
|
-
"model": "agnes-image-2.
|
|
208
|
+
"model": "agnes-image-2.5-flash",
|
|
200
209
|
"prompt": "Keep the image exactly as it is",
|
|
201
210
|
"n": 1,
|
|
202
211
|
"size": "1024x1024",
|
package/core/api/rate_limiter.py
CHANGED
|
@@ -187,21 +187,19 @@ class AgnesRateLimiter:
|
|
|
187
187
|
等待在事件循环中执行(不再占线程池);可被任务取消打断,或经
|
|
188
188
|
``stop_event`` 提前放行(停止后调用方不会再发请求,无需令牌)。
|
|
189
189
|
"""
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if wait_time is None:
|
|
193
|
-
return
|
|
194
|
-
self._record_wait(wait_time)
|
|
195
|
-
remaining = wait_time
|
|
196
|
-
while remaining > 0:
|
|
197
|
-
if stop_event is not None and stop_event.is_set():
|
|
198
|
-
return
|
|
199
|
-
await asyncio.sleep(min(0.1, remaining))
|
|
200
|
-
remaining -= 0.1
|
|
201
|
-
# 预支语义(与同步 acquire 一致):等待完成即视为已获取令牌。
|
|
202
|
-
# 注意不能再调用 _try_acquire()——last_refill 已被推进到
|
|
203
|
-
# now+wait_time,再次计算会因 elapsed≈0 而永远不足。
|
|
190
|
+
wait_time = self._try_acquire()
|
|
191
|
+
if wait_time is None:
|
|
204
192
|
return
|
|
193
|
+
self._record_wait(wait_time)
|
|
194
|
+
remaining = wait_time
|
|
195
|
+
while remaining > 0:
|
|
196
|
+
if stop_event is not None and stop_event.is_set():
|
|
197
|
+
return
|
|
198
|
+
await asyncio.sleep(min(0.1, remaining))
|
|
199
|
+
remaining -= 0.1
|
|
200
|
+
# 预支语义(与同步 acquire 一致):等待完成即视为已获取令牌。
|
|
201
|
+
# 注意不能再调用 _try_acquire()——last_refill 已被推进到
|
|
202
|
+
# now+wait_time,再次计算会因 elapsed≈0 而永远不足。
|
|
205
203
|
|
|
206
204
|
@property
|
|
207
205
|
def stats(self) -> dict:
|
package/core/async_io.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""异步文件读写辅助(Sonar ``python:S7493``)。
|
|
2
|
+
|
|
3
|
+
背景
|
|
4
|
+
----
|
|
5
|
+
在 ``async def`` 中直接使用内置 ``open()`` 会阻塞事件循环(文件 IO 虽短,
|
|
6
|
+
但在高并发/大文件场景下会拖慢整个服务)。Sonar 将此类调用判为 BUG。
|
|
7
|
+
|
|
8
|
+
方案
|
|
9
|
+
----
|
|
10
|
+
统一走 AnyIO 的 ``open_file``(FastAPI/Starlette 的既有依赖,无需新增包):
|
|
11
|
+
它把文件读写下沉到线程池,对外暴露与内置 ``open()`` 对齐的异步上下文管理器。
|
|
12
|
+
|
|
13
|
+
用法
|
|
14
|
+
----
|
|
15
|
+
需要完整文件对象时(如逐块写入)::
|
|
16
|
+
|
|
17
|
+
async with await async_open(path, "wb") as f:
|
|
18
|
+
await f.write(chunk)
|
|
19
|
+
|
|
20
|
+
只是整体读写时用便捷函数更省事::
|
|
21
|
+
|
|
22
|
+
content = await read_text(path)
|
|
23
|
+
await write_text(path, json.dumps(data, ensure_ascii=False))
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from anyio import open_file
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"async_open",
|
|
31
|
+
"read_bytes",
|
|
32
|
+
"read_text",
|
|
33
|
+
"write_bytes",
|
|
34
|
+
"write_text",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# 语义等价于内置 open(),返回 AnyIO 异步文件对象(需 ``await`` 后再 ``async with``)
|
|
38
|
+
async_open = open_file
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def read_text(path: str, encoding: str = "utf-8") -> str:
|
|
42
|
+
"""异步读取整个文本文件。"""
|
|
43
|
+
async with await async_open(path, "r", encoding=encoding) as f:
|
|
44
|
+
return await f.read()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def write_text(path: str, data: str, encoding: str = "utf-8") -> None:
|
|
48
|
+
"""异步写入整个文本文件(覆盖写)。"""
|
|
49
|
+
async with await async_open(path, "w", encoding=encoding) as f:
|
|
50
|
+
await f.write(data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def read_bytes(path: str) -> bytes:
|
|
54
|
+
"""异步读取整个二进制文件。"""
|
|
55
|
+
async with await async_open(path, "rb") as f:
|
|
56
|
+
return await f.read()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def write_bytes(path: str, data: bytes) -> None:
|
|
60
|
+
"""异步写入整个二进制文件(覆盖写)。"""
|
|
61
|
+
async with await async_open(path, "wb") as f:
|
|
62
|
+
await f.write(data)
|
package/core/audio/tts.py
CHANGED
|
@@ -11,6 +11,8 @@ from typing import Optional, Tuple
|
|
|
11
11
|
|
|
12
12
|
import edge_tts
|
|
13
13
|
|
|
14
|
+
from core.async_io import async_open
|
|
15
|
+
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
|
|
@@ -60,10 +62,10 @@ class EdgeTTSEngine(TTSEngine):
|
|
|
60
62
|
sub_maker = edge_tts.SubMaker()
|
|
61
63
|
|
|
62
64
|
tmp_path = output_path + ".tmp"
|
|
63
|
-
with
|
|
65
|
+
async with await async_open(tmp_path, "wb") as audio_file:
|
|
64
66
|
async for chunk in communicate.stream():
|
|
65
67
|
if chunk["type"] == "audio":
|
|
66
|
-
audio_file.write(chunk["data"])
|
|
68
|
+
await audio_file.write(chunk["data"])
|
|
67
69
|
elif chunk["type"] in ("WordBoundary", "SentenceBoundary"):
|
|
68
70
|
sub_maker.feed(chunk)
|
|
69
71
|
|
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.4"
|
|
22
22
|
|
|
23
23
|
# 未配置 API Key 时的统一报错文案(含免费获取与在线体验兜底,全站路由共用)
|
|
24
24
|
API_KEY_MISSING_MSG = (
|
|
@@ -870,8 +870,8 @@ DURATION_FRAME_MAP = {
|
|
|
870
870
|
# ═══════════════════════════════════════════════════
|
|
871
871
|
|
|
872
872
|
# 各类型 Agnes 模型默认值(与三个 API 客户端的默认 model 对齐)
|
|
873
|
-
DEFAULT_TEXT_MODEL = "agnes-2.
|
|
874
|
-
DEFAULT_IMAGE_MODEL = "agnes-image-2.
|
|
873
|
+
DEFAULT_TEXT_MODEL = "agnes-2.5-flash"
|
|
874
|
+
DEFAULT_IMAGE_MODEL = "agnes-image-2.5-flash"
|
|
875
875
|
DEFAULT_VIDEO_MODEL = "agnes-video-v2.0"
|
|
876
876
|
|
|
877
877
|
DEFAULT_MODELS = {
|
|
@@ -24,6 +24,7 @@ _ENCODING_EXECUTOR = concurrent.futures.ThreadPoolExecutor(
|
|
|
24
24
|
max_workers=2, thread_name_prefix="encoding"
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
from core.async_io import read_text, write_text
|
|
27
28
|
from core.compositor.watermark import add_watermark, detect_language
|
|
28
29
|
from core.config import get_watermark_config
|
|
29
30
|
from core.task_manager import TaskManager
|
|
@@ -542,8 +543,7 @@ class BasePipeline(ABC):
|
|
|
542
543
|
)
|
|
543
544
|
|
|
544
545
|
if srt_content.strip():
|
|
545
|
-
|
|
546
|
-
f.write(srt_content)
|
|
546
|
+
await write_text(srt_path, srt_content)
|
|
547
547
|
entry_count = srt_content.count("\n\n") + 1 if "\n\n" in srt_content else 0
|
|
548
548
|
logger.info(
|
|
549
549
|
"[Subtitle] Scene-aware SRT: %d entries across %d segments",
|
|
@@ -557,19 +557,17 @@ class BasePipeline(ABC):
|
|
|
557
557
|
total_dur = actual_audio_dur if actual_audio_dur > 0 else sum(segment_durations)
|
|
558
558
|
SubtitleGenerator.text_to_srt(full_text, srt_path, total_dur)
|
|
559
559
|
else:
|
|
560
|
-
|
|
561
|
-
f.write("")
|
|
560
|
+
await write_text(srt_path, "")
|
|
562
561
|
|
|
563
562
|
# ── 2.5 统一后处理:确保每条字幕不超过 2 行(参考中文短字幕规范,适配所有语言)──
|
|
564
563
|
try:
|
|
565
|
-
raw_srt =
|
|
564
|
+
raw_srt = await read_text(srt_path)
|
|
566
565
|
fixed_srt = SubtitleGenerator.enforce_max_lines(
|
|
567
566
|
raw_srt, max_lines=2,
|
|
568
567
|
video_width=video_width, fontsize=subtitle_config.style.fontsize,
|
|
569
568
|
)
|
|
570
569
|
if fixed_srt and fixed_srt != raw_srt:
|
|
571
|
-
|
|
572
|
-
f.write(fixed_srt)
|
|
570
|
+
await write_text(srt_path, fixed_srt)
|
|
573
571
|
entry_count = fixed_srt.count("\n\n") + 1 if "\n\n" in fixed_srt else 1
|
|
574
572
|
logger.info(
|
|
575
573
|
"[Subtitle] enforce_max_lines applied: ≤2 lines/entry (%d entries)",
|
|
@@ -593,8 +591,7 @@ class BasePipeline(ABC):
|
|
|
593
591
|
style_hints=subtitle_config.style.style_hints,
|
|
594
592
|
**({"role": role} if role else {}),
|
|
595
593
|
)
|
|
596
|
-
|
|
597
|
-
json.dump(styles, f, ensure_ascii=False, indent=2)
|
|
594
|
+
await write_text(sp, json.dumps(styles, ensure_ascii=False, indent=2))
|
|
598
595
|
styles_path = sp
|
|
599
596
|
logger.info(
|
|
600
597
|
"[Subtitle] LLM styles saved: %s (%d entries)",
|
|
@@ -75,8 +75,8 @@ class AnchorPipeline(MultiScenePipeline):
|
|
|
75
75
|
api_key: str,
|
|
76
76
|
task_id: str,
|
|
77
77
|
dir_name: Optional[str] = None,
|
|
78
|
-
chat_model: str = "agnes-2.
|
|
79
|
-
image_model: str = "agnes-image-2.
|
|
78
|
+
chat_model: str = "agnes-2.5-flash",
|
|
79
|
+
image_model: str = "agnes-image-2.5-flash",
|
|
80
80
|
video_model: str = "agnes-video-v2.0",
|
|
81
81
|
progress_callback: Optional[Callable] = None,
|
|
82
82
|
shutdown_event: Optional[asyncio.Event] = None,
|
|
@@ -47,8 +47,8 @@ class CreativeVideoPipeline(
|
|
|
47
47
|
api_key: str,
|
|
48
48
|
task_id: str,
|
|
49
49
|
dir_name: Optional[str] = None,
|
|
50
|
-
chat_model: str = "agnes-2.
|
|
51
|
-
image_model: str = "agnes-image-2.
|
|
50
|
+
chat_model: str = "agnes-2.5-flash",
|
|
51
|
+
image_model: str = "agnes-image-2.5-flash",
|
|
52
52
|
video_model: str = "agnes-video-v2.0",
|
|
53
53
|
progress_callback: Optional[Callable] = None,
|
|
54
54
|
shutdown_event: Optional[asyncio.Event] = None,
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import re
|
|
7
7
|
from typing import Optional
|
|
8
8
|
|
|
9
|
+
from core.async_io import read_text
|
|
9
10
|
from core.audio.voices import estimate_chars_per_sec
|
|
10
11
|
from core.compositor.concatenator import VideoConcatenator
|
|
11
12
|
from core.screenwriter import clean_narration_text
|
|
@@ -222,8 +223,7 @@ class AudioStepsMixin:
|
|
|
222
223
|
script_path = os.path.join(self.working_dir, "script.json")
|
|
223
224
|
if os.path.exists(script_path):
|
|
224
225
|
try:
|
|
225
|
-
|
|
226
|
-
script_prompts = json.load(f)
|
|
226
|
+
script_prompts = json.loads(await read_text(script_path))
|
|
227
227
|
except Exception:
|
|
228
228
|
pass
|
|
229
229
|
prompts_data = {
|
|
@@ -401,10 +401,8 @@ class AudioStepsMixin:
|
|
|
401
401
|
prompts_path = os.path.join(self.working_dir, "prompts.json")
|
|
402
402
|
existing = {}
|
|
403
403
|
if os.path.exists(prompts_path):
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
with open(styles_path, "r", encoding="utf-8") as f:
|
|
407
|
-
existing["subtitle_styles"] = json.load(f)
|
|
404
|
+
existing = json.loads(await read_text(prompts_path))
|
|
405
|
+
existing["subtitle_styles"] = json.loads(await read_text(styles_path))
|
|
408
406
|
self.save_prompts(existing)
|
|
409
407
|
except Exception:
|
|
410
408
|
pass
|
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
from typing import List
|
|
7
7
|
|
|
8
|
+
from core.async_io import read_text, write_text
|
|
8
9
|
from core.pipelines import PipelineShutdown
|
|
9
10
|
from models.task import SceneTask, StepStatus
|
|
10
11
|
|
|
@@ -55,8 +56,7 @@ class ScriptStepsMixin:
|
|
|
55
56
|
if self._state.step_image_analysis == StepStatus.COMPLETED:
|
|
56
57
|
analysis_file = self._state.image_analysis_file
|
|
57
58
|
if os.path.exists(analysis_file):
|
|
58
|
-
|
|
59
|
-
content = f.read()
|
|
59
|
+
content = await read_text(analysis_file)
|
|
60
60
|
# 检测之前分析失败留下的错误文本,强制重新分析
|
|
61
61
|
if "(分析失败" in content:
|
|
62
62
|
logger.warning(
|
|
@@ -95,8 +95,7 @@ class ScriptStepsMixin:
|
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
analysis_file = os.path.join(self.working_dir, "image_analysis.txt")
|
|
98
|
-
|
|
99
|
-
f.write(image_context)
|
|
98
|
+
await write_text(analysis_file, image_context)
|
|
100
99
|
|
|
101
100
|
self._state.step_image_analysis = StepStatus.COMPLETED
|
|
102
101
|
self._state.image_analysis_file = analysis_file
|
|
@@ -244,8 +243,7 @@ class ScriptStepsMixin:
|
|
|
244
243
|
story_path = self._state.story_file
|
|
245
244
|
if os.path.exists(story_path):
|
|
246
245
|
logger.info("[Pipeline] Step story: SKIP (already completed, file exists)")
|
|
247
|
-
|
|
248
|
-
return f.read()
|
|
246
|
+
return await read_text(story_path)
|
|
249
247
|
logger.warning("[Pipeline] Step story: marked completed but file missing, re-running")
|
|
250
248
|
|
|
251
249
|
logger.info("[Pipeline] Step story: RUNNING")
|
|
@@ -261,8 +259,7 @@ class ScriptStepsMixin:
|
|
|
261
259
|
)
|
|
262
260
|
|
|
263
261
|
story_path = os.path.join(self.working_dir, "story.txt")
|
|
264
|
-
|
|
265
|
-
f.write(story)
|
|
262
|
+
await write_text(story_path, story)
|
|
266
263
|
|
|
267
264
|
self._state.step_story = StepStatus.COMPLETED
|
|
268
265
|
self._state.story_file = story_path
|
|
@@ -314,8 +311,7 @@ class ScriptStepsMixin:
|
|
|
314
311
|
if os.path.exists(ref_img_path) and os.path.exists(ref_prompt_path):
|
|
315
312
|
self._state.step_character_ref = StepStatus.COMPLETED
|
|
316
313
|
self._state.character_ref_file = ref_img_path
|
|
317
|
-
|
|
318
|
-
self._state.character_ref_prompt = f.read()
|
|
314
|
+
self._state.character_ref_prompt = await read_text(ref_prompt_path)
|
|
319
315
|
self.task_manager.update_state(
|
|
320
316
|
step_character_ref=StepStatus.COMPLETED,
|
|
321
317
|
character_ref_file=ref_img_path,
|
|
@@ -327,8 +323,7 @@ class ScriptStepsMixin:
|
|
|
327
323
|
char_prompt = await asyncio.to_thread(
|
|
328
324
|
self.screenwriter.extract_character_description, story, self._state.style
|
|
329
325
|
)
|
|
330
|
-
|
|
331
|
-
f.write(char_prompt)
|
|
326
|
+
await write_text(ref_prompt_path, char_prompt)
|
|
332
327
|
|
|
333
328
|
await self._emit("character_ref", "running", "正在生成角色参考图 (t2i)...", _PROGRESS_CHARACTER_REF_T2I)
|
|
334
329
|
img_output = await self.image_generator.generate_single_image(
|
|
@@ -365,8 +360,7 @@ class ScriptStepsMixin:
|
|
|
365
360
|
script_path = self._state.script_file
|
|
366
361
|
if os.path.exists(script_path):
|
|
367
362
|
logger.info("[Pipeline] Step script: SKIP (already completed, file exists)")
|
|
368
|
-
|
|
369
|
-
scenes = json.load(f)
|
|
363
|
+
scenes = json.loads(await read_text(script_path))
|
|
370
364
|
if self._state.scene_count == len(scenes):
|
|
371
365
|
return scenes
|
|
372
366
|
logger.warning("[Pipeline] Step script: scene count mismatch, re-running")
|
|
@@ -383,8 +377,7 @@ class ScriptStepsMixin:
|
|
|
383
377
|
)
|
|
384
378
|
|
|
385
379
|
script_path = os.path.join(self.working_dir, "script.json")
|
|
386
|
-
|
|
387
|
-
json.dump(scenes, f, ensure_ascii=False, indent=2)
|
|
380
|
+
await write_text(script_path, json.dumps(scenes, ensure_ascii=False, indent=2))
|
|
388
381
|
|
|
389
382
|
self._state.scene_count = len(scenes)
|
|
390
383
|
# Map durations to scenes: use scene_durations list, pad/trim as needed
|
|
@@ -453,8 +446,7 @@ class ScriptStepsMixin:
|
|
|
453
446
|
prompts_path = self._state.end_frame_prompts_file
|
|
454
447
|
if os.path.exists(prompts_path):
|
|
455
448
|
logger.info("[Pipeline] Step end_frame_prompts: SKIP (already completed, file exists)")
|
|
456
|
-
|
|
457
|
-
return json.load(f)
|
|
449
|
+
return json.loads(await read_text(prompts_path))
|
|
458
450
|
logger.warning("[Pipeline] Step end_frame_prompts: marked completed but file missing, re-running")
|
|
459
451
|
|
|
460
452
|
logger.info("[Pipeline] Step end_frame_prompts: RUNNING")
|
|
@@ -471,8 +463,7 @@ class ScriptStepsMixin:
|
|
|
471
463
|
)
|
|
472
464
|
|
|
473
465
|
prompts_path = os.path.join(self.working_dir, "end_frame_prompts.json")
|
|
474
|
-
|
|
475
|
-
json.dump(end_frame_prompts, f, ensure_ascii=False, indent=2)
|
|
466
|
+
await write_text(prompts_path, json.dumps(end_frame_prompts, ensure_ascii=False, indent=2))
|
|
476
467
|
|
|
477
468
|
self._state.step_end_frame_prompts = StepStatus.COMPLETED
|
|
478
469
|
self._state.end_frame_prompts_file = prompts_path
|
|
@@ -16,6 +16,7 @@ import re
|
|
|
16
16
|
from typing import Callable, List, Optional
|
|
17
17
|
|
|
18
18
|
from core.api.agnes_video import AgnesVideoAPI, VideoTaskCancelled
|
|
19
|
+
from core.async_io import read_text
|
|
19
20
|
from core.audio.voices import duration_len, estimate_chars_per_sec
|
|
20
21
|
from core.compositor.concatenator import VideoConcatenator
|
|
21
22
|
from core.pipelines import MultiScenePipeline
|
|
@@ -143,8 +144,8 @@ class ManuscriptVideoPipeline(MultiScenePipeline):
|
|
|
143
144
|
api_key: str,
|
|
144
145
|
task_id: str,
|
|
145
146
|
dir_name: str = None,
|
|
146
|
-
chat_model: str = "agnes-2.
|
|
147
|
-
image_model: str = "agnes-image-2.
|
|
147
|
+
chat_model: str = "agnes-2.5-flash",
|
|
148
|
+
image_model: str = "agnes-image-2.5-flash",
|
|
148
149
|
video_model: str = "agnes-video-v2.0",
|
|
149
150
|
progress_callback: Optional[Callable] = None,
|
|
150
151
|
shutdown_event: Optional[asyncio.Event] = None,
|
|
@@ -542,10 +543,8 @@ class ManuscriptVideoPipeline(MultiScenePipeline):
|
|
|
542
543
|
prompts_path = os.path.join(self.working_dir, "prompts.json")
|
|
543
544
|
existing = {}
|
|
544
545
|
if os.path.exists(prompts_path):
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
with open(styles_path, "r", encoding="utf-8") as f:
|
|
548
|
-
existing["subtitle_styles"] = json.load(f)
|
|
546
|
+
existing = json.loads(await read_text(prompts_path))
|
|
547
|
+
existing["subtitle_styles"] = json.loads(await read_text(styles_path))
|
|
549
548
|
self.save_prompts(existing)
|
|
550
549
|
except Exception:
|
|
551
550
|
pass
|
|
@@ -47,9 +47,6 @@ _PROGRESS_AUDIO_SCENE = 0.75
|
|
|
47
47
|
_PROGRESS_SUBTITLE_SCENE = 0.87
|
|
48
48
|
_PROGRESS_COMPOSITE_SCENE = 0.90
|
|
49
49
|
|
|
50
|
-
_SCENE_LABEL_RE = re.compile(r"^(场景|Scene)\s*\d+|[((]\s*\d+\s*:\s*\d+")
|
|
51
|
-
|
|
52
|
-
|
|
53
50
|
def _is_scene_label(line: str) -> bool:
|
|
54
51
|
"""判断一行是否为纯场景标签(如「场景 1(00:00 - 00:10)」),而非分镜描述。
|
|
55
52
|
|
|
@@ -76,7 +73,7 @@ class PoetryVideoPipeline(MultiScenePipeline):
|
|
|
76
73
|
api_key: str,
|
|
77
74
|
task_id: str,
|
|
78
75
|
dir_name: Optional[str] = None,
|
|
79
|
-
chat_model: str = "agnes-2.
|
|
76
|
+
chat_model: str = "agnes-2.5-flash",
|
|
80
77
|
video_model: str = "agnes-video-v2.0",
|
|
81
78
|
progress_callback: Optional[callable] = None,
|
|
82
79
|
shutdown_event: Optional = None,
|
|
@@ -424,9 +421,9 @@ class PoetryVideoPipeline(MultiScenePipeline):
|
|
|
424
421
|
f"生成字幕 {idx+1}/{len(scenes)}...", _PROGRESS_SUBTITLE_SCENE,
|
|
425
422
|
)
|
|
426
423
|
# v2.0:优先用该场景 cues 生成精确字幕;cues 缺失则回退纯文本估算
|
|
427
|
-
|
|
428
|
-
if
|
|
429
|
-
SubtitleGenerator.cues_to_srt(
|
|
424
|
+
scene_sub_maker = getattr(self, "_scene_sub_makers", {}).get(idx)
|
|
425
|
+
if scene_sub_maker is not None and getattr(scene_sub_maker, "cues", None):
|
|
426
|
+
SubtitleGenerator.cues_to_srt(scene_sub_maker, srt_path)
|
|
430
427
|
if not (os.path.exists(srt_path) and os.path.getsize(srt_path) > 0):
|
|
431
428
|
SubtitleGenerator.text_to_srt(
|
|
432
429
|
text, srt_path, duration_sec=dur, chars_per_sec=_CHARS_PER_SEC,
|
|
@@ -37,8 +37,8 @@ class SimpleVideoPipeline(BasePipeline):
|
|
|
37
37
|
api_key: str,
|
|
38
38
|
task_id: str,
|
|
39
39
|
dir_name: str = None,
|
|
40
|
-
chat_model: str = "agnes-2.
|
|
41
|
-
image_model: str = "agnes-image-2.
|
|
40
|
+
chat_model: str = "agnes-2.5-flash",
|
|
41
|
+
image_model: str = "agnes-image-2.5-flash",
|
|
42
42
|
video_model: str = "agnes-video-v2.0",
|
|
43
43
|
progress_callback: Optional[Callable] = None,
|
|
44
44
|
shutdown_event: Optional[asyncio.Event] = None,
|