Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/beka4kaa_timely.json
T

1 line
456 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "beka4kaa/timely", "files": {"backend/config/settings.py": {"53fb265ef5548951b684333377c36779172eff84": {"t": 1785746923.1647239, "n": 22543, "h": "6d4bddcc9f510154", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"pomodoro\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\n# Содержимое доски (ChatSession.canvas) приезжает одним JSON-телом, и\n# иллюстрации лежат в нём как data-URI по сотне-другой килобайт каждая.\n# Django по умолчанию режет тело на 2.5 МБ и отвечает RequestDataTooBig ДО\n# входа во вью — то есть раньше, чем сериализатор успевает применить свой\n# предел в 12 МБ. Из-за этого доска с несколькими иллюстрациями молча\n# переставала сохраняться. Держим оба предела согласованными; последнее слово\n# и понятная ошибка остаются за ai_engine.serializers.validate_canvas.\nDATA_UPLOAD_MAX_MEMORY_SIZE = 16 * 1024 * 1024\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (Seedream) обязаны попадать в `_is_image_only_model`\n# (image_enrichment.py), иначе запрос уйдёт с modalities [\"image\", \"text\"] и\n# провайдер ответит 404. То же касается любой новой модели.\n# Gemini-3-pro-image мультимодальная (отдаёт и картинку, и текст), поэтому её\n# туда добавлять НЕ нужно — проверено живым запросом.\n#\n# Проверять доступность модели нужно ТОЧЕЧНЫМ запросом\n# GET /api/v1/models/<id>/endpoints, а НЕ поиском по листингу /api/v1/models:\n# листинг отдаёт не весь каталог, и по нему легко ошибочно счесть живую модель\n# снятой. bytedance-seed/seedream-4.5 в листинге отсутствует, но её endpoint\n# `bytedance-seed/seedream-4.5-20251203` жив.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# ── Выбор модели пользователем (ai_engine.image_models) ──────────────────────\n# Пользователь переключает модель прямо на доске, поэтому `IMAGE_GEN_MODEL`\n# перестал быть единственным источником истины: он остался дефолтом последней\n# инстанции для вызовов, которые ничего не выбрали явно.\n#\n# ALLOWED — жёсткий allowlist. Model ID приходит с фронтенда, и доверять ему\n# нельзя: без этого списка пользователь смог бы направить наш OpenRouter-ключ\n# в любую модель каталога. Всё, чего нет в `ai_engine.image_models.IMAGE_MODELS`,\n# игнорируется, поэтому опечатка в env не выдаст наружу нерабочий id.\nIMAGE_GEN_ALLOWED_MODELS = os.getenv(\n \"IMAGE_GEN_ALLOWED_MODELS\",\n \"bytedance-seed/seedream-4.5,openai/gpt-5.4-image-2\",\n)\n\n# Дефолт для UI и для запросов без явного выбора. Приоритет выше, чем у\n# IMAGE_GEN_MODEL, но значение обязано входить в allowlist — иначе берётся\n# модель с флагом default из реестра.\nIMAGE_GEN_DEFAULT_MODEL = os.getenv(\n \"IMAGE_GEN_DEFAULT_MODEL\",\n \"bytedance-seed/seedream-4.5\",\n)\n\n# Качество по умолчанию: low / medium / high. Намеренно НЕ \"high\" — оно молча\n# удорожает каждую генерацию. Модели без поддержки качества параметр не\n# получают вовсе.\nIMAGE_GEN_QUALITY = os.getenv(\"IMAGE_GEN_QUALITY\", \"medium\")\n\n# Соотношение сторон генерации. Читалось в image_enrichment через\n# getattr(settings, ...), но в settings отсутствовало — значение жило только\n# во фолбэке и не настраивалось через env. Заведено явно.\nIMAGE_GEN_ASPECT_RATIO = os.getenv(\"IMAGE_GEN_ASPECT_RATIO\", \"16:9\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "c6734400664b56d0b54bd902d640ac5de3b6f4f9": {"t": 1785769056.9540017, "n": 22543, "h": "6d4bddcc9f510154", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"pomodoro\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\n# Содержимое доски (ChatSession.canvas) приезжает одним JSON-телом, и\n# иллюстрации лежат в нём как data-URI по сотне-другой килобайт каждая.\n# Django по умолчанию режет тело на 2.5 МБ и отвечает RequestDataTooBig ДО\n# входа во вью — то есть раньше, чем сериализатор успевает применить свой\n# предел в 12 МБ. Из-за этого доска с несколькими иллюстрациями молча\n# переставала сохраняться. Держим оба предела согласованными; последнее слово\n# и понятная ошибка остаются за ai_engine.serializers.validate_canvas.\nDATA_UPLOAD_MAX_MEMORY_SIZE = 16 * 1024 * 1024\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (Seedream) обязаны попадать в `_is_image_only_model`\n# (image_enrichment.py), иначе запрос уйдёт с modalities [\"image\", \"text\"] и\n# провайдер ответит 404. То же касается любой новой модели.\n# Gemini-3-pro-image мультимодальная (отдаёт и картинку, и текст), поэтому её\n# туда добавлять НЕ нужно — проверено живым запросом.\n#\n# Проверять доступность модели нужно ТОЧЕЧНЫМ запросом\n# GET /api/v1/models/<id>/endpoints, а НЕ поиском по листингу /api/v1/models:\n# листинг отдаёт не весь каталог, и по нему легко ошибочно счесть живую модель\n# снятой. bytedance-seed/seedream-4.5 в листинге отсутствует, но её endpoint\n# `bytedance-seed/seedream-4.5-20251203` жив.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# ── Выбор модели пользователем (ai_engine.image_models) ──────────────────────\n# Пользователь переключает модель прямо на доске, поэтому `IMAGE_GEN_MODEL`\n# перестал быть единственным источником истины: он остался дефолтом последней\n# инстанции для вызовов, которые ничего не выбрали явно.\n#\n# ALLOWED — жёсткий allowlist. Model ID приходит с фронтенда, и доверять ему\n# нельзя: без этого списка пользователь смог бы направить наш OpenRouter-ключ\n# в любую модель каталога. Всё, чего нет в `ai_engine.image_models.IMAGE_MODELS`,\n# игнорируется, поэтому опечатка в env не выдаст наружу нерабочий id.\nIMAGE_GEN_ALLOWED_MODELS = os.getenv(\n \"IMAGE_GEN_ALLOWED_MODELS\",\n \"bytedance-seed/seedream-4.5,openai/gpt-5.4-image-2\",\n)\n\n# Дефолт для UI и для запросов без явного выбора. Приоритет выше, чем у\n# IMAGE_GEN_MODEL, но значение обязано входить в allowlist — иначе берётся\n# модель с флагом default из реестра.\nIMAGE_GEN_DEFAULT_MODEL = os.getenv(\n \"IMAGE_GEN_DEFAULT_MODEL\",\n \"bytedance-seed/seedream-4.5\",\n)\n\n# Качество по умолчанию: low / medium / high. Намеренно НЕ \"high\" — оно молча\n# удорожает каждую генерацию. Модели без поддержки качества параметр не\n# получают вовсе.\nIMAGE_GEN_QUALITY = os.getenv(\"IMAGE_GEN_QUALITY\", \"medium\")\n\n# Соотношение сторон генерации. Читалось в image_enrichment через\n# getattr(settings, ...), но в settings отсутствовало — значение жило только\n# во фолбэке и не настраивалось через env. Заведено явно.\nIMAGE_GEN_ASPECT_RATIO = os.getenv(\"IMAGE_GEN_ASPECT_RATIO\", \"16:9\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "3aa044050a9f641bdce63cd31d6ea3196d46ec06": {"t": 1785769058.481041, "n": 22714, "h": "d94cbdc327e8f3ab", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"pomodoro\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\n# Содержимое доски (ChatSession.canvas) приезжает одним JSON-телом, и\n# иллюстрации лежат в нём как data-URI по сотне-другой килобайт каждая.\n# Django по умолчанию режет тело на 2.5 МБ и отвечает RequestDataTooBig ДО\n# входа во вью — то есть раньше, чем сериализатор успевает применить свой\n# предел в 12 МБ. Из-за этого доска с несколькими иллюстрациями молча\n# переставала сохраняться. Держим оба предела согласованными; последнее слово\n# и понятная ошибка остаются за ai_engine.serializers.validate_canvas.\nDATA_UPLOAD_MAX_MEMORY_SIZE = 16 * 1024 * 1024\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (например Seedream) обязаны попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\n#\n# Дефолт сменён с bytedance-seed/seedream-4.5: этой модели БОЛЬШЕ НЕТ на\n# OpenRouter — она пропала из /api/v1/models, а запрос отвечает\n# 404 «No endpoints found that support the requested output modalities».\n# Из-за этого падала ЛЮБАЯ генерация иллюстрации. Gemini-3-pro-image\n# мультимодальная (отдаёт и картинку, и текст), поэтому в\n# `_is_image_only_model` её добавлять НЕ нужно — проверено живым запросом.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# ── Выбор модели пользователем (ai_engine.image_models) ──────────────────────\n# Пользователь переключает модель прямо на доске, поэтому `IMAGE_GEN_MODEL`\n# перестал быть единственным источником истины: он остался дефолтом последней\n# инстанции для вызовов, которые ничего не выбрали явно.\n#\n# ALLOWED — жёсткий allowlist. Model ID приходит с фронтенда, и доверять ему\n# нельзя: без этого списка пользователь смог бы направить наш OpenRouter-ключ\n# в любую модель каталога. Всё, чего нет в `ai_engine.image_models.IMAGE_MODELS`,\n# игнорируется, поэтому опечатка в env не выдаст наружу нерабочий id.\nIMAGE_GEN_ALLOWED_MODELS = os.getenv(\n \"IMAGE_GEN_ALLOWED_MODELS\",\n \"bytedance-seed/seedream-4.5,openai/gpt-5.4-image-2\",\n)\n\n# Дефолт для UI и для запросов без явного выбора. Приоритет выше, чем у\n# IMAGE_GEN_MODEL, но значение обязано входить в allowlist — иначе берётся\n# модель с флагом default из реестра.\n#\n# NB: см. предупреждение выше про Seedream — на 2026-07-29 её нет в каталоге\n# OpenRouter. Дефолт задан по прямому решению владельца продукта; откат на\n# рабочую модель делается этой же переменной, без правки кода.\nIMAGE_GEN_DEFAULT_MODEL = os.getenv(\n \"IMAGE_GEN_DEFAULT_MODEL\",\n \"bytedance-seed/seedream-4.5\",\n)\n\n# Качество по умолчанию: low / medium / high. Намеренно НЕ \"high\" — оно молча\n# удорожает каждую генерацию. Модели без поддержки качества параметр не\n# получают вовсе.\nIMAGE_GEN_QUALITY = os.getenv(\"IMAGE_GEN_QUALITY\", \"medium\")\n\n# Соотношение сторон генерации. Читалось в image_enrichment через\n# getattr(settings, ...), но в settings отсутствовало — значение жило только\n# во фолбэке и не настраивалось через env. Заведено явно.\nIMAGE_GEN_ASPECT_RATIO = os.getenv(\"IMAGE_GEN_ASPECT_RATIO\", \"16:9\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "0d83d4356dbd42931fca5940109137686419c00a": {"t": 1785769062.410255, "n": 20974, "h": "fbc63b5d5b27f77e", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"pomodoro\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\n# Содержимое доски (ChatSession.canvas) приезжает одним JSON-телом, и\n# иллюстрации лежат в нём как data-URI по сотне-другой килобайт каждая.\n# Django по умолчанию режет тело на 2.5 МБ и отвечает RequestDataTooBig ДО\n# входа во вью — то есть раньше, чем сериализатор успевает применить свой\n# предел в 12 МБ. Из-за этого доска с несколькими иллюстрациями молча\n# переставала сохраняться. Держим оба предела согласованными; последнее слово\n# и понятная ошибка остаются за ai_engine.serializers.validate_canvas.\nDATA_UPLOAD_MAX_MEMORY_SIZE = 16 * 1024 * 1024\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (например Seedream) обязаны попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\n#\n# Дефолт сменён с bytedance-seed/seedream-4.5: этой модели БОЛЬШЕ НЕТ на\n# OpenRouter — она пропала из /api/v1/models, а запрос отвечает\n# 404 «No endpoints found that support the requested output modalities».\n# Из-за этого падала ЛЮБАЯ генерация иллюстрации. Gemini-3-pro-image\n# мультимодальная (отдаёт и картинку, и текст), поэтому в\n# `_is_image_only_model` её добавлять НЕ нужно — проверено живым запросом.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "38444c56bf654961cb22fce9720a660396db233d": {"t": 1785769064.1548424, "n": 22698, "h": "232cc16dd2a3f863", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\n# Содержимое доски (ChatSession.canvas) приезжает одним JSON-телом, и\n# иллюстрации лежат в нём как data-URI по сотне-другой килобайт каждая.\n# Django по умолчанию режет тело на 2.5 МБ и отвечает RequestDataTooBig ДО\n# входа во вью — то есть раньше, чем сериализатор успевает применить свой\n# предел в 12 МБ. Из-за этого доска с несколькими иллюстрациями молча\n# переставала сохраняться. Держим оба предела согласованными; последнее слово\n# и понятная ошибка остаются за ai_engine.serializers.validate_canvas.\nDATA_UPLOAD_MAX_MEMORY_SIZE = 16 * 1024 * 1024\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (например Seedream) обязаны попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\n#\n# Дефолт сменён с bytedance-seed/seedream-4.5: этой модели БОЛЬШЕ НЕТ на\n# OpenRouter — она пропала из /api/v1/models, а запрос отвечает\n# 404 «No endpoints found that support the requested output modalities».\n# Из-за этого падала ЛЮБАЯ генерация иллюстрации. Gemini-3-pro-image\n# мультимодальная (отдаёт и картинку, и текст), поэтому в\n# `_is_image_only_model` её добавлять НЕ нужно — проверено живым запросом.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# ── Выбор модели пользователем (ai_engine.image_models) ──────────────────────\n# Пользователь переключает модель прямо на доске, поэтому `IMAGE_GEN_MODEL`\n# перестал быть единственным источником истины: он остался дефолтом последней\n# инстанции для вызовов, которые ничего не выбрали явно.\n#\n# ALLOWED — жёсткий allowlist. Model ID приходит с фронтенда, и доверять ему\n# нельзя: без этого списка пользователь смог бы направить наш OpenRouter-ключ\n# в любую модель каталога. Всё, чего нет в `ai_engine.image_models.IMAGE_MODELS`,\n# игнорируется, поэтому опечатка в env не выдаст наружу нерабочий id.\nIMAGE_GEN_ALLOWED_MODELS = os.getenv(\n \"IMAGE_GEN_ALLOWED_MODELS\",\n \"bytedance-seed/seedream-4.5,openai/gpt-5.4-image-2\",\n)\n\n# Дефолт для UI и для запросов без явного выбора. Приоритет выше, чем у\n# IMAGE_GEN_MODEL, но значение обязано входить в allowlist — иначе берётся\n# модель с флагом default из реестра.\n#\n# NB: см. предупреждение выше про Seedream — на 2026-07-29 её нет в каталоге\n# OpenRouter. Дефолт задан по прямому решению владельца продукта; откат на\n# рабочую модель делается этой же переменной, без правки кода.\nIMAGE_GEN_DEFAULT_MODEL = os.getenv(\n \"IMAGE_GEN_DEFAULT_MODEL\",\n \"bytedance-seed/seedream-4.5\",\n)\n\n# Качество по умолчанию: low / medium / high. Намеренно НЕ \"high\" — оно молча\n# удорожает каждую генерацию. Модели без поддержки качества параметр не\n# получают вовсе.\nIMAGE_GEN_QUALITY = os.getenv(\"IMAGE_GEN_QUALITY\", \"medium\")\n\n# Соотношение сторон генерации. Читалось в image_enrichment через\n# getattr(settings, ...), но в settings отсутствовало — значение жило только\n# во фолбэке и не настраивалось через env. Заведено явно.\nIMAGE_GEN_ASPECT_RATIO = os.getenv(\"IMAGE_GEN_ASPECT_RATIO\", \"16:9\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "6d5ec6cfabf0f2eb908403bd4bb84e0b3b9c966b": {"t": 1785769066.646309, "n": 22140, "h": "efd5285592c13e23", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (например Seedream) обязаны попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\n#\n# Дефолт сменён с bytedance-seed/seedream-4.5: этой модели БОЛЬШЕ НЕТ на\n# OpenRouter — она пропала из /api/v1/models, а запрос отвечает\n# 404 «No endpoints found that support the requested output modalities».\n# Из-за этого падала ЛЮБАЯ генерация иллюстрации. Gemini-3-pro-image\n# мультимодальная (отдаёт и картинку, и текст), поэтому в\n# `_is_image_only_model` её добавлять НЕ нужно — проверено живым запросом.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# ── Выбор модели пользователем (ai_engine.image_models) ──────────────────────\n# Пользователь переключает модель прямо на доске, поэтому `IMAGE_GEN_MODEL`\n# перестал быть единственным источником истины: он остался дефолтом последней\n# инстанции для вызовов, которые ничего не выбрали явно.\n#\n# ALLOWED — жёсткий allowlist. Model ID приходит с фронтенда, и доверять ему\n# нельзя: без этого списка пользователь смог бы направить наш OpenRouter-ключ\n# в любую модель каталога. Всё, чего нет в `ai_engine.image_models.IMAGE_MODELS`,\n# игнорируется, поэтому опечатка в env не выдаст наружу нерабочий id.\nIMAGE_GEN_ALLOWED_MODELS = os.getenv(\n \"IMAGE_GEN_ALLOWED_MODELS\",\n \"bytedance-seed/seedream-4.5,openai/gpt-5.4-image-2\",\n)\n\n# Дефолт для UI и для запросов без явного выбора. Приоритет выше, чем у\n# IMAGE_GEN_MODEL, но значение обязано входить в allowlist — иначе берётся\n# модель с флагом default из реестра.\n#\n# NB: см. предупреждение выше про Seedream — на 2026-07-29 её нет в каталоге\n# OpenRouter. Дефолт задан по прямому решению владельца продукта; откат на\n# рабочую модель делается этой же переменной, без правки кода.\nIMAGE_GEN_DEFAULT_MODEL = os.getenv(\n \"IMAGE_GEN_DEFAULT_MODEL\",\n \"bytedance-seed/seedream-4.5\",\n)\n\n# Качество по умолчанию: low / medium / high. Намеренно НЕ \"high\" — оно молча\n# удорожает каждую генерацию. Модели без поддержки качества параметр не\n# получают вовсе.\nIMAGE_GEN_QUALITY = os.getenv(\"IMAGE_GEN_QUALITY\", \"medium\")\n\n# Соотношение сторон генерации. Читалось в image_enrichment через\n# getattr(settings, ...), но в settings отсутствовало — значение жило только\n# во фолбэке и не настраивалось через env. Заведено явно.\nIMAGE_GEN_ASPECT_RATIO = os.getenv(\"IMAGE_GEN_ASPECT_RATIO\", \"16:9\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "9f366155254093d4f841c2ccc9b269f18af1881c": {"t": 1785769068.1723876, "n": 20958, "h": "75a7a05eaa69d433", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\n# Содержимое доски (ChatSession.canvas) приезжает одним JSON-телом, и\n# иллюстрации лежат в нём как data-URI по сотне-другой килобайт каждая.\n# Django по умолчанию режет тело на 2.5 МБ и отвечает RequestDataTooBig ДО\n# входа во вью — то есть раньше, чем сериализатор успевает применить свой\n# предел в 12 МБ. Из-за этого доска с несколькими иллюстрациями молча\n# переставала сохраняться. Держим оба предела согласованными; последнее слово\n# и понятная ошибка остаются за ai_engine.serializers.validate_canvas.\nDATA_UPLOAD_MAX_MEMORY_SIZE = 16 * 1024 * 1024\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (например Seedream) обязаны попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\n#\n# Дефолт сменён с bytedance-seed/seedream-4.5: этой модели БОЛЬШЕ НЕТ на\n# OpenRouter — она пропала из /api/v1/models, а запрос отвечает\n# 404 «No endpoints found that support the requested output modalities».\n# Из-за этого падала ЛЮБАЯ генерация иллюстрации. Gemini-3-pro-image\n# мультимодальная (отдаёт и картинку, и текст), поэтому в\n# `_is_image_only_model` её добавлять НЕ нужно — проверено живым запросом.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "24887aa1d3a1ec03c49b068f743f217abb9f476b": {"t": 1785769069.632271, "n": 20400, "h": "8c2e908ea4986410", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (например Seedream) обязаны попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\n#\n# Дефолт сменён с bytedance-seed/seedream-4.5: этой модели БОЛЬШЕ НЕТ на\n# OpenRouter — она пропала из /api/v1/models, а запрос отвечает\n# 404 «No endpoints found that support the requested output modalities».\n# Из-за этого падала ЛЮБАЯ генерация иллюстрации. Gemini-3-pro-image\n# мультимодальная (отдаёт и картинку, и текст), поэтому в\n# `_is_image_only_model` её добавлять НЕ нужно — проверено живым запросом.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "f70a358d12298fb2e15c6d149c93a9a6d166b6f2": {"t": 1785769071.2107348, "n": 17988, "h": "729c50aa9840900f", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: Seedream — ЧИСТО image-модель, поэтому она обязана попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"bytedance-seed/seedream-4.5\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (используется в ocr_views для OCR)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Хост выше живёт в CGNAT-диапазоне Tailscale (100.64.0.0/10) и из облачного\n# контейнера не маршрутизируется вообще: SYN уходит в никуда, и клиент честно\n# ждёт свой таймаут. Именно так прод и «сломался» — /api/ai/chat отдавал 504\n# ровно через 60с (таймаут skills/router), потому что BOARD_LLM_BASE_URL на\n# Northflank не задали и он молча ушёл на этот адрес. Тихий фолбэк допустим\n# только в разработке; в проде отсутствие явного URL должно быть громким.\nAI_ALLOW_LOCAL_LLM_FALLBACK = os.getenv(\n \"AI_ALLOW_LOCAL_LLM_FALLBACK\", \"True\" if DEBUG else \"False\"\n).lower() in {\"1\", \"true\", \"yes\"}\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# GLM-OCR / Smart Canvas Analyzer — существующая OpenAI-совместимая GLM\n# интеграция. OCR и analyzer используют общий ai_engine.glm_client.\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8081/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\", \"sk-local\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"mlx-community/GLM-OCR-bf16\")\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "2a385a26dd8595e3401fc313f312bf8188adb7c7": {"t": 1785769072.8717208, "n": 19802, "h": "63c02f7b55ec7123", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: чисто image-модели (например Seedream) обязаны попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\n#\n# Дефолт сменён с bytedance-seed/seedream-4.5: этой модели БОЛЬШЕ НЕТ на\n# OpenRouter — она пропала из /api/v1/models, а запрос отвечает\n# 404 «No endpoints found that support the requested output modalities».\n# Из-за этого падала ЛЮБАЯ генерация иллюстрации. Gemini-3-pro-image\n# мультимодальная (отдаёт и картинку, и текст), поэтому в\n# `_is_image_only_model` её добавлять НЕ нужно — проверено живым запросом.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3-pro-image\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "c1c47707e0e69986ff0d4bee3479d36068d09b34": {"t": 1785769074.2608352, "n": 19394, "h": "b926b48d23eaf2b2", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: Seedream — ЧИСТО image-модель, поэтому она обязана попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"bytedance-seed/seedream-4.5\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (БОЛЬШЕ НЕ ДЕФОЛТ для OCR: см.\n# блок Vision/OCR ниже — OCR по умолчанию идёт в Qwen3-VL через\n# OpenRouter, а этот порт остаётся опциональным локальным путём)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Vision / OCR — Qwen3-VL через OpenRouter (ai_engine.glm_client)\n#\n# Раньше здесь стояла GLM-OCR на Mac Studio (:8081), а PRODUCT.md §7.4 называл\n# целевой моделью self-hosted PaddleOCR-VL. От self-hosted пути отказались:\n# развернуть его негде, а адрес Mac Studio лежит в CGNAT-диапазоне Tailscale\n# (100.64.0.0/10) и из облачного контейнера не маршрутизируется вообще — SYN\n# уходит в никуда, и запрос честно ждёт свой таймаут. Ровно так уже ломался чат\n# (см. AI_ALLOW_LOCAL_LLM_FALLBACK выше), и OCR ломался бы так же.\n#\n# Поэтому по умолчанию — облачная Qwen3-VL через OpenRouter: тот же ключ, что у\n# остальных моделей, никакого своего железа. Локальный путь никуда не делся,\n# он просто перестал быть значением по умолчанию: задайте VISION_API_BASE_URL и\n# VISION_MODEL_NAME, чтобы вернуться на Mac Studio.\n#\n# VISION_API_KEY отдельным ключом оставлен намеренно (можно развести квоты), но\n# по умолчанию подхватывает OPENROUTER_API_KEY, иначе после смены базового URL\n# запрос уходил бы с прежним «sk-local» и провайдер отвечал 401.\n# ──────────────────────────────────────────────────────────────────────────────\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", \"https://openrouter.ai/api/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\") or os.getenv(\"OPENROUTER_API_KEY\", \"\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"qwen/qwen3-vl-32b-instruct\")\n# 32B-модель по сети отвечает дольше локальной: прежних 30 секунд не хватает.\nVISION_TIMEOUT = int(os.getenv(\"VISION_TIMEOUT\", \"60\"))\n# Бинаризация Оцу в ocr_views.preprocess_and_encode настраивалась под выделенную\n# OCR-модель: она давит полутона и оставляет чистый чёрный текст на белом. Qwen3-VL\n# — универсальная VLM, и ей обычно легче читать изображение без порога (порог\n# съедает слабый штрих карандаша), поэтому по умолчанию выключено.\n# На чистом печатном тексте разницы нет — проверено вживую, оба режима дали\n# одинаковую транскрипцию. Разница ожидается на реальном фото тетради; чтобы\n# было чем сравнить, прежнее поведение оставлено в этом переключателе.\nVISION_OCR_BINARIZE = os.getenv(\"VISION_OCR_BINARIZE\", \"0\").strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "89ef54e36170afd1b2b4c43f29f012da973bb18f": {"t": 1785769075.954986, "n": 17390, "h": "07a279ac779f5b39", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\n# Caps the reasoning token budget instead of letting it run unbounded: a\n# narrow/tricky topic can otherwise make the model \"think\" for most of\n# PLANNING_MAX_TOKENS before it ever writes the question JSON, which is slow\n# and burns tokens for a single adaptive follow-up question.\nPLANNING_REASONING_EFFORT = os.getenv(\"PLANNING_REASONING_EFFORT\", \"low\")\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: Seedream — ЧИСТО image-модель, поэтому она обязана попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"bytedance-seed/seedream-4.5\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (используется в ocr_views для OCR)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# GLM-OCR / Smart Canvas Analyzer — существующая OpenAI-совместимая GLM\n# интеграция. OCR и analyzer используют общий ai_engine.glm_client.\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8081/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\", \"sk-local\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"mlx-community/GLM-OCR-bf16\")\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "4983b8d1a07ae0a29f89611a2038a55c3ce6458c": {"t": 1785769077.4143682, "n": 17034, "h": "a39c7ce8bb7637de", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n \"ai_engine.middleware.AIUsageContextMiddleware\",\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Adaptive lesson planning intake (existing OpenRouter text client)\n# ──────────────────────────────────────────────────────────────────────────────\nPLANNING_MODEL = os.getenv(\n \"PLANNING_MODEL\",\n os.getenv(\"TEXT_LLM_MODEL\", \"deepseek/deepseek-v4-flash\"),\n)\nPLANNING_ENABLED = os.getenv(\"PLANNING_ENABLED\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\nPLANNING_MAX_QUESTIONS = int(os.getenv(\"PLANNING_MAX_QUESTIONS\", \"4\"))\nPLANNING_REASONING_ENABLED = os.getenv(\n \"PLANNING_REASONING_ENABLED\", \"true\"\n).lower() in {\"1\", \"true\", \"yes\", \"on\"}\nPLANNING_TIMEOUT = int(os.getenv(\"PLANNING_TIMEOUT\", \"20\"))\nPLANNING_MAX_TOKENS = int(os.getenv(\"PLANNING_MAX_TOKENS\", \"700\"))\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels`.\n# Провайдер: OpenRouter → Seedream 4.5 (ByteDance).\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter ($0.04 за картинку).\n# Предыдущая модель впечатывала в иллюстрацию псевдо-текст («Porgls»,\n# «Courcta», «Fikicn», «Filek») даже с полным TEXT_FREE-контрактом и коротким\n# терминальным запретом — и тем ломала контракт, по которому подписи\n# накладываются отдельным слоем. Она же добавляла лишние стрелки, второй угол и\n# внешнюю силу вопреки MECHANICS/INCLINED_PLANE-контрактам.\n# NB: Seedream — ЧИСТО image-модель, поэтому она обязана попадать в\n# `_is_image_only_model` (image_enrichment.py), иначе запрос уйдёт с modalities\n# [\"image\", \"text\"] и провайдер ответит 404. То же касается любой новой модели.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"bytedance-seed/seedream-4.5\")\n\n# Vision-модель для grounding координат подписей в illustration_pipeline.\n# ВАЖНО: это НЕ IMAGE_GEN_MODEL. Раньше значение наследовалось от него, но\n# генератор картинок vision-chat не умеет, из-за чего грунтинг молча\n# падал на неточные координаты. Нужна именно чат-модель со зрением.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", \"z-ai/glm-4.6v\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# AI usage ledger and Timely-token limits\n# ──────────────────────────────────────────────────────────────────────────────\n# Text requests are charged by provider token count. Image requests that do not\n# report token usage use a deterministic Timely-token charge per generated image.\nAI_IMAGE_TOKEN_CHARGE = int(os.getenv(\"AI_IMAGE_TOKEN_CHARGE\", \"4000\"))\nAI_CONTEXT_TOKEN_LIMIT = int(os.getenv(\"AI_CONTEXT_TOKEN_LIMIT\", \"128000\"))\nAI_TOKEN_5H_LIMIT = int(os.getenv(\"AI_TOKEN_5H_LIMIT\", \"250000\"))\nAI_TOKEN_WEEKLY_LIMIT = int(os.getenv(\"AI_TOKEN_WEEKLY_LIMIT\", \"1000000\"))\n# Free is intentionally small. Pro preserves the previous global limits. Max\n# gets 20x the rolling allowance of Pro while still retaining a hard ceiling.\nAI_FREE_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_FREE_CONTEXT_TOKEN_LIMIT\", \"32000\")\n)\nAI_FREE_TOKEN_5H_LIMIT = int(os.getenv(\"AI_FREE_TOKEN_5H_LIMIT\", \"25000\"))\nAI_FREE_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_FREE_TOKEN_WEEKLY_LIMIT\", \"100000\")\n)\nAI_MAX_CONTEXT_TOKEN_LIMIT = int(\n os.getenv(\"AI_MAX_CONTEXT_TOKEN_LIMIT\", \"256000\")\n)\nAI_MAX_TOKEN_5H_LIMIT = int(os.getenv(\"AI_MAX_TOKEN_5H_LIMIT\", \"5000000\"))\nAI_MAX_TOKEN_WEEKLY_LIMIT = int(\n os.getenv(\"AI_MAX_TOKEN_WEEKLY_LIMIT\", \"20000000\")\n)\nAI_PRO_PRICE_USD = float(os.getenv(\"AI_PRO_PRICE_USD\", \"19.9\"))\nAI_MAX_PRICE_USD = float(os.getenv(\"AI_MAX_PRICE_USD\", \"199\"))\nAI_PLAN_LIMITS = {\n \"free\": {\n \"label\": \"Free\",\n \"context\": AI_FREE_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_FREE_TOKEN_5H_LIMIT,\n \"weekly\": AI_FREE_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": 0.0,\n \"usage_multiplier\": 1,\n },\n \"pro\": {\n \"label\": \"Pro\",\n \"context\": AI_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_TOKEN_5H_LIMIT,\n \"weekly\": AI_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_PRO_PRICE_USD,\n \"usage_multiplier\": 10,\n },\n \"max\": {\n \"label\": \"Max\",\n \"context\": AI_MAX_CONTEXT_TOKEN_LIMIT,\n \"five_hour\": AI_MAX_TOKEN_5H_LIMIT,\n \"weekly\": AI_MAX_TOKEN_WEEKLY_LIMIT,\n \"price_usd\": AI_MAX_PRICE_USD,\n \"usage_multiplier\": 200,\n },\n}\n# Hard limits are enabled by default; set false only for an explicit A/B rollout.\nAI_USAGE_ENFORCE_LIMITS = os.getenv(\"AI_USAGE_ENFORCE_LIMITS\", \"true\").lower() in {\n \"1\",\n \"true\",\n \"yes\",\n \"on\",\n}\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Детерминированный путь научных схем (ai_engine.vector_pipeline)\n# ──────────────────────────────────────────────────────────────────────────────\n# Механику рисует не image-модель, а VectorRenderer: LLM отдаёт только семантику\n# (surface/body/vector/angle_arc), геометрию целиком считает бэкенд. Так схема\n# получается одинаковой при повторах, в кадре нет ни одной буквы, а подписи\n# приезжают структурой с ТОЧНЫМИ якорями (середина древка каждой силы) —\n# vision-грунтинг для них не нужен.\n#\n# По умолчанию ВЫКЛЮЧЕНО: путь покрывает пока только наклонную плоскость, всё\n# остальное честно уходит в растр. Включать после проверки на живой доске.\nILLUSTRATION_VECTOR_PIPELINE = os.getenv(\"ILLUSTRATION_VECTOR_PIPELINE\", \"false\")\n\n# Новый A/B-переключатель имеет приоритет над прежним boolean-флагом.\n# `legacy` не делает дополнительных Qwen-вызовов и остаётся production default.\nDIAGRAM_PIPELINE_MODE = os.getenv(\"DIAGRAM_PIPELINE_MODE\", \"legacy\")\nDIAGRAM_PLANNER_MODEL = os.getenv(\n \"DIAGRAM_PLANNER_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_CRITIC_MODEL = os.getenv(\n \"DIAGRAM_CRITIC_MODEL\",\n \"qwen/qwen3.7-plus\",\n)\nDIAGRAM_PLANNER_ENABLED = os.getenv(\"DIAGRAM_PLANNER_ENABLED\", \"true\")\nDIAGRAM_CRITIC_ENABLED = os.getenv(\"DIAGRAM_CRITIC_ENABLED\", \"true\")\nDIAGRAM_MAX_RETRIES = int(os.getenv(\"DIAGRAM_MAX_RETRIES\", \"1\"))\nDIAGRAM_PLANNER_TIMEOUT = int(os.getenv(\"DIAGRAM_PLANNER_TIMEOUT\", \"45\"))\nDIAGRAM_CRITIC_TIMEOUT = int(os.getenv(\"DIAGRAM_CRITIC_TIMEOUT\", \"45\"))\nDIAGRAM_PLANNER_MAX_TOKENS = int(os.getenv(\"DIAGRAM_PLANNER_MAX_TOKENS\", \"2400\"))\nDIAGRAM_CRITIC_MAX_TOKENS = int(os.getenv(\"DIAGRAM_CRITIC_MAX_TOKENS\", \"1200\"))\n\n# Backward-compatible aliases for the first vector prototype. Diagram planning\n# no longer inherits the global DeepSeek text model.\nVECTOR_PLANNER_MODEL = os.getenv(\n \"VECTOR_PLANNER_MODEL\",\n DIAGRAM_PLANNER_MODEL,\n)\nVECTOR_PLANNER_TIMEOUT = int(\n os.getenv(\"VECTOR_PLANNER_TIMEOUT\", str(DIAGRAM_PLANNER_TIMEOUT))\n)\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (используется в ocr_views для OCR)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# GLM-OCR / Smart Canvas Analyzer — существующая OpenAI-совместимая GLM\n# интеграция. OCR и analyzer используют общий ai_engine.glm_client.\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8081/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\", \"sk-local\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"mlx-community/GLM-OCR-bf16\")\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "15140e09c618c71fdb6504c8e16478be052ce33f": {"t": 1785769078.7773452, "n": 11246, "h": "eb749ebe8a1ddc2c", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels` от Llama.\n# Провайдер: OpenRouter → Nano Banana 2\n# (google/gemini-3.1-flash-image-preview, standard tier)\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter\n# \"Nano Banana 2\" / google/gemini-3.1-flash-image-preview — быстрее и дешевле\n# чем BananaPro (gemini-3-pro-image-preview), используем для тестов/прода.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3.1-flash-image-preview\")\n\n# Vision-фолбэк для grounding в illustration_pipeline. По умолчанию используем\n# ту же модель, что и для генерации картинки, чтобы не разъезжались режимы и\n# доступность в OpenRouter.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", IMAGE_GEN_MODEL)\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (используется в ocr_views для OCR)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# GLM-OCR / Smart Canvas Analyzer — существующая OpenAI-совместимая GLM\n# интеграция. OCR и analyzer используют общий ai_engine.glm_client.\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8081/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\", \"sk-local\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"mlx-community/GLM-OCR-bf16\")\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n \"nutrition\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "9a635e224c2ee1136851acbb57577a98841bb521": {"t": 1785769080.5473182, "n": 11161, "h": "21efc88a596b6725", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels` от Llama.\n# Провайдер: OpenRouter → Nano Banana 2\n# (google/gemini-3.1-flash-image-preview, standard tier)\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter\n# \"Nano Banana 2\" / google/gemini-3.1-flash-image-preview — быстрее и дешевле\n# чем BananaPro (gemini-3-pro-image-preview), используем для тестов/прода.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3.1-flash-image-preview\")\n\n# Vision-фолбэк для grounding в illustration_pipeline. По умолчанию используем\n# ту же модель, что и для генерации картинки, чтобы не разъезжались режимы и\n# доступность в OpenRouter.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", IMAGE_GEN_MODEL)\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timelyplan.me\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (используется в ocr_views для OCR)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# GLM-OCR / Smart Canvas Analyzer — существующая OpenAI-совместимая GLM\n# интеграция. OCR и analyzer используют общий ai_engine.glm_client.\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8081/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\", \"sk-local\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"mlx-community/GLM-OCR-bf16\")\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "b6f28973ab19a3a2263bb0b8ea99ed82e4b08d63": {"t": 1785769081.996081, "n": 11158, "h": "80e79e6158339384", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"nutrition\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels` от Llama.\n# Провайдер: OpenRouter → Nano Banana 2\n# (google/gemini-3.1-flash-image-preview, standard tier)\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter\n# \"Nano Banana 2\" / google/gemini-3.1-flash-image-preview — быстрее и дешевле\n# чем BananaPro (gemini-3-pro-image-preview), используем для тестов/прода.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3.1-flash-image-preview\")\n\n# Vision-фолбэк для grounding в illustration_pipeline. По умолчанию используем\n# ту же модель, что и для генерации картинки, чтобы не разъезжались режимы и\n# доступность в OpenRouter.\nGROUNDING_FALLBACK_MODEL = os.getenv(\"GROUNDING_FALLBACK_MODEL\", IMAGE_GEN_MODEL)\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timely.app\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (используется в ocr_views для OCR)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# GLM-OCR / Smart Canvas Analyzer — существующая OpenAI-совместимая GLM\n# интеграция. OCR и analyzer используют общий ai_engine.glm_client.\nVISION_API_BASE_URL = os.getenv(\"VISION_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8081/v1\")\nVISION_API_KEY = os.getenv(\"VISION_API_KEY\", \"sk-local\")\nVISION_MODEL_NAME = os.getenv(\"VISION_MODEL_NAME\", \"mlx-community/GLM-OCR-bf16\")\nGLM_ANALYZER_MODEL_NAME = os.getenv(\"GLM_ANALYZER_MODEL_NAME\", VISION_MODEL_NAME)\nGLM_ANALYZER_TIMEOUT = int(os.getenv(\"GLM_ANALYZER_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n\n# ── Логирование ──────────────────────────────────────────────────────\n# По умолчанию Django показывает только WARNING+ от сторонних логгеров —\n# из-за этого diagnostic-INFO ai_engine (классификация intent, режимы\n# t2i/i2i, ход illustration-пайплайна) не попадал в консоль runserver,\n# и отлаживать поток генерации приходилось вслепую.\nLOGGING = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"handlers\": {\"console\": {\"class\": \"logging.StreamHandler\"}},\n \"loggers\": {\n \"ai_engine\": {\"handlers\": [\"console\"], \"level\": \"INFO\", \"propagate\": False},\n },\n}\n"}, "1f3e4dbbaac21bc36d0033d0935295142bc297e2": {"t": 1785769083.4253323, "n": 9749, "h": "af15475694158b8b", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"habits\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels` от Llama.\n# Провайдер: OpenRouter → BananaPro (google/gemini-pro-image, standard tier)\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter\n# \"Nano Banana 2\" / google/gemini-3.1-flash-image-preview — быстрее и дешевле\n# чем BananaPro (gemini-3-pro-image-preview), используем для тестов/прода.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3.1-flash-image-preview\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timely.app\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (используется в ocr_views для OCR)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n"}, "4b3768de0a8795399e2a948282b8d679ddbba1f6": {"t": 1785769084.7784953, "n": 9735, "h": "505ca4c7b373b569", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\nload_dotenv(BASE_DIR.parent / '.env')\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n \"django_filters\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n\nAUTH_USER_MODEL = 'accounts.CustomUser'\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Image Generation (ai_engine.image_enrichment)\n# Используется для обогащения команд `image_with_labels` от Llama.\n# Провайдер: OpenRouter → BananaPro (google/gemini-pro-image, standard tier)\n# ──────────────────────────────────────────────────────────────────────────────\n\n# POST endpoint провайдера генерации изображений\nIMAGE_GEN_API_URL = os.getenv(\n \"IMAGE_GEN_API_URL\",\n \"https://openrouter.ai/api/v1/chat/completions\",\n)\n\n# API-ключ. Для OpenRouter — тот же ключ, что OPENROUTER_API_KEY.\n# В production задайте через переменную окружения IMAGE_GEN_API_KEY.\nIMAGE_GEN_API_KEY = os.getenv(\n \"IMAGE_GEN_API_KEY\",\n os.getenv(\"OPENROUTER_API_KEY\", \"\"),\n)\n\n# Идентификатор модели на OpenRouter\n# \"Nano Banana 2\" / google/gemini-3.1-flash-image-preview — быстрее и дешевле\n# чем BananaPro (gemini-3-pro-image-preview), используем для тестов/прода.\nIMAGE_GEN_MODEL = os.getenv(\"IMAGE_GEN_MODEL\", \"google/gemini-3.1-flash-image-preview\")\n\n# Таймаут одного запроса к провайдеру (секунды)\nIMAGE_GEN_TIMEOUT = int(os.getenv(\"IMAGE_GEN_TIMEOUT\", \"60\"))\n\n# Tier OpenRouter для маршрутизации запросов (standard / nitro)\nIMAGE_GEN_TIER = os.getenv(\"IMAGE_GEN_TIER\", \"standard\")\n\n# Максимальное количество одновременных запросов к API генерации\n# (актуально когда один ответ Llama содержит несколько image_with_labels команд)\nIMAGE_GEN_MAX_WORKERS = int(os.getenv(\"IMAGE_GEN_MAX_WORKERS\", \"3\"))\n\n# Публичный URL сервиса (используется в HTTP-Referer при запросах к OpenRouter)\nSITE_URL = os.getenv(\"SITE_URL\", \"https://timely.app\")\n\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Локальные модели на удалённой Mac Studio (через Tailscale, mlx_vlm.server +\n# отдельный SAM2-сервис). Используются в ai_engine.illustration_pipeline для\n# цепочки \"banana → SAM2 → Qwen → SVG\" и в ai_engine.ocr_views для OCR.\n#\n# Текущая раскладка портов на хосте MAC_STUDIO_HOST:\n# :8080 → mlx-community/Qwen3.6-27B-4bit (общая VLM: vision + текст)\n# :8081 → mlx-community/GLM-OCR-bf16 (используется в ocr_views для OCR)\n# :8002 → SAM 2 (facebook/sam2-hiera-large), отдельный FastAPI-сервис,\n# контракт: POST /api/segment/ {image_base64, points, labels}\n# → {mask_base64, score}\n# ──────────────────────────────────────────────────────────────────────────────\n\nMAC_STUDIO_HOST = os.getenv(\"MAC_STUDIO_HOST\", \"100.74.104.27\")\n\n# Qwen3.6-27B-4bit — универсальная локальная VLM (используется для семантической\n# разметки/подписей объектов в illustration_pipeline; OpenAI-совместимый API).\nQWEN_API_BASE_URL = os.getenv(\"QWEN_API_BASE_URL\", f\"http://{MAC_STUDIO_HOST}:8080/v1\")\nQWEN_API_KEY = os.getenv(\"QWEN_API_KEY\", \"sk-local\")\nQWEN_MODEL_NAME = os.getenv(\"QWEN_MODEL_NAME\", \"mlx-community/Qwen3.6-27B-4bit\")\nQWEN_TIMEOUT = int(os.getenv(\"QWEN_TIMEOUT\", \"60\"))\n\n# SAM 2 — точечная сегментация изображений (свой контракт, НЕ OpenAI-совместимый).\nSAM2_API_URL = os.getenv(\"SAM2_API_URL\", f\"http://{MAC_STUDIO_HOST}:8002/api/segment/\")\n# Таймаут на запрос немалый: даже на уменьшенной копии изображения (см.\n# ILLUSTRATION_SAM2_MAX_DIM) одна сегментация занимает несколько секунд,\n# а сервис (см. ниже) почти не выигрывает от распараллеливания.\nSAM2_TIMEOUT = int(os.getenv(\"SAM2_TIMEOUT\", \"120\"))\n\n# ──────────────────────────────────────────────────────────────────────────────\n# Параметры построения векторных иллюстраций (illustration_pipeline)\n#\n# ВАЖНО про производительность SAM2-сервиса (измерено эмпирически на реальной\n# сгенерированной иллюстрации 1408×768): время одного запроса критически\n# зависит от РАЗРЕШЕНИЯ изображения — full-res ≈ 28 секунд на точку, а при\n# нескольких ПАРАЛЛЕЛЬНЫХ запросах сервис уходит в массовые таймауты (он не\n# распараллеливает инференс — судя по всему, единственное GPU-устройство).\n# Поэтому: (1) перед отправкой в SAM2 уменьшаем длинную сторону картинки до\n# ILLUSTRATION_SAM2_MAX_DIM (это ускоряет запрос ~в 10 раз: 28с → ~3с, при\n# этом качество маски не страдает — контур всё равно упрощается на этапе\n# построения SVG); маски затем растягиваются обратно к исходному разрешению.\n# (2) держим ILLUSTRATION_SAM2_MAX_WORKERS низким (вплоть до 1 — без\n# параллелизма), чтобы не перегружать сервис и не словить контеншн-замедление.\n# ──────────────────────────────────────────────────────────────────────────────\n\n# Сетка N×N стартовых точек — ПОДСТРАХОВКА к content-aware поиску объектов\n# (см. illustration_pipeline._content_seed_points). Держим небольшим: основную\n# работу выполняет content-aware детектор, сетка лишь подстраховывает на случай\n# нетипичных изображений (текстурный фон и т.п.).\nILLUSTRATION_GRID_N = int(os.getenv(\"ILLUSTRATION_GRID_N\", \"3\"))\nILLUSTRATION_MAX_OBJECTS = int(os.getenv(\"ILLUSTRATION_MAX_OBJECTS\", \"6\"))\n\n# Воркеры для Qwen-подписей — лёгкие vision-запросы на маленьких вырезанных\n# фрагментах, параллелятся гораздо лучше, чем тяжёлая SAM2-сегментация.\nILLUSTRATION_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_MAX_WORKERS\", \"4\"))\n\n# Длинная сторона копии изображения, отправляемой в SAM2 (px). См. обоснование выше.\nILLUSTRATION_SAM2_MAX_DIM = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_DIM\", \"384\"))\n# Параллелизм запросов к SAM2 — низкий специально (см. обоснование выше).\nILLUSTRATION_SAM2_MAX_WORKERS = int(os.getenv(\"ILLUSTRATION_SAM2_MAX_WORKERS\", \"2\"))\n"}, "accb28c0d34e4ec5457f0ecc1129e286ebfe2d61": {"t": 1785769086.127853, "n": 4276, "h": "672de820ed35f03a", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n \"diary\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n"}, "74cbdda3e6b0a905d40ba454757d5f2884d837fe": {"t": 1785769087.4516594, "n": 4263, "h": "5881b9cd07ac54eb", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n \"accounts\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n"}, "1f7f6a19896a8eced8065277ab3f107a48bd28d8": {"t": 1785769088.8148577, "n": 4247, "h": "33f4fd98fba217c8", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\n# Disable APPEND_SLASH to avoid errors when Vercel strips trailing slashes\n# This allows DELETE/PUT/PATCH requests without trailing slash to work\nAPPEND_SLASH = False\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n"}, "cdc4316ee15676c970298dda1c9a2052d238a68e": {"t": 1785769090.0035164, "n": 4079, "h": "bbf81fb37f942c9a", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"corsheaders.middleware.CorsMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n \"config.middleware.UserEmailMiddleware\", # Extract user email from header\n]\n\n# CORS settings\nCORS_ALLOW_ALL_ORIGINS = True\nCORS_ALLOW_HEADERS = [\n 'content-type',\n 'x-user-email',\n 'authorization',\n 'accept',\n 'origin',\n]\n\nROOT_URLCONF = \"config.urls\"\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n\n# Cache configuration - using database cache (free, uses existing Postgres)\nCACHES = {\n 'default': {\n 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',\n 'LOCATION': 'cache_table',\n 'TIMEOUT': 300, # 5 minutes default\n }\n}\n\n# Default primary key field type\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n"}, "448fc2d36bf43e7761367b4dadc9d09780e3aacf": {"t": 1785769091.3419418, "n": 3448, "h": "eaf219773af22339", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n \"corsheaders\",\n \"rest_framework\",\n \"planner\",\n \"mind\",\n \"ai_engine\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n]\n\nROOT_URLCONF = \"config.urls\"\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n"}, "e228a19728ac6e9c69ed456c552762f3c4f215bf": {"t": 1785769092.7686656, "n": 3363, "h": "894e650085f3eac8", "c": "\"\"\"\nDjango settings for config project.\n\nGenerated by 'django-admin startproject' using Django 6.0.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/6.0/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/6.0/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\nimport os\nimport dj_database_url\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = os.getenv(\"SECRET_KEY\", \"django-insecure-zj7m&6fm&$(h$_#848i-y$c70_@uer&k#kj(v=+@48679w6q!t\")\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = os.getenv(\"DEBUG\", \"True\") == \"True\"\n\nALLOWED_HOSTS = os.getenv(\"ALLOWED_HOSTS\", \"*\").split(\",\")\n\n\n# Application definition\n\nINSTALLED_APPS = [\n \"django.contrib.admin\",\n \"django.contrib.auth\",\n \"django.contrib.contenttypes\",\n \"django.contrib.sessions\",\n \"django.contrib.messages\",\n \"django.contrib.staticfiles\",\n]\n\nMIDDLEWARE = [\n \"django.middleware.security.SecurityMiddleware\",\n \"whitenoise.middleware.WhiteNoiseMiddleware\",\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.common.CommonMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n \"django.contrib.messages.middleware.MessageMiddleware\",\n \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n]\n\nROOT_URLCONF = \"config.urls\"\n\nTEMPLATES = [\n {\n \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n \"DIRS\": [],\n \"APP_DIRS\": True,\n \"OPTIONS\": {\n \"context_processors\": [\n \"django.template.context_processors.request\",\n \"django.contrib.auth.context_processors.auth\",\n \"django.contrib.messages.context_processors.messages\",\n ],\n },\n },\n]\n\nWSGI_APPLICATION = \"config.wsgi.application\"\n\n\n# Database\n# https://docs.djangoproject.com/en/6.0/ref/settings/#databases\n\nDATABASES = {\n \"default\": dj_database_url.config(\n default=\"sqlite:///\" + str(BASE_DIR / \"db.sqlite3\"),\n conn_max_age=600,\n )\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = [\n {\n \"NAME\": \"django.contrib.auth.password_validation.UserAttributeSimilarityValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.MinimumLengthValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.CommonPasswordValidator\",\n },\n {\n \"NAME\": \"django.contrib.auth.password_validation.NumericPasswordValidator\",\n },\n]\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/6.0/topics/i18n/\n\nLANGUAGE_CODE = \"en-us\"\n\nTIME_ZONE = \"UTC\"\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/6.0/howto/static-files/\n\nSTATIC_URL = \"static/\"\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATICFILES_STORAGE = \"whitenoise.storage.CompressedManifestStaticFilesStorage\"\n"}}}}