Evanescere
Evanescere turns recorded livestreams into suggested, rendered clips:
- Poll IIS WebDAV for finished recordings.
- Record files visible during the first scan as a manual-only historical baseline.
- For files first seen later, wait until file size is unchanged across polling cycles.
- Download the stable source file to Framework-local storage.
- Remux FLV/H264 to MP4 and extract ASR-ready audio.
- Transcribe Mandarin audio through a FunASR-compatible API.
- Ask DeepSeek for ranked timeline-aware clip suggestions.
- 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:
IIS WebDAV recordings
-> first scan records existing files as manual-only baseline entries
-> later scans poll new file sizes
-> 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:
cp config.example.toml config.toml
The application looks for config in this order:
/etc/evanescere/config.toml./config.toml- Built-in development defaults, only if no config file exists.
For Docker/Compose, mount your config file to:
/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:8081 - API docs:
http://localhost:8081/docs - Redis:
localhost:6379when exposed by Compose - PostgreSQL: only started by Compose when using the
local-dbprofile
Test Environment
Create and edit config.toml:
cp config.example.toml config.toml
For a self-contained local test stack, use the Compose PostgreSQL profile:
docker compose --profile local-db up --build
Run database migrations:
docker compose run --rm api alembic upgrade head
The scheduler automatically records files visible during its first scan as existing_done. Those historical recordings appear in the UI but are not automatically processed. To establish that baseline manually before starting the scheduler:
docker compose run --rm api evanescere bootstrap-existing
In the frontend, click Sync WebDAV to perform the same safe first-time baseline import or to discover later files. Select any historical FLV row and click its play button to manually queue a test pipeline run.
Useful test commands:
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:
pytest -q
Without installing dependencies locally, the available lightweight checks are:
python -m compileall src tests migrations
docker compose config --quiet
For safe test/debug runs, set these in config.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:
window.__EVANESCERE_FRONTEND_CONFIG__ = {
apiBaseUrl: "http://192.168.1.44:8081"
};
Manual frontend development requires npm packages. Install only after you approve package installation:
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:
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:
[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, place transparent PNG variants in a directory. Evanescere picks one randomly for each thumbnail:
[thumbnail]
character_overlay_dir = "/data/evanescere/assets/"
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:
[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:
{
"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:
- Install Docker/Compose.
- Copy
config.example.tomltoconfig.toml. - Set
[database].urlto the reachable PostgreSQL server. - Set
[redis].url = "redis://redis:6379/0"if using Compose Redis. - Set
[storage].local_root = "/data/evanescere"and mount a Framework SSD directory there. - Set
[webdav]credentials andbase_url. - Set
[funasr].base_urlto the local FunASR service. - Set
[deepseek].api_key. - Keep
[upload].adapter = "noop"until the full pipeline has been tested.
Start production services:
docker compose up -d --build
docker compose run --rm api alembic upgrade head
docker compose run --rm api evanescere bootstrap-existing
Then watch logs:
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. |
llm_prompt |
system |
VTuber clip editor prompt | Custom system prompt for clip selection. The required JSON schema is appended by code. |
llm_prompt |
user |
timestamped transcript prompt | Custom user prompt template. Supports documented placeholders. |
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. |
clip |
max_candidates_total |
20 |
Maximum suggestions kept across the entire transcript after overlap deduplication. |
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_dir |
/data/evanescere/assets/ |
Directory of transparent VTuber PNG variants. One is selected randomly per thumbnail. |
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
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:
docker compose run --rm api
Useful API endpoints for generated files:
POST /webdav/scanGET /videos/{video_id}/artifactsGET /clips/{clip_id}/artifactsGET /artifacts/{artifact_id}
Thumbnail outputs are visible through the artifact endpoints. Upload command payloads include thumbnail when a thumbnail_final artifact exists.
Custom LLM Prompt
Edit [llm_prompt] in your private config.toml to tune clip selection without rebuilding containers:
[llm_prompt]
system = """
Your custom editorial guidance.
"""
user = """
Choose up to {max_candidates} clips between {min_clip_seconds} and {max_clip_seconds} seconds.
Stream range: {chunk_start_sec}-{chunk_end_sec}
Transcript:
{transcript}
"""
Available user-prompt placeholders:
{max_candidates}{min_clip_seconds}{max_clip_seconds}{chunk_start_sec}{chunk_end_sec}{transcript}
The backend appends its strict JSON response schema to the system message so custom prompt wording cannot accidentally remove the machine-readable output contract.
Debugging
Set verbose logs in config.toml:
[app]
log_level = "DEBUG"
log_sql = false
Then inspect service logs:
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, andwebdav file stable. - On first startup, scheduler logs
webdav baseline missingand stores all currently visible recordings asexisting_done. - Worker logs
pipeline start,prepare media,transcription start,clip suggestion start,render start, andupload 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:
[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 are automatically baselined on the first scheduler scan. The explicit
evanescere bootstrap-existingcommand remains available when you want to establish or refresh the baseline before starting services.