Spaces:
Running on Zero
Running on Zero
| # ============================================================ | |
| # app.py โ Tortured Phrase Detector (ZeroGPU-compatible) | |
| # ============================================================ | |
| import spaces # โ MUST be the very first import | |
| import os | |
| import re | |
| import json | |
| import torch | |
| import gradio as gr | |
| import pandas as pd | |
| from datetime import datetime | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| from peft import PeftModel | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # CONFIG | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| BASE_MODEL_ID = "meta-llama/Llama-3.2-1B-Instruct" | |
| STANDARD_ADAPTER_ID = "skygg/paperlytix_non_reasoning_medical_trained_adapter" | |
| REASONING_ADAPTER_ID = "skygg/paperlytix_reasoning_medical_trained_adapter" | |
| SONIC_DB_PATH = "./search_tortured_correct.csv" | |
| SONIC_RESULTS_DIR = "./sonic_text_results" | |
| DEEPER_RESULTS_DIR = "./go_deeper_text_results" | |
| HYBRIONIX_RESULTS_DIR = "./hybrionix_text_results" | |
| GO_BEYOND_RESULTS_DIR = "./go_beyond_text_results" | |
| MAX_NEW_TOKENS = 512 | |
| MAX_NEW_TOKENS_REASONING = 1024 | |
| MAX_INPUT_CHARS = 3000 | |
| HF_TOKEN = os.environ.get("HF_TOKEN", None) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # SYSTEM PROMPTS | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| STANDARD_SYSTEM_PROMPT = ( | |
| "You are a scientific language correction assistant specializing in " | |
| "Clinical Medicine. Your task is to detect 'tortured phrases' in the " | |
| "given sentence โ these are unnatural, paraphrased substitutions of " | |
| "standard medical terminology, often introduced by paraphrasing tools " | |
| "or paper mills to evade plagiarism detection. Identify each tortured " | |
| "phrase, replace it with the correct and widely accepted clinical term, " | |
| "and briefly explain why the original phrasing is non-standard. If no " | |
| "tortured phrases are found, explicitly state that the sentence uses " | |
| "correct clinical terminology." | |
| ) | |
| REASONING_SYSTEM_PROMPT = ( | |
| "You are a scientific language correction assistant specializing in " | |
| "Clinical Medicine. Your task is to detect 'tortured phrases' in the " | |
| "given sentence โ these are unnatural, paraphrased substitutions of " | |
| "standard medical terminology, often introduced by paraphrasing tools " | |
| "or paper mills to evade plagiarism detection.\n\n" | |
| "Follow this strict chain-of-thought reasoning process before giving " | |
| "your final answer:\n\n" | |
| "Step 1 - Segment the sentence: Break the sentence into individual " | |
| "medical phrases or noun groups.\n" | |
| "Step 2 - Evaluate each phrase: For each phrase, ask โ Is this a " | |
| "recognized, standard clinical/medical term used in peer-reviewed " | |
| "literature or clinical guidelines? If not, flag it as a potential " | |
| "tortured phrase.\n" | |
| "Step 3 - Identify the distortion pattern: For each flagged phrase, " | |
| "explain what standard term it appears to be paraphrasing and why the " | |
| "substitution is linguistically or clinically non-standard.\n" | |
| "Step 4 - Provide the correction: Replace each tortured phrase with " | |
| "the correct, widely accepted clinical terminology.\n" | |
| "Step 5 - Output the corrected sentence: Present the fully corrected " | |
| "sentence.\n\n" | |
| "If no tortured phrases are found after this reasoning, explicitly " | |
| "state that the sentence uses correct clinical terminology." | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # GLOBAL MODEL STATE | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _tokenizer = None | |
| _model = None | |
| _sonic_df = None | |
| def load_models(): | |
| """ | |
| Load base model + attach both adapters, then move to 'cuda' | |
| directly โ this is the officially documented ZeroGPU pattern. | |
| The `spaces` package (imported first, above) patches torch so | |
| this call is safe even though no physical GPU is attached to | |
| this process yet; the real transfer happens automatically when | |
| a @spaces.GPU function is later invoked. | |
| NOTE: the adapters are explicitly loaded onto CPU (torch_device="cpu") | |
| because PEFT auto-infers "cuda" as the load device (since `spaces` | |
| reports torch.cuda.is_available() == True) and tries to load the | |
| safetensors weights *directly* onto a real CUDA device โ which fails | |
| with "No CUDA GPUs are available" since no physical GPU is attached | |
| at this point in the process. Loading to CPU first and moving the | |
| whole composed model with `.to("cuda")` afterward is the supported | |
| deferred-placement pattern. | |
| """ | |
| global _tokenizer, _model | |
| print("๐ Loading tokenizer ...") | |
| _tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID, token=HF_TOKEN) | |
| if _tokenizer.pad_token is None: | |
| _tokenizer.pad_token = _tokenizer.eos_token | |
| _tokenizer.pad_token_id = _tokenizer.eos_token_id | |
| print("๐ Loading base LLaMA model ...") | |
| base_model = AutoModelForCausalLM.from_pretrained( | |
| BASE_MODEL_ID, | |
| dtype=torch.float16, # โ dtype instead of torch_dtype | |
| token=HF_TOKEN | |
| ) | |
| print(f"๐ Attaching standard adapter: {STANDARD_ADAPTER_ID}") | |
| _model = PeftModel.from_pretrained( | |
| base_model, | |
| STANDARD_ADAPTER_ID, | |
| adapter_name="standard", | |
| token=HF_TOKEN, | |
| torch_device="cpu" | |
| ) | |
| print(f"๐ Attaching reasoning adapter: {REASONING_ADAPTER_ID}") | |
| _model.load_adapter( | |
| REASONING_ADAPTER_ID, | |
| adapter_name="reasoning", | |
| token=HF_TOKEN, | |
| torch_device="cpu" | |
| ) | |
| # โโ Official ZeroGPU pattern: move to cuda right after load โ | |
| _model.to("cuda") | |
| _model.eval() | |
| print("โ Model ready with both adapters, moved to CUDA.\n") | |
| def load_sonic_db(): | |
| global _sonic_df | |
| if _sonic_df is None: | |
| print("๐ Loading Sonic helper database ...") | |
| _sonic_df = pd.read_csv(SONIC_DB_PATH, encoding="utf-8") | |
| print(f"โ Sonic database loaded โ {len(_sonic_df)} pairs.\n") | |
| return _sonic_df | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # SHARED UTILITIES | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def split_into_sentences(text): | |
| raw = re.split(r'(?<=[.!?])\s+', text.strip()) | |
| sentences = [s.strip() for s in raw if s.strip()] | |
| return sentences | |
| def validate_input(text): | |
| if not text or not text.strip(): | |
| return False, "Please enter some text to analyze." | |
| if len(text) > MAX_INPUT_CHARS: | |
| return False, ( | |
| f"Input too long ({len(text)} characters). " | |
| f"Please limit to {MAX_INPUT_CHARS} characters per request." | |
| ) | |
| return True, None | |
| def build_pattern(phrase_lower): | |
| escaped = re.escape(phrase_lower) | |
| return re.compile(r'\b' + escaped + r"s?\b", re.IGNORECASE) | |
| def get_next_result_index(folder_path): | |
| os.makedirs(folder_path, exist_ok=True) | |
| existing = [ | |
| f for f in os.listdir(folder_path) | |
| if f.startswith("test_") and f.endswith("_results.json") | |
| ] | |
| if not existing: | |
| return 1 | |
| indices = [] | |
| for fname in existing: | |
| try: | |
| idx = int(fname.replace("test_", "").replace("_results.json", "")) | |
| indices.append(idx) | |
| except ValueError: | |
| continue | |
| return max(indices) + 1 if indices else 1 | |
| def save_results(folder_path, engine_name, input_text, | |
| sentences, results_per_sentence, extra_data=None): | |
| os.makedirs(folder_path, exist_ok=True) | |
| idx = get_next_result_index(folder_path) | |
| filepath = os.path.join(folder_path, f"test_{idx}_results.json") | |
| output = { | |
| "test_index" : idx, | |
| "engine" : engine_name, | |
| "timestamp" : datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| "input_text" : input_text, | |
| "total_sentences" : len(sentences), | |
| "results" : [] | |
| } | |
| if extra_data: | |
| output.update(extra_data) | |
| for i, (sentence, result) in enumerate( | |
| zip(sentences, results_per_sentence), start=1 | |
| ): | |
| output["results"].append({ | |
| "sentence_index" : i, | |
| "original" : sentence, | |
| "output" : result | |
| }) | |
| with open(filepath, "w", encoding="utf-8") as f: | |
| json.dump(output, f, indent=2, ensure_ascii=False) | |
| return filepath | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ENGINE : SONIC (pure CPU โ CSV lookup, no GPU needed) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def sonic_process_sentence(sentence, df): | |
| found = [] | |
| for _, row in df.iterrows(): | |
| tortured_lower = str(row["tortured_lower"]).strip() | |
| correct = str(row["correct"]).strip() | |
| tortured_orig = str(row["tortured"]).strip() | |
| pattern = build_pattern(tortured_lower) | |
| match = pattern.search(sentence) | |
| if match: | |
| found.append((tortured_orig, match.group(0), correct)) | |
| if not found: | |
| return "No tortured phrases detected." | |
| corrected = sentence | |
| for tortured_orig, matched_text, correct in found: | |
| pattern = build_pattern(tortured_orig.lower()) | |
| replacement = correct + "s" if matched_text.lower() == \ | |
| (tortured_orig.lower() + "s") else correct | |
| if matched_text[0].isupper(): | |
| replacement = replacement[0].upper() + replacement[1:] | |
| corrected = pattern.sub(replacement, corrected, count=1) | |
| lines = [f"Corrected: {corrected}", "", "Tortured phrases detected:"] | |
| for idx, (tortured_orig, matched_text, correct) in enumerate(found, 1): | |
| lines.append(f'{idx}. [Tortured Phrase] "{matched_text}" โ "{correct}"') | |
| return "\n".join(lines) | |
| def run_sonic(sentences): | |
| df = load_sonic_db() | |
| return [sonic_process_sentence(s, df) for s in sentences] | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # GPU-DECORATED GENERATION FUNCTION | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def generate_batch(sentences, adapter_name, system_prompt, max_new_tokens): | |
| """ | |
| ZeroGPU attaches a real GPU only for the duration of this call. | |
| Model is already on 'cuda' (set at load time) โ no device | |
| transfer needed here. | |
| """ | |
| _model.set_adapter(adapter_name) | |
| results = [] | |
| for sentence in sentences: | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": sentence} | |
| ] | |
| input_text = _tokenizer.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=True | |
| ) | |
| inputs = _tokenizer(input_text, return_tensors="pt").to("cuda") | |
| with torch.no_grad(): | |
| output_ids = _model.generate( | |
| **inputs, | |
| max_new_tokens = max_new_tokens, | |
| do_sample = False, | |
| pad_token_id = _tokenizer.eos_token_id | |
| ) | |
| generated_ids = output_ids[0][inputs["input_ids"].shape[-1]:] | |
| response = _tokenizer.decode(generated_ids, skip_special_tokens=True) | |
| results.append(response.strip()) | |
| return results | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # MAIN DISPATCH | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def run_detection(text, engine_choice, progress=gr.Progress()): | |
| is_valid, error = validate_input(text) | |
| if not is_valid: | |
| return f"โ ๏ธ {error}" | |
| sentences = split_into_sentences(text) | |
| progress(0.2, desc="Analyzing your text...") | |
| if engine_choice.startswith("โก"): | |
| results = run_sonic(sentences) | |
| save_results(SONIC_RESULTS_DIR, "Sonic", text, sentences, results) | |
| elif engine_choice.startswith("๐ง "): | |
| progress(0.4, desc="Running AI detection (Go Deeper)...") | |
| results = generate_batch( | |
| sentences, "standard", STANDARD_SYSTEM_PROMPT, MAX_NEW_TOKENS | |
| ) | |
| save_results(DEEPER_RESULTS_DIR, "Go Deeper", text, sentences, results) | |
| elif engine_choice.startswith("๐ฎ"): | |
| progress(0.4, desc="Running AI reasoning (Go Beyond)...") | |
| results = generate_batch( | |
| sentences, "reasoning", REASONING_SYSTEM_PROMPT, | |
| MAX_NEW_TOKENS_REASONING | |
| ) | |
| save_results(GO_BEYOND_RESULTS_DIR, "Go Beyond", text, sentences, results) | |
| elif engine_choice.startswith("๐"): | |
| progress(0.3, desc="Running Go Deeper (AI)...") | |
| deeper_results = generate_batch( | |
| sentences, "standard", STANDARD_SYSTEM_PROMPT, MAX_NEW_TOKENS | |
| ) | |
| progress(0.7, desc="Running Sonic cross-check...") | |
| sonic_results = run_sonic(sentences) | |
| results = [] | |
| for d, s_r in zip(deeper_results, sonic_results): | |
| agree = "No tortured phrases" in d and "No tortured phrases" in s_r | |
| if agree: | |
| results.append(f"{d}\n\nโ Confirmed by both Sonic and Go Deeper.") | |
| else: | |
| results.append(f"{d}\n\n---\n[Sonic cross-check]\n{s_r}") | |
| save_results( | |
| HYBRIONIX_RESULTS_DIR, "Hybrionix", text, sentences, results, | |
| extra_data={ | |
| "go_deeper_results": deeper_results, | |
| "sonic_results": sonic_results | |
| } | |
| ) | |
| else: | |
| return "โ ๏ธ Please select a valid engine." | |
| progress(0.95, desc="Formatting results...") | |
| output_lines = [] | |
| for i, (sentence, result) in enumerate(zip(sentences, results), start=1): | |
| output_lines.append(f"### Sentence {i}") | |
| output_lines.append(f"**Original:** {sentence}") | |
| output_lines.append("") | |
| output_lines.append(result) | |
| output_lines.append("\n---\n") | |
| return "\n".join(output_lines) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # GRADIO INTERFACE | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ENGINE_CHOICES = [ | |
| "โก Sonic (fastest)", | |
| "๐ง Go Deeper (AI, direct)", | |
| "๐ Hybrionix (AI + Sonic)", | |
| "๐ฎ Go Beyond (AI, reasoning)", | |
| ] | |
| EXAMPLE_INPUTS = [ | |
| ["The patient was diagnosed with bosom malignant growth and referred " | |
| "for assessment of cerebrum dead tissue.", "๐ฎ Go Beyond (AI, reasoning)"], | |
| ["The patient was diagnosed with myocardial infarction and started on " | |
| "dual antiplatelet therapy.", "โก Sonic (fastest)"], | |
| ["Cardiovascular breakdown was noted alongside glucose narrow " | |
| "mindedness in the follow-up report.", "๐ Hybrionix (AI + Sonic)"], | |
| ] | |
| DESCRIPTION = """ | |
| # ๐ฌ Tortured Phrase Detector โ Clinical Medicine | |
| **What is this?** | |
| A free tool that scans clinical/medical research text for **"tortured phrases"** โ | |
| unnatural, paraphrased substitutions of standard medical terminology often | |
| introduced by paraphrasing tools or paper mills to evade plagiarism detection. | |
| **How to use it:** | |
| 1. Paste a sentence or paragraph from a clinical research paper below | |
| 2. Choose a detection engine (see guide below) | |
| 3. Click **Detect & Correct** | |
| **Choosing an engine:** | |
| | Engine | Speed | Best for | | |
| |---|---|---| | |
| | โก Sonic | Instant | Quick checks, no AI needed | | |
| | ๐ง Go Deeper | Fast (GPU) | AI-powered detection with brief explanations | | |
| | ๐ Hybrionix | Fast (GPU) | Cross-checked results from both Sonic + AI | | |
| | ๐ฎ Go Beyond | Fast (GPU) | Full step-by-step reasoning โ best for review/audit | | |
| โก *Powered by ZeroGPU โ a GPU is allocated on-demand for each AI request.* | |
| """ | |
| with gr.Blocks(title="Tortured Phrase Detector") as demo: | |
| gr.Markdown(DESCRIPTION) | |
| with gr.Row(): | |
| with gr.Column(): | |
| text_input = gr.Textbox( | |
| label="Paste your sentence(s) here", | |
| placeholder="e.g. The patient was diagnosed with bosom " | |
| "malignant growth...", | |
| lines=6 | |
| ) | |
| engine_choice = gr.Radio( | |
| choices=ENGINE_CHOICES, | |
| value="โก Sonic (fastest)", | |
| label="Choose your detection engine" | |
| ) | |
| submit_btn = gr.Button("๐ Detect & Correct", variant="primary") | |
| with gr.Column(): | |
| output_box = gr.Markdown(label="Results") | |
| gr.Examples( | |
| examples=EXAMPLE_INPUTS, | |
| inputs=[text_input, engine_choice], | |
| label="Try an example โฌ๏ธ" | |
| ) | |
| submit_btn.click( | |
| fn=run_detection, | |
| inputs=[text_input, engine_choice], | |
| outputs=output_box | |
| ) | |
| gr.Markdown( | |
| "---\n" | |
| "โ ๏ธ *This is a research-assistance tool, not a substitute for " | |
| "professional editorial or clinical review.*" | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # STARTUP | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| for folder in [SONIC_RESULTS_DIR, DEEPER_RESULTS_DIR, | |
| HYBRIONIX_RESULTS_DIR, GO_BEYOND_RESULTS_DIR]: | |
| os.makedirs(folder, exist_ok=True) | |
| print("๐ Loading models at startup ...") | |
| load_models() | |
| load_sonic_db() | |
| print("โ App ready to launch!") | |
| if __name__ == "__main__": | |
| demo.launch() |