feat: write 3 files per solo recording (index + transkript + zusammenfassung)
- pipeline: call write_solo_docs() instead of save_transcript(); broadcast paths dict - router: /open accepts paths list for Obsidian mode, copies all 3 files to vault - app.js: store _modalPaths from saved event; Obsidian button sends all paths - tests: test_write_solo_docs_creates_three_files added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+10
-3
@@ -10,7 +10,7 @@ from api.router import broadcast
|
|||||||
from config import load as load_config
|
from config import load as load_config
|
||||||
from transcription import engine as transcription_engine
|
from transcription import engine as transcription_engine
|
||||||
from llm import OllamaClient
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -80,15 +80,22 @@ async def _run_solo_pipeline(cfg, wav_path, output_dir, instructions):
|
|||||||
model=cfg["ollama"]["model"],
|
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"
|
title = "Diktat"
|
||||||
for line in refined.splitlines():
|
for line in refined.splitlines():
|
||||||
if line.startswith("# "):
|
if line.startswith("# "):
|
||||||
title = line[2:].strip()
|
title = line[2:].strip()
|
||||||
break
|
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 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):
|
async def _run_meeting_pipeline(cfg, wav_path, output_dir, instructions, diar_cfg):
|
||||||
|
|||||||
+20
-12
@@ -209,28 +209,36 @@ async def put_config(body: dict, user: dict = Depends(current_user)):
|
|||||||
@router.post("/open")
|
@router.post("/open")
|
||||||
async def open_file(body: dict, user: dict = Depends(current_user)):
|
async def open_file(body: dict, user: dict = Depends(current_user)):
|
||||||
import subprocess, shutil
|
import subprocess, shutil
|
||||||
path = body.get("path", "")
|
|
||||||
user_dir = os.path.join(user["output_dir"], user["username"])
|
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}
|
return {"ok": False}
|
||||||
|
|
||||||
mode = body.get("mode", "editor") # "editor" | "folder" | "obsidian"
|
mode = body.get("mode", "editor") # "editor" | "folder" | "obsidian"
|
||||||
if mode == "obsidian":
|
if mode == "obsidian":
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
vault = cfg.get("obsidian", {}).get("vault", "").strip()
|
vault = cfg.get("obsidian", {}).get("vault", "").strip()
|
||||||
if vault and os.path.isdir(vault):
|
open_target = paths[0]
|
||||||
dest = os.path.join(vault, os.path.basename(path))
|
for p in paths:
|
||||||
shutil.copy2(path, dest)
|
if vault and os.path.isdir(vault):
|
||||||
target = dest
|
dest = os.path.join(vault, os.path.basename(p))
|
||||||
else:
|
shutil.copy2(p, dest)
|
||||||
target = path
|
if p == paths[0]:
|
||||||
subprocess.Popen(["xdg-open", f"obsidian://open?path={quote(target, safe='/')}"])
|
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"):
|
elif mode == "folder" and shutil.which("dolphin"):
|
||||||
subprocess.Popen(["dolphin", "--select", path])
|
subprocess.Popen(["dolphin", "--select", paths[0]])
|
||||||
elif mode == "folder":
|
elif mode == "folder":
|
||||||
subprocess.Popen(["xdg-open", os.path.dirname(path)])
|
subprocess.Popen(["xdg-open", os.path.dirname(paths[0])])
|
||||||
else:
|
else:
|
||||||
subprocess.Popen(["xdg-open", path])
|
subprocess.Popen(["xdg-open", paths[0]])
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -13,6 +13,7 @@ const modalFolderBtn = document.getElementById('modal-folder-btn');
|
|||||||
const modalOpenBtn = document.getElementById('modal-open-btn');
|
const modalOpenBtn = document.getElementById('modal-open-btn');
|
||||||
const modalCloseBtn = document.getElementById('modal-close-btn');
|
const modalCloseBtn = document.getElementById('modal-close-btn');
|
||||||
let _modalPath = null;
|
let _modalPath = null;
|
||||||
|
let _modalPaths = null;
|
||||||
let _modalFilename = null;
|
let _modalFilename = null;
|
||||||
|
|
||||||
const speakerCard = document.getElementById('speaker-card');
|
const speakerCard = document.getElementById('speaker-card');
|
||||||
@@ -53,8 +54,9 @@ logoutBtn.addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function openModal(filename, path) {
|
function openModal(filename, path, paths) {
|
||||||
_modalPath = path;
|
_modalPath = path;
|
||||||
|
_modalPaths = paths || null;
|
||||||
_modalFilename = filename;
|
_modalFilename = filename;
|
||||||
modalTitle.textContent = filename.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, '');
|
modalTitle.textContent = filename.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, '');
|
||||||
modalBody.innerHTML = '';
|
modalBody.innerHTML = '';
|
||||||
@@ -69,6 +71,7 @@ function openModal(filename, path) {
|
|||||||
function closeModal() {
|
function closeModal() {
|
||||||
modal.classList.add('hidden');
|
modal.classList.add('hidden');
|
||||||
_modalPath = null;
|
_modalPath = null;
|
||||||
|
_modalPaths = null;
|
||||||
_modalFilename = null;
|
_modalFilename = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +79,11 @@ modalCloseBtn.addEventListener('click', closeModal);
|
|||||||
modal.querySelector('.modal-backdrop').addEventListener('click', closeModal);
|
modal.querySelector('.modal-backdrop').addEventListener('click', closeModal);
|
||||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
||||||
modalObsidianBtn.addEventListener('click', () => {
|
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', () => {
|
modalFolderBtn.addEventListener('click', () => {
|
||||||
if (_modalPath) apiFetch('/open', { method: 'POST', body: JSON.stringify({ path: _modalPath, mode: 'folder' }) });
|
if (_modalPath) apiFetch('/open', { method: 'POST', body: JSON.stringify({ path: _modalPath, mode: 'folder' }) });
|
||||||
|
|||||||
@@ -60,6 +60,25 @@ def test_slugify():
|
|||||||
assert slugify("test -- foo") == "test-foo"
|
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):
|
def test_write_meeting_docs_creates_three_files(tmp_path):
|
||||||
from output import write_meeting_docs
|
from output import write_meeting_docs
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
Reference in New Issue
Block a user