8.7 KiB
8.7 KiB
LLM Translator — Agent Guide
Project Overview
A 4-phase translation pipeline web app. Users input source text, then sequentially execute:
- Phase 1 (Rough Translation) — LLM translates + extracts proper nouns, summary, style
- Phase 2 (Proper Noun Review) — User reviews/edits extracted proper nouns and style
- Phase 3 (Re-translation) — LLM re-translates with proper noun/style constraints applied
- Phase 4 (Polish) — LLM polishes for readability while preserving proper nouns
Architecture: FastAPI backend + Vue 3 SPA (Vite build, served as static files by FastAPI).
Single-server deployment: uvicorn backend.main:app serves both API and frontend.
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Python 3.14+, FastAPI, Uvicorn |
| Auth | JWT (PyJWT), bcrypt (direct via bcrypt module) |
| LLM Client | OpenAI SDK (openai) — OpenAI-compatible API format |
| Frontend | Vue 3 + Composition API <script setup>, TypeScript, Pinia |
| Styling | Tailwind CSS v4 with darkMode: 'class' strategy |
| Build | Vite (frontend), no bundler for backend |
Directory Structure
LLM-translator/
├── backend/ # FastAPI application
│ ├── main.py # App entry: routes, CORS, static file mounting
│ ├── auth.py # JWT auth + bcrypt password verification
│ ├── llm_client.py # OpenAI-compatible client (async)
│ ├── models.py # Pydantic schemas for all API I/O
│ ├── prompts.py # Phase 1–4 prompt templates
│ └── sessions.py # In-memory session store (TTL 24h, UUID keys)
├── frontend/ # Vue 3 SPA source
│ ├── src/
│ │ ├── main.ts # App entry + dark mode pre-mount class application
│ │ ├── App.vue # Root: LoginView | TranslatorView based on auth state
│ │ ├── views/
│ │ │ ├── LoginView.vue # ID/PW login form → localStorage JWT
│ │ │ └── TranslatorView.vue # Main UI: model selectors, phase buttons, comparison
│ │ ├── components/
│ │ │ ├── LLMSelector.vue # Model dropdown (dark mode aware)
│ │ │ ├── ProperNounsEditor.vue # Proper noun table + style input
│ │ │ └── ComparisonView.vue # 3-column Phase 1/3/4 result comparison
│ │ ├── stores/
│ │ │ ├── auth.ts # JWT state (check/login/logout)
│ │ │ ├── translation.ts # Pipeline state + localStorage persistence
│ │ │ └── theme.ts # Dark mode toggle + localStorage + system preference detection
│ │ ├── api.ts # Axios client with JWT interceptor
│ │ └── types.ts # TypeScript interfaces mirroring backend models
│ ├── tailwind.config.js # darkMode: 'class' configured here
│ ├── vite.config.ts # Dev proxy to /api → localhost:8000; build output → dist/
│ └── package.json
├── config/
│ ├── llms.json # LLM server configs [{alias, base_url, api_key, model, context_size}]
│ └── users.json # Users [{id, password_hash}] — hashed on first startup from plain-text
├── requirements.txt # Python deps: fastapi, uvicorn[standard], pyjwt, bcrypt, openai, python-dotenv
├── .env.example # Environment variables template
└── README.md # (TBD)
Key Patterns & Conventions
Backend
- No
passlib— usebcryptmodule directly (bcrypt.hashpw,bcrypt.checkpw). The project uses Python 3.14 which has compatibility issues with passlib's bcrypt wrapper. - LLM client caching —
llm_client.pymaintains a per-alias{AsyncOpenAI, LLMConfig}cache dict keyed by alias string. Clients are lazily instantiated on first use. - Session store — simple in-memory dict (
sessions.py). Key is UUID hex (16 chars). TTL 24h from last access. No persistence beyond process lifetime. - JSON extraction — LLM responses may be wrapped in markdown code blocks.
_extract_json()strips```json` ...wrappers beforejson.loads().
Frontend
- All Vue components use
<script setup lang="ts">with Composition API. No Options API. - State management via Pinia stores —
auth,translation,theme. Stores persist model selections and history tolocalStorage. - Dark mode is class-based (
<html class="dark">). Applied inmain.tsbefore mount to prevent flash, then toggled byuseThemeStore.toggle(). Every UI component has correspondingdark:Tailwind classes. - API calls go through
/apiproxy during dev (vite.config.ts). In production the same origin serves both API and static files.
LLM Configuration
Models are defined in config/llms.json. Each entry:
{
"alias": "Qwen3.6-35B", // Display name, used as modelPhase1..4 values
"base_url": "http://...", // OpenAI-compatible base URL (must end with /v1)
"api_key": "...", // API key for this server
"model": "hx370/qwen3.6-35b-a3b", // Exact model name as used by the LLM provider
"context_size": 65536 // Token context window size (for length warnings)
}
Translation Pipeline Data Flow
source_text ──► Phase1(LLM) ──► {translated, proper_nouns[], summary, style}
│
▼ user edits in ProperNounsEditor
phase2_proper_nouns[], phase2_style
│
▼ Phase3(LLM): re-translate with constraints
phase3_result
│
▼ Phase4(LLM): polish for readability
phase4_result (final)
Phase buttons are always freely executable — user can jump to any phase independently. Dependencies are only logical: P2 needs P1 data, P3 needs P1 result, P4 needs P3 result.
Theme / Dark Mode
theme.tsstore manages state in Pinia + localStorage. On init, checks saved value or falls back to system preference (prefers-color-scheme).- Every component with background/border/text colors must include matching
dark:variants. - Default light theme: white/slate backgrounds, blue accents. Dark theme: gray-800/900 backgrounds, adjusted borders, preserved accent colors (blue, purple, emerald remain readable).
Running the Project
# 1. Install dependencies
pip install -r requirements.txt
cd frontend && npm install && cd ..
# 2. Build frontend (required before serving in production mode)
cd frontend && npx vite build && cd ..
# 3. Configure LLM servers and users
# Edit config/llms.json and config/users.json directly
# 4. Start server
uvicorn backend.main:app --host 0.0.0.0 --port 8000
# Or with env vars for custom paths:
JWT_SECRET_KEY=your-secret uvicorn backend.main:app --reload
# 5. Open http://localhost:8000
Default login: admin / changeme123. First startup auto-hashes plain-text passwords in users.json and saves the file.
Adding a New Phase or Modifying Prompts
- Edit
backend/prompts.py: add new prompt templates using f-strings with source data parameters - Add API endpoint in
backend/main.pyfollowing the existing pattern:@app.post("/api/translate/{session_id}/phaseN") async def run_phaseN(session_id: str): session = session_store.get(session_id) # ... call chat_complete(alias, system_prompt, user_content) updated = session_store.update(session_id, {"phaseN_result": result_text}) return SessionResponse(session_id=session_id, data=updated).model_dump() - Add frontend component and button in
TranslatorView.vue - Update
translation.tsstore with new execution method + state
File I/O Notes
- config/llms.json — loaded once at startup by
llm_client.py._load_llm_configs(). Not re-read on requests. Changes require server restart. - config/users.json — loaded once at startup by
auth.py._load_users_file(). Plain-text passwords are auto-migrated to bcrypt hashes and saved back on first run only.
Testing Against LLM Server
# Quick connectivity test (use exact model name from llms.json)
curl -s http://btuna.net:45455/v1/chat/completions \
-H "Authorization: Bearer btuna-public-key" \
-H "Content-Type: application/json" \
-d '{"model":"hx370/qwen3.6-35b-a3b","messages":[{"role":"user","content":"test"}],"max_tokens":10}'
If you get PermissionDeniedError, the API key lacks access to that model — try a different model or contact server admin for correct permissions.