novelforge 1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novelforge-1.0/LICENSE +21 -0
- novelforge-1.0/PKG-INFO +269 -0
- novelforge-1.0/README.md +240 -0
- novelforge-1.0/novelforge/__init__.py +10 -0
- novelforge-1.0/novelforge/__main__.py +6 -0
- novelforge-1.0/novelforge/core/generators.py +129 -0
- novelforge-1.0/novelforge/core/memory.py +166 -0
- novelforge-1.0/novelforge/core/novel_manager.py +194 -0
- novelforge-1.0/novelforge/core/prompts.py +156 -0
- novelforge-1.0/novelforge/core/rag.py +60 -0
- novelforge-1.0/novelforge/ui/app.py +449 -0
- novelforge-1.0/novelforge/ui/chat_panel.py +260 -0
- novelforge-1.0/novelforge/ui/dialogs.py +134 -0
- novelforge-1.0/novelforge/ui/sidebar.py +222 -0
- novelforge-1.0/novelforge/ui/style.py +363 -0
- novelforge-1.0/novelforge/ui/widgets.py +304 -0
- novelforge-1.0/novelforge/ui/workers.py +136 -0
- novelforge-1.0/novelforge.egg-info/PKG-INFO +269 -0
- novelforge-1.0/novelforge.egg-info/SOURCES.txt +23 -0
- novelforge-1.0/novelforge.egg-info/dependency_links.txt +1 -0
- novelforge-1.0/novelforge.egg-info/entry_points.txt +2 -0
- novelforge-1.0/novelforge.egg-info/requires.txt +3 -0
- novelforge-1.0/novelforge.egg-info/top_level.txt +1 -0
- novelforge-1.0/pyproject.toml +46 -0
- novelforge-1.0/setup.cfg +4 -0
novelforge-1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 StoryForge Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
novelforge-1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: novelforge
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: AI-powered novel writer with vector memory, RAG, and a PyQt6 desktop UI
|
|
5
|
+
Author-email: Kshitij Budholiya <kshitijbudholiya2006@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/Kshitijbudholiya/NovelForge
|
|
8
|
+
Project-URL: Issues, https://github.com/Kshitijbudholiya/NovelForge/issues
|
|
9
|
+
Keywords: novel,ai,writing,ollama,pyqt6,rag,vector-database
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Games/Entertainment
|
|
18
|
+
Classifier: Topic :: Text Editors
|
|
19
|
+
Classifier: Environment :: X11 Applications
|
|
20
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
21
|
+
Classifier: Environment :: MacOS X
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: PyQt6>=6.4.0
|
|
26
|
+
Requires-Dist: ollama>=0.2.0
|
|
27
|
+
Requires-Dist: endee>=0.1.27
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# NovelForge AI
|
|
31
|
+
|
|
32
|
+
> ⚠️ **Work in Progress** — NovelForge is actively under development. Features may change, break, or be incomplete. Not production-ready.
|
|
33
|
+
|
|
34
|
+
**NovelForge** is an AI-powered desktop novel writer that runs entirely on your local machine. Give it a title, genre, and premise — it writes Chapter 1, remembers everything, and keeps writing with full narrative continuity across as many chapters as you want. Ask it questions about your story and it searches its vector memory to answer accurately.
|
|
35
|
+
|
|
36
|
+
Built on three local services: **Ollama** for LLM inference, **Endee** for vector memory, and **PyQt6** for the desktop UI.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## What Makes This Different
|
|
41
|
+
|
|
42
|
+
Most AI writing tools forget everything after each message. NovelForge doesn't. Every chapter you generate is broken into chunks, embedded, and stored across four dedicated Endee vector indexes — story, characters, lore, and summaries. When the next chapter is generated, the RAG layer retrieves the most semantically relevant chunks from all four indexes and feeds them into the prompt. Characters stay consistent. Locations don't move. Plot threads don't get dropped.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Powered by Endee
|
|
47
|
+
|
|
48
|
+
[Endee](https://endee.io) is the vector database that makes NovelForge's memory work. It runs locally via Docker, stores all embeddings on disk, and serves queries over HTTP — no cloud, no API keys, no data leaving your machine.
|
|
49
|
+
|
|
50
|
+
**Why Endee for this project:**
|
|
51
|
+
|
|
52
|
+
- **Multi-index architecture** — NovelForge uses four separate indexes (`story_memory`, `character_memory`, `lore_memory`, `summary_memory`). Endee makes it trivial to create, manage, and query multiple indexes independently, so character recall and lore recall don't interfere with each other.
|
|
53
|
+
- **Filtered queries** — Every vector is tagged with a `novel_id` filter. Endee's `$eq` filter operator means queries are always scoped to the active novel — no cross-contamination between your different stories.
|
|
54
|
+
- **INT8 precision** — All indexes use `Precision.INT8` quantization. This halves the memory footprint of stored vectors with negligible quality loss, which matters when you're embedding hundreds of chunks across long novels.
|
|
55
|
+
- **Cosine similarity** — The embedding model (`nomic-embed-text`) and cosine space type are a natural fit; semantic similarity is what you want when searching story memory by meaning, not distance.
|
|
56
|
+
- **Persistent Docker volume** — Everything written to Endee survives restarts. Your novel memory is durable.
|
|
57
|
+
- **Zero-latency cold start** — Indexes are lazily initialized; the first query creates the index if it doesn't exist, so there's no setup ceremony.
|
|
58
|
+
|
|
59
|
+
**Endee docs:** https://docs.endee.io/quick-start
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **Create novels** — title, genre, and premise is all you need; Chapter 1 is written and saved automatically
|
|
66
|
+
- **Continue stories** — type a chapter instruction; the RAG layer retrieves relevant memory before writing
|
|
67
|
+
- **Ask questions** — query any detail about your story; answers are grounded in stored chapter memory
|
|
68
|
+
- **Interactive / Read-only toggle** — switch between writing mode and read-only browsing at any time
|
|
69
|
+
- **Collapsible sidebar** — novel tree with per-chapter navigation; click any chapter to load its full text and summary
|
|
70
|
+
- **Per-context chat history** — separate conversation thread per novel and per chapter
|
|
71
|
+
- **Story Bible** — characters, locations, factions, and lore auto-extracted from every chapter and saved to the novel's JSON metadata
|
|
72
|
+
- **GPU-accelerated inference** — all Ollama calls (generation + embeddings) are configured to use full GPU offload
|
|
73
|
+
- **Fully local** — Ollama, Endee, and the app all run on your machine; no data leaves it
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Minimum Requirements
|
|
78
|
+
|
|
79
|
+
NovelForge runs heavy local inference. These are the **minimum tested specs**:
|
|
80
|
+
|
|
81
|
+
| Component | Minimum |
|
|
82
|
+
|---|---|
|
|
83
|
+
| GPU | NVIDIA RTX 4050 6 GB VRAM (or equivalent) |
|
|
84
|
+
| RAM | 16 GB |
|
|
85
|
+
| CPU | AMD Ryzen 7 (or equivalent 8-core) |
|
|
86
|
+
| Storage | ~10 GB free (models + novel data) |
|
|
87
|
+
| OS | Windows 10/11, Ubuntu 20.04+, macOS 12+ |
|
|
88
|
+
|
|
89
|
+
> **Expected generation time:** 1–3 minutes per chapter or query, depending on GPU and chapter length. The `qwen3:8b` model runs at roughly 20–40 tokens/sec on an RTX 4050 with full GPU offload.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Prerequisites
|
|
94
|
+
|
|
95
|
+
You need **Ollama** and **Endee** running locally before launching NovelForge.
|
|
96
|
+
|
|
97
|
+
### 1. Start Ollama
|
|
98
|
+
|
|
99
|
+
Download and install Ollama from https://ollama.ai, then pull the two required models:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
ollama pull qwen3:8b
|
|
103
|
+
ollama pull nomic-embed-text
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Ollama starts automatically on install and runs at `http://localhost:11434`. Verify it's running:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
ollama list
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
You should see both `qwen3:8b` and `nomic-embed-text` listed.
|
|
113
|
+
|
|
114
|
+
### 2. Start Endee (Docker)
|
|
115
|
+
|
|
116
|
+
Endee runs as a Docker container. Install Docker Desktop from https://www.docker.com/products/docker-desktop first, then run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
docker run \
|
|
120
|
+
--ulimit nofile=100000:100000 \
|
|
121
|
+
-p 8080:8080 \
|
|
122
|
+
-v ./endee-data:/data \
|
|
123
|
+
--name endee-server \
|
|
124
|
+
--restart unless-stopped \
|
|
125
|
+
endeeio/endee-server:latest
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
What each flag does:
|
|
129
|
+
|
|
130
|
+
| Flag | Purpose |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `--ulimit nofile=100000:100000` | Raises the file descriptor limit — required for Endee's index files under load |
|
|
133
|
+
| `-p 8080:8080` | Exposes Endee's HTTP API on localhost port 8080 |
|
|
134
|
+
| `-v ./endee-data:/data` | Mounts a local folder so all vector data persists across restarts |
|
|
135
|
+
| `--name endee-server` | Names the container so you can stop/start it easily |
|
|
136
|
+
| `--restart unless-stopped` | Auto-restarts Endee if Docker restarts |
|
|
137
|
+
|
|
138
|
+
Verify Endee is running by visiting http://localhost:8080 in your browser — you should see the Endee API response.
|
|
139
|
+
|
|
140
|
+
To stop and restart later:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
docker stop endee-server
|
|
144
|
+
docker start endee-server
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Full Endee documentation: https://docs.endee.io/quick-start
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Installation
|
|
152
|
+
|
|
153
|
+
Once Ollama and Endee are both running:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pip install novelforge
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Launch
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
novelforge
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Or from Python:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from novelforge import launch
|
|
171
|
+
launch()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Usage
|
|
177
|
+
|
|
178
|
+
1. Click **+ New Novel** in the top bar
|
|
179
|
+
2. Enter a title, genre, and premise — then click **Generate Chapter 1**
|
|
180
|
+
3. Wait 1–3 minutes while the chapter is generated, embedded, and saved
|
|
181
|
+
4. The chapter appears in the chat area and the sidebar updates with Chapter 1
|
|
182
|
+
5. Type a chapter instruction (e.g. *"The lion and rat discover an abandoned village"*) and press **Send** or **Ctrl+Enter** to generate the next chapter
|
|
183
|
+
6. Ask questions about your story at any time (e.g. *"What does the rat look like?"*)
|
|
184
|
+
7. Use the **Interactive / Read-only toggle** in the top-right to switch to browse mode
|
|
185
|
+
8. Click any chapter in the sidebar to read its full text and summary
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Project Layout
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
novelforge/
|
|
193
|
+
├── pyproject.toml
|
|
194
|
+
├── README.md
|
|
195
|
+
├── LICENSE
|
|
196
|
+
│
|
|
197
|
+
├── novelforge/
|
|
198
|
+
│ ├── __init__.py # public API — exposes launch()
|
|
199
|
+
│ ├── __main__.py # python -m novelforge entry point
|
|
200
|
+
│ │
|
|
201
|
+
│ ├── core/ # backend — no UI dependency
|
|
202
|
+
│ │ ├── __init__.py
|
|
203
|
+
│ │ ├── prompts.py # all LLM prompt templates
|
|
204
|
+
│ │ ├── generators.py # Ollama calls + GPU options
|
|
205
|
+
│ │ ├── memory.py # Endee vector DB layer (lazy init)
|
|
206
|
+
│ │ ├── novel_manager.py # JSON metadata + flat-file chapter storage
|
|
207
|
+
│ │ └── rag.py # RAG orchestration layer
|
|
208
|
+
│ │
|
|
209
|
+
│ └── ui/ # PyQt6 frontend
|
|
210
|
+
│ ├── __init__.py
|
|
211
|
+
│ ├── style.py # full QSS stylesheet + colour palette
|
|
212
|
+
│ ├── widgets.py # ToggleSwitch, Spinner, LoaderOverlay, MessageBubble
|
|
213
|
+
│ ├── workers.py # QThread workers (CreateNovelWorker, SendMessageWorker)
|
|
214
|
+
│ ├── dialogs.py # New Novel dialog
|
|
215
|
+
│ ├── sidebar.py # collapsible novel/chapter tree
|
|
216
|
+
│ ├── chat_panel.py # chat area (header, scroll, bubbles, input bar)
|
|
217
|
+
│ └── app.py # MainWindow + run() entry point
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## How the Memory System Works
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
Chapter generated
|
|
226
|
+
│
|
|
227
|
+
▼
|
|
228
|
+
chunk_text() splits chapter into 400-word chunks
|
|
229
|
+
│
|
|
230
|
+
▼
|
|
231
|
+
get_embedding() nomic-embed-text via Ollama (GPU accelerated)
|
|
232
|
+
│
|
|
233
|
+
├──▶ story_memory index full chapter chunks
|
|
234
|
+
├──▶ summary_memory index compressed continuity summary
|
|
235
|
+
├──▶ character_memory index extracted character data
|
|
236
|
+
└──▶ lore_memory index locations, factions, world rules
|
|
237
|
+
|
|
238
|
+
Next chapter generation:
|
|
239
|
+
query = user instruction
|
|
240
|
+
│
|
|
241
|
+
▼
|
|
242
|
+
retrieve_all_memory()
|
|
243
|
+
├── story_memory.query(top_k=8)
|
|
244
|
+
├── character_memory.query(top_k=6)
|
|
245
|
+
├── lore_memory.query(top_k=6)
|
|
246
|
+
└── summary_memory.query(top_k=8)
|
|
247
|
+
│
|
|
248
|
+
▼
|
|
249
|
+
context injected into LLM prompt
|
|
250
|
+
│
|
|
251
|
+
▼
|
|
252
|
+
consistent chapter generated
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Known Limitations
|
|
258
|
+
|
|
259
|
+
- Generation time is 1–3 minutes — there is no streaming output yet; the full chapter appears when complete
|
|
260
|
+
- The app has been tested on Windows 11 with an RTX 4050; other configurations may need tuning
|
|
261
|
+
- Endee must be running before the app starts; there is no auto-start or connection retry yet
|
|
262
|
+
- Chapter deletion is not yet implemented in the UI
|
|
263
|
+
- Export to EPUB/PDF is not yet implemented
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT — see `LICENSE` for details.
|
novelforge-1.0/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# NovelForge AI
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Work in Progress** — NovelForge is actively under development. Features may change, break, or be incomplete. Not production-ready.
|
|
4
|
+
|
|
5
|
+
**NovelForge** is an AI-powered desktop novel writer that runs entirely on your local machine. Give it a title, genre, and premise — it writes Chapter 1, remembers everything, and keeps writing with full narrative continuity across as many chapters as you want. Ask it questions about your story and it searches its vector memory to answer accurately.
|
|
6
|
+
|
|
7
|
+
Built on three local services: **Ollama** for LLM inference, **Endee** for vector memory, and **PyQt6** for the desktop UI.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What Makes This Different
|
|
12
|
+
|
|
13
|
+
Most AI writing tools forget everything after each message. NovelForge doesn't. Every chapter you generate is broken into chunks, embedded, and stored across four dedicated Endee vector indexes — story, characters, lore, and summaries. When the next chapter is generated, the RAG layer retrieves the most semantically relevant chunks from all four indexes and feeds them into the prompt. Characters stay consistent. Locations don't move. Plot threads don't get dropped.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Powered by Endee
|
|
18
|
+
|
|
19
|
+
[Endee](https://endee.io) is the vector database that makes NovelForge's memory work. It runs locally via Docker, stores all embeddings on disk, and serves queries over HTTP — no cloud, no API keys, no data leaving your machine.
|
|
20
|
+
|
|
21
|
+
**Why Endee for this project:**
|
|
22
|
+
|
|
23
|
+
- **Multi-index architecture** — NovelForge uses four separate indexes (`story_memory`, `character_memory`, `lore_memory`, `summary_memory`). Endee makes it trivial to create, manage, and query multiple indexes independently, so character recall and lore recall don't interfere with each other.
|
|
24
|
+
- **Filtered queries** — Every vector is tagged with a `novel_id` filter. Endee's `$eq` filter operator means queries are always scoped to the active novel — no cross-contamination between your different stories.
|
|
25
|
+
- **INT8 precision** — All indexes use `Precision.INT8` quantization. This halves the memory footprint of stored vectors with negligible quality loss, which matters when you're embedding hundreds of chunks across long novels.
|
|
26
|
+
- **Cosine similarity** — The embedding model (`nomic-embed-text`) and cosine space type are a natural fit; semantic similarity is what you want when searching story memory by meaning, not distance.
|
|
27
|
+
- **Persistent Docker volume** — Everything written to Endee survives restarts. Your novel memory is durable.
|
|
28
|
+
- **Zero-latency cold start** — Indexes are lazily initialized; the first query creates the index if it doesn't exist, so there's no setup ceremony.
|
|
29
|
+
|
|
30
|
+
**Endee docs:** https://docs.endee.io/quick-start
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Create novels** — title, genre, and premise is all you need; Chapter 1 is written and saved automatically
|
|
37
|
+
- **Continue stories** — type a chapter instruction; the RAG layer retrieves relevant memory before writing
|
|
38
|
+
- **Ask questions** — query any detail about your story; answers are grounded in stored chapter memory
|
|
39
|
+
- **Interactive / Read-only toggle** — switch between writing mode and read-only browsing at any time
|
|
40
|
+
- **Collapsible sidebar** — novel tree with per-chapter navigation; click any chapter to load its full text and summary
|
|
41
|
+
- **Per-context chat history** — separate conversation thread per novel and per chapter
|
|
42
|
+
- **Story Bible** — characters, locations, factions, and lore auto-extracted from every chapter and saved to the novel's JSON metadata
|
|
43
|
+
- **GPU-accelerated inference** — all Ollama calls (generation + embeddings) are configured to use full GPU offload
|
|
44
|
+
- **Fully local** — Ollama, Endee, and the app all run on your machine; no data leaves it
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Minimum Requirements
|
|
49
|
+
|
|
50
|
+
NovelForge runs heavy local inference. These are the **minimum tested specs**:
|
|
51
|
+
|
|
52
|
+
| Component | Minimum |
|
|
53
|
+
|---|---|
|
|
54
|
+
| GPU | NVIDIA RTX 4050 6 GB VRAM (or equivalent) |
|
|
55
|
+
| RAM | 16 GB |
|
|
56
|
+
| CPU | AMD Ryzen 7 (or equivalent 8-core) |
|
|
57
|
+
| Storage | ~10 GB free (models + novel data) |
|
|
58
|
+
| OS | Windows 10/11, Ubuntu 20.04+, macOS 12+ |
|
|
59
|
+
|
|
60
|
+
> **Expected generation time:** 1–3 minutes per chapter or query, depending on GPU and chapter length. The `qwen3:8b` model runs at roughly 20–40 tokens/sec on an RTX 4050 with full GPU offload.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Prerequisites
|
|
65
|
+
|
|
66
|
+
You need **Ollama** and **Endee** running locally before launching NovelForge.
|
|
67
|
+
|
|
68
|
+
### 1. Start Ollama
|
|
69
|
+
|
|
70
|
+
Download and install Ollama from https://ollama.ai, then pull the two required models:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
ollama pull qwen3:8b
|
|
74
|
+
ollama pull nomic-embed-text
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Ollama starts automatically on install and runs at `http://localhost:11434`. Verify it's running:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
ollama list
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You should see both `qwen3:8b` and `nomic-embed-text` listed.
|
|
84
|
+
|
|
85
|
+
### 2. Start Endee (Docker)
|
|
86
|
+
|
|
87
|
+
Endee runs as a Docker container. Install Docker Desktop from https://www.docker.com/products/docker-desktop first, then run:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
docker run \
|
|
91
|
+
--ulimit nofile=100000:100000 \
|
|
92
|
+
-p 8080:8080 \
|
|
93
|
+
-v ./endee-data:/data \
|
|
94
|
+
--name endee-server \
|
|
95
|
+
--restart unless-stopped \
|
|
96
|
+
endeeio/endee-server:latest
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
What each flag does:
|
|
100
|
+
|
|
101
|
+
| Flag | Purpose |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `--ulimit nofile=100000:100000` | Raises the file descriptor limit — required for Endee's index files under load |
|
|
104
|
+
| `-p 8080:8080` | Exposes Endee's HTTP API on localhost port 8080 |
|
|
105
|
+
| `-v ./endee-data:/data` | Mounts a local folder so all vector data persists across restarts |
|
|
106
|
+
| `--name endee-server` | Names the container so you can stop/start it easily |
|
|
107
|
+
| `--restart unless-stopped` | Auto-restarts Endee if Docker restarts |
|
|
108
|
+
|
|
109
|
+
Verify Endee is running by visiting http://localhost:8080 in your browser — you should see the Endee API response.
|
|
110
|
+
|
|
111
|
+
To stop and restart later:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
docker stop endee-server
|
|
115
|
+
docker start endee-server
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Full Endee documentation: https://docs.endee.io/quick-start
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Installation
|
|
123
|
+
|
|
124
|
+
Once Ollama and Endee are both running:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install novelforge
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Launch
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
novelforge
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Or from Python:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from novelforge import launch
|
|
142
|
+
launch()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Usage
|
|
148
|
+
|
|
149
|
+
1. Click **+ New Novel** in the top bar
|
|
150
|
+
2. Enter a title, genre, and premise — then click **Generate Chapter 1**
|
|
151
|
+
3. Wait 1–3 minutes while the chapter is generated, embedded, and saved
|
|
152
|
+
4. The chapter appears in the chat area and the sidebar updates with Chapter 1
|
|
153
|
+
5. Type a chapter instruction (e.g. *"The lion and rat discover an abandoned village"*) and press **Send** or **Ctrl+Enter** to generate the next chapter
|
|
154
|
+
6. Ask questions about your story at any time (e.g. *"What does the rat look like?"*)
|
|
155
|
+
7. Use the **Interactive / Read-only toggle** in the top-right to switch to browse mode
|
|
156
|
+
8. Click any chapter in the sidebar to read its full text and summary
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Project Layout
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
novelforge/
|
|
164
|
+
├── pyproject.toml
|
|
165
|
+
├── README.md
|
|
166
|
+
├── LICENSE
|
|
167
|
+
│
|
|
168
|
+
├── novelforge/
|
|
169
|
+
│ ├── __init__.py # public API — exposes launch()
|
|
170
|
+
│ ├── __main__.py # python -m novelforge entry point
|
|
171
|
+
│ │
|
|
172
|
+
│ ├── core/ # backend — no UI dependency
|
|
173
|
+
│ │ ├── __init__.py
|
|
174
|
+
│ │ ├── prompts.py # all LLM prompt templates
|
|
175
|
+
│ │ ├── generators.py # Ollama calls + GPU options
|
|
176
|
+
│ │ ├── memory.py # Endee vector DB layer (lazy init)
|
|
177
|
+
│ │ ├── novel_manager.py # JSON metadata + flat-file chapter storage
|
|
178
|
+
│ │ └── rag.py # RAG orchestration layer
|
|
179
|
+
│ │
|
|
180
|
+
│ └── ui/ # PyQt6 frontend
|
|
181
|
+
│ ├── __init__.py
|
|
182
|
+
│ ├── style.py # full QSS stylesheet + colour palette
|
|
183
|
+
│ ├── widgets.py # ToggleSwitch, Spinner, LoaderOverlay, MessageBubble
|
|
184
|
+
│ ├── workers.py # QThread workers (CreateNovelWorker, SendMessageWorker)
|
|
185
|
+
│ ├── dialogs.py # New Novel dialog
|
|
186
|
+
│ ├── sidebar.py # collapsible novel/chapter tree
|
|
187
|
+
│ ├── chat_panel.py # chat area (header, scroll, bubbles, input bar)
|
|
188
|
+
│ └── app.py # MainWindow + run() entry point
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## How the Memory System Works
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
Chapter generated
|
|
197
|
+
│
|
|
198
|
+
▼
|
|
199
|
+
chunk_text() splits chapter into 400-word chunks
|
|
200
|
+
│
|
|
201
|
+
▼
|
|
202
|
+
get_embedding() nomic-embed-text via Ollama (GPU accelerated)
|
|
203
|
+
│
|
|
204
|
+
├──▶ story_memory index full chapter chunks
|
|
205
|
+
├──▶ summary_memory index compressed continuity summary
|
|
206
|
+
├──▶ character_memory index extracted character data
|
|
207
|
+
└──▶ lore_memory index locations, factions, world rules
|
|
208
|
+
|
|
209
|
+
Next chapter generation:
|
|
210
|
+
query = user instruction
|
|
211
|
+
│
|
|
212
|
+
▼
|
|
213
|
+
retrieve_all_memory()
|
|
214
|
+
├── story_memory.query(top_k=8)
|
|
215
|
+
├── character_memory.query(top_k=6)
|
|
216
|
+
├── lore_memory.query(top_k=6)
|
|
217
|
+
└── summary_memory.query(top_k=8)
|
|
218
|
+
│
|
|
219
|
+
▼
|
|
220
|
+
context injected into LLM prompt
|
|
221
|
+
│
|
|
222
|
+
▼
|
|
223
|
+
consistent chapter generated
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Known Limitations
|
|
229
|
+
|
|
230
|
+
- Generation time is 1–3 minutes — there is no streaming output yet; the full chapter appears when complete
|
|
231
|
+
- The app has been tested on Windows 11 with an RTX 4050; other configurations may need tuning
|
|
232
|
+
- Endee must be running before the app starts; there is no auto-start or connection retry yet
|
|
233
|
+
- Chapter deletion is not yet implemented in the UI
|
|
234
|
+
- Export to EPUB/PDF is not yet implemented
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT — see `LICENSE` for details.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import ollama
|
|
6
|
+
|
|
7
|
+
from novelforge.core.prompts import (
|
|
8
|
+
NOVEL_SYSTEM_PROMPT,
|
|
9
|
+
CREATE_NOVEL_PROMPT,
|
|
10
|
+
CHAPTER_PLAN_PROMPT,
|
|
11
|
+
CONTINUE_NOVEL_PROMPT,
|
|
12
|
+
MEMORY_COMPRESSION_PROMPT,
|
|
13
|
+
CHARACTER_EXTRACTION_PROMPT,
|
|
14
|
+
LORE_EXTRACTION_PROMPT,
|
|
15
|
+
QA_PROMPT,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
STORY_MODEL = "qwen3:8b"
|
|
19
|
+
|
|
20
|
+
GPU_OPTIONS = {
|
|
21
|
+
"num_gpu": -1,
|
|
22
|
+
"main_gpu": 0,
|
|
23
|
+
"num_batch": 512,
|
|
24
|
+
"num_ctx": 4096,
|
|
25
|
+
"f16_kv": True,
|
|
26
|
+
"use_mmap": True,
|
|
27
|
+
"use_mlock": False,
|
|
28
|
+
"num_thread": 0,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def llm(
|
|
33
|
+
prompt: str,
|
|
34
|
+
system: str = NOVEL_SYSTEM_PROMPT,
|
|
35
|
+
temperature: float = 0.8,
|
|
36
|
+
) -> str:
|
|
37
|
+
response = ollama.chat(
|
|
38
|
+
model=STORY_MODEL,
|
|
39
|
+
messages=[
|
|
40
|
+
{"role": "system", "content": system},
|
|
41
|
+
{"role": "user", "content": prompt},
|
|
42
|
+
],
|
|
43
|
+
options={**GPU_OPTIONS, "temperature": temperature},
|
|
44
|
+
)
|
|
45
|
+
return response["message"]["content"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def classify_intent(user_input: str) -> str:
|
|
49
|
+
text = user_input.lower().strip()
|
|
50
|
+
new_keywords = [
|
|
51
|
+
"new novel",
|
|
52
|
+
"new story",
|
|
53
|
+
"write a novel",
|
|
54
|
+
"create a novel",
|
|
55
|
+
"start a novel",
|
|
56
|
+
"novel idea",
|
|
57
|
+
]
|
|
58
|
+
question_keywords = ["who", "what", "when", "where", "why", "how"]
|
|
59
|
+
if any(k in text for k in new_keywords):
|
|
60
|
+
return "NEW_NOVEL"
|
|
61
|
+
if "?" in text:
|
|
62
|
+
return "QUESTION"
|
|
63
|
+
if any(text.startswith(k) for k in question_keywords):
|
|
64
|
+
return "QUESTION"
|
|
65
|
+
return "CONTINUE"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def create_first_chapter(idea: str) -> str:
|
|
69
|
+
return llm(CREATE_NOVEL_PROMPT.format(idea=idea))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_chapter_plan(memory: str, instruction: str) -> str:
|
|
73
|
+
return llm(
|
|
74
|
+
CHAPTER_PLAN_PROMPT.format(memory=memory, instruction=instruction),
|
|
75
|
+
temperature=0.4,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def generate_chapter(memory: str, plan: str, instruction: str) -> str:
|
|
80
|
+
return llm(
|
|
81
|
+
CONTINUE_NOVEL_PROMPT.format(memory=memory, plan=plan, instruction=instruction)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def compress_memory(chapter: str) -> str:
|
|
86
|
+
return llm(
|
|
87
|
+
MEMORY_COMPRESSION_PROMPT.format(chapter=chapter),
|
|
88
|
+
temperature=0.2,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def extract_characters(chapter: str) -> list[dict]:
|
|
93
|
+
result = llm(
|
|
94
|
+
CHARACTER_EXTRACTION_PROMPT.format(chapter=chapter),
|
|
95
|
+
temperature=0.1,
|
|
96
|
+
)
|
|
97
|
+
try:
|
|
98
|
+
return json.loads(result)
|
|
99
|
+
except Exception:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def extract_lore(chapter: str) -> str:
|
|
104
|
+
return llm(
|
|
105
|
+
LORE_EXTRACTION_PROMPT.format(chapter=chapter),
|
|
106
|
+
temperature=0.2,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def answer_story_question(context: str, question: str) -> str:
|
|
111
|
+
return llm(
|
|
112
|
+
QA_PROMPT.format(context=context, question=question),
|
|
113
|
+
temperature=0.1,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def generate_story_package(memory: str, instruction: str) -> dict:
|
|
118
|
+
plan = create_chapter_plan(memory, instruction)
|
|
119
|
+
chapter = generate_chapter(memory, plan, instruction)
|
|
120
|
+
summary = compress_memory(chapter)
|
|
121
|
+
characters = extract_characters(chapter)
|
|
122
|
+
lore = extract_lore(chapter)
|
|
123
|
+
return {
|
|
124
|
+
"plan": plan,
|
|
125
|
+
"chapter": chapter,
|
|
126
|
+
"summary": summary,
|
|
127
|
+
"characters": characters,
|
|
128
|
+
"lore": lore,
|
|
129
|
+
}
|