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.
Files changed (121) hide show
  1. inscien-0.2.0/.gitignore +312 -0
  2. inscien-0.2.0/PKG-INFO +65 -0
  3. inscien-0.2.0/core/db.py +95 -0
  4. inscien-0.2.0/core/paths.py +25 -0
  5. inscien-0.2.0/launcher.py +58 -0
  6. inscien-0.2.0/main.py +139 -0
  7. inscien-0.2.0/models/app_settings.py +32 -0
  8. inscien-0.2.0/pyproject.toml +92 -0
  9. inscien-0.2.0/repositories/settings_repository.py +33 -0
  10. inscien-0.2.0/requirements.txt +69 -0
  11. inscien-0.2.0/routers/graph.py +107 -0
  12. inscien-0.2.0/routers/narrate.py +151 -0
  13. inscien-0.2.0/routers/papers.py +61 -0
  14. inscien-0.2.0/routers/settings.py +98 -0
  15. inscien-0.2.0/routers/zotero.py +123 -0
  16. inscien-0.2.0/run_server.py +29 -0
  17. inscien-0.2.0/schemas/settings.py +23 -0
  18. inscien-0.2.0/scripts/check_refs.py +78 -0
  19. inscien-0.2.0/services/job_runner.py +192 -0
  20. inscien-0.2.0/services/lab/pdf_parser.py +69 -0
  21. inscien-0.2.0/services/llm/client.py +287 -0
  22. inscien-0.2.0/services/narration/__init__.py +0 -0
  23. inscien-0.2.0/services/narration/jobs.py +94 -0
  24. inscien-0.2.0/services/narration/model.py +94 -0
  25. inscien-0.2.0/services/narration/pipeline.py +190 -0
  26. inscien-0.2.0/services/narration/tts_engine.py +213 -0
  27. inscien-0.2.0/services/refs/fetch_jobs.py +59 -0
  28. inscien-0.2.0/services/refs/openalex.py +190 -0
  29. inscien-0.2.0/services/refs/refstore.py +364 -0
  30. inscien-0.2.0/services/state_guard.py +51 -0
  31. inscien-0.2.0/services/zotero/__init__.py +0 -0
  32. inscien-0.2.0/services/zotero/reader.py +513 -0
  33. inscien-0.2.0/services/zotero/settings.py +52 -0
  34. inscien-0.2.0/webui/404/index.html +1 -0
  35. inscien-0.2.0/webui/404.html +1 -0
  36. inscien-0.2.0/webui/__next.__PAGE__.txt +9 -0
  37. inscien-0.2.0/webui/__next._full.txt +22 -0
  38. inscien-0.2.0/webui/__next._head.txt +6 -0
  39. inscien-0.2.0/webui/__next._index.txt +7 -0
  40. inscien-0.2.0/webui/__next._tree.txt +4 -0
  41. inscien-0.2.0/webui/_next/static/TSQ7-kGL6XBkP1azJpkxU/_buildManifest.js +11 -0
  42. inscien-0.2.0/webui/_next/static/TSQ7-kGL6XBkP1azJpkxU/_clientMiddlewareManifest.json +1 -0
  43. inscien-0.2.0/webui/_next/static/TSQ7-kGL6XBkP1azJpkxU/_ssgManifest.js +1 -0
  44. inscien-0.2.0/webui/_next/static/chunks/0f6b8b320c4d90c3.js +1 -0
  45. inscien-0.2.0/webui/_next/static/chunks/191ccb8e48d87c18.js +1 -0
  46. inscien-0.2.0/webui/_next/static/chunks/3555439a4ec6d383.js +2 -0
  47. inscien-0.2.0/webui/_next/static/chunks/35c307818fb96058.js +1 -0
  48. inscien-0.2.0/webui/_next/static/chunks/38a348bf88033a6c.js +1 -0
  49. inscien-0.2.0/webui/_next/static/chunks/40b2c258e0c3f3b2.js +1 -0
  50. inscien-0.2.0/webui/_next/static/chunks/4d708587bb9ed414.js +1 -0
  51. inscien-0.2.0/webui/_next/static/chunks/632079a7dfdf58c3.js +2 -0
  52. inscien-0.2.0/webui/_next/static/chunks/79764a2363f49b32.js +1 -0
  53. inscien-0.2.0/webui/_next/static/chunks/7d626b9c3eda6e32.js +1 -0
  54. inscien-0.2.0/webui/_next/static/chunks/7e2eb80b6e54e441.js +1 -0
  55. inscien-0.2.0/webui/_next/static/chunks/7ebc8031df18b89e.js +1 -0
  56. inscien-0.2.0/webui/_next/static/chunks/8a460e86ba76adb4.js +1 -0
  57. inscien-0.2.0/webui/_next/static/chunks/99bd76c5a9b85646.js +1 -0
  58. inscien-0.2.0/webui/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  59. inscien-0.2.0/webui/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  60. inscien-0.2.0/webui/_next/static/chunks/b533e0edc315d683.css +3 -0
  61. inscien-0.2.0/webui/_next/static/chunks/b899275a30baa369.js +1 -0
  62. inscien-0.2.0/webui/_next/static/chunks/cbd63a10c5edde05.css +3 -0
  63. inscien-0.2.0/webui/_next/static/chunks/d2be314c3ece3fbe.js +1 -0
  64. inscien-0.2.0/webui/_next/static/chunks/d4357b8e9f3078d8.js +1 -0
  65. inscien-0.2.0/webui/_next/static/chunks/d573c5342989b77f.js +1 -0
  66. inscien-0.2.0/webui/_next/static/chunks/d59f830a2b8e768c.js +1 -0
  67. inscien-0.2.0/webui/_next/static/chunks/f091501564eb2ea3.js +4 -0
  68. inscien-0.2.0/webui/_next/static/chunks/ff1a16fafef87110.js +1 -0
  69. inscien-0.2.0/webui/_next/static/chunks/turbopack-b7fd4216d7d389e6.js +4 -0
  70. inscien-0.2.0/webui/_next/static/media/032b8f4a4d05772e-s.4fc86242.woff2 +0 -0
  71. inscien-0.2.0/webui/_next/static/media/53b9e256198e5412-s.853d50a3.woff2 +0 -0
  72. inscien-0.2.0/webui/_next/static/media/617e1646a751849a-s.6b81bcff.woff2 +0 -0
  73. inscien-0.2.0/webui/_next/static/media/7178b3e590c64307-s.55554cd0.woff2 +0 -0
  74. inscien-0.2.0/webui/_next/static/media/7c7fbf33ae4e53ea-s.8f558b88.woff2 +0 -0
  75. inscien-0.2.0/webui/_next/static/media/8a480f0b521d4e75-s.ea323500.woff2 +0 -0
  76. inscien-0.2.0/webui/_next/static/media/a0f3c1c4f2337ba2-s.11606649.woff2 +0 -0
  77. inscien-0.2.0/webui/_next/static/media/a96137a1efe31085-s.p.344af709.woff2 +0 -0
  78. inscien-0.2.0/webui/_next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2 +0 -0
  79. inscien-0.2.0/webui/_next/static/media/d20b999fa2a8347f-s.0e2a9354.woff2 +0 -0
  80. inscien-0.2.0/webui/_next/static/media/f6927c9762797221-s.597c407c.woff2 +0 -0
  81. inscien-0.2.0/webui/_next/static/media/fef07dbb0973bf53-s.518e079e.woff2 +0 -0
  82. inscien-0.2.0/webui/_not-found/__next._full.txt +16 -0
  83. inscien-0.2.0/webui/_not-found/__next._head.txt +6 -0
  84. inscien-0.2.0/webui/_not-found/__next._index.txt +7 -0
  85. inscien-0.2.0/webui/_not-found/__next._not-found.__PAGE__.txt +5 -0
  86. inscien-0.2.0/webui/_not-found/__next._not-found.txt +4 -0
  87. inscien-0.2.0/webui/_not-found/__next._tree.txt +2 -0
  88. inscien-0.2.0/webui/_not-found/index.html +1 -0
  89. inscien-0.2.0/webui/_not-found/index.txt +16 -0
  90. inscien-0.2.0/webui/doc-1.png +0 -0
  91. inscien-0.2.0/webui/doc-2.png +0 -0
  92. inscien-0.2.0/webui/doc-3.png +0 -0
  93. inscien-0.2.0/webui/doc-4.png +0 -0
  94. inscien-0.2.0/webui/docs.png +0 -0
  95. inscien-0.2.0/webui/hero.png +0 -0
  96. inscien-0.2.0/webui/icon.png +0 -0
  97. inscien-0.2.0/webui/icon.svg +1 -0
  98. inscien-0.2.0/webui/index.html +1 -0
  99. inscien-0.2.0/webui/index.txt +22 -0
  100. inscien-0.2.0/webui/map/__next._full.txt +23 -0
  101. inscien-0.2.0/webui/map/__next._head.txt +6 -0
  102. inscien-0.2.0/webui/map/__next._index.txt +7 -0
  103. inscien-0.2.0/webui/map/__next._tree.txt +5 -0
  104. inscien-0.2.0/webui/map/__next.map.__PAGE__.txt +9 -0
  105. inscien-0.2.0/webui/map/__next.map.txt +5 -0
  106. inscien-0.2.0/webui/map/index.html +1 -0
  107. inscien-0.2.0/webui/map/index.txt +23 -0
  108. inscien-0.2.0/webui/notebook-1.png +0 -0
  109. inscien-0.2.0/webui/notebook-2.png +0 -0
  110. inscien-0.2.0/webui/notebook-3.png +0 -0
  111. inscien-0.2.0/webui/notebook-4.png +0 -0
  112. inscien-0.2.0/webui/notebooks.png +0 -0
  113. inscien-0.2.0/webui/pdf.worker.min.mjs +21 -0
  114. inscien-0.2.0/webui/settings/__next._full.txt +22 -0
  115. inscien-0.2.0/webui/settings/__next._head.txt +6 -0
  116. inscien-0.2.0/webui/settings/__next._index.txt +7 -0
  117. inscien-0.2.0/webui/settings/__next._tree.txt +4 -0
  118. inscien-0.2.0/webui/settings/__next.settings.__PAGE__.txt +9 -0
  119. inscien-0.2.0/webui/settings/__next.settings.txt +4 -0
  120. inscien-0.2.0/webui/settings/index.html +1 -0
  121. inscien-0.2.0/webui/settings/index.txt +22 -0
@@ -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
@@ -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")