Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a6d55b9
Add Mistral as an alternative AI backend
ddulic Mar 10, 2026
098813a
Use Mistral dedicated OCR API and address code review suggestions
ddulic Mar 10, 2026
50d84bf
Address Copilot PR review comments
ddulic Mar 10, 2026
9b6aca5
Fix unused logger and test kwarg assertion
ddulic Mar 10, 2026
8fb46cd
Address Copilot review: candidate zero-norm guard, Mistral tests, con…
ddulic Mar 10, 2026
cf75c77
Fix incorrect ocr_model value in concurrency test
ddulic Mar 10, 2026
5a7e047
Update documentation to reflect Mistral AI backend support
ddulic Mar 10, 2026
8977156
Guard against zero/negative max_concurrency to prevent semaphore dead…
ddulic Mar 10, 2026
515e773
Remove unused config parameter from SummaryModule
ddulic Mar 10, 2026
76c4b22
Address Copilot review: OCR robustness, compact JSON schema, config w…
ddulic Mar 10, 2026
7d5899c
Fix mistralai v2 import, version constraint, and broken integration test
ddulic Mar 10, 2026
6c2c9bc
Update uv.lock for mistralai>=2.0.0
ddulic Mar 10, 2026
0aa2ab0
Use json.dumps for non-string generate_json content to ensure valid J…
ddulic Mar 10, 2026
df6936d
Add missing test coverage for Gemini, Mistral edge cases, embedding v…
ddulic Mar 10, 2026
ee6b4e8
Update docs to reflect provider-agnostic AI backend
ddulic Mar 10, 2026
ec2ec1f
Fix mypy lint errors in Gemini and Mistral services and tests
ddulic Mar 10, 2026
cdb725e
Change default ports from 8080/8081 to 8000/8001
ddulic Mar 11, 2026
c8df416
Address PR review comments
ddulic Mar 11, 2026
1672069
Merge pull request #1 from ddulic/feat/mistral-ai-backend
ddulic Mar 11, 2026
3fec311
Fix Dockerfile ports to match updated default (8080 -> 8000)
ddulic Mar 11, 2026
b67a6a3
Add Docker Compose example, fix README ports and broken server docs link
ddulic Mar 11, 2026
7a94ee5
Fix GHCR image URL to allenporter/supernote, remove invalid JWT secre…
ddulic Mar 11, 2026
0093665
Remove broken server README link
ddulic Mar 11, 2026
8ec1b27
Merge pull request #2 from ddulic/fix/dockerfile-ports
ddulic Mar 11, 2026
e130588
Fix default port to match Dockerfile (8080 -> 8000)
ddulic Mar 11, 2026
1161902
Fix ephemeral mode default port (8080 -> 8000)
ddulic Mar 11, 2026
2d6dfec
Merge pull request #3 from ddulic/fix/dockerfile-ports
ddulic Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
SUPERNOTE_STORAGE_DIR=/data \
SUPERNOTE_CONFIG_DIR=/data/config \
SUPERNOTE_HOST=0.0.0.0 \
SUPERNOTE_PORT=8080
SUPERNOTE_PORT=8000

# Create a non-root user
RUN groupadd -g 1000 -r supernote && useradd -u 1000 -r -g supernote supernote
Expand All @@ -32,7 +32,7 @@ RUN mkdir -p /data /data/config && \
# Switch to non-root user
USER supernote

EXPOSE 8080
EXPOSE 8000

VOLUME ["/data"]

Expand Down
38 changes: 28 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**The AI-powered intelligence layer for your Ratta Supernote.**

This toolkit is a self-hosted implementation of the **Supernote Private Cloud** protocol. While Ratta's official private cloud provides a solid and reliable sync foundation, this project extends that experience with an **AI-driven synthesis engine**—transforming your handwritten notes into structured, searchable knowledge using Google Gemini.
This toolkit is a self-hosted implementation of the **Supernote Private Cloud** protocol. While Ratta's official private cloud provides a solid and reliable sync foundation, this project extends that experience with an **AI-driven synthesis engine**—transforming your handwritten notes into structured, searchable knowledge using Google Gemini or Mistral AI.

<p align="center">
<img src="docs/static-assets/hero-overview.jpg" alt="Supernote Overview" width="800">
Expand All @@ -26,7 +26,7 @@ This project is designed to be **fully compatible** with the official Supernote
Beyond simple storage, Supernote provides an active processing pipeline to increase the utility of your notes:

1. **Sync**: Your device uploads `.note` files using the official Private Cloud protocol.
2. **Transcribe**: The server extract pages and use Gemini Vision to OCR your handwriting.
2. **Transcribe**: The server extracts pages and uses an AI provider (Gemini or Mistral) to OCR your handwriting.
3. **Synthesize**: AI Analyzers review your journals to find tasks, themes, and summaries.
4. **Index**: Every word is vectorized, enabling semantic search across your entire library.

Expand All @@ -43,10 +43,15 @@ The integrated frontend allows you to review your notes and AI insights side-by-

### 1. Launch the Cloud

The easiest way to start is with the `all` bundle and a Gemini API key:
The easiest way to start is with the `all` bundle and an AI API key. Choose either Google Gemini or Mistral AI:

```bash
export SUPERNOTE_GEMINI_API_KEY="your-api-key"
# Option A: Google Gemini (default)
export SUPERNOTE_GEMINI_API_KEY="your-gemini-api-key"

# Option B: Mistral AI
export SUPERNOTE_MISTRAL_API_KEY="your-mistral-api-key"

pip install "supernote[all]"
supernote serve
```
Expand All @@ -55,16 +60,16 @@ supernote serve

```bash
# Create the initial admin account
supernote admin user add you@example.com --url http://localhost:8080
supernote admin user add you@example.com --url http://localhost:8000

# Authenticate your CLI
supernote cloud login you@example.com --url http://localhost:8080
supernote cloud login you@example.com --url http://localhost:8000
```

### 3. Connect Your Device

1. On your Supernote, go to **Settings > Sync > Private Cloud**.
2. Enter your server URL (e.g., `http://192.168.1.5:8080`).
2. Enter your server URL (e.g., `http://192.168.1.5:8000`).
3. Log in with the email and password you created in Step 2.
4. Tap **Sync** to begin processing your notes.

Expand Down Expand Up @@ -130,13 +135,26 @@ The notebook parser is a fork and slightly lighter dependency version of [supern

### Run with Docker

The pre-built image is published to the GitHub Container Registry:

```bash
# Pull and run the latest image
docker run -d \
-p 8000:8000 \
-p 8001:8001 \
-v supernote-data:/data \
-e SUPERNOTE_GEMINI_API_KEY="your-api-key" \
ghcr.io/allenporter/supernote:latest
```

Or build from source:

```bash
# Build & Run server
docker build -t supernote .
docker run -d -p 8080:8080 -v $(pwd)/storage:/storage supernote serve
docker run -d -p 8000:8000 -v supernote-data:/data supernote
```

See [Server Documentation](https://github.com/allenporter/supernote/blob/main/supernote/server/README.md) for details.
For a full setup with Docker Compose, see [docker-compose.yml](docker-compose.yml).

### Developer API

Expand Down
2 changes: 1 addition & 1 deletion config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
---
# Host and port to bind the server to.
host: 0.0.0.0
port: 8080
port: 8000

# Directory where uploaded files and databases will be stored.
storage_dir: storage
Expand Down
33 changes: 33 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
services:
supernote:
image: ghcr.io/allenporter/supernote:latest
# Alternatively, build from source:
# build: .
restart: unless-stopped
ports:
- "8000:8000" # Main server
- "8001:8001" # MCP server
volumes:
- supernote-data:/data
environment:
# AI Provider — set one of the following:
SUPERNOTE_GEMINI_API_KEY: "" # Google Gemini API key
# SUPERNOTE_MISTRAL_API_KEY: "" # Mistral AI API key (alternative)

# Storage & server
SUPERNOTE_STORAGE_DIR: /data
SUPERNOTE_CONFIG_DIR: /data/config
SUPERNOTE_HOST: 0.0.0.0
SUPERNOTE_PORT: "8000"
SUPERNOTE_MCP_PORT: "8001"

# Optional: set the public-facing base URL (e.g. behind a reverse proxy)
# SUPERNOTE_BASE_URL: "https://supernote.example.com"
# SUPERNOTE_MCP_BASE_URL: "https://mcp.example.com"

# Optional: enable user self-registration
# SUPERNOTE_ENABLE_REGISTRATION: "true"

volumes:
supernote-data:
5 changes: 3 additions & 2 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ This script will initialize a virtual environment using `uv`, install dependenci
For rapid iteration, run an ephemeral server. It starts with a clean state and a pre-configured debug user.

```bash
# Enable AI features for development
export SUPERNOTE_GEMINI_API_KEY="your_api_key"
# Enable AI features for development (choose one)
export SUPERNOTE_GEMINI_API_KEY="your-gemini-api-key" # Google Gemini (default)
# export SUPERNOTE_MISTRAL_API_KEY="your-mistral-api-key" # Mistral AI (alternative)

# Start the ephemeral server
supernote serve --ephemeral
Expand Down
8 changes: 4 additions & 4 deletions docs/note_processing_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ If we don't want to parse the large "Transcript Summary" every time we need a si
1. **Diff Phase**: Parser extracts page streams. Each stream is hashed and compared to the database.
2. **Visual Phase**: Generate PNGs for new/changed pages. Assemble full PDF using cached PNGs for unchanged pages.
3. **Intelligence Phase**:
- Send PNG to Gemini (with retry/backoff) for OCR.
- Send PNG to the configured AI provider (with retry/backoff) for OCR.
- **Chunk Embeddings (Page-indexed)**: Generated per-page from raw OCR text. Ideal for "finding the needle in the haystack."
4. **Document Phase**:
- **Transcript Generation**: Aggregate all page text into a single "OCR Transcript" `SummaryDO`.
- **Insight Generation**: Prompt Gemini with the transcript to create an "AI Insights" `SummaryDO`.
- **Insight Generation**: Prompt the AI provider with the transcript to create an "AI Insights" `SummaryDO`.
- **Vector Indexing**:
- **Chunks**: Generate vectors for each page window. Store in-memory index `(file_id, page_index)`.
- **Document**: Generate vector for the Insight Summary. Store in-memory index `(file_id)`.
Expand Down Expand Up @@ -102,7 +102,7 @@ To maintain a resilient pipeline, modules must follow specific error handling pa

### 1. Expectations for `process()`
- **No Internal Try/Except (Mostly)**: Modules should let exceptions bubble up. The base class's `run()` method is the centralized error handler.
- **Descriptive Exceptions**: Raise specific exceptions (e.g., `FileNotFoundError`, `GeminiAPIError`) so the automated logs are useful.
- **Descriptive Exceptions**: Raise specific exceptions (e.g., `FileNotFoundError`, `ValueError`) so the automated logs are useful.
- **Idempotency is Mandatory**: If `process()` fails halfway (e.g., after writing a file but before updating a DB record), the next attempt must be able to resume or overwrite without creating duplicates or corruption.

### 2. Orchestrator Reaction
Expand All @@ -118,5 +118,5 @@ To maintain a resilient pipeline, modules must follow specific error handling pa
### Failure Modes & Corner Cases

1. **Dependency Staleness**: If `PageHashingModule` detects a change, it deletes the `SystemTaskDO` entries for `OCR` and `Embedding`. This causes their `run_if_needed` to return `True` on the next run, forcing a re-poll.
2. **Concurrency Limits**: `ProcessorService` limits the number of files processed in parallel. Modules should use internal semaphores (like `GeminiService`) if they have external API rate limits.
2. **Concurrency Limits**: `ProcessorService` limits the number of files processed in parallel. AI service implementations (like `GeminiService` and `MistralService`) use internal semaphores to respect external API rate limits.
3. **Idempotency Requirement**: If a task fails *after* writing data but *before* updating its status to `COMPLETED`, it will be re-run. `process()` must be safe to call again (e.g., using `UPSERT` or overwriting files).
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ server = [
"aiofiles>=25.1.0",
"aiohttp-remotes>=1.3.0",
"google-genai>=1.57.0",
"mistralai>=2.0.0",
"mcp>=1.25.0",
"aiohttp-asgi>=0.6.1",
]
Expand Down
4 changes: 2 additions & 2 deletions supernote/cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async def async_cloud_login(
Args:
email: User email/account
password: User password
url: Server URL (e.g. http://localhost:8080)
url: Server URL (e.g. http://localhost:8000)
verbose: Enable verbose HTTP logging
"""
setup_logging(verbose)
Expand Down Expand Up @@ -504,7 +504,7 @@ def add_parser(subparsers):
"--password", type=str, help="user password (prompt if omitted)"
)
parser_login.add_argument(
"--url", type=str, required=True, help="Server URL (e.g. http://localhost:8080)"
"--url", type=str, required=True, help="Server URL (e.g. http://localhost:8000)"
)
parser_login.add_argument(
"-v", "--verbose", action="store_true", help="Enable verbose logging"
Expand Down
2 changes: 1 addition & 1 deletion supernote/cli/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def serve_run(args: argparse.Namespace) -> None:
# Set environment variables for the server process
os.environ["SUPERNOTE_EPHEMERAL"] = "true"
if not os.getenv("SUPERNOTE_PORT"):
os.environ["SUPERNOTE_PORT"] = "8080"
os.environ["SUPERNOTE_PORT"] = "8000"
if not os.getenv("SUPERNOTE_HOST"):
os.environ["SUPERNOTE_HOST"] = "127.0.0.1"
os.environ["SUPERNOTE_STORAGE_DIR"] = str(tmp_path)
Expand Down
4 changes: 2 additions & 2 deletions supernote/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Client library for accessing Supernote Cloud services.

Example:
async with await Supernote.login("email@example.com", "password", host="http://localhost:8080") as sn:
async with await Supernote.login("email@example.com", "password", host="http://localhost:8000") as sn:
# Access Web and Device APIs directly through the session object
# Example: List root folder using path-based Device API
result = await sn.device.list_folder("/")
Expand All @@ -10,7 +10,7 @@
print(sn.token)

# Use an existing token obtained with `LoginClient`
sn = Supernote.from_token("your-token", host="http://localhost:8080")
sn = Supernote.from_token("your-token", host="http://localhost:8000")
"""

from . import (
Expand Down
4 changes: 2 additions & 2 deletions supernote/client/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Supernote:
"""A session-managed entry point for Supernote clients.

Example:
async with await Supernote.login("email@example.com", "password", host="http://localhost:8080") as sn:
async with await Supernote.login("email@example.com", "password", host="http://localhost:8000") as sn:
# Access Web and Device APIs directly through the session object
# Example: List root folder using path-based Device API
result = await sn.device.list_folder("/")
Expand All @@ -24,7 +24,7 @@ class Supernote:
print(sn.token)

Example using an existing token:
sn = Supernote.from_token("your-token", host="http://localhost:8080")
sn = Supernote.from_token("your-token", host="http://localhost:8000")
# Note: When created this way, you are responsible for closing the session
# or passing an existing one.
"""
Expand Down
8 changes: 5 additions & 3 deletions supernote/server/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@
- [x] Add test isolation for storage.

## Phase 5: AI & Intelligence (Completed)
- [x] Implement `GeminiService` for LLM interaction.
- [x] Implement `AIService` abstract base class for provider-agnostic AI integration.
- [x] Implement `GeminiService` for Google Gemini LLM interaction.
- [x] Implement `MistralService` for Mistral AI (OCR API, embeddings, chat).
- [x] Implement `ProcessorService` background worker.
- [x] Implement `GeminiOcrModule` for handwriting transcription.
- [x] Implement `GeminiEmbeddingModule` for semantic indexing.
- [x] Implement `OcrModule` for handwriting transcription (provider-agnostic).
- [x] Implement `EmbeddingModule` for semantic indexing (provider-agnostic).
- [x] Implement `SummaryModule` for AI-generated highlights.
- [x] Implement `SearchService` for semantic search.

Expand Down
42 changes: 34 additions & 8 deletions supernote/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This package provides a self-hosted implementation of the Supernote Cloud server
## Core Features

- **Seamless Sync**: Implements the native Supernote sync protocol.
- **AI Synthesis**: Automatically transcribes handwriting and identifies key insights using Google Gemini.
- **AI Synthesis**: Automatically transcribes handwriting and identifies key insights using Google Gemini or Mistral AI.
- **Knowledge Exploration**: Cross-notebook semantic search and web-based file browsing.
- **Private & Local**: Store your notes and metadata on your own infrastructure.

Expand All @@ -17,7 +17,7 @@ See the main [README.md](../../README.md) for a quick start guide.

- A Supernote device (Nomad, A5 X, A6 X, etc.)
- Python 3.13+ or Docker.
- (Recommended) **Gemini API Key** for OCR and Summarization.
- (Recommended) A **Gemini** or **Mistral AI** API key for OCR and Summarization.

### Configuration

Expand All @@ -26,24 +26,50 @@ The server is configured via `config/config.yaml` or environment variables.
For a comprehensive reference, see the [ServerConfig documentation](https://allenporter.github.io/supernote/supernote/server.html#ServerConfig).

#### AI Configuration
To enable AI features, set the Gemini API key:

AI features require an API key from either Google Gemini (default) or Mistral AI. Set one of the following:

```bash
export SUPERNOTE_GEMINI_API_KEY="your-api-key"
# Option A: Google Gemini (default)
export SUPERNOTE_GEMINI_API_KEY="your-gemini-api-key"

# Option B: Mistral AI (takes priority when set)
export SUPERNOTE_MISTRAL_API_KEY="your-mistral-api-key"
```

> **Note on provider switching**: Gemini embeddings are 3072-dimensional while Mistral embeddings are 1024-dimensional. Switching providers after notes have been indexed requires re-processing all files to regenerate embeddings.

Additional Gemini model settings:

| Env var | Default | Description |
|---|---|---|
| `SUPERNOTE_GEMINI_OCR_MODEL` | `gemini-3-flash-preview` | Vision model for OCR |
| `SUPERNOTE_GEMINI_EMBEDDING_MODEL` | `gemini-embedding-001` | Embedding model |
| `SUPERNOTE_GEMINI_CHAT_MODEL` | `gemini-2.0-flash` | Chat model for summaries |
| `SUPERNOTE_GEMINI_MAX_CONCURRENCY` | `5` | Max concurrent API calls (minimum 1) |

Additional Mistral model settings:

| Env var | Default | Description |
|---|---|---|
| `SUPERNOTE_MISTRAL_OCR_MODEL` | `mistral-ocr-latest` | Dedicated OCR model |
| `SUPERNOTE_MISTRAL_EMBEDDING_MODEL` | `mistral-embed` | Embedding model |
| `SUPERNOTE_MISTRAL_CHAT_MODEL` | `mistral-large-latest` | Chat model for summaries |
| `SUPERNOTE_MISTRAL_MAX_CONCURRENCY` | `5` | Max concurrent API calls (minimum 1) |

### Running the Server

Start the server using the unified `supernote` CLI:

```bash
# Start the server on port 8080
# Start the server on port 8000
supernote serve
```

To override settings via environment:

```bash
export SUPERNOTE_PORT=8080
export SUPERNOTE_PORT=8000
export SUPERNOTE_HOST=0.0.0.0
supernote serve
```
Expand All @@ -56,7 +82,7 @@ docker build -t supernote .

# Run the container
docker run -d \
-p 8080:8080 \
-p 8000:8000 \
-v $(pwd)/storage:/storage \
-e SUPERNOTE_GEMINI_API_KEY="your-key" \
--name supernote-server \
Expand All @@ -68,7 +94,7 @@ docker run -d \
1. Review the [official Private Cloud setup guide](https://support.supernote.com/Whats-New/setting-up-your-own-supernote-private-cloud-beta).
2. Ensure your Supernote device and server are on the same Wi-Fi network.
3. On your Supernote device, go to **Settings** > **Sync** > **Supernote Cloud**.
4. Select **Private Cloud** and enter your server's IP and port (e.g., `192.168.1.100:8080`).
4. Select **Private Cloud** and enter your server's IP and port (e.g., `192.168.1.100:8000`).
5. Attempt to login using the credentials created via `supernote admin user add`.
6. Configure folders to sync (e.g., `Note`, `Document`, `EXPORT`) in **Settings** > **Drive** > **Private Cloud**.

Expand Down
Loading