inscien 0.2.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.
- inscien-0.2.0/.gitignore +312 -0
- inscien-0.2.0/PKG-INFO +65 -0
- inscien-0.2.0/core/db.py +95 -0
- inscien-0.2.0/core/paths.py +25 -0
- inscien-0.2.0/launcher.py +58 -0
- inscien-0.2.0/main.py +139 -0
- inscien-0.2.0/models/app_settings.py +32 -0
- inscien-0.2.0/pyproject.toml +92 -0
- inscien-0.2.0/repositories/settings_repository.py +33 -0
- inscien-0.2.0/requirements.txt +69 -0
- inscien-0.2.0/routers/graph.py +107 -0
- inscien-0.2.0/routers/narrate.py +151 -0
- inscien-0.2.0/routers/papers.py +61 -0
- inscien-0.2.0/routers/settings.py +98 -0
- inscien-0.2.0/routers/zotero.py +123 -0
- inscien-0.2.0/run_server.py +29 -0
- inscien-0.2.0/schemas/settings.py +23 -0
- inscien-0.2.0/scripts/check_refs.py +78 -0
- inscien-0.2.0/services/job_runner.py +192 -0
- inscien-0.2.0/services/lab/pdf_parser.py +69 -0
- inscien-0.2.0/services/llm/client.py +287 -0
- inscien-0.2.0/services/narration/__init__.py +0 -0
- inscien-0.2.0/services/narration/jobs.py +94 -0
- inscien-0.2.0/services/narration/model.py +94 -0
- inscien-0.2.0/services/narration/pipeline.py +190 -0
- inscien-0.2.0/services/narration/tts_engine.py +213 -0
- inscien-0.2.0/services/refs/fetch_jobs.py +59 -0
- inscien-0.2.0/services/refs/openalex.py +190 -0
- inscien-0.2.0/services/refs/refstore.py +364 -0
- inscien-0.2.0/services/state_guard.py +51 -0
- inscien-0.2.0/services/zotero/__init__.py +0 -0
- inscien-0.2.0/services/zotero/reader.py +513 -0
- inscien-0.2.0/services/zotero/settings.py +52 -0
- inscien-0.2.0/webui/404/index.html +1 -0
- inscien-0.2.0/webui/404.html +1 -0
- inscien-0.2.0/webui/__next.__PAGE__.txt +9 -0
- inscien-0.2.0/webui/__next._full.txt +22 -0
- inscien-0.2.0/webui/__next._head.txt +6 -0
- inscien-0.2.0/webui/__next._index.txt +7 -0
- inscien-0.2.0/webui/__next._tree.txt +4 -0
- inscien-0.2.0/webui/_next/static/TSQ7-kGL6XBkP1azJpkxU/_buildManifest.js +11 -0
- inscien-0.2.0/webui/_next/static/TSQ7-kGL6XBkP1azJpkxU/_clientMiddlewareManifest.json +1 -0
- inscien-0.2.0/webui/_next/static/TSQ7-kGL6XBkP1azJpkxU/_ssgManifest.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/0f6b8b320c4d90c3.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/191ccb8e48d87c18.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/3555439a4ec6d383.js +2 -0
- inscien-0.2.0/webui/_next/static/chunks/35c307818fb96058.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/38a348bf88033a6c.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/40b2c258e0c3f3b2.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/4d708587bb9ed414.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/632079a7dfdf58c3.js +2 -0
- inscien-0.2.0/webui/_next/static/chunks/79764a2363f49b32.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/7d626b9c3eda6e32.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/7e2eb80b6e54e441.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/7ebc8031df18b89e.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/8a460e86ba76adb4.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/99bd76c5a9b85646.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
- inscien-0.2.0/webui/_next/static/chunks/b533e0edc315d683.css +3 -0
- inscien-0.2.0/webui/_next/static/chunks/b899275a30baa369.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/cbd63a10c5edde05.css +3 -0
- inscien-0.2.0/webui/_next/static/chunks/d2be314c3ece3fbe.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/d4357b8e9f3078d8.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/d573c5342989b77f.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/d59f830a2b8e768c.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/f091501564eb2ea3.js +4 -0
- inscien-0.2.0/webui/_next/static/chunks/ff1a16fafef87110.js +1 -0
- inscien-0.2.0/webui/_next/static/chunks/turbopack-b7fd4216d7d389e6.js +4 -0
- inscien-0.2.0/webui/_next/static/media/032b8f4a4d05772e-s.4fc86242.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/53b9e256198e5412-s.853d50a3.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/617e1646a751849a-s.6b81bcff.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/7178b3e590c64307-s.55554cd0.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/7c7fbf33ae4e53ea-s.8f558b88.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/8a480f0b521d4e75-s.ea323500.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/a0f3c1c4f2337ba2-s.11606649.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/a96137a1efe31085-s.p.344af709.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/d20b999fa2a8347f-s.0e2a9354.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/f6927c9762797221-s.597c407c.woff2 +0 -0
- inscien-0.2.0/webui/_next/static/media/fef07dbb0973bf53-s.518e079e.woff2 +0 -0
- inscien-0.2.0/webui/_not-found/__next._full.txt +16 -0
- inscien-0.2.0/webui/_not-found/__next._head.txt +6 -0
- inscien-0.2.0/webui/_not-found/__next._index.txt +7 -0
- inscien-0.2.0/webui/_not-found/__next._not-found.__PAGE__.txt +5 -0
- inscien-0.2.0/webui/_not-found/__next._not-found.txt +4 -0
- inscien-0.2.0/webui/_not-found/__next._tree.txt +2 -0
- inscien-0.2.0/webui/_not-found/index.html +1 -0
- inscien-0.2.0/webui/_not-found/index.txt +16 -0
- inscien-0.2.0/webui/doc-1.png +0 -0
- inscien-0.2.0/webui/doc-2.png +0 -0
- inscien-0.2.0/webui/doc-3.png +0 -0
- inscien-0.2.0/webui/doc-4.png +0 -0
- inscien-0.2.0/webui/docs.png +0 -0
- inscien-0.2.0/webui/hero.png +0 -0
- inscien-0.2.0/webui/icon.png +0 -0
- inscien-0.2.0/webui/icon.svg +1 -0
- inscien-0.2.0/webui/index.html +1 -0
- inscien-0.2.0/webui/index.txt +22 -0
- inscien-0.2.0/webui/map/__next._full.txt +23 -0
- inscien-0.2.0/webui/map/__next._head.txt +6 -0
- inscien-0.2.0/webui/map/__next._index.txt +7 -0
- inscien-0.2.0/webui/map/__next._tree.txt +5 -0
- inscien-0.2.0/webui/map/__next.map.__PAGE__.txt +9 -0
- inscien-0.2.0/webui/map/__next.map.txt +5 -0
- inscien-0.2.0/webui/map/index.html +1 -0
- inscien-0.2.0/webui/map/index.txt +23 -0
- inscien-0.2.0/webui/notebook-1.png +0 -0
- inscien-0.2.0/webui/notebook-2.png +0 -0
- inscien-0.2.0/webui/notebook-3.png +0 -0
- inscien-0.2.0/webui/notebook-4.png +0 -0
- inscien-0.2.0/webui/notebooks.png +0 -0
- inscien-0.2.0/webui/pdf.worker.min.mjs +21 -0
- inscien-0.2.0/webui/settings/__next._full.txt +22 -0
- inscien-0.2.0/webui/settings/__next._head.txt +6 -0
- inscien-0.2.0/webui/settings/__next._index.txt +7 -0
- inscien-0.2.0/webui/settings/__next._tree.txt +4 -0
- inscien-0.2.0/webui/settings/__next.settings.__PAGE__.txt +9 -0
- inscien-0.2.0/webui/settings/__next.settings.txt +4 -0
- inscien-0.2.0/webui/settings/index.html +1 -0
- inscien-0.2.0/webui/settings/index.txt +22 -0
inscien-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# testing
|
|
14
|
+
/coverage
|
|
15
|
+
|
|
16
|
+
# next.js
|
|
17
|
+
.next/
|
|
18
|
+
/out/
|
|
19
|
+
|
|
20
|
+
# production
|
|
21
|
+
/build
|
|
22
|
+
|
|
23
|
+
# misc
|
|
24
|
+
.DS_Store
|
|
25
|
+
*.pem
|
|
26
|
+
|
|
27
|
+
# debug
|
|
28
|
+
npm-debug.log*
|
|
29
|
+
yarn-debug.log*
|
|
30
|
+
yarn-error.log*
|
|
31
|
+
.pnpm-debug.log*
|
|
32
|
+
|
|
33
|
+
# env files (config lives in the in-app Settings page; no .env is shipped)
|
|
34
|
+
.env*
|
|
35
|
+
*.env*
|
|
36
|
+
|
|
37
|
+
# vercel
|
|
38
|
+
.vercel
|
|
39
|
+
|
|
40
|
+
# typescript
|
|
41
|
+
*.tsbuildinfo
|
|
42
|
+
next-env.d.ts
|
|
43
|
+
|
|
44
|
+
# Byte-compiled / optimized / DLL files
|
|
45
|
+
__pycache__/
|
|
46
|
+
*.py[codz]
|
|
47
|
+
*$py.class
|
|
48
|
+
|
|
49
|
+
# C extensions
|
|
50
|
+
*.so
|
|
51
|
+
|
|
52
|
+
# Distribution / packaging
|
|
53
|
+
.Python
|
|
54
|
+
build/
|
|
55
|
+
develop-eggs/
|
|
56
|
+
dist/
|
|
57
|
+
downloads/
|
|
58
|
+
eggs/
|
|
59
|
+
.eggs/
|
|
60
|
+
/lib/
|
|
61
|
+
/lib64/
|
|
62
|
+
parts/
|
|
63
|
+
sdist/
|
|
64
|
+
var/
|
|
65
|
+
wheels/
|
|
66
|
+
share/python-wheels/
|
|
67
|
+
*.egg-info/
|
|
68
|
+
.installed.cfg
|
|
69
|
+
*.egg
|
|
70
|
+
MANIFEST
|
|
71
|
+
|
|
72
|
+
# PyInstaller
|
|
73
|
+
# Usually these files are written by a python script from a template
|
|
74
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
75
|
+
*.manifest
|
|
76
|
+
*.spec
|
|
77
|
+
# ...but ours is a hand-written, committed build input the release CI depends on.
|
|
78
|
+
!backend/inscien.spec
|
|
79
|
+
|
|
80
|
+
# Installer logs
|
|
81
|
+
pip-log.txt
|
|
82
|
+
pip-delete-this-directory.txt
|
|
83
|
+
|
|
84
|
+
# Unit test / coverage reports
|
|
85
|
+
htmlcov/
|
|
86
|
+
.tox/
|
|
87
|
+
.nox/
|
|
88
|
+
.coverage
|
|
89
|
+
.coverage.*
|
|
90
|
+
.cache
|
|
91
|
+
nosetests.xml
|
|
92
|
+
coverage.xml
|
|
93
|
+
*.cover
|
|
94
|
+
*.py.cover
|
|
95
|
+
.hypothesis/
|
|
96
|
+
.pytest_cache/
|
|
97
|
+
cover/
|
|
98
|
+
|
|
99
|
+
# Translations
|
|
100
|
+
*.mo
|
|
101
|
+
*.pot
|
|
102
|
+
|
|
103
|
+
# Django stuff:
|
|
104
|
+
*.log
|
|
105
|
+
local_settings.py
|
|
106
|
+
db.sqlite3
|
|
107
|
+
db.sqlite3-journal
|
|
108
|
+
|
|
109
|
+
# Flask stuff:
|
|
110
|
+
instance/
|
|
111
|
+
.webassets-cache
|
|
112
|
+
|
|
113
|
+
# Scrapy stuff:
|
|
114
|
+
.scrapy
|
|
115
|
+
|
|
116
|
+
# Sphinx documentation
|
|
117
|
+
docs/_build/
|
|
118
|
+
|
|
119
|
+
# PyBuilder
|
|
120
|
+
.pybuilder/
|
|
121
|
+
target/
|
|
122
|
+
|
|
123
|
+
# Jupyter Notebook
|
|
124
|
+
.ipynb_checkpoints
|
|
125
|
+
|
|
126
|
+
# IPython
|
|
127
|
+
profile_default/
|
|
128
|
+
ipython_config.py
|
|
129
|
+
|
|
130
|
+
# pyenv
|
|
131
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
132
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
133
|
+
# .python-version
|
|
134
|
+
|
|
135
|
+
# pipenv
|
|
136
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
137
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
138
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
139
|
+
# install all needed dependencies.
|
|
140
|
+
#Pipfile.lock
|
|
141
|
+
|
|
142
|
+
# UV
|
|
143
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
144
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
145
|
+
# commonly ignored for libraries.
|
|
146
|
+
#uv.lock
|
|
147
|
+
|
|
148
|
+
# poetry
|
|
149
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
150
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
151
|
+
# commonly ignored for libraries.
|
|
152
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
153
|
+
#poetry.lock
|
|
154
|
+
#poetry.toml
|
|
155
|
+
|
|
156
|
+
# pdm
|
|
157
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
158
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
159
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
160
|
+
#pdm.lock
|
|
161
|
+
#pdm.toml
|
|
162
|
+
.pdm-python
|
|
163
|
+
.pdm-build/
|
|
164
|
+
|
|
165
|
+
# pixi
|
|
166
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
167
|
+
#pixi.lock
|
|
168
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
169
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
170
|
+
.pixi
|
|
171
|
+
|
|
172
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
173
|
+
__pypackages__/
|
|
174
|
+
|
|
175
|
+
# Celery stuff
|
|
176
|
+
celerybeat-schedule
|
|
177
|
+
celerybeat.pid
|
|
178
|
+
|
|
179
|
+
# SageMath parsed files
|
|
180
|
+
*.sage.py
|
|
181
|
+
|
|
182
|
+
# Environments
|
|
183
|
+
.env
|
|
184
|
+
.envrc
|
|
185
|
+
.venv
|
|
186
|
+
.venv.local
|
|
187
|
+
env/
|
|
188
|
+
venv/
|
|
189
|
+
ENV/
|
|
190
|
+
env.bak/
|
|
191
|
+
venv.bak/
|
|
192
|
+
|
|
193
|
+
# Spyder project settings
|
|
194
|
+
.spyderproject
|
|
195
|
+
.spyproject
|
|
196
|
+
|
|
197
|
+
# Rope project settings
|
|
198
|
+
.ropeproject
|
|
199
|
+
|
|
200
|
+
# mkdocs documentation (not used here). NOTE: `site/` is our Astro marketing+docs SOURCE and
|
|
201
|
+
# must be tracked, so this rule is disabled. Astro's build output (site/dist) is ignored by
|
|
202
|
+
# site/.gitignore instead.
|
|
203
|
+
# /site
|
|
204
|
+
|
|
205
|
+
# mypy
|
|
206
|
+
.mypy_cache/
|
|
207
|
+
.dmypy.json
|
|
208
|
+
dmypy.json
|
|
209
|
+
|
|
210
|
+
# Pyre type checker
|
|
211
|
+
.pyre/
|
|
212
|
+
|
|
213
|
+
# pytype static type analyzer
|
|
214
|
+
.pytype/
|
|
215
|
+
|
|
216
|
+
# Cython debug symbols
|
|
217
|
+
cython_debug/
|
|
218
|
+
|
|
219
|
+
# PyCharm
|
|
220
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
221
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
222
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
223
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
224
|
+
#.idea/
|
|
225
|
+
|
|
226
|
+
# Abstra
|
|
227
|
+
# Abstra is an AI-powered process automation framework.
|
|
228
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
229
|
+
# Learn more at https://abstra.io/docs
|
|
230
|
+
.abstra/
|
|
231
|
+
|
|
232
|
+
# Visual Studio Code
|
|
233
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
234
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
235
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
236
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
237
|
+
# .vscode/
|
|
238
|
+
|
|
239
|
+
# Ruff stuff:
|
|
240
|
+
.ruff_cache/
|
|
241
|
+
|
|
242
|
+
# PyPI configuration file
|
|
243
|
+
.pypirc
|
|
244
|
+
|
|
245
|
+
# Cursor
|
|
246
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
247
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
248
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
249
|
+
.cursorignore
|
|
250
|
+
.cursorindexingignore
|
|
251
|
+
|
|
252
|
+
# Marimo
|
|
253
|
+
marimo/_static/
|
|
254
|
+
marimo/_lsp/
|
|
255
|
+
__marimo__/
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Database files
|
|
260
|
+
*.sqlite3
|
|
261
|
+
|
|
262
|
+
# Temporary files
|
|
263
|
+
*.tmp
|
|
264
|
+
|
|
265
|
+
# Celery (scaffolding only — not run in InScien)
|
|
266
|
+
celerybeat-schedule*
|
|
267
|
+
celerybeat.pid
|
|
268
|
+
|
|
269
|
+
# InScien-specific
|
|
270
|
+
*.sqlite
|
|
271
|
+
*.sqlite3
|
|
272
|
+
inscien.db
|
|
273
|
+
|
|
274
|
+
# Durable user data (DB, indexes, narration audio) — host-owned, never tracked.
|
|
275
|
+
# Use `data/*` (not `data/`) so the negation below can re-include the keep file:
|
|
276
|
+
# Git can't re-include a path once its parent directory itself is excluded.
|
|
277
|
+
data/*
|
|
278
|
+
!data/.gitkeep
|
|
279
|
+
|
|
280
|
+
# throwaway TTS benchmark harness + pre-Kokoro GPU-TTS leftovers
|
|
281
|
+
tts-bench/
|
|
282
|
+
tts/
|
|
283
|
+
|
|
284
|
+
# empty AI-assistant scratch dirs
|
|
285
|
+
.agents/
|
|
286
|
+
.codex/
|
|
287
|
+
|
|
288
|
+
# Editor + AI-assistant local files (kept locally, not shipped in the public repo).
|
|
289
|
+
# CLAUDE.md IS tracked: it's project-wide architecture/onboarding docs, useful to any
|
|
290
|
+
# contributor or assistant. AGENTS.md stays local (it encodes this host's workflow rules).
|
|
291
|
+
.vscode/
|
|
292
|
+
AGENTS.md
|
|
293
|
+
|
|
294
|
+
# Local Zotero test snapshot (never commit — full library metadata + PDFs)
|
|
295
|
+
zotero-sample/
|
|
296
|
+
zotero.sqlite
|
|
297
|
+
zotero.sqlite*
|
|
298
|
+
zotero-storage/
|
|
299
|
+
|
|
300
|
+
# Generated pdf.js worker (copied from pdfjs-dist on predev/prebuild)
|
|
301
|
+
frontend/public/pdf.worker.min.mjs
|
|
302
|
+
|
|
303
|
+
# Next.js static export (build artifact — regenerated on every build)
|
|
304
|
+
frontend/out/
|
|
305
|
+
|
|
306
|
+
# Vendored UI + wheel artifacts for the pip/uvx build (regenerated by `make web` / `make wheel`)
|
|
307
|
+
backend/webui/
|
|
308
|
+
backend/dist/
|
|
309
|
+
|
|
310
|
+
# Build scratch / stray files
|
|
311
|
+
.build-tmp/
|
|
312
|
+
/docker
|
inscien-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inscien
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A local, private companion over your own Zotero library - citation Map + audio Narrate, served in your browser.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: <3.13,>=3.12
|
|
7
|
+
Requires-Dist: annotated-doc==0.0.4
|
|
8
|
+
Requires-Dist: annotated-types==0.7.0
|
|
9
|
+
Requires-Dist: anyio==4.13.0
|
|
10
|
+
Requires-Dist: audioop-lts; python_version >= '3.13'
|
|
11
|
+
Requires-Dist: certifi==2026.2.25
|
|
12
|
+
Requires-Dist: cffi==2.0.0
|
|
13
|
+
Requires-Dist: charset-normalizer==3.4.7
|
|
14
|
+
Requires-Dist: click==8.3.2
|
|
15
|
+
Requires-Dist: cryptography==46.0.7
|
|
16
|
+
Requires-Dist: distro==1.9.0
|
|
17
|
+
Requires-Dist: fastapi==0.135.3
|
|
18
|
+
Requires-Dist: filelock==3.29.0
|
|
19
|
+
Requires-Dist: flatbuffers==25.12.19
|
|
20
|
+
Requires-Dist: fsspec==2026.4.0
|
|
21
|
+
Requires-Dist: greenlet==3.4.0
|
|
22
|
+
Requires-Dist: grpcio==1.80.0
|
|
23
|
+
Requires-Dist: h11==0.16.0
|
|
24
|
+
Requires-Dist: h2==4.3.0
|
|
25
|
+
Requires-Dist: hf-xet==1.5.0
|
|
26
|
+
Requires-Dist: hpack==4.1.0
|
|
27
|
+
Requires-Dist: httpcore==1.0.9
|
|
28
|
+
Requires-Dist: httpx==0.28.1
|
|
29
|
+
Requires-Dist: huggingface-hub==1.15.0
|
|
30
|
+
Requires-Dist: hyperframe==6.1.0
|
|
31
|
+
Requires-Dist: idna==3.11
|
|
32
|
+
Requires-Dist: imageio-ffmpeg>=0.5
|
|
33
|
+
Requires-Dist: jiter==0.14.0
|
|
34
|
+
Requires-Dist: kokoro-onnx>=0.5
|
|
35
|
+
Requires-Dist: mmh3==5.2.1
|
|
36
|
+
Requires-Dist: numpy==2.4.4
|
|
37
|
+
Requires-Dist: onnxruntime==1.26.0
|
|
38
|
+
Requires-Dist: openai==2.37.0
|
|
39
|
+
Requires-Dist: packaging==26.1
|
|
40
|
+
Requires-Dist: pillow==12.2.0
|
|
41
|
+
Requires-Dist: platformdirs>=4
|
|
42
|
+
Requires-Dist: portalocker==3.2.0
|
|
43
|
+
Requires-Dist: protobuf==7.34.1
|
|
44
|
+
Requires-Dist: py-rust-stemmers==0.1.5
|
|
45
|
+
Requires-Dist: pycparser==3.0
|
|
46
|
+
Requires-Dist: pydantic-core==2.46.0
|
|
47
|
+
Requires-Dist: pydantic==2.13.0
|
|
48
|
+
Requires-Dist: pydub>=0.25
|
|
49
|
+
Requires-Dist: pymupdf<2.0,>=1.24
|
|
50
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
51
|
+
Requires-Dist: python-dotenv==1.2.2
|
|
52
|
+
Requires-Dist: python-multipart==0.0.27
|
|
53
|
+
Requires-Dist: pyyaml==6.0.3
|
|
54
|
+
Requires-Dist: requests==2.33.1
|
|
55
|
+
Requires-Dist: six==1.17.0
|
|
56
|
+
Requires-Dist: sniffio==1.3.1
|
|
57
|
+
Requires-Dist: sqlalchemy==2.0.49
|
|
58
|
+
Requires-Dist: starlette==1.0.0
|
|
59
|
+
Requires-Dist: tokenizers==0.23.1
|
|
60
|
+
Requires-Dist: tqdm==4.67.3
|
|
61
|
+
Requires-Dist: typing-extensions==4.15.0
|
|
62
|
+
Requires-Dist: typing-inspection==0.4.2
|
|
63
|
+
Requires-Dist: tzdata==2026.1
|
|
64
|
+
Requires-Dist: urllib3==2.6.3
|
|
65
|
+
Requires-Dist: uvicorn==0.44.0
|
inscien-0.2.0/core/db.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database connection and session management.
|
|
3
|
+
|
|
4
|
+
InScien is single-user and local-first, so the store is a file-based SQLite
|
|
5
|
+
database (no server to run). `DATABASE_URL` overrides the location/backend -
|
|
6
|
+
the compose file sets it to `sqlite:////workspace/data/inscien.db`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from sqlalchemy import create_engine, inspect, text
|
|
12
|
+
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
|
|
13
|
+
|
|
14
|
+
from core.paths import data_path
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
Base = declarative_base()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _norm(s: str | None) -> str:
|
|
22
|
+
return "" if s is None else s.strip()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_database_url() -> str:
|
|
26
|
+
# Explicit URL wins; otherwise default to the local-first SQLite store.
|
|
27
|
+
explicit = _norm(os.getenv("DATABASE_URL"))
|
|
28
|
+
if explicit:
|
|
29
|
+
return explicit
|
|
30
|
+
path = _norm(os.getenv("SQLITE_PATH")) or data_path("inscien.db")
|
|
31
|
+
return f"sqlite:///{path}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DATABASE_URL = _get_database_url()
|
|
35
|
+
|
|
36
|
+
if DATABASE_URL.startswith("sqlite"):
|
|
37
|
+
# SQLite is accessed across FastAPI's threadpool / the streaming generator's
|
|
38
|
+
# own session, so disable the same-thread check. No pool recycling needed.
|
|
39
|
+
engine = create_engine(
|
|
40
|
+
DATABASE_URL,
|
|
41
|
+
connect_args={"check_same_thread": False},
|
|
42
|
+
future=True,
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
engine = create_engine(
|
|
46
|
+
DATABASE_URL,
|
|
47
|
+
pool_pre_ping=True,
|
|
48
|
+
pool_recycle=3600,
|
|
49
|
+
future=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
SessionLocal = scoped_session(
|
|
53
|
+
sessionmaker(bind=engine, autocommit=False, autoflush=False)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_db():
|
|
58
|
+
"""FastAPI dependency - yields a DB session."""
|
|
59
|
+
db = SessionLocal()
|
|
60
|
+
try:
|
|
61
|
+
yield db
|
|
62
|
+
finally:
|
|
63
|
+
db.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def ensure_app_settings_columns() -> None:
|
|
67
|
+
"""Additively reconcile `app_settings` columns on a pre-existing DB.
|
|
68
|
+
|
|
69
|
+
There is no migration framework - `Base.metadata.create_all` builds missing *tables* but
|
|
70
|
+
never adds missing *columns* to a table that already exists. So a returning user's
|
|
71
|
+
`app_settings` won't gain `llm_provider` on its own. Add it with an idempotent, guarded
|
|
72
|
+
`ALTER TABLE ... ADD COLUMN` (SQLite backfills existing rows from the literal DEFAULT). A
|
|
73
|
+
brand-new DB already has the column via the model, so this is a no-op there; a legacy DB
|
|
74
|
+
that already carries the column is also a clean no-op.
|
|
75
|
+
"""
|
|
76
|
+
inspector = inspect(engine)
|
|
77
|
+
if "app_settings" not in inspector.get_table_names():
|
|
78
|
+
return # fresh DB - create_all builds it with the column already present
|
|
79
|
+
|
|
80
|
+
existing = {c["name"] for c in inspector.get_columns("app_settings")}
|
|
81
|
+
additive = {
|
|
82
|
+
"llm_provider": "ALTER TABLE app_settings ADD COLUMN llm_provider VARCHAR(20) NOT NULL DEFAULT 'local'",
|
|
83
|
+
"openai_api_key": "ALTER TABLE app_settings ADD COLUMN openai_api_key VARCHAR(200)",
|
|
84
|
+
"zotero_data_dir": "ALTER TABLE app_settings ADD COLUMN zotero_data_dir VARCHAR(500)",
|
|
85
|
+
}
|
|
86
|
+
for column, ddl in additive.items():
|
|
87
|
+
if column in existing:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
with engine.begin() as conn:
|
|
91
|
+
conn.execute(text(ddl))
|
|
92
|
+
logger.info("app_settings: added missing column %s", column)
|
|
93
|
+
except Exception:
|
|
94
|
+
# A concurrent boot or a legacy duplicate column must never crash startup.
|
|
95
|
+
logger.warning("app_settings: could not add column %s (may already exist)", column, exc_info=True)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Durable data-dir resolution - one base dir for everything InScien persists.
|
|
2
|
+
|
|
3
|
+
SQLite, the OpenAlex cache, job records, narration audio, the Zotero snapshot, and the Kokoro
|
|
4
|
+
weights all live under one base directory. For host dev that's the repo-root `data/` folder
|
|
5
|
+
(default below); in the **desktop app** the Tauri shell sets `INSCIEN_DATA_DIR` to the OS per-user
|
|
6
|
+
app-data dir, so a single env var redirects them all. Each individual path still honours its own
|
|
7
|
+
explicit override (e.g. `INSCIEN_VECTORS_PATH`) for back-compat.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Default base = the repo-root `data/` dir (this file is backend/core/paths.py, so parents[2] is
|
|
14
|
+
# the repo root, regardless of cwd). The desktop build overrides this via INSCIEN_DATA_DIR.
|
|
15
|
+
_DEFAULT_DATA_DIR = str(Path(__file__).resolve().parents[2] / "data")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def data_dir() -> str:
|
|
19
|
+
"""The base directory for all durable state (`INSCIEN_DATA_DIR`, else the ./data mount)."""
|
|
20
|
+
return (os.getenv("INSCIEN_DATA_DIR") or "").strip() or _DEFAULT_DATA_DIR
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def data_path(*parts: str) -> str:
|
|
24
|
+
"""A path under the data dir, e.g. `data_path("pdf-index.json")` -> `<data_dir>/pdf-index.json`."""
|
|
25
|
+
return str(Path(data_dir(), *parts))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""pip / uvx entry point - run InScien as a local web app in the user's own browser.
|
|
2
|
+
|
|
3
|
+
This is the non-desktop launch path: instead of the Tauri window (whose bundled WebKit is fragile
|
|
4
|
+
on new GPUs), the backend serves BOTH the API and the static UI on one loopback port, and we open
|
|
5
|
+
the system browser at it. Same one-process shape as the frozen desktop build (see run_server.py),
|
|
6
|
+
minus the native window.
|
|
7
|
+
|
|
8
|
+
Installed as the `inscien` console script (see pyproject.toml). Everything is self-contained:
|
|
9
|
+
Kokoro bundles espeak via espeakng-loader, imageio-ffmpeg bundles ffmpeg - no system packages.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import socket
|
|
14
|
+
import threading
|
|
15
|
+
import webbrowser
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _pick_port(preferred: int = 8000) -> int:
|
|
20
|
+
"""Use the preferred port if free, else let the OS hand out any open one."""
|
|
21
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
22
|
+
try:
|
|
23
|
+
s.bind(("127.0.0.1", preferred))
|
|
24
|
+
return preferred
|
|
25
|
+
except OSError:
|
|
26
|
+
s.bind(("127.0.0.1", 0))
|
|
27
|
+
return s.getsockname()[1]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
here = Path(__file__).resolve().parent
|
|
32
|
+
|
|
33
|
+
# Production mode: relative API base (no CORS), and serve the vendored static UI.
|
|
34
|
+
os.environ.setdefault("ENV_NAME", "production")
|
|
35
|
+
os.environ.setdefault("FRONTEND_DIST", str(here / "webui"))
|
|
36
|
+
|
|
37
|
+
# Durable state goes in the OS per-user app-data dir, not the install location.
|
|
38
|
+
if not (os.getenv("INSCIEN_DATA_DIR") or "").strip():
|
|
39
|
+
from platformdirs import user_data_dir
|
|
40
|
+
|
|
41
|
+
os.environ["INSCIEN_DATA_DIR"] = user_data_dir("inscien")
|
|
42
|
+
os.makedirs(os.environ["INSCIEN_DATA_DIR"], exist_ok=True)
|
|
43
|
+
|
|
44
|
+
port = int((os.getenv("PORT") or "").strip() or _pick_port())
|
|
45
|
+
url = f"http://127.0.0.1:{port}"
|
|
46
|
+
|
|
47
|
+
import uvicorn
|
|
48
|
+
|
|
49
|
+
from main import app # main.py builds the app on import (routers + static mount)
|
|
50
|
+
|
|
51
|
+
# Open the browser shortly after uvicorn starts serving.
|
|
52
|
+
threading.Timer(1.2, lambda: webbrowser.open(url)).start()
|
|
53
|
+
print(f"InScien running at {url} (press Ctrl+C to stop)")
|
|
54
|
+
uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
inscien-0.2.0/main.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
# In a frozen desktop build all config comes from the parent (Tauri) environment - don't pick up a
|
|
6
|
+
# stray `.env` from the working directory.
|
|
7
|
+
if not getattr(sys, "frozen", False):
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
# Ensure app logs (tracebacks from services) reach the console. basicConfig is a no-op
|
|
13
|
+
# if the root logger already has handlers, so it won't fight uvicorn/gunicorn config.
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from contextlib import asynccontextmanager
|
|
20
|
+
|
|
21
|
+
from fastapi import FastAPI
|
|
22
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
23
|
+
|
|
24
|
+
from core.db import engine, Base, ensure_app_settings_columns
|
|
25
|
+
from routers.settings import router as settings_router
|
|
26
|
+
from routers.papers import router as papers_router
|
|
27
|
+
from routers.graph import router as graph_router
|
|
28
|
+
from routers.narrate import router as narrate_router
|
|
29
|
+
from routers.zotero import router as zotero_router
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
ENV_NAME = os.getenv("ENV_NAME", "development")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@asynccontextmanager
|
|
36
|
+
async def lifespan(app: FastAPI):
|
|
37
|
+
Base.metadata.create_all(bind=engine)
|
|
38
|
+
# No migration framework: additively add columns that create_all can't add to an
|
|
39
|
+
# already-existing table (e.g. llm_provider on a returning user's app_settings).
|
|
40
|
+
ensure_app_settings_columns()
|
|
41
|
+
# In-process jobs don't survive a restart - fail any that were mid-run.
|
|
42
|
+
from services.narration.jobs import recover_stale as recover_narration
|
|
43
|
+
from services.narration.model import recover_stale as recover_narrate_model
|
|
44
|
+
from services.refs.fetch_jobs import recover_stale as recover_graph_fetch
|
|
45
|
+
recover_narration()
|
|
46
|
+
recover_narrate_model()
|
|
47
|
+
recover_graph_fetch()
|
|
48
|
+
yield
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if ENV_NAME == "production":
|
|
52
|
+
app = FastAPI(
|
|
53
|
+
title="InScien Backend API",
|
|
54
|
+
version="0.1.6",
|
|
55
|
+
docs_url=None,
|
|
56
|
+
redoc_url=None,
|
|
57
|
+
openapi_url=None,
|
|
58
|
+
lifespan=lifespan,
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
app = FastAPI(
|
|
62
|
+
title="InScien Backend API",
|
|
63
|
+
version="0.1.6",
|
|
64
|
+
lifespan=lifespan,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Dev runs the Next dev server and the API on separate localhost origins, so CORS is needed.
|
|
68
|
+
# Default to localhost (zero-config host dev); `CORS_ORIGINS` overrides. Prod/desktop serve the
|
|
69
|
+
# UI same-origin, so the list is never consulted there - and localhost-only is safe to ship.
|
|
70
|
+
_DEV_ORIGINS = ["http://localhost:3000", "http://127.0.0.1:3000"]
|
|
71
|
+
_configured = [o.strip() for o in (os.getenv("CORS_ORIGINS") or "").split(",") if o.strip()]
|
|
72
|
+
ALLOWED_ORIGINS = _configured or _DEV_ORIGINS
|
|
73
|
+
|
|
74
|
+
app.add_middleware(
|
|
75
|
+
CORSMiddleware,
|
|
76
|
+
allow_origins=ALLOWED_ORIGINS,
|
|
77
|
+
allow_credentials=True,
|
|
78
|
+
allow_methods=["*"],
|
|
79
|
+
allow_headers=["*"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
app.include_router(settings_router)
|
|
83
|
+
app.include_router(papers_router)
|
|
84
|
+
app.include_router(graph_router)
|
|
85
|
+
app.include_router(narrate_router)
|
|
86
|
+
app.include_router(zotero_router)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.get("/health")
|
|
90
|
+
async def health():
|
|
91
|
+
# Liveness only - fast and dependency-free, so the compose healthcheck reflects "the API
|
|
92
|
+
# process is serving" and never flaps on a host Ollama being down. See /health/ready.
|
|
93
|
+
return {"status": "ok"}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.get("/health/ready")
|
|
97
|
+
def health_ready():
|
|
98
|
+
"""Readiness/diagnostics: probe each dependency without ever failing the request. Ollama
|
|
99
|
+
is informational (it lives on the host and may legitimately be down); readiness needs only
|
|
100
|
+
the API's own store (SQLite)."""
|
|
101
|
+
from sqlalchemy import text
|
|
102
|
+
from core.db import SessionLocal
|
|
103
|
+
from services.llm.client import list_ollama_models_status
|
|
104
|
+
|
|
105
|
+
db_ok = False
|
|
106
|
+
try:
|
|
107
|
+
session = SessionLocal()
|
|
108
|
+
try:
|
|
109
|
+
session.execute(text("SELECT 1"))
|
|
110
|
+
db_ok = True
|
|
111
|
+
finally:
|
|
112
|
+
session.close()
|
|
113
|
+
except Exception:
|
|
114
|
+
logging.getLogger("health").exception("readiness: DB probe failed")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
ollama_ok = bool(list_ollama_models_status().get("reachable"))
|
|
118
|
+
except Exception:
|
|
119
|
+
ollama_ok = False
|
|
120
|
+
|
|
121
|
+
return {"db": db_ok, "ollama": ollama_ok, "ready": db_ok}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Serve the built frontend (Next static export) when present. The production image bakes the
|
|
125
|
+
# export into FRONTEND_DIST and serves it here, so the UI and API share one origin - no CORS,
|
|
126
|
+
# no separate Next server. Mounted LAST so the /api routers and /health above take precedence;
|
|
127
|
+
# html=True resolves /map -> /map/index.html. In development FRONTEND_DIST is unset (the Next
|
|
128
|
+
# dev server serves the UI on its own port), so this is a no-op.
|
|
129
|
+
FRONTEND_DIST = os.getenv("FRONTEND_DIST")
|
|
130
|
+
if FRONTEND_DIST and os.path.isdir(FRONTEND_DIST):
|
|
131
|
+
import mimetypes
|
|
132
|
+
from fastapi.staticfiles import StaticFiles
|
|
133
|
+
|
|
134
|
+
# The pdf.js worker is an ES module (.mjs); browsers refuse to load a module worker
|
|
135
|
+
# unless it's served with a JavaScript MIME type, and Python's mimetypes doesn't always
|
|
136
|
+
# register .mjs. Set it before mounting so StaticFiles guesses the right type.
|
|
137
|
+
mimetypes.add_type("text/javascript", ".mjs")
|
|
138
|
+
|
|
139
|
+
app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="frontend")
|