free-short-video 7.1.0 → 7.1.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 +44 -18
- package/core/api/agnes_chat.py +28 -14
- package/core/api/agnes_video.py +14 -102
- package/core/api/key_manager.py +5 -2
- package/core/config.py +17 -29
- package/core/i18n_backend.py +19 -0
- package/core/pipelines/anchor_video.py +1 -1
- package/core/pipelines/creative/pipeline.py +1 -1
- package/core/pipelines/creative/steps_frames.py +16 -5
- package/core/pipelines/manuscript_video.py +1 -1
- package/core/pipelines/poetry_video.py +1 -1
- package/core/pipelines/simple_video.py +1 -1
- package/core/screenwriter/scenes.py +2 -1
- package/package.json +1 -1
- package/static/assets/ar-uqdQsD8d.js +1 -0
- package/static/assets/bn-Cs0c8ay0.js +1 -0
- package/static/assets/de-C03R7qVE.js +1 -0
- package/static/assets/es-LJ2OkYIx.js +1 -0
- package/static/assets/{fa-BzLk3cXs.js → fa-DySEd8zE.js} +1 -1
- package/static/assets/fr-BTafm0bE.js +1 -0
- package/static/assets/hi-Bk8MY-fF.js +1 -0
- package/static/assets/id-DZGFWLC-.js +1 -0
- package/static/assets/{index-BYtvF5AK.js → index-C1tAWGVu.js} +17 -17
- package/static/assets/it-DEnLooJL.js +1 -0
- package/static/assets/ja-D7VxE6bk.js +1 -0
- package/static/assets/ko-nc2euVdS.js +1 -0
- package/static/assets/ms-Jz3t8mz-.js +1 -0
- package/static/assets/nl-D4aafNI8.js +1 -0
- package/static/assets/pt-CBY3pDPr.js +1 -0
- package/static/assets/ru-B3WZA3F-.js +1 -0
- package/static/assets/th-BSUvfnqU.js +1 -0
- package/static/assets/tl-DSeqPP4N.js +1 -0
- package/static/assets/tr-jcUJ-jsZ.js +1 -0
- package/static/assets/ur-cYxmbnCI.js +1 -0
- package/static/assets/vi-Dh-Jp2tU.js +1 -0
- package/static/index.html +1 -1
- package/web/routes/task_creation_routes.py +80 -18
- package/static/assets/ar-C1KWUlNh.js +0 -1
- package/static/assets/bn-C4SfxNGN.js +0 -1
- package/static/assets/de-B4OvLzBp.js +0 -1
- package/static/assets/es-IQACehBJ.js +0 -1
- package/static/assets/fr-BMhNx-iQ.js +0 -1
- package/static/assets/hi-B7Vaoj8j.js +0 -1
- package/static/assets/id-B5vxBOhd.js +0 -1
- package/static/assets/it-DwDZ9skF.js +0 -1
- package/static/assets/ja-DyBi5ySN.js +0 -1
- package/static/assets/ko-DtnJDAHo.js +0 -1
- package/static/assets/ms-CCjt7pyM.js +0 -1
- package/static/assets/nl-Bqpu5-or.js +0 -1
- package/static/assets/pt-DzUv3ga-.js +0 -1
- package/static/assets/ru-CMGJcpUZ.js +0 -1
- package/static/assets/th-DnlP-4Ac.js +0 -1
- package/static/assets/tl-BzNuvuWV.js +0 -1
- package/static/assets/tr-Ca1gT8-V.js +0 -1
- package/static/assets/ur-DMIuAGiE.js +0 -1
- package/static/assets/vi-BURrm61d.js +0 -1
package/README.md
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
|
|
3
|
-
# What's New in v7.1.
|
|
3
|
+
# What's New in v7.1.1
|
|
4
4
|
|
|
5
5
|
## What's New
|
|
6
6
|
|
|
7
7
|
### Features & Improvements
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
- **
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- **Unified upstream retry policy ("busy / fault" two-track model).** Every upstream call — video submission, image generation, image upload and chat — now shares one retry policy module (`core/api/retry_policy.py`) as the single source of truth for classifying a response and computing the next interval:
|
|
15
|
-
- **Busy track** (HTTP 429, *all* 503 including bare 503 without an error code, and queue-full codes): the first retry fires immediately, hugging the rate limit so the token bucket — not a hand-rolled sleep — sets the pace; subsequent retries use a fixed interval (steady "knocking") under an attempt cap.
|
|
16
|
-
- **Fault track** (other 5xx, timeouts, connection errors): the previous escalating backoff `base × (n + 1)`, also attempt-capped.
|
|
17
|
-
This replaces the previously scattered per-provider retry branches, so behavior is consistent across all upstream calls and easier to reason about and tune. The busy-track cap also moved from a wall-clock budget to an attempt count, which on multi-key setups lets the same number of attempts finish sooner.
|
|
18
|
-
- **Robust image upload path.** The image-upload retry loop was rebuilt on the same two-track policy, with a bounded loop and consistent backoff instead of a hard-coded pause.
|
|
9
|
+
- **`agnes-video-2.5-flash` is the default video model.** With the legacy Video 2.0 channel retired upstream, the model lineup now covers the 2.5 series only, and every video task is submitted through the 2.5 standard protocol (`mode` / `seconds` / `size` / `aspect_ratio`). The per-model guidance shown in the create-task form reflects the available models.
|
|
10
|
+
- **Configurable configuration directory for npm deployments.** A new `AGNES_CONFIG_DIR` environment variable lets `npx free-short-video` users keep `config.json` (their saved keys and settings) in a stable location such as `~/.agnes_config`, instead of inside the ephemeral npm cache that is lost on `npm cache clean` or a cache-directory change.
|
|
11
|
+
- **Image uploads are validated on arrival.** Non-whitelisted file types are rejected with a readable message instead of being silently rewritten, and uploaded bytes are checked to be a decodable image. A corrupt file now fails immediately with a clear, scene-scoped message rather than surfacing as a low-level error during generation.
|
|
12
|
+
- **Feedback panel and diagnostic report fully localized.** The remaining feedback-panel and diagnostic-report labels that still appeared in English on non-English interfaces are now translated across all 22 interface languages.
|
|
19
13
|
|
|
20
14
|
### Bug Fixes
|
|
21
15
|
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
No action is required after upgrading. If you previously set `AGNES_VIDEO_QUEUE_RETRY_SECONDS`, switch to `AGNES_VIDEO_BUSY_RETRY_ATTEMPTS` (see Upgrade notes).
|
|
16
|
+
- **Custom end-frame images keep their scene mapping.** Uploaded end-frame images are now saved to their own scene index, so an empty slot no longer shifts later images onto the wrong scenes.
|
|
17
|
+
- **Chat calls retry once on an empty response** instead of aborting the screenwriting stage with a `None` or `KeyError`.
|
|
18
|
+
- **Poetry scene prompts use a language-neutral default style.** When no style is specified, the default hint is no longer a hardcoded Chinese phrase, so non-Chinese runs stay in their own language.
|
|
19
|
+
- **API-key authentication diagnostics no longer write the configured site domain into the log line.**
|
|
28
20
|
|
|
29
21
|
---
|
|
30
22
|
|
|
@@ -112,12 +104,45 @@ schema.org structured data for SEO/GEO indexing. GitHub does not execute this sc
|
|
|
112
104
|
|
|
113
105
|
> **🖥️ 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.
|
|
114
106
|
|
|
115
|
-
## 🚀
|
|
107
|
+
## 🚀 Three Ways to Use — All Completely Free
|
|
116
108
|
|
|
117
109
|
| Project | Run Where | Features | Link |
|
|
118
110
|
|---------|-----------|----------|------|
|
|
119
111
|
| **[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) |
|
|
120
112
|
| **[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) |
|
|
113
|
+
| **[Agnes Video Generator](https://github.com/lcy362/agnes-video-generator)** — **agent-ready** | **Driven by an AI coding agent** | **Least effort** — ships [`AGENTS.md`](AGENTS.md) plus a companion skill set (usage / troubleshoot / contrib): the agent installs the project, generates the video and debugs failures itself | [Agent guide](docs/public/agent-operations.md) · [Flint](https://github.com/lcy362/flint) |
|
|
114
|
+
|
|
115
|
+
## 🤖 Or Let an AI Agent Drive It
|
|
116
|
+
|
|
117
|
+
This project ships an [`AGENTS.md`](AGENTS.md) written for AI coding assistants, so an agent can install it, generate videos and debug failures after reading one document. Clone the repo, then tell your agent:
|
|
118
|
+
|
|
119
|
+
> "Read the AGENTS.md in this project, install dependencies, configure the API key `<your-key>`, and start the server."
|
|
120
|
+
|
|
121
|
+
Then ask it to prove the setup with `"Run the deployment verification checks."` — it executes the four-layer checklist (connectivity → static analysis → endpoint tests → features) and reports each result.
|
|
122
|
+
|
|
123
|
+
**Want your agent to skip the learning step?** Install the companion skill set — three ready-made skills (**usage**: deploy, pick a mode, submit and watch a task, edit artifacts at Manual Mode checkpoints; **troubleshoot**: find out why a run failed; **contrib**: change the code, get the PR through CI) plus small scripts for task control, deployment verification, evidence collection, media checks and the local CI gate:
|
|
124
|
+
|
|
125
|
+
**[lcy362/agnes-video-generator-skills](https://github.com/lcy362/agnes-video-generator-skills)**
|
|
126
|
+
|
|
127
|
+
Clone the repository first:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
git clone https://github.com/lcy362/agnes-video-generator-skills.git
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Recommended — let [flint](https://github.com/lcy362/flint) install and distribute it.** flint is a local-first, open-source skills manager that keeps your skills in one place and deploys them into any agent or project directory by symlink or copy. It works on local sources only — it never clones anything for you — so you register the directory you just cloned, and the three skills appear in front of every agent you use:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx flint-skills-hub # Sources → add the cloned directory (flint reads its skills/ subdirectory)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Or copy it in by hand** — no flint required, but you repeat it for every project and every new agent:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
cp -R agnes-video-generator-skills/skills/* <project>/.agents/skills/ # any agent that reads .agents/skills
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Full guide: **[Let an AI Agent Operate This Project](docs/public/agent-operations.md)**.
|
|
121
146
|
|
|
122
147
|
## ⭐ Support & Contribute
|
|
123
148
|
|
|
@@ -203,6 +228,7 @@ Full walkthrough: [Getting Started → Configure API Key](docs/public/getting-st
|
|
|
203
228
|
|
|
204
229
|
- **[Features](docs/public/features.md)** — Creation modes, the completely free AI model chain, AI narration & smart subtitles, flexible creative controls, production-grade reliability, and the multilingual Web UI.
|
|
205
230
|
- **[Getting Started](docs/public/getting-started.md)** — Install and deploy in 4 ways: Manual (`start.sh`), Docker, npm (`npx free-short-video`), or AI-Agent assisted.
|
|
231
|
+
- **[AI Agent Operations](docs/public/agent-operations.md)** — Three levels of agent-driven operation: hand your agent `AGENTS.md`, install the companion skill set, or distribute it to every agent with flint.
|
|
206
232
|
- **[Usage Guide](docs/public/usage.md)** — Configure your API key, pick a video mode, resume from checkpoints, the three chaining modes, and logs & output layout.
|
|
207
233
|
- **[Architecture](docs/public/architecture.md)** — Project structure and tech stack.
|
|
208
234
|
- **[API Reference](docs/public/api.md)** — Full REST endpoint list (progress via polling, no WebSocket).
|
package/core/api/agnes_chat.py
CHANGED
|
@@ -191,20 +191,34 @@ class AgnesChatAPI:
|
|
|
191
191
|
from core.config import get_settings
|
|
192
192
|
|
|
193
193
|
timeout_retry_limit = 1
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
207
|
-
|
|
194
|
+
payload = {
|
|
195
|
+
"model": self.model,
|
|
196
|
+
"messages": [
|
|
197
|
+
{"role": "system", "content": system_prompt},
|
|
198
|
+
{"role": "user", "content": user_prompt},
|
|
199
|
+
],
|
|
200
|
+
"temperature": 0.7,
|
|
201
|
+
"max_tokens": max_tokens,
|
|
202
|
+
}
|
|
203
|
+
# 空响应守卫:缺失 / 空正文时重试一次,避免把 ``None`` 或 KeyError 直接
|
|
204
|
+
# 抛给上层(原实现为裸 ``data["choices"][0]["message"]["content"]``)。
|
|
205
|
+
content = ""
|
|
206
|
+
for attempt in range(2):
|
|
207
|
+
data = self._request_with_retry(
|
|
208
|
+
payload,
|
|
209
|
+
timeout=get_settings().agnes_chat_timeout,
|
|
210
|
+
timeout_retry_limit=timeout_retry_limit,
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
content = data["choices"][0]["message"]["content"] or ""
|
|
214
|
+
except (KeyError, IndexError, TypeError):
|
|
215
|
+
content = ""
|
|
216
|
+
if isinstance(content, str) and content.strip():
|
|
217
|
+
return content
|
|
218
|
+
content = ""
|
|
219
|
+
if attempt == 0:
|
|
220
|
+
logger.warning("[AgnesChat] Empty chat response; retrying once...")
|
|
221
|
+
raise RuntimeError("Agnes Chat returned empty content after retry")
|
|
208
222
|
|
|
209
223
|
def chat_json(self, system_prompt: str, user_prompt: str, max_tokens: int = 4096) -> dict:
|
|
210
224
|
"""Chat 调用并解析 JSON 响应(健壮版)。
|
package/core/api/agnes_video.py
CHANGED
|
@@ -34,14 +34,6 @@ from utils.video import download_video
|
|
|
34
34
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
36
36
|
|
|
37
|
-
DURATION_PRESETS = {
|
|
38
|
-
5: (121, 24),
|
|
39
|
-
10: (241, 24),
|
|
40
|
-
15: (361, 24),
|
|
41
|
-
18: (409, 24), # capped at 409 (API max for 720p); actual ~17s
|
|
42
|
-
20: (409, 24), # capped at 409 (API max for 720p); actual ~17s
|
|
43
|
-
}
|
|
44
|
-
|
|
45
37
|
_UPLOAD_RETRY_BASE_DELAY_SECONDS = 30 # 上传退避基数:忙轨取固定值,故障轨取 base×(n+1)
|
|
46
38
|
_UPLOAD_RETRY_MAX_ATTEMPTS = 10 # v7.2 上传忙轨次数封顶(等效旧的 300s 预算 ÷ 30s)
|
|
47
39
|
|
|
@@ -309,7 +301,7 @@ class AgnesVideoAPI:
|
|
|
309
301
|
def __init__(
|
|
310
302
|
self,
|
|
311
303
|
api_key: str,
|
|
312
|
-
model: str = "agnes-video-
|
|
304
|
+
model: str = "agnes-video-2.5-flash",
|
|
313
305
|
default_duration: int = 5,
|
|
314
306
|
max_retries: int = 5,
|
|
315
307
|
retry_base_delay: float = 30.0,
|
|
@@ -493,45 +485,6 @@ class AgnesVideoAPI:
|
|
|
493
485
|
attempt += 1
|
|
494
486
|
return None
|
|
495
487
|
|
|
496
|
-
# API frame limits by resolution tier (from Agnes API error messages)
|
|
497
|
-
_FRAME_LIMITS = {
|
|
498
|
-
"1080p": 169,
|
|
499
|
-
"720p": 409,
|
|
500
|
-
"480p": 961,
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
@staticmethod
|
|
504
|
-
def _get_max_frames(width: int, height: int) -> int:
|
|
505
|
-
"""Get the maximum allowed num_frames for the given resolution."""
|
|
506
|
-
pixels = width * height
|
|
507
|
-
if pixels > 1280 * 720:
|
|
508
|
-
return 169 # 1080p tier
|
|
509
|
-
elif pixels > 854 * 480:
|
|
510
|
-
return 409 # 720p tier
|
|
511
|
-
else:
|
|
512
|
-
return 961 # 480p tier
|
|
513
|
-
|
|
514
|
-
def _get_frame_config(self, duration: Optional[int] = None,
|
|
515
|
-
width: int = 1280, height: int = 720) -> tuple:
|
|
516
|
-
d = duration or self.default_duration
|
|
517
|
-
max_nf = self._get_max_frames(width, height)
|
|
518
|
-
if d in DURATION_PRESETS:
|
|
519
|
-
nf, fr = DURATION_PRESETS[d]
|
|
520
|
-
if nf <= max_nf:
|
|
521
|
-
return nf, fr
|
|
522
|
-
# preset exceeds limit for this resolution, cap it
|
|
523
|
-
logger.warning(
|
|
524
|
-
f"[AgnesVideo] Duration preset {d}s has {nf} frames, "
|
|
525
|
-
f"exceeds {max_nf} for {width}x{height}. Capping."
|
|
526
|
-
)
|
|
527
|
-
return max_nf, fr
|
|
528
|
-
best = None
|
|
529
|
-
for nf in range(9, min(410, max_nf + 1), 8):
|
|
530
|
-
fr = round(nf / d)
|
|
531
|
-
if 1 <= fr <= 60:
|
|
532
|
-
best = (nf, fr)
|
|
533
|
-
return best or DURATION_PRESETS[5]
|
|
534
|
-
|
|
535
488
|
async def _poll_task(self, video_id: str, interval: int = 60,
|
|
536
489
|
max_poll_duration: int = 1800,
|
|
537
490
|
max_consecutive_failures: int = 10,
|
|
@@ -969,60 +922,19 @@ class AgnesVideoAPI:
|
|
|
969
922
|
progress_callback=None,
|
|
970
923
|
**kwargs,
|
|
971
924
|
) -> str:
|
|
972
|
-
# 2.5
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
payload: dict = {
|
|
987
|
-
"model": self.model,
|
|
988
|
-
"prompt": prompt,
|
|
989
|
-
"width": width,
|
|
990
|
-
"height": height,
|
|
991
|
-
"num_frames": num_frames,
|
|
992
|
-
"frame_rate": frame_rate,
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
if seed is not None:
|
|
996
|
-
payload["seed"] = seed
|
|
997
|
-
if negative_prompt:
|
|
998
|
-
payload["negative_prompt"] = negative_prompt
|
|
999
|
-
|
|
1000
|
-
resolved_refs = []
|
|
1001
|
-
for p in reference_image_paths:
|
|
1002
|
-
# 优化 2:入参处先归一化参考图(尺寸统一 + 体积压缩),再 resolve。
|
|
1003
|
-
# URL/data: 透传、失败回退原图,安全无回归。
|
|
1004
|
-
norm = await asyncio.to_thread(normalize_reference_path, p, width, height)
|
|
1005
|
-
resolved_refs.append(await self._resolve_image_ref(norm))
|
|
1006
|
-
n_refs = len(resolved_refs)
|
|
1007
|
-
|
|
1008
|
-
if n_refs == 0:
|
|
1009
|
-
mode_desc = "text-to-video"
|
|
1010
|
-
elif n_refs == 1:
|
|
1011
|
-
payload["image"] = resolved_refs[0]
|
|
1012
|
-
payload["mode"] = "ti2vid"
|
|
1013
|
-
mode_desc = "image-to-video"
|
|
1014
|
-
else:
|
|
1015
|
-
payload["extra_body"] = {
|
|
1016
|
-
"image": resolved_refs,
|
|
1017
|
-
"mode": "keyframes",
|
|
1018
|
-
}
|
|
1019
|
-
mode_desc = f"keyframes ({n_refs} frames)"
|
|
1020
|
-
|
|
1021
|
-
logger.info(f"[AgnesVideo] {mode_desc}: {prompt[:80]}...")
|
|
1022
|
-
|
|
1023
|
-
video_id = await self._submit_with_retry(payload, mode_desc, progress_callback)
|
|
1024
|
-
logger.info(f"[AgnesVideo] Video submitted: {video_id[:20]}...")
|
|
1025
|
-
return video_id
|
|
925
|
+
# 2.5 系列为标准协议(v6.2):mode / seconds / size / aspect_ratio。
|
|
926
|
+
# v2.0 渠道已于 2026-10-14 下线(docs/dev/agnes_video_upstream_behavior.md §4.0.2),
|
|
927
|
+
# 不再保留 width/height/num_frames 旧协议分支,统一走 2.5 提交路径。
|
|
928
|
+
return await self._submit_video_v25(
|
|
929
|
+
prompt=prompt,
|
|
930
|
+
reference_image_paths=reference_image_paths,
|
|
931
|
+
duration=duration,
|
|
932
|
+
width=width,
|
|
933
|
+
height=height,
|
|
934
|
+
seed=seed,
|
|
935
|
+
progress_callback=progress_callback,
|
|
936
|
+
**kwargs,
|
|
937
|
+
)
|
|
1026
938
|
|
|
1027
939
|
@staticmethod
|
|
1028
940
|
def _width_height_to_aspect_ratio(width: int, height: int) -> str:
|
package/core/api/key_manager.py
CHANGED
|
@@ -122,9 +122,12 @@ class KeyRing:
|
|
|
122
122
|
if key in self._keys else "key#(不在池内)"
|
|
123
123
|
)
|
|
124
124
|
# 只打日志,不触发任何池调整;用户据配置页提示自行决定是否删除该 Key
|
|
125
|
+
# 日志不拼接 domain:它由 get_api_key_domains() 返回、数据流源自 API Key
|
|
126
|
+
# 配置,CodeQL py/clear-text-logging-sensitive-data 会判为明文泄露敏感数据。
|
|
127
|
+
# 域名归因已存入登记表并由配置页下发,日志无需重复(同 config_routes.py 先例)。
|
|
125
128
|
logger.warning(
|
|
126
|
-
f"[KeyAuth] {label} 认证失败 (HTTP {status}"
|
|
127
|
-
f"{
|
|
129
|
+
f"[KeyAuth] {label} 认证失败 (HTTP {status}): "
|
|
130
|
+
f"{message or 'invalid token'} "
|
|
128
131
|
f"— 已登记第 {snapshot['count']} 次,请在配置页确认是否移除该 Key"
|
|
129
132
|
)
|
|
130
133
|
return snapshot
|
package/core/config.py
CHANGED
|
@@ -12,13 +12,27 @@ from models.task import AudioConfig, SubtitleConfig, SubtitleStyle
|
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
def _resolve_config_dir() -> str:
|
|
17
|
+
"""解析配置目录(可被 ``AGNES_CONFIG_DIR`` 覆盖)。
|
|
18
|
+
|
|
19
|
+
默认:项目根目录下 ``.agnes_config``。npx 部署时该目录位于 npm 缓存内
|
|
20
|
+
(`npm cache clean` 或缓存目录变化即丢配置、回退默认域名),可用
|
|
21
|
+
``AGNES_CONFIG_DIR`` 指向稳定的用户级目录(如 ``~/.agnes_config``)。
|
|
22
|
+
"""
|
|
23
|
+
override = os.environ.get("AGNES_CONFIG_DIR", "").strip()
|
|
24
|
+
if override:
|
|
25
|
+
return os.path.abspath(os.path.expanduser(override))
|
|
26
|
+
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".agnes_config")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
CONFIG_DIR = _resolve_config_dir()
|
|
16
30
|
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
|
17
31
|
|
|
18
32
|
# ═══════════════════════════════════════════════════
|
|
19
33
|
# 应用版本号(v6.1 新增:发版时同步更新,见 docs/dev/release_process.md)
|
|
20
34
|
# ═══════════════════════════════════════════════════
|
|
21
|
-
APP_VERSION = "7.1.
|
|
35
|
+
APP_VERSION = "7.1.1"
|
|
22
36
|
|
|
23
37
|
# 未配置 API Key 时的统一报错文案(含免费获取与在线体验兜底,全站路由共用)
|
|
24
38
|
#
|
|
@@ -942,7 +956,7 @@ DURATION_FRAME_MAP = {
|
|
|
942
956
|
# 各类型 Agnes 模型默认值(与三个 API 客户端的默认 model 对齐)
|
|
943
957
|
DEFAULT_TEXT_MODEL = "agnes-3.0-flash"
|
|
944
958
|
DEFAULT_IMAGE_MODEL = "agnes-image-2.5-flash"
|
|
945
|
-
DEFAULT_VIDEO_MODEL = "agnes-video-
|
|
959
|
+
DEFAULT_VIDEO_MODEL = "agnes-video-2.5-flash"
|
|
946
960
|
|
|
947
961
|
DEFAULT_MODELS = {
|
|
948
962
|
"text": DEFAULT_TEXT_MODEL,
|
|
@@ -1165,32 +1179,6 @@ VIDEO_25_DURATIONS = [4, 5, 6, 8, 10, 12]
|
|
|
1165
1179
|
|
|
1166
1180
|
# 视频模型能力元数据:供前端「选模型阶段」展示差异 + 按模型归拢表单选项
|
|
1167
1181
|
VIDEO_MODEL_CAPABILITIES = {
|
|
1168
|
-
DEFAULT_VIDEO_MODEL: { # agnes-video-v2.0(免费旧版)
|
|
1169
|
-
"label": "Video 2.0",
|
|
1170
|
-
"price": "free",
|
|
1171
|
-
"price_text": {"zh": "免费", "en": "Free"},
|
|
1172
|
-
"modes": [
|
|
1173
|
-
{"id": "t2v", "label": {"zh": "文生视频", "en": "Text to video"}},
|
|
1174
|
-
{"id": "i2v", "label": {"zh": "图生视频(1 张参考图)", "en": "Image to video (1 ref)"}},
|
|
1175
|
-
{"id": "keyframes", "label": {"zh": "关键帧动画(首尾帧)", "en": "Keyframes (first+last)"}},
|
|
1176
|
-
],
|
|
1177
|
-
"durations": [5, 10, 15, 18, 20],
|
|
1178
|
-
"resolution": {
|
|
1179
|
-
"type": "pixels",
|
|
1180
|
-
"options": [
|
|
1181
|
-
{"value": "768x1152", "label": {"zh": "竖屏 768x1152", "en": "Portrait 768x1152"}},
|
|
1182
|
-
{"value": "1280x720", "label": {"zh": "横屏 1280x720", "en": "Landscape 1280x720"}},
|
|
1183
|
-
{"value": "1024x1024", "label": {"zh": "方形 1024x1024", "en": "Square 1024x1024"}},
|
|
1184
|
-
],
|
|
1185
|
-
},
|
|
1186
|
-
"supports_negative": True,
|
|
1187
|
-
"max_ref_images": None, # 不限
|
|
1188
|
-
"supports_ref_video": False,
|
|
1189
|
-
"desc": {
|
|
1190
|
-
"zh": "免费旧版。支持任意像素分辨率、最长约 17 秒、多参考图关键帧;有负面提示词。输出分辨率由上游按标准档归一(标准档保真,非标准档会被吸附)。",
|
|
1191
|
-
"en": "Free legacy model. Arbitrary pixel resolution, up to ~17s, multi-image keyframes; supports negative prompt. Output resolution is normalized by upstream to standard tiers.",
|
|
1192
|
-
},
|
|
1193
|
-
},
|
|
1194
1182
|
"agnes-video-2.5": { # 付费模型
|
|
1195
1183
|
"label": "Video 2.5",
|
|
1196
1184
|
"price": "paid",
|
package/core/i18n_backend.py
CHANGED
|
@@ -822,6 +822,10 @@ CATALOG: Dict[str, Dict[str, str]] = {
|
|
|
822
822
|
"zh": "reference_images_map 必须为 JSON 数组,元素为段落 index 的整数数组",
|
|
823
823
|
"en": "reference_images_map must be a JSON array whose elements are integer arrays of paragraph indices",
|
|
824
824
|
},
|
|
825
|
+
"validation.end_frame_indices_not_list": {
|
|
826
|
+
"zh": "end_frame_indices_json 必须为 JSON 数组,元素为场景 index 的整数",
|
|
827
|
+
"en": "end_frame_indices_json must be a JSON array of integer scene indices",
|
|
828
|
+
},
|
|
825
829
|
"validation.poem_empty": {
|
|
826
830
|
"zh": "古诗原文不能为空",
|
|
827
831
|
"en": "Poem text must not be empty",
|
|
@@ -1021,6 +1025,21 @@ CATALOG: Dict[str, Dict[str, str]] = {
|
|
|
1021
1025
|
"en": "Voice {voice} does not support video generation in {lang_name} (supported: {supported} only). Please change the voice or the language.",
|
|
1022
1026
|
},
|
|
1023
1027
|
|
|
1028
|
+
# ── 上传图片校验(issue #88):非白名单后缀拒绝 / 字节不可解码 ──
|
|
1029
|
+
# 上传时即校验,避免坏文件拖到流水线深处(end_frame_gen)才抛 PIL 原始栈打死整任务。
|
|
1030
|
+
"error.upload_ext_invalid": {
|
|
1031
|
+
"zh": "不支持的图片格式 {ext}(仅支持:{options})。请更换为 PNG/JPG 等常见图片格式后重试。",
|
|
1032
|
+
"en": "Unsupported image format {ext} (allowed: {options}). Please upload a common image format such as PNG or JPG, then retry.",
|
|
1033
|
+
},
|
|
1034
|
+
"error.image_unreadable": {
|
|
1035
|
+
"zh": "上传的图片无法解码,可能不是有效的图片文件(或已被截断/损坏)。请更换为可正常打开的 PNG/JPG 图片后重试。",
|
|
1036
|
+
"en": "The uploaded image could not be decoded — it may not be a valid image file (or it is truncated/corrupted). Please upload a PNG/JPG image that opens normally, then retry.",
|
|
1037
|
+
},
|
|
1038
|
+
"error.image_unreadable_scene": {
|
|
1039
|
+
"zh": "场景 {scene_index} 上传的图片无法解码,可能不是有效的图片文件(或已被截断/损坏)。请更换为该场景的可正常打开的 PNG/JPG 图片后重试。",
|
|
1040
|
+
"en": "The image uploaded for scene {scene_index} could not be decoded — it may not be a valid image file (or it is truncated/corrupted). Please replace it with a PNG/JPG image that opens normally, then retry.",
|
|
1041
|
+
},
|
|
1042
|
+
|
|
1024
1043
|
}
|
|
1025
1044
|
|
|
1026
1045
|
|
|
@@ -78,7 +78,7 @@ class AnchorPipeline(MultiScenePipeline):
|
|
|
78
78
|
dir_name: Optional[str] = None,
|
|
79
79
|
chat_model: str = DEFAULT_TEXT_MODEL,
|
|
80
80
|
image_model: str = "agnes-image-2.5-flash",
|
|
81
|
-
video_model: str = "agnes-video-
|
|
81
|
+
video_model: str = "agnes-video-2.5-flash",
|
|
82
82
|
progress_callback: Optional[Callable] = None,
|
|
83
83
|
shutdown_event: Optional[asyncio.Event] = None,
|
|
84
84
|
):
|
|
@@ -50,7 +50,7 @@ class CreativeVideoPipeline(
|
|
|
50
50
|
dir_name: Optional[str] = None,
|
|
51
51
|
chat_model: str = DEFAULT_TEXT_MODEL,
|
|
52
52
|
image_model: str = "agnes-image-2.5-flash",
|
|
53
|
-
video_model: str = "agnes-video-
|
|
53
|
+
video_model: str = "agnes-video-2.5-flash",
|
|
54
54
|
progress_callback: Optional[Callable] = None,
|
|
55
55
|
shutdown_event: Optional[asyncio.Event] = None,
|
|
56
56
|
):
|
|
@@ -218,11 +218,22 @@ class FramesStepsMixin:
|
|
|
218
218
|
)
|
|
219
219
|
if os.path.exists(user_ef):
|
|
220
220
|
dest = os.path.join(scene_dir, "end_frame.png")
|
|
221
|
-
# 优化 2:用户上传尾帧统一走归一化模块(PAD/PNG/黑边,与原 ffmpeg
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
# 优化 2:用户上传尾帧统一走归一化模块(PAD/PNG/黑边,与原 ffmpeg 语义一致)。
|
|
222
|
+
# issue #88:坏文件(不可解码)应在上传时即被拒;此处再兜底一层,
|
|
223
|
+
# 把 PIL 原始栈(UnidentifiedImageError/OSError)换成含场景序号的可读
|
|
224
|
+
# 本地化报错,避免单个坏文件锁死整任务、且「重试」反复读同一坏文件。
|
|
225
|
+
try:
|
|
226
|
+
end_frame_path = await normalize_image_async(
|
|
227
|
+
src=user_ef, width=vw, height=vh, dst=dest,
|
|
228
|
+
strategy=PAD, fmt="PNG", background=(0, 0, 0),
|
|
229
|
+
)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.error(
|
|
232
|
+
f"[EndFrame] Scene {scene_idx} user end frame not decodable: {e}"
|
|
233
|
+
)
|
|
234
|
+
raise RuntimeError(
|
|
235
|
+
self._t("error.image_unreadable_scene", scene_index=scene_idx + 1)
|
|
236
|
+
) from e
|
|
226
237
|
pregenerated[scene_idx] = end_frame_path
|
|
227
238
|
cached[str(scene_idx)] = end_frame_path
|
|
228
239
|
prev_end_frame = end_frame_path # 维护视觉链
|
|
@@ -147,7 +147,7 @@ class ManuscriptVideoPipeline(MultiScenePipeline):
|
|
|
147
147
|
dir_name: str = None,
|
|
148
148
|
chat_model: str = DEFAULT_TEXT_MODEL,
|
|
149
149
|
image_model: str = "agnes-image-2.5-flash",
|
|
150
|
-
video_model: str = "agnes-video-
|
|
150
|
+
video_model: str = "agnes-video-2.5-flash",
|
|
151
151
|
progress_callback: Optional[Callable] = None,
|
|
152
152
|
shutdown_event: Optional[asyncio.Event] = None,
|
|
153
153
|
):
|
|
@@ -76,7 +76,7 @@ class PoetryVideoPipeline(MultiScenePipeline):
|
|
|
76
76
|
task_id: str,
|
|
77
77
|
dir_name: Optional[str] = None,
|
|
78
78
|
chat_model: str = DEFAULT_TEXT_MODEL,
|
|
79
|
-
video_model: str = "agnes-video-
|
|
79
|
+
video_model: str = "agnes-video-2.5-flash",
|
|
80
80
|
progress_callback: Optional[callable] = None,
|
|
81
81
|
shutdown_event: Optional = None,
|
|
82
82
|
):
|
|
@@ -45,7 +45,7 @@ class SimpleVideoPipeline(BasePipeline):
|
|
|
45
45
|
dir_name: str = None,
|
|
46
46
|
chat_model: str = DEFAULT_TEXT_MODEL,
|
|
47
47
|
image_model: str = "agnes-image-2.5-flash",
|
|
48
|
-
video_model: str = "agnes-video-
|
|
48
|
+
video_model: str = "agnes-video-2.5-flash",
|
|
49
49
|
progress_callback: Optional[Callable] = None,
|
|
50
50
|
shutdown_event: Optional[asyncio.Event] = None,
|
|
51
51
|
):
|
|
@@ -264,7 +264,8 @@ Output ONLY the visual prompt text, no JSON, no explanation.
|
|
|
264
264
|
dur_hint = f"各场景时长:{dur_list}(合计 {total} 秒)"
|
|
265
265
|
else:
|
|
266
266
|
dur_hint = f"目标总时长:{total_duration} 秒(用于把握节奏,不要求每句等长)"
|
|
267
|
-
|
|
267
|
+
# issue #87:风格缺省时不再回填硬编码中文,改用语言中性的英文默认值
|
|
268
|
+
style_hint = style.strip() if style and style.strip() else "cinematic realistic style"
|
|
268
269
|
system_prompt = self._prompt(
|
|
269
270
|
zh_text="""你是一位诗意的视觉艺术家兼诗词分镜导演,专精将中国古典诗词转化为视频分镜。
|
|
270
271
|
请将整首诗拆分为若干场景,每个场景一行,严格使用如下格式:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-short-video",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.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",
|