# 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.