app/app.py aktualisiert
This commit is contained in:
parent
d691adec05
commit
c3d0b48254
251
app/app.py
251
app/app.py
@ -7,9 +7,8 @@ from difflib import SequenceMatcher
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# --- DATENBANK VERBINDUNG ---
|
# --- DB VERBINDUNG & RESET (Wie gehabt) ---
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
"""Stellt Verbindung zur Postgres-DB her, mit Warteschleife beim Start."""
|
|
||||||
max_retries = 10
|
max_retries = 10
|
||||||
for i in range(max_retries):
|
for i in range(max_retries):
|
||||||
try:
|
try:
|
||||||
@ -21,67 +20,35 @@ def get_db_connection():
|
|||||||
)
|
)
|
||||||
return conn
|
return conn
|
||||||
except psycopg2.OperationalError:
|
except psycopg2.OperationalError:
|
||||||
if i < max_retries - 1:
|
time.sleep(2)
|
||||||
print("Datenbank noch nicht bereit... warte 2 Sekunden.")
|
continue
|
||||||
time.sleep(2)
|
raise Exception("DB Connection failed")
|
||||||
continue
|
|
||||||
else:
|
|
||||||
print("FEHLER: Konnte keine Verbindung zur Datenbank herstellen.")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# --- RESET LOGIK (STATELESS CONTAINER) ---
|
|
||||||
def reset_db_on_startup():
|
def reset_db_on_startup():
|
||||||
"""Löscht beim Start alles und lädt die init.sql neu."""
|
|
||||||
print("Starte Datenbank-Reset...")
|
|
||||||
try:
|
try:
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Wir lesen die init.sql und führen sie aus.
|
|
||||||
# Voraussetzung: In init.sql steht am Anfang 'DROP TABLE IF EXISTS ...'
|
|
||||||
if os.path.exists('init.sql'):
|
if os.path.exists('init.sql'):
|
||||||
with open('init.sql', 'r', encoding='utf-8') as f:
|
with open('init.sql', 'r', encoding='utf-8') as f:
|
||||||
sql_commands = f.read()
|
cur.execute(f.read())
|
||||||
cur.execute(sql_commands)
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print("Datenbank erfolgreich neu aufgebaut (Clean State)!")
|
print("DB Reset erfolgreich.")
|
||||||
else:
|
|
||||||
print("WARNUNG: init.sql nicht gefunden! Datenbank könnte leer sein.")
|
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"KRITISCHER FEHLER beim DB-Reset: {e}")
|
print(f"DB Reset Fehler: {e}")
|
||||||
|
|
||||||
# Dies wird ausgeführt, sobald der Container startet (bevor der Webserver läuft)
|
|
||||||
reset_db_on_startup()
|
reset_db_on_startup()
|
||||||
|
|
||||||
|
|
||||||
# --- HILFSFUNKTIONEN ---
|
# --- HILFSFUNKTIONEN ---
|
||||||
def expand_variants(text):
|
def clean_text(text):
|
||||||
"""Zerlegt 'Wort1; Wort2' in eine Liste für den Vergleich."""
|
|
||||||
parts = text.split(';')
|
|
||||||
results = []
|
|
||||||
for p in parts:
|
|
||||||
clean = p.strip()
|
|
||||||
if clean:
|
|
||||||
results.append(clean)
|
|
||||||
|
|
||||||
# Umgang mit Klammern z.B. "(to) go" -> "to go" und "go"
|
|
||||||
final_res = []
|
|
||||||
for r in results:
|
|
||||||
final_res.append(r)
|
|
||||||
if '(' in r:
|
|
||||||
without_parens = r.replace('(', '').replace(')', '').strip()
|
|
||||||
final_res.append(without_parens)
|
|
||||||
return final_res
|
|
||||||
|
|
||||||
def clean_text_for_comparison(text):
|
|
||||||
"""Macht Text klein und entfernt Leerzeichen."""
|
|
||||||
return text.lower().strip()
|
return text.lower().strip()
|
||||||
|
|
||||||
|
def check_part(user_in, correct_str):
|
||||||
|
variants = [x.strip().lower() for x in correct_str.split('/')]
|
||||||
|
return user_in in variants
|
||||||
|
|
||||||
# --- ROUTEN ---
|
# --- API ROUTEN ---
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
@ -89,167 +56,151 @@ def index():
|
|||||||
|
|
||||||
@app.route('/api/pages')
|
@app.route('/api/pages')
|
||||||
def get_pages():
|
def get_pages():
|
||||||
"""Liefert verfügbare Buchseiten."""
|
# Wir filtern jetzt nach Typ!
|
||||||
|
mode_type = request.args.get('type', 'vocab') # 'vocab' oder 'irregular'
|
||||||
|
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Seiten aus der Vokabel-Tabelle
|
if mode_type == 'irregular':
|
||||||
cur.execute("SELECT DISTINCT page FROM vocabulary ORDER BY page")
|
cur.execute("SELECT DISTINCT page FROM irregular_verbs ORDER BY page")
|
||||||
|
else:
|
||||||
|
cur.execute("SELECT DISTINCT page FROM vocabulary ORDER BY page")
|
||||||
|
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
# Seiten aus der Verben-Tabelle (falls vorhanden)
|
|
||||||
cur.execute("SELECT DISTINCT page FROM irregular_verbs ORDER BY page")
|
|
||||||
rows_verbs = cur.fetchall()
|
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# Listen zusammenführen und sortieren
|
pages = sorted([r[0] for r in rows])
|
||||||
pages = set([r[0] for r in rows])
|
return jsonify(pages)
|
||||||
for r in rows_verbs:
|
|
||||||
pages.add(r[0])
|
|
||||||
|
|
||||||
sorted_pages = sorted(list(pages))
|
|
||||||
return jsonify(sorted_pages)
|
|
||||||
|
|
||||||
@app.route('/api/question', methods=['POST'])
|
@app.route('/api/question', methods=['POST'])
|
||||||
def get_question():
|
def get_question():
|
||||||
"""Holt eine zufällige Frage basierend auf Modus und Seiten."""
|
|
||||||
data = request.json
|
data = request.json
|
||||||
mode = data.get('mode', 'random')
|
main_mode = data.get('mainMode') # 'vocab' oder 'irregular'
|
||||||
|
sub_mode = data.get('subMode') # 'de-en', 'start-german', etc.
|
||||||
pages = data.get('pages', [])
|
pages = data.get('pages', [])
|
||||||
|
|
||||||
# Fallback: Wenn keine Seiten gewählt, nimm einfach Seite 206 (oder leer)
|
|
||||||
if not pages:
|
|
||||||
# Optional: Hier könnte man "alle" Logik einbauen,
|
|
||||||
# aber Frontend sendet meistens was.
|
|
||||||
pass
|
|
||||||
|
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# --- MODUS: UNREGELMÄSSIGE VERBEN ---
|
if main_mode == 'irregular':
|
||||||
if mode == 'irregular':
|
# Verben Logik
|
||||||
query = "SELECT infinitive, simple_past, past_participle, german, page FROM irregular_verbs WHERE page = ANY(%s) ORDER BY RANDOM() LIMIT 1"
|
query = "SELECT infinitive, simple_past, past_participle, german, page FROM irregular_verbs WHERE page = ANY(%s) ORDER BY RANDOM() LIMIT 1"
|
||||||
cur.execute(query, (pages,))
|
cur.execute(query, (pages,))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if not row:
|
if not row: return jsonify({'error': 'Keine Verben gefunden.'})
|
||||||
return jsonify({'error': 'Keine Verben auf diesen Seiten gefunden (Wähle 206 oder 207).'})
|
|
||||||
|
|
||||||
return jsonify({
|
# Entscheiden basierend auf sub_mode
|
||||||
'type': 'irregular',
|
if sub_mode == 'start-german':
|
||||||
'question': row[0], # Infinitive (z.B. "go")
|
# Frage: Deutsch -> Antwort: Alle 3 Formen
|
||||||
'german_hint': row[3], # Deutsch (z.B. "gehen")
|
return jsonify({
|
||||||
'answer_simple': row[1], # Simple Past (z.B. "went")
|
'type': 'irregular_full',
|
||||||
'answer_participle': row[2], # Past Participle (z.B. "gone")
|
'question': row[3], # Deutsch
|
||||||
'page': row[4]
|
'answer_infinitive': row[0],
|
||||||
})
|
'answer_simple': row[1],
|
||||||
|
'answer_participle': row[2],
|
||||||
|
'page': row[4]
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Frage: Infinitiv -> Antwort: Past & Participle (Standard)
|
||||||
|
return jsonify({
|
||||||
|
'type': 'irregular_standard',
|
||||||
|
'question': row[0], # Infinitive
|
||||||
|
'german_hint': row[3],
|
||||||
|
'answer_simple': row[1],
|
||||||
|
'answer_participle': row[2],
|
||||||
|
'page': row[4]
|
||||||
|
})
|
||||||
|
|
||||||
# --- MODUS: NORMALE VOKABELN ---
|
|
||||||
else:
|
else:
|
||||||
|
# Vokabel Logik
|
||||||
query = "SELECT english, german, page FROM vocabulary WHERE page = ANY(%s) ORDER BY RANDOM() LIMIT 1"
|
query = "SELECT english, german, page FROM vocabulary WHERE page = ANY(%s) ORDER BY RANDOM() LIMIT 1"
|
||||||
cur.execute(query, (pages,))
|
cur.execute(query, (pages,))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if not row:
|
if not row: return jsonify({'error': 'Keine Vokabeln gefunden.'})
|
||||||
return jsonify({'error': 'Keine Vokabeln auf den gewählten Seiten gefunden.'})
|
|
||||||
|
|
||||||
question_text = ""
|
q_text, a_text = "", ""
|
||||||
answer_text = ""
|
if sub_mode == 'de-en':
|
||||||
|
q_text, a_text = row[1], row[0]
|
||||||
if mode == 'de-en':
|
elif sub_mode == 'en-de':
|
||||||
question_text = row[1] # Deutsch
|
q_text, a_text = row[0], row[1]
|
||||||
answer_text = row[0] # Englisch
|
|
||||||
elif mode == 'en-de':
|
|
||||||
question_text = row[0] # Englisch
|
|
||||||
answer_text = row[1] # Deutsch
|
|
||||||
else: # random
|
else: # random
|
||||||
if random.random() > 0.5:
|
if random.random() > 0.5: q_text, a_text = row[1], row[0]
|
||||||
question_text = row[1]
|
else: q_text, a_text = row[0], row[1]
|
||||||
answer_text = row[0]
|
|
||||||
else:
|
|
||||||
question_text = row[0]
|
|
||||||
answer_text = row[1]
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'type': 'vocab',
|
'type': 'vocab',
|
||||||
'question': question_text,
|
'question': q_text,
|
||||||
'answer': answer_text,
|
'answer': a_text,
|
||||||
'page': row[2]
|
'page': row[2]
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route('/api/check', methods=['POST'])
|
@app.route('/api/check', methods=['POST'])
|
||||||
def check_answer():
|
def check_answer():
|
||||||
"""Prüft die Antwort des Nutzers."""
|
|
||||||
data = request.json
|
data = request.json
|
||||||
|
q_type = data.get('type')
|
||||||
|
|
||||||
# --- CHECK: UNREGELMÄSSIGE VERBEN ---
|
# 1. Verben: Alles abfragen (Start: Deutsch)
|
||||||
if data.get('type') == 'irregular':
|
if q_type == 'irregular_full':
|
||||||
user_simple = clean_text_for_comparison(data.get('input_simple', ''))
|
u_inf = clean_text(data.get('infinitive', ''))
|
||||||
user_participle = clean_text_for_comparison(data.get('input_participle', ''))
|
u_simp = clean_text(data.get('simple', ''))
|
||||||
|
u_part = clean_text(data.get('participle', ''))
|
||||||
|
|
||||||
correct_simple = data.get('correct_simple', '')
|
ok_inf = check_part(u_inf, data.get('correct_infinitive', ''))
|
||||||
correct_participle = data.get('correct_participle', '')
|
ok_simp = check_part(u_simp, data.get('correct_simple', ''))
|
||||||
|
ok_part = check_part(u_part, data.get('correct_participle', ''))
|
||||||
|
|
||||||
# Hilfsfunktion: Prüft ob Eingabe in "was/were" enthalten ist
|
if ok_inf and ok_simp and ok_part:
|
||||||
def check_part(user_in, correct_str):
|
return jsonify({'status': 'correct', 'msg': 'Perfekt! Alles richtig.'})
|
||||||
# correct_str könnte "was/were" oder "burnt/burned" sein
|
|
||||||
variants = [x.strip().lower() for x in correct_str.split('/')]
|
|
||||||
return user_in in variants
|
|
||||||
|
|
||||||
is_simple_ok = check_part(user_simple, correct_simple)
|
|
||||||
is_participle_ok = check_part(user_participle, correct_participle)
|
|
||||||
|
|
||||||
if is_simple_ok and is_participle_ok:
|
|
||||||
return jsonify({'status': 'correct', 'msg': 'Perfekt! Beide Formen richtig.'})
|
|
||||||
else:
|
else:
|
||||||
# Detailliertes Feedback, was falsch war
|
sol = f"{data['correct_infinitive']} -> {data['correct_simple']} -> {data['correct_participle']}"
|
||||||
msg = "Leider falsch."
|
return jsonify({'status': 'wrong', 'msg': f'Leider falsch. Lösung: {sol}'})
|
||||||
if not is_simple_ok: msg += f" Simple Past war '{user_simple}' (Richtig: {correct_simple})."
|
|
||||||
if not is_participle_ok: msg += f" Participle war '{user_participle}' (Richtig: {correct_participle})."
|
|
||||||
|
|
||||||
return jsonify({
|
# 2. Verben: Standard (Start: Infinitiv)
|
||||||
'status': 'wrong',
|
elif q_type == 'irregular_standard':
|
||||||
'msg': f'Lösung: {correct_simple} | {correct_participle}'
|
u_simp = clean_text(data.get('simple', ''))
|
||||||
})
|
u_part = clean_text(data.get('participle', ''))
|
||||||
|
|
||||||
# --- CHECK: NORMALE VOKABELN ---
|
ok_simp = check_part(u_simp, data.get('correct_simple', ''))
|
||||||
|
ok_part = check_part(u_part, data.get('correct_participle', ''))
|
||||||
|
|
||||||
|
if ok_simp and ok_part:
|
||||||
|
return jsonify({'status': 'correct', 'msg': 'Richtig!'})
|
||||||
|
else:
|
||||||
|
sol = f"{data['correct_simple']} -> {data['correct_participle']}"
|
||||||
|
return jsonify({'status': 'wrong', 'msg': f'Lösung: {sol}'})
|
||||||
|
|
||||||
|
# 3. Vokabeln
|
||||||
else:
|
else:
|
||||||
user_input = data.get('input', '')
|
u_input = clean_text(data.get('input', ''))
|
||||||
db_answer = data.get('correct', '')
|
correct_raw = data.get('correct', '')
|
||||||
|
|
||||||
clean_user = clean_text_for_comparison(user_input)
|
# Split variants (e.g. "car; auto")
|
||||||
valid_answers_raw = expand_variants(db_answer)
|
valid_answers = [clean_text(x) for x in correct_raw.split(';')]
|
||||||
valid_answers_clean = [clean_text_for_comparison(v) for v in valid_answers_raw]
|
# Add version without brackets "(to) go" -> "go"
|
||||||
|
for v in list(valid_answers):
|
||||||
|
if '(' in v: valid_answers.append(v.replace('(','').replace(')','').strip())
|
||||||
|
|
||||||
if clean_user in valid_answers_clean:
|
if u_input in valid_answers:
|
||||||
return jsonify({'status': 'correct', 'msg': 'Richtig!'})
|
return jsonify({'status': 'correct', 'msg': 'Richtig!'})
|
||||||
|
|
||||||
# Fuzzy Logic (Tippfehler erkennen)
|
# Fuzzy Check
|
||||||
best_ratio = 0
|
best_ratio = 0
|
||||||
best_match = ""
|
for v in valid_answers:
|
||||||
for correct_option in valid_answers_clean:
|
ratio = SequenceMatcher(None, u_input, v).ratio()
|
||||||
ratio = SequenceMatcher(None, clean_user, correct_option).ratio()
|
if ratio > best_ratio: best_ratio = ratio
|
||||||
if ratio > best_ratio:
|
|
||||||
best_ratio = ratio
|
|
||||||
best_match = correct_option
|
|
||||||
|
|
||||||
threshold = 0.85
|
if best_ratio >= 0.8:
|
||||||
if len(best_match) <= 4:
|
return jsonify({'status': 'typo', 'msg': f'Fast richtig! Lösung: {correct_raw}'})
|
||||||
threshold = 0.9 # Bei kurzen Wörtern strenger sein
|
|
||||||
|
|
||||||
if best_ratio >= threshold:
|
return jsonify({'status': 'wrong', 'msg': f'Falsch. Lösung: {correct_raw}'})
|
||||||
return jsonify({
|
|
||||||
'status': 'typo',
|
|
||||||
'msg': f'Fast richtig! Meintest du: "{best_match}"? (Es war "{user_input}")'
|
|
||||||
})
|
|
||||||
|
|
||||||
return jsonify({'status': 'wrong', 'msg': f'Falsch. Lösung: {db_answer}'})
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000)
|
app.run(host='0.0.0.0', port=5000)
|
||||||
Loading…
x
Reference in New Issue
Block a user