File size: 9,344 Bytes
f6f45d5
 
 
 
 
 
 
1fed70a
 
f6f45d5
 
1fed70a
 
 
 
f6f45d5
1fed70a
 
f6f45d5
 
 
1fed70a
 
 
 
 
86328d1
 
 
 
 
 
1fed70a
 
f6f45d5
 
 
 
 
 
 
 
 
 
 
 
1fed70a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6f45d5
 
 
 
 
1fed70a
f6f45d5
 
1fed70a
f6f45d5
 
 
1fed70a
 
 
 
 
 
f6f45d5
1fed70a
a801fe8
f6f45d5
 
 
 
 
 
 
 
 
 
 
 
 
 
1fed70a
f28d091
f6f45d5
 
 
1fed70a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6f45d5
 
 
1fed70a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
"""
SinCode v3 — Streamlit demo UI.

Architecture: ByT5-small (seq2seq candidate generation) +
              XLM-RoBERTa (MLM contextual reranking)

Two transliteration modes:
  - Code-Mixed  — ByT5 + MLM; retains English words where contextually apt
  - Full Sinhala — mBart50 sentence-level; transliterates everything to Sinhala
"""

from __future__ import annotations

import os

import streamlit as st

from feedback_store import FeedbackStore, format_feedback_error
from sincode_model import BeamSearchDecoder, SentenceTransliterator


st.set_page_config(page_title="සිංCode", page_icon="🇱🇰", layout="centered")


def _secret_or_env(name: str, default: str = "") -> str:
    try:
        value = st.secrets.get(name)
        if value is not None:
            return str(value)
    except Exception:
        # Compatible with Streamlit versions where secrets backend differs
        # or no secrets file is configured.
        pass
    return os.getenv(name, default)


@st.cache_resource(show_spinner="Loading models (ByT5 + XLM-RoBERTa)…")
def load_decoder() -> BeamSearchDecoder:
    return BeamSearchDecoder()


@st.cache_resource(show_spinner="Loading mBart50 model…")
def load_transliterator() -> SentenceTransliterator:
    return SentenceTransliterator()


@st.cache_resource
def load_feedback_store() -> FeedbackStore:
    return FeedbackStore(
        supabase_url=_secret_or_env("SUPABASE_URL"),
        supabase_anon_key=_secret_or_env("SUPABASE_ANON_KEY"),
        supabase_service_key=_secret_or_env("SUPABASE_SERVICE_ROLE_KEY"),
        table_name=_secret_or_env("SUPABASE_FEEDBACK_TABLE", "feedback_submissions"),
    )


st.title("සිංCode")

with st.sidebar:
    st.markdown("### About")
    st.write(
        "A model-driven Singlish to Sinhala transliteration interface with "
        "optional user feedback for future model refinement."
    )
    store = load_feedback_store()
    st.caption(f"Feedback storage: {store.backend_label}")

mode = st.radio(
    "Transliteration mode",
    options=["Code-Mixed Output", "Full Sinhala Output"],
    horizontal=True,
    help=(
        "**Code-Mixed**: keeps English technical or borrowed words where natural "
        "(e.g. *buffer*, *bit rate*). "
        "**Full Sinhala**: transliterates every word to Sinhala script "
        "(e.g. *business* -> ව්‍යාපාරය)."
    ),
)

if mode == "Full Sinhala Output":
    st.caption("mBart50 sentence-level transliteration")
else:
    st.caption("ByT5 seq2seq + XLM-RoBERTa MLM reranking")

sentence = st.text_area(
    "Enter Singlish sentence",
    height=120,
    placeholder="Type here",
)

show_trace = st.checkbox(
    "Show step-by-step trace",
    value=False,
    disabled=(mode == "Full Sinhala Output"),
    help="Trace is only available in Code-Mixed mode.",
)

if st.button("Transliterate", type="primary") and sentence.strip():
    if mode == "Full Sinhala Output":
        with st.spinner("Transliterating (mBart50)…"):
            transliterator = load_transliterator()
            result = transliterator.transliterate(sentence.strip())
        trace_logs: list[str] = []
        word_candidates: list[tuple] = []
    else:
        with st.spinner("Transliterating…"):
            decoder = load_decoder()
            decode_out = decoder.decode(sentence.strip())
            if isinstance(decode_out, tuple) and len(decode_out) == 3:
                result, trace_logs, word_candidates = decode_out
            else:
                # Backward compatibility for cached/older decoders returning (result, trace_logs)
                result, trace_logs = decode_out
                word_candidates = [(w, []) for w in str(result).split()]

    st.session_state["last_input"] = sentence.strip()
    st.session_state["last_mode"] = mode
    st.session_state["last_result"] = result
    st.session_state["last_trace_logs"] = trace_logs
    st.session_state["last_word_candidates"] = word_candidates if mode == "Code-Mixed Output" else []
    st.session_state["last_output_words"] = [w for w, _ in word_candidates] if word_candidates else result.split()
    st.session_state["feedback_submitted_for"] = None

if st.session_state.get("last_result"):
    current_input = st.session_state.get("last_input", "")
    original_result = st.session_state["last_result"]
    current_mode = st.session_state.get("last_mode", mode)
    word_candidates = st.session_state.get("last_word_candidates", [])

    # output_words may be mutated by alternative selection — keep in session state
    if "last_output_words" not in st.session_state:
        st.session_state["last_output_words"] = original_result.split()
    output_words: list[str] = st.session_state["last_output_words"]
    current_result = " ".join(output_words)

    st.markdown("### Result")
    st.success(current_result)
    st.caption(f"Mode: {current_mode}")

    # ── Alternative selection (Code-Mixed only) ───────────────────────────────
    if current_mode == "Code-Mixed Output" and word_candidates:
        swappable = [
            (i, sel, alts)
            for i, (sel, alts) in enumerate(word_candidates)
            if len(alts) > 1
        ]
        if swappable:
            st.markdown("##### Alternative transliterations")
            st.caption("Click a word to pick an alternative.")
            widths = [max(len(output_words[i]), 3) for i, _, _ in swappable]
            cols = st.columns(widths, gap="small")
            for col, (i, _sel, alts) in zip(cols, swappable):
                current_word = output_words[i]
                was_changed = current_word != word_candidates[i][0]
                chip = (
                    f":green[**{current_word}**]"
                    if was_changed
                    else f":blue[**{current_word}**]"
                )
                with col:
                    with st.popover(chip, use_container_width=True):
                        st.markdown(f"**Alternatives for `{current_word}`:**")
                        for alt in alts:
                            is_sel = alt == current_word
                            if st.button(
                                f"{'✅ ' if is_sel else ''}{alt}",
                                key=f"alt_{i}_{alt}",
                                use_container_width=True,
                                type="primary" if is_sel else "secondary",
                            ):
                                st.session_state["last_output_words"][i] = alt
                                st.session_state["corrected_output"] = " ".join(
                                    st.session_state["last_output_words"]
                                )
                                st.rerun()
                        st.markdown("---")
                        custom = st.text_input(
                            "Not listed? Type correct word:",
                            key=f"custom_{i}",
                            placeholder="Type Sinhala word",
                        )
                        if custom and st.button(
                            "Use this", key=f"custom_apply_{i}", use_container_width=True
                        ):
                            st.session_state["last_output_words"][i] = custom
                            st.session_state["corrected_output"] = " ".join(
                                st.session_state["last_output_words"]
                            )
                            st.rerun()

    if current_mode == "Code-Mixed Output" and show_trace:
        trace_logs = st.session_state.get("last_trace_logs", [])
        if trace_logs:
            st.markdown("### Trace")
            for log in trace_logs:
                st.markdown(log)

    with st.expander("Submit a correction", expanded=False):
        st.caption(
            "If the output is incorrect, submit a corrected sentence. "
            "These feedback samples can be reviewed and used in future fine-tuning."
        )
        corrected_output = st.text_area(
            "Corrected output",
            value=current_result,
            height=120,
            key="corrected_output",
        )
        feedback_comment = st.text_area(
            "Optional note",
            placeholder="Example: In this sentence, this word should stay in English.",
            key="feedback_comment",
        )

        submit_key = (current_input, original_result, corrected_output)
        already_submitted = st.session_state.get("feedback_submitted_for") == submit_key

        if already_submitted:
            st.success("Correction submitted.")
        elif st.button("Submit Feedback", use_container_width=True):
            try:
                load_feedback_store().save_submission(
                    input_sentence=current_input,
                    original_output=original_result,
                    corrected_output=corrected_output.strip(),
                    user_comment=feedback_comment,
                    decode_mode=current_mode,
                )
                st.session_state["feedback_submitted_for"] = submit_key
                st.success("Feedback submitted successfully.")
            except Exception as exc:
                st.error(f"Could not submit feedback: {format_feedback_error(exc)}")