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.
Files changed (56) hide show
  1. package/README.md +44 -18
  2. package/core/api/agnes_chat.py +28 -14
  3. package/core/api/agnes_video.py +14 -102
  4. package/core/api/key_manager.py +5 -2
  5. package/core/config.py +17 -29
  6. package/core/i18n_backend.py +19 -0
  7. package/core/pipelines/anchor_video.py +1 -1
  8. package/core/pipelines/creative/pipeline.py +1 -1
  9. package/core/pipelines/creative/steps_frames.py +16 -5
  10. package/core/pipelines/manuscript_video.py +1 -1
  11. package/core/pipelines/poetry_video.py +1 -1
  12. package/core/pipelines/simple_video.py +1 -1
  13. package/core/screenwriter/scenes.py +2 -1
  14. package/package.json +1 -1
  15. package/static/assets/ar-uqdQsD8d.js +1 -0
  16. package/static/assets/bn-Cs0c8ay0.js +1 -0
  17. package/static/assets/de-C03R7qVE.js +1 -0
  18. package/static/assets/es-LJ2OkYIx.js +1 -0
  19. package/static/assets/{fa-BzLk3cXs.js → fa-DySEd8zE.js} +1 -1
  20. package/static/assets/fr-BTafm0bE.js +1 -0
  21. package/static/assets/hi-Bk8MY-fF.js +1 -0
  22. package/static/assets/id-DZGFWLC-.js +1 -0
  23. package/static/assets/{index-BYtvF5AK.js → index-C1tAWGVu.js} +17 -17
  24. package/static/assets/it-DEnLooJL.js +1 -0
  25. package/static/assets/ja-D7VxE6bk.js +1 -0
  26. package/static/assets/ko-nc2euVdS.js +1 -0
  27. package/static/assets/ms-Jz3t8mz-.js +1 -0
  28. package/static/assets/nl-D4aafNI8.js +1 -0
  29. package/static/assets/pt-CBY3pDPr.js +1 -0
  30. package/static/assets/ru-B3WZA3F-.js +1 -0
  31. package/static/assets/th-BSUvfnqU.js +1 -0
  32. package/static/assets/tl-DSeqPP4N.js +1 -0
  33. package/static/assets/tr-jcUJ-jsZ.js +1 -0
  34. package/static/assets/ur-cYxmbnCI.js +1 -0
  35. package/static/assets/vi-Dh-Jp2tU.js +1 -0
  36. package/static/index.html +1 -1
  37. package/web/routes/task_creation_routes.py +80 -18
  38. package/static/assets/ar-C1KWUlNh.js +0 -1
  39. package/static/assets/bn-C4SfxNGN.js +0 -1
  40. package/static/assets/de-B4OvLzBp.js +0 -1
  41. package/static/assets/es-IQACehBJ.js +0 -1
  42. package/static/assets/fr-BMhNx-iQ.js +0 -1
  43. package/static/assets/hi-B7Vaoj8j.js +0 -1
  44. package/static/assets/id-B5vxBOhd.js +0 -1
  45. package/static/assets/it-DwDZ9skF.js +0 -1
  46. package/static/assets/ja-DyBi5ySN.js +0 -1
  47. package/static/assets/ko-DtnJDAHo.js +0 -1
  48. package/static/assets/ms-CCjt7pyM.js +0 -1
  49. package/static/assets/nl-Bqpu5-or.js +0 -1
  50. package/static/assets/pt-DzUv3ga-.js +0 -1
  51. package/static/assets/ru-CMGJcpUZ.js +0 -1
  52. package/static/assets/th-DnlP-4Ac.js +0 -1
  53. package/static/assets/tl-BzNuvuWV.js +0 -1
  54. package/static/assets/tr-Ca1gT8-V.js +0 -1
  55. package/static/assets/ur-DMIuAGiE.js +0 -1
  56. 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.0
3
+ # What's New in v7.1.1
4
4
 
5
5
  ## What's New
6
6
 
7
7
  ### Features & Improvements
8
8
 
9
- - **Off-peak retry guidance for AI video generation.** When the upstream video queue is full (HTTP 503 / queue-full) the failure panel and the create-task toast now suggest the time windows in which upstream is historically smoother, based on observed success rates rather than guesswork. The windows are stored in UTC and rendered in each viewer's own local time with a timezone label, so users in different regions see their own best hours; the wording is localized in all 22 interface languages.
10
- - **API key panel shows upstream authentication failures.** Keys that hit an upstream `401 Invalid token` are now badged **Auth failed ×N** in the key list, with a tooltip showing the failure count, the last failure time and the upstream message, plus a matching log line. Registration is observation only: no key is removed, reweighted or reordered automatically — whether to delete a key stays entirely your decision in the configuration panel. Keys are only ever shown masked.
11
-
12
- ### Refactoring & Optimizations
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
- - **Queue-full 503s are no longer mistaken for hard failures.** Bare 503 responses carrying no error code are now classified as "server busy" and retried on the busy track, so video tasks ride out queue saturation instead of giving up early.
23
- - **Image upload can no longer spin on an unusable response.** An upload response that contains no usable hosting URL no longer loops indefinitely and falls back cleanly.
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
- ## 🚀 Two Ways to Use — Both Completely Free
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).
@@ -191,20 +191,34 @@ class AgnesChatAPI:
191
191
  from core.config import get_settings
192
192
 
193
193
  timeout_retry_limit = 1
194
- data = self._request_with_retry(
195
- {
196
- "model": self.model,
197
- "messages": [
198
- {"role": "system", "content": system_prompt},
199
- {"role": "user", "content": user_prompt},
200
- ],
201
- "temperature": 0.7,
202
- "max_tokens": max_tokens,
203
- },
204
- timeout=get_settings().agnes_chat_timeout,
205
- timeout_retry_limit=timeout_retry_limit,
206
- )
207
- return data["choices"][0]["message"]["content"]
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 响应(健壮版)。
@@ -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-v2.0",
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 系列模型(v6.2):新参数协议(mode/seconds/size/aspect_ratio)
973
- if is_v25_video_model(self.model):
974
- return await self._submit_video_v25(
975
- prompt=prompt,
976
- reference_image_paths=reference_image_paths,
977
- duration=duration,
978
- width=width,
979
- height=height,
980
- seed=seed,
981
- progress_callback=progress_callback,
982
- **kwargs,
983
- )
984
- num_frames, frame_rate = self._get_frame_config(duration, width, height)
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:
@@ -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"{f' · {domain}' if domain else ''}): {message or 'invalid token'} "
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
- CONFIG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".agnes_config")
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.0"
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-v2.0"
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",
@@ -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-v2.0",
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-v2.0",
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
- end_frame_path = await normalize_image_async(
223
- src=user_ef, width=vw, height=vh, dst=dest,
224
- strategy=PAD, fmt="PNG", background=(0, 0, 0),
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-v2.0",
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-v2.0",
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-v2.0",
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
- style_hint = style.strip() if style and style.strip() else "未指定,请采用通用电影质感写实风格"
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.0",
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",