initial commit
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
# Evanescere
|
||||
|
||||
Evanescere turns recorded livestreams into suggested, rendered clips:
|
||||
|
||||
1. Poll IIS WebDAV for finished recordings.
|
||||
2. Wait until file size is unchanged across polling cycles.
|
||||
3. Download the stable source file to Framework-local storage.
|
||||
4. Remux FLV/H264 to MP4 and extract ASR-ready audio.
|
||||
5. Transcribe Mandarin audio through a FunASR-compatible API.
|
||||
6. Ask DeepSeek for ranked timeline-aware clip suggestions.
|
||||
7. Generate subtitles, render clips, generate thumbnails with optional local image generation and VTuber overlay, and optionally upload.
|
||||
|
||||
The Framework Arch server is the intended production host. The Hyper-V Arch VM can be used for development, API testing, and database/control-plane work.
|
||||
|
||||
## Architecture
|
||||
|
||||
Evanescere is split into a static frontend and an API/worker backend.
|
||||
|
||||
- `frontend`: React/Vite UI. It builds to static files and is served by nginx in Docker.
|
||||
- `src/evanescere/api.py`: FastAPI backend. It serves JSON only.
|
||||
- `src/evanescere/scheduler.py`: polling loop for IIS WebDAV.
|
||||
- `src/evanescere/jobs.py`: Dramatiq queue actors backed by Redis.
|
||||
- `src/evanescere/pipeline.py`: orchestration for media, ASR, LLM, render, thumbnail, and upload stages.
|
||||
- `src/evanescere/services`: adapters for WebDAV, ffmpeg, FunASR, DeepSeek, subtitles, thumbnails, artifacts, and upload.
|
||||
- `migrations`: Alembic migrations for PostgreSQL.
|
||||
|
||||
Runtime data flow:
|
||||
|
||||
```text
|
||||
IIS WebDAV recordings
|
||||
-> scheduler polls file size
|
||||
-> PostgreSQL records video state
|
||||
-> Redis queues pipeline job
|
||||
-> worker downloads source to configured storage.local_root
|
||||
-> ffmpeg remuxes/extracts audio
|
||||
-> FunASR creates timestamped transcript
|
||||
-> DeepSeek creates clip suggestions
|
||||
-> ffmpeg renders clips/subtitles/thumbnails
|
||||
-> thumbnail service composes frame/generated image + VTuber overlay + title
|
||||
-> uploader adapter runs, or noop for testing
|
||||
```
|
||||
|
||||
The backend tracks durable state in PostgreSQL. Large media artifacts stay on the Framework SSD under `[storage].local_root`; WebDAV is used for source ingestion and can later be used for pushing final artifacts back to the Windows server.
|
||||
|
||||
## Configuration
|
||||
|
||||
Runtime configuration is TOML-based. Copy the commented example and edit your private config:
|
||||
|
||||
```bash
|
||||
cp config.example.toml config.toml
|
||||
```
|
||||
|
||||
The application looks for config in this order:
|
||||
|
||||
1. `/etc/evanescere/config.toml`
|
||||
2. `./config.toml`
|
||||
3. Built-in development defaults, only if no config file exists.
|
||||
|
||||
For Docker/Compose, mount your config file to:
|
||||
|
||||
```text
|
||||
/etc/evanescere/config.toml
|
||||
```
|
||||
|
||||
The checked-in `docker-compose.yml` mounts local `./config.toml` there for the API, workers, and scheduler.
|
||||
|
||||
## Services And Ports
|
||||
|
||||
- Frontend: `http://localhost:3000`
|
||||
- Backend API: `http://localhost:8000`
|
||||
- API docs: `http://localhost:8000/docs`
|
||||
- Redis: `localhost:6379` when exposed by Compose
|
||||
- PostgreSQL: only started by Compose when using the `local-db` profile
|
||||
|
||||
## Test Environment
|
||||
|
||||
Create and edit `config.toml`:
|
||||
|
||||
```bash
|
||||
cp config.example.toml config.toml
|
||||
```
|
||||
|
||||
For a self-contained local test stack, use the Compose PostgreSQL profile:
|
||||
|
||||
```bash
|
||||
docker compose --profile local-db up --build
|
||||
```
|
||||
|
||||
Run database migrations:
|
||||
|
||||
```bash
|
||||
docker compose run --rm api alembic upgrade head
|
||||
```
|
||||
|
||||
Bootstrap existing WebDAV files so old recordings are marked `existing_done` instead of auto-processed:
|
||||
|
||||
```bash
|
||||
docker compose run --rm api evanescere bootstrap-existing
|
||||
```
|
||||
|
||||
Useful test commands:
|
||||
|
||||
```bash
|
||||
docker compose run --rm api evanescere scan
|
||||
docker compose run --rm api evanescere videos
|
||||
docker compose run --rm api evanescere run-video VIDEO_ID
|
||||
docker compose logs -f api scheduler worker-media worker-ai
|
||||
```
|
||||
|
||||
Automated tests are written with `pytest`. After installing Python dev dependencies:
|
||||
|
||||
```bash
|
||||
pytest -q
|
||||
```
|
||||
|
||||
Without installing dependencies locally, the available lightweight checks are:
|
||||
|
||||
```bash
|
||||
python -m compileall src tests migrations
|
||||
docker compose config --quiet
|
||||
```
|
||||
|
||||
For safe test/debug runs, set these in `config.toml`:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
log_level = "DEBUG"
|
||||
|
||||
[defaults]
|
||||
upload_enabled = false
|
||||
|
||||
[upload]
|
||||
adapter = "noop"
|
||||
```
|
||||
|
||||
`adapter = "noop"` prevents accidental real uploads. `upload_enabled = false` lets you inspect rendered artifacts before adding an uploader.
|
||||
|
||||
## Frontend Development
|
||||
|
||||
The frontend reads the backend URL from `frontend/public/config.js` at runtime:
|
||||
|
||||
```js
|
||||
window.__EVANESCERE_FRONTEND_CONFIG__ = {
|
||||
apiBaseUrl: "http://localhost:8000"
|
||||
};
|
||||
```
|
||||
|
||||
Manual frontend development requires npm packages. Install only after you approve package installation:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The Vite dev server runs on `http://localhost:5173`. Make sure `[app].cors_origins` includes that URL.
|
||||
|
||||
Static build:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
The production Compose frontend service builds the static app and serves `dist/` through nginx.
|
||||
|
||||
## Thumbnail Generation
|
||||
|
||||
Each rendered clip can produce three thumbnail artifacts:
|
||||
|
||||
- `thumbnail_base`: a representative frame extracted from the source MP4 near the middle of the clip.
|
||||
- `thumbnail_generated`: optional output from a local image generation command.
|
||||
- `thumbnail_final`: the final composed JPG with optional title and VTuber character overlay.
|
||||
|
||||
Default provider:
|
||||
|
||||
```toml
|
||||
[thumbnail]
|
||||
provider = "frame_overlay"
|
||||
```
|
||||
|
||||
This uses the extracted frame as the background, then programmatically overlays the title and character PNG using Pillow.
|
||||
|
||||
To overlay the VTuber character, use a transparent PNG:
|
||||
|
||||
```toml
|
||||
[thumbnail]
|
||||
character_overlay_path = "/data/evanescere/assets/vtuber.png"
|
||||
character_scale = 0.42
|
||||
character_position = "bottom-right"
|
||||
```
|
||||
|
||||
Supported positions are `bottom-right`, `bottom-left`, `center-right`, and `center-left`.
|
||||
|
||||
For a local image generation model, configure a command provider:
|
||||
|
||||
```toml
|
||||
[thumbnail]
|
||||
provider = "command"
|
||||
command = "/opt/evanescere-thumbnail/generate-thumbnail"
|
||||
```
|
||||
|
||||
The command receives JSON on stdin and must create the file named by `output_path`. This makes it easy to wrap ComfyUI, Stable Diffusion, Flux, or any other local generator. Payload shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"video_id": 123,
|
||||
"clip_id": 456,
|
||||
"source_frame": "/data/evanescere/videos/123/clips/456/thumbnail_base.jpg",
|
||||
"output_path": "/data/evanescere/videos/123/clips/456/thumbnail_generated.png",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"title_zh": "标题",
|
||||
"summary_zh": "摘要",
|
||||
"reason": "推荐原因",
|
||||
"tags": ["tag"],
|
||||
"transcript": [
|
||||
{"start_sec": 10.0, "end_sec": 15.0, "text": "片段台词"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
After the command writes `thumbnail_generated.png`, Evanescere still applies the same title and VTuber overlay to produce `thumbnail_final.jpg`.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
On the Framework Arch server:
|
||||
|
||||
1. Install Docker/Compose.
|
||||
2. Copy `config.example.toml` to `config.toml`.
|
||||
3. Set `[database].url` to the reachable PostgreSQL server.
|
||||
4. Set `[redis].url = "redis://redis:6379/0"` if using Compose Redis.
|
||||
5. Set `[storage].local_root = "/data/evanescere"` and mount a Framework SSD directory there.
|
||||
6. Set `[webdav]` credentials and `base_url`.
|
||||
7. Set `[funasr].base_url` to the local FunASR service.
|
||||
8. Set `[deepseek].api_key`.
|
||||
9. Keep `[upload].adapter = "noop"` until the full pipeline has been tested.
|
||||
|
||||
Start production services:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
docker compose run --rm api alembic upgrade head
|
||||
docker compose run --rm api evanescere bootstrap-existing
|
||||
```
|
||||
|
||||
Then watch logs:
|
||||
|
||||
```bash
|
||||
docker compose logs -f api scheduler worker-media worker-ai
|
||||
```
|
||||
|
||||
If you use your existing PostgreSQL server instead of the Compose `local-db` profile, do not start the profile. Ensure PostgreSQL accepts connections from the Framework host and that `[database].url` uses the reachable host/IP.
|
||||
|
||||
## Config Reference
|
||||
|
||||
The full commented example lives in `config.example.toml`. These are the keys the backend reads:
|
||||
|
||||
| Section | Key | Default | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `app` | `env` | `dev` | Free-form environment label included in logs. Use `prod` on production. |
|
||||
| `app` | `log_level` | `INFO` | Python logging level. Use `DEBUG` for pipeline bring-up. |
|
||||
| `app` | `log_sql` | `false` | Enables SQLAlchemy SQL logging and engine echo. Noisy in production. |
|
||||
| `app` | `cors_origins` | local frontend URLs | Array of frontend origins allowed to call FastAPI. |
|
||||
| `database` | `url` | local PostgreSQL URL | SQLAlchemy PostgreSQL connection string. |
|
||||
| `redis` | `url` | local Redis URL | Redis broker URL for Dramatiq jobs. |
|
||||
| `storage` | `local_root` | `./storage` | Root directory for downloaded sources, remuxed MP4s, audio, transcripts, subtitles, clips, and thumbnails. |
|
||||
| `webdav` | `base_url` | local placeholder | IIS WebDAV directory containing recording files. |
|
||||
| `webdav` | `username` | empty | WebDAV basic-auth username, if required. |
|
||||
| `webdav` | `password` | empty | WebDAV basic-auth password, if required. |
|
||||
| `webdav` | `verify_tls` | `true` | Whether WebDAV HTTPS certificates are verified. |
|
||||
| `webdav` | `poll_interval_seconds` | `60` | Scheduler interval. A file is stable after two equal positive size samples. |
|
||||
| `funasr` | `base_url` | local FunASR URL | FunASR-compatible API base URL. The client calls `/audio/transcriptions`. |
|
||||
| `funasr` | `api_key` | empty | Optional bearer token for the FunASR service. |
|
||||
| `funasr` | `model` | `paraformer-zh` | Model name sent to FunASR. |
|
||||
| `deepseek` | `base_url` | `https://api.deepseek.com` | OpenAI-compatible DeepSeek API base URL. |
|
||||
| `deepseek` | `api_key` | empty | DeepSeek API key. Required when clip suggestion is enabled. |
|
||||
| `deepseek` | `model` | `deepseek-v4-pro` | Model used for clip suggestion. |
|
||||
| `deepseek` | `temperature` | `0.2` | Sampling temperature for clip suggestion. |
|
||||
| `defaults` | `suggest_enabled` | `true` | Initial setting for automatic LLM clip suggestion. |
|
||||
| `defaults` | `render_enabled` | `true` | Initial setting for automatic rendering. |
|
||||
| `defaults` | `upload_enabled` | `true` | Initial setting for automatic upload after render. For testing, set false. |
|
||||
| `defaults` | `preserve_final_artifacts` | `true` | Whether rendered final artifacts are marked for preservation. |
|
||||
| `defaults` | `bake_subtitles` | `true` | Whether rendered clips include hard-baked subtitles by default. |
|
||||
| `clip` | `min_seconds` | `30` | Minimum LLM clip duration accepted by backend. |
|
||||
| `clip` | `max_seconds` | `360` | Maximum LLM clip duration accepted by backend. |
|
||||
| `clip` | `transcript_chunk_seconds` | `900` | Transcript seconds sent to DeepSeek per request. |
|
||||
| `thumbnail` | `enabled` | `true` | Enables thumbnail generation during clip render. |
|
||||
| `thumbnail` | `provider` | `frame_overlay` | `frame_overlay` or `command`. |
|
||||
| `thumbnail` | `width` | `1920` | Final thumbnail width in pixels. |
|
||||
| `thumbnail` | `height` | `1080` | Final thumbnail height in pixels. |
|
||||
| `thumbnail` | `character_overlay_path` | empty | Optional transparent PNG of the VTuber character. |
|
||||
| `thumbnail` | `character_scale` | `0.42` | Character overlay height as a fraction of final thumbnail height. |
|
||||
| `thumbnail` | `character_position` | `bottom-right` | Character placement. |
|
||||
| `thumbnail` | `title_enabled` | `true` | Draws the clip title onto the final thumbnail. |
|
||||
| `thumbnail` | `command` | empty | Local image generation command used when `provider = "command"`. |
|
||||
| `upload` | `adapter` | `noop` | `noop` or `command`. |
|
||||
| `upload` | `command` | empty | Command invoked when `adapter = "command"`; receives JSON metadata on stdin. |
|
||||
|
||||
Frontend runtime config:
|
||||
|
||||
| File | Key | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `frontend/public/config.js` | `apiBaseUrl` | URL the static frontend uses to call the backend. Change this when hosting frontend/backend on different hosts. |
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
evanescere scan
|
||||
evanescere bootstrap-existing
|
||||
evanescere videos
|
||||
evanescere run-video VIDEO_ID
|
||||
evanescere transcribe VIDEO_ID
|
||||
evanescere suggest VIDEO_ID
|
||||
evanescere render CLIP_ID
|
||||
evanescere upload CLIP_ID
|
||||
```
|
||||
|
||||
Inside Compose, prefix commands with:
|
||||
|
||||
```bash
|
||||
docker compose run --rm api
|
||||
```
|
||||
|
||||
Useful API endpoints for generated files:
|
||||
|
||||
- `GET /videos/{video_id}/artifacts`
|
||||
- `GET /clips/{clip_id}/artifacts`
|
||||
- `GET /artifacts/{artifact_id}`
|
||||
|
||||
Thumbnail outputs are visible through the artifact endpoints. Upload command payloads include `thumbnail` when a `thumbnail_final` artifact exists.
|
||||
|
||||
## Debugging
|
||||
|
||||
Set verbose logs in `config.toml`:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
log_level = "DEBUG"
|
||||
log_sql = false
|
||||
```
|
||||
|
||||
Then inspect service logs:
|
||||
|
||||
```bash
|
||||
docker compose logs -f scheduler
|
||||
docker compose logs -f worker-media
|
||||
docker compose logs -f worker-ai
|
||||
docker compose logs -f api
|
||||
```
|
||||
|
||||
What to look for:
|
||||
|
||||
- Scheduler logs `webdav propfind done`, `new webdav file observed`, and `webdav file stable`.
|
||||
- Worker logs `pipeline start`, `prepare media`, `transcription start`, `clip suggestion start`, `render start`, and `upload start`.
|
||||
- ffmpeg command failures include the failing command and stderr tail.
|
||||
- DeepSeek logs include transcript chunk bounds, character counts, candidate counts, and usage when returned by the SDK.
|
||||
- Thumbnail logs include frame extraction, local image generation command execution, VTuber overlay path, and final artifact path.
|
||||
- API logs include method, path, status code, and elapsed time for each frontend request.
|
||||
|
||||
Enable SQL logs only when diagnosing database behavior:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
log_sql = true
|
||||
```
|
||||
|
||||
This is intentionally noisy and should usually stay off in production.
|
||||
|
||||
## Notes
|
||||
|
||||
- FunASR/ROCm setup is intentionally outside the main Compose file for now. Benchmark the Framework host manually before binding the project to a specific GPU runtime.
|
||||
- The uploader adapter is deliberately small. `[upload].adapter = "command"` is enough to integrate a Bilibili uploader later without changing the pipeline core.
|
||||
- Existing recordings should be bootstrapped before scheduler-driven production runs, otherwise old stable files may be queued as new work.
|
||||
Reference in New Issue
Block a user