Skip to content

Commit 233251d

Browse files
authored
Improved LTL to English translation (#119)
* I think smoother translation, added engtoltl endpoint * I think this is better?
1 parent f48383e commit 233251d

File tree

8 files changed

+308
-126
lines changed

8 files changed

+308
-126
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ zipp
3333
psycopg2-binary
3434
flask-login
3535
inflect
36-
36+
wordfreq>=3.0

src/app.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from flask import Flask, render_template, request, Blueprint
2-
from flask_login import login_required, current_user
3-
4-
5-
from ltlnode import parse_ltl_string, SUPPORTED_SYNTAXES
6-
from codebook import getAllApplicableMisconceptions
1+
from flask import Flask, render_template, request, Blueprint, jsonify
2+
from flask_login import login_required, current_user
3+
4+
5+
from ltlnode import parse_ltl_string, SUPPORTED_SYNTAXES
6+
from codebook import getAllApplicableMisconceptions
77
import os
88
import json
99
import sys
@@ -19,6 +19,7 @@
1919
import uuid
2020
import requests
2121
from stepper import traceSatisfactionPerStep
22+
import ltltoeng
2223
from authroutes import (
2324
authroutes,
2425
init_app,
@@ -170,12 +171,54 @@ def ltl():
170171
return render_template('ltl.html', uid = getUserName())
171172

172173
@app.route('/loadfromjson')
173-
def loadfromjson():
174-
return render_template('loadfromjson.html', uid = getUserName())
175-
176-
@app.route('/authorquestion/', methods=['POST'])
177-
@login_required
178-
def authorquestion():
174+
def loadfromjson():
175+
return render_template('loadfromjson.html', uid = getUserName())
176+
177+
@app.route('/ltltoeng', methods=['GET'])
178+
def ltl_to_english():
179+
"""Lightweight endpoint to translate an LTL formula to English for testing."""
180+
formula = request.args.get('formula', '').strip()
181+
if not formula:
182+
return jsonify({"error": "Missing required query parameter 'formula'."}), 400
183+
try:
184+
node = parse_ltl_string(formula)
185+
english = ltltoeng.finalize_sentence(node.__to_english__())
186+
except Exception as e:
187+
return jsonify({"error": "Failed to translate formula.", "details": str(e)}), 400
188+
189+
return jsonify({"formula": formula, "english": english})
190+
191+
192+
@app.route('/ltltoeng/ui', methods=['GET', 'POST'])
193+
@login_required
194+
def ltl_to_english_ui():
195+
"""Simple HTML interface to translate an LTL formula to English."""
196+
translation = None
197+
error = None
198+
input_formula = ""
199+
200+
if request.method == 'POST':
201+
input_formula = request.form.get('formula', '').strip()
202+
if not input_formula:
203+
error = "Please enter an LTL formula."
204+
else:
205+
try:
206+
node = parse_ltl_string(input_formula)
207+
translation = ltltoeng.finalize_sentence(node.__to_english__())
208+
except Exception as e:
209+
error = f"Failed to translate formula: {e}"
210+
211+
return render_template(
212+
'ltltoengui.html',
213+
uid=getUserName(),
214+
translation=translation,
215+
error=error,
216+
input_formula=input_formula,
217+
)
218+
219+
@app.route('/authorquestion/', methods=['POST'])
220+
@login_required
221+
def authorquestion():
179222

180223
kind = request.form.get('kind')
181224
answer = request.form.get('answer')

src/exercisebuilder.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,8 @@ def gen_nl_question(self, formula):
436436

437437
formula_eng_corrected = ltltoeng.correct_grammar(formula_eng)
438438
### If there are multiple '.' in a row, replace with a single '.'
439-
formula_eng_corrected = re.sub(r'\.{2,}', '.', formula_eng)
440-
return formula_eng_corrected
439+
formula_eng_corrected = re.sub(r'\.{2,}', '.', formula_eng_corrected)
440+
return ltltoeng.finalize_sentence(formula_eng_corrected)
441441

442442

443443
def get_options_with_misconceptions_as_formula(self, answer):
@@ -695,4 +695,4 @@ def _get_trend_label(self, trend_score):
695695
elif trend_score < 0.5:
696696
return "Needs attention"
697697
else:
698-
return "Needs focus"
698+
return "Needs focus"

src/ltlnode.py

Lines changed: 94 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,13 @@ class UntilNode(BinaryOperatorNode):
217217
def __init__(self, left, right):
218218
super().__init__(UntilNode.symbol, left, right)
219219

220-
def __to_english__(self):
221-
x = ltltoeng.apply_special_pattern_if_possible(self)
222-
if x is not None:
223-
return x
224-
lhs = self.left.__to_english__().rstrip('.')
225-
rhs = self.right.__to_english__().rstrip('.')
226-
return ltltoeng.capitalize_sentence(f"{lhs} until {rhs}")
220+
def __to_english__(self):
221+
x = ltltoeng.apply_special_pattern_if_possible(self)
222+
if x is not None:
223+
return x
224+
lhs = self.left.__to_english__().rstrip('.')
225+
rhs = self.right.__to_english__().rstrip('.')
226+
return f"{lhs} until {rhs}"
227227

228228
def __forge__(self):
229229
return f"({self.left.__forge__()} UNTIL {self.right.__forge__()})"
@@ -237,12 +237,12 @@ class NextNode(UnaryOperatorNode):
237237
def __init__(self, operand):
238238
super().__init__(NextNode.symbol, operand)
239239

240-
def __to_english__(self):
241-
x = ltltoeng.apply_special_pattern_if_possible(self)
242-
if x is not None:
243-
return x
244-
op = self.operand.__to_english__().rstrip('.')
245-
return ltltoeng.capitalize_sentence(f"in the next step, {op}")
240+
def __to_english__(self):
241+
x = ltltoeng.apply_special_pattern_if_possible(self)
242+
if x is not None:
243+
return x
244+
op = self.operand.__to_english__().rstrip('.')
245+
return f"in the next step, {op}"
246246

247247
def __forge__(self):
248248
return f"(NEXT_STATE {self.operand.__forge__()})"
@@ -256,20 +256,20 @@ class GloballyNode(UnaryOperatorNode):
256256
def __init__(self, operand):
257257
super().__init__(GloballyNode.symbol, operand)
258258

259-
def __to_english__(self):
260-
x = ltltoeng.apply_special_pattern_if_possible(self)
261-
if x is not None:
262-
return x
263-
264-
op = self.operand.__to_english__().rstrip('.')
265-
patterns = [
266-
f"it is always the case that {op}",
267-
f"at all times, {op}",
268-
f"{op} is always true"
269-
]
270-
271-
english = random.choice(patterns)
272-
return ltltoeng.capitalize_sentence(english)
259+
def __to_english__(self):
260+
x = ltltoeng.apply_special_pattern_if_possible(self)
261+
if x is not None:
262+
return x
263+
264+
op = self.operand.__to_english__().rstrip('.')
265+
patterns = [
266+
f"it is always the case that {op}",
267+
f"at all times, {op}",
268+
f"{op} is always true"
269+
]
270+
271+
english = ltltoeng.choose_best_sentence(patterns)
272+
return english
273273

274274
def __forge__(self):
275275
return f"(ALWAYS {self.operand.__forge__()})"
@@ -287,10 +287,10 @@ def __to_english__(self):
287287
x = ltltoeng.apply_special_pattern_if_possible(self)
288288
if x is not None:
289289
return x
290-
op = self.operand.__to_english__().rstrip('.')
291-
292-
english = f"eventually, {op}"
293-
return ltltoeng.capitalize_sentence(english)
290+
op = self.operand.__to_english__().rstrip('.')
291+
292+
english = f"eventually, {op}"
293+
return english
294294

295295
def __forge__(self):
296296
return f"(EVENTUALLY {self.operand.__forge__()})"
@@ -306,13 +306,13 @@ class OrNode(BinaryOperatorNode):
306306
def __init__(self, left, right):
307307
super().__init__(OrNode.symbol, left, right)
308308

309-
def __to_english__(self):
310-
x = ltltoeng.apply_special_pattern_if_possible(self)
311-
if x is not None:
312-
return x
313-
lhs = self.left.__to_english__().rstrip('.')
314-
rhs = self.right.__to_english__().rstrip('.')
315-
return ltltoeng.capitalize_sentence(f"either {lhs} or {rhs}")
309+
def __to_english__(self):
310+
x = ltltoeng.apply_special_pattern_if_possible(self)
311+
if x is not None:
312+
return x
313+
lhs = self.left.__to_english__().rstrip('.')
314+
rhs = self.right.__to_english__().rstrip('.')
315+
return f"either {lhs} or {rhs}"
316316

317317

318318

@@ -322,13 +322,13 @@ class AndNode(BinaryOperatorNode):
322322
def __init__(self, left, right):
323323
super().__init__(AndNode.symbol, left, right)
324324

325-
def __to_english__(self):
326-
x = ltltoeng.apply_special_pattern_if_possible(self)
327-
if x is not None:
328-
return x
329-
lhs = self.left.__to_english__().rstrip('.')
330-
rhs = self.right.__to_english__().rstrip('.')
331-
return ltltoeng.capitalize_sentence(f"both {lhs} and {rhs}")
325+
def __to_english__(self):
326+
x = ltltoeng.apply_special_pattern_if_possible(self)
327+
if x is not None:
328+
return x
329+
lhs = self.left.__to_english__().rstrip('.')
330+
rhs = self.right.__to_english__().rstrip('.')
331+
return f"both {lhs} and {rhs}"
332332

333333

334334
class NotNode(UnaryOperatorNode):
@@ -338,64 +338,70 @@ def __init__(self, operand):
338338

339339
def __to_english__(self):
340340
x = ltltoeng.apply_special_pattern_if_possible(self)
341-
if x is not None:
342-
return x
343-
344-
op = self.operand.__to_english__().rstrip('.')
345-
346-
## If the operand is a literal, we can just negate it
347-
if isinstance(self.operand, LiteralNode):
348-
return ltltoeng.capitalize_sentence(f"not {op}")
349-
else:
350-
return ltltoeng.capitalize_sentence(f"it is not the case that {op}")
341+
if x is not None:
342+
return x
343+
344+
op = self.operand.__to_english__().rstrip('.')
345+
346+
## If the operand is a literal, we can just negate it
347+
if isinstance(self.operand, LiteralNode):
348+
return f"not {op}"
349+
else:
350+
return f"it is not the case that {op}"
351351

352352
class ImpliesNode(BinaryOperatorNode):
353353
symbol = IMPLIES_SYMBOL
354354
def __init__(self, left, right):
355355
super().__init__(ImpliesNode.symbol, left, right)
356356

357-
def __to_english__(self):
358-
x = ltltoeng.apply_special_pattern_if_possible(self)
359-
if x is not None:
360-
return x
361-
362-
lhs = self.left.__to_english__().rstrip('.')
363-
rhs = self.right.__to_english__().rstrip('.')
364-
365-
# Potential patterns:
366-
patterns = [
367-
f"if {lhs}, then {rhs}",
368-
f"{lhs} implies {rhs}",
357+
def __to_english__(self):
358+
x = ltltoeng.apply_special_pattern_if_possible(self)
359+
if x is not None:
360+
return x
361+
362+
lhs = self.left.__to_english__().rstrip('.')
363+
rhs = self.right.__to_english__().rstrip('.')
364+
365+
lhs = ltltoeng.normalize_embedded_clause(lhs)
366+
rhs = ltltoeng.normalize_embedded_clause(rhs)
367+
368+
# Potential patterns:
369+
patterns = [
370+
f"if {lhs}, then {rhs}",
371+
f"{lhs} implies {rhs}",
369372
f"whenever {lhs}, then {rhs}"
370-
]
371-
372-
# Choose a pattern randomly, and then return the corrected sentence
373-
english = random.choice(patterns)
374-
return ltltoeng.capitalize_sentence(english)
373+
]
374+
375+
# Choose the most fluent pattern rather than picking randomly
376+
english = ltltoeng.choose_best_sentence(patterns)
377+
return english
375378

376379

377380
class EquivalenceNode(BinaryOperatorNode):
378381
symbol = EQUIVALENCE_SYMBOL
379382
def __init__(self, left, right):
380383
super().__init__(EquivalenceNode.symbol, left, right)
381384

382-
def __to_english__(self):
383-
x = ltltoeng.apply_special_pattern_if_possible(self)
384-
if x is not None:
385-
return x
386-
lhs = self.left.__to_english__().rstrip('.')
387-
rhs = self.right.__to_english__().rstrip('.')
388-
389-
# Potential patterns:
390-
patterns = [
391-
f"{lhs} if and only if {rhs}",
392-
f"{lhs} exactly when {rhs}",
385+
def __to_english__(self):
386+
x = ltltoeng.apply_special_pattern_if_possible(self)
387+
if x is not None:
388+
return x
389+
lhs = self.left.__to_english__().rstrip('.')
390+
rhs = self.right.__to_english__().rstrip('.')
391+
392+
lhs = ltltoeng.normalize_embedded_clause(lhs)
393+
rhs = ltltoeng.normalize_embedded_clause(rhs)
394+
395+
# Potential patterns:
396+
patterns = [
397+
f"{lhs} if and only if {rhs}",
398+
f"{lhs} exactly when {rhs}",
393399
f"{lhs} is equivalent to {rhs}"
394-
]
395-
396-
# Choose a pattern randomly, and then return the corrected sentence
397-
english = random.choice(patterns)
398-
return ltltoeng.capitalize_sentence(english)
400+
]
401+
402+
# Choose the most fluent pattern rather than picking randomly
403+
english = ltltoeng.choose_best_sentence(patterns)
404+
return english
399405

400406

401407

0 commit comments

Comments
 (0)