Spaces:
Runtime error
Runtime error
Refactor VideoTemplateEditor component: remove AI editing features and related state management, streamline template loading, and update styles for improved layout. Enhance CSS for responsive design and visual consistency across various components, including dropdowns, sync stats, and quiz elements. Remove deprecated premium badge styles and adjust layout for chat components.
66de9c8 Download backend/app/config.py from firebird-technologies/NoteStack-Test: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/backend/app/config.py
- Command line
-
hf download hf://spaces/firebird-technologies/NoteStack-Test/backend/app/config.py
-
curl -L -o config.py https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/backend/app/config.py
11.4 kB
| from functools import lru_cache | |
| from pathlib import Path | |
| from pydantic import AliasChoices, Field, field_validator | |
| from pydantic_settings import BaseSettings, SettingsConfigDict | |
| # One .env at the repo root serves every way of running the API; a backend/.env (or the | |
| # working directory's .env) overrides it. Real environment variables beat both. | |
| _ROOT_ENV = Path(__file__).resolve().parents[2] / ".env" | |
| class Settings(BaseSettings): | |
| model_config = SettingsConfigDict(env_file=(_ROOT_ENV, ".env"), extra="ignore") | |
| env: str = "development" | |
| app_name: str = "Notestack" | |
| frontend_url: str = "http://localhost:5173" | |
| api_url: str = "http://localhost:8000" | |
| cors_origins: str = "http://localhost:5173" | |
| # Local default is a SQLite file (relative to the working directory, normally backend/). | |
| # Prod and docker-compose use Postgres: postgresql+psycopg://user:pass@host:5432/db | |
| # (plain postgres:// and postgresql:// URLs are rewritten to use psycopg, see below) | |
| database_url: str = "sqlite:///./notestack.db" | |
| # Worker (Postgres backed queue, see app/worker.py) | |
| worker_concurrency: int = 4 | |
| worker_poll_seconds: float = 1.0 | |
| job_stale_seconds: int = 900 # a running job with no heartbeat for this long is retried | |
| # Run the worker loop inside the API process (local dev: one command, no second terminal). | |
| # None = on in development, off elsewhere. | |
| run_worker_in_api: bool | None = None | |
| # Auth | |
| jwt_secret: str = "change-me" | |
| jwt_algorithm: str = "HS256" | |
| jwt_expiration_hours: int = 72 | |
| jwt_refresh_expiration_days: int = 30 | |
| google_client_id: str = "" | |
| # Setting the secret switches Google sign in from the GIS popup to the redirect flow | |
| # (server side code exchange). The redirect URI must be listed on the OAuth client. | |
| google_client_secret: str = "" | |
| google_redirect_uri: str = "" # defaults to {api_url}/api/auth/google/callback | |
| # Email (Resend) | |
| email_provider: str = "console" # resend | console | |
| resend_api_key: str = "" | |
| from_email: str = "Notestack <hello@notestack.ai>" | |
| alerts_email: str = "arslan@firebird-technologies.com" | |
| unsubscribe_secret: str = "change-me-too" | |
| update_email_send_hour: int = 9 | |
| # Storage: auto = local disk when no R2 account or endpoint is set, else R2 (S3 compatible) | |
| storage_backend: str = "auto" # auto | r2 | local | |
| local_storage_dir: str = ".storage" | |
| r2_account_id: str = "" | |
| r2_access_key_id: str = "" | |
| r2_secret_access_key: str = "" | |
| r2_bucket: str = "notestack-assets" | |
| r2_endpoint_url: str = "" # override for MinIO locally; defaults to the R2 account endpoint | |
| r2_public_base_url: str = "" # optional custom domain for public objects | |
| # Folder inside the bucket, so Notestack can share a bucket with other apps. Empty = bucket root. | |
| r2_prefix: str = "" | |
| r2_presign_ttl_seconds: int = 3600 | |
| max_upload_bytes: int = 300 * 1024 * 1024 | |
| # Posts an infographic reads, and a report that is not built from themes (REPORT_PLANNER below). A quiz or flashcard set reads up to that many straight from the posts; with more (no limit | |
| # on how many are picked) it reads a small sample of the ideas already extracted from them, so its size does not grow. | |
| artifact_max_posts: int = 20 | |
| # Characters of post text a report, quiz or flashcard set may read in all (split evenly between the posts, 1,500 at least each). | |
| artifact_passage_budget: int = 120_000 | |
| # The same for a quiz: it needs less text than a report to ask good questions. | |
| artifact_quiz_passage_budget: int = 60_000 | |
| # Ideas (already extracted from each post) in the evidence of a flashcard set, or of a quiz above ARTIFACT_MAX_POSTS posts. | |
| artifact_idea_sample: int = 30 | |
| # Reports. "themes": the posts' stored ideas are grouped into weighted themes, a blueprint plans the report, and each section is | |
| # written from its own evidence (pipeline/report_themes.py). "legacy": the posts' text goes straight to the writer, at most | |
| # ARTIFACT_MAX_POSTS of them. Chats, small selections and posts without stored ideas always use the legacy way. | |
| report_planner: str = "themes" | |
| # Posts a themed report reads: 0 = every post picked (the plan's indexed_posts still applies). | |
| report_max_posts: int = 0 | |
| # Posts whose ideas are extracted while the report is made (in parallel); the rest are represented by their opening. | |
| report_inline_ideas: int = 25 | |
| # LLM (LiteLLM model strings; Z.ai GLM by default) | |
| llm_model: str = "openai/glm-5.3" | |
| llm_fast_model: str = "openai/glm-5.3-flash" | |
| llm_report_model: str = "" # optional: writes report prose (default: the fast model, with a bigger output cap) | |
| # Chat triage (small talk, off topic, follow up rewriting). Empty = LLM_FAST_MODEL. | |
| llm_triage_model: str = "" | |
| # TypeSafe Jev classifier for chat triage. When set, triage uses Jev instead of an LLM call. | |
| typesafe_api_key: str = "" | |
| typesafe_model: str = "jev-latest" | |
| typesafe_url: str = "https://api.typesafe.ai" | |
| # Firecrawl: crawls sites without an RSS feed and renders JavaScript pages. Empty = plain HTTP only. | |
| firecrawl_api_key: str = "" | |
| firecrawl_url: str = "https://api.firecrawl.dev" | |
| # Help bot (bottom right widget). Empty = LLM_FAST_MODEL. | |
| support_llm_model: str = "" | |
| support_enabled: bool = True | |
| support_messages_per_minute: int = 20 | |
| llm_api_base: str = "https://api.z.ai/api/paas/v4" | |
| llm_api_key: str = "" | |
| llm_temperature: float = 1.0 # Z.ai recommends 1.0 for GLM-5.x | |
| # Reasoning output counts against max_tokens, so leave headroom for the structured answer. | |
| llm_max_tokens: int = 10000 | |
| # The output cap of the fast model: quizzes, flashcards, infographics, idea extraction, template suggestions. A cap, not a target: | |
| # only the tokens written are charged, and the thinking counts too, so keep room for it. Measured answers are under 3,000. | |
| llm_fast_max_tokens: int = 6000 | |
| # GLM-5.x always reasons (it cannot be disabled); this sets how hard. low | high | max | |
| llm_reasoning_effort: str = "low" | |
| # Corpus: posts as markdown files. R2 is the source of truth; each process keeps a disk cache. | |
| corpus_cache_dir: str = ".corpus-cache" | |
| research_max_steps: int = 10 | |
| # After an answer, how long the chat stream waits for the memory update so it can say what was saved. | |
| memory_notice_wait_seconds: float = 25.0 | |
| # Chat memory: topic files built from a notebook's chats (docs/CHAT_RECALL_IMPLEMENTATION.md). | |
| # CHAT_MEMORY_ENABLED writes the files after each answer. CHAT_MEMORY_READ lets answers use them. | |
| chat_memory_enabled: bool = True | |
| chat_memory_read: bool = False | |
| chat_memory_delay_seconds: float = 10.0 | |
| chat_memory_max_rounds_per_job: int = 12 | |
| chat_memory_summary_chars: int = 1400 | |
| chat_memory_recent_rounds: int = 4 | |
| chat_memory_context_chars: int = 3000 | |
| chat_memory_topics_shown: int = 12 | |
| # Log what the research agent is given on every chat message (conversation, writer notes, chat memory). The logs | |
| # then hold writers' text, so it is on only in development unless CHAT_CONTEXT_LOG=true. None follows ENV. | |
| chat_context_log: bool | None = None | |
| # ElevenLabs (premade voices by default; a consented clone can replace host A) | |
| elevenlabs_api_key: str = "" | |
| elevenlabs_model: str = "eleven_multilingual_v2" | |
| elevenlabs_voice_a: str = "EXAVITQu4vr4xnSDxMaL" # Sarah (premade) | |
| elevenlabs_voice_b: str = "JBFqnCBsd6RMkjVDRZzb" # George (premade) | |
| # Social publishing (Launchpad). Redirect URIs: {API_URL}/api/social/{x,linkedin}/callback | |
| social_token_key: str = "" # Fernet key; derived from JWT_SECRET when empty | |
| x_enabled: bool = False # X posting is switched off for now (LinkedIn only); set X_ENABLED=true to bring it back | |
| x_client_id: str = "" | |
| x_client_secret: str = "" | |
| linkedin_client_id: str = "" | |
| linkedin_client_secret: str = "" | |
| # blog2video: makes the videos. We call its public API with our own account's key (b2v_live_...), so every | |
| # video is charged to that account. Backend only: the key must never reach the browser or the logs. | |
| b2v_api_base_url: str = Field("", validation_alias=AliasChoices("B2V_API_BASE_URL", "B2V_API_URL")) | |
| b2v_api_key: str = "" | |
| # blog2video's web app: serves the template poster images (<app>/template-posters/<id>.webp). | |
| b2v_app_url: str = "https://blog2video.app" | |
| b2v_timeout_seconds: float = 30 # creates, uploads and full-project reads; other calls also wait 30s (STATUS_TIMEOUT) | |
| b2v_capacity_alert_below: int = 20 # log an error when our blog2video account has fewer videos left | |
| # Renderer | |
| renderer_url: str = "http://localhost:3100" | |
| internal_token: str = "internal-change-me" | |
| # Billing (Stripe). With BILLING_ENABLED=false everyone gets Studio limits and checkout is off. | |
| billing_enabled: bool = False | |
| stripe_secret_key: str = Field( | |
| "", validation_alias=AliasChoices("STRIPE_SECRET_KEY", "STRIPE_API_KEY", "STRIPE_SECRET") | |
| ) | |
| stripe_webhook_secret: str = Field( | |
| "", | |
| validation_alias=AliasChoices( | |
| "STRIPE_WEBHOOK_SECRET", "STRIPE_WEBHOOK_SIGNING_SECRET", "STRIPE_SIGNING_SECRET" | |
| ), | |
| ) | |
| # Optional existing Price ids. Empty = Notestack finds or creates prices by lookup key | |
| # (notestack_<plan>_<cycle>) from the plan table in services/plans.py. | |
| stripe_price_writer_monthly: str = "" | |
| stripe_price_writer_annual: str = "" | |
| stripe_price_studio_monthly: str = "" | |
| stripe_price_studio_annual: str = "" | |
| def _blank_is_default(cls, value): | |
| """RUN_WORKER_IN_API= (blank, as in .env.example) means "decide from ENV", not a parse error.""" | |
| return None if isinstance(value, str) and not value.strip() else value | |
| def _use_psycopg_driver(cls, value): | |
| """Accept plain postgres:// or postgresql:// URLs (as Neon, Heroku etc. hand out) and | |
| point them at psycopg 3, the only Postgres driver installed. Query params pass through.""" | |
| if isinstance(value, str): | |
| value = value.strip() | |
| for prefix in ("postgres://", "postgresql://"): | |
| if value.startswith(prefix): | |
| return "postgresql+psycopg://" + value[len(prefix):] | |
| return value | |
| def worker_in_api(self) -> bool: | |
| return self.env == "development" if self.run_worker_in_api is None else self.run_worker_in_api | |
| def use_local_storage(self) -> bool: | |
| if self.storage_backend != "auto": | |
| return self.storage_backend == "local" | |
| return not (self.r2_account_id or self.r2_endpoint_url) | |
| def r2_endpoint(self) -> str: | |
| if self.r2_endpoint_url: | |
| return self.r2_endpoint_url | |
| return f"https://{self.r2_account_id}.r2.cloudflarestorage.com" | |
| def google_redirect_url(self) -> str: | |
| return self.google_redirect_uri or f"{self.api_url.rstrip('/')}/api/auth/google/callback" | |
| def cors_origin_list(self) -> list[str]: | |
| return [o.strip() for o in self.cors_origins.split(",") if o.strip()] | |
| def get_settings() -> Settings: | |
| return Settings() | |
| settings = get_settings() | |