Spaces:
Runtime error
Runtime error
Documentation
Browse files- README.md +50 -27
- frontend/README.md +44 -2
- frontend/src/App.css +1 -2
- frontend/src/index.css +0 -38
- frontend/src/main.jsx +0 -1
- scripts/cleaners/clean_ds.py +1 -1
- scripts/cleaners/clean_ms.py +1 -1
README.md
CHANGED
|
@@ -31,22 +31,34 @@ pinned: false
|
|
| 31 |
| **Article / transcript** | Paste any text directly |
|
| 32 |
| **File upload** | Drag-and-drop `.txt` files |
|
| 33 |
| **Streaming** | Summaries stream token-by-token from Ollama via NDJSON |
|
| 34 |
-
| **Model switching** | Choose between available Ollama models from the UI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
---
|
| 37 |
|
| 38 |
## API Endpoints
|
| 39 |
|
| 40 |
-
| Method |
|
| 41 |
-
|---------|-------------------------|-----------------------|
|
| 42 |
-
| `GET` | `/health`
|
| 43 |
-
| `GET` | `/status`
|
| 44 |
-
| `GET` | `/models`
|
| 45 |
-
| `POST` | `/
|
| 46 |
-
| `
|
| 47 |
-
| `POST` | `/
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
---
|
| 52 |
|
|
@@ -78,17 +90,26 @@ cp .env.example .env
|
|
| 78 |
# VITE_API_KEY must match PRECIS_API_KEY so the baked frontend can auth
|
| 79 |
```
|
| 80 |
|
| 81 |
-
> **
|
| 82 |
-
>
|
| 83 |
-
>
|
| 84 |
-
>
|
| 85 |
-
>
|
| 86 |
-
>
|
| 87 |
-
>
|
| 88 |
-
>
|
| 89 |
-
>
|
| 90 |
-
>
|
| 91 |
-
> ```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
### 3. Run
|
| 94 |
|
|
@@ -116,7 +137,7 @@ The API will 503 until a model is installed. On **host**:
|
|
| 116 |
```bash
|
| 117 |
ollama pull phi4-mini:latest
|
| 118 |
# optional extras:
|
| 119 |
-
ollama pull
|
| 120 |
ollama list
|
| 121 |
```
|
| 122 |
|
|
@@ -125,15 +146,15 @@ Then refresh http://localhost:5173 — the model selector populates automaticall
|
|
| 125 |
#### Useful commands
|
| 126 |
|
| 127 |
```bash
|
| 128 |
-
docker compose up --watch -d # detached with watch
|
| 129 |
-
docker compose logs -f api # tail API logs
|
| 130 |
docker compose logs -f frontend
|
| 131 |
docker compose down # stop api+frontend
|
| 132 |
docker compose ps
|
| 133 |
curl http://localhost:8000/health
|
| 134 |
curl http://localhost:8000/status
|
| 135 |
curl http://host.docker.internal:11434/api/tags # verify host Ollama reachable from Docker
|
| 136 |
-
# prod without watch/HMR (static frontend at 8000):
|
| 137 |
docker compose -f docker-compose.yml up --build -d
|
| 138 |
```
|
| 139 |
|
|
@@ -142,7 +163,9 @@ docker compose -f docker-compose.yml up --build -d
|
|
| 142 |
If you just want the API image and already run Ollama natively (`ollama serve` on host):
|
| 143 |
|
| 144 |
```bash
|
| 145 |
-
docker build -t precis
|
|
|
|
|
|
|
| 146 |
docker run --rm -p 8000:8000 --env-file .env \
|
| 147 |
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
|
| 148 |
--add-host=host.docker.internal:host-gateway \
|
|
|
|
| 31 |
| **Article / transcript** | Paste any text directly |
|
| 32 |
| **File upload** | Drag-and-drop `.txt` files |
|
| 33 |
| **Streaming** | Summaries stream token-by-token from Ollama via NDJSON |
|
| 34 |
+
| **Model switching** | Choose between available Ollama models from the UI with live VRAM warmup |
|
| 35 |
+
|
| 36 |
+
### Keyboard Shortcuts
|
| 37 |
+
|
| 38 |
+
| Shortcut | Action | Behavior |
|
| 39 |
+
|---|---|---|
|
| 40 |
+
| <kbd>Ctrl</kbd> + <kbd>Enter</kbd> | **Generate** | Triggers summarization on the active tab |
|
| 41 |
+
| <kbd>Ctrl</kbd> + <kbd>C</kbd> | **Cancel** | Stops active generation; preserves streamed tokens as a `· partial` summary |
|
| 42 |
+
| <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>C</kbd> | **Copy Link** | Copies current YouTube URL to clipboard |
|
| 43 |
|
| 44 |
---
|
| 45 |
|
| 46 |
## API Endpoints
|
| 47 |
|
| 48 |
+
| Method | Path | Auth Required | Description |
|
| 49 |
+
|---------|--------------------------|:-------------:|-------------|
|
| 50 |
+
| `GET` | `/health` | No | Service liveness check |
|
| 51 |
+
| `GET` | `/status` | No | Ollama reachability, model configuration, version info |
|
| 52 |
+
| `GET` | `/models` | No | Lists installed models, currently running VRAM models, and default |
|
| 53 |
+
| `POST` | `/warmup` | No | Pre-loads model into Ollama VRAM (`?model=<name>`) |
|
| 54 |
+
| `GET` | `/warmup/status` | No | Checks if model is currently loaded in memory (`?model=<name>`) |
|
| 55 |
+
| `POST` | `/unload` | No | Evicts a specific model or all models from VRAM |
|
| 56 |
+
| `GET` | `/api` | No | API root metadata and documentation links |
|
| 57 |
+
| `POST` | `/summarize/transcript` | `X-API-Key` | Plain-text summarization (JSON body) |
|
| 58 |
+
| `POST` | `/summarize/youtube` | `X-API-Key` | YouTube video transcript fetch + summary (JSON body) |
|
| 59 |
+
| `POST` | `/summarize/file` | `X-API-Key` | Plain `.txt` file upload (`multipart/form-data`, optional `?model=`) |
|
| 60 |
+
|
| 61 |
+
All `/summarize/*` endpoints stream responses via NDJSON (`application/x-ndjson`) and require the `X-API-Key` header matching `PRECIS_API_KEY`. Model reasoning and `<think>` blocks are isolated server-side and streamed as formatted markdown blockquotes.
|
| 62 |
|
| 63 |
---
|
| 64 |
|
|
|
|
| 90 |
# VITE_API_KEY must match PRECIS_API_KEY so the baked frontend can auth
|
| 91 |
```
|
| 92 |
|
| 93 |
+
> **Configuration & Environment Variables:**
|
| 94 |
+
>
|
| 95 |
+
> | Variable | Default | Purpose |
|
| 96 |
+
> |---|---|---|
|
| 97 |
+
> | `OLLAMA_BASE_URL` | `http://127.0.0.1:11434` | Host Ollama URL (Docker Compose sets `http://host.docker.internal:11434`) |
|
| 98 |
+
> | `DEFAULT_MODEL` | `phi4-mini:latest` | Default model tag when none specified in request |
|
| 99 |
+
> | `AVAILABLE_MODELS` | `phi4-mini:latest` | Comma-separated allowlist of models |
|
| 100 |
+
> | `PRECIS_API_KEY` | *(required)* | Secret token checked against `X-API-Key` on `/summarize/*` |
|
| 101 |
+
> | `VITE_API_KEY` | *(required)* | Inlined into frontend bundle at build time to authenticate with API |
|
| 102 |
+
> | `VITE_API_BASE_URL` | `""` | Base API URL; empty string uses same origin (or Vite proxy in dev) |
|
| 103 |
+
> | `VITE_PROXY_TARGET` | `http://api:8000` | Target backend host used by Docker frontend container |
|
| 104 |
+
> | `PORT` | `8000` | Service listening port (defaults to `8000`, Hugging Face Spaces sets `7860`) |
|
| 105 |
+
> | `PRECIS_ALLOWED_ORIGINS` | `http://localhost:5173,http://localhost:8000` | CORS allowed origins |
|
| 106 |
+
> | `OLLAMA_KEEP_ALIVE` | `30m` | Duration Ollama retains loaded model in VRAM |
|
| 107 |
+
> | `MAX_SUMMARY_TOKENS` | `120` | Max tokens generated per summary |
|
| 108 |
+
> | `TEMPERATURE` | `0.2` | Generation sampling temperature |
|
| 109 |
+
> | `PRECIS_MAX_UPLOAD_BYTES` | `10485760` | Max upload size for `/summarize/file` (10 MB) |
|
| 110 |
+
> | `PRECIS_MAX_TRANSCRIPT_CHARS` | `250000` | Max characters allowed for text summarization |
|
| 111 |
+
> | `CHOKIDAR_USEPOLLING` | `1` | Enables filesystem polling for Vite HMR across Docker on Windows |
|
| 112 |
+
> | `CHOKIDAR_INTERVAL` | `1000` | Polling interval in milliseconds to prevent CPU thrashing |
|
| 113 |
|
| 114 |
### 3. Run
|
| 115 |
|
|
|
|
| 137 |
```bash
|
| 138 |
ollama pull phi4-mini:latest
|
| 139 |
# optional extras:
|
| 140 |
+
ollama pull qwen2.5:3b
|
| 141 |
ollama list
|
| 142 |
```
|
| 143 |
|
|
|
|
| 146 |
#### Useful commands
|
| 147 |
|
| 148 |
```bash
|
| 149 |
+
docker compose up --watch -d # detached with watch (api + frontend containers)
|
| 150 |
+
docker compose logs -f api # tail API logs
|
| 151 |
docker compose logs -f frontend
|
| 152 |
docker compose down # stop api+frontend
|
| 153 |
docker compose ps
|
| 154 |
curl http://localhost:8000/health
|
| 155 |
curl http://localhost:8000/status
|
| 156 |
curl http://host.docker.internal:11434/api/tags # verify host Ollama reachable from Docker
|
| 157 |
+
# prod without watch/HMR (static frontend baked into api image at 8000):
|
| 158 |
docker compose -f docker-compose.yml up --build -d
|
| 159 |
```
|
| 160 |
|
|
|
|
| 163 |
If you just want the API image and already run Ollama natively (`ollama serve` on host):
|
| 164 |
|
| 165 |
```bash
|
| 166 |
+
docker build -t precis \
|
| 167 |
+
--build-arg PRECIS_API_KEY=your-secret-key \
|
| 168 |
+
--build-arg VITE_API_KEY=your-secret-key .
|
| 169 |
docker run --rm -p 8000:8000 --env-file .env \
|
| 170 |
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
|
| 171 |
--add-host=host.docker.internal:host-gateway \
|
frontend/README.md
CHANGED
|
@@ -2,11 +2,53 @@
|
|
| 2 |
|
| 3 |
React 19 + Vite frontend for Précis. See the [root README](../README.md) for full setup instructions.
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
```bash
|
| 8 |
npm install
|
| 9 |
npm run dev
|
| 10 |
```
|
| 11 |
|
| 12 |
-
Requires the backend API running at `http://localhost:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
React 19 + Vite frontend for Précis. See the [root README](../README.md) for full setup instructions.
|
| 4 |
|
| 5 |
+
## Development
|
| 6 |
+
|
| 7 |
+
### 1. With Docker (Recommended)
|
| 8 |
+
From the repository root, run:
|
| 9 |
+
|
| 10 |
+
```bash
|
| 11 |
+
docker compose up --watch -d
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
The frontend container starts the Vite dev server at `http://localhost:5173` with HMR enabled, proxying API requests to the backend container at `http://api:8000`.
|
| 15 |
+
|
| 16 |
+
### 2. Manual (Without Docker)
|
| 17 |
+
From the `frontend/` directory:
|
| 18 |
|
| 19 |
```bash
|
| 20 |
npm install
|
| 21 |
npm run dev
|
| 22 |
```
|
| 23 |
|
| 24 |
+
Served at `http://localhost:5173`. Requires the backend API running at `http://localhost:8000` (requests to `/health`, `/status`, `/models`, `/warmup`, `/unload`, and `/summarize` are proxied via `vite.config.js`).
|
| 25 |
+
|
| 26 |
+
## Production Build
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
npm run build
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Generates an optimized static bundle in `frontend/dist/`. In production or single-container Docker deployments, the FastAPI backend automatically detects `frontend/dist` and serves the single-page application directly from `/`.
|
| 33 |
+
|
| 34 |
+
## Environment Variables
|
| 35 |
+
|
| 36 |
+
Read at build/dev time from the repo root `.env` or injected by Docker Compose:
|
| 37 |
+
|
| 38 |
+
| Variable | Description |
|
| 39 |
+
|---|---|
|
| 40 |
+
| `VITE_API_KEY` | API secret key inlined into the frontend bundle at build time; sent as `X-API-Key` to authorize with the backend. Must match `PRECIS_API_KEY`. |
|
| 41 |
+
| `VITE_API_BASE_URL` | Base API URL. Leave empty (`""`) to use same-origin (production) or the Vite dev proxy. |
|
| 42 |
+
| `VITE_PROXY_TARGET` | Proxy destination for Vite in dev mode (default: `http://localhost:8000`, set to `http://api:8000` inside Docker Compose). |
|
| 43 |
+
|
| 44 |
+
## Architecture
|
| 45 |
+
|
| 46 |
+
* **`src/App.jsx`**: Main application shell with tab controls (YouTube, Text, File) and model selector with active VRAM status indicators.
|
| 47 |
+
* **`src/hooks/useStreaming.js`**: Custom hook for reading NDJSON streams (`ReadableStream` -> `TextDecoder`). Uses `requestAnimationFrame` frame-throttling (~60fps) to eliminate UI lag during rapid token delivery. Supports in-flight cancellation while preserving partial summaries.
|
| 48 |
+
* **`src/components/InlineResult.jsx`**: Renders markdown output, streaming status indicators, and model reasoning tags.
|
| 49 |
+
|
| 50 |
+
### Keyboard Shortcuts
|
| 51 |
+
|
| 52 |
+
* <kbd>Ctrl</kbd> + <kbd>Enter</kbd>: Submit active form and generate summary.
|
| 53 |
+
* <kbd>Ctrl</kbd> + <kbd>C</kbd>: Cancel active generation and keep streamed tokens as a `· partial` result.
|
| 54 |
+
* <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>C</kbd>: Copy current YouTube URL to clipboard.
|
frontend/src/App.css
CHANGED
|
@@ -339,7 +339,6 @@
|
|
| 339 |
color: var(--color-danger-fg);
|
| 340 |
}
|
| 341 |
|
| 342 |
-
.inline-result--loading,
|
| 343 |
.inline-result--streaming {
|
| 344 |
background-color: var(--color-canvas-inset);
|
| 345 |
border-color: var(--color-border-muted);
|
|
@@ -521,7 +520,7 @@
|
|
| 521 |
background-color: #24292f;
|
| 522 |
color: #ffffff;
|
| 523 |
font-size: 11px;
|
| 524 |
-
font-family: var(--font-family
|
| 525 |
font-weight: 500;
|
| 526 |
padding: 5px 8px;
|
| 527 |
border-radius: 6px;
|
|
|
|
| 339 |
color: var(--color-danger-fg);
|
| 340 |
}
|
| 341 |
|
|
|
|
| 342 |
.inline-result--streaming {
|
| 343 |
background-color: var(--color-canvas-inset);
|
| 344 |
border-color: var(--color-border-muted);
|
|
|
|
| 520 |
background-color: #24292f;
|
| 521 |
color: #ffffff;
|
| 522 |
font-size: 11px;
|
| 523 |
+
font-family: var(--font-family);
|
| 524 |
font-weight: 500;
|
| 525 |
padding: 5px 8px;
|
| 526 |
border-radius: 6px;
|
frontend/src/index.css
CHANGED
|
@@ -120,20 +120,6 @@ body {
|
|
| 120 |
background-color: var(--color-btn-primary-hover-bg);
|
| 121 |
}
|
| 122 |
|
| 123 |
-
.btn-lg {
|
| 124 |
-
padding: 10px 24px;
|
| 125 |
-
font-size: 16px;
|
| 126 |
-
line-height: 24px;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
/* Card styles */
|
| 130 |
-
.card {
|
| 131 |
-
background-color: var(--color-canvas-subtle);
|
| 132 |
-
border: 1px solid var(--color-border-default);
|
| 133 |
-
border-radius: var(--radius-1);
|
| 134 |
-
padding: var(--spacing-4);
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
/* Input styles */
|
| 138 |
.input {
|
| 139 |
width: 100%;
|
|
@@ -207,17 +193,6 @@ body {
|
|
| 207 |
color: var(--color-fg-muted);
|
| 208 |
}
|
| 209 |
|
| 210 |
-
.dropzone-text {
|
| 211 |
-
font-size: 16px;
|
| 212 |
-
color: var(--color-fg-muted);
|
| 213 |
-
text-align: center;
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
.dropzone-hint {
|
| 217 |
-
font-size: 12px;
|
| 218 |
-
color: var(--color-fg-subtle);
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
/* Animations */
|
| 222 |
@keyframes fadeIn {
|
| 223 |
from {
|
|
@@ -230,23 +205,10 @@ body {
|
|
| 230 |
}
|
| 231 |
}
|
| 232 |
|
| 233 |
-
@keyframes pulse {
|
| 234 |
-
0%, 100% {
|
| 235 |
-
opacity: 1;
|
| 236 |
-
}
|
| 237 |
-
50% {
|
| 238 |
-
opacity: 0.5;
|
| 239 |
-
}
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
.fade-in {
|
| 243 |
animation: fadeIn 0.3s ease-out forwards;
|
| 244 |
}
|
| 245 |
|
| 246 |
-
.pulse {
|
| 247 |
-
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
/* Scrollbar */
|
| 251 |
::-webkit-scrollbar {
|
| 252 |
width: 8px;
|
|
|
|
| 120 |
background-color: var(--color-btn-primary-hover-bg);
|
| 121 |
}
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
/* Input styles */
|
| 124 |
.input {
|
| 125 |
width: 100%;
|
|
|
|
| 193 |
color: var(--color-fg-muted);
|
| 194 |
}
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
/* Animations */
|
| 197 |
@keyframes fadeIn {
|
| 198 |
from {
|
|
|
|
| 205 |
}
|
| 206 |
}
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
.fade-in {
|
| 209 |
animation: fadeIn 0.3s ease-out forwards;
|
| 210 |
}
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
/* Scrollbar */
|
| 213 |
::-webkit-scrollbar {
|
| 214 |
width: 8px;
|
frontend/src/main.jsx
CHANGED
|
@@ -6,6 +6,5 @@ import App from './App.jsx'
|
|
| 6 |
// caused duplicate /models + /warmup calls on every reload → perceived lag
|
| 7 |
// and "stops after script fetching" race. We run without it for stability.
|
| 8 |
// Re-enable only if you need to audit effects.
|
| 9 |
-
// import { StrictMode } from 'react'
|
| 10 |
|
| 11 |
createRoot(document.getElementById('root')).render(<App />)
|
|
|
|
| 6 |
// caused duplicate /models + /warmup calls on every reload → perceived lag
|
| 7 |
// and "stops after script fetching" race. We run without it for stability.
|
| 8 |
// Re-enable only if you need to audit effects.
|
|
|
|
| 9 |
|
| 10 |
createRoot(document.getElementById('root')).render(<App />)
|
scripts/cleaners/clean_ds.py
CHANGED
|
@@ -22,7 +22,7 @@ def write_jsonl(rows, output_path):
|
|
| 22 |
with open(output_path, 'w', encoding='utf-8') as f:
|
| 23 |
for row in rows:
|
| 24 |
new_data = {
|
| 25 |
-
"id":
|
| 26 |
"original_source": "DialogSum",
|
| 27 |
"dialogue": row["dialogue"],
|
| 28 |
"summary": row["summary"],
|
|
|
|
| 22 |
with open(output_path, 'w', encoding='utf-8') as f:
|
| 23 |
for row in rows:
|
| 24 |
new_data = {
|
| 25 |
+
"id": row["id"],
|
| 26 |
"original_source": "DialogSum",
|
| 27 |
"dialogue": row["dialogue"],
|
| 28 |
"summary": row["summary"],
|
scripts/cleaners/clean_ms.py
CHANGED
|
@@ -24,7 +24,7 @@ def write_jsonl(lines, output_path):
|
|
| 24 |
for line in lines:
|
| 25 |
data = json.loads(line)
|
| 26 |
new_data = {
|
| 27 |
-
"id":
|
| 28 |
"original_source": "MediaSum",
|
| 29 |
"url": data["url"],
|
| 30 |
"summary": data["summary"],
|
|
|
|
| 24 |
for line in lines:
|
| 25 |
data = json.loads(line)
|
| 26 |
new_data = {
|
| 27 |
+
"id": data["id"],
|
| 28 |
"original_source": "MediaSum",
|
| 29 |
"url": data["url"],
|
| 30 |
"summary": data["summary"],
|