Füge nvWindow.py hinzu
This commit is contained in:
parent
5836423a65
commit
97296cf649
164
mapper_gui.py
Normal file
164
mapper_gui.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import tkinter as tk
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import pandas as pd
|
||||||
|
import re
|
||||||
|
import spacy
|
||||||
|
from rapidfuzz import fuzz
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
TMP_DIR = os.path.join(BASE_DIR, "tmp")
|
||||||
|
ACTIVE_FILE = os.path.join(TMP_DIR, "active_term.json")
|
||||||
|
CACHE_FILE = os.path.join(BASE_DIR, "mapper_cache_2.3.json")
|
||||||
|
NV_MASTER_PATH = os.path.join(BASE_DIR, "NV_MASTER.ods")
|
||||||
|
STOPWORDS = {"mit","ohne","der","die","das","ein","eine","und","zu","von","im","in","auf","an","als","bei","für","aus","dem","den","des","eines","einer"}
|
||||||
|
CONF_THRESHOLD = 0.75
|
||||||
|
MAX_SUGGESTIONS = 20
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Logging
|
||||||
|
# -------------------------
|
||||||
|
def log(msg):
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# NV_MASTER laden
|
||||||
|
# -------------------------
|
||||||
|
def normalize_text(s):
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
s = str(s).strip().lower()
|
||||||
|
s = re.sub(r"[\(\)\[\]\"'\\;:\?!,\.]", "", s)
|
||||||
|
s = re.sub(r"\s+", " ", s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
nlp = spacy.load("de_core_news_sm")
|
||||||
|
|
||||||
|
lemma_cache = {}
|
||||||
|
def lemmatize_term(term):
|
||||||
|
term_norm = normalize_text(term)
|
||||||
|
if term_norm in lemma_cache:
|
||||||
|
return lemma_cache[term_norm]
|
||||||
|
doc = nlp(term_norm)
|
||||||
|
lemma = " ".join([token.lemma_ for token in doc])
|
||||||
|
lemma_cache[term_norm] = lemma
|
||||||
|
return lemma
|
||||||
|
|
||||||
|
def build_norm_index(nv_path):
|
||||||
|
norm_dict = {}
|
||||||
|
lemma_index = {}
|
||||||
|
sheets = pd.read_excel(nv_path, sheet_name=None, engine="odf")
|
||||||
|
for sheet_name, df in sheets.items():
|
||||||
|
if str(sheet_name).strip().lower() == "master":
|
||||||
|
continue
|
||||||
|
df = df.fillna("")
|
||||||
|
cols = [str(c).strip().lower() for c in df.columns]
|
||||||
|
id_col = None
|
||||||
|
word_col = None
|
||||||
|
for i, c in enumerate(cols):
|
||||||
|
if "id" in c:
|
||||||
|
id_col = df.columns[i]
|
||||||
|
if "wort" in c or "vokabel" in c:
|
||||||
|
word_col = df.columns[i]
|
||||||
|
if word_col is None and len(df.columns) >= 1:
|
||||||
|
word_col = df.columns[-1]
|
||||||
|
if id_col is None and len(df.columns) >= 1:
|
||||||
|
id_col = df.columns[0]
|
||||||
|
current_parent_id = None
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
id_val = str(row[id_col]).strip() if id_col in df.columns else ""
|
||||||
|
word_val = str(row[word_col]).strip() if word_col in df.columns else ""
|
||||||
|
if id_val:
|
||||||
|
current_parent_id = id_val
|
||||||
|
if not word_val:
|
||||||
|
continue
|
||||||
|
norm_name = normalize_text(word_val)
|
||||||
|
lemma = lemmatize_term(word_val)
|
||||||
|
entry = {"Name": word_val.strip(), "ID": current_parent_id or "", "Sheet": sheet_name}
|
||||||
|
norm_dict.setdefault(norm_name, []).append(entry)
|
||||||
|
lemma_index.setdefault(lemma, []).append(entry)
|
||||||
|
log(f"NV_MASTER geladen. Begriffe: {sum(len(v) for v in norm_dict.values())}")
|
||||||
|
return norm_dict, lemma_index
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Matching
|
||||||
|
# -------------------------
|
||||||
|
def fuzzy_score(a, b):
|
||||||
|
return fuzz.token_set_ratio(a, b)/100.0
|
||||||
|
|
||||||
|
def get_suggestions(term, norm_dict, lemma_index, threshold=CONF_THRESHOLD):
|
||||||
|
term_norm = normalize_text(term)
|
||||||
|
term_lemma = lemmatize_term(term)
|
||||||
|
candidates = []
|
||||||
|
for key_lemma, entries in lemma_index.items():
|
||||||
|
score = fuzzy_score(term_lemma, key_lemma)
|
||||||
|
if key_lemma.startswith(term_lemma):
|
||||||
|
score = min(score + 0.1, 1.0)
|
||||||
|
if score >= threshold:
|
||||||
|
for e in entries:
|
||||||
|
candidates.append((score, e["Name"], e["ID"]))
|
||||||
|
for norm_key, entries in norm_dict.items():
|
||||||
|
score = fuzzy_score(term_lemma, norm_key)
|
||||||
|
if norm_key.startswith(term_lemma):
|
||||||
|
score = min(score + 0.1, 1.0)
|
||||||
|
if score >= threshold:
|
||||||
|
for e in entries:
|
||||||
|
candidates.append((score, e["Name"], e["ID"]))
|
||||||
|
candidates.sort(key=lambda t: t[0], reverse=True)
|
||||||
|
seen = set()
|
||||||
|
results = []
|
||||||
|
for score, name, id_ in candidates:
|
||||||
|
key = (name, id_)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
results.append(f"{name} ({id_})" if id_ else name)
|
||||||
|
if len(results) >= MAX_SUGGESTIONS:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# GUI
|
||||||
|
# -------------------------
|
||||||
|
class SuggestionPopup(tk.Tk):
|
||||||
|
def __init__(self, norm_dict, lemma_index):
|
||||||
|
super().__init__()
|
||||||
|
self.norm_dict = norm_dict
|
||||||
|
self.lemma_index = lemma_index
|
||||||
|
self.geometry("+1000+700") # unten rechts
|
||||||
|
self.overrideredirect(True)
|
||||||
|
self.configure(bg="white")
|
||||||
|
self.label = tk.Label(self, text="", justify="left", bg="white", anchor="nw")
|
||||||
|
self.label.pack(padx=5, pady=5)
|
||||||
|
self.last_term = None
|
||||||
|
self.check_loop()
|
||||||
|
|
||||||
|
def check_loop(self):
|
||||||
|
term = None
|
||||||
|
try:
|
||||||
|
if os.path.exists(ACTIVE_FILE):
|
||||||
|
with open(ACTIVE_FILE, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
term = data.get("term")
|
||||||
|
except Exception:
|
||||||
|
term = None
|
||||||
|
if term != self.last_term:
|
||||||
|
self.last_term = term
|
||||||
|
if term:
|
||||||
|
suggestions = get_suggestions(term, self.norm_dict, self.lemma_index)
|
||||||
|
self.label.config(text="\n".join(suggestions))
|
||||||
|
self.deiconify()
|
||||||
|
else:
|
||||||
|
self.withdraw()
|
||||||
|
self.after(300, self.check_loop) # alle 300ms prüfen
|
||||||
|
|
||||||
|
def main():
|
||||||
|
norm_dict, lemma_index = build_norm_index(NV_MASTER_PATH)
|
||||||
|
app = SuggestionPopup(norm_dict, lemma_index)
|
||||||
|
app.mainloop()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
66
mapper_macro_listener.py
Normal file
66
mapper_macro_listener.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
BASE_DIR = "/home/jarnold/.config/libreoffice/4/user/Scripts/python/Vokabular_Abgleich_Makro"
|
||||||
|
TMP_DIR = os.path.join(BASE_DIR, "tmp")
|
||||||
|
ACTIVE_FILE = os.path.join(TMP_DIR, "active_term.json")
|
||||||
|
|
||||||
|
def write_active_term(term):
|
||||||
|
os.makedirs(TMP_DIR, exist_ok=True)
|
||||||
|
if term:
|
||||||
|
with open(ACTIVE_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"term": term}, f, ensure_ascii=False)
|
||||||
|
else:
|
||||||
|
if os.path.exists(ACTIVE_FILE):
|
||||||
|
os.remove(ACTIVE_FILE)
|
||||||
|
|
||||||
|
def poll_objektbeschreibung(doc, interval=0.5):
|
||||||
|
"""Polling-Funktion für aktive Zelle"""
|
||||||
|
sheet = doc.CurrentController.ActiveSheet
|
||||||
|
|
||||||
|
# Spalte Objektbeschreibung finden
|
||||||
|
objekt_col = None
|
||||||
|
cursor = sheet.createCursor()
|
||||||
|
cursor.gotoStartOfUsedArea(False)
|
||||||
|
cursor.gotoEndOfUsedArea(True)
|
||||||
|
for r in range(0, min(5, cursor.RangeAddress.EndRow+1)):
|
||||||
|
for c in range(0, cursor.RangeAddress.EndColumn+1):
|
||||||
|
val = str(sheet.getCellByPosition(c,r).String).strip().lower()
|
||||||
|
if val == "objektbeschreibung":
|
||||||
|
objekt_col = c
|
||||||
|
break
|
||||||
|
if objekt_col is not None:
|
||||||
|
break
|
||||||
|
if objekt_col is None:
|
||||||
|
return # Spalte nicht gefunden
|
||||||
|
|
||||||
|
last_row = None
|
||||||
|
last_value = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
controller = doc.CurrentController
|
||||||
|
cell = controller.getActiveCell()
|
||||||
|
if cell.CellAddress.Column == objekt_col:
|
||||||
|
term = str(cell.String).strip()
|
||||||
|
if term != last_value or cell.CellAddress.Row != last_row:
|
||||||
|
write_active_term(term)
|
||||||
|
last_value = term
|
||||||
|
last_row = cell.CellAddress.Row
|
||||||
|
else:
|
||||||
|
if last_value is not None:
|
||||||
|
write_active_term(None)
|
||||||
|
last_value = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
def start_polling():
|
||||||
|
doc = XSCRIPTCONTEXT.getDocument()
|
||||||
|
# Polling in Hintergrund-Thread
|
||||||
|
threading.Thread(target=poll_objektbeschreibung, args=(doc,), daemon=True).start()
|
||||||
|
|
||||||
|
g_exportedScripts = (start_polling,)
|
||||||
235
nvWindow.py
Normal file
235
nvWindow.py
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
===============================================================================
|
||||||
|
nvWindow.py – Tkinter-Vorschlagsfenster für NV_MASTER-Abgleich (LibreOffice)
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
Zweck:
|
||||||
|
Dieses Skript öffnet ein Tkinter-Fenster, das Live-Vorschläge zu Begriffen
|
||||||
|
aus der Datei NV_MASTER.ods anzeigt. Es ist so ausgelegt, dass es direkt
|
||||||
|
mit LibreOffice Calc über UNO kommunizieren kann – sofern möglich.
|
||||||
|
|
||||||
|
Robustheit:
|
||||||
|
Das Skript erkennt automatisch, ob UNO verfügbar und kompatibel ist:
|
||||||
|
- wenn ja → direkte Integration mit LibreOffice (bidirektional)
|
||||||
|
- wenn nein → Fallback in isolierten Modus, aber weiterhin lauffähig
|
||||||
|
Alle Ereignisse, Fehler und Systemzustände werden geloggt.
|
||||||
|
|
||||||
|
Start LibreOffice (empfohlen):
|
||||||
|
soffice --calc --accept="socket,host=localhost,port=2002;urp;" --nologo --norestore &
|
||||||
|
|
||||||
|
Start der Anwendung:
|
||||||
|
/usr/lib/libreoffice/program/python3 ~/projects/nvWindow/nvWindow.py
|
||||||
|
(oder notfalls:) python3 ~/projects/nvWindow/nvWindow.py
|
||||||
|
|
||||||
|
Abhängigkeiten:
|
||||||
|
pip install rapidfuzz odfpy
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import tkinter as tk
|
||||||
|
from datetime import datetime
|
||||||
|
from rapidfuzz import process, fuzz
|
||||||
|
from odf.opendocument import load
|
||||||
|
from odf.table import Table, TableRow, TableCell
|
||||||
|
from odf.text import P
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 1. Verzeichnis-Setup und Logging
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
BASE_DIR = os.path.expanduser("~/projects/nvWindow")
|
||||||
|
LOG_FILE = os.path.join(BASE_DIR, "nvWindow.log")
|
||||||
|
NV_MASTER_PATH = os.path.join(BASE_DIR, "NV_MASTER.ods")
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
"""Schreibt Zeitstempel und Nachricht in die Logdatei."""
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
try:
|
||||||
|
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"[{timestamp}] {msg}\n")
|
||||||
|
except Exception:
|
||||||
|
# im absoluten Notfall auf stdout ausweichen
|
||||||
|
print(f"[{timestamp}] {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 2. UNO-Initialisierung (robust, mit Fallback)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
uno_available = False
|
||||||
|
desktop = None
|
||||||
|
|
||||||
|
def init_uno():
|
||||||
|
"""Versucht UNO-Bridge zu initialisieren, erkennt Systemumgebung automatisch."""
|
||||||
|
global uno_available, desktop
|
||||||
|
|
||||||
|
# bereits aktiv in LibreOffice? (XSCRIPTCONTEXT vorhanden)
|
||||||
|
if "XSCRIPTCONTEXT" in globals():
|
||||||
|
try:
|
||||||
|
ctx = XSCRIPTCONTEXT.getComponentContext()
|
||||||
|
smgr = ctx.ServiceManager
|
||||||
|
desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
|
||||||
|
uno_available = True
|
||||||
|
log("UNO erkannt: Skript läuft innerhalb von LibreOffice.")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
log(f"UNO innerhalb LO-Session nicht initialisierbar: {e}")
|
||||||
|
uno_available = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# außerhalb LibreOffice: versuche Systemintegration
|
||||||
|
try:
|
||||||
|
sys.path.append("/usr/lib/python3/dist-packages")
|
||||||
|
sys.path.append("/usr/lib/libreoffice/program")
|
||||||
|
os.environ["URE_BOOTSTRAP"] = (
|
||||||
|
"vnd.sun.star.pathname:/usr/lib/libreoffice/program/fundamentalrc"
|
||||||
|
)
|
||||||
|
|
||||||
|
import uno
|
||||||
|
from com.sun.star.beans import PropertyValue
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx = uno.getComponentContext()
|
||||||
|
smgr = ctx.ServiceManager
|
||||||
|
desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
|
||||||
|
uno_available = True
|
||||||
|
log("UNO erfolgreich initialisiert – externe Verbindung aktiv.")
|
||||||
|
except Exception as inner:
|
||||||
|
log(f"UNO verfügbar, aber keine Desktop-Instanz: {inner}")
|
||||||
|
uno_available = False
|
||||||
|
except Exception as outer:
|
||||||
|
log(f"UNO-Import fehlgeschlagen: {outer}\n{traceback.format_exc()}")
|
||||||
|
uno_available = False
|
||||||
|
|
||||||
|
|
||||||
|
init_uno()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 3. NV_MASTER laden
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def load_nv_master(filepath):
|
||||||
|
"""Liest NV_MASTER.ods mit odfpy ein, extrahiert alle Textinhalte."""
|
||||||
|
entries = []
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
log(f"NV_MASTER.ods nicht gefunden: {filepath}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = load(filepath)
|
||||||
|
for table in doc.getElementsByType(Table):
|
||||||
|
for row in table.getElementsByType(TableRow):
|
||||||
|
cells = row.getElementsByType(TableCell)
|
||||||
|
if not cells:
|
||||||
|
continue
|
||||||
|
cell_text = []
|
||||||
|
for c in cells:
|
||||||
|
ps = c.getElementsByType(P)
|
||||||
|
for p in ps:
|
||||||
|
if p.firstChild:
|
||||||
|
text = str(p.firstChild.data).strip()
|
||||||
|
if text:
|
||||||
|
cell_text.append(text)
|
||||||
|
if cell_text:
|
||||||
|
entries.append(" ".join(cell_text))
|
||||||
|
log(f"NV_MASTER geladen – {len(entries)} Begriffe erkannt.")
|
||||||
|
return entries
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Fehler beim Lesen NV_MASTER: {e}\n{traceback.format_exc()}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
NV_MASTER_LIST = load_nv_master(NV_MASTER_PATH)
|
||||||
|
|
||||||
|
if not NV_MASTER_LIST:
|
||||||
|
log("Abbruch: NV_MASTER.ods konnte nicht gelesen werden oder war leer.")
|
||||||
|
sys.exit("NV_MASTER.ods fehlt oder enthält keine Daten.")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 4. Tkinter-GUI
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("Objektbeschreibung – Vorschläge")
|
||||||
|
root.attributes("-topmost", True)
|
||||||
|
root.geometry("420x260+1500+800")
|
||||||
|
root.configure(bg="#202020")
|
||||||
|
|
||||||
|
label = tk.Label(
|
||||||
|
root,
|
||||||
|
text="Live-Vorschläge für Objektbeschreibung:",
|
||||||
|
fg="white",
|
||||||
|
bg="#202020",
|
||||||
|
font=("Arial", 10, "bold"),
|
||||||
|
)
|
||||||
|
label.pack(pady=(10, 5))
|
||||||
|
|
||||||
|
entry = tk.Entry(root, width=52, font=("Arial", 11))
|
||||||
|
entry.pack(pady=(0, 10))
|
||||||
|
|
||||||
|
listbox = tk.Listbox(root, width=52, height=9, font=("Arial", 10))
|
||||||
|
listbox.pack(pady=5)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 5. Matching + UNO-Kommunikation
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def update_suggestions(event=None):
|
||||||
|
"""Aktualisiert Vorschläge bei jeder Tasteneingabe."""
|
||||||
|
query = entry.get().strip()
|
||||||
|
listbox.delete(0, tk.END)
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
matches = process.extract(query, NV_MASTER_LIST, scorer=fuzz.token_set_ratio, limit=8)
|
||||||
|
for m in matches:
|
||||||
|
item, score, _ = m
|
||||||
|
listbox.insert(tk.END, f"{item} ({score:.1f}%)")
|
||||||
|
|
||||||
|
if uno_available and desktop:
|
||||||
|
try:
|
||||||
|
doc = desktop.getCurrentComponent()
|
||||||
|
if not doc:
|
||||||
|
log("UNO: Kein aktives Dokument.")
|
||||||
|
return
|
||||||
|
sheet = doc.CurrentController.ActiveSheet
|
||||||
|
cell = sheet.getCurrentSelection()
|
||||||
|
if cell:
|
||||||
|
cell.String = query
|
||||||
|
except Exception as e:
|
||||||
|
log(f"UNO-Update-Fehler: {e}")
|
||||||
|
log(f"Suchanfrage '{query}' – {len(matches)} Vorschläge generiert.")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Fehler in update_suggestions(): {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
|
|
||||||
|
entry.bind("<KeyRelease>", update_suggestions)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 6. Laufzeit / Beenden
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
log("nvWindow gestartet. Tkinter-Fenster aktiv.")
|
||||||
|
print("nvWindow läuft – Details siehe nvWindow.log")
|
||||||
|
|
||||||
|
try:
|
||||||
|
root.mainloop()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log("Manuell abgebrochen.")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Fehler in mainloop(): {e}\n{traceback.format_exc()}")
|
||||||
|
finally:
|
||||||
|
log("nvWindow beendet.")
|
||||||
Loading…
x
Reference in New Issue
Block a user