free-short-video 6.2.1 → 6.4.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/.env.example +65 -11
- package/README.md +81 -2
- package/core/__init__.py +3 -3
- package/core/api/__init__.py +1 -1
- package/core/api/agnes_chat.py +0 -1
- package/core/api/agnes_image.py +16 -7
- package/core/api/agnes_models.py +1 -1
- package/core/api/agnes_video.py +61 -10
- package/core/api/error_collector.py +22 -0
- package/core/api/key_manager.py +18 -2
- package/core/api/rate_limiter.py +67 -15
- package/core/artifacts.py +14 -7
- package/core/audio/__init__.py +2 -2
- package/core/audio/subtitle/generator.py +19 -3
- package/core/audio/subtitle/renderer.py +1 -1
- package/core/audio/tashkeel.py +117 -0
- package/core/audio/tts.py +1 -1
- package/core/audio/voices.py +210 -15
- package/core/compositor/concatenator/audio_overlay.py +571 -12
- package/core/compositor/concatenator/concat.py +120 -3
- package/core/compositor/processor.py +2 -3
- package/core/compositor/watermark.py +30 -15
- package/core/config.py +132 -11
- package/core/dependency_graph.py +0 -1
- package/core/pipelines/__init__.py +132 -16
- package/core/pipelines/anchor_video.py +9 -9
- package/core/pipelines/creative/__init__.py +2 -2
- package/core/pipelines/creative/pipeline.py +23 -1
- package/core/pipelines/creative/steps_audio.py +29 -11
- package/core/pipelines/creative/steps_frames.py +2 -2
- package/core/pipelines/creative/steps_script.py +3 -3
- package/core/pipelines/creative/steps_video.py +37 -15
- package/core/pipelines/manuscript_video.py +176 -99
- package/core/pipelines/multi_scene.py +56 -15
- package/core/pipelines/poetry_video.py +65 -21
- package/core/pipelines/simple_video.py +11 -6
- package/core/screenwriter/__init__.py +71 -10
- package/core/screenwriter/scenes.py +1 -1
- package/core/screenwriter/story.py +24 -8
- package/core/screenwriter/style.py +0 -1
- package/core/task_manager.py +4 -3
- package/images/home.png +0 -0
- package/models/__init__.py +17 -17
- package/models/task.py +11 -2
- package/package.json +1 -1
- package/requirements.txt +13 -0
- package/resource/fonts/NotoNaskhArabicUI.ttf +0 -0
- package/resource/fonts/NotoSansBengali-Regular.ttf +0 -0
- package/resource/fonts/NotoSansDevanagari-Regular.ttf +0 -0
- package/resource/fonts/NotoSansThai-Regular.ttf +0 -0
- package/ruff.toml +21 -0
- package/server.py +63 -10
- package/static/assets/ar-Co0g8IHi.js +1 -0
- package/static/assets/bn-CEnXv-Ci.js +1 -0
- package/static/assets/de-DFCb-VQb.js +1 -0
- package/static/assets/es-BB_h_gjW.js +1 -0
- package/static/assets/fa-1AsF74IR.js +1 -0
- package/static/assets/fr-D46wN4E5.js +1 -0
- package/static/assets/hi-c8b3jY49.js +1 -0
- package/static/assets/id-BHQhGk9v.js +1 -0
- package/static/assets/index-B9DuV3Tq.css +1 -0
- package/static/assets/index-DA1ElcyA.js +45 -0
- package/static/assets/it-WSb6hkMy.js +1 -0
- package/static/assets/ja-CnXP4P5V.js +1 -0
- package/static/assets/ko-DHM410FB.js +1 -0
- package/static/assets/ms-BbdBN1mH.js +1 -0
- package/static/assets/nl-ClA4sipR.js +1 -0
- package/static/assets/pt-5buuwYmD.js +1 -0
- package/static/assets/ru-BeoxxbcJ.js +1 -0
- package/static/assets/th-D0e7n9S1.js +1 -0
- package/static/assets/tl-Ci8Qz9u5.js +1 -0
- package/static/assets/tr-BJqJ4N-5.js +1 -0
- package/static/assets/ur-BFgi64hX.js +1 -0
- package/static/assets/vi-DkyI43Z4.js +1 -0
- package/static/index.html +2 -2
- package/utils/image.py +8 -3
- package/utils/video.py +6 -2
- package/web/app_state.py +49 -3
- package/web/deps.py +44 -10
- package/web/helpers.py +0 -1
- package/web/routes/config_routes.py +16 -7
- package/web/routes/health_routes.py +55 -0
- package/web/routes/image_routes.py +1 -2
- package/web/routes/preview_routes.py +279 -0
- package/web/routes/task_creation_routes.py +44 -1
- package/web/routes/task_routes.py +41 -12
- package/web/routes/video_routes.py +2 -3
- package/web/routes/voice_routes.py +0 -1
- package/web/routes/workspace_routes.py +0 -1
- package/static/assets/index-CNMlDGN1.js +0 -98
- package/static/assets/index-CP4jXMrr.css +0 -1
package/.env.example
CHANGED
|
@@ -7,29 +7,83 @@
|
|
|
7
7
|
# 3. 重启服务生效(需已安装 python-dotenv;未安装时跳过 .env 读取,
|
|
8
8
|
# Key 仍可通过环境变量或 Web 设置页 POST /api/config/keys 配置)
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# 说明:
|
|
11
|
+
# - 系统环境变量优先级高于 .env(同名时覆盖 .env 中的值)
|
|
12
|
+
# - 注释掉(以 # 开头)的项表示「使用代码内默认值」,无需取消注释
|
|
13
|
+
# - 完整部署方式见 docs/public/getting-started.md
|
|
11
14
|
# ═══════════════════════════════════════════════════════════════════
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
# ── 1. API Key(必填)──────────────────────────────────────────────
|
|
14
18
|
# 从 https://platform.agnes-ai.com 获取免费 Key
|
|
15
19
|
AGNES_API_KEY=your-api-key-here
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
#
|
|
21
|
+
|
|
22
|
+
# ── 2. 多 Key 轮询(可选,推荐)────────────────────────────────────
|
|
23
|
+
# 每个 Key 的配额独立,总量 ≈ 20 × Key 数 / 分钟。
|
|
24
|
+
# 命名规则:AGNES_API_KEY_2、AGNES_API_KEY_3 ... 序号依次递增,中间不可断号
|
|
25
|
+
# (断号之后的 Key 不会被加载)。
|
|
26
|
+
# 遇到 429 会自动轮换到下一个 Key;限速配额与并发上限随 Key 数线性放大。
|
|
19
27
|
# AGNES_API_KEY_2=your-second-api-key
|
|
20
28
|
# AGNES_API_KEY_3=your-third-api-key
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
|
|
31
|
+
# ── 3. 服务地址 ────────────────────────────────────────────────────
|
|
32
|
+
HOST=0.0.0.0
|
|
33
|
+
PORT=8765
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 4. 限速(可选)─────────────────────────────────────────────────
|
|
37
|
+
# 共享桶:Chat / 图片生成 / 图片上传 / 结果轮询 共用
|
|
38
|
+
# 默认 = 20 × Key 数 × 0.8 安全系数;仍频繁 429 时可适当调低
|
|
23
39
|
# AGNES_RATE_LIMIT=160
|
|
24
40
|
|
|
25
|
-
#
|
|
41
|
+
# 共享桶容量(默认 = 4 × Key 数;仅需调节突发并发时使用)
|
|
26
42
|
# AGNES_RATE_BURST=32
|
|
27
43
|
|
|
28
|
-
#
|
|
44
|
+
# 视频提交独立桶(默认 = 1 × Key 数)
|
|
45
|
+
# 若服务端对视频提交是「全局限 1/min」而非 per-Key,显式设为 1
|
|
29
46
|
# AGNES_VIDEO_RATE_LIMIT=8
|
|
30
47
|
# AGNES_VIDEO_RATE_BURST=8
|
|
31
48
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
49
|
+
# ⚠️ 与并发的联动:任务并发权重上限 = AGNES_RATE_LIMIT // 2(默认 10)
|
|
50
|
+
# 各任务类型权重:简单视频 1 / 创意视频 3 / 稿件视频 4 / 数字人 2 / 诗词 3
|
|
51
|
+
# 若把 AGNES_RATE_LIMIT 调得过低(例如 6),并发上限降到 3,
|
|
52
|
+
# 权重为 4 的稿件类任务会因「权重超过并发上限」而无法启动。
|
|
53
|
+
# 此时请调高该值,或配置多个 API Key。
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── 5. 图生图模型(可选)───────────────────────────────────────────
|
|
57
|
+
# i2i 默认与 t2i 同模型(agnes-image-2.1-flash);如需回退到 2.0:
|
|
58
|
+
# AGNES_IMAGE_I2I_MODEL=agnes-image-2.0
|
|
59
|
+
#
|
|
60
|
+
# 注:视频 / 图片主模型请通过 Web UI 或 POST /api/config 选择,
|
|
61
|
+
# 不再通过环境变量配置(AGNES_VIDEO_MODEL、AGNES_IMAGE_MODEL、
|
|
62
|
+
# AGNES_BASE_URL 均已废弃,代码中不再读取)。
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── 6. 运维(可选)─────────────────────────────────────────────────
|
|
66
|
+
# 启动时清理 N 天前的僵尸任务目录(不设置则不执行清理)
|
|
67
|
+
# AGNES_SWEEP_AGE_DAYS=7
|
|
68
|
+
|
|
69
|
+
# 多 Key 管理接口生成 Key id 所用的哈希盐,一般无需修改
|
|
70
|
+
# AGNES_CONFIG_ID_HMAC_KEY=agnes-config-keys-id-v1
|
|
71
|
+
|
|
72
|
+
# 视频任务轮询总超时(秒,默认 1800)
|
|
73
|
+
# AGNES_VIDEO_POLL_TIMEOUT=1800
|
|
74
|
+
|
|
75
|
+
# 字幕合成:=0 关闭 ffmpeg ASS 单链(回退 moviepy 词级动效路径),默认开启
|
|
76
|
+
# AGNES_SUBTITLE_ASS=1
|
|
77
|
+
|
|
78
|
+
# 提示词语言(zh/en,影响 LLM meta-prompt 语言)
|
|
79
|
+
# PROMPT_LANGUAGE=zh
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── 7. CORS 跨源白名单(可选,供独立本地伴侣工具调用本服务 API)──────────
|
|
83
|
+
# 场景:本地跑一个独立的简化前端(如 agnes-simple-ui,监听 :8787),
|
|
84
|
+
# 从浏览器直接调用本服务的 /api/* 接口。设置后服务仅允许列出的源跨源调用。
|
|
85
|
+
# 默认空 = 不启用 CORS(攻击面不变);同源页面不受影响。
|
|
86
|
+
# AGNES_CORS_ORIGINS=http://localhost:8787,http://127.0.0.1:3000
|
|
87
|
+
#
|
|
88
|
+
# 显式禁用(即使设置了 origins 也不启用中间件):
|
|
89
|
+
# AGNES_CORS_ENABLED=false
|
package/README.md
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
|
|
3
|
-
# What's New in v6.
|
|
3
|
+
# What's New in v6.4.0
|
|
4
4
|
|
|
5
5
|
## What's New
|
|
6
6
|
|
|
7
|
+
### Features & Improvements
|
|
8
|
+
|
|
9
|
+
- **Preview ("dry-run") endpoints for creative and manuscript tasks** — `POST /api/creative/preview-script` and `POST /api/manuscript/preview-split` let you inspect the generated script or manuscript segments before submitting a full task. Synchronous, no task created. Preview calls share the Chat API rate limiter and have a lightweight in-process concurrency cap (429 + Retry-After) to prevent abuse. (Credit: @Khaled97Sho, PR #33)
|
|
10
|
+
- **Arabic tashkeel for TTS narration** — creative and manuscript pipelines now automatically apply Arabic diacritic marks (harakat) to narration text before TTS synthesis, improving pronunciation quality. Tashkeel only affects TTS input; subtitle text and `narration.txt` remain clean. (Credit: @Khaled97Sho, PR #33)
|
|
11
|
+
- **Manuscript reference images** — per-segment reference images for manuscript tasks are now supported via `reference_images` + `reference_images_map` parameters, matching the same feature used in creative pipelines. (Credit: @Khaled97Sho, PR #33)
|
|
12
|
+
- **Language-aware narration budget** — narration length for non-CJK languages now uses a language-specific characters-per-second rate (Arabic: 4.0, CJK: 5.0, others: 12.0), fixing the "narration too short for non-CJK text" issue. (Credit: @Khaled97Sho, PR #33)
|
|
13
|
+
- **Configurable CORS origins** — the `AGNES_CORS_ORIGINS` environment variable (comma-separated) allows any companion tool to call the core API cross-origin. `AGNES_CORS_ENABLED` can force-enable or disable; auto-detects when origins are set.
|
|
14
|
+
- **China domestic API domain** — the `cn` domain in `AGNES_DOMAIN_MAP` now correctly points to `api.agnes-ai.cn` (the official China service endpoint), updated from the incorrect `apihub.agnes-ai.cn` (international-site fallback). The frontend domain picker reflects the correct hostname.
|
|
15
|
+
|
|
16
|
+
### Refactoring & Optimizations
|
|
17
|
+
|
|
18
|
+
- **`split_manuscript_text()` extracted as a public utility** — the manuscript-splitting algorithm is now available as a standalone function, used by both the manuscript pipeline and the new preview-split endpoint.
|
|
19
|
+
|
|
7
20
|
### Bug Fixes
|
|
8
21
|
|
|
9
|
-
- **
|
|
22
|
+
- **Fixed rate-limiter live-lock** — a rare synchronous acquire path race condition could stall all pipelines indefinitely. The token bucket's async path is now the primary path, with the sync path hardened to prevent the live-lock. (Credit: @Khaled97Sho, PR #33)
|
|
23
|
+
- **Fixed image save not persisting** — `POST /api/images/generations` now `await`s the image download, ensuring the generated image is actually written to disk before the response.
|
|
24
|
+
- **Fixed GitHub CodeQL alerts #43 and #47** — path-injection and information-disclosure security alerts resolved.
|
|
25
|
+
- **Fixed PBKDF2-HMAC-SHA256 for config key IDs** — config key IDs are now hashed with a proper key-derivation function instead of weaker hashing.
|
|
26
|
+
- **Fixed Docker Hub Cloudflare WAF blocking** — the Docker Hub overview update script now strips `<script>` blocks and escapes `<` in the JSON payload to bypass the WAF XSS rule.
|
|
27
|
+
- **Fixed China domestic site domain** — `AGNES_DOMAIN_MAP["cn"]` corrected to `api.agnes-ai.cn` per the official Agnes model catalog. (Issue #37)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
**Compatibility notes**: The `core/audio/tashkeel.py` module adds a new optional dependency (`mishkal`). It is installed by default via `requirements.txt`; if missing, tashkeel silently falls back to the original text. Existing task state files are forward-compatible; no migration needed.
|
|
10
32
|
|
|
11
33
|
---
|
|
12
34
|
|
|
@@ -35,6 +57,49 @@ free-short-video
|
|
|
35
57
|
[](https://hub.docker.com/r/lcy362/free-short-video)
|
|
36
58
|
[](https://www.npmjs.com/package/free-short-video)
|
|
37
59
|
|
|
60
|
+
<p align="center">
|
|
61
|
+
<img src="images/home.png" alt="Agnes Video Generator — Free AI Video Generator" width="720">
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<!--
|
|
65
|
+
schema.org structured data for SEO/GEO indexing. GitHub does not execute this script, but the raw JSON-LD is visible to search engines and AI engines that scan repository READMEs.
|
|
66
|
+
-->
|
|
67
|
+
<!--
|
|
68
|
+
<script type="application/ld+json">
|
|
69
|
+
{
|
|
70
|
+
"@context": "https://schema.org",
|
|
71
|
+
"@type": "SoftwareApplication",
|
|
72
|
+
"name": "Agnes Video Generator",
|
|
73
|
+
"alternateName": "Free AI Video Generator",
|
|
74
|
+
"applicationCategory": "MultimediaApplication",
|
|
75
|
+
"operatingSystem": "Linux, macOS, Windows",
|
|
76
|
+
"description": "A completely free, open-source AI video generator. No subscription, no high-end GPU, no usage limits — type a text idea and get narrated, auto-subtitled multi-scene AI videos. Supports text-to-video, image-to-video, keyframes animation, digital anchor and manuscript-to-video.",
|
|
77
|
+
"url": "https://github.com/lcy362/agnes-video-generator",
|
|
78
|
+
"downloadUrl": "https://github.com/lcy362/agnes-video-generator",
|
|
79
|
+
"softwareVersion": "1.0.0",
|
|
80
|
+
"license": "https://opensource.org/licenses/MIT",
|
|
81
|
+
"keywords": "free AI video generator, AI video generation, text to video, AI video creator, open source video generator, AI narration, auto subtitles, multi-scene video, Runway alternative, Pika alternative",
|
|
82
|
+
"offers": {
|
|
83
|
+
"@type": "Offer",
|
|
84
|
+
"price": "0",
|
|
85
|
+
"priceCurrency": "USD"
|
|
86
|
+
},
|
|
87
|
+
"author": {
|
|
88
|
+
"@type": "Person",
|
|
89
|
+
"@id": "https://lichuanyang.top/#author",
|
|
90
|
+
"name": "SandGrid",
|
|
91
|
+
"alternateName": "lcy362",
|
|
92
|
+
"url": "https://lichuanyang.top/",
|
|
93
|
+
"sameAs": [
|
|
94
|
+
"https://github.com/lcy362",
|
|
95
|
+
"https://gitee.com/sandgrid/agnes-video-generator",
|
|
96
|
+
"https://video.lichuanyang.top/"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
-->
|
|
102
|
+
|
|
38
103
|
> **🌏 Mirror Notice / 镜像说明**
|
|
39
104
|
> This project is also mirrored on [Gitee](https://gitee.com/sandgrid/agnes-video-generator) for faster access in mainland China. The **GitHub repository is the primary home** of this project — issues, PRs, and stars are managed there.
|
|
40
105
|
> 本项目在国内 Gitee 设有镜像仓库,便于国内访问加速;**GitHub 为项目主仓库**,Issue / PR / Star 均在 GitHub 提交。
|
|
@@ -121,6 +186,20 @@ To be honest, Agnes's video model isn't perfect yet. The generated frames are so
|
|
|
121
186
|
| **Watermark** | No watermark | Built-in watermark | Built-in watermark | C2PA metadata | Built-in watermark |
|
|
122
187
|
| **Usage Limit** | No limit (16 req/min rate limit) | Billed by compute | Billed by generation | Billed by generation | Billed by generation |
|
|
123
188
|
|
|
189
|
+
## ⚙️ Configuration
|
|
190
|
+
|
|
191
|
+
Everything is configured through environment variables — no config file is required. To start from a documented template:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
cp .env.example .env # then edit AGNES_API_KEY inside
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
[`.env.example`](.env.example) lists every supported variable with its default value: API key, multi-key rotation, rate limits, port, model overrides, and maintenance options.
|
|
198
|
+
|
|
199
|
+
> **Have more than one API key?** Set `AGNES_API_KEY`, `AGNES_API_KEY_2`, `AGNES_API_KEY_3` … (numbering must be contiguous). Rate-limit quotas scale with your key count, a `429` automatically rotates to the next key, and the concurrency limit scales along with it.
|
|
200
|
+
|
|
201
|
+
Full walkthrough: [Getting Started → Configure API Key](docs/public/getting-started.md).
|
|
202
|
+
|
|
124
203
|
## 📚 Documentation
|
|
125
204
|
|
|
126
205
|
- **[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.
|
package/core/__init__.py
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
导出所有子包的核心类和工具函数。
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from core.api import AgnesImageAPI, AgnesVideoAPI
|
|
6
|
+
from core.api import AgnesChatAPI, AgnesImageAPI, AgnesVideoAPI
|
|
7
7
|
from core.audio import EdgeTTSEngine, SilentTTSEngine, SubtitleGenerator
|
|
8
8
|
from core.compositor import VideoConcatenator, VideoProcessor
|
|
9
9
|
from core.pipelines import (
|
|
10
10
|
BasePipeline,
|
|
11
|
-
PipelineShutdown,
|
|
12
|
-
SimpleVideoPipeline,
|
|
13
11
|
CreativeVideoPipeline,
|
|
14
12
|
ManuscriptVideoPipeline,
|
|
13
|
+
PipelineShutdown,
|
|
14
|
+
SimpleVideoPipeline,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
package/core/api/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""core.api — Agnes AI API 调用层"""
|
|
2
2
|
|
|
3
|
+
from core.api.agnes_chat import AgnesChatAPI
|
|
3
4
|
from core.api.agnes_image import AgnesImageAPI, ImageOutput
|
|
4
5
|
from core.api.agnes_video import AgnesVideoAPI, VideoOutput
|
|
5
|
-
from core.api.agnes_chat import AgnesChatAPI
|
|
6
6
|
|
|
7
7
|
__all__ = ["AgnesImageAPI", "ImageOutput", "AgnesVideoAPI", "VideoOutput", "AgnesChatAPI"]
|
package/core/api/agnes_chat.py
CHANGED
package/core/api/agnes_image.py
CHANGED
|
@@ -5,7 +5,6 @@ import base64
|
|
|
5
5
|
import logging
|
|
6
6
|
import mimetypes
|
|
7
7
|
import os
|
|
8
|
-
import time
|
|
9
8
|
from typing import List, Optional
|
|
10
9
|
|
|
11
10
|
import requests
|
|
@@ -29,7 +28,16 @@ class ImageOutput:
|
|
|
29
28
|
self.ext = ext
|
|
30
29
|
self.data = data
|
|
31
30
|
|
|
32
|
-
def save(self, path: str) -> None:
|
|
31
|
+
async def save(self, path: str) -> None:
|
|
32
|
+
"""保存图片到 path(异步)。
|
|
33
|
+
|
|
34
|
+
优化路线图 0.3:URL 下载为同步 requests 流式读取,协程中直接调用会
|
|
35
|
+
阻塞事件循环;整体下沉到线程池执行。
|
|
36
|
+
"""
|
|
37
|
+
await asyncio.to_thread(self._save_sync, path)
|
|
38
|
+
|
|
39
|
+
def _save_sync(self, path: str) -> None:
|
|
40
|
+
"""同步保存实现(供线程池调用;同步上下文可直接使用)。"""
|
|
33
41
|
if self.fmt == "url":
|
|
34
42
|
download_image(self.data, path)
|
|
35
43
|
else:
|
|
@@ -59,7 +67,8 @@ class AgnesImageAPI:
|
|
|
59
67
|
self.api_key = api_key
|
|
60
68
|
self.model = model
|
|
61
69
|
# i2i 默认与 t2i 同模型(官方 2.1 同时支持 t2i/i2i);环境变量可回退到 2.0。
|
|
62
|
-
|
|
70
|
+
from core.config import get_settings
|
|
71
|
+
env_i2i = get_settings().agnes_image_i2i_model
|
|
63
72
|
self.i2i_model = i2i_model or env_i2i or model
|
|
64
73
|
# 基础 headers(不含 Authorization):每次请求前经 _auth_headers() 注入当前 Key
|
|
65
74
|
self._base_headers = {
|
|
@@ -143,8 +152,8 @@ class AgnesImageAPI:
|
|
|
143
152
|
max_rotations = len(ring) * max_retries
|
|
144
153
|
while attempt < max_retries:
|
|
145
154
|
try:
|
|
146
|
-
# 全局限速:在发起 HTTP
|
|
147
|
-
await
|
|
155
|
+
# 全局限速:在发起 HTTP 请求前获取令牌(2.3 异步原生)
|
|
156
|
+
await get_rate_limiter().acquire_async()
|
|
148
157
|
# 动态超时:第一次 120s,后续逐步增加(图像生成较慢,放宽读超时)
|
|
149
158
|
read_timeout = _READ_TIMEOUT_BASE_SECONDS * (attempt + 1)
|
|
150
159
|
resp = await asyncio.to_thread(
|
|
@@ -175,7 +184,7 @@ class AgnesImageAPI:
|
|
|
175
184
|
"image", "generate_single_image",
|
|
176
185
|
prompt=prompt,
|
|
177
186
|
error_type="RateLimit429",
|
|
178
|
-
error_message=
|
|
187
|
+
error_message="HTTP 429: rate limited",
|
|
179
188
|
status_code=429,
|
|
180
189
|
response_body=resp.text,
|
|
181
190
|
retry_count=attempt + 1,
|
|
@@ -188,7 +197,7 @@ class AgnesImageAPI:
|
|
|
188
197
|
"image", "generate_single_image",
|
|
189
198
|
prompt=prompt,
|
|
190
199
|
error_type="RateLimit429",
|
|
191
|
-
error_message=
|
|
200
|
+
error_message="HTTP 429: retries exhausted",
|
|
192
201
|
status_code=429,
|
|
193
202
|
response_body=resp.text,
|
|
194
203
|
retry_count=max_retries,
|
package/core/api/agnes_models.py
CHANGED
package/core/api/agnes_video.py
CHANGED
|
@@ -37,13 +37,56 @@ DURATION_PRESETS = {
|
|
|
37
37
|
_UPLOAD_RETRY_BASE_DELAY_SECONDS = 30
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def _adaptive_poll_interval(interval: int, poll_count: int) -> int:
|
|
41
|
+
"""优化路线图 1.3:自适应轮询间隔。
|
|
42
|
+
|
|
43
|
+
此前固定 ``interval``(默认 60s),每个视频平均多等 ~30s 检测延迟。
|
|
44
|
+
现改为 20s 起步,每 5 次轮询 +5s,上限为调用方 ``interval``;
|
|
45
|
+
调用方传小间隔(<20,如测试)时保持原样。
|
|
46
|
+
"""
|
|
47
|
+
if interval < 20:
|
|
48
|
+
return interval
|
|
49
|
+
return min(interval, 20 + (poll_count // 5) * 5)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class VideoTaskCancelled(RuntimeError):
|
|
53
|
+
"""用户停止任务导致的取消(优化路线图 0.2)。
|
|
54
|
+
|
|
55
|
+
继承 RuntimeError 以保持向后兼容,但语义上区别于可重试的临时错误
|
|
56
|
+
(超时 / 网络 / 5xx):停止必须立即穿透上层重试循环,否则用户点停止后
|
|
57
|
+
仍要经历 20s/40s 退避才真正停下。
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_remote_video_failure(exc: BaseException) -> bool:
|
|
62
|
+
"""判断是否为「服务端已确认失败」——只有这种情况才可安全丢弃 video_id。
|
|
63
|
+
|
|
64
|
+
仅当服务端明确返回 ``status=failed``(异常信息含 "Video generation failed:")
|
|
65
|
+
时为 True。超时、用户取消、网络中断时服务端任务**可能仍在运行**,必须返回
|
|
66
|
+
False 以保留 video_id 供续传,避免重复提交浪费视频配额(1 次/分钟/Key)。
|
|
67
|
+
|
|
68
|
+
优化路线图 0.2:此前流水线在任何异常下都删除 task.json,导致超时/取消后
|
|
69
|
+
续传只能重新提交。
|
|
70
|
+
"""
|
|
71
|
+
return "Video generation failed:" in str(exc)
|
|
72
|
+
|
|
73
|
+
|
|
40
74
|
class VideoOutput:
|
|
41
75
|
def __init__(self, fmt: str, ext: str, data: str):
|
|
42
76
|
self.fmt = fmt
|
|
43
77
|
self.ext = ext
|
|
44
78
|
self.data = data
|
|
45
79
|
|
|
46
|
-
def save(self, path: str) -> None:
|
|
80
|
+
async def save(self, path: str) -> None:
|
|
81
|
+
"""保存视频到 path(异步)。
|
|
82
|
+
|
|
83
|
+
优化路线图 0.3:URL 下载为同步 requests 流式读取,耗时 5~30s+,
|
|
84
|
+
此前在协程中直接调用会阻塞事件循环;整体下沉到线程池执行。
|
|
85
|
+
"""
|
|
86
|
+
await asyncio.to_thread(self._save_sync, path)
|
|
87
|
+
|
|
88
|
+
def _save_sync(self, path: str) -> None:
|
|
89
|
+
"""同步保存实现(供线程池调用;同步上下文可直接使用)。"""
|
|
47
90
|
if self.fmt == "url":
|
|
48
91
|
download_video(self.data, path)
|
|
49
92
|
else:
|
|
@@ -158,7 +201,7 @@ class AgnesVideoAPI:
|
|
|
158
201
|
},
|
|
159
202
|
}
|
|
160
203
|
logger.info(f"[AgnesVideo] Uploading image to hosted URL (attempt {attempt + 1}/{retries})...")
|
|
161
|
-
await
|
|
204
|
+
await get_rate_limiter().acquire_async(self.shutdown_event)
|
|
162
205
|
resp = await asyncio.to_thread(
|
|
163
206
|
requests.post,
|
|
164
207
|
f"{get_agnes_base_url()}/images/generations",
|
|
@@ -250,7 +293,7 @@ class AgnesVideoAPI:
|
|
|
250
293
|
while True:
|
|
251
294
|
# M2: 每次轮询前检查停止信号
|
|
252
295
|
if self.shutdown_event and self.shutdown_event.is_set():
|
|
253
|
-
raise
|
|
296
|
+
raise VideoTaskCancelled("Video generation cancelled by user")
|
|
254
297
|
|
|
255
298
|
elapsed = asyncio.get_event_loop().time() - start_time
|
|
256
299
|
if elapsed > max_poll_duration:
|
|
@@ -270,8 +313,8 @@ class AgnesVideoAPI:
|
|
|
270
313
|
try:
|
|
271
314
|
if poll_count % 10 == 0:
|
|
272
315
|
logger.info(f"[AgnesVideo] Polling video {video_id[:16]}... (poll #{poll_count + 1}, elapsed {elapsed:.0f}s)")
|
|
273
|
-
#
|
|
274
|
-
await
|
|
316
|
+
# 全局限速:每次轮询都消耗一个令牌(2.3 异步原生,停止可打断)
|
|
317
|
+
await get_rate_limiter().acquire_async(self.shutdown_event)
|
|
275
318
|
# M2: 用 wait_for 包裹以支持取消;429 换 Key 立即重试(轮询也轮转 Key 分摊配额)
|
|
276
319
|
poll_attempts = 0
|
|
277
320
|
while True:
|
|
@@ -344,7 +387,8 @@ class AgnesVideoAPI:
|
|
|
344
387
|
)
|
|
345
388
|
raise RuntimeError(error_msg)
|
|
346
389
|
|
|
347
|
-
|
|
390
|
+
# 优化路线图 1.3:自适应轮询间隔(20s 起步,每 5 次 +5s,上限 interval)
|
|
391
|
+
await asyncio.sleep(_adaptive_poll_interval(interval, poll_count))
|
|
348
392
|
|
|
349
393
|
async def _submit_with_retry(self, payload: dict, mode_desc: str) -> str:
|
|
350
394
|
frame_reductions_left = 2 # allow up to 2 frame-count reductions on 400
|
|
@@ -354,11 +398,12 @@ class AgnesVideoAPI:
|
|
|
354
398
|
max_rotations = len(ring) * self.max_retries
|
|
355
399
|
while attempt < self.max_retries:
|
|
356
400
|
if self.shutdown_event and self.shutdown_event.is_set():
|
|
357
|
-
raise
|
|
401
|
+
raise VideoTaskCancelled("Video generation cancelled by user")
|
|
358
402
|
try:
|
|
359
403
|
logger.info(f"[AgnesVideo] Submitting {mode_desc} (attempt {attempt + 1}/{self.max_retries})...")
|
|
360
|
-
# 视频提交独立限速桶(服务端 1/min 硬限制,不与 chat/image
|
|
361
|
-
|
|
404
|
+
# 视频提交独立限速桶(服务端 1/min 硬限制,不与 chat/image 共享配额;
|
|
405
|
+
# 2.3 异步原生,停止可打断)
|
|
406
|
+
await get_video_submit_limiter().acquire_async(self.shutdown_event)
|
|
362
407
|
# M2: 缩短读超时从 180s 到 60s,使 stop() 更快生效
|
|
363
408
|
resp = await asyncio.wait_for(
|
|
364
409
|
asyncio.to_thread(
|
|
@@ -671,7 +716,13 @@ class AgnesVideoAPI:
|
|
|
671
716
|
return video_id
|
|
672
717
|
|
|
673
718
|
async def wait_for_video(self, video_id: str, progress_callback=None) -> VideoOutput:
|
|
674
|
-
|
|
719
|
+
# 1.2:轮询总超时可经 AGNES_VIDEO_POLL_TIMEOUT 配置(3.5 RuntimeSettings 收敛)
|
|
720
|
+
from core.config import get_settings
|
|
721
|
+
poll_timeout = get_settings().agnes_video_poll_timeout
|
|
722
|
+
final = await self._poll_task(
|
|
723
|
+
video_id, progress_callback=progress_callback,
|
|
724
|
+
max_poll_duration=poll_timeout,
|
|
725
|
+
)
|
|
675
726
|
|
|
676
727
|
video_url = (
|
|
677
728
|
final.get("remixed_from_video_id")
|
|
@@ -71,6 +71,25 @@ def _get_log_dir() -> Path:
|
|
|
71
71
|
return log_dir
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
# 优化路线图 1.5b:error_logs 按数量轮转,超过上限后删除最旧文件,
|
|
75
|
+
# 防止长期运行的工作区无限膨胀(诊断端点 _iter_error_logs 全量读取也受影响)。
|
|
76
|
+
_MAX_ERROR_LOGS = 500
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _rotate_error_logs(log_dir: Path) -> None:
|
|
80
|
+
"""超过 _MAX_ERROR_LOGS 个文件时,按 mtime 删除最旧的超量文件。"""
|
|
81
|
+
try:
|
|
82
|
+
files = [p for p in log_dir.glob("*.json") if p.is_file()]
|
|
83
|
+
if len(files) <= _MAX_ERROR_LOGS:
|
|
84
|
+
return
|
|
85
|
+
files.sort(key=lambda p: p.stat().st_mtime) # 最旧在前
|
|
86
|
+
for p in files[: len(files) - _MAX_ERROR_LOGS]:
|
|
87
|
+
p.unlink(missing_ok=True)
|
|
88
|
+
logger.info(f"[ErrorCollector] Rotated old error log → {p.name}")
|
|
89
|
+
except OSError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
74
93
|
def set_error_task_id(task_id: str) -> None:
|
|
75
94
|
"""设置当前上下文的 task_id(v6.1 二期,诊断关联用)。
|
|
76
95
|
|
|
@@ -179,6 +198,9 @@ def collect_error(
|
|
|
179
198
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
180
199
|
json.dump(error_data, f, ensure_ascii=False, indent=2)
|
|
181
200
|
|
|
201
|
+
# 优化路线图 1.5b:按数量轮转,防止 error_logs 无限膨胀
|
|
202
|
+
_rotate_error_logs(log_dir)
|
|
203
|
+
|
|
182
204
|
logger.info(f"[ErrorCollector] Error saved → {filepath}")
|
|
183
205
|
return str(filepath)
|
|
184
206
|
|
package/core/api/key_manager.py
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import itertools
|
|
17
17
|
import logging
|
|
18
18
|
import threading
|
|
19
|
+
from typing import Optional
|
|
19
20
|
|
|
20
21
|
from core.config import get_api_keys
|
|
21
22
|
|
|
@@ -29,15 +30,30 @@ class KeyRing:
|
|
|
29
30
|
self._keys = list(keys)
|
|
30
31
|
self._count = itertools.count()
|
|
31
32
|
self._lock = threading.Lock()
|
|
33
|
+
# rotate() 钉住的下一个 Key 索引:rotate 后紧接的 next() 必须返回该 Key
|
|
34
|
+
# (此前 rotate 与 next 共享递增计数,rotate 消费一个序号后 next 取模
|
|
35
|
+
# 又回到原 Key,导致 429 换 Key 重试实际仍用旧 Key)
|
|
36
|
+
self._force_next: Optional[int] = None
|
|
32
37
|
|
|
33
38
|
def next(self) -> str:
|
|
34
39
|
"""轮转取下一个 Key(普通请求调用,均匀分摊)。"""
|
|
35
|
-
|
|
40
|
+
with self._lock:
|
|
41
|
+
if self._force_next is not None:
|
|
42
|
+
idx = self._force_next
|
|
43
|
+
self._force_next = None
|
|
44
|
+
return self._keys[idx]
|
|
45
|
+
return self._keys[next(self._count) % len(self._keys)]
|
|
36
46
|
|
|
37
47
|
def rotate(self) -> str:
|
|
38
|
-
"""强制切换到下一个 Key(429 换 Key 重试调用)。
|
|
48
|
+
"""强制切换到下一个 Key(429 换 Key 重试调用)。
|
|
49
|
+
|
|
50
|
+
递增计数返回下一个 Key,并记录为 ``_force_next``,确保紧随其后的
|
|
51
|
+
``next()``(即重试请求的 ``_auth_headers()``)真的使用新 Key,
|
|
52
|
+
之后恢复正常 round-robin。
|
|
53
|
+
"""
|
|
39
54
|
with self._lock:
|
|
40
55
|
idx = next(self._count) % len(self._keys)
|
|
56
|
+
self._force_next = idx
|
|
41
57
|
return self._keys[idx]
|
|
42
58
|
|
|
43
59
|
@property
|