|
|
|
|
|
"""
|
|
|
Merged Rephraser app
|
|
|
- GUI from original (first) file
|
|
|
- Models/logic from later big file (kept unchanged)
|
|
|
- Grammar highlight (red for issues; green underline for corrected words)
|
|
|
- File upload/download for .docx/.pdf/.txt with best-effort format preservation
|
|
|
- Tools independent (no automatic chaining)
|
|
|
- Prev/Next browsing for multi-version outputs
|
|
|
"""
|
|
|
|
|
|
import streamlit as st
|
|
|
import io, os, random, re, difflib, html, tempfile
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
st.set_page_config(page_title="Rephraser", layout="wide")
|
|
|
|
|
|
|
|
|
if st.button("🏠 Home"):
|
|
|
st.rerun()
|
|
|
|
|
|
|
|
|
try:
|
|
|
import docx
|
|
|
except Exception:
|
|
|
docx = None
|
|
|
|
|
|
try:
|
|
|
import fitz
|
|
|
except Exception:
|
|
|
fitz = None
|
|
|
|
|
|
try:
|
|
|
import language_tool_python
|
|
|
except Exception:
|
|
|
language_tool_python = None
|
|
|
|
|
|
try:
|
|
|
from textblob import TextBlob
|
|
|
except Exception:
|
|
|
TextBlob = None
|
|
|
|
|
|
|
|
|
try:
|
|
|
import nltk
|
|
|
from nltk.corpus import wordnet as wn
|
|
|
nltk_available = True
|
|
|
except Exception:
|
|
|
nltk_available = False
|
|
|
|
|
|
|
|
|
try:
|
|
|
import spacy
|
|
|
nlp = spacy.load("en_core_web_sm")
|
|
|
SPACY_AVAILABLE = True
|
|
|
except Exception:
|
|
|
nlp = None
|
|
|
SPACY_AVAILABLE = False
|
|
|
|
|
|
|
|
|
try:
|
|
|
import transformers
|
|
|
TRANSFORMERS_AVAILABLE = True
|
|
|
except Exception:
|
|
|
TRANSFORMERS_AVAILABLE = False
|
|
|
|
|
|
|
|
|
try:
|
|
|
from spellchecker import SpellChecker
|
|
|
SPELLCHECKER_AVAILABLE = True
|
|
|
spell = SpellChecker()
|
|
|
except Exception:
|
|
|
SPELLCHECKER_AVAILABLE = False
|
|
|
|
|
|
|
|
|
try:
|
|
|
import pyperclip
|
|
|
PYPERCLIP = True
|
|
|
except Exception:
|
|
|
PYPERCLIP = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if "versions" not in st.session_state:
|
|
|
st.session_state.versions = []
|
|
|
if "version_index" not in st.session_state:
|
|
|
st.session_state.version_index = 0
|
|
|
if "last_input" not in st.session_state:
|
|
|
st.session_state.last_input = ""
|
|
|
if "current_text" not in st.session_state:
|
|
|
st.session_state.current_text = ""
|
|
|
if "history" not in st.session_state:
|
|
|
st.session_state.history = []
|
|
|
|
|
|
if "_uploaded_bytes" not in st.session_state:
|
|
|
st.session_state._uploaded_bytes = None
|
|
|
if "_uploaded_name" not in st.session_state:
|
|
|
st.session_state._uploaded_name = None
|
|
|
if "_last_grammar_issues" not in st.session_state:
|
|
|
st.session_state._last_grammar_issues = None
|
|
|
if "_last_output_file" not in st.session_state:
|
|
|
st.session_state._last_output_file = None
|
|
|
if "_last_output_name" not in st.session_state:
|
|
|
st.session_state._last_output_name = None
|
|
|
if "_last_tool" not in st.session_state:
|
|
|
st.session_state._last_tool = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mark_grammar_issues(text, issues):
|
|
|
"""Wrap problem spans in red (inline). issues is list of dicts with offset & length & message & replacements"""
|
|
|
if not issues:
|
|
|
return html.escape(text)
|
|
|
spans = []
|
|
|
for it in issues:
|
|
|
off = it.get("offset", 0)
|
|
|
length = it.get("length", 0)
|
|
|
msg = it.get("message", "")
|
|
|
spans.append((off, off + length, msg))
|
|
|
spans.sort()
|
|
|
out = ""
|
|
|
idx = 0
|
|
|
for s, e, msg in spans:
|
|
|
if s > idx:
|
|
|
out += html.escape(text[idx:s])
|
|
|
problem = html.escape(text[s:e])
|
|
|
out += f'<span title="{html.escape(msg)}" style="border-bottom:2px solid #c0392b;">{problem}</span>'
|
|
|
idx = e
|
|
|
if idx < len(text):
|
|
|
out += html.escape(text[idx:])
|
|
|
return out
|
|
|
|
|
|
def underline_changes_in_output(orig, corrected):
|
|
|
"""
|
|
|
Token-level diff: underline changed/inserted fragments in green in corrected text.
|
|
|
"""
|
|
|
a = orig.split()
|
|
|
b = corrected.split()
|
|
|
sm = difflib.SequenceMatcher(a=a, b=b)
|
|
|
parts = []
|
|
|
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
|
|
if tag == "equal":
|
|
|
parts.append(" ".join(b[j1:j2]))
|
|
|
elif tag in ("replace", "insert"):
|
|
|
changed = " ".join(b[j1:j2])
|
|
|
parts.append(f'<span style="text-decoration: underline; text-decoration-color: #27ae60;">{html.escape(changed)}</span>')
|
|
|
elif tag == "delete":
|
|
|
pass
|
|
|
return " ".join(parts) if parts else html.escape(corrected)
|
|
|
|
|
|
|
|
|
import html
|
|
|
import difflib
|
|
|
|
|
|
def text_to_html_with_highlights(orig, new):
|
|
|
"""
|
|
|
Compare original and new text word-by-word.
|
|
|
Underline only the changed/added words in green.
|
|
|
"""
|
|
|
orig_words = orig.split()
|
|
|
new_words = new.split()
|
|
|
|
|
|
diff = list(difflib.ndiff(orig_words, new_words))
|
|
|
highlighted = []
|
|
|
for word in diff:
|
|
|
if word.startswith("+ "):
|
|
|
highlighted.append(
|
|
|
f"<span style='color:black;text-decoration:underline;text-decoration-color:green'>{html.escape(word[2:])}</span>"
|
|
|
)
|
|
|
elif word.startswith(" "):
|
|
|
highlighted.append(html.escape(word[2:]))
|
|
|
|
|
|
|
|
|
return " ".join(highlighted)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def paraphrase_variants_fast(text, n_variants=3):
|
|
|
text = text.strip()
|
|
|
if not text:
|
|
|
return []
|
|
|
sents = re.split(r'(?<=[.!?])\s+', text)
|
|
|
variants = []
|
|
|
for v in range(n_variants):
|
|
|
outs = []
|
|
|
for s in sents:
|
|
|
sent = s.strip()
|
|
|
if not sent:
|
|
|
continue
|
|
|
if SPACY_AVAILABLE:
|
|
|
doc = nlp(sent)
|
|
|
|
|
|
if random.random() < 0.3 and len(list(doc.noun_chunks)) >= 2:
|
|
|
chunks = list(doc.noun_chunks)
|
|
|
text_chunks = [c.text for c in chunks]
|
|
|
s2 = sent
|
|
|
try:
|
|
|
s2 = s2.replace(text_chunks[0], "<<<A>>>").replace(text_chunks[1], text_chunks[0]).replace("<<<A>>>", text_chunks[1])
|
|
|
except Exception:
|
|
|
s2 = sent
|
|
|
outs.append(s2)
|
|
|
continue
|
|
|
if ',' in sent and random.random() < 0.4:
|
|
|
parts = [p.strip() for p in sent.split(',')]
|
|
|
random.shuffle(parts)
|
|
|
outs.append(", ".join(parts))
|
|
|
continue
|
|
|
outs.append(_synonym_replace(sent, prob=0.15 + 0.05 * v))
|
|
|
else:
|
|
|
if random.random() < 0.2:
|
|
|
words = sent.split()
|
|
|
if len(words) > 3:
|
|
|
i = random.randint(0, len(words) - 3)
|
|
|
words[i], words[i+1] = words[i+1], words[i]
|
|
|
outs.append(" ".join(words))
|
|
|
else:
|
|
|
outs.append(_synonym_replace(sent, prob=0.12 + 0.04 * v))
|
|
|
final = " ".join(outs)
|
|
|
if random.random() < 0.3 and len(sents) > 1:
|
|
|
random.shuffle(sents)
|
|
|
final = " ".join(outs)
|
|
|
variants.append(final)
|
|
|
uniq = []
|
|
|
for x in variants:
|
|
|
if x not in uniq and x.strip():
|
|
|
uniq.append(x)
|
|
|
return uniq[:n_variants]
|
|
|
|
|
|
def _synonym_replace(sentence, prob=0.12, max_replacements=2):
|
|
|
if not nltk_available:
|
|
|
words = sentence.split()
|
|
|
for i in range(len(words)):
|
|
|
if random.random() < prob:
|
|
|
j = random.randrange(len(words))
|
|
|
words[i], words[j] = words[j], words[i]
|
|
|
return " ".join(words)
|
|
|
tokens = re.findall(r"\w+|\W+", sentence)
|
|
|
words = [t for t in tokens]
|
|
|
replaced = 0
|
|
|
for i, tok in enumerate(words):
|
|
|
if not re.match(r'\w+', tok):
|
|
|
continue
|
|
|
lower = tok.lower()
|
|
|
if random.random() > prob:
|
|
|
continue
|
|
|
syns = wn.synsets(lower)
|
|
|
if not syns:
|
|
|
continue
|
|
|
cand = None
|
|
|
for s in syns:
|
|
|
for l in s.lemmas():
|
|
|
name = l.name().replace('_', ' ')
|
|
|
if name.lower() != lower and ' ' not in name:
|
|
|
cand = name
|
|
|
break
|
|
|
if cand:
|
|
|
break
|
|
|
if cand:
|
|
|
if tok[0].isupper():
|
|
|
cand = cand.capitalize()
|
|
|
words[i] = cand
|
|
|
replaced += 1
|
|
|
if replaced >= max_replacements:
|
|
|
break
|
|
|
return "".join(words)
|
|
|
|
|
|
def simple_mix_versions(versions_list):
|
|
|
if not versions_list:
|
|
|
return ""
|
|
|
pieces = []
|
|
|
for v in versions_list:
|
|
|
s = v.strip()
|
|
|
if not s:
|
|
|
continue
|
|
|
sents = re.split(r'(?<=[.!?])\s+', s)
|
|
|
take_n = max(1, min(3, len(sents)))
|
|
|
picks = random.sample(sents, take_n) if len(sents) > take_n else sents
|
|
|
pieces.extend(picks)
|
|
|
random.shuffle(pieces)
|
|
|
return " ".join(pieces)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@st.cache_resource(show_spinner=False)
|
|
|
def load_small_model(model_name="t5-small"):
|
|
|
if not TRANSFORMERS_AVAILABLE:
|
|
|
raise ImportError("transformers not installed")
|
|
|
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline
|
|
|
tok = AutoTokenizer.from_pretrained(model_name)
|
|
|
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
|
|
|
pipe = pipeline("text2text-generation", model=model, tokenizer=tok, device=-1)
|
|
|
return tok, model, pipe
|
|
|
|
|
|
def hf_paraphrase_with_pipe(pipe, text, max_len=256):
|
|
|
try:
|
|
|
out = pipe(text, max_length=max_len, do_sample=True, top_p=0.95, temperature=0.8, num_return_sequences=1)
|
|
|
if isinstance(out, list) and out:
|
|
|
return out[0].get("generated_text") or out[0].get("summary_text") or str(out[0])
|
|
|
return str(out)
|
|
|
except Exception:
|
|
|
return text
|
|
|
|
|
|
def plagiarism_remover_pipeline(text, aggressive=1, light_only=False):
|
|
|
versions = []
|
|
|
v_light = paraphrase_variants_fast(text, n_variants=1)[0] if paraphrase_variants_fast(text, n_variants=1) else text
|
|
|
versions.append(v_light)
|
|
|
if TRANSFORMERS_AVAILABLE and not light_only:
|
|
|
try:
|
|
|
_, _, t5_pipe = load_small_model("t5-small")
|
|
|
v_t5 = hf_paraphrase_with_pipe(t5_pipe, "paraphrase: " + text)
|
|
|
versions.append(v_t5)
|
|
|
except Exception:
|
|
|
pass
|
|
|
try:
|
|
|
_, _, p_pipe = load_small_model("google/pegasus-xsum")
|
|
|
v_peg = hf_paraphrase_with_pipe(p_pipe, text)
|
|
|
versions.append(v_peg)
|
|
|
except Exception:
|
|
|
pass
|
|
|
v_combo = simple_mix_versions(versions)
|
|
|
versions.append(v_combo)
|
|
|
uniq = []
|
|
|
for v in versions:
|
|
|
if v and v.strip() and v not in uniq:
|
|
|
uniq.append(v)
|
|
|
if len(uniq) >= 5:
|
|
|
break
|
|
|
return uniq
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def grammar_and_spelling_check(text):
|
|
|
if language_tool_python is not None:
|
|
|
try:
|
|
|
tool = language_tool_python.LanguageTool('en-US')
|
|
|
matches = tool.check(text)
|
|
|
corrected = language_tool_python.utils.correct(text, matches)
|
|
|
issues = []
|
|
|
for m in matches:
|
|
|
issues.append({
|
|
|
"message": m.message,
|
|
|
"replacements": m.replacements,
|
|
|
"offset": m.offset,
|
|
|
"length": m.errorLength,
|
|
|
"context": text[max(0, m.offset - 30): m.offset + 30]
|
|
|
})
|
|
|
return corrected, issues
|
|
|
except Exception:
|
|
|
pass
|
|
|
if TextBlob is not None:
|
|
|
try:
|
|
|
tb = TextBlob(text)
|
|
|
corr = str(tb.correct())
|
|
|
return corr, []
|
|
|
except Exception:
|
|
|
pass
|
|
|
return text, []
|
|
|
|
|
|
def spelling_suggestions(word, top_n=5, sentence=None):
|
|
|
"""Return contextual synonyms if NLTK WordNet is available, else fallback to spellchecker."""
|
|
|
if not word or not word.strip():
|
|
|
return []
|
|
|
|
|
|
|
|
|
def get_wordnet_pos(treebank_tag):
|
|
|
from nltk.corpus import wordnet
|
|
|
if treebank_tag.startswith('J'):
|
|
|
return wordnet.ADJ
|
|
|
elif treebank_tag.startswith('V'):
|
|
|
return wordnet.VERB
|
|
|
elif treebank_tag.startswith('N'):
|
|
|
return wordnet.NOUN
|
|
|
elif treebank_tag.startswith('R'):
|
|
|
return wordnet.ADV
|
|
|
return None
|
|
|
|
|
|
|
|
|
if nltk_available:
|
|
|
wn_pos = None
|
|
|
if sentence:
|
|
|
try:
|
|
|
tokens = nltk.word_tokenize(sentence)
|
|
|
tagged = nltk.pos_tag(tokens)
|
|
|
for tok, tag in tagged:
|
|
|
if tok.lower() == word.lower():
|
|
|
wn_pos = get_wordnet_pos(tag)
|
|
|
break
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
syns = wn.synsets(word, pos=wn_pos) if wn_pos else wn.synsets(word)
|
|
|
suggestions = set()
|
|
|
for s in syns:
|
|
|
for l in s.lemmas():
|
|
|
name = l.name().replace('_', ' ')
|
|
|
if name.lower() != word.lower():
|
|
|
suggestions.add(name)
|
|
|
if suggestions:
|
|
|
return sorted(suggestions)[:top_n]
|
|
|
|
|
|
|
|
|
if SPELLCHECKER_AVAILABLE:
|
|
|
suggestions = spell.candidates(word)
|
|
|
return list(suggestions)[:top_n]
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_text_from_docx_bytes(b):
|
|
|
if docx is None:
|
|
|
raise RuntimeError("python-docx not installed")
|
|
|
f = io.BytesIO(b)
|
|
|
document = docx.Document(f)
|
|
|
paras = [p.text for p in document.paragraphs]
|
|
|
return "\n\n".join(paras)
|
|
|
|
|
|
def extract_text_from_pdf_bytes(b):
|
|
|
if fitz is None:
|
|
|
raise RuntimeError("PyMuPDF not installed")
|
|
|
doc = fitz.open(stream=b, filetype="pdf")
|
|
|
text = ""
|
|
|
for p in doc:
|
|
|
text += p.get_text() + "\n\n"
|
|
|
return text
|
|
|
|
|
|
def extract_text_from_txt_bytes(b):
|
|
|
try:
|
|
|
return b.decode("utf-8")
|
|
|
except Exception:
|
|
|
try:
|
|
|
return b.decode("latin-1")
|
|
|
except Exception:
|
|
|
return str(b)
|
|
|
|
|
|
def make_docx_bytes_from_text(text):
|
|
|
if docx is None:
|
|
|
raise RuntimeError("python-docx not installed")
|
|
|
out = io.BytesIO()
|
|
|
d = docx.Document()
|
|
|
for para in text.split("\n\n"):
|
|
|
d.add_paragraph(para)
|
|
|
d.save(out)
|
|
|
out.seek(0)
|
|
|
return out.read()
|
|
|
|
|
|
def make_pdf_bytes_from_text(text):
|
|
|
if fitz is None:
|
|
|
raise RuntimeError("PyMuPDF not installed")
|
|
|
doc = fitz.open()
|
|
|
lines = text.split("\n")
|
|
|
page = doc.new_page()
|
|
|
y = 72
|
|
|
for line in lines:
|
|
|
if y > 720:
|
|
|
page = doc.new_page()
|
|
|
y = 72
|
|
|
page.insert_text((72, y), line)
|
|
|
y += 14
|
|
|
buf = doc.write()
|
|
|
doc.close()
|
|
|
return buf
|
|
|
|
|
|
def _build_replacement_spans(orig_text, corrected_text):
|
|
|
a = orig_text.split()
|
|
|
b = corrected_text.split()
|
|
|
sm = difflib.SequenceMatcher(a=a, b=b)
|
|
|
spans = []
|
|
|
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
|
|
if tag == "equal":
|
|
|
continue
|
|
|
orig_span = " ".join(a[i1:i2]).strip()
|
|
|
corr_span = " ".join(b[j1:j2]).strip()
|
|
|
if orig_span:
|
|
|
spans.append((orig_span, corr_span))
|
|
|
spans.sort(key=lambda x: -len(x[0]))
|
|
|
return spans
|
|
|
|
|
|
def apply_replacements_to_docx_bytes(original_bytes, orig_text, corrected_text):
|
|
|
"""Replace occurrences of orig spans with corrected spans inside docx runs and table cells (best-effort)."""
|
|
|
if docx is None:
|
|
|
raise RuntimeError("python-docx not installed")
|
|
|
from io import BytesIO
|
|
|
document = docx.Document(BytesIO(original_bytes))
|
|
|
spans = _build_replacement_spans(orig_text, corrected_text)
|
|
|
if not spans:
|
|
|
out = BytesIO()
|
|
|
document.save(out)
|
|
|
out.seek(0)
|
|
|
return out.read()
|
|
|
def replace_in_paragraph_runs(par):
|
|
|
for orig_span, corr_span in spans:
|
|
|
for run in par.runs:
|
|
|
if orig_span in run.text:
|
|
|
run.text = run.text.replace(orig_span, corr_span)
|
|
|
for p in document.paragraphs:
|
|
|
replace_in_paragraph_runs(p)
|
|
|
for table in document.tables:
|
|
|
for row in table.rows:
|
|
|
for cell in row.cells:
|
|
|
for p in cell.paragraphs:
|
|
|
replace_in_paragraph_runs(p)
|
|
|
out = io.BytesIO()
|
|
|
document.save(out)
|
|
|
out.seek(0)
|
|
|
return out.read()
|
|
|
|
|
|
def apply_replacements_to_pdf_bytes(original_bytes, orig_text, corrected_text):
|
|
|
"""Best-effort PDF replacement: redact original token bbox and write corrected text in place using PyMuPDF."""
|
|
|
if fitz is None:
|
|
|
raise RuntimeError("PyMuPDF not installed")
|
|
|
orig_tokens = orig_text.split()
|
|
|
corr_tokens = corrected_text.split()
|
|
|
sm = difflib.SequenceMatcher(a=orig_tokens, b=corr_tokens)
|
|
|
ops = []
|
|
|
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
|
|
if tag == "equal":
|
|
|
continue
|
|
|
ops.append((tag, i1, i2, j1, j2))
|
|
|
if not ops:
|
|
|
return original_bytes
|
|
|
pdf = fitz.open(stream=original_bytes, filetype="pdf")
|
|
|
global_words = []
|
|
|
for pno in range(len(pdf)):
|
|
|
page = pdf[pno]
|
|
|
words = page.get_text("words")
|
|
|
words_sorted = sorted(words, key=lambda w: (round(w[3],1), round(w[0],1)))
|
|
|
for w in words_sorted:
|
|
|
global_words.append((pno, w))
|
|
|
N = len(global_words)
|
|
|
M = len(orig_tokens)
|
|
|
map_len = min(N, M)
|
|
|
token_to_global = {}
|
|
|
for i in range(map_len):
|
|
|
token_to_global[i] = global_words[i]
|
|
|
redactions_per_page = {}
|
|
|
inserts_per_page = {}
|
|
|
for op in ops:
|
|
|
tag, i1, i2, j1, j2 = op
|
|
|
corr_span = " ".join(corr_tokens[j1:j2])
|
|
|
for ti in range(i1, i2):
|
|
|
if ti in token_to_global:
|
|
|
pno, wtuple = token_to_global[ti]
|
|
|
x0, y0, x1, y1 = wtuple[0], wtuple[1], wtuple[2], wtuple[3]
|
|
|
bbox = fitz.Rect(x0, y0, x1, y1)
|
|
|
redactions_per_page.setdefault(pno, []).append(bbox)
|
|
|
inserts_per_page.setdefault(pno, []).append((bbox, corr_span))
|
|
|
break
|
|
|
for pno, rects in redactions_per_page.items():
|
|
|
page = pdf[pno]
|
|
|
for r in rects:
|
|
|
page.add_redact_annot(r, fill=(1,1,1))
|
|
|
page.apply_redactions()
|
|
|
for bbox, corr_span in inserts_per_page.get(pno, []):
|
|
|
fontsize = max(6, round(bbox.height * 0.8))
|
|
|
try:
|
|
|
page.insert_textbox(bbox, corr_span, fontsize=fontsize, fontname="helv", align=0)
|
|
|
except Exception:
|
|
|
page.insert_text((bbox.x0, bbox.y0), corr_span, fontsize=fontsize, fontname="helv")
|
|
|
out = pdf.write()
|
|
|
pdf.close()
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.title("Rephraser — Paraphrase · Plagiarism Remover · Grammar & Spelling")
|
|
|
st.markdown("Paste text or upload DOCX/PDF/TXT. Tools are independent and chainable (use output as input manually).")
|
|
|
|
|
|
col_left, col_right = st.columns([2,1])
|
|
|
with col_left:
|
|
|
input_mode = st.radio("Input:", ("Paste text", "Upload file (.docx/.pdf/.txt)"))
|
|
|
uploaded_bytes = None
|
|
|
uploaded_name = None
|
|
|
input_text = ""
|
|
|
if input_mode == "Paste text":
|
|
|
input_text = st.text_area("Paste your paragraph(s) here:", height=200, value=st.session_state.current_text or "")
|
|
|
|
|
|
st.session_state._uploaded_bytes = None
|
|
|
st.session_state._uploaded_name = None
|
|
|
else:
|
|
|
uploaded = st.file_uploader("Upload .docx, .pdf or .txt", type=["docx","pdf","txt"])
|
|
|
if uploaded is not None:
|
|
|
uploaded_bytes = uploaded.read()
|
|
|
uploaded_name = uploaded.name
|
|
|
st.session_state._uploaded_bytes = uploaded_bytes
|
|
|
st.session_state._uploaded_name = uploaded_name
|
|
|
try:
|
|
|
if uploaded.name.lower().endswith(".docx"):
|
|
|
input_text = extract_text_from_docx_bytes(uploaded_bytes)
|
|
|
elif uploaded.name.lower().endswith(".pdf"):
|
|
|
input_text = extract_text_from_pdf_bytes(uploaded_bytes)
|
|
|
else:
|
|
|
input_text = extract_text_from_txt_bytes(uploaded_bytes)
|
|
|
st.success(f"Loaded {uploaded.name} (approx {len(input_text.split())} words)")
|
|
|
except Exception as e:
|
|
|
st.error(f"Could not extract text from file: {e}")
|
|
|
st.markdown("**Tools (choose one)**")
|
|
|
st.markdown("- **Para-phraser (fast):** Focused on rephrase sentence, regardless of Plagiarism ")
|
|
|
st.markdown("- **Plagiarism Remover (deep):** Focused on Plagiarism, Convert text to human like ")
|
|
|
st.markdown("- **Grammar & Spelling:** Spelling And Grammar Check")
|
|
|
|
|
|
with col_right:
|
|
|
st.header("Actions")
|
|
|
variants_to_generate = st.slider("Max variants (deep)", 1, 5, 3)
|
|
|
use_light_only = st.checkbox("Force light-only (no HF models)", value=True)
|
|
|
if st.button("1) Para-phraser (fast)"):
|
|
|
st.session_state._last_tool = "paraphrase"
|
|
|
source = input_text.strip() or st.session_state.current_text.strip()
|
|
|
if not source:
|
|
|
st.warning("Provide text or upload a file first.")
|
|
|
else:
|
|
|
st.session_state.history.append(st.session_state.current_text or source)
|
|
|
variants = paraphrase_variants_fast(source, n_variants=variants_to_generate)
|
|
|
if not variants:
|
|
|
st.error("No paraphrase produced.")
|
|
|
else:
|
|
|
st.session_state.versions = variants
|
|
|
st.session_state.version_index = 0
|
|
|
st.session_state.current_text = variants[0]
|
|
|
st.session_state.last_input = source
|
|
|
st.session_state._last_grammar_issues = None
|
|
|
st.session_state._last_output_file = None
|
|
|
st.success("Para-phraser done. Use Prev/Next to browse.")
|
|
|
|
|
|
if st.button("2) Plagiarism Remover (deep)"):
|
|
|
st.session_state._last_tool = "plagiarism"
|
|
|
source = input_text.strip() or st.session_state.current_text.strip()
|
|
|
if not source:
|
|
|
st.warning("Provide text or upload a file first.")
|
|
|
else:
|
|
|
st.session_state.history.append(st.session_state.current_text or source)
|
|
|
st.info("Running plagiarism remover pipeline...")
|
|
|
try:
|
|
|
variants = plagiarism_remover_pipeline(source, aggressive=1, light_only=use_light_only)
|
|
|
except Exception as e:
|
|
|
st.error(f"Pipeline failed: {e}")
|
|
|
variants = paraphrase_variants_fast(source, n_variants=variants_to_generate)
|
|
|
if not variants:
|
|
|
st.error("No variants produced.")
|
|
|
else:
|
|
|
st.session_state.versions = variants
|
|
|
st.session_state.version_index = 0
|
|
|
st.session_state.current_text = variants[0]
|
|
|
st.session_state.last_input = source
|
|
|
st.session_state._last_grammar_issues = None
|
|
|
st.session_state._last_output_file = None
|
|
|
st.success(f"Produced {len(variants)} variants.")
|
|
|
|
|
|
if st.button("3) Grammar & Spelling (check)"):
|
|
|
st.session_state._last_tool = "grammar"
|
|
|
source = st.session_state.current_text.strip() or input_text.strip()
|
|
|
if not source:
|
|
|
st.warning("Provide text or upload a file first.")
|
|
|
else:
|
|
|
st.session_state.history.append(st.session_state.current_text or source)
|
|
|
try:
|
|
|
corrected, issues = grammar_and_spelling_check(source)
|
|
|
st.session_state.current_text = corrected
|
|
|
st.session_state.versions = [corrected]
|
|
|
st.session_state.version_index = 0
|
|
|
st.session_state._last_grammar_issues = issues or []
|
|
|
st.success(f"Grammar check applied ({len(issues)} issues).")
|
|
|
|
|
|
|
|
|
uploaded_bytes = st.session_state.get("_uploaded_bytes")
|
|
|
uploaded_name = st.session_state.get("_uploaded_name")
|
|
|
if uploaded_bytes and uploaded_name:
|
|
|
suffix = Path(uploaded_name).suffix.lower()
|
|
|
try:
|
|
|
if suffix == ".docx" and docx is not None:
|
|
|
out_bytes = apply_replacements_to_docx_bytes(uploaded_bytes, source, corrected)
|
|
|
st.session_state._last_output_file = out_bytes
|
|
|
st.session_state._last_output_name = f"corrected_{uploaded_name}"
|
|
|
elif suffix == ".pdf" and fitz is not None:
|
|
|
out_bytes = apply_replacements_to_pdf_bytes(uploaded_bytes, source, corrected)
|
|
|
st.session_state._last_output_file = out_bytes
|
|
|
st.session_state._last_output_name = f"corrected_{uploaded_name}"
|
|
|
elif suffix == ".txt":
|
|
|
st.session_state._last_output_file = corrected.encode("utf-8")
|
|
|
st.session_state._last_output_name = f"corrected_{uploaded_name}"
|
|
|
else:
|
|
|
st.session_state._last_output_file = make_docx_bytes_from_text(corrected)
|
|
|
st.session_state._last_output_name = "corrected_output.docx"
|
|
|
except Exception as e:
|
|
|
st.warning(f"Could not create corrected file preserving format: {e}")
|
|
|
st.session_state._last_output_file = None
|
|
|
st.session_state._last_output_name = None
|
|
|
|
|
|
if issues:
|
|
|
st.subheader("Detected issues (sample):")
|
|
|
for i, it in enumerate(issues[:30]):
|
|
|
st.write(f"- {it.get('message')} → suggestions: {it.get('replacements')}")
|
|
|
except Exception as e:
|
|
|
st.error(f"Grammar check failed: {e}")
|
|
|
|
|
|
|
|
|
st.markdown("---")
|
|
|
st.subheader("Preview / Versions")
|
|
|
colv1, colv2, colv3 = st.columns([1,1,2])
|
|
|
with colv1:
|
|
|
if st.button("◀ Previous Version"):
|
|
|
if st.session_state.versions:
|
|
|
st.session_state.version_index = max(0, st.session_state.version_index - 1)
|
|
|
st.session_state.current_text = st.session_state.versions[st.session_state.version_index]
|
|
|
with colv2:
|
|
|
if st.button("Next Version ▶"):
|
|
|
if st.session_state.versions:
|
|
|
st.session_state.version_index = min(len(st.session_state.versions)-1, st.session_state.version_index + 1)
|
|
|
st.session_state.current_text = st.session_state.versions[st.session_state.version_index]
|
|
|
with colv3:
|
|
|
st.write(f"Version {st.session_state.version_index+1} of {max(1, len(st.session_state.versions))}")
|
|
|
|
|
|
|
|
|
|
|
|
st.markdown("---")
|
|
|
st.subheader("Original (top) — Processed Output (bottom)")
|
|
|
orig_display = st.session_state.last_input or ""
|
|
|
out_display = st.session_state.current_text or (input_text or "")
|
|
|
|
|
|
if st.session_state._last_tool == "grammar" and out_display.strip():
|
|
|
orig_html = mark_grammar_issues(orig_display, st.session_state._last_grammar_issues or []) if orig_display else html.escape(orig_display)
|
|
|
out_html = underline_changes_in_output(orig_display or "", out_display)
|
|
|
st.markdown("<b>Original (issues highlighted)</b>", unsafe_allow_html=True)
|
|
|
st.markdown(f"<div style='padding:8px;border:1px solid #e6e6e6;background:transparent;white-space:pre-wrap'>{orig_html}</div>", unsafe_allow_html=True)
|
|
|
st.markdown("<b>Corrected (changes underlined in green)</b>", unsafe_allow_html=True)
|
|
|
st.markdown(f"<div style='padding:8px;border:1px solid #e6e6e6;background:transparent;white-space:pre-wrap'>{out_html}</div>", unsafe_allow_html=True)
|
|
|
else:
|
|
|
|
|
|
preview_html = text_to_html_with_highlights(orig_display, out_display) if orig_display else html.escape(out_display)
|
|
|
st.markdown(
|
|
|
f"""
|
|
|
<div style='padding:10px;border:1px solid #eee;background:transparent;white-space:pre-wrap'>
|
|
|
{preview_html}
|
|
|
</div>
|
|
|
""",
|
|
|
unsafe_allow_html=True
|
|
|
)
|
|
|
|
|
|
|
|
|
st.subheader("Editable result (you can manually edit before saving)")
|
|
|
st.session_state.editable_area = st.text_area("Edit here:", value=st.session_state.current_text or out_display, height=300)
|
|
|
|
|
|
|
|
|
if st.session_state._last_output_file is not None and st.session_state._last_output_name:
|
|
|
st.markdown("**Download corrected file**")
|
|
|
st.download_button("Download corrected file", data=st.session_state._last_output_file, file_name=st.session_state._last_output_name)
|
|
|
|
|
|
|
|
|
st.markdown("---")
|
|
|
st.markdown("**Spelling suggestions / replace single word:**")
|
|
|
col_s1, col_s2 = st.columns([2,3])
|
|
|
with col_s1:
|
|
|
word_for_sugg = st.text_input("Enter token to suggest replacements:", value="")
|
|
|
if st.button("Get suggestions"):
|
|
|
if not word_for_sugg.strip():
|
|
|
st.warning("Type a token to get suggestions.")
|
|
|
else:
|
|
|
suggs = spelling_suggestions(word_for_sugg, sentence=st.session_state.editable_area)
|
|
|
if suggs:
|
|
|
sel = st.selectbox("Choose replacement:", options=["(keep)"] + suggs)
|
|
|
if sel and sel != "(keep)":
|
|
|
st.session_state.editable_area = st.session_state.editable_area.replace(word_for_sugg, sel)
|
|
|
st.success(f"Replaced '{word_for_sugg}' with '{sel}'")
|
|
|
else:
|
|
|
st.info("No suggestions found.")
|
|
|
with col_s2:
|
|
|
if st.button("Apply editable area to current text"):
|
|
|
st.session_state.current_text = st.session_state.editable_area
|
|
|
st.success("Applied edits to current text.")
|
|
|
|
|
|
|
|
|
st.markdown("---")
|
|
|
col_d1, col_d2, col_d3 = st.columns(3)
|
|
|
with col_d1:
|
|
|
if st.button("Save as DOCX"):
|
|
|
try:
|
|
|
b = make_docx_bytes_from_text(st.session_state.editable_area or "")
|
|
|
st.download_button("Download DOCX", data=b, file_name="rephrased.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
|
|
|
except Exception as e:
|
|
|
st.error(f"Could not create DOCX: {e}")
|
|
|
with col_d2:
|
|
|
if st.button("Save as PDF"):
|
|
|
try:
|
|
|
b = make_pdf_bytes_from_text(st.session_state.editable_area or "")
|
|
|
st.download_button("Download PDF", data=b, file_name="rephrased.pdf", mime="application/pdf")
|
|
|
except Exception as e:
|
|
|
st.error(f"Could not create PDF: {e}")
|
|
|
with col_d3:
|
|
|
if st.button("Copy to clipboard"):
|
|
|
if PYPERCLIP:
|
|
|
pyperclip.copy(st.session_state.editable_area or "")
|
|
|
st.success("Copied to clipboard")
|
|
|
else:
|
|
|
path = os.path.join(tempfile.gettempdir(), "rephrased_output.txt")
|
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
|
f.write(st.session_state.editable_area or "")
|
|
|
st.info(f"Saved to {path} (pyperclip not available)")
|
|
|
|
|
|
|
|
|
if st.button("Undo"):
|
|
|
if st.session_state.history:
|
|
|
st.session_state.current_text = st.session_state.history.pop()
|
|
|
st.session_state.versions = [st.session_state.current_text]
|
|
|
st.session_state.version_index = 0
|
|
|
st.success("Undone last step")
|
|
|
else:
|
|
|
st.info("Nothing to undo")
|
|
|
|
|
|
st.markdown("---")
|
|
|
st.caption("Notes: Paraphraser & Plagiarism Remover code preserved. Grammar prefers LanguageTool (requires Java) else falls back to TextBlob. DOCX/PDF replacements are best-effort to preserve layout.")
|
|
|
|
|
|
|
|
|
|
|
|
if st.button("🔄 Refresh"):
|
|
|
st.rerun()
|
|
|
|