free-short-video 5.1.7 → 5.2.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 +10 -481
- package/core/api/agnes_chat.py +2 -3
- package/core/api/agnes_image.py +2 -3
- package/core/api/agnes_models.py +3 -6
- package/core/api/agnes_video.py +7 -9
- package/core/compositor/concatenator.py +1 -1
- package/core/config.py +47 -0
- package/core/pipelines/__init__.py +3 -3
- package/core/pipelines/creative_video.py +13 -13
- package/core/task_manager.py +2 -2
- package/package.json +1 -1
- package/server.py +25 -1
- package/static/index.html +189 -1
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ free-short-video
|
|
|
20
20
|
[](https://github.com/lcy362/agnes-video-generator/blob/main/LICENSE)
|
|
21
21
|
[](https://www.python.org/)
|
|
22
22
|
[](https://video.lichuanyang.top)
|
|
23
|
+
[](https://hub.docker.com/r/lcy362/free-short-video)
|
|
24
|
+
[](https://www.npmjs.com/package/free-short-video)
|
|
23
25
|
|
|
24
26
|
> **Completely free AI video generator** — Built on Agnes AI's free models, no subscription, no high-end GPU, no usage limits. Type in a text idea and automatically generate multi-scene AI videos with narration and subtitles. Supports text-to-video, image-to-video, keyframes animation, digital anchor, and more. All AI compute runs in the cloud — a regular laptop is all you need. **[Try it online →](https://video.lichuanyang.top)**
|
|
25
27
|
|
|
@@ -96,487 +98,14 @@ To be honest, Agnes's video model isn't perfect yet. The generated frames are so
|
|
|
96
98
|
| **Watermark** | No watermark | Built-in watermark | Built-in watermark | C2PA metadata | Built-in watermark |
|
|
97
99
|
| **Usage Limit** | No limit (16 req/min rate limit) | Billed by compute | Billed by generation | Billed by generation | Billed by generation |
|
|
98
100
|
|
|
99
|
-
##
|
|
101
|
+
## 📚 Documentation
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
| **Digital Anchor** | AI-generated digital anchor (or upload custom image) → dynamic anchor clip → TTS narration → subtitle positioning → looped concatenation. Optional reference image for appearance consistency. | Virtual anchors, product presentations, news broadcasts |
|
|
109
|
-
|
|
110
|
-
### 🆓 Completely Free AI Model Chain
|
|
111
|
-
|
|
112
|
-
All core AI capabilities are **completely free** — no trial period, no watermarks, no token limits:
|
|
113
|
-
|
|
114
|
-
| Capability | Model | Cost |
|
|
115
|
-
|-----------|-------|------|
|
|
116
|
-
| Text / Script Generation | `agnes-2.0-flash` | Free |
|
|
117
|
-
| Image Generation | `agnes-image-2.1-flash` | Free |
|
|
118
|
-
| Video Generation | `agnes-video-v2.0` | Free |
|
|
119
|
-
| Text-to-Speech Narration | Edge TTS (Microsoft) | Free, no extra API key needed |
|
|
120
|
-
|
|
121
|
-
All AI API calls share a global token bucket rate limiter (16 requests/min), with automatic retries and exponential backoff to ensure stable operation.
|
|
122
|
-
|
|
123
|
-
### 🎙️ AI Narration & Smart Subtitles
|
|
124
|
-
|
|
125
|
-
Both Creative Video and Manuscript Video support:
|
|
126
|
-
|
|
127
|
-
- **Free TTS narration**: Based on Microsoft Edge TTS, offering 4 Chinese voice roles (gentle female, steady male, lively female, young male) with adjustable speech rate (-30% to +30%)
|
|
128
|
-
- **Word-level fine-grained subtitles**: SRT subtitles generated from TTS word-level timestamps, one entry every 2-3 seconds, with precise audio-video sync
|
|
129
|
-
- **Multi-line auto-wrapping**: Long subtitle text is intelligently split into two lines, preferring punctuation break points to prevent screen overflow
|
|
130
|
-
- **Fully configurable subtitle style**: Font, color, size, position (top/bottom), stroke, and semi-transparent background
|
|
131
|
-
- **Audio-video sync strategy**: All video clips are concatenated first, then audio and subtitles are overlaid as a whole, avoiding cumulative errors from per-segment overlay. TTS output is automatically amplified 2.5× to compensate for Edge TTS's low default volume
|
|
132
|
-
|
|
133
|
-
### 🎨 Flexible Creative Controls
|
|
134
|
-
|
|
135
|
-
- **Custom reference images** — Upload character or scene reference images to maintain visual consistency across scenes
|
|
136
|
-
- **Custom end frames** — Specify end frame images per scene for precise visual transition control
|
|
137
|
-
- **Image-to-image end frames** — Auto-generate scene end frames via img2img from your reference image
|
|
138
|
-
- **Three video chaining modes** — `keyframes` (first+last frame interpolation, recommended) / `ti2vid` (inter-scene transition frames) / `none` (independent scenes)
|
|
139
|
-
- **Multiple resolutions** — Portrait 9:16 (768×1152), Landscape 16:9 (1152×768), Square 1:1 (1024×1024)
|
|
140
|
-
- **Flexible duration** — Custom scene duration
|
|
141
|
-
- **Smart manuscript splitting** — Splits by period/question mark/exclamation mark, greedily merges into 5-12 second segments based on reading speed (~4 chars/sec), preserves long sentences, auto-merges short sentences forward
|
|
142
|
-
|
|
143
|
-
### 🔧 Production-Grade Reliability
|
|
144
|
-
|
|
145
|
-
- **Checkpoint resume** — Automatically resumes from the last checkpoint after interruption; state is persisted after each step, no duplicate API calls
|
|
146
|
-
- **Task management** — Create, view, resume, and stop tasks from the Web UI
|
|
147
|
-
- **Real-time progress** — WebSocket pushes per-step generation progress (step name, status, percentage, current/total)
|
|
148
|
-
- **Built-in CJK fonts** — Project ships with Chinese fonts, no garbled characters in subtitle rendering
|
|
149
|
-
|
|
150
|
-
### 🤖 AI Agent Friendly
|
|
151
|
-
|
|
152
|
-
Designed specifically for AI coding assistants (Claude, Cursor, QoderWork, etc.), with a complete `AGENTS.md` deployment guide. AI Agents can automatically:
|
|
153
|
-
|
|
154
|
-
- Check environment (Python 3.10+, ffmpeg)
|
|
155
|
-
- Install dependencies and start the server
|
|
156
|
-
- Configure API key
|
|
157
|
-
- Run 4-layer deployment verification (connectivity → static analysis → endpoint testing → subtitle feature)
|
|
158
|
-
- Execute 10-scenario regression test suite
|
|
159
|
-
|
|
160
|
-
### 🌐 Multilingual Web UI
|
|
161
|
-
|
|
162
|
-
One-click launch, operate entirely in the browser. Interface available in **13 languages**: 中文, English, Deutsch, Français, Nederlands, Español, Português, Italiano, Русский, 日本語, 한국어, Bahasa Melayu, Bahasa Indonesia.
|
|
163
|
-
|
|
164
|
-
## 🚀 Quick Start
|
|
165
|
-
|
|
166
|
-
### Prerequisites
|
|
167
|
-
|
|
168
|
-
- Python 3.10+
|
|
169
|
-
- ffmpeg — required only for **manual setup** (Option A); the Docker (Option B) and npm (Option C) options bundle ffmpeg automatically, so no system ffmpeg install is needed there.
|
|
170
|
-
|
|
171
|
-
That's it. No GPU, no large RAM, a regular laptop is all you need.
|
|
172
|
-
|
|
173
|
-
### Option A: Manual Setup
|
|
174
|
-
|
|
175
|
-
**Step 1 — Clone & Launch**
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
git clone https://github.com/lcy362/agnes-video-generator.git
|
|
179
|
-
cd agnes-video-generator
|
|
180
|
-
./start.sh
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
The script automatically creates a virtual environment, installs dependencies, and opens `http://localhost:8765` in your browser. You can also start manually:
|
|
184
|
-
|
|
185
|
-
```bash
|
|
186
|
-
python3 -m venv .venv
|
|
187
|
-
.venv/bin/pip install -r requirements.txt
|
|
188
|
-
.venv/bin/python server.py
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**Step 2 — Configure API Key**
|
|
192
|
-
|
|
193
|
-
Get a free API key from [Agnes AI](https://platform.agnes-ai.com), then choose one of two ways:
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
# Way 1: Environment variable
|
|
197
|
-
export AGNES_API_KEY="your-api-key"
|
|
198
|
-
|
|
199
|
-
# Way 2: Via API (same as entering it in the Web UI)
|
|
200
|
-
curl -X POST http://localhost:8765/api/config \
|
|
201
|
-
-H "Content-Type: application/json" \
|
|
202
|
-
-d '{"api_key": "your-api-key"}'
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
**Step 3 — Create Your First Video**
|
|
206
|
-
|
|
207
|
-
Open `http://localhost:8765`, choose a video mode (Simple / Creative / Manuscript / Anchor), enter your idea, and click "Start Generating".
|
|
208
|
-
|
|
209
|
-
### Option B: Docker (No Python/FFmpeg Required)
|
|
210
|
-
|
|
211
|
-
Pre-built multi-arch images (`linux/amd64`, `linux/arm64`) are published to both **GitHub Container Registry (GHCR)** and **Docker Hub** on every release.
|
|
212
|
-
|
|
213
|
-
**Pull & Run**
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
# GHCR
|
|
217
|
-
docker run -d -p 8765:8765 \
|
|
218
|
-
-e AGNES_API_KEY=<your-key> \
|
|
219
|
-
-v ~/agnes-data/working:/app/.working_dir \
|
|
220
|
-
-v ~/agnes-data/config:/app/.agnes_config \
|
|
221
|
-
ghcr.io/lcy362/free-short-video:latest
|
|
222
|
-
|
|
223
|
-
# Docker Hub
|
|
224
|
-
docker run -d -p 8765:8765 \
|
|
225
|
-
-e AGNES_API_KEY=<your-key> \
|
|
226
|
-
-v ~/agnes-data/working:/app/.working_dir \
|
|
227
|
-
-v ~/agnes-data/config:/app/.agnes_config \
|
|
228
|
-
lcy362/free-short-video:latest
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Then open `http://localhost:8765`.
|
|
232
|
-
|
|
233
|
-
**Data Persistence:** The app writes videos, uploads, and settings inside the container (`/app/.working_dir`, `/app/.agnes_config`). Mount them to your host so outputs survive container recreation and are accessible from your local filesystem. Your generated videos will be at `~/agnes-data/working/` on your machine.
|
|
234
|
-
|
|
235
|
-
Or use `docker compose` with the included `docker-compose.yml`:
|
|
236
|
-
|
|
237
|
-
```bash
|
|
238
|
-
git clone https://github.com/lcy362/agnes-video-generator.git
|
|
239
|
-
cd agnes-video-generator
|
|
240
|
-
AGNES_API_KEY=<your-key> docker compose up -d
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Option C: npm (One Command)
|
|
244
|
-
|
|
245
|
-
If you have **Node.js 18+** and **Python 3.10+** installed, the whole service ships as an npm package — no cloning, no manual venv:
|
|
246
|
-
|
|
247
|
-
```bash
|
|
248
|
-
# Run directly without installing
|
|
249
|
-
npx free-short-video
|
|
250
|
-
|
|
251
|
-
# Or install globally, then run
|
|
252
|
-
npm install -g free-short-video
|
|
253
|
-
free-short-video # short alias: fsv
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
On first run the launcher automatically creates a local virtual environment, installs Python dependencies, wires up a bundled `ffmpeg` (via `imageio-ffmpeg`, so no system ffmpeg needed), starts the server on `http://localhost:8765`, and opens your browser. Pass your key through the environment or set it later in the Web UI:
|
|
257
|
-
|
|
258
|
-
```bash
|
|
259
|
-
AGNES_API_KEY=<your-key> npx free-short-video
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Options: `--port <n>`, `--host <h>` (use `0.0.0.0` for LAN access), `--no-open`.
|
|
263
|
-
|
|
264
|
-
#### ffmpeg: bundled by default, or install your own
|
|
265
|
-
|
|
266
|
-
With the npm package you normally **don't need to install ffmpeg yourself** — the launcher (`bin/cli.js`) automatically installs `imageio-ffmpeg` (a static ffmpeg binary, now an explicit dependency in `requirements.txt`) into the local venv and prepends its directory to `PATH`, so every `ffmpeg` call inside the Python service resolves to the bundled binary. This works out of the box on macOS, Linux, and Windows.
|
|
267
|
-
|
|
268
|
-
**If you prefer to install ffmpeg on your system** (recommended for production / maximum stability — your system ffmpeg takes precedence over the bundled one because it appears earlier on `PATH`):
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
|
-
# macOS
|
|
272
|
-
brew install ffmpeg
|
|
273
|
-
|
|
274
|
-
# Ubuntu / Debian
|
|
275
|
-
sudo apt update && sudo apt install ffmpeg
|
|
276
|
-
|
|
277
|
-
# CentOS / RHEL (requires RPM Fusion)
|
|
278
|
-
sudo dnf install ffmpeg
|
|
279
|
-
|
|
280
|
-
# Windows (Chocolatey)
|
|
281
|
-
choco install ffmpeg
|
|
282
|
-
|
|
283
|
-
# Windows (Scoop)
|
|
284
|
-
scoop install ffmpeg
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
Or download a build from <https://ffmpeg.org/download.html> and add it to your `PATH`. Verify with:
|
|
288
|
-
|
|
289
|
-
```bash
|
|
290
|
-
ffmpeg -version
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
**Risks if you do NOT install a system ffmpeg (i.e. rely solely on the bundled `imageio-ffmpeg`):**
|
|
294
|
-
|
|
295
|
-
- **Platform / architecture support** — `imageio-ffmpeg` ships pre-built binaries only for common platforms (macOS x86_64/arm64, Linux x86_64/arm64, Windows x64). On niche or very old architectures a matching wheel may not exist, and the bundled binary would be missing.
|
|
296
|
-
- **Single source of truth** — all ffmpeg capability comes from that one static binary. If its install/extract fails (disk permissions, corruption), the failure only surfaces **when you generate a video**, not at server startup — the error is a low-level `FileNotFoundError: 'ffmpeg'`, which is harder to diagnose than a startup check.
|
|
297
|
-
- **Pinned version** — the bundled ffmpeg is locked to whatever version `imageio-ffmpeg` ships (e.g. ffmpeg 7.1); you can't easily upgrade it on your own.
|
|
298
|
-
- **Mitigation** — for production or stability-critical use, install a system ffmpeg as shown above; the bundled one then acts only as a fallback.
|
|
299
|
-
|
|
300
|
-
### Option D: AI Agent Assisted Setup
|
|
301
|
-
|
|
302
|
-
This project is designed for AI coding assistants. First, download the code and prepare your API key:
|
|
303
|
-
|
|
304
|
-
```bash
|
|
305
|
-
git clone https://github.com/lcy362/agnes-video-generator.git
|
|
306
|
-
cd agnes-video-generator
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
Then tell your agent:
|
|
310
|
-
|
|
311
|
-
> "Read the AGENTS.md in this project, install dependencies, configure the API key `<your-key>`, and start the server."
|
|
312
|
-
|
|
313
|
-
The agent will read `AGENTS.md` (a comprehensive deployment guide) and handle: environment checks (Python 3.10+, ffmpeg), `pip install`, server launch, and API key configuration. After startup, you can also ask the agent to verify the deployment:
|
|
314
|
-
|
|
315
|
-
> "Run the deployment verification checks."
|
|
316
|
-
|
|
317
|
-
The agent will execute the 4-layer checklist from `AGENTS.md` (connectivity → static analysis → endpoint testing → subtitle feature) and report results.
|
|
318
|
-
|
|
319
|
-
## 📖 Usage
|
|
320
|
-
|
|
321
|
-
### 1. Configure API Key
|
|
322
|
-
|
|
323
|
-
Enter your free [Agnes AI](https://platform.agnes-ai.com) API key at the top of the page and save it. Or set it via environment variable:
|
|
324
|
-
|
|
325
|
-
```bash
|
|
326
|
-
export AGNES_API_KEY="your-api-key"
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### 2. Choose a Video Mode
|
|
330
|
-
|
|
331
|
-
#### Simple Video
|
|
332
|
-
|
|
333
|
-
Quick single-clip generation with full parameter control:
|
|
334
|
-
|
|
335
|
-
| Field | Description |
|
|
336
|
-
|-------|-------------|
|
|
337
|
-
| Prompt | Describe the AI video scene in natural language |
|
|
338
|
-
| Generation Mode | Text-to-Video / Image-to-Video / Text+Image / Keyframes |
|
|
339
|
-
| Resolution | Portrait 9:16 / Landscape 16:9 / Square 1:1 |
|
|
340
|
-
| Duration | 5s / 10s / 15s / 18s / 20s |
|
|
341
|
-
| Reference Image | Optional upload for image-to-video modes |
|
|
342
|
-
| End Frame Image | Optional end frame for keyframes mode |
|
|
343
|
-
|
|
344
|
-
#### Creative Video
|
|
345
|
-
|
|
346
|
-
AI-driven multi-scene storytelling:
|
|
347
|
-
|
|
348
|
-
| Field | Description | Required |
|
|
349
|
-
|-------|-------------|----------|
|
|
350
|
-
| Idea | Describe your AI video concept | Yes |
|
|
351
|
-
| User Requirements | Scene count, duration, and other constraints | - |
|
|
352
|
-
| Visual Style | Cinematic realism, anime, cyberpunk, etc. | - |
|
|
353
|
-
| Chaining Mode | keyframes (recommended) / ti2vid / none | - |
|
|
354
|
-
| Narration | Enable/disable TTS narration, choose voice and speed | - |
|
|
355
|
-
| Subtitle Style | Font, color, size, position, stroke, background | - |
|
|
356
|
-
| Reference Image | Optional character reference for visual consistency | - |
|
|
357
|
-
| End Frames | Custom or auto-generated per-scene end frames | - |
|
|
358
|
-
|
|
359
|
-
#### Manuscript Video
|
|
360
|
-
|
|
361
|
-
Long-form text to narrated video:
|
|
362
|
-
|
|
363
|
-
| Field | Description | Required |
|
|
364
|
-
|-------|-------------|----------|
|
|
365
|
-
| Manuscript Text | Paste your full article, script, or narration | Yes |
|
|
366
|
-
| Resolution | Portrait / Landscape / Square | - |
|
|
367
|
-
| Narration | Voice role and speech rate | - |
|
|
368
|
-
| Subtitle Style | Full subtitle customization | - |
|
|
369
|
-
|
|
370
|
-
> **Note**: Segment duration is auto-calculated based on text length (~4 chars/sec, 5–12s per segment) — no manual setting needed.
|
|
371
|
-
|
|
372
|
-
#### Digital Anchor
|
|
373
|
-
|
|
374
|
-
| Field | Description | Required |
|
|
375
|
-
|-------|-------------|----------|
|
|
376
|
-
| Anchor Script | Enter the text the anchor will say | Yes |
|
|
377
|
-
| Anchor Image | AI-generated or upload custom reference image | - |
|
|
378
|
-
| Resolution | Portrait / Landscape / Square | - |
|
|
379
|
-
| Narration | Voice role and speech rate | - |
|
|
380
|
-
| Subtitle Style | Full subtitle customization | - |
|
|
381
|
-
|
|
382
|
-
### 3. Click "Start Generating"
|
|
383
|
-
|
|
384
|
-
The progress panel shows real-time generation status for each step. For Creative Video: Init → Image Analysis → Story → Character Reference → Script → Narration → End Frame Prompts → End Frame Generation → Video Generation → Audio & Subtitles → Concatenation.
|
|
385
|
-
|
|
386
|
-
### 4. Checkpoint Resume & Task Management
|
|
387
|
-
|
|
388
|
-
If the server is interrupted, restart it and find the incomplete task in the "Task List" tab. Click "Resume" to continue from the last checkpoint. Running tasks can also be stopped and resumed later.
|
|
389
|
-
|
|
390
|
-
## 🏗️ Project Structure
|
|
391
|
-
|
|
392
|
-
```
|
|
393
|
-
agnes-video-generator/
|
|
394
|
-
├── start.sh # One-click launch script
|
|
395
|
-
├── requirements.txt # Python dependencies
|
|
396
|
-
├── Dockerfile # Multi-arch Docker image (Python 3.11 + ffmpeg via imageio)
|
|
397
|
-
├── docker-compose.yml # Docker Compose with persisted volumes
|
|
398
|
-
├── docker-run.sh # One-command Docker launch (wrapper with bind mounts)
|
|
399
|
-
├── server.py # FastAPI server (REST + WebSocket)
|
|
400
|
-
├── static/
|
|
401
|
-
│ └── index.html # Frontend SPA — 5 task tabs, 13 languages (Tailwind CSS)
|
|
402
|
-
├── core/
|
|
403
|
-
│ ├── config.py # API key, font resolution, default configs
|
|
404
|
-
│ ├── screenwriter.py # Screenwriter Agent (LLM-powered story/script/narration)
|
|
405
|
-
│ ├── task_manager.py # Task state persistence & checkpoint resume
|
|
406
|
-
│ ├── api/
|
|
407
|
-
│ │ ├── agnes_chat.py # LLM Chat API (agnes-2.0-flash)
|
|
408
|
-
│ │ ├── agnes_image.py # Image generation API (agnes-image-2.1-flash / 2.0-flash)
|
|
409
|
-
│ │ ├── agnes_video.py # Video generation API (agnes-video-v2.0)
|
|
410
|
-
│ │ └── rate_limiter.py # Global token bucket rate limiter (16 requests/min)
|
|
411
|
-
│ ├── audio/
|
|
412
|
-
│ │ ├── tts.py # Edge TTS engine + silent fallback engine
|
|
413
|
-
│ │ └── subtitle.py # SRT generation (fine-grained word-level) + overlay
|
|
414
|
-
│ ├── compositor/
|
|
415
|
-
│ │ ├── concatenator.py # Video concatenation + audio/subtitle overlay
|
|
416
|
-
│ │ └── processor.py # Video resize, frame extraction, freeze, silence gen
|
|
417
|
-
│ └── pipelines/
|
|
418
|
-
│ ├── simple_video.py # Pipeline: Simple Video
|
|
419
|
-
│ ├── creative_video.py # Pipeline: Creative Video (10-step)
|
|
420
|
-
│ ├── manuscript_video.py # Pipeline: Manuscript Video (5-step)
|
|
421
|
-
│ └── anchor_video.py # Pipeline: Digital Anchor
|
|
422
|
-
├── models/
|
|
423
|
-
│ └── task.py # Data models (5 task types, configs, requests)
|
|
424
|
-
├── resource/
|
|
425
|
-
│ └── fonts/ # Built-in CJK fonts for subtitle rendering
|
|
426
|
-
├── utils/
|
|
427
|
-
│ ├── image.py # Image download / base64 conversion
|
|
428
|
-
│ └── video.py # Video download
|
|
429
|
-
├── scripts/
|
|
430
|
-
│ └── regression_runner.py # 10-scenario regression test suite
|
|
431
|
-
└── docs/
|
|
432
|
-
├── regression_test_plan.md # Regression test plan
|
|
433
|
-
├── plans-v1.0/ # v1.0 design & planning docs
|
|
434
|
-
├── plans-v2.0/ # v2.0 review & optimization docs
|
|
435
|
-
└── plans-v3.0/ # v3.0 feature planning docs
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
## 🔧 Tech Stack
|
|
439
|
-
|
|
440
|
-
| Layer | Choice | Notes |
|
|
441
|
-
|-------|--------|-------|
|
|
442
|
-
| Backend | Python FastAPI | Async + WebSocket |
|
|
443
|
-
| Frontend | HTML/CSS/JS + Tailwind CSS CDN | Zero build steps, single-file SPA |
|
|
444
|
-
| LLM | Agnes Chat (`agnes-2.0-flash`) | Free — story, script, narration generation |
|
|
445
|
-
| Image AI | `agnes-image-2.1-flash` (t2i) / `agnes-image-2.0-flash` (i2i) | Free — reference images, end frames, standalone image generation |
|
|
446
|
-
| Video AI | `agnes-video-v2.0` | Free — text-to-video, image-to-video, keyframes |
|
|
447
|
-
| TTS | Edge TTS (Microsoft) | Free — 4 Chinese voices, no extra API key needed |
|
|
448
|
-
| Subtitles | moviepy + srt | Fine-grained word-level SRT, multi-line wrapping |
|
|
449
|
-
| Video Processing | moviepy + ffmpeg | Concatenation, subtitle overlay, audio mixing |
|
|
450
|
-
|
|
451
|
-
## 🎬 Three AI Video Chaining Modes
|
|
452
|
-
|
|
453
|
-
| Mode | How It Works | Best For |
|
|
454
|
-
|------|-------------|----------|
|
|
455
|
-
| **keyframes** | Specify first + last frame per scene; server auto-interpolates transitions | Smooth transitions (recommended) |
|
|
456
|
-
| **ti2vid** | Last frame of previous scene → img2img transition → first frame of next scene | Visual continuity between scenes |
|
|
457
|
-
| **none** | All scenes share the same reference image, independent of each other | Fast output, independent scenes |
|
|
458
|
-
|
|
459
|
-
## 📋 API Endpoints
|
|
460
|
-
|
|
461
|
-
| Method | Path | Description |
|
|
462
|
-
|--------|------|-------------|
|
|
463
|
-
| GET | `/` | Serve Web UI |
|
|
464
|
-
| GET | `/api/config` | Get API key (masked) |
|
|
465
|
-
| POST | `/api/config` | Save API key |
|
|
466
|
-
| DELETE | `/api/config` | Delete API key |
|
|
467
|
-
| GET | `/api/voices` | List available TTS voices |
|
|
468
|
-
| POST | `/api/image/generate` | Image generation |
|
|
469
|
-
| GET | `/api/image/{task_id}` | Query image task status |
|
|
470
|
-
| POST | `/api/tasks/simple` | Create simple video task |
|
|
471
|
-
| POST | `/api/tasks/creative` | Create creative video task |
|
|
472
|
-
| POST | `/api/tasks/manuscript` | Create manuscript video task |
|
|
473
|
-
| POST | `/api/tasks/anchor` | Create digital anchor task |
|
|
474
|
-
| POST | `/api/tasks` | Generic task creation (backward-compatible) |
|
|
475
|
-
| GET | `/api/tasks` | List all tasks (with type badges) |
|
|
476
|
-
| GET | `/api/tasks/{id}` | Get task details |
|
|
477
|
-
| POST | `/api/tasks/{id}/resume` | Resume an interrupted task |
|
|
478
|
-
| POST | `/api/tasks/{id}/stop` | Stop a running task |
|
|
479
|
-
| GET | `/api/video/{id}` | Download/stream final video |
|
|
480
|
-
| WS | `/ws/{id}` | WebSocket real-time progress |
|
|
481
|
-
|
|
482
|
-
## ⚠️ Important Notes
|
|
483
|
-
|
|
484
|
-
This project is in early stage — corner cases may not be fully handled. Recommended workflow:
|
|
485
|
-
|
|
486
|
-
1. Fill in your idea on the page and submit an AI video task
|
|
487
|
-
2. Watch the **console logs** (the terminal running `server.py`) and be patient
|
|
488
|
-
3. All key operations are logged for easy debugging
|
|
489
|
-
|
|
490
|
-
### Log Reference
|
|
491
|
-
|
|
492
|
-
All important operations are logged to the server console:
|
|
493
|
-
|
|
494
|
-
| Prefix | Module |
|
|
495
|
-
|--------|--------|
|
|
496
|
-
| `[Startup]` | Server startup, stale task reset |
|
|
497
|
-
| `[WS]` | WebSocket connect/disconnect |
|
|
498
|
-
| `[Resume]` / `[Stop]` | Task resume/stop |
|
|
499
|
-
| `[Pipeline]` / `[Simple]` / `[Manuscript]` | Pipeline step execution |
|
|
500
|
-
| `[TTS]` / `[Subtitle]` | Audio and subtitle generation |
|
|
501
|
-
| `[Compositor]` | Video concatenation and processing |
|
|
502
|
-
| `[AgnesImage]` / `[AgnesVideo]` / `[AgnesChat]` | AI API calls |
|
|
503
|
-
| `[RateLimiter]` | Global rate limiter |
|
|
504
|
-
| `[TaskManager]` | Task state persistence |
|
|
505
|
-
| `[Screenwriter]` | Screenwriter Agent |
|
|
506
|
-
|
|
507
|
-
### Output Directory
|
|
508
|
-
|
|
509
|
-
All AI video task artifacts are stored under `.working_dir/{timestamp}_{task_id}/`:
|
|
510
|
-
|
|
511
|
-
```
|
|
512
|
-
.working_dir/{timestamp}_{task_id}/
|
|
513
|
-
├── task_state.json # Task state (required for checkpoint resume)
|
|
514
|
-
├── final_video.mp4 # Final video with narration + subtitles
|
|
515
|
-
├── story.txt # AI-generated story (creative mode)
|
|
516
|
-
├── script.json # Scene script (JSON format)
|
|
517
|
-
├── narration.mp3 # Combined TTS narration audio
|
|
518
|
-
├── narration.srt # Combined subtitle file
|
|
519
|
-
├── scene_0/
|
|
520
|
-
│ ├── video.mp4 # Scene 0 AI video
|
|
521
|
-
│ ├── end_frame.png # Scene 0 end frame
|
|
522
|
-
│ └── task.json # Video generation task ID
|
|
523
|
-
├── scene_1/
|
|
524
|
-
│ └── ...
|
|
525
|
-
└── scene_2/
|
|
526
|
-
└── ...
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
## 🙏 Acknowledgments
|
|
530
|
-
|
|
531
|
-
This project is built upon the following open-source projects:
|
|
532
|
-
|
|
533
|
-
- [ViMax](https://github.com/HKUDS/ViMax) — AI video generation framework by HKU Data Science Lab
|
|
534
|
-
- [vimax-agnes](https://github.com/easyeye163/vimax-agnes) — Agnes AI adaptation based on ViMax
|
|
535
|
-
|
|
536
|
-
Special thanks to [Agnes AI](https://platform.agnes-ai.com) for providing **completely free**, high-quality AI model APIs (text, image, and video generation) — this project runs at absolute zero cost thanks to their generosity.
|
|
537
|
-
|
|
538
|
-
## 📄 License
|
|
539
|
-
|
|
540
|
-
MIT
|
|
541
|
-
|
|
542
|
-
---
|
|
543
|
-
|
|
544
|
-
## ❓ FAQ
|
|
545
|
-
|
|
546
|
-
### Is Agnes Video Generator really free? Are there any hidden costs?
|
|
547
|
-
|
|
548
|
-
Yes, it is **completely free**. All AI model calls (Agnes Chat, Agnes Image, Agnes Video) are free of charge with no trial period, no watermarks, and no usage limits. The only TTS integration (Microsoft Edge TTS) is also free and requires no extra API key. You only need a free API key from [Agnes AI](https://platform.agnes-ai.com) to get started.
|
|
549
|
-
|
|
550
|
-
### Do I need a GPU to run this AI video generator?
|
|
551
|
-
|
|
552
|
-
No. All AI compute runs in the cloud via Agnes AI's free API. You just need a regular laptop or desktop computer that can run Python 3.10+ and ffmpeg. No GPU, no high RAM, no special hardware required.
|
|
553
|
-
|
|
554
|
-
### How is this different from Runway, Pika, or Sora?
|
|
555
|
-
|
|
556
|
-
Unlike commercial AI video tools that charge $10–$95/month, Agnes Video Generator is completely free and open-source (MIT). It offers built-in multi-scene pipelines, AI narration, auto subtitles, and digital anchor — features that require third-party tools or manual editing elsewhere. See the [comparison table](#comparison-agnes-vs-commercial-ai-video-tools) above for details.
|
|
557
|
-
|
|
558
|
-
### What video generation modes are supported?
|
|
559
|
-
|
|
560
|
-
Four modes: **Simple Video** (single prompt, full parameter control), **Creative Video** (AI story → multi-scene video with narration), **Manuscript Video** (long text → auto-split → narrated video), and **Digital Anchor** (AI anchor with TTS). Additional options include text-to-video, image-to-video, keyframes animation, and image-to-image end frame generation.
|
|
561
|
-
|
|
562
|
-
### Can I use my own images as references?
|
|
563
|
-
|
|
564
|
-
Yes. You can upload reference images for character or scene consistency across scenes, use custom end frames for precise visual transitions, or choose img2img to auto-generate end frames from your reference. Reference images are supported in both Creative Video and Digital Anchor modes.
|
|
565
|
-
|
|
566
|
-
### What languages does the UI support?
|
|
567
|
-
|
|
568
|
-
The Web UI supports 13 languages: 中文, English, Deutsch, Français, Nederlands, Español, Português, Italiano, Русский, 日本語, 한국어, Bahasa Melayu, and Bahasa Indonesia. Subtitles are generated in the source text language with CJK font support built-in.
|
|
569
|
-
|
|
570
|
-
### Can I run this with Docker?
|
|
571
|
-
|
|
572
|
-
Yes. Pre-built images are published to both [GHCR](https://github.com/lcy362/agnes-video-generator/pkgs/container/free-short-video) and [Docker Hub](https://hub.docker.com/r/lcy362/free-short-video). Just pull the `latest` tag and run — no Python or ffmpeg installation needed. See **[Option B: Docker](#option-b-docker-no-pythonffmpeg-required)** in Quick Start for the full command and volume mount instructions.
|
|
573
|
-
|
|
574
|
-
### Can I host this on my own server?
|
|
575
|
-
|
|
576
|
-
Absolutely. The project is designed for self-hosting. Just clone the repo, run `./start.sh`, and the server starts on `http://localhost:8765`. No external dependencies, no cloud lock-in. See the [Quick Start](#🚀-quick-start) section above.
|
|
577
|
-
|
|
578
|
-
### How do I get help or report issues?
|
|
579
|
-
|
|
580
|
-
Check the [GitHub Issues](https://github.com/lcy362/agnes-video-generator/issues) page for existing reports or open a new one. The project also includes a comprehensive `AGENTS.md` for AI-agent-assisted debugging. For feature requests, bug reports, or questions, the Issues page is the best place.
|
|
103
|
+
- **[Features](docs/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.
|
|
104
|
+
- **[Getting Started](docs/getting-started.md)** — Install and deploy in 4 ways: Manual (`start.sh`), Docker, npm (`npx free-short-video`), or AI-Agent assisted.
|
|
105
|
+
- **[Usage Guide](docs/usage.md)** — Configure your API key, pick a video mode, resume from checkpoints, the three chaining modes, and logs & output layout.
|
|
106
|
+
- **[Architecture](docs/architecture.md)** — Project structure and tech stack.
|
|
107
|
+
- **[API Reference](docs/api.md)** — Full REST + WebSocket endpoint list.
|
|
108
|
+
- **[FAQ](docs/faq.md)** — Frequently asked questions.
|
|
109
|
+
- **[About & License](docs/about.md)** — Acknowledgments and the MIT license.
|
|
581
110
|
|
|
582
111
|
**Keywords**: free AI video generator, AI video generation tool, text to video AI, free AI video maker, AI video creator, open source video generator, Agnes AI, text-to-video, image-to-video, keyframes video, AI narration, auto subtitles, multi-scene video, zero cost AI video, no subscription AI video tool, digital anchor, self-hosted AI video generator, open source alternative to Runway
|
package/core/api/agnes_chat.py
CHANGED
|
@@ -17,11 +17,10 @@ import requests
|
|
|
17
17
|
|
|
18
18
|
from core.api.error_collector import collect_error, collect_error_from_exception
|
|
19
19
|
from core.api.rate_limiter import get_rate_limiter
|
|
20
|
+
from core.config import get_agnes_base_url
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
|
-
BASE_URL = "https://apihub.agnes-ai.com/v1"
|
|
24
|
-
|
|
25
24
|
# 重试配置
|
|
26
25
|
_MAX_RETRIES = 3
|
|
27
26
|
_RETRY_BASE_DELAY = 15 # 秒,指数退避基数
|
|
@@ -111,7 +110,7 @@ class AgnesChatAPI:
|
|
|
111
110
|
try:
|
|
112
111
|
get_rate_limiter().acquire()
|
|
113
112
|
resp = requests.post(
|
|
114
|
-
f"{
|
|
113
|
+
f"{get_agnes_base_url()}/chat/completions",
|
|
115
114
|
headers=self.headers,
|
|
116
115
|
json=payload,
|
|
117
116
|
timeout=timeout,
|
package/core/api/agnes_image.py
CHANGED
|
@@ -12,12 +12,11 @@ import requests
|
|
|
12
12
|
|
|
13
13
|
from core.api.error_collector import collect_error, collect_error_from_exception
|
|
14
14
|
from core.api.rate_limiter import get_rate_limiter
|
|
15
|
+
from core.config import get_agnes_base_url
|
|
15
16
|
from utils.image import download_image
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
19
|
-
BASE_URL = "https://apihub.agnes-ai.com/v1"
|
|
20
|
-
|
|
21
20
|
|
|
22
21
|
class ImageOutput:
|
|
23
22
|
def __init__(self, fmt: str, ext: str, data: str):
|
|
@@ -119,7 +118,7 @@ class AgnesImageAPI:
|
|
|
119
118
|
read_timeout = 60 * (attempt + 1)
|
|
120
119
|
resp = await asyncio.to_thread(
|
|
121
120
|
requests.post,
|
|
122
|
-
f"{
|
|
121
|
+
f"{get_agnes_base_url()}/images/generations",
|
|
123
122
|
headers=self.headers,
|
|
124
123
|
json=payload,
|
|
125
124
|
timeout=(30, read_timeout),
|
package/core/api/agnes_models.py
CHANGED
|
@@ -8,19 +8,15 @@ import logging
|
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
|
-
from core.api.agnes_chat import BASE_URL
|
|
12
11
|
from core.config import (
|
|
13
12
|
DEFAULT_TEXT_MODEL,
|
|
14
13
|
DEFAULT_IMAGE_MODEL,
|
|
15
14
|
DEFAULT_VIDEO_MODEL,
|
|
15
|
+
get_agnes_base_url,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
|
-
# 带 beta 开关的模型列表端点:裸 /v1/models 会被网关过滤掉内测模型,
|
|
21
|
-
# 加 ?all=true(等价 X-Beta-Access: true / ?include_beta=true)才会返回全量(含 2.5-flash)。
|
|
22
|
-
MODELS_ENDPOINT = f"{BASE_URL}/models?all=true"
|
|
23
|
-
|
|
24
20
|
REQUEST_TIMEOUT = 20
|
|
25
21
|
|
|
26
22
|
# 分组失败时的兜底列表
|
|
@@ -56,8 +52,9 @@ def fetch_available_models(api_key: str) -> dict:
|
|
|
56
52
|
接口失败(网络/鉴权/非 200)时返回硬编码兜底列表。
|
|
57
53
|
"""
|
|
58
54
|
try:
|
|
55
|
+
endpoint = f"{get_agnes_base_url()}/models?all=true"
|
|
59
56
|
resp = requests.get(
|
|
60
|
-
|
|
57
|
+
endpoint,
|
|
61
58
|
headers={"Authorization": f"Bearer {api_key}"},
|
|
62
59
|
timeout=REQUEST_TIMEOUT,
|
|
63
60
|
)
|
package/core/api/agnes_video.py
CHANGED
|
@@ -13,13 +13,11 @@ import requests
|
|
|
13
13
|
|
|
14
14
|
from core.api.error_collector import collect_error, collect_error_from_exception
|
|
15
15
|
from core.api.rate_limiter import get_rate_limiter
|
|
16
|
+
from core.config import get_agnes_base_url, get_agnes_api_root
|
|
16
17
|
from utils.video import download_video
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
19
20
|
|
|
20
|
-
BASE_URL = "https://apihub.agnes-ai.com/v1"
|
|
21
|
-
API_ROOT = "https://apihub.agnes-ai.com"
|
|
22
|
-
|
|
23
21
|
DURATION_PRESETS = {
|
|
24
22
|
5: (121, 24),
|
|
25
23
|
10: (241, 24),
|
|
@@ -82,7 +80,7 @@ class AgnesVideoAPI:
|
|
|
82
80
|
_URL_CACHE_MAX_AGE = 3600 # 1 小时
|
|
83
81
|
if os.path.exists(url_file):
|
|
84
82
|
try:
|
|
85
|
-
with open(url_file, "r") as f:
|
|
83
|
+
with open(url_file, "r", encoding="utf-8") as f:
|
|
86
84
|
cache_data = json.loads(f.read())
|
|
87
85
|
cached_url = cache_data.get("url", "")
|
|
88
86
|
cached_ts = cache_data.get("ts", 0)
|
|
@@ -107,7 +105,7 @@ class AgnesVideoAPI:
|
|
|
107
105
|
try:
|
|
108
106
|
cache_data = {"url": url, "ts": time.time()}
|
|
109
107
|
tmp_file = url_file + ".tmp"
|
|
110
|
-
with open(tmp_file, "w") as f:
|
|
108
|
+
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
111
109
|
json.dump(cache_data, f)
|
|
112
110
|
f.flush()
|
|
113
111
|
os.fsync(f.fileno())
|
|
@@ -140,7 +138,7 @@ class AgnesVideoAPI:
|
|
|
140
138
|
await asyncio.to_thread(get_rate_limiter().acquire)
|
|
141
139
|
resp = await asyncio.to_thread(
|
|
142
140
|
requests.post,
|
|
143
|
-
f"{
|
|
141
|
+
f"{get_agnes_base_url()}/images/generations",
|
|
144
142
|
headers=self.headers,
|
|
145
143
|
json=payload,
|
|
146
144
|
timeout=(30, 120),
|
|
@@ -213,7 +211,7 @@ class AgnesVideoAPI:
|
|
|
213
211
|
start_time = asyncio.get_event_loop().time()
|
|
214
212
|
curl_cmd = (
|
|
215
213
|
f'curl -s -H "Authorization: Bearer $AGNES_API_KEY" '
|
|
216
|
-
f'"{
|
|
214
|
+
f'"{get_agnes_api_root()}/agnesapi?video_id={video_id}"'
|
|
217
215
|
)
|
|
218
216
|
while True:
|
|
219
217
|
# M2: 每次轮询前检查停止信号
|
|
@@ -244,7 +242,7 @@ class AgnesVideoAPI:
|
|
|
244
242
|
resp = await asyncio.wait_for(
|
|
245
243
|
asyncio.to_thread(
|
|
246
244
|
requests.get,
|
|
247
|
-
f"{
|
|
245
|
+
f"{get_agnes_api_root()}/agnesapi?video_id={video_id}",
|
|
248
246
|
headers=self.headers,
|
|
249
247
|
timeout=15,
|
|
250
248
|
),
|
|
@@ -319,7 +317,7 @@ class AgnesVideoAPI:
|
|
|
319
317
|
resp = await asyncio.wait_for(
|
|
320
318
|
asyncio.to_thread(
|
|
321
319
|
requests.post,
|
|
322
|
-
f"{
|
|
320
|
+
f"{get_agnes_base_url()}/videos",
|
|
323
321
|
headers=self.headers,
|
|
324
322
|
json=payload,
|
|
325
323
|
timeout=(15, 60),
|
|
@@ -534,7 +534,7 @@ class VideoConcatenator:
|
|
|
534
534
|
# Step 3: Build concat file list for ffmpeg
|
|
535
535
|
loop_dir = os.path.dirname(output_path)
|
|
536
536
|
concat_file = os.path.join(loop_dir, "_anchor_concat.txt")
|
|
537
|
-
with open(concat_file, "w") as f:
|
|
537
|
+
with open(concat_file, "w", encoding="utf-8") as f:
|
|
538
538
|
for _ in range(n):
|
|
539
539
|
f.write(f"file '{clip_path}'\n")
|
|
540
540
|
|
package/core/config.py
CHANGED
|
@@ -472,3 +472,50 @@ def set_selected_models(text: str = None, image: str = None, video: str = None)
|
|
|
472
472
|
config["models"] = m
|
|
473
473
|
save_config(config)
|
|
474
474
|
return get_selected_models()
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# ═══════════════════════════════════════════════════
|
|
478
|
+
# Agnes API 域名配置(v6.0)
|
|
479
|
+
# ═══════════════════════════════════════════════════
|
|
480
|
+
|
|
481
|
+
# 可用域名映射
|
|
482
|
+
AGNES_DOMAIN_MAP = {
|
|
483
|
+
"com": "https://apihub.agnes-ai.com",
|
|
484
|
+
"cn": "https://apihub.agnes-ai.cn",
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
_DEFAULT_DOMAIN = "com"
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def get_agnes_domain() -> str:
|
|
491
|
+
"""返回当前配置的域名后缀。
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
"com" 或 "cn"
|
|
495
|
+
"""
|
|
496
|
+
config = load_config()
|
|
497
|
+
return config.get("agnes_domain", _DEFAULT_DOMAIN)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def set_agnes_domain(domain: str):
|
|
501
|
+
"""设置 Agnes API 域名后缀。
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
domain: "com" 或 "cn"
|
|
505
|
+
"""
|
|
506
|
+
config = load_config()
|
|
507
|
+
config["agnes_domain"] = domain
|
|
508
|
+
save_config(config)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def get_agnes_base_url() -> str:
|
|
512
|
+
"""返回基于当前域名配置的完整 API Base URL(含 /v1 后缀)。"""
|
|
513
|
+
domain = get_agnes_domain()
|
|
514
|
+
root = AGNES_DOMAIN_MAP.get(domain, AGNES_DOMAIN_MAP[_DEFAULT_DOMAIN])
|
|
515
|
+
return f"{root}/v1"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def get_agnes_api_root() -> str:
|
|
519
|
+
"""返回基于当前域名配置的 API Root URL(不含 /v1 后缀)。"""
|
|
520
|
+
domain = get_agnes_domain()
|
|
521
|
+
return AGNES_DOMAIN_MAP.get(domain, AGNES_DOMAIN_MAP[_DEFAULT_DOMAIN])
|
|
@@ -393,10 +393,10 @@ class BasePipeline(ABC):
|
|
|
393
393
|
"""持久化任务元数据(video_id 等)到 sub_dir/task.json + curl.sh。"""
|
|
394
394
|
os.makedirs(sub_dir, exist_ok=True)
|
|
395
395
|
task_file = os.path.join(sub_dir, "task.json")
|
|
396
|
-
with open(task_file, "w") as f:
|
|
396
|
+
with open(task_file, "w", encoding="utf-8") as f:
|
|
397
397
|
json.dump(data, f, indent=2)
|
|
398
398
|
curl_file = os.path.join(sub_dir, "curl.sh")
|
|
399
|
-
with open(curl_file, "w") as f:
|
|
399
|
+
with open(curl_file, "w", encoding="utf-8") as f:
|
|
400
400
|
f.write(self._make_curl(data.get("video_id", "")) + "\n")
|
|
401
401
|
|
|
402
402
|
def _load_task_json(self, sub_dir: str) -> Optional[str]:
|
|
@@ -404,7 +404,7 @@ class BasePipeline(ABC):
|
|
|
404
404
|
task_file = os.path.join(sub_dir, "task.json")
|
|
405
405
|
if os.path.exists(task_file):
|
|
406
406
|
try:
|
|
407
|
-
with open(task_file, "r") as f:
|
|
407
|
+
with open(task_file, "r", encoding="utf-8") as f:
|
|
408
408
|
data = json.load(f)
|
|
409
409
|
return data.get("video_id") or data.get("task_id")
|
|
410
410
|
except Exception as e:
|
|
@@ -233,7 +233,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
233
233
|
if self._state.step_image_analysis == StepStatus.COMPLETED:
|
|
234
234
|
analysis_file = self._state.image_analysis_file
|
|
235
235
|
if os.path.exists(analysis_file):
|
|
236
|
-
with open(analysis_file, "r") as f:
|
|
236
|
+
with open(analysis_file, "r", encoding="utf-8") as f:
|
|
237
237
|
content = f.read()
|
|
238
238
|
# 检测之前分析失败留下的错误文本,强制重新分析
|
|
239
239
|
if "(分析失败" in content:
|
|
@@ -273,7 +273,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
273
273
|
)
|
|
274
274
|
|
|
275
275
|
analysis_file = os.path.join(self.working_dir, "image_analysis.txt")
|
|
276
|
-
with open(analysis_file, "w") as f:
|
|
276
|
+
with open(analysis_file, "w", encoding="utf-8") as f:
|
|
277
277
|
f.write(image_context)
|
|
278
278
|
|
|
279
279
|
self._state.step_image_analysis = StepStatus.COMPLETED
|
|
@@ -411,7 +411,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
411
411
|
story_path = self._state.story_file
|
|
412
412
|
if os.path.exists(story_path):
|
|
413
413
|
logger.info("[Pipeline] Step story: SKIP (already completed, file exists)")
|
|
414
|
-
with open(story_path, "r") as f:
|
|
414
|
+
with open(story_path, "r", encoding="utf-8") as f:
|
|
415
415
|
return f.read()
|
|
416
416
|
logger.warning("[Pipeline] Step story: marked completed but file missing, re-running")
|
|
417
417
|
|
|
@@ -428,7 +428,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
428
428
|
)
|
|
429
429
|
|
|
430
430
|
story_path = os.path.join(self.working_dir, "story.txt")
|
|
431
|
-
with open(story_path, "w") as f:
|
|
431
|
+
with open(story_path, "w", encoding="utf-8") as f:
|
|
432
432
|
f.write(story)
|
|
433
433
|
|
|
434
434
|
self._state.step_story = StepStatus.COMPLETED
|
|
@@ -481,7 +481,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
481
481
|
if os.path.exists(ref_img_path) and os.path.exists(ref_prompt_path):
|
|
482
482
|
self._state.step_character_ref = StepStatus.COMPLETED
|
|
483
483
|
self._state.character_ref_file = ref_img_path
|
|
484
|
-
with open(ref_prompt_path, "r") as f:
|
|
484
|
+
with open(ref_prompt_path, "r", encoding="utf-8") as f:
|
|
485
485
|
self._state.character_ref_prompt = f.read()
|
|
486
486
|
self.task_manager.update_state(
|
|
487
487
|
step_character_ref=StepStatus.COMPLETED,
|
|
@@ -494,7 +494,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
494
494
|
char_prompt = await asyncio.to_thread(
|
|
495
495
|
self.screenwriter.extract_character_description, story, self._state.style
|
|
496
496
|
)
|
|
497
|
-
with open(ref_prompt_path, "w") as f:
|
|
497
|
+
with open(ref_prompt_path, "w", encoding="utf-8") as f:
|
|
498
498
|
f.write(char_prompt)
|
|
499
499
|
|
|
500
500
|
await self._emit("character_ref", "running", "正在生成角色参考图 (t2i)...", 0.12)
|
|
@@ -532,7 +532,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
532
532
|
script_path = self._state.script_file
|
|
533
533
|
if os.path.exists(script_path):
|
|
534
534
|
logger.info("[Pipeline] Step script: SKIP (already completed, file exists)")
|
|
535
|
-
with open(script_path, "r") as f:
|
|
535
|
+
with open(script_path, "r", encoding="utf-8") as f:
|
|
536
536
|
scenes = json.load(f)
|
|
537
537
|
if self._state.scene_count == len(scenes):
|
|
538
538
|
return scenes
|
|
@@ -550,7 +550,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
550
550
|
)
|
|
551
551
|
|
|
552
552
|
script_path = os.path.join(self.working_dir, "script.json")
|
|
553
|
-
with open(script_path, "w") as f:
|
|
553
|
+
with open(script_path, "w", encoding="utf-8") as f:
|
|
554
554
|
json.dump(scenes, f, ensure_ascii=False, indent=2)
|
|
555
555
|
|
|
556
556
|
self._state.scene_count = len(scenes)
|
|
@@ -620,7 +620,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
620
620
|
prompts_path = self._state.end_frame_prompts_file
|
|
621
621
|
if os.path.exists(prompts_path):
|
|
622
622
|
logger.info("[Pipeline] Step end_frame_prompts: SKIP (already completed, file exists)")
|
|
623
|
-
with open(prompts_path, "r") as f:
|
|
623
|
+
with open(prompts_path, "r", encoding="utf-8") as f:
|
|
624
624
|
return json.load(f)
|
|
625
625
|
logger.warning("[Pipeline] Step end_frame_prompts: marked completed but file missing, re-running")
|
|
626
626
|
|
|
@@ -638,7 +638,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
638
638
|
)
|
|
639
639
|
|
|
640
640
|
prompts_path = os.path.join(self.working_dir, "end_frame_prompts.json")
|
|
641
|
-
with open(prompts_path, "w") as f:
|
|
641
|
+
with open(prompts_path, "w", encoding="utf-8") as f:
|
|
642
642
|
json.dump(end_frame_prompts, f, ensure_ascii=False, indent=2)
|
|
643
643
|
|
|
644
644
|
self._state.step_end_frame_prompts = StepStatus.COMPLETED
|
|
@@ -921,10 +921,10 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
921
921
|
video_id: Remote video task identifier.
|
|
922
922
|
"""
|
|
923
923
|
task_file = os.path.join(scene_dir, "task.json")
|
|
924
|
-
with open(task_file, "w") as f:
|
|
924
|
+
with open(task_file, "w", encoding="utf-8") as f:
|
|
925
925
|
json.dump({"video_id": video_id}, f, indent=2)
|
|
926
926
|
curl_file = os.path.join(scene_dir, "curl.sh")
|
|
927
|
-
with open(curl_file, "w") as f:
|
|
927
|
+
with open(curl_file, "w", encoding="utf-8") as f:
|
|
928
928
|
f.write(self._make_curl(video_id) + "\n")
|
|
929
929
|
|
|
930
930
|
def _load_scene_task(self, scene_dir: str) -> Optional[str]:
|
|
@@ -939,7 +939,7 @@ class CreativeVideoPipeline(MultiScenePipeline):
|
|
|
939
939
|
task_file = os.path.join(scene_dir, "task.json")
|
|
940
940
|
if os.path.exists(task_file):
|
|
941
941
|
try:
|
|
942
|
-
with open(task_file, "r") as f:
|
|
942
|
+
with open(task_file, "r", encoding="utf-8") as f:
|
|
943
943
|
data = json.load(f)
|
|
944
944
|
return data.get("video_id") or data.get("task_id")
|
|
945
945
|
except Exception as e:
|
package/core/task_manager.py
CHANGED
|
@@ -77,7 +77,7 @@ class TaskManager:
|
|
|
77
77
|
return None
|
|
78
78
|
|
|
79
79
|
try:
|
|
80
|
-
with open(self._task_file, "r") as f:
|
|
80
|
+
with open(self._task_file, "r", encoding="utf-8") as f:
|
|
81
81
|
data = json.load(f)
|
|
82
82
|
|
|
83
83
|
# v2.0:通过 parse_task_state 工厂函数反序列化
|
|
@@ -195,7 +195,7 @@ class TaskManager:
|
|
|
195
195
|
task_file = os.path.join(working_dir, name, "task_state.json")
|
|
196
196
|
if os.path.exists(task_file):
|
|
197
197
|
try:
|
|
198
|
-
with open(task_file, "r") as f:
|
|
198
|
+
with open(task_file, "r", encoding="utf-8") as f:
|
|
199
199
|
data = json.load(f)
|
|
200
200
|
tasks.append({
|
|
201
201
|
"task_id": data.get("task_id", name),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-short-video",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
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.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"free-short-video": "bin/cli.js",
|
package/server.py
CHANGED
|
@@ -34,7 +34,7 @@ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
|
|
|
34
34
|
from fastapi.staticfiles import StaticFiles
|
|
35
35
|
from fastapi.responses import FileResponse, JSONResponse
|
|
36
36
|
|
|
37
|
-
from core.config import get_api_key, set_api_key, delete_api_key, get_api_key_source, get_working_dir, DURATION_FRAME_MAP, get_workspaces, add_workspace, remove_workspace, set_active_workspace, get_active_workspace, REGRESSION_WORKING_DIR_ENV, get_watermark_config, set_watermark_config, WATERMARK_PROMO_TEXT_ZH, WATERMARK_PROMO_TEXT_EN, get_selected_models, set_selected_models
|
|
37
|
+
from core.config import get_api_key, set_api_key, delete_api_key, get_api_key_source, get_working_dir, DURATION_FRAME_MAP, get_workspaces, add_workspace, remove_workspace, set_active_workspace, get_active_workspace, REGRESSION_WORKING_DIR_ENV, get_watermark_config, set_watermark_config, WATERMARK_PROMO_TEXT_ZH, WATERMARK_PROMO_TEXT_EN, get_selected_models, set_selected_models, get_agnes_domain, set_agnes_domain, AGNES_DOMAIN_MAP
|
|
38
38
|
from core.path_security import safe_join, safe_workspace_path, UnsafePathError
|
|
39
39
|
from core.audio.voices import (
|
|
40
40
|
get_voice_catalog,
|
|
@@ -369,6 +369,8 @@ async def get_config():
|
|
|
369
369
|
"watermark_promo_zh": WATERMARK_PROMO_TEXT_ZH,
|
|
370
370
|
"watermark_promo_en": WATERMARK_PROMO_TEXT_EN,
|
|
371
371
|
"models": get_selected_models(),
|
|
372
|
+
"agnes_domain": get_agnes_domain(),
|
|
373
|
+
"agnes_domains": list(AGNES_DOMAIN_MAP.keys()),
|
|
372
374
|
}
|
|
373
375
|
return data
|
|
374
376
|
|
|
@@ -461,6 +463,28 @@ async def save_watermark_config(enabled: bool = Form(False)):
|
|
|
461
463
|
return {"ok": True, "enabled": enabled}
|
|
462
464
|
|
|
463
465
|
|
|
466
|
+
# ═══════════════════════════════════════════════════
|
|
467
|
+
# 域名配置(v6.0)
|
|
468
|
+
# ═══════════════════════════════════════════════════
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@app.post("/api/config/domain")
|
|
472
|
+
async def save_agnes_domain(domain: str = Form(...)):
|
|
473
|
+
"""设置 Agnes API 域名后缀。
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
domain: "com" 或 "cn"
|
|
477
|
+
"""
|
|
478
|
+
domain = domain.strip().lower()
|
|
479
|
+
if domain not in AGNES_DOMAIN_MAP:
|
|
480
|
+
raise HTTPException(
|
|
481
|
+
status_code=422,
|
|
482
|
+
detail=f"域名后缀必须为 {list(AGNES_DOMAIN_MAP.keys())} 之一",
|
|
483
|
+
)
|
|
484
|
+
set_agnes_domain(domain)
|
|
485
|
+
return {"ok": True, "agnes_domain": domain}
|
|
486
|
+
|
|
487
|
+
|
|
464
488
|
# ═══════════════════════════════════════════════════
|
|
465
489
|
# 工作目录管理(多工作目录,同时仅一个 active)
|
|
466
490
|
# ═══════════════════════════════════════════════════
|
package/static/index.html
CHANGED
|
@@ -405,6 +405,50 @@
|
|
|
405
405
|
</div>
|
|
406
406
|
</div>
|
|
407
407
|
|
|
408
|
+
<!-- 域名配置(v6.0) -->
|
|
409
|
+
<div id="domain-section" class="glass-card rounded-2xl mb-6 overflow-hidden transition-all duration-300">
|
|
410
|
+
<!-- Compact bar (shown when collapsed) -->
|
|
411
|
+
<div id="domain-compact" class="hidden flex items-center justify-between px-6 py-3 cursor-pointer hover:bg-white/[0.02] transition" onclick="toggleConfigPanel('domain')" role="button" tabindex="0" aria-expanded="false" aria-label="展开域名配置" onkeydown="if(event.key==='Enter')toggleConfigPanel('domain')">
|
|
412
|
+
<div class="flex items-center gap-3">
|
|
413
|
+
<span class="text-sm">🌐</span>
|
|
414
|
+
<span id="domain-compact-text" class="text-sm text-gray-400"></span>
|
|
415
|
+
</div>
|
|
416
|
+
<span id="domain-arrow" class="text-gray-600 text-xs transition-transform duration-300">▶</span>
|
|
417
|
+
</div>
|
|
418
|
+
<!-- Expanded body -->
|
|
419
|
+
<div id="domain-body" class="p-6 pt-4">
|
|
420
|
+
<div class="flex items-center justify-between mb-3">
|
|
421
|
+
<h2 class="text-lg font-semibold text-blue-300" data-i18n="domainTitle">Agnes 域名配置</h2>
|
|
422
|
+
<div class="flex items-center gap-2">
|
|
423
|
+
<span id="domain-status" class="text-xs px-2 py-1 rounded-full bg-gray-800 text-gray-400">apihub.agnes-ai.com</span>
|
|
424
|
+
<button onclick="toggleConfigPanel('domain')" class="text-xs text-gray-500 hover:text-gray-300 transition px-2 py-1 rounded" title="收起" aria-label="收起域名配置">▲</button>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
<p class="text-xs text-gray-500 mb-4" data-i18n="domainHint">选择 Agnes API 的接入域名。在中国大陆推荐使用 .cn 域名,以获得更稳定流畅的访问体验。</p>
|
|
428
|
+
<div class="space-y-3">
|
|
429
|
+
<label class="flex items-center gap-3 glass-input rounded-lg px-4 py-3 cursor-pointer hover:border-blue-500/40 transition">
|
|
430
|
+
<input type="radio" name="agnes-domain" value="com" class="accent-blue-500 w-4 h-4 cursor-pointer">
|
|
431
|
+
<div>
|
|
432
|
+
<span class="text-sm text-gray-200 font-medium">apihub.agnes-ai.com</span>
|
|
433
|
+
<span class="text-xs text-gray-500 ml-2" data-i18n="domainComLabel">国际域名</span>
|
|
434
|
+
</div>
|
|
435
|
+
</label>
|
|
436
|
+
<label class="flex items-center gap-3 glass-input rounded-lg px-4 py-3 cursor-pointer hover:border-blue-500/40 transition">
|
|
437
|
+
<input type="radio" name="agnes-domain" value="cn" class="accent-blue-500 w-4 h-4 cursor-pointer">
|
|
438
|
+
<div>
|
|
439
|
+
<span class="text-sm text-gray-200 font-medium">apihub.agnes-ai.cn</span>
|
|
440
|
+
<span class="text-xs text-gray-500 ml-2" data-i18n="domainCnLabel">中国大陆推荐</span>
|
|
441
|
+
</div>
|
|
442
|
+
</label>
|
|
443
|
+
</div>
|
|
444
|
+
<div class="flex gap-3 mt-4">
|
|
445
|
+
<button id="domain-save" class="px-5 py-2.5 bg-blue-600 hover:bg-blue-500 rounded-lg text-sm font-medium transition shadow-lg shadow-blue-600/20" data-i18n="save">保存</button>
|
|
446
|
+
<span id="domain-save-status" class="self-center text-xs text-green-400 hidden" data-i18n="domainSaved">已保存</span>
|
|
447
|
+
<span id="domain-save-error" class="self-center text-xs text-red-400 hidden"></span>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
408
452
|
<!-- 工作目录 -->
|
|
409
453
|
<div id="workspace-section" class="glass-card rounded-2xl mb-6 overflow-hidden transition-all duration-300">
|
|
410
454
|
<!-- Compact bar (shown when collapsed) -->
|
|
@@ -1603,6 +1647,12 @@ const T = {
|
|
|
1603
1647
|
connLostMsg: '与服务端的实时连接已断开。服务可能仍在后台运行。可到任务列表查看状态并续传。',
|
|
1604
1648
|
gotoList: '前往任务列表', reconnecting: '实时连接断开,正在重试...',
|
|
1605
1649
|
ttAnchor: '数字人口播',
|
|
1650
|
+
domainTitle: 'Agnes 域名配置',
|
|
1651
|
+
domainHint: '选择 Agnes API 的接入域名。在中国大陆推荐使用 .cn 域名,以获得更稳定流畅的访问体验。',
|
|
1652
|
+
domainComLabel: '国际域名',
|
|
1653
|
+
domainCnLabel: '中国大陆推荐',
|
|
1654
|
+
domainSaved: '已保存',
|
|
1655
|
+
domainSaveFailed: '保存失败',
|
|
1606
1656
|
ttPoetry: '诗词视频', poetrySettings: '诗词设置',
|
|
1607
1657
|
poemLabel: '古诗原文', poemPlaceholder: '输入完整古诗原文,例如:春眠不觉晓,处处闻啼鸟...',
|
|
1608
1658
|
poemHint: '可多段;诗句间用换行或标点自然分隔。AI 会按诗意拆分为若干场景。',
|
|
@@ -1743,6 +1793,12 @@ const T = {
|
|
|
1743
1793
|
connLostMsg: 'Connection lost. Server may still be running. Check task list for status.',
|
|
1744
1794
|
gotoList: 'Go to task list', reconnecting: 'Connection lost, retrying...',
|
|
1745
1795
|
ttAnchor: 'Digital Human',
|
|
1796
|
+
domainTitle: 'Agnes Domain',
|
|
1797
|
+
domainHint: 'Choose the Agnes API access domain. In China, .cn is recommended for better connectivity.',
|
|
1798
|
+
domainComLabel: 'Global',
|
|
1799
|
+
domainCnLabel: 'China (recommended)',
|
|
1800
|
+
domainSaved: 'Saved',
|
|
1801
|
+
domainSaveFailed: 'Save failed',
|
|
1746
1802
|
ttPoetry: 'Poetry Video', poetrySettings: 'Poetry Settings',
|
|
1747
1803
|
poemLabel: 'Poem Text', poemPlaceholder: 'Enter the full poem, e.g. 春眠不觉晓,处处闻啼鸟...', poemHint: 'Multiple stanzas allowed; separated by line breaks or punctuation. AI splits into scenes by meaning.',
|
|
1748
1804
|
scenePromptsOptional: 'Scene Prompts (Optional)', scenePromptsPlaceholder: 'Format: verse | visual description (one scene per line)', scenePromptsHint: 'Optional. One scene per line, fixed format "verse | visual description". Two ways give identical results: ① leave empty and let the system generate; ② copy the prompt below to any AI, then paste the result back.', scenePromptsExample: 'e.g. Spring sleeps, unaware of dawn... | Misty spring morning, birds singing',
|
|
@@ -1872,8 +1928,14 @@ const T = {
|
|
|
1872
1928
|
resumed: 'Задача возобновлена', watchLog: 'Следите за логами.',
|
|
1873
1929
|
task_: 'Задача', genFailed: 'Ошибка', genFailedMsg: 'Проверьте логи.',
|
|
1874
1930
|
gotoResume: 'К списку задач', connLost: 'Соединение потеряно',
|
|
1875
|
-
connLostMsg: 'Соединение потеряно.', gotoList: 'К списку',
|
|
1931
|
+
connLostMsg: 'Соединение потеряно.', gotoList: 'К списку', reconnecting: 'Переподключение...',
|
|
1876
1932
|
ttAnchor: 'Цифровой человек',
|
|
1933
|
+
domainTitle: 'Домен Agnes',
|
|
1934
|
+
domainHint: 'Выберите домен для API Agnes. В Китае рекомендуется .cn для стабильного доступа.',
|
|
1935
|
+
domainComLabel: 'Глобальный',
|
|
1936
|
+
domainCnLabel: 'Китай (рекомендуется)',
|
|
1937
|
+
domainSaved: 'Сохранено',
|
|
1938
|
+
domainSaveFailed: 'Ошибка сохранения',
|
|
1877
1939
|
ttImage: 'Простое изображение', imageSettings: 'Настройки изображения',
|
|
1878
1940
|
imagePrompt: 'Описание', imagePromptPlaceholder: 'Опишите желаемое изображение...',
|
|
1879
1941
|
imageSize: 'Размер', refImageHint: 'Опционально. Загрузите для создания вариаций.',
|
|
@@ -1981,6 +2043,12 @@ const T = {
|
|
|
1981
2043
|
task_: 'タスク', genFailed: '生成失敗', genFailedMsg: 'ログを確認。',
|
|
1982
2044
|
gotoResume: 'タスク一覧へ', connLost: '接続切断', connLostMsg: '接続が切れました。', gotoList: '一覧へ', reconnecting: '再接続中...',
|
|
1983
2045
|
ttAnchor: 'デジタルヒューマン',
|
|
2046
|
+
domainTitle: 'Agnes ドメイン',
|
|
2047
|
+
domainHint: 'Agnes API のドメインを選択。中国では .cn が推奨されます。',
|
|
2048
|
+
domainComLabel: 'グローバル',
|
|
2049
|
+
domainCnLabel: '中国 (推奨)',
|
|
2050
|
+
domainSaved: '保存しました',
|
|
2051
|
+
domainSaveFailed: '保存に失敗しました',
|
|
1984
2052
|
ttImage: '簡単画像', imageSettings: '画像設定',
|
|
1985
2053
|
imagePrompt: '画像説明', imagePromptPlaceholder: '希望する画像を説明してください...',
|
|
1986
2054
|
imageSize: '画像サイズ', refImageHint: '任意。アップロードしてバリエーションを生成。',
|
|
@@ -2088,6 +2156,12 @@ const T = {
|
|
|
2088
2156
|
task_: '작업', genFailed: '생성 실패', genFailedMsg: '로그를 확인하세요.',
|
|
2089
2157
|
gotoResume: '작업 목록으로', connLost: '연결 끊김', connLostMsg: '연결이 끊어졌습니다.', gotoList: '목록으로', reconnecting: '재연결 중...',
|
|
2090
2158
|
ttAnchor: '디지털 휴먼',
|
|
2159
|
+
domainTitle: 'Agnes 도메인',
|
|
2160
|
+
domainHint: 'Agnes API 도메인을 선택하세요. 중국에서는 .cn 사용을 권장합니다.',
|
|
2161
|
+
domainComLabel: '글로벌',
|
|
2162
|
+
domainCnLabel: '중국 (권장)',
|
|
2163
|
+
domainSaved: '저장됨',
|
|
2164
|
+
domainSaveFailed: '저장 실패',
|
|
2091
2165
|
ttImage: '간단 이미지', imageSettings: '이미지 설정',
|
|
2092
2166
|
imagePrompt: '이미지 설명', imagePromptPlaceholder: '원하는 이미지를 설명하세요...',
|
|
2093
2167
|
imageSize: '이미지 크기', refImageHint: '선택사항. 업로드하여 변형을 생성합니다.',
|
|
@@ -2195,6 +2269,12 @@ const T = {
|
|
|
2195
2269
|
task_: 'Tugas', genFailed: 'Gagal', genFailedMsg: 'Periksa log.',
|
|
2196
2270
|
gotoResume: 'Ke senarai', connLost: 'Sambungan terputus', connLostMsg: 'Sambungan terputus.', gotoList: 'Ke senarai', reconnecting: 'Menyambung semula...',
|
|
2197
2271
|
ttAnchor: 'Digital Human',
|
|
2272
|
+
domainTitle: 'Domain Agnes',
|
|
2273
|
+
domainHint: 'Pilih domain API Agnes. Di China, .cn disyorkan untuk akses yang lebih stabil.',
|
|
2274
|
+
domainComLabel: 'Global',
|
|
2275
|
+
domainCnLabel: 'China (disyorkan)',
|
|
2276
|
+
domainSaved: 'Disimpan',
|
|
2277
|
+
domainSaveFailed: 'Gagal simpan',
|
|
2198
2278
|
ttImage: 'Imej Ringkas', imageSettings: 'Tetapan Imej',
|
|
2199
2279
|
imagePrompt: 'Penerangan Imej', imagePromptPlaceholder: 'Huraikan imej yang anda mahukan...',
|
|
2200
2280
|
imageSize: 'Saiz Imej', refImageHint: 'Pilihan. Muat naik untuk hasilkan variasi.',
|
|
@@ -2301,6 +2381,12 @@ const T = {
|
|
|
2301
2381
|
task_: 'Tugas', genFailed: 'Gagal', genFailedMsg: 'Periksa log.',
|
|
2302
2382
|
gotoResume: 'Ke daftar', connLost: 'Koneksi terputus', connLostMsg: 'Koneksi terputus.', gotoList: 'Ke daftar', reconnecting: 'Menyambung ulang...',
|
|
2303
2383
|
ttAnchor: 'Digital Human',
|
|
2384
|
+
domainTitle: 'Domain Agnes',
|
|
2385
|
+
domainHint: 'Pilih domain API Agnes. Di China, .cn direkomendasikan untuk akses lebih stabil.',
|
|
2386
|
+
domainComLabel: 'Global',
|
|
2387
|
+
domainCnLabel: 'China (direkomendasikan)',
|
|
2388
|
+
domainSaved: 'Disimpan',
|
|
2389
|
+
domainSaveFailed: 'Gagal simpan',
|
|
2304
2390
|
ttImage: 'Gambar Sederhana', imageSettings: 'Pengaturan Gambar',
|
|
2305
2391
|
imagePrompt: 'Deskripsi Gambar', imagePromptPlaceholder: 'Jelaskan gambar yang Anda inginkan...',
|
|
2306
2392
|
imageSize: 'Ukuran Gambar', refImageHint: 'Opsional. Unggah untuk menghasilkan variasi.',
|
|
@@ -2413,6 +2499,12 @@ const T = {
|
|
|
2413
2499
|
connLostMsg: 'Verbindung zum Server verloren. Server läuft möglicherweise noch. Aufgabenliste prüfen.',
|
|
2414
2500
|
gotoList: 'Zur Aufgabenliste', reconnecting: 'Verbindung verloren, neuer Versuch...',
|
|
2415
2501
|
ttAnchor: 'Digitaler Mensch',
|
|
2502
|
+
domainTitle: 'Agnes-Domain',
|
|
2503
|
+
domainHint: 'Wählen Sie die API-Domain für Agnes. In China wird .cn für bessere Konnektivität empfohlen.',
|
|
2504
|
+
domainComLabel: 'Global',
|
|
2505
|
+
domainCnLabel: 'China (empfohlen)',
|
|
2506
|
+
domainSaved: 'Gespeichert',
|
|
2507
|
+
domainSaveFailed: 'Speichern fehlgeschlagen',
|
|
2416
2508
|
ttImage: 'Einfaches Bild', imageSettings: 'Bildeinstellungen',
|
|
2417
2509
|
imagePrompt: 'Bildbeschreibung', imagePromptPlaceholder: 'Gewünschtes Bild beschreiben...',
|
|
2418
2510
|
imageSize: 'Bildgröße', refImageHint: 'Optional. Hochladen für Varianten; leer lassen für reine KI-Generierung.',
|
|
@@ -2525,6 +2617,12 @@ const T = {
|
|
|
2525
2617
|
connLostMsg: 'Connexion au serveur perdue. Le serveur fonctionne peut-être encore. Vérifiez la liste des tâches.',
|
|
2526
2618
|
gotoList: 'Aller à la liste', reconnecting: 'Connexion perdue, nouvelle tentative...',
|
|
2527
2619
|
ttAnchor: 'Humain numérique',
|
|
2620
|
+
domainTitle: 'Domaine Agnes',
|
|
2621
|
+
domainHint: 'Choisissez le domaine API Agnes. En Chine, .cn est recommandé pour une meilleure connectivité.',
|
|
2622
|
+
domainComLabel: 'Mondial',
|
|
2623
|
+
domainCnLabel: 'Chine (recommandé)',
|
|
2624
|
+
domainSaved: 'Enregistré',
|
|
2625
|
+
domainSaveFailed: 'Échec de l\'enregistrement',
|
|
2528
2626
|
ttImage: 'Image simple', imageSettings: 'Paramètres image',
|
|
2529
2627
|
imagePrompt: 'Description image', imagePromptPlaceholder: 'Décrivez l\'image souhaitée...',
|
|
2530
2628
|
imageSize: 'Taille image', refImageHint: 'Optionnel. Télécharger pour générer des variations ; laisser vide pour génération IA pure.',
|
|
@@ -2637,6 +2735,12 @@ const T = {
|
|
|
2637
2735
|
connLostMsg: 'Verbinding met server verbroken. Server draait mogelijk nog. Controleer de takenlijst.',
|
|
2638
2736
|
gotoList: 'Naar takenlijst', reconnecting: 'Verbinding verbroken, opnieuw proberen...',
|
|
2639
2737
|
ttAnchor: 'Digitale mens',
|
|
2738
|
+
domainTitle: 'Agnes-domein',
|
|
2739
|
+
domainHint: 'Kies het API-domein voor Agnes. In China wordt .cn aanbevolen voor een betere connectiviteit.',
|
|
2740
|
+
domainComLabel: 'Globaal',
|
|
2741
|
+
domainCnLabel: 'China (aanbevolen)',
|
|
2742
|
+
domainSaved: 'Opgeslagen',
|
|
2743
|
+
domainSaveFailed: 'Opslaan mislukt',
|
|
2640
2744
|
ttImage: 'Eenvoudige afbeelding', imageSettings: 'Afbeeldingsinstellingen',
|
|
2641
2745
|
imagePrompt: 'Afbeeldingsbeschrijving', imagePromptPlaceholder: 'Beschrijf de gewenste afbeelding...',
|
|
2642
2746
|
imageSize: 'Afbeeldingsgrootte', refImageHint: 'Optioneel. Uploaden voor variaties; leeg laten voor pure AI-generatie.',
|
|
@@ -2749,6 +2853,12 @@ const T = {
|
|
|
2749
2853
|
connLostMsg: 'Conexión con el servidor perdida. El servidor puede seguir funcionando. Revise la lista de tareas.',
|
|
2750
2854
|
gotoList: 'Ir a la lista', reconnecting: 'Conexión perdida, reintentando...',
|
|
2751
2855
|
ttAnchor: 'Humano digital',
|
|
2856
|
+
domainTitle: 'Dominio de Agnes',
|
|
2857
|
+
domainHint: 'Elija el dominio de API de Agnes. En China, se recomienda .cn para una mejor conectividad.',
|
|
2858
|
+
domainComLabel: 'Global',
|
|
2859
|
+
domainCnLabel: 'China (recomendado)',
|
|
2860
|
+
domainSaved: 'Guardado',
|
|
2861
|
+
domainSaveFailed: 'Error al guardar',
|
|
2752
2862
|
ttImage: 'Imagen simple', imageSettings: 'Configuración de imagen',
|
|
2753
2863
|
imagePrompt: 'Descripción de imagen', imagePromptPlaceholder: 'Describa la imagen deseada...',
|
|
2754
2864
|
imageSize: 'Tamaño de imagen', refImageHint: 'Opcional. Subir para generar variaciones; dejar vacío para generación IA pura.',
|
|
@@ -2861,6 +2971,12 @@ const T = {
|
|
|
2861
2971
|
connLostMsg: 'Conexão com o servidor perdida. O servidor pode ainda estar funcionando. Verifique a lista de tarefas.',
|
|
2862
2972
|
gotoList: 'Ir para a lista', reconnecting: 'Conexão perdida, tentando novamente...',
|
|
2863
2973
|
ttAnchor: 'Humano digital',
|
|
2974
|
+
domainTitle: 'Domínio Agnes',
|
|
2975
|
+
domainHint: 'Escolha o domínio da API Agnes. Na China, recomenda-se .cn para melhor conectividade.',
|
|
2976
|
+
domainComLabel: 'Global',
|
|
2977
|
+
domainCnLabel: 'China (recomendado)',
|
|
2978
|
+
domainSaved: 'Salvo',
|
|
2979
|
+
domainSaveFailed: 'Falha ao salvar',
|
|
2864
2980
|
ttImage: 'Imagem simples', imageSettings: 'Configurações de imagem',
|
|
2865
2981
|
imagePrompt: 'Descrição da imagem', imagePromptPlaceholder: 'Descreva a imagem desejada...',
|
|
2866
2982
|
imageSize: 'Tamanho da imagem', refImageHint: 'Opcional. Enviar para gerar variações; deixar vazio para geração IA pura.',
|
|
@@ -2973,6 +3089,12 @@ const T = {
|
|
|
2973
3089
|
connLostMsg: 'Connessione al server persa. Il server potrebbe essere ancora in esecuzione. Controlla l\'elenco dei task.',
|
|
2974
3090
|
gotoList: 'Vai all\'elenco', reconnecting: 'Connessione persa, nuovo tentativo...',
|
|
2975
3091
|
ttAnchor: 'Umano digitale',
|
|
3092
|
+
domainTitle: 'Dominio Agnes',
|
|
3093
|
+
domainHint: 'Scegli il dominio API per Agnes. In Cina, .cn è consigliato per una migliore connettività.',
|
|
3094
|
+
domainComLabel: 'Globale',
|
|
3095
|
+
domainCnLabel: 'Cina (consigliato)',
|
|
3096
|
+
domainSaved: 'Salvato',
|
|
3097
|
+
domainSaveFailed: 'Salvataggio fallito',
|
|
2976
3098
|
ttImage: 'Immagine semplice', imageSettings: 'Impostazioni immagine',
|
|
2977
3099
|
imagePrompt: 'Descrizione immagine', imagePromptPlaceholder: 'Descrivi l\'immagine desiderata...',
|
|
2978
3100
|
imageSize: 'Dimensione immagine', refImageHint: 'Opzionale. Carica per generare varianti; lascia vuoto per generazione IA pura.',
|
|
@@ -3067,6 +3189,9 @@ function applyLanguage(lang) {
|
|
|
3067
3189
|
image: (document.getElementById('model-image') || {}).value || '',
|
|
3068
3190
|
video: (document.getElementById('model-video') || {}).value || ''
|
|
3069
3191
|
});
|
|
3192
|
+
// 刷新域名紧凑栏(含 i18n 标题)
|
|
3193
|
+
const domainRadio = document.querySelector('input[name="agnes-domain"]:checked');
|
|
3194
|
+
if (domainRadio) updateDomainCompact(domainRadio.value);
|
|
3070
3195
|
}
|
|
3071
3196
|
|
|
3072
3197
|
function switchLang(lang) { applyLanguage(lang); }
|
|
@@ -3149,6 +3274,15 @@ const STEPS = {
|
|
|
3149
3274
|
}
|
|
3150
3275
|
// 加载模型选择(v5.0)
|
|
3151
3276
|
try { await loadModels(); } catch(e) { console.error('init models error:', e); }
|
|
3277
|
+
// 加载域名配置(v6.0)
|
|
3278
|
+
if (d.agnes_domain) {
|
|
3279
|
+
const radios = document.querySelectorAll('input[name="agnes-domain"]');
|
|
3280
|
+
for (const r of radios) {
|
|
3281
|
+
if (r.value === d.agnes_domain) r.checked = true;
|
|
3282
|
+
}
|
|
3283
|
+
updateDomainCompact(d.agnes_domain);
|
|
3284
|
+
updateDomainStatus(d.agnes_domain);
|
|
3285
|
+
}
|
|
3152
3286
|
} catch(e) { console.error('init config load error:', e); }
|
|
3153
3287
|
|
|
3154
3288
|
// Character count for manuscript
|
|
@@ -5062,6 +5196,60 @@ document.getElementById('model-save').onclick = async () => {
|
|
|
5062
5196
|
}
|
|
5063
5197
|
};
|
|
5064
5198
|
|
|
5199
|
+
// ═══════════════════════════════════════════════════
|
|
5200
|
+
// 域名配置(v6.0)
|
|
5201
|
+
// ═══════════════════════════════════════════════════
|
|
5202
|
+
|
|
5203
|
+
function updateDomainCompact(domain) {
|
|
5204
|
+
const compact = document.getElementById('domain-compact');
|
|
5205
|
+
const textEl = document.getElementById('domain-compact-text');
|
|
5206
|
+
const isCn = domain === 'cn';
|
|
5207
|
+
textEl.innerHTML =
|
|
5208
|
+
'<span class="text-gray-300 font-medium">' + t('domainTitle') + '</span>' +
|
|
5209
|
+
'<span class="text-gray-600 mx-2">·</span>' +
|
|
5210
|
+
'<span class="' + (isCn ? 'text-green-400' : 'text-gray-400') + '">' +
|
|
5211
|
+
'apihub.agnes-ai.' + escapeHtml(domain) +
|
|
5212
|
+
'</span>';
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
function updateDomainStatus(domain) {
|
|
5216
|
+
const statusEl = document.getElementById('domain-status');
|
|
5217
|
+
statusEl.textContent = 'apihub.agnes-ai.' + domain;
|
|
5218
|
+
if (domain === 'cn') {
|
|
5219
|
+
statusEl.className = 'text-xs px-2 py-1 rounded-full bg-green-900/40 text-green-300';
|
|
5220
|
+
} else {
|
|
5221
|
+
statusEl.className = 'text-xs px-2 py-1 rounded-full bg-gray-800 text-gray-400';
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
document.getElementById('domain-save').onclick = async () => {
|
|
5226
|
+
const selected = document.querySelector('input[name="agnes-domain"]:checked');
|
|
5227
|
+
if (!selected) return;
|
|
5228
|
+
const domain = selected.value;
|
|
5229
|
+
const errEl = document.getElementById('domain-save-error');
|
|
5230
|
+
const okEl = document.getElementById('domain-save-status');
|
|
5231
|
+
errEl.classList.add('hidden');
|
|
5232
|
+
okEl.classList.add('hidden');
|
|
5233
|
+
const form = new FormData();
|
|
5234
|
+
form.append('domain', domain);
|
|
5235
|
+
try {
|
|
5236
|
+
const r = await fetch('/api/config/domain', { method: 'POST', body: form });
|
|
5237
|
+
if (r.ok) {
|
|
5238
|
+
okEl.classList.remove('hidden');
|
|
5239
|
+
updateDomainCompact(domain);
|
|
5240
|
+
updateDomainStatus(domain);
|
|
5241
|
+
setTimeout(() => okEl.classList.add('hidden'), 2000);
|
|
5242
|
+
} else {
|
|
5243
|
+
const d = await r.json().catch(() => ({}));
|
|
5244
|
+
errEl.textContent = d.detail || (t('domainSaveFailed') || '保存失败');
|
|
5245
|
+
errEl.classList.remove('hidden');
|
|
5246
|
+
}
|
|
5247
|
+
} catch (e) {
|
|
5248
|
+
errEl.textContent = '网络错误';
|
|
5249
|
+
errEl.classList.remove('hidden');
|
|
5250
|
+
}
|
|
5251
|
+
};
|
|
5252
|
+
|
|
5065
5253
|
// ═══════════════════════════════════════════════════
|
|
5066
5254
|
// 水印开关
|
|
5067
5255
|
// ═══════════════════════════════════════════════════
|