Initial working translator implementation
This commit is contained in:
commit
9439858b11
35 changed files with 5365 additions and 0 deletions
169
AGENTS.md
Normal file
169
AGENTS.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# LLM Translator — Agent Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
A 4-phase translation pipeline web app. Users input source text, then sequentially execute:
|
||||
1. **Phase 1 (Rough Translation)** — LLM translates + extracts proper nouns, summary, style
|
||||
2. **Phase 2 (Proper Noun Review)** — User reviews/edits extracted proper nouns and style
|
||||
3. **Phase 3 (Re-translation)** — LLM re-translates with proper noun/style constraints applied
|
||||
4. **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`** — use `bcrypt` module 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.py` maintains 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 before `json.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 to `localStorage`.
|
||||
- **Dark mode is class-based** (`<html class="dark">`). Applied in `main.ts` before mount to prevent flash, then toggled by `useThemeStore.toggle()`. Every UI component has corresponding `dark:` Tailwind classes.
|
||||
- **API calls go through `/api` proxy** 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:
|
||||
```json
|
||||
{
|
||||
"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.ts` store 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
1. Edit `backend/prompts.py`: add new prompt templates using f-strings with source data parameters
|
||||
2. Add API endpoint in `backend/main.py` following the existing pattern:
|
||||
```python
|
||||
@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()
|
||||
```
|
||||
3. Add frontend component and button in `TranslatorView.vue`
|
||||
4. Update `translation.ts` store 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
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue