Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +5 -13
- docs/README.md +2 -2
- docs/SPEC_WEB_UI.md +23 -54
- docs/WEB_DEV_GUIDE.md +36 -83
- web/backend/static/assets/index-DK0fQxAx.js +0 -0
- web/backend/static/assets/index-DKu6Axh1.css +1 -0
- web/backend/static/index.html +17 -7
- web/frontend/index.html +15 -5
- web/frontend/src/context/GenerationOptionsContext.tsx +1 -5
- web/frontend/src/context/UiSettingsContext.tsx +59 -16
- web/frontend/src/index.css +40 -7
- web/frontend/src/pages/Layout.tsx +31 -2
- web/frontend/src/pages/Login.tsx +2 -2
- web/frontend/tailwind.config.js +7 -12
README.md
CHANGED
|
@@ -9,21 +9,13 @@ pinned: false
|
|
| 9 |
|
| 10 |
# Gemini Studio Web
|
| 11 |
|
| 12 |
-
Gemini 图片 / 视频 Web
|
| 13 |
-
|
| 14 |
-
## Documentation
|
| 15 |
|
| 16 |
| Document | Contents |
|
| 17 |
|----------|----------|
|
| 18 |
-
| [
|
| 19 |
-
| [
|
| 20 |
-
|
| 21 |
-
索引:[docs/README.md](docs/README.md)。
|
| 22 |
-
|
| 23 |
-
---
|
| 24 |
-
|
| 25 |
-
## Hugging Face(摘要)
|
| 26 |
|
| 27 |
-
|
| 28 |
|
| 29 |
-
|
|
|
|
| 9 |
|
| 10 |
# Gemini Studio Web
|
| 11 |
|
| 12 |
+
Gemini 图片 / 视频 Web(FastAPI + React)。根目录 YAML 供 Hugging Face Spaces;说明见 `docs/`。
|
|
|
|
|
|
|
| 13 |
|
| 14 |
| Document | Contents |
|
| 15 |
|----------|----------|
|
| 16 |
+
| [docs/SPEC_WEB_UI.md](docs/SPEC_WEB_UI.md) | 功能与界面规格 |
|
| 17 |
+
| [docs/WEB_DEV_GUIDE.md](docs/WEB_DEV_GUIDE.md) | 环境、部署、HF Docker |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
## Hugging Face
|
| 20 |
|
| 21 |
+
上传与 Secrets:见 [docs/WEB_DEV_GUIDE.md — Hugging Face](docs/WEB_DEV_GUIDE.md#hugging-face-spaces-docker)。至少配置 `GEMINI_API_KEY`、`WEB_UI_PASSWORD`、`SESSION_SECRET`。
|
docs/README.md
CHANGED
|
@@ -3,6 +3,6 @@
|
|
| 3 |
| File | Contents |
|
| 4 |
|------|----------|
|
| 5 |
| [SPEC_WEB_UI.md](./SPEC_WEB_UI.md) | Product spec |
|
| 6 |
-
| [WEB_DEV_GUIDE.md](./WEB_DEV_GUIDE.md) |
|
| 7 |
|
| 8 |
-
|
|
|
|
| 3 |
| File | Contents |
|
| 4 |
|------|----------|
|
| 5 |
| [SPEC_WEB_UI.md](./SPEC_WEB_UI.md) | Product spec |
|
| 6 |
+
| [WEB_DEV_GUIDE.md](./WEB_DEV_GUIDE.md) | Setup, env, Docker, Hugging Face |
|
| 7 |
|
| 8 |
+
[README.md](../README.md)
|
docs/SPEC_WEB_UI.md
CHANGED
|
@@ -1,72 +1,41 @@
|
|
| 1 |
# Web UI — product specification
|
| 2 |
|
| 3 |
-
**
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
|
| 9 |
-
|
| 10 |
|
| 11 |
-
|
| 12 |
|
| 13 |
-
**
|
| 14 |
|
| 15 |
-
|
| 16 |
-
- 宽高比:`1:1` … `21:9`(见 `generation_options.json`)。
|
| 17 |
-
- 分辨率:`1K` / `2K` / `4K`。
|
| 18 |
|
| 19 |
-
|
| 20 |
|
| 21 |
-
-
|
| 22 |
-
-
|
| 23 |
-
-
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
|
| 27 |
-
|
| 28 |
-
- 时长:**固定 8 秒**(首/尾帧条件不接受 4/6 秒)。
|
| 29 |
|
| 30 |
-
##
|
| 31 |
|
| 32 |
-
|
| 33 |
-
- **提取视频帧:** 上传视频 → 可拖动时间轴 → 预览时间与画面 → 「下载」导出该帧 PNG。
|
| 34 |
-
- **图像裁剪**(前端 Canvas,`/tools/crop`,不上传服务器):裁剪框可平移、四角缩放;「自由」比例下四边白条可拖;固定比例选项与图片宽高比列表一致(另加「自由」);裁剪区即原图、结果实时更新;下载 PNG。
|
| 35 |
-
- **替换纯色背景**(前端 Canvas,`/tools/replace-bg`):适合纯色或大块相近背景;原色/目标色(系统调色板、手动 R/G/B 或 Hex、预览图点击取色);不透明度 0–100%;RGB 距离 ≤ 容差的像素替换为目标色;预览与 PNG 下载。
|
| 36 |
|
| 37 |
-
##
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
3. 循环视频:提示词 + input + output 视频。
|
| 46 |
-
4. 超分辨率:input + output。
|
| 47 |
-
5. 视频延伸(长视频):仅 output +「敬请期待」。
|
| 48 |
-
|
| 49 |
-
## 示例脚本(仓库内)
|
| 50 |
-
|
| 51 |
-
- A:`run_gen_image_image_cond.sh`、`run_gen_image_prompt_only.sh`
|
| 52 |
-
- B:`run_gen_video_prompt_only.sh`
|
| 53 |
-
- C:`run_gen_video_image_start_end_diff.sh`
|
| 54 |
-
|
| 55 |
-
脚本可能只覆盖单图等简化场景。完整 API 说明:[图片](https://ai.google.dev/gemini-api/docs/image-generation?hl=zh-cn)、[视频](https://ai.google.dev/gemini-api/docs/video?hl=zh-cn)。
|
| 56 |
-
|
| 57 |
-
# 界面要求
|
| 58 |
-
|
| 59 |
-
- 启动页:密码登录(密码来自环境变量)。
|
| 60 |
-
- 登录后侧栏进入各功能,每项含:简介、**必填**提示词、上传区(A/B 最多三张参考图,均可空;C 起始帧必填、结尾可选、可勾选与起始相同)、模型/宽高比/分辨率(视频含时长,受 API/参考图约束)、结果展示区。
|
| 61 |
-
- 生成中显示耗时,避免「卡住」感。
|
| 62 |
-
|
| 63 |
-
# 其他
|
| 64 |
-
|
| 65 |
-
- `GEMINI_API_KEY` 仅环境变量,禁止写入代码仓库。
|
| 66 |
-
- 代码英文;界面文案与提示词可用中文。
|
| 67 |
-
- 视觉:简洁、有一定设计感。
|
| 68 |
-
|
| 69 |
-
# 稳定 URL 与可配置选项
|
| 70 |
-
|
| 71 |
-
- 稳定访问域名:用自有域名 + DNS、静态 IP、反代或隧道等(见 WEB_DEV_GUIDE §8)。
|
| 72 |
-
- 模型名、分辨率、宽高比等:**不要写死在代码里**;集中在 **`web/config/generation_options.json`**,可用 **`GENERATION_OPTIONS_PATH`** 指向其他文件。改 JSON 通常不必重编前端;改 React/样式需 `cd web/frontend && npm run build`。
|
|
|
|
| 1 |
# Web UI — product specification
|
| 2 |
|
| 3 |
+
**What to build:** this file. **How to run / deploy:** [WEB_DEV_GUIDE.md](./WEB_DEV_GUIDE.md).
|
| 4 |
|
| 5 |
+
## Overview
|
| 6 |
|
| 7 |
+
Image and video generation from prompts and optional reference images.
|
| 8 |
|
| 9 |
+
## AI 创作台
|
| 10 |
|
| 11 |
+
**A. 图片** — 0–3 张参考图 + 提示词 → 一张图。思考强度三档(Flash minimal / Flash high / Pro high);模型 ID 与 `thinking_level` 见后端。宽高比、分辨率见 `generation_options.json`。
|
| 12 |
|
| 13 |
+
**B. 视频** — 0–3 张参考图 + 提示词 → 短视频。Veo 模型可配置;Lite **`supports_reference_images: false`**。有参考图时时长 **8s**;仅文案时 4/6/8s(以 API 为准)。
|
| 14 |
|
| 15 |
+
**C. 首尾过渡** — 起始帧必填 + 提示词;结尾帧可选或「与起始相同」。时长 **固定 8s**。
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
## 辅助工具
|
| 18 |
|
| 19 |
+
- **超分辨率:** 单张原图 → 高清图;默认提示词可改;宽高比按原图自动建议(表单底部可改);比例不匹配时警告仍可生成。
|
| 20 |
+
- **提取视频帧:** 本地视频 → 时间轴选帧 → PNG。
|
| 21 |
+
- **图像裁剪:** 前端 Canvas,不上传服务器;自由或固定比例 → PNG。
|
| 22 |
+
- **替换纯色背景:** 前端 Canvas;原/目标色、容差、预览与 PNG。
|
| 23 |
|
| 24 |
+
## 示例
|
| 25 |
|
| 26 |
+
静态演示(预置素材),不调用模型。
|
|
|
|
| 27 |
|
| 28 |
+
## 仓库脚本
|
| 29 |
|
| 30 |
+
`run_gen_image_*.sh`、`run_gen_video_*.sh` 等可能只覆盖简化场景。API 文档:[图片](https://ai.google.dev/gemini-api/docs/image-generation?hl=zh-cn)、[视频](https://ai.google.dev/gemini-api/docs/video?hl=zh-cn)。
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
+
## 界面
|
| 33 |
|
| 34 |
+
- 密码登录(`WEB_UI_PASSWORD`)。
|
| 35 |
+
- 侧栏进入各功能;提示词必填;参考图规则按 A/B/C;生成中显示耗时。
|
| 36 |
+
- 模型名、分辨率、宽高比等来自 **`web/config/generation_options.json`**(可用 **`GENERATION_OPTIONS_PATH`**),避免写死在代码里;改 JSON 通常不必重编前端。
|
| 37 |
|
| 38 |
+
## 其他
|
| 39 |
|
| 40 |
+
- `GEMINI_API_KEY` 仅环境变量,勿入库。
|
| 41 |
+
- 代码英文为主;界面与提示词可用中文。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/WEB_DEV_GUIDE.md
CHANGED
|
@@ -1,18 +1,14 @@
|
|
| 1 |
# Web UI — developer guide
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
**Imports:** Code
|
| 6 |
|
| 7 |
-
##
|
| 8 |
|
| 9 |
-
|
| 10 |
-
- Node.js 18+ and npm (frontend build)
|
| 11 |
-
- **ffmpeg** on `PATH` (strip audio from MP4s)
|
| 12 |
|
| 13 |
-
##
|
| 14 |
-
|
| 15 |
-
From **`VideoGeneration-release`**:
|
| 16 |
|
| 17 |
```bash
|
| 18 |
cd /path/to/VideoGeneration-release
|
|
@@ -21,17 +17,17 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
| 21 |
pip install -r web/requirements.txt
|
| 22 |
```
|
| 23 |
|
| 24 |
-
##
|
| 25 |
|
| 26 |
-
Do not commit secrets
|
| 27 |
|
| 28 |
| Variable | Purpose |
|
| 29 |
|----------|---------|
|
| 30 |
-
| `GEMINI_API_KEY` | Server-side
|
| 31 |
| `WEB_UI_PASSWORD` | Login password. |
|
| 32 |
| `SESSION_SECRET` | Session signing, e.g. `openssl rand -hex 32`. |
|
| 33 |
-
| `GENERATION_OPTIONS_PATH` | Optional
|
| 34 |
-
| `SESSION_COOKIE_SECURE` | Optional.
|
| 35 |
|
| 36 |
```bash
|
| 37 |
export GEMINI_API_KEY="your_key"
|
|
@@ -39,14 +35,14 @@ export WEB_UI_PASSWORD="password"
|
|
| 39 |
export SESSION_SECRET="$(openssl rand -hex 32)"
|
| 40 |
```
|
| 41 |
|
| 42 |
-
##
|
| 43 |
|
| 44 |
-
Edit **`web/config/generation_options.json`** (or
|
| 45 |
|
| 46 |
-
- **`image`**: `models`
|
| 47 |
-
- **`video`** / **`video_frames`**: `models`, `aspect_ratios`, `resolutions`, `durations_seconds`. On **`video`**, **`supports_reference_images`** (e.g. Veo Lite
|
| 48 |
|
| 49 |
-
##
|
| 50 |
|
| 51 |
```bash
|
| 52 |
cd web/frontend
|
|
@@ -54,59 +50,44 @@ npm install
|
|
| 54 |
npm run build
|
| 55 |
```
|
| 56 |
|
| 57 |
-
Output: **`web/backend/static/`**.
|
| 58 |
|
| 59 |
-
##
|
| 60 |
|
| 61 |
```bash
|
| 62 |
cd /path/to/VideoGeneration-release
|
| 63 |
PYTHONPATH=. uvicorn web.backend.main:app --host 127.0.0.1 --port 8000
|
| 64 |
```
|
| 65 |
|
| 66 |
-
LAN: `--host 0.0.0.0`. Dev:
|
| 67 |
-
|
| 68 |
-
## 7. Dev mode (hot reload)
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
-
``
|
| 73 |
-
PYTHONPATH=. uvicorn web.backend.main:app --reload --host 127.0.0.1 --port 8000
|
| 74 |
-
```
|
| 75 |
|
| 76 |
-
**
|
| 77 |
|
| 78 |
-
##
|
| 79 |
|
| 80 |
-
|
| 81 |
|
| 82 |
-
##
|
| 83 |
|
| 84 |
- [ ] `ffmpeg -version`
|
| 85 |
- [ ] `pip install -r web/requirements.txt`
|
| 86 |
- [ ] `npm run build` in `web/frontend` at least once
|
| 87 |
- [ ] `GEMINI_API_KEY`, `WEB_UI_PASSWORD`, `SESSION_SECRET` set
|
| 88 |
-
- [ ] Uvicorn from
|
| 89 |
-
|
| 90 |
-
## 10. Hugging Face Spaces (Docker)
|
| 91 |
-
|
| 92 |
-
Use **Docker** (`sdk: docker`), not Streamlit/Gradio. **`Dockerfile`** at repo root next to `web/` and `assets/`.
|
| 93 |
|
| 94 |
-
##
|
| 95 |
|
| 96 |
-
|
| 97 |
|
| 98 |
-
###
|
| 99 |
|
| 100 |
-
|
| 101 |
|
| 102 |
-
|
| 103 |
-
- **`colorFrom`** / **`colorTo`**: each must be one of **`red`**, **`yellow`**, **`green`**, **`blue`**, **`indigo`**, **`purple`**, **`pink`**, **`gray`** (other names → upload error).
|
| 104 |
-
|
| 105 |
-
### 10.3 Upload
|
| 106 |
-
|
| 107 |
-
**Cwd:** **`VideoGeneration-release`** (contains `Dockerfile`, `web/`, `assets/`).
|
| 108 |
-
|
| 109 |
-
**CLI** ([write token](https://huggingface.co/settings/tokens)):
|
| 110 |
|
| 111 |
```bash
|
| 112 |
pip install -U huggingface_hub
|
|
@@ -115,45 +96,17 @@ cd /path/to/VideoGeneration-release
|
|
| 115 |
hf upload YOUR_USERNAME/YOUR_SPACE_NAME . . --repo-type space
|
| 116 |
```
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
```bash
|
| 121 |
-
hf upload LehongWu/VideoGeneration-release . . --repo-type space
|
| 122 |
-
```
|
| 123 |
-
|
| 124 |
-
Then check **Space → Logs** for the Docker build.
|
| 125 |
-
|
| 126 |
-
**Git:**
|
| 127 |
-
|
| 128 |
-
```bash
|
| 129 |
-
git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 130 |
-
git push -u hf main
|
| 131 |
-
```
|
| 132 |
-
|
| 133 |
-
Or connect GitHub in Space settings and push there.
|
| 134 |
-
|
| 135 |
-
### 10.4 Secrets
|
| 136 |
-
|
| 137 |
-
**Settings → Variables and secrets** — names must match exactly (case-sensitive). Same as [§3](#3-environment-variables).
|
| 138 |
-
|
| 139 |
-
| Name | Purpose |
|
| 140 |
-
|------|---------|
|
| 141 |
-
| `GEMINI_API_KEY` | Gemini key; never commit. |
|
| 142 |
-
| `WEB_UI_PASSWORD` | Login password. |
|
| 143 |
-
| `SESSION_SECRET` | e.g. `openssl rand -hex 32`. |
|
| 144 |
|
| 145 |
-
|
| 146 |
|
| 147 |
-
|
| 148 |
|
| 149 |
-
###
|
| 150 |
|
| 151 |
-
|
| 152 |
-
- **`--forwarded-allow-ips='*'`** — required behind HF’s reverse proxy so `X-Forwarded-Proto: https` is trusted. Without it, Uvicorn often sees `http`, session cookies misbehave, and the UI **keeps asking you to log in**.
|
| 153 |
-
- **ffmpeg** in image.
|
| 154 |
-
- First build / cold start can take minutes.
|
| 155 |
|
| 156 |
-
###
|
| 157 |
|
| 158 |
```bash
|
| 159 |
cd /path/to/VideoGeneration-release
|
|
|
|
| 1 |
# Web UI — developer guide
|
| 2 |
|
| 3 |
+
Setup, run, deploy (FastAPI + React). Product scope: **[SPEC_WEB_UI.md](./SPEC_WEB_UI.md)**.
|
| 4 |
|
| 5 |
+
**Imports:** Code under `VideoGeneration-release/web/`. Run Python with cwd **`VideoGeneration-release`** and **`PYTHONPATH=.`** (not `web/` alone).
|
| 6 |
|
| 7 |
+
## Prerequisites
|
| 8 |
|
| 9 |
+
Python 3.10+, Node 18+, npm, **ffmpeg** on `PATH`.
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
## Python venv
|
|
|
|
|
|
|
| 12 |
|
| 13 |
```bash
|
| 14 |
cd /path/to/VideoGeneration-release
|
|
|
|
| 17 |
pip install -r web/requirements.txt
|
| 18 |
```
|
| 19 |
|
| 20 |
+
## Environment variables
|
| 21 |
|
| 22 |
+
Do not commit secrets.
|
| 23 |
|
| 24 |
| Variable | Purpose |
|
| 25 |
|----------|---------|
|
| 26 |
+
| `GEMINI_API_KEY` | Server-side API key. |
|
| 27 |
| `WEB_UI_PASSWORD` | Login password. |
|
| 28 |
| `SESSION_SECRET` | Session signing, e.g. `openssl rand -hex 32`. |
|
| 29 |
+
| `GENERATION_OPTIONS_PATH` | Optional; overrides default `web/config/generation_options.json`. |
|
| 30 |
+
| `SESSION_COOKIE_SECURE` | Optional. `1`/`true`/`yes` for HTTPS-only cookies (e.g. Hugging Face). Unset for local `http://127.0.0.1`. Proxy must send `X-Forwarded-Proto` (see Hugging Face / Docker below). |
|
| 31 |
|
| 32 |
```bash
|
| 33 |
export GEMINI_API_KEY="your_key"
|
|
|
|
| 35 |
export SESSION_SECRET="$(openssl rand -hex 32)"
|
| 36 |
```
|
| 37 |
|
| 38 |
+
## `generation_options.json`
|
| 39 |
|
| 40 |
+
Edit **`web/config/generation_options.json`** (or path from `GENERATION_OPTIONS_PATH`). UI loads it via **`GET /api/config/generation-options`** after login. JSON-only changes need no frontend rebuild.
|
| 41 |
|
| 42 |
+
- **`image`**: `models`, `aspect_ratios`, `resolutions`.
|
| 43 |
+
- **`video`** / **`video_frames`**: `models`, `aspect_ratios`, `resolutions`, `durations_seconds`. On **`video`**, **`supports_reference_images: false`** hides reference images (e.g. Veo Lite). With reference images, duration is **8s**. **首尾帧** route is always **8s**.
|
| 44 |
|
| 45 |
+
## Build frontend
|
| 46 |
|
| 47 |
```bash
|
| 48 |
cd web/frontend
|
|
|
|
| 50 |
npm run build
|
| 51 |
```
|
| 52 |
|
| 53 |
+
Output: **`web/backend/static/`**. Without it, root URL returns 503.
|
| 54 |
|
| 55 |
+
## Run server
|
| 56 |
|
| 57 |
```bash
|
| 58 |
cd /path/to/VideoGeneration-release
|
| 59 |
PYTHONPATH=. uvicorn web.backend.main:app --host 127.0.0.1 --port 8000
|
| 60 |
```
|
| 61 |
|
| 62 |
+
LAN: `--host 0.0.0.0`. Dev: `--reload`.
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
## Dev (hot reload)
|
| 65 |
|
| 66 |
+
**API:** `PYTHONPATH=. uvicorn web.backend.main:app --reload --host 127.0.0.1 --port 8000` from repo root.
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
**Vite:** `cd web/frontend && npm run dev` — proxies `/api` to 8000; open the printed URL (e.g. `http://127.0.0.1:5173`).
|
| 69 |
|
| 70 |
+
## Stable URL
|
| 71 |
|
| 72 |
+
Use your own DNS/domain, static IP, reverse proxy, or tunnel — no hostname is baked into the app.
|
| 73 |
|
| 74 |
+
## Checklist
|
| 75 |
|
| 76 |
- [ ] `ffmpeg -version`
|
| 77 |
- [ ] `pip install -r web/requirements.txt`
|
| 78 |
- [ ] `npm run build` in `web/frontend` at least once
|
| 79 |
- [ ] `GEMINI_API_KEY`, `WEB_UI_PASSWORD`, `SESSION_SECRET` set
|
| 80 |
+
- [ ] Uvicorn from repo root with `PYTHONPATH=.`
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
+
## Hugging Face Spaces (Docker)
|
| 83 |
|
| 84 |
+
Use **Docker** Space (`sdk: docker`). **`Dockerfile`** at repo root beside `web/` and `assets/`.
|
| 85 |
|
| 86 |
+
### Create & upload
|
| 87 |
|
| 88 |
+
[Hugging Face](https://huggingface.co/) → New Space → **SDK: Docker**. Root **`README.md`** YAML must validate (`sdk: docker`; **`colorFrom`** / **`colorTo`** each one of: `red`, `yellow`, `green`, `blue`, `indigo`, `purple`, `pink`, `gray`).
|
| 89 |
|
| 90 |
+
**Upload** from **`VideoGeneration-release`** (contains `Dockerfile`, `web/`, `assets/`):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
```bash
|
| 93 |
pip install -U huggingface_hub
|
|
|
|
| 96 |
hf upload YOUR_USERNAME/YOUR_SPACE_NAME . . --repo-type space
|
| 97 |
```
|
| 98 |
|
| 99 |
+
Or add git remote `https://huggingface.co/spaces/USER/SPACE` and push.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
### Secrets
|
| 102 |
|
| 103 |
+
**Settings → Variables and secrets** — same names as the env table above (case-sensitive). Optional: `GENERATION_OPTIONS_PATH`. Saving restarts the Space.
|
| 104 |
|
| 105 |
+
### Runtime (Dockerfile)
|
| 106 |
|
| 107 |
+
Listens on **`PORT`** or **7860**. **`--forwarded-allow-ips='*'`** is required behind HF’s proxy so HTTPS and session cookies work. **ffmpeg** in image. Cold start can take minutes.
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
+
### Local Docker
|
| 110 |
|
| 111 |
```bash
|
| 112 |
cd /path/to/VideoGeneration-release
|
web/backend/static/assets/index-DK0fQxAx.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
web/backend/static/assets/index-DKu6Axh1.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:DM Sans,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.order-1{order:1}.order-2{order:2}.m-0{margin:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-auto{margin-top:auto}.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-2{height:.5rem}.h-20{height:5rem}.max-h-64{max-height:16rem}.max-h-\[360px\]{max-height:360px}.max-h-\[min\(70vh\,52rem\)\]{max-height:min(70vh,52rem)}.min-h-\[100px\]{min-height:100px}.min-h-\[120px\]{min-height:120px}.min-h-screen{min-height:100vh}.w-16{width:4rem}.w-20{width:5rem}.w-\[17\.5rem\]{width:17.5rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[140px\]{min-width:140px}.min-w-\[15rem\]{min-width:15rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.cursor-crosshair{cursor:crosshair}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-7{gap:1.75rem}.space-y-14>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(3.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(3.5rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-none{border-radius:0}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-0{border-width:0px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-200\/80{border-color:#fde68acc}.border-ink{--tw-border-opacity: 1;border-color:rgb(var(--color-ink) / var(--tw-border-opacity, 1))}.border-line\/80{border-color:rgb(var(--color-line) / .8)}.border-line\/90{border-color:rgb(var(--color-line) / .9)}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-slate-200\/80{border-color:#e2e8f0cc}.border-slate-200\/90{border-color:#e2e8f0e6}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.border-slate-300\/80{border-color:#cbd5e1cc}.border-slate-400\/90{border-color:#94a3b8e6}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/5{background-color:#0000000d}.bg-ink{--tw-bg-opacity: 1;background-color:rgb(var(--color-ink) / var(--tw-bg-opacity, 1))}.bg-neutral-200{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / var(--tw-bg-opacity, 1))}.bg-panel\/95{background-color:rgb(var(--color-panel) / .95)}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-slate-400\/90{background-color:#94a3b8e6}.bg-slate-50\/90{background-color:#f8fafce6}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/60{background-color:#fff9}.bg-white\/80{background-color:#fffc}.bg-white\/85{background-color:#ffffffd9}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-paper{--tw-gradient-from: rgb(var(--color-paper) / 1) var(--tw-gradient-from-position);--tw-gradient-to: rgb(var(--color-paper) / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-panel\/30{--tw-gradient-to: rgb(var(--color-panel) / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(var(--color-panel) / .3) var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-paper{--tw-gradient-to: rgb(var(--color-paper) / 1) var(--tw-gradient-to-position)}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-10{padding:2.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pl-0\.5{padding-left:.125rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.625rem;line-height:2rem}.text-3xl{font-size:2rem;line-height:2.25rem}.text-base{font-size:1.0625rem;line-height:1.5rem}.text-sm{font-size:.9375rem;line-height:1.375rem}.text-xl{font-size:1.3125rem;line-height:1.75rem}.text-xs{font-size:.8125rem;line-height:1.125rem}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-ink{--tw-text-opacity: 1;color:rgb(var(--color-ink) / var(--tw-text-opacity, 1))}.text-ink\/70{color:rgb(var(--color-ink) / .7)}.text-ink\/80{color:rgb(var(--color-ink) / .8)}.text-ink\/85{color:rgb(var(--color-ink) / .85)}.text-ink\/90{color:rgb(var(--color-ink) / .9)}.text-ink\/95{color:rgb(var(--color-ink) / .95)}.text-mist{--tw-text-opacity: 1;color:rgb(var(--color-mist) / var(--tw-text-opacity, 1))}.text-mist\/90{color:rgb(var(--color-mist) / .9)}.text-paper{--tw-text-opacity: 1;color:rgb(var(--color-paper) / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.accent-ink{accent-color:rgb(var(--color-ink) / 1)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/10{--tw-shadow-color: rgb(0 0 0 / .1);--tw-shadow: var(--tw-shadow-colored)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root,html[data-theme=blue]{--color-paper: 238 241 248;--color-panel: 216 228 242;--color-line: 168 184 208;--color-mist: 71 85 105;--color-ink: 15 23 42;--color-clay: 99 102 241}html[data-theme=cream]{--color-paper: 252 250 245;--color-panel: 241 236 226;--color-line: 214 204 188;--color-mist: 87 83 78;--color-ink: 15 23 42;--color-clay: 120 113 108}html[data-theme=gray]{--color-paper: 244 244 246;--color-panel: 232 232 235;--color-line: 186 190 198;--color-mist: 82 82 91;--color-ink: 15 23 42;--color-clay: 82 82 91}html[data-theme=lavender]{--color-paper: 247 246 252;--color-panel: 234 231 245;--color-line: 196 191 220;--color-mist: 78 74 98;--color-ink: 15 23 42;--color-clay: 109 40 217}html{font-size:100%}html[data-font-size=compact]{font-size:75%}html[data-font-size=small]{font-size:90%}html[data-font-size=standard]{font-size:105%}html[data-font-size=large]{font-size:120%}html[data-font-size=xlarge]{font-size:135%}body{min-height:100vh;--tw-bg-opacity: 1;background-color:rgb(var(--color-paper) / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(var(--color-ink) / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:DM Sans,system-ui,sans-serif;line-height:1.55}::-moz-selection{background-color:rgb(var(--color-clay) / .22)}::selection{background-color:rgb(var(--color-clay) / .22)}h1,h2,h3,.font-display{font-family:"Source Serif 4",Georgia,serif}.file\:mr-3::file-selector-button{margin-right:.75rem}.file\:rounded-lg::file-selector-button{border-radius:.5rem}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-slate-200::file-selector-button{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.file\:px-3::file-selector-button{padding-left:.75rem;padding-right:.75rem}.file\:py-1\.5::file-selector-button{padding-top:.375rem;padding-bottom:.375rem}.file\:text-sm::file-selector-button{font-size:.9375rem;line-height:1.375rem}.file\:text-ink::file-selector-button{--tw-text-opacity: 1;color:rgb(var(--color-ink) / var(--tw-text-opacity, 1))}.placeholder\:text-slate-400::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.placeholder\:text-slate-400::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.hover\:border-clay\/80:hover{border-color:rgb(var(--color-clay) / .8)}.hover\:bg-slate-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-800:hover{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.hover\:bg-white\/60:hover{background-color:#fff9}.hover\:text-ink:hover{--tw-text-opacity: 1;color:rgb(var(--color-ink) / var(--tw-text-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-clay\/40:focus{--tw-ring-color: rgb(var(--color-clay) / .4)}.focus\:ring-clay\/50:focus{--tw-ring-color: rgb(var(--color-clay) / .5)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.disabled\:opacity-\[0\.92\]:disabled{opacity:.92}@media (min-width: 640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:p-10{padding:2.5rem}.sm\:p-12{padding:3rem}.sm\:p-7{padding:1.75rem}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:sticky{position:sticky}.lg\:top-4{top:1rem}.lg\:order-1{order:1}.lg\:order-2{order:2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-\[1fr_minmax\(200px\,260px\)\]{grid-template-columns:1fr minmax(200px,260px)}.lg\:p-12{padding:3rem}}@media (min-width: 1280px){.xl\:max-w-\[96rem\]{max-width:96rem}}@media (min-width: 1536px){.\32xl\:max-w-\[min\(104rem\,calc\(100vw-3rem\)\)\]{max-width:min(104rem,calc(100vw - 3rem))}}
|
web/backend/static/index.html
CHANGED
|
@@ -13,7 +13,8 @@
|
|
| 13 |
<script>
|
| 14 |
(function () {
|
| 15 |
var V2 = 2;
|
| 16 |
-
var
|
|
|
|
| 17 |
return (
|
| 18 |
fs === "compact" ||
|
| 19 |
fs === "small" ||
|
|
@@ -21,30 +22,39 @@
|
|
| 21 |
fs === "large" ||
|
| 22 |
fs === "xlarge"
|
| 23 |
);
|
| 24 |
-
}
|
|
|
|
|
|
|
|
|
|
| 25 |
try {
|
| 26 |
var raw = localStorage.getItem("videogen-ui-settings");
|
| 27 |
var fs = "standard";
|
|
|
|
| 28 |
if (raw) {
|
| 29 |
var o = JSON.parse(raw);
|
| 30 |
if (o && o.fontSize) {
|
| 31 |
-
if (o.version === V2
|
| 32 |
-
fs = o.fontSize;
|
| 33 |
} else {
|
| 34 |
if (o.fontSize === "comfortable") fs = "standard";
|
| 35 |
else if (o.fontSize === "standard") fs = "small";
|
| 36 |
-
else if (
|
| 37 |
}
|
| 38 |
}
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
document.documentElement.dataset.fontSize = fs;
|
|
|
|
| 41 |
} catch (e) {
|
| 42 |
document.documentElement.dataset.fontSize = "standard";
|
|
|
|
| 43 |
}
|
| 44 |
})();
|
| 45 |
</script>
|
| 46 |
-
<script type="module" crossorigin src="/assets/index-
|
| 47 |
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
| 48 |
</head>
|
| 49 |
<body>
|
| 50 |
<div id="root"></div>
|
|
|
|
| 13 |
<script>
|
| 14 |
(function () {
|
| 15 |
var V2 = 2;
|
| 16 |
+
var V3 = 3;
|
| 17 |
+
function validFs(fs) {
|
| 18 |
return (
|
| 19 |
fs === "compact" ||
|
| 20 |
fs === "small" ||
|
|
|
|
| 22 |
fs === "large" ||
|
| 23 |
fs === "xlarge"
|
| 24 |
);
|
| 25 |
+
}
|
| 26 |
+
function validTheme(t) {
|
| 27 |
+
return t === "blue" || t === "cream" || t === "gray" || t === "lavender";
|
| 28 |
+
}
|
| 29 |
try {
|
| 30 |
var raw = localStorage.getItem("videogen-ui-settings");
|
| 31 |
var fs = "standard";
|
| 32 |
+
var theme = "blue";
|
| 33 |
if (raw) {
|
| 34 |
var o = JSON.parse(raw);
|
| 35 |
if (o && o.fontSize) {
|
| 36 |
+
if (o.version === V2 || o.version === V3) {
|
| 37 |
+
if (validFs(o.fontSize)) fs = o.fontSize;
|
| 38 |
} else {
|
| 39 |
if (o.fontSize === "comfortable") fs = "standard";
|
| 40 |
else if (o.fontSize === "standard") fs = "small";
|
| 41 |
+
else if (validFs(o.fontSize)) fs = o.fontSize;
|
| 42 |
}
|
| 43 |
}
|
| 44 |
+
if (o && o.colorTheme && validTheme(o.colorTheme)) {
|
| 45 |
+
theme = o.colorTheme;
|
| 46 |
+
}
|
| 47 |
}
|
| 48 |
document.documentElement.dataset.fontSize = fs;
|
| 49 |
+
document.documentElement.dataset.theme = theme;
|
| 50 |
} catch (e) {
|
| 51 |
document.documentElement.dataset.fontSize = "standard";
|
| 52 |
+
document.documentElement.dataset.theme = "blue";
|
| 53 |
}
|
| 54 |
})();
|
| 55 |
</script>
|
| 56 |
+
<script type="module" crossorigin src="/assets/index-DK0fQxAx.js"></script>
|
| 57 |
+
<link rel="stylesheet" crossorigin href="/assets/index-DKu6Axh1.css">
|
| 58 |
</head>
|
| 59 |
<body>
|
| 60 |
<div id="root"></div>
|
web/frontend/index.html
CHANGED
|
@@ -13,7 +13,8 @@
|
|
| 13 |
<script>
|
| 14 |
(function () {
|
| 15 |
var V2 = 2;
|
| 16 |
-
var
|
|
|
|
| 17 |
return (
|
| 18 |
fs === "compact" ||
|
| 19 |
fs === "small" ||
|
|
@@ -21,25 +22,34 @@
|
|
| 21 |
fs === "large" ||
|
| 22 |
fs === "xlarge"
|
| 23 |
);
|
| 24 |
-
}
|
|
|
|
|
|
|
|
|
|
| 25 |
try {
|
| 26 |
var raw = localStorage.getItem("videogen-ui-settings");
|
| 27 |
var fs = "standard";
|
|
|
|
| 28 |
if (raw) {
|
| 29 |
var o = JSON.parse(raw);
|
| 30 |
if (o && o.fontSize) {
|
| 31 |
-
if (o.version === V2
|
| 32 |
-
fs = o.fontSize;
|
| 33 |
} else {
|
| 34 |
if (o.fontSize === "comfortable") fs = "standard";
|
| 35 |
else if (o.fontSize === "standard") fs = "small";
|
| 36 |
-
else if (
|
| 37 |
}
|
| 38 |
}
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
document.documentElement.dataset.fontSize = fs;
|
|
|
|
| 41 |
} catch (e) {
|
| 42 |
document.documentElement.dataset.fontSize = "standard";
|
|
|
|
| 43 |
}
|
| 44 |
})();
|
| 45 |
</script>
|
|
|
|
| 13 |
<script>
|
| 14 |
(function () {
|
| 15 |
var V2 = 2;
|
| 16 |
+
var V3 = 3;
|
| 17 |
+
function validFs(fs) {
|
| 18 |
return (
|
| 19 |
fs === "compact" ||
|
| 20 |
fs === "small" ||
|
|
|
|
| 22 |
fs === "large" ||
|
| 23 |
fs === "xlarge"
|
| 24 |
);
|
| 25 |
+
}
|
| 26 |
+
function validTheme(t) {
|
| 27 |
+
return t === "blue" || t === "cream" || t === "gray" || t === "lavender";
|
| 28 |
+
}
|
| 29 |
try {
|
| 30 |
var raw = localStorage.getItem("videogen-ui-settings");
|
| 31 |
var fs = "standard";
|
| 32 |
+
var theme = "blue";
|
| 33 |
if (raw) {
|
| 34 |
var o = JSON.parse(raw);
|
| 35 |
if (o && o.fontSize) {
|
| 36 |
+
if (o.version === V2 || o.version === V3) {
|
| 37 |
+
if (validFs(o.fontSize)) fs = o.fontSize;
|
| 38 |
} else {
|
| 39 |
if (o.fontSize === "comfortable") fs = "standard";
|
| 40 |
else if (o.fontSize === "standard") fs = "small";
|
| 41 |
+
else if (validFs(o.fontSize)) fs = o.fontSize;
|
| 42 |
}
|
| 43 |
}
|
| 44 |
+
if (o && o.colorTheme && validTheme(o.colorTheme)) {
|
| 45 |
+
theme = o.colorTheme;
|
| 46 |
+
}
|
| 47 |
}
|
| 48 |
document.documentElement.dataset.fontSize = fs;
|
| 49 |
+
document.documentElement.dataset.theme = theme;
|
| 50 |
} catch (e) {
|
| 51 |
document.documentElement.dataset.fontSize = "standard";
|
| 52 |
+
document.documentElement.dataset.theme = "blue";
|
| 53 |
}
|
| 54 |
})();
|
| 55 |
</script>
|
web/frontend/src/context/GenerationOptionsContext.tsx
CHANGED
|
@@ -104,16 +104,12 @@ export function GenerationOptionsProvider({ children }: { children: ReactNode })
|
|
| 104 |
);
|
| 105 |
}
|
| 106 |
|
| 107 |
-
/** Image Flash + video Fast — default selection prefers these IDs. */
|
| 108 |
const FAST_MODEL_IDS = new Set([
|
| 109 |
"gemini-3.1-flash-image-preview",
|
| 110 |
"veo-3.1-fast-generate-preview",
|
| 111 |
]);
|
| 112 |
|
| 113 |
-
/**
|
| 114 |
-
* Default model: always the "快速" variant when present in `models`
|
| 115 |
-
* (by id or label), else first entry, else `fallback`.
|
| 116 |
-
*/
|
| 117 |
export function defaultFastModelValue(
|
| 118 |
models: { value: string; label?: string }[],
|
| 119 |
fallback: string,
|
|
|
|
| 104 |
);
|
| 105 |
}
|
| 106 |
|
|
|
|
| 107 |
const FAST_MODEL_IDS = new Set([
|
| 108 |
"gemini-3.1-flash-image-preview",
|
| 109 |
"veo-3.1-fast-generate-preview",
|
| 110 |
]);
|
| 111 |
|
| 112 |
+
/** Prefer Flash/Fast id, else label 快速, else first or `fallback`. */
|
|
|
|
|
|
|
|
|
|
| 113 |
export function defaultFastModelValue(
|
| 114 |
models: { value: string; label?: string }[],
|
| 115 |
fallback: string,
|
web/frontend/src/context/UiSettingsContext.tsx
CHANGED
|
@@ -10,31 +10,38 @@ import {
|
|
| 10 |
} from "react";
|
| 11 |
|
| 12 |
const STORAGE_KEY = "videogen-ui-settings";
|
| 13 |
-
const SETTINGS_VERSION =
|
| 14 |
|
| 15 |
-
/**
|
| 16 |
-
* Keys match English: `standard` = default "中" (Standard). `small` = 较小 (Smaller).
|
| 17 |
-
* Legacy v1 stored `comfortable` for 中 and `standard` for 较小 — migrated on read.
|
| 18 |
-
*/
|
| 19 |
export type FontSizePreset = "compact" | "small" | "standard" | "large" | "xlarge";
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
type UiSettings = {
|
| 22 |
version: number;
|
| 23 |
fontSize: FontSizePreset;
|
|
|
|
| 24 |
};
|
| 25 |
|
| 26 |
const defaultSettings: UiSettings = {
|
| 27 |
version: SETTINGS_VERSION,
|
| 28 |
fontSize: "standard",
|
|
|
|
| 29 |
};
|
| 30 |
|
| 31 |
const PRESETS: FontSizePreset[] = ["compact", "small", "standard", "large", "xlarge"];
|
| 32 |
|
|
|
|
|
|
|
| 33 |
function isPreset(v: string): v is FontSizePreset {
|
| 34 |
return PRESETS.includes(v as FontSizePreset);
|
| 35 |
}
|
| 36 |
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
function migrateV1FontSize(raw: string | undefined): FontSizePreset | null {
|
| 39 |
if (!raw) return null;
|
| 40 |
if (raw === "comfortable") return "standard";
|
|
@@ -50,16 +57,28 @@ function readStored(): UiSettings {
|
|
| 50 |
const parsed = JSON.parse(raw) as {
|
| 51 |
version?: number;
|
| 52 |
fontSize?: string;
|
|
|
|
| 53 |
};
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
| 62 |
}
|
|
|
|
|
|
|
| 63 |
} catch {
|
| 64 |
/* ignore */
|
| 65 |
}
|
|
@@ -70,9 +89,15 @@ function applyFontSizeToDocument(fontSize: FontSizePreset) {
|
|
| 70 |
document.documentElement.dataset.fontSize = fontSize;
|
| 71 |
}
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
type UiSettingsContextValue = {
|
| 74 |
fontSize: FontSizePreset;
|
| 75 |
setFontSize: (v: FontSizePreset) => void;
|
|
|
|
|
|
|
| 76 |
};
|
| 77 |
|
| 78 |
const UiSettingsContext = createContext<UiSettingsContextValue | null>(null);
|
|
@@ -82,6 +107,7 @@ export function UiSettingsProvider({ children }: { children: ReactNode }) {
|
|
| 82 |
const s = readStored();
|
| 83 |
if (typeof document !== "undefined") {
|
| 84 |
applyFontSizeToDocument(s.fontSize);
|
|
|
|
| 85 |
}
|
| 86 |
return s;
|
| 87 |
});
|
|
@@ -90,10 +116,18 @@ export function UiSettingsProvider({ children }: { children: ReactNode }) {
|
|
| 90 |
applyFontSizeToDocument(settings.fontSize);
|
| 91 |
}, [settings.fontSize]);
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
useEffect(() => {
|
| 94 |
localStorage.setItem(
|
| 95 |
STORAGE_KEY,
|
| 96 |
-
JSON.stringify({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
);
|
| 98 |
}, [settings]);
|
| 99 |
|
|
@@ -101,9 +135,18 @@ export function UiSettingsProvider({ children }: { children: ReactNode }) {
|
|
| 101 |
setSettings((s) => ({ ...s, fontSize }));
|
| 102 |
}, []);
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
const value = useMemo(
|
| 105 |
-
() => ({
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
);
|
| 108 |
|
| 109 |
return <UiSettingsContext.Provider value={value}>{children}</UiSettingsContext.Provider>;
|
|
|
|
| 10 |
} from "react";
|
| 11 |
|
| 12 |
const STORAGE_KEY = "videogen-ui-settings";
|
| 13 |
+
const SETTINGS_VERSION = 3;
|
| 14 |
|
| 15 |
+
/** Font scale presets; v1 `comfortable`/`standard` names migrated on read. */
|
|
|
|
|
|
|
|
|
|
| 16 |
export type FontSizePreset = "compact" | "small" | "standard" | "large" | "xlarge";
|
| 17 |
|
| 18 |
+
/** Sidebar/page palette via `html[data-theme]`. */
|
| 19 |
+
export type ColorTheme = "blue" | "cream" | "gray" | "lavender";
|
| 20 |
+
|
| 21 |
type UiSettings = {
|
| 22 |
version: number;
|
| 23 |
fontSize: FontSizePreset;
|
| 24 |
+
colorTheme: ColorTheme;
|
| 25 |
};
|
| 26 |
|
| 27 |
const defaultSettings: UiSettings = {
|
| 28 |
version: SETTINGS_VERSION,
|
| 29 |
fontSize: "standard",
|
| 30 |
+
colorTheme: "blue",
|
| 31 |
};
|
| 32 |
|
| 33 |
const PRESETS: FontSizePreset[] = ["compact", "small", "standard", "large", "xlarge"];
|
| 34 |
|
| 35 |
+
const THEMES: ColorTheme[] = ["blue", "cream", "gray", "lavender"];
|
| 36 |
+
|
| 37 |
function isPreset(v: string): v is FontSizePreset {
|
| 38 |
return PRESETS.includes(v as FontSizePreset);
|
| 39 |
}
|
| 40 |
|
| 41 |
+
function isTheme(v: string): v is ColorTheme {
|
| 42 |
+
return THEMES.includes(v as ColorTheme);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
function migrateV1FontSize(raw: string | undefined): FontSizePreset | null {
|
| 46 |
if (!raw) return null;
|
| 47 |
if (raw === "comfortable") return "standard";
|
|
|
|
| 57 |
const parsed = JSON.parse(raw) as {
|
| 58 |
version?: number;
|
| 59 |
fontSize?: string;
|
| 60 |
+
colorTheme?: string;
|
| 61 |
};
|
| 62 |
+
|
| 63 |
+
let fs: FontSizePreset = "standard";
|
| 64 |
+
if (parsed.fontSize) {
|
| 65 |
+
if (
|
| 66 |
+
(parsed.version === 2 || parsed.version === SETTINGS_VERSION) &&
|
| 67 |
+
isPreset(parsed.fontSize)
|
| 68 |
+
) {
|
| 69 |
+
fs = parsed.fontSize;
|
| 70 |
+
} else {
|
| 71 |
+
const m = migrateV1FontSize(parsed.fontSize);
|
| 72 |
+
if (m) fs = m;
|
| 73 |
+
}
|
| 74 |
}
|
| 75 |
+
|
| 76 |
+
let colorTheme: ColorTheme = "blue";
|
| 77 |
+
if (parsed.colorTheme && isTheme(parsed.colorTheme)) {
|
| 78 |
+
colorTheme = parsed.colorTheme;
|
| 79 |
}
|
| 80 |
+
|
| 81 |
+
return { version: SETTINGS_VERSION, fontSize: fs, colorTheme };
|
| 82 |
} catch {
|
| 83 |
/* ignore */
|
| 84 |
}
|
|
|
|
| 89 |
document.documentElement.dataset.fontSize = fontSize;
|
| 90 |
}
|
| 91 |
|
| 92 |
+
function applyColorThemeToDocument(theme: ColorTheme) {
|
| 93 |
+
document.documentElement.dataset.theme = theme;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
type UiSettingsContextValue = {
|
| 97 |
fontSize: FontSizePreset;
|
| 98 |
setFontSize: (v: FontSizePreset) => void;
|
| 99 |
+
colorTheme: ColorTheme;
|
| 100 |
+
setColorTheme: (v: ColorTheme) => void;
|
| 101 |
};
|
| 102 |
|
| 103 |
const UiSettingsContext = createContext<UiSettingsContextValue | null>(null);
|
|
|
|
| 107 |
const s = readStored();
|
| 108 |
if (typeof document !== "undefined") {
|
| 109 |
applyFontSizeToDocument(s.fontSize);
|
| 110 |
+
applyColorThemeToDocument(s.colorTheme);
|
| 111 |
}
|
| 112 |
return s;
|
| 113 |
});
|
|
|
|
| 116 |
applyFontSizeToDocument(settings.fontSize);
|
| 117 |
}, [settings.fontSize]);
|
| 118 |
|
| 119 |
+
useLayoutEffect(() => {
|
| 120 |
+
applyColorThemeToDocument(settings.colorTheme);
|
| 121 |
+
}, [settings.colorTheme]);
|
| 122 |
+
|
| 123 |
useEffect(() => {
|
| 124 |
localStorage.setItem(
|
| 125 |
STORAGE_KEY,
|
| 126 |
+
JSON.stringify({
|
| 127 |
+
version: SETTINGS_VERSION,
|
| 128 |
+
fontSize: settings.fontSize,
|
| 129 |
+
colorTheme: settings.colorTheme,
|
| 130 |
+
}),
|
| 131 |
);
|
| 132 |
}, [settings]);
|
| 133 |
|
|
|
|
| 135 |
setSettings((s) => ({ ...s, fontSize }));
|
| 136 |
}, []);
|
| 137 |
|
| 138 |
+
const setColorTheme = useCallback((colorTheme: ColorTheme) => {
|
| 139 |
+
setSettings((s) => ({ ...s, colorTheme }));
|
| 140 |
+
}, []);
|
| 141 |
+
|
| 142 |
const value = useMemo(
|
| 143 |
+
() => ({
|
| 144 |
+
fontSize: settings.fontSize,
|
| 145 |
+
setFontSize,
|
| 146 |
+
colorTheme: settings.colorTheme,
|
| 147 |
+
setColorTheme,
|
| 148 |
+
}),
|
| 149 |
+
[settings.fontSize, settings.colorTheme, setFontSize, setColorTheme],
|
| 150 |
);
|
| 151 |
|
| 152 |
return <UiSettingsContext.Provider value={value}>{children}</UiSettingsContext.Provider>;
|
web/frontend/src/index.css
CHANGED
|
@@ -2,12 +2,45 @@
|
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
-
/*
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
html {
|
| 12 |
font-size: 100%;
|
| 13 |
}
|
|
@@ -39,7 +72,7 @@ body {
|
|
| 39 |
}
|
| 40 |
|
| 41 |
::selection {
|
| 42 |
-
background-color: rgb(
|
| 43 |
}
|
| 44 |
|
| 45 |
h1,
|
|
|
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
+
/* Themes: html[data-theme]; RGB triplets for Tailwind alpha (e.g. bg-panel/95). */
|
| 6 |
+
:root,
|
| 7 |
+
html[data-theme="blue"] {
|
| 8 |
+
--color-paper: 238 241 248;
|
| 9 |
+
--color-panel: 216 228 242;
|
| 10 |
+
--color-line: 168 184 208;
|
| 11 |
+
--color-mist: 71 85 105;
|
| 12 |
+
--color-ink: 15 23 42;
|
| 13 |
+
--color-clay: 99 102 241;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
html[data-theme="cream"] {
|
| 17 |
+
--color-paper: 252 250 245;
|
| 18 |
+
--color-panel: 241 236 226;
|
| 19 |
+
--color-line: 214 204 188;
|
| 20 |
+
--color-mist: 87 83 78;
|
| 21 |
+
--color-ink: 15 23 42;
|
| 22 |
+
--color-clay: 120 113 108;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
html[data-theme="gray"] {
|
| 26 |
+
--color-paper: 244 244 246;
|
| 27 |
+
--color-panel: 232 232 235;
|
| 28 |
+
--color-line: 186 190 198;
|
| 29 |
+
--color-mist: 82 82 91;
|
| 30 |
+
--color-ink: 15 23 42;
|
| 31 |
+
--color-clay: 82 82 91;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
html[data-theme="lavender"] {
|
| 35 |
+
--color-paper: 247 246 252;
|
| 36 |
+
--color-panel: 234 231 245;
|
| 37 |
+
--color-line: 196 191 220;
|
| 38 |
+
--color-mist: 78 74 98;
|
| 39 |
+
--color-ink: 15 23 42;
|
| 40 |
+
--color-clay: 109 40 217;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Rem scale: html[data-font-size] from UiSettingsContext / index.html */
|
| 44 |
html {
|
| 45 |
font-size: 100%;
|
| 46 |
}
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
::selection {
|
| 75 |
+
background-color: rgb(var(--color-clay) / 0.22);
|
| 76 |
}
|
| 77 |
|
| 78 |
h1,
|
web/frontend/src/pages/Layout.tsx
CHANGED
|
@@ -2,7 +2,11 @@ import { useEffect, useState } from "react";
|
|
| 2 |
import { Link, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
| 3 |
import { fetchMe, logout } from "../api";
|
| 4 |
import { GenerationOptionsProvider } from "../context/GenerationOptionsContext";
|
| 5 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
type NavItem = { to: string; title: string; sub: string };
|
| 8 |
|
|
@@ -39,6 +43,13 @@ function NavLink({ item, active }: { item: NavItem; active: boolean }) {
|
|
| 39 |
);
|
| 40 |
}
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
const FONT_SIZE_OPTIONS: { value: FontSizePreset; label: string; labelEn: string }[] = [
|
| 43 |
{ value: "compact", label: "小", labelEn: "Extra small" },
|
| 44 |
{ value: "small", label: "较小", labelEn: "Small" },
|
|
@@ -48,7 +59,7 @@ const FONT_SIZE_OPTIONS: { value: FontSizePreset; label: string; labelEn: string
|
|
| 48 |
];
|
| 49 |
|
| 50 |
export function Layout() {
|
| 51 |
-
const { fontSize, setFontSize } = useUiSettings();
|
| 52 |
const [ok, setOk] = useState<boolean | null>(null);
|
| 53 |
const loc = useLocation();
|
| 54 |
const navigate = useNavigate();
|
|
@@ -114,6 +125,24 @@ export function Layout() {
|
|
| 114 |
</div>
|
| 115 |
|
| 116 |
<div className="mt-auto space-y-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
<div>
|
| 118 |
<p className="text-sm font-semibold text-ink tracking-wide mb-2">字体大小</p>
|
| 119 |
<label htmlFor="ui-font-size" className="sr-only">
|
|
|
|
| 2 |
import { Link, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
| 3 |
import { fetchMe, logout } from "../api";
|
| 4 |
import { GenerationOptionsProvider } from "../context/GenerationOptionsContext";
|
| 5 |
+
import {
|
| 6 |
+
type ColorTheme,
|
| 7 |
+
type FontSizePreset,
|
| 8 |
+
useUiSettings,
|
| 9 |
+
} from "../context/UiSettingsContext";
|
| 10 |
|
| 11 |
type NavItem = { to: string; title: string; sub: string };
|
| 12 |
|
|
|
|
| 43 |
);
|
| 44 |
}
|
| 45 |
|
| 46 |
+
const COLOR_THEME_OPTIONS: { value: ColorTheme; label: string }[] = [
|
| 47 |
+
{ value: "blue", label: "淡蓝" },
|
| 48 |
+
{ value: "cream", label: "淡米黄" },
|
| 49 |
+
{ value: "gray", label: "浅灰" },
|
| 50 |
+
{ value: "lavender", label: "淡紫灰" },
|
| 51 |
+
];
|
| 52 |
+
|
| 53 |
const FONT_SIZE_OPTIONS: { value: FontSizePreset; label: string; labelEn: string }[] = [
|
| 54 |
{ value: "compact", label: "小", labelEn: "Extra small" },
|
| 55 |
{ value: "small", label: "较小", labelEn: "Small" },
|
|
|
|
| 59 |
];
|
| 60 |
|
| 61 |
export function Layout() {
|
| 62 |
+
const { fontSize, setFontSize, colorTheme, setColorTheme } = useUiSettings();
|
| 63 |
const [ok, setOk] = useState<boolean | null>(null);
|
| 64 |
const loc = useLocation();
|
| 65 |
const navigate = useNavigate();
|
|
|
|
| 125 |
</div>
|
| 126 |
|
| 127 |
<div className="mt-auto space-y-4">
|
| 128 |
+
<div>
|
| 129 |
+
<p className="text-sm font-semibold text-ink tracking-wide mb-2">界面配色</p>
|
| 130 |
+
<label htmlFor="ui-color-theme" className="sr-only">
|
| 131 |
+
界面配色
|
| 132 |
+
</label>
|
| 133 |
+
<select
|
| 134 |
+
id="ui-color-theme"
|
| 135 |
+
value={colorTheme}
|
| 136 |
+
onChange={(e) => setColorTheme(e.target.value as ColorTheme)}
|
| 137 |
+
className="w-full rounded-lg border border-slate-400/90 bg-white px-3 py-2.5 text-base text-ink shadow-sm focus:outline-none focus:ring-2 focus:ring-clay/40"
|
| 138 |
+
>
|
| 139 |
+
{COLOR_THEME_OPTIONS.map((o) => (
|
| 140 |
+
<option key={o.value} value={o.value}>
|
| 141 |
+
{o.label}
|
| 142 |
+
</option>
|
| 143 |
+
))}
|
| 144 |
+
</select>
|
| 145 |
+
</div>
|
| 146 |
<div>
|
| 147 |
<p className="text-sm font-semibold text-ink tracking-wide mb-2">字体大小</p>
|
| 148 |
<label htmlFor="ui-font-size" className="sr-only">
|
web/frontend/src/pages/Login.tsx
CHANGED
|
@@ -42,8 +42,8 @@ export function Login() {
|
|
| 42 |
}
|
| 43 |
|
| 44 |
return (
|
| 45 |
-
<div className="min-h-screen flex items-center justify-center p-6 sm:p-10 bg-gradient-to-br from-paper via-
|
| 46 |
-
<div className="w-full max-w-md rounded-2xl border border-line/80 bg-white/85 backdrop-blur-sm shadow-xl shadow-
|
| 47 |
<h1 className="font-display text-3xl font-semibold text-ink mb-2">AI创作台</h1>
|
| 48 |
<p className="text-base text-mist mb-8">输入密码后即可使用</p>
|
| 49 |
<form onSubmit={onSubmit} className="space-y-5">
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
return (
|
| 45 |
+
<div className="min-h-screen flex items-center justify-center p-6 sm:p-10 bg-gradient-to-br from-paper via-panel/30 to-paper">
|
| 46 |
+
<div className="w-full max-w-md rounded-2xl border border-line/80 bg-white/85 backdrop-blur-sm shadow-xl shadow-black/10 p-10 sm:p-12">
|
| 47 |
<h1 className="font-display text-3xl font-semibold text-ink mb-2">AI创作台</h1>
|
| 48 |
<p className="text-base text-mist mb-8">输入密码后即可使用</p>
|
| 49 |
<form onSubmit={onSubmit} className="space-y-5">
|
web/frontend/tailwind.config.js
CHANGED
|
@@ -18,18 +18,13 @@ export default {
|
|
| 18 |
serif: ['"Source Serif 4"', "Georgia", "serif"],
|
| 19 |
},
|
| 20 |
colors: {
|
| 21 |
-
/**
|
| 22 |
-
paper: "
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
ink: "#0f172a",
|
| 29 |
-
/** Accents: focus rings, links, subtle highlights */
|
| 30 |
-
clay: "#6366f1",
|
| 31 |
-
/** Secondary / muted text — readable but clearly below ink */
|
| 32 |
-
mist: "#475569",
|
| 33 |
},
|
| 34 |
},
|
| 35 |
},
|
|
|
|
| 18 |
serif: ['"Source Serif 4"', "Georgia", "serif"],
|
| 19 |
},
|
| 20 |
colors: {
|
| 21 |
+
/** Theme tokens — RGB channels in index.css per html[data-theme] */
|
| 22 |
+
paper: "rgb(var(--color-paper) / <alpha-value>)",
|
| 23 |
+
panel: "rgb(var(--color-panel) / <alpha-value>)",
|
| 24 |
+
line: "rgb(var(--color-line) / <alpha-value>)",
|
| 25 |
+
ink: "rgb(var(--color-ink) / <alpha-value>)",
|
| 26 |
+
clay: "rgb(var(--color-clay) / <alpha-value>)",
|
| 27 |
+
mist: "rgb(var(--color-mist) / <alpha-value>)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
},
|
| 29 |
},
|
| 30 |
},
|