File size: 5,141 Bytes
b92b744
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53c0860
b92b744
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
befe23e
 
 
 
 
 
b92b744
 
 
 
 
 
0cb871f
 
 
 
 
 
 
 
 
 
 
 
 
 
befe23e
 
 
 
 
 
0cb871f
 
285b041
 
 
0cb871f
 
 
 
 
285b041
b92b744
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from pathlib import Path

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=Path(__file__).resolve().parents[1] / ".env", extra="ignore"
    )

    elastic_url: str = "http://localhost:9200"
    elastic_api_key: str = ""
    elastic_index: str = "studies"
    elastic_local: bool = False
    openai_api_key: str = ""
    openalex_snapshot_manifest: str = (
        "https://openalex.s3.amazonaws.com/data/parquet/works/manifest.json"
    )
    hf_token: str = ""
    embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2"
    embedding_dimensions: int = 384
    embedding_device: str = "cpu"
    embeddings_enabled: bool = True
    reference_min_similarity: float = Field(default=0.55, ge=0, le=1)
    classifier_model_path: str = ""
    small_model: str = "gpt-4.1-mini"
    narration_model: str = "gpt-4.1"
    extraction_limit: int = Field(default=100, ge=0, le=200)
    llm_concurrency: int = Field(default=5, ge=1, le=20)
    # USD per million tokens: configurable assumptions, never claimed as a bill.
    small_input_price: float = 0.4
    small_cached_price: float = 0.1
    small_output_price: float = 1.6
    narration_input_price: float = 2.0
    narration_cached_price: float = 0.5
    narration_output_price: float = 8.0
    ttc_api_key: str = ""
    ttc_model: str = "bear-2"
    compression_enabled: bool = False
    compression_validated: bool = False
    ttc_input_price: float = 0.0
    request_timeout: float = 45.0
    extraction_cache_version: str = "v4-result-direction"
    # Query-time full text from Europe PMC for papers with a PMCID; abstract otherwise.
    fulltext_enabled: bool = True
    europepmc_url: str = "https://www.ebi.ac.uk/europepmc/webservices/rest"
    fulltext_timeout: float = 20.0
    fulltext_max_lines: int = Field(default=160, ge=20, le=600)
    # Gap map: the studies nearest the question, fetched by kNN and clustered per
    # question. A few hundred nodes is what a reader can take in and what a force
    # layout can settle in a second; the full-index scan below only runs when
    # there is no question to center on.
    gapmap_neighborhood: int = Field(default=800, ge=50, le=5000)
    # Whole-index build (no question): every embedded study, scanned in pages.
    # The region count is a resolution, and what counts as an empty band depends on
    # it: measured on 6000 documents of the live index, 24 regions leave the corpus
    # continuous (no band under the occupancy threshold) while 80 keep a median of
    # 64 primary attempts per region and still expose bands.
    gapmap_regions: int = Field(default=80, ge=2, le=200)
    gapmap_seed: int = 0
    # A cap on how much of the embedded corpus one build reads; 0 is no cap, and
    # the coverage line and the warning always report what was actually read.
    # Measured on the live index (2,078,515 embedded studies, 384 dimensions):
    # the scan runs at ~11k documents/s and the vectors weigh ~3.0 GiB, so an
    # uncapped build is ~3 minutes and needs a host with several GB to spare.
    gapmap_scan_limit: int = Field(default=0, ge=0)
    # Page size and concurrent point-in-time slices for that scan. Sixteen slices
    # measured ~11k docs/s against ~3.2k for a single unsliced walk.
    gapmap_batch: int = Field(default=2000, ge=100, le=10000)
    gapmap_slices: int = Field(default=16, ge=1, le=64)
    # How many of the clustered studies the canvas draws. Every study shapes the
    # regions; a browser cannot paint two million marks, so the drawn subset is
    # thinned by a hash of the document id.
    gapmap_points: int = Field(default=6000, ge=100, le=50000)
    # Edges the canvas draws between drawn studies: each study joins its most
    # similar few, and only when the cosine reaches the floor. The cap is what
    # keeps a dense literature from becoming one hub; the floor is what leaves an
    # isolated paper isolated instead of tethered to its nearest stranger.
    gapmap_edge_neighbors: int = Field(default=3, ge=1, le=12)
    gapmap_edge_floor: float = Field(default=0.8, ge=-1.0, le=1.0)
    # Rows the 2-D projection is fitted on. The basis is a drawing choice, and a
    # full-corpus SVD would stall the build for minutes to move points by pixels.
    # Nothing can be drawn before it exists, so it is fitted on the first pages
    # rather than after a long silent read.
    gapmap_projection_sample: int = Field(default=1000, ge=1000, le=500000)
    # Bound on the full-corpus k-means. Measured at 200k documents and 80
    # regions: 10 iterations ~3.2 s, 40 ~7.7 s; the assignment usually settles
    # first and the loop stops when it does.
    gapmap_iterations: int = Field(default=25, ge=1, le=200)
    # Minimum seconds between two streamed frames while the corpus is scanning.
    gapmap_tick_seconds: float = Field(default=0.15, ge=0.0, le=10.0)
    max_concurrent_searches: int = 4
    frontend_dist: str = str(Path(__file__).resolve().parents[2] / "dist")
    cors_origins: list[str] = ["http://localhost:5173"]


settings = Settings()