Files

10 KiB

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:

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:

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 also defines 480p, 1080p, 1K, 2K, and 4K, but each model supports only a subset. Confirm the current capabilities through the video-models endpoint 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:

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:

    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:

    APP_ENV=production
    PUBLIC_BASE_URL=https://preface-tools.internal.example.com
    
  3. Build and start the service:

    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:

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:

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:

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:

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.