264 lines
10 KiB
Markdown
264 lines
10 KiB
Markdown
# Preface Tools
|
|
|
|
Preface Tools is a small, database-free internal classroom application. Its
|
|
Comic Animator workflow lets a student:
|
|
|
|
1. upload a complete comic page;
|
|
2. describe the movement they want;
|
|
3. generate and edit an image-to-video prompt with an OpenRouter LLM; and
|
|
4. ask an instructor to approve the paid video-generation request with a PIN.
|
|
|
|
The application has separate student and instructor sessions, CSRF protection,
|
|
rate-limited PIN checks, signed provider-facing image URLs, bounded uploads and
|
|
downloads, and an instructor recovery view for completed files.
|
|
|
|
## Requirements
|
|
|
|
- Docker Engine with Docker Compose v2 for the recommended deployment, or Go
|
|
1.26 or later for a local source build.
|
|
- An OpenRouter API key with access and sufficient credit for both configured
|
|
models.
|
|
- A public HTTPS URL that OpenRouter can reach. OpenRouter fetches the uploaded
|
|
comic through a short-lived signed URL when starting image-to-video jobs.
|
|
- A TLS-terminating reverse proxy for production. Caddy, Traefik, nginx, or an
|
|
existing internal ingress is sufficient.
|
|
|
|
## Quick start for local development
|
|
|
|
Copy the environment template and replace all secret placeholders:
|
|
|
|
```sh
|
|
cp .env.example .env
|
|
go run ./cmd/preface-tools
|
|
```
|
|
|
|
Open `http://localhost:8080`. A local-only `PUBLIC_BASE_URL` is enough to view
|
|
the interface, but video generation cannot work until that value is an HTTPS
|
|
address reachable by OpenRouter. A temporary HTTPS tunnel is suitable for
|
|
development.
|
|
|
|
The executable loads `.env` automatically and does not overwrite variables
|
|
already supplied by the process environment.
|
|
|
|
## Configuration
|
|
|
|
The supplied model and video defaults are:
|
|
|
|
```env
|
|
COMIC_ANIMATOR_PROMPT_MODEL=openai/gpt-5.6-luna
|
|
COMIC_ANIMATOR_VIDEO_MODEL=alibaba/happyhorse-1.1
|
|
COMIC_ANIMATOR_VIDEO_DURATION=4
|
|
COMIC_ANIMATOR_VIDEO_RESOLUTION=720p
|
|
COMIC_ANIMATOR_GENERATE_AUDIO=false
|
|
```
|
|
|
|
`720p` is the recommended balance of cost, generation time, and classroom
|
|
quality. [OpenRouter's video API](https://openrouter.ai/docs/guides/overview/multimodal/video-generation)
|
|
also defines `480p`, `1080p`, `1K`, `2K`, and `4K`, but each model supports only
|
|
a subset. Confirm the current capabilities through the
|
|
[video-models endpoint](https://openrouter.ai/docs/api/api-reference/video-generation/list-videos-models)
|
|
before changing resolution or duration. HappyHorse 1.1 advertises output up to
|
|
1080p; unsupported combinations will be rejected by the provider.
|
|
|
|
Important application variables:
|
|
|
|
| Variable | Purpose | Example/default |
|
|
| --- | --- | --- |
|
|
| `APP_ENV` | Enables secure production cookies when set to `production` | `development` |
|
|
| `HTTP_ADDRESS` | Server listen address | `:8080` |
|
|
| `PUBLIC_BASE_URL` | Public HTTPS origin reachable by OpenRouter | required |
|
|
| `STUDENT_PIN` | Shared classroom login PIN | required |
|
|
| `INSTRUCTOR_PIN` | Instructor login and paid-action approval PIN | required |
|
|
| `SESSION_SIGNING_SECRET` | Signs browser sessions; at least 32 characters | required |
|
|
| `SESSION_DURATION` | Browser session lifetime | `4h` |
|
|
| `LOG_LEVEL` | `debug`, `info`, `warn`, or `error` | `info` |
|
|
| `LOG_FORMAT` | `text` or `json` | `text` |
|
|
| `COMIC_ANIMATOR_OPENROUTER_API_KEY` | OpenRouter bearer token | required |
|
|
| `COMIC_ANIMATOR_PROMPT_MODEL` | Multimodal model that writes the video prompt | `openai/gpt-5.6-luna` |
|
|
| `COMIC_ANIMATOR_VIDEO_MODEL` | Image-to-video model | `alibaba/happyhorse-1.1` |
|
|
| `COMIC_ANIMATOR_PROMPT_FILE` | Reloadable LLM system-prompt path | `prompts/comic-animator-system.txt` |
|
|
| `COMIC_ANIMATOR_VIDEO_DURATION` | Requested video length in seconds | `4` |
|
|
| `COMIC_ANIMATOR_VIDEO_RESOLUTION` | Provider-supported resolution | `720p` |
|
|
| `COMIC_ANIMATOR_GENERATE_AUDIO` | Requests provider audio when supported | `false` |
|
|
| `COMIC_ANIMATOR_UPLOAD_DIR` | Temporary uploaded comic storage | `data/comic-animator/uploads` |
|
|
| `COMIC_ANIMATOR_OUTPUT_DIR` | Completed video storage | `data/comic-animator/outputs` |
|
|
| `COMIC_ANIMATOR_MAX_UPLOAD_BYTES` | Maximum source-image size | `20971520` (20 MiB) |
|
|
| `COMIC_ANIMATOR_MAX_VIDEO_BYTES` | Maximum downloaded video size | `536870912` (512 MiB) |
|
|
| `COMIC_ANIMATOR_QUEUE_CAPACITY` | In-memory generation queue capacity | `100` |
|
|
| `COMIC_ANIMATOR_POLL_INTERVAL` | Provider status polling frequency | `30s` |
|
|
| `COMIC_ANIMATOR_JOB_TIMEOUT` | Whole video-job deadline | `15m` |
|
|
| `COMIC_ANIMATOR_HTTP_TIMEOUT` | Individual OpenRouter request deadline | `60s` |
|
|
| `COMIC_ANIMATOR_SIGNING_SECRET` | Signs temporary image URLs; at least 32 characters | required |
|
|
| `COMIC_ANIMATOR_SIGNED_URL_TTL` | Provider image URL lifetime | `30m` |
|
|
|
|
Optional `COMIC_ANIMATOR_OPENROUTER_SITE_URL` and
|
|
`COMIC_ANIMATOR_OPENROUTER_APP_NAME` values populate OpenRouter attribution
|
|
headers. The complete template, including HTTP timeout settings, is in
|
|
`.env.example`.
|
|
|
|
Generate independent secrets rather than copying the placeholders:
|
|
|
|
```sh
|
|
openssl rand -hex 32
|
|
openssl rand -hex 32
|
|
```
|
|
|
|
Use the two results for `SESSION_SIGNING_SECRET` and
|
|
`COMIC_ANIMATOR_SIGNING_SECRET`. Choose non-trivial, different student and
|
|
instructor PINs. The `.env` file is ignored by Git; keep it readable only by the
|
|
deployment account.
|
|
|
|
## Editing the LLM system prompt
|
|
|
|
The LLM system message lives in
|
|
`prompts/comic-animator-system.txt`. The server reads it for every Generate
|
|
Prompt request, so saving the file changes the next request without restarting
|
|
the application. The student's movement description and uploaded image remain
|
|
a separate user message.
|
|
|
|
The generated `video_prompt` string is displayed in the third panel and can be
|
|
edited before it is sent to the video model. Missing, empty, or oversized system
|
|
prompt files fail safely; the default file is also checked during startup.
|
|
|
|
With Compose, the local `prompts` directory is mounted read-only inside the
|
|
container. Edit the host file normally; the updated content is visible to the
|
|
running process immediately.
|
|
|
|
## Production deployment with Docker Compose
|
|
|
|
1. Copy and secure the environment file:
|
|
|
|
```sh
|
|
cp .env.example .env
|
|
chmod 600 .env
|
|
```
|
|
|
|
2. Set at least the API key, PINs, signing secrets, and public URL. The public
|
|
URL must be the final HTTPS origin, without a path, for example:
|
|
|
|
```env
|
|
APP_ENV=production
|
|
PUBLIC_BASE_URL=https://preface-tools.internal.example.com
|
|
```
|
|
|
|
3. Build and start the service:
|
|
|
|
```sh
|
|
docker compose up -d --build
|
|
docker compose ps
|
|
docker compose logs -f preface-tools
|
|
```
|
|
|
|
4. Put a TLS reverse proxy in front of `127.0.0.1:8080`. The Compose file binds
|
|
only to loopback deliberately. If TLS is terminated by an ingress on another
|
|
host, adjust the `ports` mapping or use an external Docker network while
|
|
keeping the application container otherwise private.
|
|
|
|
The container runs as a non-root user with all Linux capabilities dropped, a
|
|
read-only root filesystem, `no-new-privileges`, and a named volume for runtime
|
|
data. It exposes:
|
|
|
|
- `GET /healthz` for liveness;
|
|
- `GET /readyz` for readiness.
|
|
|
|
Docker checks `/readyz` every 30 seconds. To inspect it manually:
|
|
|
|
```sh
|
|
curl -fsS http://127.0.0.1:8080/readyz
|
|
```
|
|
|
|
### Reverse-proxy notes
|
|
|
|
- Forward the original `Host` header and use HTTPS externally.
|
|
- Set `Strict-Transport-Security` at the TLS reverse proxy after confirming the
|
|
hostname is HTTPS-only.
|
|
- Do not expose port 8080 directly to an untrusted network.
|
|
- Allow normal video response sizes and request durations; completed downloads
|
|
may take several minutes on slow links.
|
|
- `PUBLIC_BASE_URL` must resolve publicly from OpenRouter even if the login UI
|
|
itself is restricted by VPN, identity-aware proxy, or network policy. The
|
|
provider-media route is protected by a short-lived signature and exposes only
|
|
the requested uploaded image.
|
|
- The browser currently loads DaisyUI/HTMX from jsDelivr and the Preface logo
|
|
from `preface.ai`, so client networks must allow those hosts.
|
|
|
|
## Usage
|
|
|
|
### Student workflow
|
|
|
|
1. Sign in with the student PIN.
|
|
2. Upload one PNG, JPEG, or WebP comic page, up to 20 MiB by default.
|
|
3. Describe panel movement and click **Generate Prompt**. The button is disabled
|
|
while the request is running.
|
|
4. Review or edit the generated video prompt.
|
|
5. Click **Generate Video** and ask an instructor to enter their PIN.
|
|
6. Follow progress in Recent Animations, then play or download the completed
|
|
video.
|
|
|
|
Recent student generations belong to that browser session. Logging out and back
|
|
in creates a new student session and therefore a new recent-generation view.
|
|
|
|
### Instructor workflow
|
|
|
|
Sign in with the instructor PIN to see generations retained by the current
|
|
process and completed output files found on disk. The disk-backed output view is
|
|
useful after restarts, but it is not a complete audit log.
|
|
|
|
## Storage, backups, and restarts
|
|
|
|
Uploads and generation metadata are held in memory. A restart loses queued and
|
|
in-progress jobs, student recent-generation associations, and detailed prompt
|
|
metadata. Completed video files are stored in the configured output directory
|
|
and survive Compose restarts in the `preface-data` volume.
|
|
|
|
OpenRouter does not provide this application with a complete restart recovery
|
|
mechanism for its asynchronous job state. Avoid deploying multiple replicas:
|
|
sessions can reach any replica, while uploads, queues, and job registries are
|
|
process-local. A single worker processes video generations sequentially; later
|
|
approved requests remain in the bounded in-memory queue.
|
|
|
|
Uploads and outputs are not automatically deleted. Monitor volume usage and
|
|
establish an internal retention process. Back up or export the named volume if
|
|
completed videos must be retained:
|
|
|
|
```sh
|
|
docker compose stop
|
|
docker run --rm -v preface-tools_preface-data:/data -v "$PWD":/backup \
|
|
alpine tar czf /backup/preface-data.tgz -C /data .
|
|
docker compose start
|
|
```
|
|
|
|
Adjust the generated volume name if the Compose project name differs.
|
|
|
|
## Updating and rollback
|
|
|
|
Build before replacing the running container, then inspect health and logs:
|
|
|
|
```sh
|
|
docker compose build --pull
|
|
docker compose up -d
|
|
docker compose ps
|
|
docker compose logs --tail=100 preface-tools
|
|
```
|
|
|
|
For repeatable production rollbacks, tag images in a registry and replace
|
|
`preface-tools:local` in `compose.yml` with an immutable version tag rather than
|
|
building directly on the server.
|
|
|
|
## Verification
|
|
|
|
Run the local checks before deployment:
|
|
|
|
```sh
|
|
gofmt -w .
|
|
go test ./...
|
|
go test -race ./...
|
|
go vet ./...
|
|
go build ./...
|
|
docker compose config
|
|
docker build -t preface-tools:test .
|
|
```
|
|
|
|
The automated test suite does not make live OpenRouter calls and does not spend
|
|
API credit.
|