Kalana commited on
Commit
5a42ed8
Β·
1 Parent(s): a31933e

Add correction mode with word-swap popovers, feedback system, and fix copy button

Browse files

- Interactive word-click alternatives using st.popover (FR6)
- HITL feedback: save corrections to feedback.csv for future retraining
- Replace broken JS clipboard with native st.code copy button (FR5)
- Guard against duplicate feedback submissions
- Fix stale session state on new transliteration

Files changed (1) hide show
  1. app.py +151 -43
app.py CHANGED
@@ -5,36 +5,55 @@ SinCode Web UI β€” Streamlit interface for the transliteration engine.
5
  import streamlit as st
6
  import time
7
  import os
 
 
8
  import base64
 
 
9
  from PIL import Image
10
  from sincode_model import BeamSearchDecoder
11
 
 
 
12
  st.set_page_config(page_title="ΰ·ƒΰ·’ΰΆ‚Code", page_icon="πŸ‡±πŸ‡°", layout="centered")
13
 
14
 
15
  # ─── Helpers ─────────────────────────────────────────────────────────────────
16
 
17
- def _set_background(image_file: str) -> None:
18
- """Inject a dark-overlay background from a local image."""
 
19
  try:
20
  with open(image_file, "rb") as f:
21
  b64 = base64.b64encode(f.read()).decode()
22
- st.markdown(
23
- f"""
24
- <style>
25
- .stApp {{
26
- background-image: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)),
27
- url(data:image/png;base64,{b64});
28
- background-size: cover;
29
- background-position: center;
30
- background-attachment: fixed;
31
- }}
32
- </style>
33
- """,
34
- unsafe_allow_html=True,
35
  )
36
  except FileNotFoundError:
37
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
 
40
  @st.cache_resource
@@ -52,7 +71,7 @@ def _load_decoder() -> BeamSearchDecoder:
52
  _set_background("images/background.png")
53
 
54
  with st.sidebar:
55
- st.image(Image.open("images/SinCodeLogo.jpg"), width=200)
56
  st.title("ΰ·ƒΰ·’ΰΆ‚Code Project")
57
  st.info("Prototype")
58
 
@@ -99,34 +118,123 @@ if st.button("Transliterate", type="primary", use_container_width=True) and inpu
99
  with st.spinner("Processing..."):
100
  decoder = _load_decoder()
101
  t0 = time.time()
102
- result, trace_logs = decoder.decode(input_text, mode=decode_mode)
 
 
 
103
  elapsed = time.time() - t0
104
 
105
- st.success("Transliteration Complete")
106
- st.markdown(f"### {result}")
107
-
108
- col1, col2 = st.columns([3, 1])
109
- with col1:
110
- st.caption(f"Mode: {decode_mode} Β· Time: {round(elapsed, 2)}s")
111
- with col2:
112
- if st.button("πŸ“‹ Copy", key="copy_result"):
113
- st.session_state["copied"] = True
114
- if st.session_state.get("copied"):
115
- st.components.v1.html(
116
- f"""<script>navigator.clipboard.writeText(`{result}`);</script>""",
117
- height=0,
118
- )
119
- st.toast("Copied to clipboard!")
120
- st.session_state["copied"] = False
121
-
122
- with st.expander("Scoring Breakdown", expanded=True):
123
- st.caption(
124
- "MLM = contextual fit Β· Fid = transliteration fidelity Β· "
125
- "Rank = dictionary prior Β· πŸ”€ = English"
126
- )
127
- for log in trace_logs:
128
- st.markdown(log)
129
- st.divider()
130
 
131
  except Exception as e:
132
  st.error(f"Error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import streamlit as st
6
  import time
7
  import os
8
+ import csv
9
+ import html as html_lib
10
  import base64
11
+ from datetime import datetime
12
+ from pathlib import Path
13
  from PIL import Image
14
  from sincode_model import BeamSearchDecoder
15
 
16
+ FEEDBACK_FILE = Path("feedback.csv")
17
+
18
  st.set_page_config(page_title="ΰ·ƒΰ·’ΰΆ‚Code", page_icon="πŸ‡±πŸ‡°", layout="centered")
19
 
20
 
21
  # ─── Helpers ─────────────────────────────────────────────────────────────────
22
 
23
+ @st.cache_data
24
+ def _background_css(image_file: str) -> str:
25
+ """Return the CSS string for the background image (cached after first read)."""
26
  try:
27
  with open(image_file, "rb") as f:
28
  b64 = base64.b64encode(f.read()).decode()
29
+ return (
30
+ f"<style>.stApp {{background-image: linear-gradient(rgba(0,0,0,0.7),"
31
+ f"rgba(0,0,0,0.7)),url(data:image/png;base64,{b64});"
32
+ f"background-size:cover;background-position:center;"
33
+ f"background-attachment:fixed;}}</style>"
 
 
 
 
 
 
 
 
34
  )
35
  except FileNotFoundError:
36
+ return ""
37
+
38
+
39
+ def _set_background(image_file: str) -> None:
40
+ css = _background_css(image_file)
41
+ if css:
42
+ st.markdown(css, unsafe_allow_html=True)
43
+
44
+
45
+ @st.cache_data
46
+ def _load_logo(image_file: str):
47
+ return Image.open(image_file)
48
+
49
+
50
+ def _save_feedback(input_sentence: str, original_output: str, corrected_output: str) -> None:
51
+ """Append a full-sentence correction to the feedback CSV."""
52
+ with FEEDBACK_FILE.open("a", newline="", encoding="utf-8") as f:
53
+ writer = csv.writer(f)
54
+ if f.tell() == 0:
55
+ writer.writerow(["timestamp", "input_sentence", "original_output", "corrected_output"])
56
+ writer.writerow([datetime.now().isoformat(), input_sentence, original_output, corrected_output])
57
 
58
 
59
  @st.cache_resource
 
71
  _set_background("images/background.png")
72
 
73
  with st.sidebar:
74
+ st.image(_load_logo("images/SinCodeLogo.jpg"), width=200)
75
  st.title("ΰ·ƒΰ·’ΰΆ‚Code Project")
76
  st.info("Prototype")
77
 
 
118
  with st.spinner("Processing..."):
119
  decoder = _load_decoder()
120
  t0 = time.time()
121
+ if decode_mode == "greedy":
122
+ result, trace_logs, diagnostics = decoder.greedy_decode_with_diagnostics(input_text)
123
+ else:
124
+ result, trace_logs, diagnostics = decoder.decode_with_diagnostics(input_text)
125
  elapsed = time.time() - t0
126
 
127
+ # Store results in session state for interactive word swapping
128
+ selected = [d.selected_candidate for d in diagnostics]
129
+ st.session_state["diagnostics"] = diagnostics
130
+ st.session_state["output_words"] = selected
131
+ st.session_state["original_words"] = list(selected)
132
+ st.session_state["input_sentence"] = input_text
133
+ st.session_state["trace_logs"] = trace_logs
134
+ st.session_state["elapsed"] = elapsed
135
+ st.session_state["correction_mode"] = False
136
+ st.session_state["correction_submitted_for"] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  except Exception as e:
139
  st.error(f"Error: {e}")
140
+
141
+ # ─── Render output (persists across reruns for word swapping) ─────────────
142
+
143
+ if "output_words" in st.session_state and st.session_state["output_words"]:
144
+ diagnostics = st.session_state["diagnostics"]
145
+ output_words = st.session_state["output_words"]
146
+ original_words = st.session_state.get("original_words", list(output_words))
147
+ trace_logs = st.session_state["trace_logs"]
148
+ elapsed = st.session_state["elapsed"]
149
+
150
+ current_result = " ".join(output_words)
151
+ original_result = " ".join(original_words)
152
+ has_changes = output_words != original_words
153
+
154
+ st.success("Transliteration Complete")
155
+
156
+ # Output display with native copy button (st.code has built-in clipboard support)
157
+ safe_display = html_lib.escape(current_result)
158
+ st.markdown(
159
+ f'<span style="font-size:1.4em;font-weight:700;">{safe_display}</span>',
160
+ unsafe_allow_html=True,
161
+ )
162
+ st.code(current_result, language=None)
163
+ st.caption(f"Mode: {decode_mode} Β· Time: {round(elapsed, 2)}s")
164
+
165
+ # ── Correction mode toggle ────────────────────────────────────────
166
+ correction_mode = st.toggle(
167
+ "Correct this translation",
168
+ value=st.session_state.get("correction_mode", False),
169
+ key="correction_toggle",
170
+ )
171
+
172
+ if correction_mode:
173
+ st.caption("Click any highlighted word to see alternatives and swap it.")
174
+
175
+ # Word chips in rows β€” only ambiguous words are interactive
176
+ ROW_SIZE = 6
177
+ for row_start in range(0, len(output_words), ROW_SIZE):
178
+ row_slice = list(enumerate(diagnostics[row_start:row_start + ROW_SIZE], start=row_start))
179
+ cols = st.columns(len(row_slice))
180
+
181
+ for col, (i, diag) in zip(cols, row_slice):
182
+ has_alts = len(diag.candidate_breakdown) > 1
183
+ was_changed = output_words[i] != original_words[i]
184
+ with col:
185
+ if has_alts:
186
+ chip = f":green[**{output_words[i]}**] :material/check:" if was_changed else f":blue[**{output_words[i]}**]"
187
+ with st.popover(chip, use_container_width=True):
188
+ st.markdown(f"**`{diag.input_word}`** β€” pick alternative:")
189
+ for scored in diag.candidate_breakdown[:5]:
190
+ eng_tag = " πŸ”€" if scored.is_english else ""
191
+ is_sel = scored.text == output_words[i]
192
+ if st.button(
193
+ f"{'βœ… ' if is_sel else ''}{scored.text}{eng_tag}",
194
+ key=f"alt_{i}_{scored.text}",
195
+ help=f"Score: {scored.combined_score:.2f}",
196
+ use_container_width=True,
197
+ type="primary" if is_sel else "secondary",
198
+ ):
199
+ st.session_state["output_words"][i] = scored.text
200
+ st.rerun()
201
+ st.markdown("---")
202
+ custom = st.text_input(
203
+ "Not listed? Type correct word:",
204
+ key=f"custom_{i}",
205
+ placeholder="Type Sinhala word",
206
+ )
207
+ if custom and st.button("Use this", key=f"custom_apply_{i}", use_container_width=True):
208
+ st.session_state["output_words"][i] = custom
209
+ st.rerun()
210
+ else:
211
+ st.markdown(f"**{output_words[i]}**")
212
+
213
+ # ── Submit correction button (only when changes exist, once per result) ──
214
+ # Guard key: (original sentence, original output) β€” stable regardless of swaps
215
+ submit_key = (st.session_state["input_sentence"], original_result)
216
+ already_submitted = st.session_state.get("correction_submitted_for") == submit_key
217
+ if has_changes and not already_submitted:
218
+ st.info(f"**Original:** {original_result}\n\n**Corrected:** {current_result}")
219
+ if st.button("Submit Correction", type="primary", use_container_width=True):
220
+ _save_feedback(
221
+ input_sentence=st.session_state["input_sentence"],
222
+ original_output=original_result,
223
+ corrected_output=current_result,
224
+ )
225
+ st.session_state["correction_submitted_for"] = submit_key
226
+ st.session_state["correction_mode"] = False
227
+ st.toast("Correction submitted β€” thank you!")
228
+ st.rerun()
229
+
230
+ # Show outside toggle so it remains visible after submission closes the toggle
231
+ input_sent = st.session_state.get("input_sentence", "")
232
+ if st.session_state.get("correction_submitted_for") == (input_sent, original_result):
233
+ st.success("Correction already submitted.")
234
+
235
+ with st.expander("Scoring Breakdown", expanded=False):
236
+ st.caption(
237
+ "MLM = contextual fit Β· Fid = transliteration fidelity Β· "
238
+ "Rank = dictionary prior Β· πŸ”€ = English"
239
+ )
240
+ st.markdown("\n\n---\n\n".join(trace_logs))