diff --git a/api/pipeline.py b/api/pipeline.py index 783b5fb..f5d4bf8 100644 --- a/api/pipeline.py +++ b/api/pipeline.py @@ -10,7 +10,7 @@ from api.router import broadcast from config import load as load_config from transcription import engine as transcription_engine from llm import OllamaClient -from output import save_transcript, write_meeting_docs +from output import write_solo_docs, write_meeting_docs logger = logging.getLogger(__name__) @@ -80,15 +80,22 @@ async def _run_solo_pipeline(cfg, wav_path, output_dir, instructions): model=cfg["ollama"]["model"], ) + dt = datetime.now() + paths = write_solo_docs(raw_text=raw_text, refined=refined, output_dir=output_dir, dt=dt) + title = "Diktat" for line in refined.splitlines(): if line.startswith("# "): title = line[2:].strip() break - path = save_transcript(title=title, content=refined, output_dir=output_dir) - await broadcast({"event": "saved", "path": path, "title": title}) await state.set_status(Status.IDLE) + await broadcast({ + "event": "saved", + "path": paths["index"], + "title": title, + "paths": paths, + }) async def _run_meeting_pipeline(cfg, wav_path, output_dir, instructions, diar_cfg): diff --git a/api/router.py b/api/router.py index 12d7a2d..4f7d750 100644 --- a/api/router.py +++ b/api/router.py @@ -209,28 +209,36 @@ async def put_config(body: dict, user: dict = Depends(current_user)): @router.post("/open") async def open_file(body: dict, user: dict = Depends(current_user)): import subprocess, shutil - path = body.get("path", "") user_dir = os.path.join(user["output_dir"], user["username"]) - if not (path and os.path.exists(path) and os.path.abspath(path).startswith(os.path.abspath(user_dir))): + abs_user_dir = os.path.abspath(user_dir) + + # Accept either a single path or a list of paths (for 3-file recordings) + raw_paths = body.get("paths") or ([body.get("path")] if body.get("path") else []) + paths = [p for p in raw_paths if p and os.path.exists(p) and os.path.abspath(p).startswith(abs_user_dir)] + if not paths: return {"ok": False} + mode = body.get("mode", "editor") # "editor" | "folder" | "obsidian" if mode == "obsidian": from urllib.parse import quote cfg = load_config() vault = cfg.get("obsidian", {}).get("vault", "").strip() - if vault and os.path.isdir(vault): - dest = os.path.join(vault, os.path.basename(path)) - shutil.copy2(path, dest) - target = dest - else: - target = path - subprocess.Popen(["xdg-open", f"obsidian://open?path={quote(target, safe='/')}"]) + open_target = paths[0] + for p in paths: + if vault and os.path.isdir(vault): + dest = os.path.join(vault, os.path.basename(p)) + shutil.copy2(p, dest) + if p == paths[0]: + open_target = dest + else: + open_target = p + subprocess.Popen(["xdg-open", f"obsidian://open?path={quote(open_target, safe='/')}"]) elif mode == "folder" and shutil.which("dolphin"): - subprocess.Popen(["dolphin", "--select", path]) + subprocess.Popen(["dolphin", "--select", paths[0]]) elif mode == "folder": - subprocess.Popen(["xdg-open", os.path.dirname(path)]) + subprocess.Popen(["xdg-open", os.path.dirname(paths[0])]) else: - subprocess.Popen(["xdg-open", path]) + subprocess.Popen(["xdg-open", paths[0]]) return {"ok": True} diff --git a/frontend/app.js b/frontend/app.js index ec66e99..d18a154 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -13,6 +13,7 @@ const modalFolderBtn = document.getElementById('modal-folder-btn'); const modalOpenBtn = document.getElementById('modal-open-btn'); const modalCloseBtn = document.getElementById('modal-close-btn'); let _modalPath = null; +let _modalPaths = null; let _modalFilename = null; const speakerCard = document.getElementById('speaker-card'); @@ -53,8 +54,9 @@ logoutBtn.addEventListener('click', () => { }); }); -function openModal(filename, path) { +function openModal(filename, path, paths) { _modalPath = path; + _modalPaths = paths || null; _modalFilename = filename; modalTitle.textContent = filename.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, ''); modalBody.innerHTML = ''; @@ -69,6 +71,7 @@ function openModal(filename, path) { function closeModal() { modal.classList.add('hidden'); _modalPath = null; + _modalPaths = null; _modalFilename = null; } @@ -76,7 +79,11 @@ modalCloseBtn.addEventListener('click', closeModal); modal.querySelector('.modal-backdrop').addEventListener('click', closeModal); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); }); modalObsidianBtn.addEventListener('click', () => { - if (_modalPath) apiFetch('/open', { method: 'POST', body: JSON.stringify({ path: _modalPath, mode: 'obsidian' }) }); + if (_modalPaths) { + apiFetch('/open', { method: 'POST', body: JSON.stringify({ paths: Object.values(_modalPaths), mode: 'obsidian' }) }); + } else if (_modalPath) { + apiFetch('/open', { method: 'POST', body: JSON.stringify({ path: _modalPath, mode: 'obsidian' }) }); + } }); modalFolderBtn.addEventListener('click', () => { if (_modalPath) apiFetch('/open', { method: 'POST', body: JSON.stringify({ path: _modalPath, mode: 'folder' }) }); diff --git a/tests/test_output.py b/tests/test_output.py index 13bc88f..bc2f225 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -60,6 +60,25 @@ def test_slugify(): assert slugify("test -- foo") == "test-foo" +def test_write_solo_docs_creates_three_files(tmp_path): + from output import write_solo_docs + from datetime import datetime + paths = write_solo_docs( + raw_text="Das ist der rohe Text vom Mikrofon.", + refined="# Projektstatus\n\nDas Projekt läuft gut.\n", + output_dir=str(tmp_path), + dt=datetime(2026, 4, 2, 15, 0), + ) + assert set(paths.keys()) == {"index", "transkript", "zusammenfassung"} + assert all(os.path.exists(p) for p in paths.values()) + index = open(paths["index"]).read() + assert "Projektstatus" in index + assert "transkript" in index + assert "zusammenfassung" in index + assert "Das ist der rohe Text" in open(paths["transkript"]).read() + assert "Projekt läuft gut" in open(paths["zusammenfassung"]).read() + + def test_write_meeting_docs_creates_three_files(tmp_path): from output import write_meeting_docs from datetime import datetime