Under normal load on the shared host, a /wheelofnames GIF takes 11-12s and a /gacha wish 14-15s to render, so the 15s default turned ordinary renders into 504s. The default is now 30s, and the bot waits 45s so it never abandons a render the renderer is still allowed to finish.
5.9 KiB
renderer
Part of miti99bot: the service behind /wheelofnames, /gacha, and
/genshin, deployed by the root compose.yml as the renderer service.
Self-hosted API that renders wheel-of-names GIF animations and Genshin-style
meteor wish MP4 animations with Remotion, and card-pack gacha wish MP4
animations with pack-cards.
API
POST /api/gif
Content-Type: application/json
Accept: image/gif
{
"options": ["alice", "bob", "carol"],
"winnerIndex": 1,
"durationMs": 6500,
"holdMs": 1200,
"fps": 15,
"size": 512,
"theme": "classic"
}
Response is image/gif with winner metadata headers:
X-Wheel-Winner-IndexX-Wheel-WinnerX-Render-Duration-Ms
X-Wheel-Winner is URL-encoded so non-ASCII labels are safe in HTTP headers.
Gacha wish
POST /api/gacha
Content-Type: application/json
Accept: video/mp4
{
"label": "Bún bò",
"rarity": 5,
"fps": 24,
"width": 640
}
Renders a 6-second portrait wish: a collectible card pack from
pack-cards is torn open, its card
spins out with a burst of sparkles, and lands showing the request's label,
its rank, and its stars. rarity (required) picks the rank (B for 3★, A
for 4★, S for 5★), the star count, the card's material (rare, epic,
legendary), and the colour (blue, purple, gold). 4★ and 5★ cards are engraved
with pack-cards' rarity textures (contour on 4★; rings or facets on 5★, chosen
by the label) drawn in a still polychrome rainbow; 3★ cards are plain.
fps is 24 or 30. width is the long edge of the portrait frame:
640 renders 360×640 and 854 renders 480×854. Optional seed (integer,
0 to 2147483647) seeds the page's randomness; when omitted the service
picks a random one, so every roll looks different. The caller chooses the
result and its rarity — the service only draws it.
Response is a silent H.264 video/mp4 (Telegram plays it as an animation)
with X-Gacha-Rarity and X-Render-Duration-Ms headers. All routes share the
RENDERER_MAX_CONCURRENT_RENDERS slots. No game assets are used.
pack-cards animates on the browser clock, so the wish does not use Remotion
compositions. src/render/render-gacha.js keeps one headless Chrome per
server process in --deterministic-mode, steps virtual time one frame at a
time, tears the pack with a scripted drag, captures each frame, and encodes
them with Remotion's bundled ffmpeg. The page (src/gacha/page/) and the
package are served from disk; the page has no network access. The first render
compiles the pack's WebGL shaders in software and the first card of each
engraving rasterises its texture, which takes several seconds, so server
start-up renders a throwaway 5★ and 4★ wish first.
pack-cards has no npm release, so it is installed from a GitHub tarball pinned
to a commit. A moving branch URL would change the tarball's checksum and break
npm ci against the lockfile, and the Docker image has no git for a git
dependency.
Genshin wish
POST /api/genshin takes the same body as /api/gacha and returns the same
response, rendering a 7-second landscape wish in the style of a gacha game: a
meteor coloured by rarity (blue 3★, purple 4★, gold 5★) flies in from the left
across a night sky and bursts in a white flash where the rank emblem appears,
and the label is revealed beside a rank emblem (B, A, S) with its stars
popping in. Each tier is louder than the one below: 4★ adds a bigger meteor, a
lens flare, impact shake, and a double shockwave; 5★ adds a rainbow sunburst
before landing, a gold sky flood, a starburst, counter rotating rays, falling
sparkles, and a sheen across the emblem. The per-tier table lives in
src/remotion/genshin-timeline.js. width is 640 (360 tall) or 854 (480
tall), and seed lays out the twinkling stars and particles. All visuals are
drawn procedurally; no game assets are used.
Local
Install dependencies and Chromium once:
npm install
npm run browser:ensure
Start the local API:
npm run dev
Generate GIF files locally
Generate the quick smoke fixtures at the git-ignored paths
fixtures/smoke.gif, fixtures/genshin-5-star.mp4, and fixtures/gacha-5-star.mp4:
npm run render:smoke
Generate the complete fixture set at fixtures/smoke.gif,
fixtures/vietnamese.gif, fixtures/sixteen-options.gif, and
fixtures/genshin-{3,4,5}-star.mp4, and fixtures/gacha-{3,4,5}-star.mp4:
npm run render:fixtures
Render a custom GIF directly without starting the API server:
npm run render:local -- `
--output wheel.gif `
--option "Chiều nay uống CraneTea" `
--option "Chiều nay uống CraneTea" `
--option "Cà phê" `
--winner 1
macOS, Linux, or Git Bash:
npm run render:local -- \
--output wheel.gif \
--option "Chiều nay uống CraneTea" \
--option "Chiều nay uống CraneTea" \
--option "Cà phê" \
--winner 1
--winner is a zero-based index and is random when omitted. Run
npm run render:local -- --help for duration, hold, FPS, size, theme, and timeout
options. The documented root wheel.gif and fixtures/*.gif/*.mp4 outputs are
git-ignored and safe to delete; custom output paths may need their own ignore
rule.
Verify
Run the API smoke test and quality gates:
npm run api:smoke
npm run lint
npm run typecheck
npm test
Deploy
Use a container runtime first. Static-only platforms cannot satisfy
POST /api/gif because Remotion server rendering needs Node, Chromium/runtime
dependencies, and FFmpeg/compositor support.
docker build -t miti99bot-renderer .
docker run --rm -p 3000:3000 miti99bot-renderer
Recommended starting resources: 1-2 vCPU and 1-2 GB RAM, with
RENDERER_MAX_CONCURRENT_RENDERS=1. RENDERER_RENDER_TIMEOUT_MS defaults to
30000 and is raised to Remotion's 7000ms browser timeout floor when configured lower.
The API has no authentication; publish its port only on a trusted network.