ShizukuTranslate is an AI translation service for Japanese, Korean, and Chinese novels. The source language is detected by the model and the target language is chosen per request (zh-CN by default, vi and en supported), so one work can be read in more than one language. The repository contains a Vue web application, a Spring Boot API, and a Chrome/Edge Manifest V3 extension for translating Pixiv novels in place.
-
Novel translation through DeepSeek, OpenAI-compatible, or Anthropic-compatible model endpoints.
-
Personal model profiles with reusable provider API keys: one personal API key can be shared by multiple model profiles. The profile page can proxy provider model-list detection and still permits manual model names when detection is unavailable.
-
Streaming translation over Server-Sent Events (SSE), with cancellation support in the web UI. Both request paths stream: image pages arrive token by token just like text, instead of showing nothing until the whole multimodal call returns. The stream carries a comment heartbeat every 25 seconds, so long silent stages (chunked-term audit, slow pre-fill) survive the proxies, NATs, and firewalls on the path that drop idle TCP connections.
-
Reasoning toggle on the translate page: “Reasoning mode” asks DeepSeek models to think before translating (higher quality, roughly 6× slower). Thinking runs deliberately skip the shared-translation replay — a shared copy may come from either mode — and the personal cache is keyed by thinking mode, so a fast-path result never shadows a reasoning result.
-
Translation cache for streaming requests. Cache entries are keyed by user, provider, endpoint, model, thinking mode, prompt, and source text, and are removed after 30 days.
-
Shared translations: before calling the model, a request reuses the newest translation of the same source text into the same target language made by any other user (streaming and non-streaming). The response is tagged
fromSharedTranslation, and a personal cache hit is taggedfromCache; the web UI shows a matching badge. AskipCacherequest bypasses both and forces a fresh model call — the web UI's "Re-translate" button.Shared translation: before calling the model, a request reuses the newest translation another user produced for the same source text into the same target language (applies to both streaming and non-streaming). The response carries a
fromSharedTranslationflag, a personal cache hit carriesfromCache, and the web UI badges the result accordingly; askipCacherequest skips both and forces a fresh model call (the web UI's "Retranslate" button). -
Preset prompts for series-specific style rules, plus a terminology glossary. Presets live in a database table that administrators manage through the admin panel (create, edit, delete), so style rules can be tuned without a redeployment;
app.presetsin the configuration file only seeds missing names on first startup. Each glossed concept stores one spelling per language (Japanese, Korean, Chinese, Vietnamese), and the request's target language decides which spelling is rendered into the prompt — the right-hand side is always the target language, so another language's output can never leak in. Adding a language is a configuration change underapp.translation.target-languages, not a code change. An optional custom prompt is appended last. -
Interface languages (Simplified Chinese, Vietnamese, and English) through vue-i18n. The interface language also sets the default translation target, so a reader who switches the site to Vietnamese gets Vietnamese output without touching the language picker; the picker itself stays available as an override.
-
Pixiv import panel (a standalone card above the translate box): paste a Pixiv novel link (or a bare work id) and the backend fetches the novel text through Pixiv's AJAX endpoint and drops it into the translate box, with Pixiv's control tags (
[newpage], ruby, formatting) reduced to plain text — or paste/drop a screenshot of a novel page and let the vision model extract its title/author/tags/summary — the matching work is then imported from a Pixiv search automatically, and the screenshot stays visible afterwards (the card's clear button removes it when you are done). A short list of related works is offered only when no outright match is found. The work's title, author, tags, and summary (Pixiv HTML stripped) are shown on the card with a one-click clear (✕) button; a "Translate title & summary" button translates them on their own, and an opt-in checkbox prepends them as a labelled block to the next translation so title/author/tags/summary are translated together with the body. Public novels work without a Pixiv session; login-gated novels (R-18) are importable when the server-side session cookie is configured (seePIXIV_SESSION_COOKIE) and are reported clearly when it is not. -
Long-novel pipeline: long texts translate in paragraph-aligned chunks on one continuous SSE stream, each chunk carrying the previous chunk's source and translation tails as a continuity anchor; the chunk size follows the model's output budget — 42,000 characters for DeepSeek, 4,000 for the small-output models (
Index-Translate-35B-A3B, which reports a 4096-token cap in practice whatevermax_tokensasks for, and Anthropic-compatible profiles), where a larger chunk could never finish; an opt-in coined-term correction pre-extracts proper nouns across the whole text (in thinking mode), injects the table into every chunk, and audits the finished translation for terminology drift with deterministic replacements. Above 30,000 characters the UI gates the request: with the correction off, a dialog asks whether to enable it first (long texts without it easily drift on names and numbers); with it on, the translation opens a dedicated focus page (/long-novel) that narrates the pipeline stages (think → chunk → output → audit), streams the output with follow-along scrolling, and offers copy/cancel — without the announcement panel, import card, or any other controls. -
Novel translation attachments: upload TXT/MD files for automatic text parsing, or upload up to ten images at once for multimodal model processing with a site vision model (
deepseek-flashor the keylessIndex-Translate-35B-A3B; all pages are translated in one call, in upload order). Word/PDF files are not supported yet. Image uploads follow the same streaming switch as text: with "Stream output" on, the status line narrates the upload and then the incoming translation, and cancelling aborts either phase. When the model stops at its output cap (finish_reason=length) the result is still saved and shown, but flagged as cut off with advice to submit less at once — a capped answer is never presented as a complete translation. The flag is stored on the history row and kept in the restored last-result context, so a reload or the history detail page cannot turn it back into a finished answer. It also applies to the text path:Index-Translate-35B-A3Bstops at 4096 output tokens however muchmax_tokensasks for, and Anthropic-compatible profiles cap text output at 8192, so a capped text translation is deliberately not written to the translation cache and a half translation is never replayed as a cache hit. -
Accounts and access control with JWT login, API keys for the browser extension, email verification codes, and administrator-only usage and announcement management.
-
Translation history stored per user.
-
Token usage tracking for live model calls, with personal totals and administrator charts, per-user summaries, and detailed logs.
-
Markdown announcements rendered in the web application: the panel lists every announcement in a scrollable region, newest first; long announcements fold to five lines until expanded, and the whole panel can be collapsed. Announcement content is stored as Markdown and raw HTML is not executed.
-
Feedback submission through the authenticated survey endpoint.
-
Translation quality feedback pipeline: every displayed result carries a
requestId, so a low-key one-click rating widget (one click submits 1–5; scores of 3 or below offer optional reason tags, and any score can carry an optional free-text reason) and behaviour events (copy / re-translate / edit) all join back to the exact translation. Ordinary traffic is rate-sampled (2% by default) while dissatisfaction signals — a low rating, a re-translation, an edit — retain that sample in full. All text is redacted (e-mail, phone, Chinese ID, bank card, URL query values, mentions, plus a configurable term list) before anything is persisted, and everything is stored as UTC-dated JSONL underapp.feedback.dir. The administrator panel's Feedback tab reads it back, andtools/export_feedback.py+tools/verify_feedback_export.pyexport and validate a date range.
The tranShilator-plugin/ directory contains the Pixiv & GitHub Translator Chrome/Edge Manifest V3 extension. The current manifest version is 1.5.0 (build metadata in version.js is generated from the release commit).
The extension supports:
- A floating side-panel translation view (used as a fallback when inline paragraphs cannot be located).
- Inline translation under paragraphs on the current Pixiv page (the default display mode).
- GitHub page translation: on any
github.compage a floating panel collects the natural-language body (README, issue and PR descriptions, comments, release notes, wiki pages), translates it in numbered batches, and streams each paragraph's translation under its source block. Code blocks, file lists, diffs, navigation and page chrome are never translated; the translated text is rendered as plain text, so links and emphasis are flattened. A Turbo navigation clears the previous page's translations before the new body is scanned. - Full-novel inline translation with global paragraph IDs mapped across Pixiv page breaks.
- Paged mode that translates the whole novel at once and renders it in the floating window, split into blocks at Pixiv's
[newpage]markers. - Chinese, English, and Korean output.
- Site-provided models — DeepSeek and the keyless bilibili Index-Translate — plus the user's saved model profiles.
- Multiple server URL presets, selectable translation presets, and a custom prompt.
- Optional DeepSeek thinking mode.
- Automatic translation, per-tab cancellation, and service-worker keep-alive alarms for long requests.
- Progress-time liveness probes: if the browser reclaims the extension's background worker mid-translation, the progress button resets itself with an explicit "connection lost" notice instead of staying on "pre-filling" with the seconds climbing forever. When the failure is actually an extension update leaving a stale page behind, it says so and asks for a page refresh instead.
- A read-idle timeout on the translation stream: a stream that sends nothing at all — not even a heartbeat — for 90 seconds is reported as interrupted instead of hanging forever.
- One-click retranslation, targeted repair of missing numbered paragraphs, and local error-log submission from the popup.
- A history button that opens the web application's translation history.
- Release update checks at browser startup and every six hours, plus a manual update check in the popup.
- One-click authorization: the popup requests a device code, opens the consent page in a new tab, and stores the resulting API key by itself. Manual key entry stays available as a fallback.
The extension can translate public Pixiv novels without a Pixiv login; login-gated novels additionally require an authenticated Pixiv session. GitHub pages are translated on demand from the in-page panel or the popup, with no automatic translation. All translations require a reachable ShizukuTranslate backend URL and an extension API key, either fetched with the popup's one-click authorization or generated on the profile page and pasted in by hand. Because users may configure any HTTP(S) deployment and change it without rebuilding the extension, the Manifest V3 package requests HTTP/HTTPS host access. The extension uses that access for the configured backend and Pixiv requests, and injects a content script only on pixiv.net/novel/* and github.com/*; it does not inject content scripts into arbitrary sites. Unpacked browser extensions cannot silently replace their own files, so updates must be applied through the bundled Windows updater and then reloaded in the browser's extension management page.
+----------------------+ +------------------------+ +----------------------+
| Vue 3 web application| ----> | Spring Boot API | ----> | DeepSeek or |
| TypeScript / Vite | <---- | JWT and API-key auth | | compatible provider |
+----------------------+ | SSE / JPA / H2 | +----------------------+
+-----------+------------+
+----------------------+ +------------------------+
| Pixiv novel page | ----> | MV3 extension |
| content.js | <---- | background.js |
+----------------------+ +-----------+------------+
|
v
ShizukuTranslate SSE API
| Layer | Technology |
|---|---|
| Web frontend | Vue 3, TypeScript, Vite, Pinia, Vue Router, Axios, vue-i18n |
| Backend | Java 21 source/target, Spring Boot 3.2.0, Spring Data JPA, H2, Spring Security, JWT |
| AI integration | DeepSeek API, OpenAI-compatible chat completions, Anthropic Messages API |
| Browser extension | Chrome/Edge Manifest V3, vanilla JavaScript, SSE |
| Windows updater | .NET 8 Windows Forms, self-contained win-x64 executable |
1. content.js extracts the Pixiv novel ID and sends it to background.js.
2. background.js fetches the novel from https://www.pixiv.net/ajax/novel/{id}.
3. background.js sends the source text to /api/v1/translate/stream with X-API-Key.
4. The backend calls the selected model provider and returns SSE events.
5. background.js forwards tokens to content.js, which renders the selected view.
GitHub pages take a shorter path with the same backend contract:
1. github-content.js collects leaf blocks inside .markdown-body and numbers them [1]..[N].
2. background.js splits the blocks into batches (60 blocks / 12k chars) and sends
the numbered source plus a GitHub-specific prompt to /api/v1/translate/stream.
3. The model answers with JSON Lines ({"id":N,"text":"..."}), streamed as tokens.
4. github-content.js accumulates the stream, parses complete objects and renders each
translation directly under its source block.
The extension makes a best-effort PHPSESSID check before requesting a novel and includes browser-managed Pixiv credentials for login-gated content. It does not expose the user's configured model API keys to the extension: model profile responses requested with an extension API key omit API key previews.
On Windows, download a release package and run the bundled tranShilator-plugin/CheckUpdate.exe. The updater downloads the latest release zip and installs the extension under:
%LOCALAPPDATA%\PixivNovelTranslator\tranShilator-plugin
Then:
- Open
edge://extensionsorchrome://extensions. - Enable Developer mode.
- Choose Load unpacked and select the installed extension directory.
- Log in to Pixiv, open the extension popup, and set the backend URL. Then either use the authorization button to fetch an extension API key automatically or paste one generated on the profile page.
The standalone updater supports:
CheckUpdate.exe --path <extension-folder>
CheckUpdate.exe --force
CheckUpdate.exe --no-pause
--force reinstalls the current version. --no-pause is retained for script compatibility. The updater checks the release version and validates the GitHub asset SHA-256 digest when one is available. It keeps a timestamped backup of the previous extension directory. After updating, click Reload on the extension card and refresh the Pixiv tab.
For local development, enable Developer mode on the browser extension page, choose Load unpacked, and select the repository's tranShilator-plugin/ directory. Configure the backend URL in the popup, then authorize or paste an extension API key.
The repository includes a self-contained .NET 8 Windows GUI updater. On Windows with the .NET 8 SDK installed:
dotnet publish tranShilator-plugin/updatechecking/CheckUpdate.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o tranShilator-pluginThe resulting tranShilator-plugin/CheckUpdate.exe is placed beside the extension files and does not require .NET to be installed on the user's machine.
- JDK 21 for the backend compiler configuration.
- Node.js 20+ and npm for the frontend.
- Python for the local deployment script, which requires a modern Python version supporting the repository's type-hint syntax.
- Maven for the backend build.
- OpenSSH for the deployment workflow.
- A DeepSeek API key if the server should provide the default DeepSeek models or fallback service key.
cd ShizukuTranslate
$env:DEEPSEEK_API_KEY = "your-deepseek-api-key"
mvn spring-boot:runThe backend listens on http://localhost:5566 and exposes the API under http://localhost:5566/api/v1. It serves the frontend from src/main/resources/static when a production frontend build has been copied there.
cd ShizukuTranslate-frontend
npm ci
npm run devVite listens on http://localhost:5173. The frontend uses http://localhost:5566/api/v1 by default. Set VITE_API_BASE_URL before starting Vite to use another backend:
$env:VITE_API_BASE_URL = "http://localhost:5566/api/v1"
npm run devThe About page receives the build timestamp and short Git commit from vite.config.js during a frontend build.
The web application's profile page supports multiple saved model profiles and reusable personal provider API keys. Each profile contains:
- A display name.
- A provider protocol:
deepseek,openai, oranthropic. - A model name.
- A reference to a personal API key (legacy inline keys are migrated lazily).
- A base URL for compatible providers.
One API key may be used by multiple profiles. The authenticated endpoint POST /auth/model-profiles/detect proxies the provider's /models endpoint; model detection is optional and manual model entry remains supported. Model selectors use slash-separated labels such as 站方/deepseek-flash, 站方/deepseek-v4-pro, 站方/Index-Translate-35B-A3B, and 我的配置/openai/gpt-5.6-sol.
The defaults are:
| Provider | Default base URL | API key behavior |
|---|---|---|
| DeepSeek | The server's deepseek.api.base-url setting |
An empty personal key falls back to DEEPSEEK_API_KEY |
| OpenAI-compatible | https://api.openai.com/v1 |
A personal API key is required |
| Anthropic-compatible | https://api.anthropic.com/v1 |
A personal API key is required |
DeepSeek requests use /chat/completions. OpenAI-compatible requests use the same format. Anthropic-compatible requests use /messages and translate Anthropic usage fields into the application's token usage format. API keys are not returned in full; the authenticated web profile shows a masked preview, while extension model-profile responses omit the preview.
To build a backend JAR containing the latest frontend assets:
# Build the frontend
cd ShizukuTranslate-frontend
npm ci
npm run build
# Replace the backend static directory
Remove-Item ..\ShizukuTranslate\src\main\resources\static -Recurse -Force -ErrorAction SilentlyContinue
New-Item ..\ShizukuTranslate\src\main\resources\static -ItemType Directory -Force
Copy-Item dist\* ..\ShizukuTranslate\src\main\resources\static\ -Recurse
# Package the backend
cd ..\ShizukuTranslate
mvn clean package -DskipTestsThe deployment workflows are:
GitHub Actions (.github/workflows/deploy.yml, started manually with gh workflow run deploy.yml): builds the frontend and backend on a hosted runner, backs up the live jar and configuration file, uploads the jar, syncs deepseek.api.default-model from the repository application.yml, restarts the shizuku-backend systemd unit, verifies the release, and rolls back automatically when any step fails.
Local script (tools/ship.py, a local-only tool not tracked in Git):
python tools/ship.py # full release: pull, build, upload, reconfigure, restart, verify
python tools/ship.py --recon # read-only status of the production host
python tools/ship.py --no-build # upload the existing target/translator.jar
python tools/ship.py --no-restart # upload only
python tools/ship.py --rollback # restore the most recent backup and restart
python tools/ship.py --skip-pull # skip git pull
ship.py reads credentials from SHIZUKU_HOST / SHIZUKU_USER / SHIZUKU_PWD / SHIZUKU_KEY, then from D:\CodeWhaleData\secrets\shizuku_server.json, then from ~/.ssh/id_rsa. Both workflows target a Debian host running the shizuku-backend systemd unit (port 5566); the nginx virtual host on port 80 proxies to 5566. Both have remote side effects and are not portable local-only build commands.
Sh1Zuku_Translate/
├── ShizukuTranslate/ # Spring Boot backend and production static files
│ ├── src/main/java/com/shizuku/translate/
│ │ ├── config/ # Application, CORS, model, and security configuration
│ │ ├── controller/ # Auth, translation, history, survey, admin, and announcement APIs
│ │ ├── dto/ # Request and response DTOs
│ │ ├── entity/ # JPA entities, model profiles, cache, and usage logs
│ │ ├── exception/ # Global exception handler and custom exceptions
│ │ ├── integration/ # AI provider clients and protocol adapters
│ │ ├── repository/ # Spring Data repositories
│ │ ├── security/ # JWT and API-key authentication filters
│ │ └── service/ # Translation, user, survey, usage, and announcement services
│ └── src/main/resources/
│ ├── application.yml # Runtime settings, target languages, glossary, preset seeds
│ └── static/ # Copied production frontend assets
├── ShizukuTranslate-frontend/ # Vue 3 and TypeScript frontend
│ └── src/
│ ├── api/ # Axios API client and SSE streaming
│ ├── components/ # Preset, result, and announcement components
│ ├── i18n/ # vue-i18n setup plus per-language message packs
│ ├── router/ # Vue Router routes and auth guards
│ ├── stores/ # Pinia stores
│ ├── types/ # TypeScript interfaces
│ ├── utils/ # Shared utilities, including Markdown rendering
│ └── views/ # Translation, history, profile, survey, logs, admin, and auth pages
├── tranShilator-plugin/ # Chrome/Edge extension and CheckUpdate.exe
│ └── updatechecking/ # .NET updater source project
└── tools/ # Local-only tooling (git-ignored)
├── build_extension.py # Generates extension build metadata
└── ship.py # Local build, packaging, upload, and systemd restart workflow
| Variable | Description |
|---|---|
DEEPSEEK_API_KEY |
Server-wide DeepSeek key. Required when using the site-provided DeepSeek models or when a DeepSeek profile has no personal key. |
JWT_SECRET |
Required in every deployment. Use a strong random value; blank and known weak defaults are rejected, so the backend will not start without it. |
JWT_ISSUER |
JWT issuer claim. Default: shizuku-translate. |
MAIL_HOST |
SMTP host for verification-code emails. Leave blank to disable sending (registration and email verification then fail with a clear error). |
MAIL_PORT |
SMTP port. Default: 465 (SSL). |
MAIL_USERNAME / MAIL_PASSWORD |
SMTP account credentials. |
MAIL_FROM |
From address for verification emails. Defaults to MAIL_USERNAME. |
MAIL_SSL |
Use implicit SSL/TLS (true) or STARTTLS (false). Default: true. |
CORS_ALLOWED_ORIGIN_PATTERNS |
Comma-separated allowed browser origins. Default: localhost frontend/backend origins. |
STREAM_CORE_POOL_SIZE |
Core threads for streaming translations. Default: 4. |
STREAM_MAX_POOL_SIZE |
Maximum streaming translation threads. Default: 16. |
STREAM_QUEUE_CAPACITY |
Queued streaming requests before rejection. Default: 64; saturation returns a clear request-rejection error. |
PIXIV_SESSION_COOKIE |
PHPSESSID value of a server-side Pixiv account (with R-18 display enabled in that account's settings). With it set, the importer can fetch login-gated (R-18) novels and the search endpoint returns R-18 results; blank means public works only. The value is only ever sent to pixiv.net. A daily health check probes the session and e-mails the administrators (once per outage, auto-cleared on recovery) when it expires — no silent breakage. |
VITE_API_BASE_URL |
Frontend build/development API base URL override. Default: http://localhost:5566/api/v1. |
Other runtime defaults in application.yml:
- Backend HTTP port:
5566. - DeepSeek base URL:
https://api.deepseek.com/v1. - Default DeepSeek model:
deepseek-flash(DeepSeek-V4.1-Flash). - Site-provided models:
deepseek-flash(natively multimodal) and the keylessIndex-Translate-35B-A3B(bilibili's public translation model, served fromhttps://index-translate.bilibili.com/v1, which also accepts image input), plus text-onlydeepseek-v4-pro. The retired namesdeepseek-v4-flashanddeepseek-v4-flash-vision-expstill resolve because the DeepSeek API routes them to V4.1 Flash. - DeepSeek thinking mode: disabled by default; requests may override it.
- H2 file database:
./data/translatordb. - Database connection pool: HikariCP, 10 connections, 10 s acquisition timeout, plus a 60 s leak-detection sentinel that logs a stack trace if a connection is ever held longer than that. Two settings keep translation from starving the pool, and both are needed for different reasons.
spring.jpa.open-in-viewis deliberatelyfalse: while it is enabled Spring binds an EntityManager to the whole request and the connection it acquired is only released when the response is written, so a single slow upstream model call pins a pooled connection and every other request then fails withHikariPool-1 - Connection is not available. Separately, no transaction wraps an upstream model call -TranslationService.translate/translateImages/translateImagesStreamdo the HTTP call outside any transaction and hand the database writes toTranslationResultWriter, which owns a short one; splitting the transaction alone is not sufficient (the connection is then borrowed on the first query instead of atdoBegin, and is still held for the whole call), it is the combination that leaves the pool idle. Measured locally with three concurrent 15 s translations against a 3-connection pool: with open-in-view on the pool went toidle=0and a fourth request got500 / Connection is not available; with it off the pool stayedidle=3and the fourth request succeeded. Keep both properties the way they are. - Multipart limits: 20 MB per file and 60 MB per request (a multi-image upload arrives as one request).
- Translation cache cleanup: entries older than 30 days are removed daily at 03:00.
- Shared translations: served from
translation_recordsby exact source-text and resolved target-language match across users; legacy rows without atargetLanguagenever match, so nothing is served in the wrong language. - Administrator usernames: configured by
app.admin-usernamesinapplication.yml. - Target languages:
app.translation.target-languages(currentlyzh-CN,vi, anden), withapp.translation.default-target-languageas the fallback. A translate request may carrytargetLanguage; a missing or unrecognised value falls back to the default, so older clients keep behaving exactly as before. - Series terminology:
app.glossaryseeds theglossary_conceptsandglossary_termstables. Seeding runs per series and only when that series has no concepts yet, so edits made later (for example through a future admin UI) survive a restart. A preset whose name matches aseriesinjects that series' terms into the system prompt. - Glossaries are anchored on concepts, not language pairs: a nickname and a full name are deliberately separate concepts because they translate differently, and each concept carries one spelling per language. Japanese proper nouns are not rendered with Sino-Vietnamese readings; Vietnamese readers use the romanised name as-is (
Iroha,Kaguya,Yachiyo), which is what thevicolumn holds.
Registration requires a six-digit code delivered to the address by email, which prevents throwaway (fake-mailbox) accounts from registering and draining the server's token budget. Accounts created before this feature was enabled are unverified (email_verified is NULL for existing rows); they keep their history and settings but receive HTTP 403 from the translation endpoints and from extension API-key creation until they verify the address on the profile page. The profile page can also change the account email, which sends a code to the new address and only then switches and verifies it. Configured administrator usernames are exempt from the verification gate.
Codes are stored hashed in memory, expire after 10 minutes, are single-use, and are invalidated after five failed attempts. Sending is rate limited with a 60-second resend cooldown, a daily cap per address, a per-IP window cap, and a global hourly cap.
The backend uses an H2 file database with Hibernate schema updates enabled. The database contains users, translation history, model profiles, API keys, plugin device codes, announcements, translation cache entries, survey records, token usage logs, and the terminology glossary (glossary_concepts / glossary_terms).
Live model responses that report token usage are logged with the provider, model, input tokens, output tokens, total tokens, source type, estimate flag, and timestamp. Cache hits return cached translations without creating a new live provider usage event. On startup, the historical migration service can backfill cache usage and estimate older translation records that do not contain provider usage data; estimated entries are marked separately in the administrator log view.
The feedback pipeline appends two JSONL streams under app.feedback.dir (default ./data/feedback), dated by UTC day: samples_YYYYMMDD.jsonl (redacted source/target pairs with engine, model, latency and sampling origin) and events_YYYYMMDD.jsonl (ratings and behaviour events). Sampling rate, daily cap, pending-retention window, queue size, text truncation and the redaction term list are configured under app.feedback.* in application.yml. Redaction runs in the writer thread immediately before anything touches disk — never at export time. A rating at or below 3, a re-translation or an edit promotes the pending sample to a full, always-kept row regardless of the sample rate. The admin panel's Feedback tab reads the same files through /admin/feedback/*, and tools/export_feedback.py merges a date range into per-day feedback_YYYYMMDD.jsonl files plus a summary.json (rate events deduplicated per request+device), which tools/verify_feedback_export.py validates line by line.
All backend API routes use the /api/v1 prefix. JWT-authenticated requests use Authorization: Bearer <token>. The browser extension uses X-API-Key.
| Route | Access | Purpose |
|---|---|---|
POST /auth/register |
Public | Register an account. Requires a verification code sent to the email address, so the address must be reachable; new accounts are created email-verified. |
POST /auth/email/send-code |
Public | Send a six-digit verification code to an email address (rate limited per address and IP). |
POST /auth/email/verify |
Authenticated | Verify the code for the account address; also updates the account when the address changed. |
POST /auth/login |
Public | Return a JWT. |
GET /auth/me |
Authenticated | Return the current username, administrator status, and email-verification status. |
GET /auth/profile |
Authenticated | Return profile (including emailVerified) and masked model configuration information. |
GET /auth/model-profiles |
Authenticated or extension API key | List the current user's model profiles. Extension requests omit key previews. |
POST /auth/model-profiles/detect |
Authenticated | Proxy a provider /models request for optional model-name detection. |
POST /auth/model-profiles |
Authenticated | Create a model profile. |
PUT /auth/model-profiles/{id} |
Authenticated | Update a model profile. |
DELETE /auth/model-profiles/{id} |
Authenticated | Delete a model profile. |
PUT /auth/profile/model |
Authenticated | Legacy single-profile configuration endpoint. |
PUT /auth/profile/ai-key |
Authenticated | Legacy personal API key endpoint. |
GET /auth/usage |
Authenticated | Return the current user's token usage summary and charts. |
POST /auth/api-key |
Authenticated + email verified | Generate a browser-extension API key. |
GET /auth/api-keys |
Authenticated | List the user's extension API keys. |
DELETE /auth/api-key/{id} |
Authenticated | Revoke an extension API key. |
POST /translate |
Authenticated or extension API key + email verified | Perform a non-streaming translation. Unverified accounts receive HTTP 403. Accepts an optional targetLanguage tag. |
POST /translate/stream |
Authenticated or extension API key + email verified | Stream a translation over SSE. Unverified accounts receive HTTP 403. Accepts an optional targetLanguage tag. |
GET /translations |
Authenticated | List the current user's translation history. |
GET /translations/{id} |
Authenticated | Read one history record owned by the current user. |
POST /translate/image |
Authenticated + email verified | Translate one or more uploaded images in a single multimodal call. Send repeatable images fields (up to 10); the singular image field is still accepted for older clients. |
POST /translate/image/stream |
Authenticated + email verified | Same upload and same prompt as /translate/image, answered as an SSE stream (one JSON event per token, then a done event). Send repeatable images fields (up to 10). |
GET /pixiv/novel |
Public | Import a Pixiv novel by URL or work id (?url=). Returns novelId, title, author, description (plain text), tags, the cleaned body text, and metadataText (the labelled title/author/tags/description block). Proxies https://www.pixiv.net/ajax/novel/{id} and strips Pixiv control tags; login-gated novels return an explanatory error. Stores nothing. |
GET /presets |
Public | Return configured preset names, served from the database. |
GET /admin/presets |
Administrator | List all presets including their prompts. |
POST /admin/presets |
Administrator | Create a preset with a unique name and its prompt text. |
PUT /admin/presets/{id} |
Administrator | Rename a preset or edit its prompt. |
DELETE /admin/presets/{id} |
Administrator | Delete a preset. |
GET /translation/languages |
Public | Return the configured target languages (code + label). A translate request may carry one of these codes as targetLanguage; a missing or unrecognised value falls back to app.translation.default-target-language, so older clients are unaffected. |
GET /announcements |
Public | Return announcements in reverse chronological order; each item includes requireConfirmation. |
GET /announcements/pending |
Authenticated | Return announcements flagged as requiring confirmation that the current user has not confirmed yet. |
POST /announcements/{id}/acknowledge |
Authenticated | Record that the current user confirmed the announcement (idempotent). |
GET /pixiv/novel |
Public | Import one novel by url (link or bare id) into the translate box; strips Pixiv control tags and returns title/author/tags/summary plus the plain text. Login-gated works need PIXIV_SESSION_COOKIE. |
GET /pixiv/search |
Public | Search Pixiv novels by keyword; returns candidate items with id, title, author, tags, xRestrict (R-18 marker) and textCount. R-18 results appear only with a server session cookie. |
POST /pixiv/extract |
Authenticated + email verified | Run the site vision model over 1–3 pasted screenshots and return {title, author, tags[], summary} for the screenshot-import flow. |
POST /survey |
Authenticated | Submit translation and experience feedback. |
POST /feedback/rate |
Public | Record one anonymous rating (requestId, rating 1–5, optional tags/comment, deviceFp). Re-submitting an unchanged value is ignored; a change overwrites. Frequency-capped per device. |
POST /feedback/event |
Public | Record a behaviour event (copy, retranslate, edit) against a requestId. |
GET /admin/feedback/summary |
Administrator | Aggregate sample / event / rating counts over days (default 7). Ratings are deduplicated per (request, device), latest wins. |
GET /admin/feedback/samples |
Administrator | Return the newest samples, most recent first (limit, default 50, max 200). |
GET /admin/feedback/events |
Administrator | Return the newest behaviour events, most recent first (limit, default 50, max 200). |
GET /stats/users |
Authenticated | Return the registered-user count. |
GET /admin/usage |
Administrator | Return global token totals, charts, and per-user summaries. |
GET /admin/usage/users/{userId} |
Administrator | Return one user's token log. |
POST /admin/announcements |
Administrator | Publish an announcement as raw Markdown text; requireConfirmation: true makes every user confirm it once before it stops popping up. |
PATCH /admin/announcements/{id}/confirmation-required |
Administrator | Toggle whether the announcement still requires user confirmation. Disabling stops the pop-up for unconfirmed users; re-enabling brings it back, keeping existing confirmations. |
GET /admin/announcements/{id}/acknowledgements |
Administrator | List the users who confirmed an announcement, newest first, plus a total count. |
POST /plugin/device-code |
Public | Issue a short device code for the extension's one-click authorization. Returns code, expiresIn (600 s), interval, and verificationPath. |
GET /plugin/device-code/{code} |
Public | Poll a device code. Always answers HTTP 200 with a status of PENDING, APPROVED, CONSUMED, or EXPIRED — never 404, because unmatched paths fall through to the SPA handler. APPROVED carries the plaintext keyValue exactly once. |
POST /plugin/device-code/approve |
Authenticated + email verified | Approve a device code shown in the extension. Creates an API key named pixiv-plugin and answers 400 invalid_or_expired_code, 403 email_not_verified, or 409 code_already_approved. |
POST /plugin/logs |
Authenticated or extension API key | Submit browser-extension error reports. |
GET /plugin/logs |
Authenticated | List the current user's reports; administrators see all reports. |
DELETE /admin/announcements/{id} |
Administrator | Delete an announcement. |
A push/PR triggered GitHub Actions workflow (.github/workflows/test.yml) runs the backend tests and the frontend typecheck plus unit tests on every change; the deploy workflow remains manually triggered.
# Backend (JUnit, 119 tests)
cd ShizukuTranslate
mvn test
# Frontend (vue-tsc typecheck + vitest)
cd ShizukuTranslate-frontend
npm run typecheck
npm testThe backend suite covers prompt assembly, translation and cache-replay behavior, streaming vision requests, preset CRUD, announcement confirmation toggling, email verification flows, API-key hashing and migration, plugin device-code authorization, and JWT issuing. The frontend suite covers the safe Markdown renderer, HTML escaping helpers, announcement ordering, and the SSE streaming client (token framing, multipart image uploads, backend error surfacing).
These are the exact commands executed by .github/workflows/test.yml on every push and pull request:
# Backend tests — run from ShizukuTranslate/
mvn -B test
# Frontend typecheck and unit tests — run from ShizukuTranslate-frontend/
npm ci
npm run typecheck
npm test- The announcement renderer supports a safe Markdown subset implemented in the frontend; raw HTML is escaped rather than rendered.
- The backend exposes the H2 console at
/h2-consolein the checked-in configuration; protect or disable it before exposing the service outside a trusted environment. - H2 file storage is convenient for this deployment but is not a replacement for a production database with stronger operational tooling.
- The browser extension is distributed as an unpacked extension rather than through a browser store, so users must reload it after updates.
- The extension requests broad HTTP/HTTPS host access because the backend URL is user-configurable and may change independently of the extension release; requests are limited in code to the configured backend and Pixiv flows.
- Upstream cancellation is cooperative: disconnecting or cancelling an SSE request interrupts the backend worker and stops reading the model response; providers that do not react immediately to a closed HTTP connection may continue processing briefly.
- Model providers may impose their own rate limits, context limits, outages, or content policies.