Files

263 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 port `8080`. The Compose file publishes
that port on the host. Restrict it with the host firewall if the server is on
an untrusted network, or change the mapping to `127.0.0.1:8080:8080` after
confirming loopback Docker forwarding works on the deployment host.
The container runs as a dedicated non-root user and stores runtime data in a
named volume. 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.