Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import os | |
| import re | |
| from inference import predict | |
| from download_report import generate_report_pdf | |
| from utils import compute_all_linguistic_features, text_input_generate, generate_explanation_with_gemma | |
| # st.title("AI vs Human text Classifier", anchor=False) | |
| st.set_page_config(page_title="Report Generator", layout="wide", initial_sidebar_state="collapsed") | |
| st.markdown("<h1 style='text-align: center;'>AI vs Human text Classifier</h1>", unsafe_allow_html=True) | |
| st.divider() | |
| MAX_WORDS = 1000 | |
| def on_explanation_toggle(): | |
| """Callback when explanation toggle is changed.""" | |
| if st.session_state.show_explanation: | |
| st.session_state.selected_model = "nn_model_bert" | |
| def enforce_word_limit(): | |
| text = st.session_state.get("input_text", "") | |
| word_count = len(text.split()) | |
| if word_count > MAX_WORDS: | |
| # Truncate to max words | |
| words = text.split() | |
| st.session_state.input_text = " ".join(words[:MAX_WORDS]) | |
| def extract_text_from_file(uploaded_file): | |
| """Extract text from an uploaded .doc, .docx, or .pdf file.""" | |
| file_extension = os.path.splitext(uploaded_file.name)[1].lower() | |
| text = "" | |
| if file_extension == ".pdf": | |
| try: | |
| import PyPDF2 | |
| pdf_reader = PyPDF2.PdfReader(uploaded_file) | |
| for page in pdf_reader.pages: | |
| text += page.extract_text() + "\n" | |
| except ImportError: | |
| st.error("PyPDF2 is required to read PDF files. Install it with: pip install PyPDF2") | |
| return "" | |
| elif file_extension == ".docx": | |
| try: | |
| import docx | |
| doc = docx.Document(uploaded_file) | |
| text = "\n".join([paragraph.text for paragraph in doc.paragraphs]) | |
| except ImportError: | |
| st.error("python-docx is required to read Word files. Install it with: pip install python-docx") | |
| return "" | |
| # Limit to MAX_WORDS | |
| words = text.split() | |
| if len(words) > MAX_WORDS: | |
| text = " ".join(words[:MAX_WORDS]) | |
| return text | |
| def on_file_upload(): | |
| """Callback when a file is uploaded. Populates the text area with extracted content.""" | |
| uploaded_file = st.session_state.get("file_uploader") | |
| if uploaded_file is not None: | |
| extracted_text = extract_text_from_file(uploaded_file) | |
| if extracted_text: | |
| st.session_state.input_text = extracted_text | |
| def generate_text_stats(input_text): | |
| """Generate statistics for the input text.""" | |
| words = input_text.split() | |
| word_count = len(words) | |
| # Sentence length distribution (split by . ! ?) | |
| sentences = re.split(r'[.!?]+', input_text) | |
| sentences = [s.strip() for s in sentences if s.strip()] | |
| sentence_lengths = [len(s.split()) for s in sentences] | |
| stats_lines = [f"**Word Count:** {word_count}"] | |
| stats_lines.append("") | |
| stats_lines.append("**Sentence Length Distribution:**") | |
| if sentence_lengths: | |
| avg_sent_len = sum(sentence_lengths) / len(sentence_lengths) | |
| stats_lines.append(f"- Number of sentences: {len(sentences)}") | |
| stats_lines.append(f"- Average sentence length: {avg_sent_len:.1f} words") | |
| stats_lines.append(f"- Shortest sentence: {min(sentence_lengths)} words") | |
| stats_lines.append(f"- Longest sentence: {max(sentence_lengths)} words") | |
| stats_lines.append("") | |
| stats_lines.append("| Sentence # | Word Count |") | |
| stats_lines.append("|------------|------------|") | |
| for i, slen in enumerate(sentence_lengths, 1): | |
| stats_lines.append(f"| {i} | {slen} |") | |
| else: | |
| stats_lines.append("- No sentences detected.") | |
| return "\n".join(stats_lines) | |
| def generate_prediction_response(input_text, selected_model): | |
| """Run inference and format the response.""" | |
| # Load model (already cached in inference.py) | |
| label, proba = predict(input_text, selected_model) | |
| label_str = "AI-generated" if label == 1 else "Human-written" | |
| confidence = proba if label == 1 else 1 - proba | |
| lines = [f"**Model:** {selected_model}"] | |
| lines.append("") | |
| lines.append(f"**Prediction:** {label_str}") | |
| lines.append(f"**Confidence:** {confidence:.4f} ({confidence:.2%})") | |
| return "\n".join(lines) | |
| def generate_linguistic_features_text(input_text): | |
| """Generate linguistic features report.""" | |
| features = compute_all_linguistic_features(input_text) | |
| lines = ["**Linguistic Features Analysis:**", ""] | |
| lines.append(f"**Burstiness:** {features['burstiness']:.4f}") | |
| lines.append(f"- Measures irregular word repetition patterns") | |
| lines.append(f"- Higher values indicate more bursty (human-like) writing") | |
| lines.append("") | |
| lines.append(f"**TTR (Type-Token Ratio):** {features['TTR']:.4f}") | |
| lines.append(f"- Ratio of unique words to total words") | |
| lines.append(f"- Higher values indicate richer vocabulary") | |
| lines.append("") | |
| lines.append(f"**CV (Coefficient of Variation of Sentence Length):** {features['CV_sentence_len']:.4f}") | |
| lines.append(f"- Standard deviation divided by mean of sentence lengths") | |
| lines.append(f"- Higher values indicate more variation in sentence structure") | |
| lines.append("") | |
| lines.append(f"**Perplexity:** {features['perplexity']:.4f}") | |
| lines.append(f"- How 'surprised' GPT-2 is by the text") | |
| lines.append(f"- Lower values indicate more predictable (likely AI-generated) text") | |
| return "\n".join(lines) | |
| left_col, right_col = st.columns([0.38, 0.62], gap="large") | |
| with left_col: | |
| # st.markdown('<div class="section-label">Title</div>', unsafe_allow_html=True) | |
| st.text("Paste your text here or Upload a file") | |
| st.text_area("", value="", height=150, placeholder="text field", label_visibility="collapsed", key="input_text", on_change=enforce_word_limit) | |
| word_count = len(st.session_state.get("input_text", "").split()) | |
| if word_count > 0: | |
| if word_count > MAX_WORDS: | |
| st.warning(f"Word limit exceeded! Maximum {MAX_WORDS} words allowed. (Current: {word_count})") | |
| else: | |
| st.caption(f"Word count: {word_count}/{MAX_WORDS}") | |
| st.markdown("<p style='text-align: center; margin: 8px 0;'><b>OR</b></p>", unsafe_allow_html=True) | |
| st.file_uploader("Upload a file (.docx, .pdf)", type=["docx", "pdf"], accept_multiple_files=False, label_visibility="collapsed", key="file_uploader", on_change=on_file_upload) | |
| st.divider() | |
| model_col, dropdown_col = st.columns([0.8, 0.2]) | |
| with model_col: | |
| model_files = [os.path.splitext(f)[0] for f in os.listdir("models") if f.endswith((".pkl", ".h5", ".pt")) and os.path.splitext(f)[0] != "tfidf_vectorizer"] | |
| model_options = ["pick model here"] + sorted(model_files) + ["all models for comparison"] | |
| st.selectbox("", model_options, label_visibility="collapsed", key="selected_model") | |
| show_explanation = st.toggle("Explanation on", key="show_explanation", value=False, on_change=on_explanation_toggle) | |
| if show_explanation: | |
| st.info("Since explanation is on, we will be using nn_model_bert. Click Generate report.") | |
| st.divider() | |
| if st.button("Generate report", use_container_width=False, type="primary"): | |
| input_text = st.session_state.get("input_text", "").strip() | |
| selected_model = st.session_state.get("selected_model", "pick model here") | |
| show_explanation = st.session_state.get("show_explanation", False) | |
| if not input_text: | |
| st.warning("Please enter some text or upload a file first.") | |
| else: | |
| # Generate text statistics | |
| stats_text = generate_text_stats(input_text) | |
| # Generate prediction response | |
| if selected_model == "pick model here": | |
| response_text = "**No model selected.** Please choose a model from the dropdown." | |
| elif selected_model == "all models for comparison": | |
| # Run all models and compare (exclude tfidf_vectorizer since it's not a classifier) | |
| model_names = [m for m in model_files if m != "tfidf_vectorizer"] | |
| response_lines = ["**All Models Comparison:**", ""] | |
| response_lines.append("| Model | Prediction | Confidence |") | |
| response_lines.append("|-------|------------|------------|") | |
| for mname in model_names: | |
| try: | |
| label, proba = predict(input_text, mname) | |
| label_str = "AI" if label == 1 else "Human" | |
| conf = proba if label == 1 else 1 - proba | |
| response_lines.append(f"| {mname} | {label_str} | {conf:.2%} |") | |
| except Exception as e: | |
| response_lines.append(f"| {mname} | Error | {str(e)} |") | |
| response_text = "\n".join(response_lines) | |
| else: | |
| response_text = generate_prediction_response(input_text, selected_model) | |
| # Generate linguistic features if explanation is on | |
| if show_explanation: | |
| linguistic_text = generate_linguistic_features_text(input_text) | |
| features = compute_all_linguistic_features(input_text) | |
| # Get prediction details | |
| if selected_model != "pick model here" and selected_model != "all models for comparison": | |
| label, proba = predict(input_text, selected_model) | |
| label_str = "AI-generated" if label == 1 else "Human-written" | |
| confidence = proba if label == 1 else 1 - proba | |
| # Generate combined text for later use (not shown to user) | |
| combined_text = text_input_generate(input_text, label_str, confidence, features) | |
| st.session_state.combined_text_input = combined_text | |
| # Generate explanation using Gemma model | |
| with st.spinner("Generating explanation with Gemma model..."): | |
| explanation = generate_explanation_with_gemma(combined_text) | |
| st.session_state.gemma_explanation = explanation | |
| st.session_state.linguistic_features = linguistic_text | |
| st.session_state.report_stats = stats_text | |
| st.session_state.report_response = response_text | |
| with right_col: | |
| text_stats = st.container() | |
| response = st.container() | |
| linguistic_features = st.container() | |
| gemma_explanation_container = st.container() | |
| show_explanation = st.session_state.get("show_explanation", False) | |
| has_report = bool(st.session_state.get("report_stats")) and bool(st.session_state.get("report_response")) | |
| download_left, download_right = st.columns([0.64, 0.36]) | |
| with download_right: | |
| if has_report: | |
| st.download_button( | |
| "Download report", | |
| data=generate_report_pdf( | |
| st.session_state.report_stats, | |
| st.session_state.report_response, | |
| ), | |
| file_name="report.pdf", | |
| mime="application/pdf", | |
| use_container_width=True, | |
| ) | |
| else: | |
| st.button("Download report", disabled=True, use_container_width=True) | |
| show_explanation = st.session_state.get("show_explanation", False) | |
| with text_stats: | |
| report_stats = st.session_state.get("report_stats", "") | |
| if report_stats and show_explanation: | |
| st.markdown(report_stats) | |
| elif not show_explanation: | |
| st.markdown("*Explanations are turned off.*") | |
| else: | |
| st.markdown("*No statistics yet.*") | |
| with response: | |
| report_response = st.session_state.get("report_response", "") | |
| if report_response and show_explanation: | |
| st.markdown(report_response) | |
| elif not show_explanation: | |
| st.markdown('*Explanations are turned off.*') | |
| else: | |
| st.markdown('*No response yet.*') | |
| with linguistic_features: | |
| linguistic_text = st.session_state.get("linguistic_features", "") | |
| if linguistic_text and show_explanation: | |
| st.divider() | |
| st.markdown(linguistic_text) | |
| elif not show_explanation: | |
| st.divider() | |
| st.markdown('*Linguistic features are turned off.*') | |
| with gemma_explanation_container: | |
| gemma_explanation = st.session_state.get("gemma_explanation", "") | |
| if gemma_explanation and show_explanation: | |
| st.divider() | |
| st.subheader("AI Explanation") | |
| st.markdown(gemma_explanation) | |
| st.markdown("</div>", unsafe_allow_html=True) | |