free-short-video 6.1.0 → 6.2.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 CHANGED
@@ -1,40 +1,12 @@
1
1
  ---
2
2
 
3
- # What's New in v6.1.0
3
+ # What's New in v6.2.1
4
4
 
5
5
  ## What's New
6
6
 
7
- ### Retry-First Guidance on Task Failure
7
+ ### Bug Fixes
8
8
 
9
- The failed-task panel now gives you a clear action path instead of a dead-end error line:
10
-
11
- - **Retry Task button** — resumes the task from the failed step via the existing checkpoint resume (no new backend endpoint, no full resubmit).
12
- - **Retry count tracking** — retries are counted per task and persist across refreshes; the count resets when the task succeeds.
13
- - **Transient-failure copy** — the panel explains that most failures come from temporary rate limiting / network timeouts / model-service fluctuation and are worth retrying before reporting.
14
-
15
- ### One-Click Issue Feedback with Progressive Expansion
16
-
17
- When retries don't help, the feedback area helps you report efficiently:
18
-
19
- - **Progressive expansion** — the feedback area stays a low-key hint line under the retry guidance; after **2 failed retries** it auto-expands, switching the copy to "still failing after N retries, please report with diagnostic info." You can open it manually at any time (your choice is remembered per task).
20
- - **Deterministic-failure pre-screen** — parameter/content errors (HTTP 400, content policy, invalid API key) are detected from the error text, skip the retry guidance, and expand the feedback area directly with a "retrying may not help" hint and a de-emphasized retry button.
21
- - **Diagnostic info preview** — a collapsible structured report: app version, task type, mode, failed step, error message, retry count, and key generation configs (no prompt text included).
22
- - **Copy diagnostic info** — one click copies the full report for pasting anywhere.
23
- - **Check the FAQ first** — a direct link to the official FAQ page for self-service troubleshooting.
24
- - **Open a GitHub Issue** — jumps to the project issue tracker with title and body pre-filled from the diagnostic report (long bodies are safely truncated); a bug-report issue template is available in the repo.
25
-
26
- ### Diagnostics Endpoint (Phase 2)
27
-
28
- - **`GET /api/tasks/{id}/diagnostics`** — returns a task summary plus the model-call error details associated with that task (exact task match first, time-window fallback), sourced from the existing `error_logs/` collector.
29
- - **Task-linked error collection** — model errors are now written with their `task_id`, so a failure can be traced back to the exact API calls that caused it.
30
- - **Privacy by design** — the endpoint never returns prompt text, response bodies, or system prompts; error messages are truncated. The feedback report is fully client-side and **nothing is uploaded automatically**.
31
-
32
- ### Other Features & Improvements
33
-
34
- - **App version in diagnostics** — `APP_VERSION` is now exposed via `GET /api/config`, so every report carries the exact version that failed (kept in sync by the release process).
35
- - **Official FAQ updated** — new entries explain what to do when generation fails (retry first, then report with diagnostic info) and how to get help / report issues; synced across the official site and wiki.
36
-
37
- ---
9
+ - **Localized feedback & diagnostic report** — the diagnostic report header, field names and the generated issue (GitHub) template are no longer hard-coded in Chinese. They now respect your UI language (22 languages), so fields, titles and truncation hints in the feedback panel render correctly for non-Chinese users.
38
10
 
39
11
  ---
40
12
 
@@ -14,7 +14,12 @@ import requests
14
14
  from core.api.error_collector import collect_error, collect_error_from_exception
15
15
  from core.api.key_manager import get_key_ring
16
16
  from core.api.rate_limiter import get_rate_limiter, get_video_submit_limiter
17
- from core.config import get_agnes_base_url, get_agnes_api_root
17
+ from core.config import (
18
+ VIDEO_ASPECT_RATIOS,
19
+ get_agnes_api_root,
20
+ get_agnes_base_url,
21
+ is_v25_video_model,
22
+ )
18
23
  from utils.image_normalizer import normalize_reference_path
19
24
  from utils.video import download_video
20
25
 
@@ -236,9 +241,11 @@ class AgnesVideoAPI:
236
241
  poll_count = 0
237
242
  consecutive_failures = 0
238
243
  start_time = asyncio.get_event_loop().time()
244
+ # 2.5 系列查询需带 model_name(text 模式可省略,但带上更通用)
245
+ model_param = f"&model_name={self.model}" if is_v25_video_model(self.model) else ""
239
246
  curl_cmd = (
240
247
  f'curl -s -H "Authorization: Bearer $AGNES_API_KEY" '
241
- f'"{get_agnes_api_root()}/agnesapi?video_id={video_id}"'
248
+ f'"{get_agnes_api_root()}/agnesapi?video_id={video_id}{model_param}"'
242
249
  )
243
250
  while True:
244
251
  # M2: 每次轮询前检查停止信号
@@ -271,7 +278,7 @@ class AgnesVideoAPI:
271
278
  resp = await asyncio.wait_for(
272
279
  asyncio.to_thread(
273
280
  requests.get,
274
- f"{get_agnes_api_root()}/agnesapi?video_id={video_id}",
281
+ f"{get_agnes_api_root()}/agnesapi?video_id={video_id}{model_param}",
275
282
  headers=self._auth_headers(),
276
283
  timeout=15,
277
284
  ),
@@ -525,6 +532,17 @@ class AgnesVideoAPI:
525
532
  negative_prompt: Optional[str] = None,
526
533
  **kwargs,
527
534
  ) -> str:
535
+ # 2.5 系列模型(v6.2):新参数协议(mode/seconds/size/aspect_ratio)
536
+ if is_v25_video_model(self.model):
537
+ return await self._submit_video_v25(
538
+ prompt=prompt,
539
+ reference_image_paths=reference_image_paths,
540
+ duration=duration,
541
+ width=width,
542
+ height=height,
543
+ seed=seed,
544
+ **kwargs,
545
+ )
528
546
  num_frames, frame_rate = self._get_frame_config(duration, width, height)
529
547
 
530
548
  payload: dict = {
@@ -568,6 +586,90 @@ class AgnesVideoAPI:
568
586
  logger.info(f"[AgnesVideo] Video submitted: {video_id[:20]}...")
569
587
  return video_id
570
588
 
589
+ @staticmethod
590
+ def _width_height_to_aspect_ratio(width: int, height: int) -> str:
591
+ """将像素宽高映射到最接近的 2.5 系列 aspect_ratio 枚举。
592
+
593
+ 找不到精确比例时取误差最小的档位(默认 16:9)。
594
+ """
595
+ if not width or not height:
596
+ return "16:9"
597
+ ratio = width / height
598
+ best = "16:9"
599
+ best_dist = float("inf")
600
+ for ar in VIDEO_ASPECT_RATIOS:
601
+ w, h = ar.split(":")
602
+ target = int(w) / int(h)
603
+ dist = abs(ratio - target)
604
+ if dist < best_dist:
605
+ best_dist = dist
606
+ best = ar
607
+ return best
608
+
609
+ async def _submit_video_v25(
610
+ self,
611
+ prompt: str,
612
+ reference_image_paths: List[str],
613
+ duration: Optional[int] = None,
614
+ width: int = 1152,
615
+ height: int = 768,
616
+ seed: Optional[int] = None,
617
+ **kwargs,
618
+ ) -> str:
619
+ """2.5 / 2.5-flash 新协议提交:mode / seconds / size / aspect_ratio。
620
+
621
+ 与 v2.0 的关键差异:
622
+ - size 固定 720P(flash)或 720P/960P/2K(2.5);不传 width/height/num_frames
623
+ - seconds 为字符串 "4"–"12"
624
+ - 单参考图 → reference(images);多图 → keyframe(first_frame + last_frame)
625
+ - 不支持 negative_prompt(忽略)
626
+ """
627
+ # 时长:duration(秒) → seconds 字符串(4–12,超界截断)
628
+ secs = int(duration) if duration else 5
629
+ seconds = str(max(4, min(secs, 12)))
630
+ # 分辨率:flash 固定 720P;2.5 用 video_size 参数(缺省 720P)
631
+ size = kwargs.get("video_size") or "720P"
632
+ if self.model == "agnes-video-2.5-flash":
633
+ size = "720P"
634
+ aspect_ratio = self._width_height_to_aspect_ratio(width, height)
635
+
636
+ payload: dict = {
637
+ "model": self.model,
638
+ "prompt": prompt,
639
+ "mode": "text",
640
+ "seconds": seconds,
641
+ "size": size,
642
+ "aspect_ratio": aspect_ratio,
643
+ }
644
+ if seed is not None:
645
+ payload["seed"] = seed
646
+
647
+ # 参考图解析(与 v2.0 一致:归一化 + 上传为公开 URL)
648
+ resolved_refs = []
649
+ for p in reference_image_paths:
650
+ norm = await asyncio.to_thread(normalize_reference_path, p, width, height)
651
+ resolved_refs.append(await self._resolve_image_ref(norm))
652
+ n_refs = len(resolved_refs)
653
+
654
+ if n_refs == 1:
655
+ payload["mode"] = "reference"
656
+ payload["images"] = resolved_refs
657
+ mode_desc = "reference (1 image)"
658
+ elif n_refs >= 2:
659
+ # v6.2.1: Agnes 2.5 系列 keyframe 模式固定输出 704x704 正方形
660
+ # (忽略 aspect_ratio,竖屏/横屏画面会被拉伸,人物显宽)。
661
+ # 降级为 reference 模式(多图参考,≤5 张),输出遵循 aspect_ratio。
662
+ payload["mode"] = "reference"
663
+ payload["images"] = resolved_refs[:5]
664
+ mode_desc = f"reference ({n_refs} images, keyframe fallback)"
665
+ else:
666
+ mode_desc = "text-to-video"
667
+
668
+ logger.info(f"[AgnesVideo] {mode_desc}: {prompt[:80]}...")
669
+ video_id = await self._submit_with_retry(payload, mode_desc)
670
+ logger.info(f"[AgnesVideo] Video submitted: {video_id[:20]}...")
671
+ return video_id
672
+
571
673
  async def wait_for_video(self, video_id: str, progress_callback=None) -> VideoOutput:
572
674
  final = await self._poll_task(video_id, progress_callback=progress_callback)
573
675
 
package/core/artifacts.py CHANGED
@@ -1082,7 +1082,7 @@ def write_manifest_md(state: BaseTaskState, task_dir: str) -> str:
1082
1082
  lines += [
1083
1083
  "## 协作提示",
1084
1084
  "",
1085
- "- 文本 / JSON / SRT 产物可直接用编辑器或外部 AI Agent(如 opencode、workbuddy)修改。",
1085
+ "- 文本 / JSON / SRT 产物可直接用编辑器或外部 AI Agent(如 TraeWork、opencode、workbuddy)修改。",
1086
1086
  "- 图片 / 视频产物可用 ffmpeg、Python 等外部工具处理后覆盖同名文件。",
1087
1087
  "- 产物字段说明见 `docs/dev/artifact_standard.md`。",
1088
1088
  "",
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.1.0"
21
+ APP_VERSION = "6.2.1"
22
22
 
23
23
  # 未配置 API Key 时的统一报错文案(含免费获取与在线体验兜底,全站路由共用)
24
24
  API_KEY_MISSING_MSG = (
@@ -739,6 +739,110 @@ def set_selected_models(text: str = None, image: str = None, video: str = None)
739
739
  return get_selected_models()
740
740
 
741
741
 
742
+ # ═══════════════════════════════════════════════════
743
+ # 视频模型能力元数据(v6.2:选模型阶段差异说明 + 表单选项归拢)
744
+ # ═══════════════════════════════════════════════════
745
+
746
+ # 2.5 系列模型 ID(新参数协议:mode/seconds/size/aspect_ratio,与 v2.0 不兼容)
747
+ VIDEO_MODEL_25_PREFIX = "agnes-video-2.5"
748
+
749
+
750
+ def is_v25_video_model(model: str) -> bool:
751
+ """判断是否为 2.5 系列视频模型(新参数协议)。"""
752
+ return bool(model) and model.startswith(VIDEO_MODEL_25_PREFIX)
753
+
754
+
755
+ # 支持的画幅比例(2.5 系列 aspect_ratio 枚举)
756
+ VIDEO_ASPECT_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"]
757
+
758
+ # 2.5 系列时长档位(seconds 字符串 "4"–"12")
759
+ VIDEO_25_DURATIONS = [4, 5, 6, 8, 10, 12]
760
+
761
+ # 视频模型能力元数据:供前端「选模型阶段」展示差异 + 按模型归拢表单选项
762
+ VIDEO_MODEL_CAPABILITIES = {
763
+ DEFAULT_VIDEO_MODEL: { # agnes-video-v2.0(免费旧版)
764
+ "label": "Video 2.0",
765
+ "price": "free",
766
+ "price_text": {"zh": "免费", "en": "Free"},
767
+ "modes": [
768
+ {"id": "t2v", "label": {"zh": "文生视频", "en": "Text to video"}},
769
+ {"id": "i2v", "label": {"zh": "图生视频(1 张参考图)", "en": "Image to video (1 ref)"}},
770
+ {"id": "keyframes", "label": {"zh": "关键帧动画(首尾帧)", "en": "Keyframes (first+last)"}},
771
+ ],
772
+ "durations": [5, 10, 15, 18, 20],
773
+ "resolution": {
774
+ "type": "pixels",
775
+ "options": [
776
+ {"value": "768x1152", "label": {"zh": "竖屏 768x1152", "en": "Portrait 768x1152"}},
777
+ {"value": "1152x768", "label": {"zh": "横屏 1152x768", "en": "Landscape 1152x768"}},
778
+ {"value": "1024x1024", "label": {"zh": "方形 1024x1024", "en": "Square 1024x1024"}},
779
+ ],
780
+ },
781
+ "supports_negative": True,
782
+ "max_ref_images": None, # 不限
783
+ "supports_ref_video": False,
784
+ "desc": {
785
+ "zh": "免费旧版。支持任意像素分辨率、最长约 17 秒、多参考图关键帧;有负面提示词。",
786
+ "en": "Free legacy model. Arbitrary pixel resolution, up to ~17s, multi-image keyframes; supports negative prompt.",
787
+ },
788
+ },
789
+ "agnes-video-2.5": { # 付费模型
790
+ "label": "Video 2.5",
791
+ "price": "paid",
792
+ "price_text": {
793
+ "zh": "付费:720P $0.025/s · 960P $0.04/s · 2K $0.055/s;图片前 5 张免费",
794
+ "en": "Paid: 720P $0.025/s · 960P $0.04/s · 2K $0.055/s; first 5 images free",
795
+ },
796
+ "modes": [
797
+ {"id": "text", "label": {"zh": "文生视频", "en": "Text to video"}},
798
+ {"id": "keyframe", "label": {"zh": "首尾帧(自动降级图片参考)", "en": "First/last frame (auto fallback)"}},
799
+ {"id": "reference", "label": {"zh": "图片/音频参考", "en": "Image/audio reference"}},
800
+ ],
801
+ "durations": VIDEO_25_DURATIONS,
802
+ "resolution": {
803
+ "type": "ratio_size",
804
+ "ratios": VIDEO_ASPECT_RATIOS,
805
+ "sizes": ["720P", "960P", "2K"],
806
+ },
807
+ "supports_negative": False,
808
+ "max_ref_images": 5, # 第 6 张起 $0.005/张
809
+ "supports_ref_video": True,
810
+ "desc": {
811
+ "zh": "新一代付费模型。最高 2K 分辨率、4–12 秒、支持参考视频;不支持负面提示词。",
812
+ "en": "New paid model. Up to 2K, 4–12s, supports reference videos; no negative prompt.",
813
+ },
814
+ },
815
+ "agnes-video-2.5-flash": { # 免费新版(限时免费)
816
+ "label": "Video 2.5 Flash",
817
+ "price": "free",
818
+ "price_text": {"zh": "免费(限时)", "en": "Free (limited time)"},
819
+ "modes": [
820
+ {"id": "text", "label": {"zh": "文生视频", "en": "Text to video"}},
821
+ {"id": "keyframe", "label": {"zh": "首尾帧(自动降级图片参考)", "en": "First/last frame (auto fallback)"}},
822
+ {"id": "reference", "label": {"zh": "图片参考(≤5 张)", "en": "Image reference (≤5)"}},
823
+ ],
824
+ "durations": VIDEO_25_DURATIONS,
825
+ "resolution": {
826
+ "type": "ratio_size",
827
+ "ratios": VIDEO_ASPECT_RATIOS,
828
+ "sizes": ["720P"], # 固定 720P
829
+ },
830
+ "supports_negative": False,
831
+ "max_ref_images": 5,
832
+ "supports_ref_video": False,
833
+ "desc": {
834
+ "zh": "免费新版(限时免费)。固定 720P、4–12 秒、图片参考最多 5 张;不支持负面提示词与参考视频。",
835
+ "en": "Free new model (limited time). Fixed 720P, 4–12s, up to 5 ref images; no negative prompt or ref video.",
836
+ },
837
+ },
838
+ }
839
+
840
+
841
+ def get_video_model_capabilities() -> dict:
842
+ """返回全部视频模型能力元数据(含未知模型的兜底默认)。"""
843
+ return dict(VIDEO_MODEL_CAPABILITIES)
844
+
845
+
742
846
  # ═══════════════════════════════════════════════════
743
847
  # Agnes API 域名配置(v6.0)
744
848
  # ═══════════════════════════════════════════════════
@@ -135,6 +135,7 @@ class SimpleVideoPipeline(BasePipeline):
135
135
  height=self._state.video_height,
136
136
  seed=self._state.seed,
137
137
  negative_prompt=self._state.negative_prompt,
138
+ video_size=getattr(self._state, "video_size", None) or "720P",
138
139
  )
139
140
 
140
141
  # 持久化 video_id + curl 命令
package/models/task.py CHANGED
@@ -213,6 +213,8 @@ class SimpleVideoTask(BaseTaskState):
213
213
  reference_image: str = ""
214
214
  end_frame_image: str = ""
215
215
  duration: int = 5
216
+ # v6.2:2.5 系列模型分辨率档位(720P/960P/2K;v2.0 忽略,默认 720P)
217
+ video_size: str = "720P"
216
218
  seed: Optional[int] = None
217
219
  negative_prompt: Optional[str] = None
218
220
  system_prompt: str = ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-short-video",
3
- "version": "6.1.0",
3
+ "version": "6.2.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",