free-short-video 5.7.4 → 6.1.0

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,15 +1,40 @@
1
1
  ---
2
2
 
3
- # What's New in v5.7.4
3
+ # What's New in v6.1.0
4
4
 
5
5
  ## What's New
6
6
 
7
- ### Bug Fixes
7
+ ### Retry-First Guidance on Task Failure
8
8
 
9
- - **Blank page after v5.7.3 upgrade fixed** — during the merge of the analytics changes into `master`, git auto-merge treated the new minified JS bundle as a rename of the old one and injected `<<<<<<<<` / `>>>>>>>>` conflict markers into the shipped bundle. Browsers failed to parse the file with `SyntaxError: Unexpected end of input`, leaving a white screen. The production bundle is now restored byte-for-byte from the original build output, and the page renders normally.
9
+ The failed-task panel now gives you a clear action path instead of a dead-end error line:
10
10
 
11
- ---
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)
12
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
+ ---
13
38
 
14
39
  ---
15
40
 
@@ -18,6 +18,7 @@
18
18
  import json
19
19
  import logging
20
20
  import os
21
+ from contextvars import ContextVar
21
22
  from datetime import datetime
22
23
  from pathlib import Path
23
24
  from typing import Optional
@@ -28,6 +29,10 @@ logger = logging.getLogger(__name__)
28
29
 
29
30
  _WORKSPACE_ROOT: Optional[Path] = None
30
31
 
32
+ # v6.1 二期:当前任务 ID 上下文(BasePipeline 执行入口 set,record 时落盘 task_id)。
33
+ # 用 contextvar 而非逐层传参,将改动面收敛到 API 模块内部(PRD FR9)。
34
+ _TASK_ID_CTX: ContextVar[str] = ContextVar("error_collector_task_id", default="")
35
+
31
36
 
32
37
  def set_workspace_root(path: str) -> None:
33
38
  """设置错误日志存储的根目录(通常为激活的工作空间路径)。
@@ -66,6 +71,15 @@ def _get_log_dir() -> Path:
66
71
  return log_dir
67
72
 
68
73
 
74
+ def set_error_task_id(task_id: str) -> None:
75
+ """设置当前上下文的 task_id(v6.1 二期,诊断关联用)。
76
+
77
+ 由流水线执行入口(``web/deps.run_pipeline``)调用,使后续 ``collect_error``
78
+ 落盘时带上 task_id。通过 contextvar 传递,避免逐层向 API 模块传参。
79
+ """
80
+ _TASK_ID_CTX.set(task_id or "")
81
+
82
+
69
83
  def _extract_from_http_error(exc: Exception) -> tuple[Optional[int], str, str]:
70
84
  """从 HTTPError 异常中提取状态码、响应体、以及 API 级错误详情。
71
85
 
@@ -147,6 +161,8 @@ def collect_error(
147
161
  # 限制字段长度,避免日志文件过大
148
162
  error_data = {
149
163
  "timestamp": now.isoformat(),
164
+ # v6.1 二期:任务关联(诊断端点精确匹配用;无则留空串)
165
+ "task_id": _TASK_ID_CTX.get(),
150
166
  "model_type": model_type,
151
167
  "api_method": api_method,
152
168
  "prompt": prompt[:5000] if prompt else "",
package/core/artifacts.py CHANGED
@@ -25,6 +25,7 @@ from models.task import (
25
25
  BaseTaskState,
26
26
  CreativeVideoTask,
27
27
  ManuscriptVideoTask,
28
+ PoetryVideoTask,
28
29
  StepStatus,
29
30
  )
30
31
 
@@ -115,6 +116,16 @@ _ANCHOR_STEPS_MODEL = [
115
116
  ("step_clip_generation", "clip_gen"),
116
117
  ]
117
118
 
119
+ # Poetry 步骤序列(v6.0 P3:与 multi_scene 模板方法步骤对齐)
120
+ _POETRY_STEPS = [
121
+ ("step_build_scenes", "build_scenes"),
122
+ ("step_reference_images", "reference_images"),
123
+ ("step_video_generation", "video_gen"),
124
+ ("step_audio", "audio"),
125
+ ("step_subtitle", "subtitle"),
126
+ ("step_concatenation", "concatenate"),
127
+ ]
128
+
118
129
 
119
130
  # ═══════════════════════════════════════════════════════════════
120
131
  # 产物定义(每种模式的产物模板)
@@ -183,6 +194,31 @@ def _manuscript_artifact_defs() -> list[dict]:
183
194
  ]
184
195
 
185
196
 
197
+ def _poetry_artifact_defs() -> list[dict]:
198
+ """Poetry 模式的产物定义模板(v6.0 P3)。
199
+
200
+ 诗词视频逐场景产物:scene_{i}/video.mp4、scene_{i}/narration.mp3、
201
+ scene_{i}/subtitle.srt。无参考图阶段(空实现跳过)。
202
+ """
203
+ return [
204
+ # 场景级:视频 / 配音 / 字幕
205
+ {"type": "video", "step_key": "video_gen", "label": "artVideo",
206
+ "category": "video", "scope": "scene", "file": "scene_{i}/video.mp4",
207
+ "scene_fields": ["video_file", "video_id"],
208
+ "extra_files": ["scene_{i}/task.json", "scene_{i}/curl.sh"]},
209
+ {"type": "audio", "step_key": "audio", "label": "artAudio",
210
+ "category": "audio", "scope": "scene", "file": "scene_{i}/narration.mp3",
211
+ "scene_fields": ["narration_audio"]},
212
+ {"type": "subtitle", "step_key": "subtitle", "label": "artSubtitle",
213
+ "category": "subtitle", "scope": "scene", "file": "scene_{i}/subtitle.srt",
214
+ "scene_fields": ["subtitle_srt"]},
215
+ # 任务级:成片
216
+ {"type": "final_video", "step_key": "concatenate", "label": "artFinalVideo",
217
+ "category": "video", "scope": "task", "file": "final_video.mp4",
218
+ "fields": ["final_video_file"]},
219
+ ]
220
+
221
+
186
222
  def _anchor_artifact_defs(is_model_mode: bool) -> list[dict]:
187
223
  """Anchor 模式的产物定义模板。"""
188
224
  artifacts = [
@@ -228,6 +264,8 @@ def _get_steps_for_state(state: BaseTaskState) -> list[tuple[Optional[str], str]
228
264
  if state.audio_source == "model":
229
265
  return _ANCHOR_STEPS_MODEL
230
266
  return _ANCHOR_STEPS_POST_STITCH
267
+ elif isinstance(state, PoetryVideoTask):
268
+ return _POETRY_STEPS
231
269
  return []
232
270
 
233
271
 
@@ -254,6 +292,8 @@ _SCHEMA_HINTS = {
254
292
  "anchor_image": "数字人形象 PNG。可替换后影响数字人视频。",
255
293
  "clip_prompts": "数字人视频 prompt JSON(prompts.json)。",
256
294
  "clip": "数字人循环视频 MP4。",
295
+ "narration_audio": "诗词场景朗诵音频 MP3(scene_{i}/narration.mp3)。逐场景修改后重跑该场景字幕与成片。",
296
+ "subtitle_srt": "诗词场景字幕 SRT(scene_{i}/subtitle.srt)。",
257
297
  }
258
298
 
259
299
 
@@ -270,6 +310,8 @@ def _get_artifact_defs(state: BaseTaskState) -> list[dict]:
270
310
  return _manuscript_artifact_defs()
271
311
  elif isinstance(state, AnchorVideoTask):
272
312
  return _anchor_artifact_defs(state.audio_source == "model")
313
+ elif isinstance(state, PoetryVideoTask):
314
+ return _poetry_artifact_defs()
273
315
  return []
274
316
 
275
317
 
@@ -324,6 +366,8 @@ def list_artifacts(state: BaseTaskState, task_dir: str) -> list[ArtifactDescript
324
366
  scope_count = len(state.paragraphs)
325
367
  elif isinstance(state, AnchorVideoTask):
326
368
  scope_count = len(state.paragraphs) if state.paragraphs else 0
369
+ elif isinstance(state, PoetryVideoTask):
370
+ scope_count = len(state.scenes)
327
371
  else:
328
372
  scope_count = 0
329
373
 
@@ -611,6 +655,25 @@ def apply_cascade_plan(state: BaseTaskState, plan: CascadePlan) -> dict:
611
655
  state.status = StepStatus.PENDING
612
656
  update_kwargs["status"] = StepStatus.PENDING
613
657
 
658
+ # 6. v6.1:删除前置环节产物后,重置受影响检查点的"已批准"状态
659
+ # 级联重置了某检查点对应步骤 → 该检查点不再视为已确认,
660
+ # 恢复执行时 _maybe_pause 会重新在此暂停等待用户(否则会静默跳过)。
661
+ mc = getattr(state, "manual_config", None)
662
+ if mc is not None:
663
+ approved = list(mc.approved_checkpoints or [])
664
+ removed: list[str] = []
665
+ for cp in list(approved):
666
+ step_field = _checkpoint_to_step_field(cp, state)
667
+ if step_field and step_field in plan.steps_to_reset:
668
+ approved.remove(cp)
669
+ removed.append(cp)
670
+ if removed:
671
+ mc.approved_checkpoints = approved
672
+ update_kwargs["manual_config"] = mc
673
+ logger.info(
674
+ "[Artifacts] Cascade reset un-approved checkpoints: %s", removed
675
+ )
676
+
614
677
  return update_kwargs
615
678
 
616
679
 
@@ -698,7 +761,177 @@ def sweep_stale_tasks(age_days: int = 7,
698
761
  # ═══════════════════════════════════════════════════════════════
699
762
 
700
763
  # 清单自身文件名(扫描文件树时排除)
701
- _MANIFEST_FILES = {"manifest.json", "MANIFEST.md", "task_state.json"}
764
+ _MANIFEST_FILES = {"manifest.json", "MANIFEST.md", "task_state.json", "checkpoint.json"}
765
+
766
+
767
+ # ═══════════════════════════════════════════════════════════════
768
+ # v6.0 检查点分组(PRD §4.3 产物矩阵)
769
+ # ═══════════════════════════════════════════════════════════════
770
+
771
+ # 产物 type → 检查点名(与 dependency_graph._TYPE_TO_CHECKPOINT 语义一致)
772
+ # creative:细粒度检查点(每个有产物的环节独立,v6.1)
773
+ _ARTIFACT_TO_CHECKPOINT_FINE: dict[str, str] = {
774
+ "image_analysis": "image_analysis",
775
+ "story": "story",
776
+ "script": "script",
777
+ "character_ref": "character_ref",
778
+ "end_frame_prompts": "end_frame_prompts",
779
+ "end_frame": "end_frame_gen",
780
+ "video": "videos",
781
+ "audio": "audio",
782
+ "subtitle": "subtitle",
783
+ "final_video": "final",
784
+ }
785
+ # 非 creative(manuscript/poetry/anchor):粗粒度合并检查点
786
+ _ARTIFACT_TO_CHECKPOINT_COARSE: dict[str, str] = {
787
+ "story": "scenes",
788
+ "script": "scenes",
789
+ "end_frame_prompts": "scenes",
790
+ "character_ref": "references",
791
+ "end_frame": "references",
792
+ "video": "videos",
793
+ "audio": "audio",
794
+ "subtitle": "subtitle",
795
+ "final_video": "final",
796
+ "scene_prompts": "scenes",
797
+ "anchor_image": "references",
798
+ "clip_prompts": "scenes",
799
+ "clip": "videos",
800
+ }
801
+
802
+ # 检查点展示顺序(creative 细粒度;其余粗粒度)
803
+ _CHECKPOINT_ORDER_FINE = [
804
+ "image_analysis", "story", "script", "character_ref",
805
+ "end_frame_prompts", "end_frame_gen", "videos", "audio", "subtitle", "final",
806
+ ]
807
+ _CHECKPOINT_ORDER = ["scenes", "references", "videos", "audio", "subtitle", "final"]
808
+
809
+
810
+ def _checkpoint_order_for(state: BaseTaskState) -> list[str]:
811
+ """按任务类型返回检查点展示顺序。"""
812
+ if isinstance(state, CreativeVideoTask):
813
+ return _CHECKPOINT_ORDER_FINE
814
+ return _CHECKPOINT_ORDER
815
+
816
+
817
+ def checkpoint_for_artifact(artifact_id: str, task_type: Optional[str] = None) -> str:
818
+ """返回产物 id 所属检查点名(未知产物 → "other")。
819
+
820
+ Args:
821
+ artifact_id: 产物 id(含任务类型前缀,如 ``creative:script``)。
822
+ task_type: 任务类型(缺省时从产物 id 前缀解析)。
823
+ """
824
+ parts = artifact_id.split(":")
825
+ if len(parts) >= 2:
826
+ t = task_type or parts[0]
827
+ mapping = _ARTIFACT_TO_CHECKPOINT_FINE if t == "creative" else _ARTIFACT_TO_CHECKPOINT_COARSE
828
+ return mapping.get(parts[1], "other")
829
+ return "other"
830
+
831
+
832
+ def build_checkpoint_manifest(state: BaseTaskState, task_dir: str) -> dict:
833
+ """构建检查点级产物清单(checkpoint.json 数据)。
834
+
835
+ 按检查点分组展示产物(PRD §4.3 / §4.8),供手动模式检查点等待页
836
+ 与外部 Agent 使用。每组含该检查点的产物列表(复用 manifest 的产物条目)。
837
+
838
+ Returns:
839
+ {
840
+ "format_version": "1.0",
841
+ "task_id": ...,
842
+ "task_type": ...,
843
+ "current_checkpoint": ...,
844
+ "checkpoints": {
845
+ "scenes": { "artifacts": [ ...产物条目... ], "status": "completed|pending" },
846
+ "videos": { "artifacts": [ ... ], "status": ... },
847
+ ...
848
+ }
849
+ }
850
+ """
851
+ task_dir = safe_join(get_working_dir(), task_dir)
852
+ manifest = build_manifest(state, task_dir)
853
+ order = _checkpoint_order_for(state)
854
+
855
+ groups: dict[str, dict] = {cp: {"artifacts": []} for cp in order}
856
+ for a in manifest.get("artifacts", []):
857
+ cp = checkpoint_for_artifact(a["artifact_id"], state.task_type.value)
858
+ if cp in groups:
859
+ groups[cp]["artifacts"].append(a)
860
+
861
+ # 状态:检查点对应的 step 字段状态(复用 step 状态)
862
+ for cp in order:
863
+ step_field = _checkpoint_to_step_field(cp, state)
864
+ status = "pending"
865
+ if step_field:
866
+ val = getattr(state, step_field, None)
867
+ if val is not None:
868
+ status = val.value
869
+ groups[cp]["status"] = status
870
+
871
+ current = ""
872
+ manual_cfg = getattr(state, "manual_config", None)
873
+ if manual_cfg is not None:
874
+ current = manual_cfg.current_checkpoint or ""
875
+
876
+ return {
877
+ "format_version": "1.0",
878
+ "task_id": state.task_id,
879
+ "task_type": state.task_type.value,
880
+ "current_checkpoint": current,
881
+ "checkpoints": groups,
882
+ "files": manifest.get("files", []),
883
+ }
884
+
885
+
886
+ def _checkpoint_to_step_field(checkpoint: str, state: BaseTaskState) -> Optional[str]:
887
+ """检查点名 → 步骤字段名(按任务类型)。"""
888
+ if isinstance(state, CreativeVideoTask):
889
+ mapping = {
890
+ "image_analysis": "step_image_analysis",
891
+ "story": "step_story",
892
+ "script": "step_script",
893
+ "character_ref": "step_character_ref",
894
+ "end_frame_prompts": "step_end_frame_prompts",
895
+ "end_frame_gen": "step_end_frame_generation",
896
+ "videos": "step_video_generation",
897
+ "audio": "step_audio",
898
+ "subtitle": "step_subtitle",
899
+ "final": "step_concatenation",
900
+ }
901
+ return mapping.get(checkpoint)
902
+ if isinstance(state, ManuscriptVideoTask):
903
+ mapping = {
904
+ "scenes": "step_scene_prompts",
905
+ "videos": "step_video_generation",
906
+ "audio": "step_audio",
907
+ "subtitle": "step_subtitle",
908
+ "final": "step_concatenation",
909
+ }
910
+ return mapping.get(checkpoint)
911
+ if isinstance(state, AnchorVideoTask):
912
+ mapping = {
913
+ "scenes": "step_generate_anchor",
914
+ "references": "step_generate_anchor",
915
+ "videos": "step_clip_generation",
916
+ "audio": "step_audio",
917
+ "subtitle": "step_subtitle",
918
+ "final": "step_concatenation",
919
+ }
920
+ return mapping.get(checkpoint)
921
+ return None
922
+
923
+
924
+ def write_checkpoint_manifest(state: BaseTaskState, task_dir: str) -> str:
925
+ """将检查点清单落盘为 ``checkpoint.json``,返回路径;失败返回空串。"""
926
+ try:
927
+ manifest = build_checkpoint_manifest(state, task_dir)
928
+ path = os.path.join(task_dir, "checkpoint.json")
929
+ with open(path, "w", encoding="utf-8") as f:
930
+ json.dump(manifest, f, ensure_ascii=False, indent=2)
931
+ return path
932
+ except Exception as e:
933
+ logger.warning("[Artifacts] failed to write checkpoint.json: %s", e)
934
+ return ""
702
935
 
703
936
 
704
937
  def _scan_task_files(task_dir: str) -> list[dict]:
@@ -759,11 +992,24 @@ def build_manifest(state: BaseTaskState, task_dir: str) -> dict:
759
992
  ),
760
993
  })
761
994
 
995
+ # v6.0 手动模式:暴露执行模式与当前检查点(供前端判断暂停态 / 渲染依赖图)
996
+ manual_cfg = getattr(state, "manual_config", None)
997
+ current_checkpoint = ""
998
+ current_mode = "auto"
999
+ if manual_cfg is not None:
1000
+ current_mode = "manual" if manual_cfg.enabled else "auto"
1001
+ current_checkpoint = manual_cfg.current_checkpoint or ""
1002
+
762
1003
  return {
763
1004
  "format_version": "1.0",
764
1005
  "task_id": state.task_id,
765
1006
  "task_type": state.task_type.value,
766
1007
  "task_status": state.status.value if state.status else "pending",
1008
+ "current_mode": current_mode,
1009
+ "current_checkpoint": current_checkpoint,
1010
+ "manual_config": (
1011
+ manual_cfg.model_dump() if manual_cfg is not None else {}
1012
+ ),
767
1013
  "dir_name": os.path.basename(task_dir.rstrip(os.sep)),
768
1014
  "working_dir": task_dir,
769
1015
  "artifacts": artifacts,
package/core/config.py CHANGED
@@ -15,6 +15,11 @@ logger = logging.getLogger(__name__)
15
15
  CONFIG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".agnes_config")
16
16
  CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
17
17
 
18
+ # ═══════════════════════════════════════════════════
19
+ # 应用版本号(v6.1 新增:发版时同步更新,见 docs/dev/release_process.md)
20
+ # ═══════════════════════════════════════════════════
21
+ APP_VERSION = "6.1.0"
22
+
18
23
  # 未配置 API Key 时的统一报错文案(含免费获取与在线体验兜底,全站路由共用)
19
24
  API_KEY_MISSING_MSG = (
20
25
  "请先配置 API Key。免费获取:https://platform.agnes-ai.com | "