per-campaign settings, default campaign folders, custom prompt fix
All checks were successful
Build and Push / build (push) Successful in 12m10s
All checks were successful
Build and Push / build (push) Successful in 12m10s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from .. import database as db
|
||||
from .. import database as db, config
|
||||
|
||||
router = APIRouter(prefix="/api/campaigns", tags=["campaigns"])
|
||||
|
||||
@@ -45,3 +45,24 @@ def delete_campaign(campaign_id: str):
|
||||
raise HTTPException(404, "Campaign not found")
|
||||
db.delete_campaign(campaign_id)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/{campaign_id}/settings")
|
||||
def get_campaign_settings(campaign_id: str):
|
||||
if not db.get_campaign(campaign_id):
|
||||
raise HTTPException(404, "Campaign not found")
|
||||
raw = db.get_campaign_settings(campaign_id)
|
||||
# return defaults for any missing keys
|
||||
merged = {**config.CAMPAIGN_SETTINGS, **raw}
|
||||
return merged
|
||||
|
||||
|
||||
@router.post("/{campaign_id}/settings")
|
||||
def update_campaign_settings(campaign_id: str, body: dict):
|
||||
if not db.get_campaign(campaign_id):
|
||||
raise HTTPException(404, "Campaign not found")
|
||||
clean = {k: str(v) for k, v in body.items() if k in config.CAMPAIGN_SETTINGS}
|
||||
db.update_campaign_settings(campaign_id, clean)
|
||||
raw = db.get_campaign_settings(campaign_id)
|
||||
merged = {**config.CAMPAIGN_SETTINGS, **raw}
|
||||
return merged
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from ..diagnostics import run_diagnostics
|
||||
|
||||
@@ -6,6 +6,6 @@ router = APIRouter(prefix="/api/diagnostics", tags=["diagnostics"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_diagnostics():
|
||||
checks = run_diagnostics()
|
||||
def get_diagnostics(campaign_id: str = Query("default")):
|
||||
checks = run_diagnostics(campaign_id)
|
||||
return {"ok": all(c["ok"] for c in checks), "checks": checks}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import requests
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from .. import database as db
|
||||
|
||||
@@ -7,10 +7,10 @@ router = APIRouter(prefix="/api/models", tags=["models"])
|
||||
|
||||
|
||||
@router.get("/ollama")
|
||||
def list_ollama_models():
|
||||
def list_ollama_models(campaign_id: str = Query("default")):
|
||||
"""List models already pulled on the configured Ollama host, so the setup
|
||||
wizard can offer a dropdown instead of asking the user to type a tag blind."""
|
||||
settings = db.get_settings()
|
||||
settings = db.get_campaign_settings(campaign_id)
|
||||
host = settings.get("ollama_host", "http://host.docker.internal:11434")
|
||||
try:
|
||||
resp = requests.get(f"{host.rstrip('/')}/api/tags", timeout=5)
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from .. import database as db, jobs
|
||||
from ..pipeline.turns import load_turns
|
||||
from ..pipeline.summarize import summarize_session, PLAYER_FINAL_PROMPTS
|
||||
from ..pipeline.summarize import summarize_session
|
||||
|
||||
router = APIRouter(prefix="/api/sessions/{session_id}/notes", tags=["notes"])
|
||||
|
||||
@@ -18,26 +18,21 @@ async def generate_notes(session_id: str, request: Request):
|
||||
if not session["transcript_path"]:
|
||||
raise HTTPException(400, "Session hasn't been transcribed yet")
|
||||
|
||||
settings = db.get_settings()
|
||||
cid = session["campaign_id"] or "default"
|
||||
settings = db.get_campaign_settings(cid)
|
||||
speaker_map = {r["raw_label"]: r["display_name"] for r in
|
||||
db.get_conn().execute("SELECT raw_label, display_name FROM speakers WHERE session_id = ?", (session_id,))
|
||||
if r["display_name"]}
|
||||
|
||||
# Per-request style overrides the global setting
|
||||
# Per-request style overrides the campaign setting
|
||||
style = body.get("player_recap_style") or settings.get("player_recap_style") or "story"
|
||||
custom = body.get("player_recap_custom_prompt") or settings.get("player_recap_custom_prompt") or ""
|
||||
|
||||
# Resolve the prompt template (same logic as summarize_session)
|
||||
if style == "custom":
|
||||
player_template = custom if custom else PLAYER_FINAL_PROMPTS["story"]
|
||||
else:
|
||||
player_template = PLAYER_FINAL_PROMPTS.get(style, PLAYER_FINAL_PROMPTS["story"])
|
||||
|
||||
job_id = db.create_job(session_id, "summarize")
|
||||
|
||||
def run(progress_cb):
|
||||
turns = load_turns(Path(session["transcript_path"]), speaker_map)
|
||||
dm_notes, player_recap = summarize_session(
|
||||
dm_notes, player_recap, used_template = summarize_session(
|
||||
turns, settings, progress_cb=progress_cb,
|
||||
player_recap_style=style,
|
||||
player_recap_custom_prompt=custom,
|
||||
@@ -47,7 +42,7 @@ async def generate_notes(session_id: str, request: Request):
|
||||
"INSERT INTO notes (session_id, dm_notes, player_recap, player_recap_prompt, generated_at) VALUES (?, ?, ?, ?, ?) "
|
||||
"ON CONFLICT(session_id) DO UPDATE SET dm_notes=excluded.dm_notes, "
|
||||
"player_recap=excluded.player_recap, player_recap_prompt=excluded.player_recap_prompt, generated_at=excluded.generated_at",
|
||||
(session_id, dm_notes, player_recap, player_template, db.now()),
|
||||
(session_id, dm_notes, player_recap, used_template, db.now()),
|
||||
)
|
||||
conn.execute("UPDATE sessions SET status = 'complete' WHERE id = ?", (session_id,))
|
||||
|
||||
|
||||
@@ -84,8 +84,9 @@ def get_session(session_id: str):
|
||||
return session
|
||||
|
||||
|
||||
def _run_transcription(session_id: str, video_path: Path, audio_path: Path, transcript_path: Path):
|
||||
settings = db.get_settings()
|
||||
def _run_transcription(session_id: str, video_path: Path, audio_path: Path, transcript_path: Path, campaign_id: str | None = None):
|
||||
cid = campaign_id or "default"
|
||||
settings = db.get_campaign_settings(cid)
|
||||
|
||||
def run(progress_cb):
|
||||
db.get_conn() # ensure thread-local connection exists in this worker thread
|
||||
@@ -159,7 +160,7 @@ async def create_session(
|
||||
|
||||
audio_path = audio_dir / f"{session_id}.wav"
|
||||
transcript_path = transcript_dir / f"{session_id}.json"
|
||||
job_id = _run_transcription(session_id, video_path, audio_path, transcript_path)
|
||||
job_id = _run_transcription(session_id, video_path, audio_path, transcript_path, campaign_id)
|
||||
|
||||
return {"session_id": session_id, "job_id": job_id}
|
||||
|
||||
@@ -179,7 +180,7 @@ def retry_transcription(session_id: str):
|
||||
else:
|
||||
audio_path = config.AUDIO_DIR / f"{session_id}.wav"
|
||||
transcript_path = config.TRANSCRIPT_DIR / f"{session_id}.json"
|
||||
job_id = _run_transcription(session_id, video_path, audio_path, transcript_path)
|
||||
job_id = _run_transcription(session_id, video_path, audio_path, transcript_path, campaign_id)
|
||||
return {"job_id": job_id}
|
||||
|
||||
|
||||
|
||||
@@ -6,27 +6,10 @@ from .. import database as db, config
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
|
||||
# Settings are stored as flat key/value rows (see database.py / config.DEFAULT_SETTINGS).
|
||||
# This router is a thin pass-through over that table - it must not invent its own
|
||||
# schema, or the Settings/Setup pages will silently stop affecting the actual
|
||||
# transcription/summarization pipeline, which reads these same flat keys.
|
||||
# Only global (GLOBAL_SETTINGS) keys live here; campaign settings are under /api/campaigns/{id}/settings.
|
||||
|
||||
# Env var overrides — set these in docker-compose.yml to prefill the setup wizard
|
||||
_ENV_OVERRIDES = {
|
||||
"onboarding_completed": "NAT20_ONBOARDING_COMPLETED",
|
||||
"hf_token": "NAT20_HF_TOKEN",
|
||||
"whisper_model": "NAT20_WHISPER_MODEL",
|
||||
"whisper_compute_type": "NAT20_WHISPER_COMPUTE_TYPE",
|
||||
"ollama_host": "NAT20_OLLAMA_HOST",
|
||||
"ollama_model": "NAT20_OLLAMA_MODEL",
|
||||
"api_base_url": "NAT20_API_BASE_URL",
|
||||
"api_key": "NAT20_API_KEY",
|
||||
"api_model": "NAT20_API_MODEL",
|
||||
"chunk_word_target": "NAT20_CHUNK_WORD_TARGET",
|
||||
"world_context": "NAT20_WORLD_CONTEXT",
|
||||
"world_context_path": "NAT20_WORLD_CONTEXT_PATH",
|
||||
"player_recap_style": "NAT20_PLAYER_RECAP_STYLE",
|
||||
"player_recap_custom_prompt": "NAT20_PLAYER_RECAP_CUSTOM_PROMPT",
|
||||
}
|
||||
|
||||
_BOOL_KEYS = {"onboarding_completed"}
|
||||
@@ -39,15 +22,15 @@ def _coerce(key: str, value: str):
|
||||
|
||||
|
||||
def _merge_with_env(raw: dict) -> dict:
|
||||
"""Priority: explicit user DB saves (non-default) > env vars > DEFAULT_SETTINGS."""
|
||||
merged = {**config.DEFAULT_SETTINGS}
|
||||
"""Priority: explicit user DB saves (non-default) > env vars > GLOBAL_SETTINGS."""
|
||||
merged = {**config.GLOBAL_SETTINGS}
|
||||
for key, env_name in _ENV_OVERRIDES.items():
|
||||
val = os.environ.get(env_name)
|
||||
if val is not None:
|
||||
merged[key] = val
|
||||
for key, val in raw.items():
|
||||
db_val = val.strip()
|
||||
if db_val and db_val != config.DEFAULT_SETTINGS.get(key, ""):
|
||||
if db_val and db_val != config.GLOBAL_SETTINGS.get(key, ""):
|
||||
merged[key] = db_val
|
||||
return merged
|
||||
|
||||
@@ -61,7 +44,7 @@ async def get_settings():
|
||||
|
||||
@router.post("")
|
||||
async def update_settings(patch: dict):
|
||||
clean = {k: str(v) for k, v in patch.items() if k in config.DEFAULT_SETTINGS}
|
||||
clean = {k: str(v) for k, v in patch.items() if k in config.GLOBAL_SETTINGS}
|
||||
db.update_settings(clean)
|
||||
raw = db.get_settings()
|
||||
merged = _merge_with_env(raw)
|
||||
|
||||
Reference in New Issue
Block a user