Spaces:
Running
Running
| """Lynote AI Text Detector Lite — bilingual, dependency-light heuristic demo. | |
| The detector is transparent and CPU-only: it computes a small set of | |
| well-documented statistical signals for AI-generated prose (low burstiness, | |
| formulaic phrase density, repeated n-grams, uniform vocabulary) and combines | |
| them into a single probabilistic score. It needs no model download, starts | |
| instantly, and works on English, Chinese, and other languages. | |
| The score is a probabilistic signal, not proof of authorship. | |
| """ | |
| import math | |
| import re | |
| from collections import Counter | |
| import gradio as gr | |
| MAX_CHARS = 5_000 | |
| MIN_CHARS = 80 | |
| UTM_URL = ( | |
| "https://lynote.ai/ai-detector?utm_source=huggingface" | |
| "&utm_medium=space&utm_campaign=hf_launch&utm_content=text_detector" | |
| ) | |
| # High-confidence formulaic phrases (mirrors spaces/humanizer-lite). | |
| EN_PHRASES = [ | |
| "in today's rapidly evolving world", | |
| "in today's fast-paced world", | |
| "it is important to note that", | |
| "it is worth noting that", | |
| "serves as a testament to", | |
| "delve into", | |
| "leverage", | |
| "seamless", | |
| "robust", | |
| "game-changer", | |
| "groundbreaking", | |
| "cutting-edge", | |
| "state-of-the-art", | |
| "plays a crucial role", | |
| "in the realm of", | |
| "in conclusion", | |
| "moreover", | |
| "furthermore", | |
| ] | |
| ZH_PHRASES = [ | |
| "值得注意的是", | |
| "综上所述", | |
| "在当今快速发展的时代", | |
| "赋能", | |
| "助力", | |
| "降本增效", | |
| "闭环", | |
| "无缝", | |
| "总而言之", | |
| "由此可见", | |
| "发挥着重要作用", | |
| ] | |
| WEIGHTS = { | |
| "burstiness": 0.35, | |
| "formulaic_phrases": 0.25, | |
| "repetition": 0.20, | |
| "vocabulary_uniformity": 0.20, | |
| } | |
| def _words(text: str): | |
| latin = re.findall(r"[A-Za-z0-9]+", text) | |
| han = re.findall(r"[\u4e00-\u9fff]", text) | |
| return latin + han | |
| def _sentences(text: str): | |
| return [ | |
| s.strip() | |
| for s in re.split(r"[。!?!?;;\n]+|\.(?:\s+|$)", text) | |
| if s.strip() | |
| ] | |
| def _burstiness(text: str) -> float: | |
| """1.0 = uniform sentence rhythm (AI-like); 0.0 = bursty (human-like).""" | |
| lengths = [max(len(_words(s)), 1) for s in _sentences(text)] | |
| if len(lengths) < 3: | |
| return 0.5 | |
| mean = sum(lengths) / len(lengths) | |
| variance = sum((length - mean) ** 2 for length in lengths) / len(lengths) | |
| cv = math.sqrt(variance) / mean | |
| return 1.0 - min(cv / 0.75, 1.0) | |
| def _formulaic_phrases(text: str) -> float: | |
| """Hits of known AI clichés per 100 tokens, clamped to [0, 1].""" | |
| lowered = text.lower() | |
| hits = 0 | |
| for phrase in EN_PHRASES: | |
| hits += len(re.findall(re.escape(phrase), lowered)) | |
| for phrase in ZH_PHRASES: | |
| hits += text.count(phrase) | |
| per_hundred = hits * 100 / max(len(_words(text)), 1) | |
| return min(per_hundred / 2.0, 1.0) | |
| def _repetition(text: str) -> float: | |
| """Repeated 3-grams plus repeated sentence starters, scaled to [0, 1].""" | |
| tokens = _words(text) | |
| if len(tokens) < 12: | |
| return 0.5 | |
| grams = [tuple(tokens[i:i + 3]) for i in range(len(tokens) - 2)] | |
| counts = Counter(grams) | |
| total = len(grams) | |
| repeated = sum(count for count in counts.values() if count > 1) | |
| ngram_rate = repeated / total | |
| sentences = _sentences(text) | |
| starters = [tuple(_words(s)[:2]) for s in sentences if _words(s)] | |
| starter_counts = Counter(starters) | |
| starter_repeats = sum(count - 1 for count in starter_counts.values() if count > 1) | |
| starter_rate = starter_repeats / max(len(starters), 1) | |
| return min(ngram_rate * 2.5 + starter_rate, 1.0) | |
| def _vocabulary_uniformity(text: str) -> float: | |
| """1.0 = very repetitive vocabulary; 0.0 = highly diverse.""" | |
| tokens = _words(text) | |
| if len(tokens) < 8: | |
| return 0.5 | |
| ttr = len(set(tokens)) / len(tokens) | |
| return 1.0 - ttr | |
| def detect(text: str): | |
| original = (text or "").strip() | |
| if not original: | |
| return "Paste some text to run the detector.", {} | |
| if len(original) > MAX_CHARS: | |
| return f"Input is limited to {MAX_CHARS:,} characters in this demo.", {} | |
| if len(original) < MIN_CHARS: | |
| return ( | |
| f"Add at least {MIN_CHARS:,} characters for a meaningful signal " | |
| "(short texts are easy to misjudge).", | |
| {}, | |
| ) | |
| signals = { | |
| "burstiness": round(_burstiness(original), 3), | |
| "formulaic_phrases": round(_formulaic_phrases(original), 3), | |
| "repetition": round(_repetition(original), 3), | |
| "vocabulary_uniformity": round(_vocabulary_uniformity(original), 3), | |
| } | |
| probability = sum(signals[name] * weight for name, weight in WEIGHTS.items()) | |
| probability = min(max(probability, 0.0), 1.0) | |
| if probability < 0.35: | |
| band = "Weak AI-generated signal" | |
| elif probability < 0.65: | |
| band = "Uncertain / mixed signal" | |
| else: | |
| band = "Strong AI-generated signal" | |
| message = f""" | |
| ## {band} | |
| Estimated AI-generated score: **{probability:.1%}** | |
| This is a transparent statistical heuristic, not proof of origin. Human writing | |
| can be polished, and AI writing can be varied; very short, translated, or | |
| heavily edited texts are especially easy to misjudge. Do not use this demo | |
| alone for disciplinary, legal, or moderation decisions. | |
| [Try Lynote's full detection experience]({UTM_URL}) | |
| """ | |
| return message, signals | |
| DESCRIPTION = f""" | |
| This free, local demo estimates how much a text resembles AI-generated prose | |
| using four transparent signals—sentence-rhythm uniformity (burstiness), | |
| formulaic phrase density, repeated n-grams, and vocabulary uniformity. It is | |
| CPU-only, needs no model download, and works for English, Chinese, and other | |
| languages. It is a heuristic demo, not the full [Lynote](https://lynote.ai) | |
| production pipeline. | |
| """ | |
| EXAMPLES = [ | |
| [ | |
| "In today's rapidly evolving world, it is important to note that this " | |
| "robust solution serves as a testament to our commitment to innovation. " | |
| "Moreover, we leverage cutting-edge technology to deliver a seamless " | |
| "experience to our users. Furthermore, our team has built a " | |
| "comprehensive approach that plays a crucial role in improving " | |
| "efficiency. It is worth noting that this groundbreaking platform " | |
| "fosters collaboration and drives growth across the organization." | |
| ], | |
| [ | |
| "I walked the dog this morning and the sky was grey and low. Halfway " | |
| "through the park it started raining, which I hadn't expected, so we " | |
| "ran home. The dog seemed to enjoy it more than me. When we got back " | |
| "I made coffee and watched the rain run down the window for a while." | |
| ], | |
| [ | |
| "值得注意的是,在当今快速发展的时代,我们通过赋能团队来助力企业实现" | |
| "降本增效。综上所述,这一方案形成了完整的业务闭环,无缝连接各个环节," | |
| "发挥着重要作用。由此可见,未来我们需要继续优化流程,从而为企业创造" | |
| "更大的价值。" | |
| ], | |
| [ | |
| "今天早上我带狗去公园,出门时天阴阴的。走到一半忽然下起雨,我们只好" | |
| "跑回家,狗倒是挺高兴,一路甩着尾巴。回来以后我煮了杯咖啡,坐在窗边" | |
| "看了会儿雨,觉得周末就这么过也挺好。" | |
| ], | |
| ] | |
| with gr.Blocks(title="Lynote AI Text Detector") as demo: | |
| gr.Markdown("# 🔍 Lynote AI Text Detector") | |
| gr.Markdown(DESCRIPTION) | |
| source = gr.Textbox( | |
| label="Text to analyze", | |
| lines=12, | |
| max_lines=20, | |
| placeholder=f"Paste {MIN_CHARS:,}-{MAX_CHARS:,} characters of text…", | |
| ) | |
| run = gr.Button("Analyze text", variant="primary") | |
| with gr.Row(): | |
| result = gr.Markdown() | |
| signals = gr.JSON(label="Signal breakdown") | |
| run.click(detect, inputs=source, outputs=[result, signals]) | |
| gr.Examples(examples=EXAMPLES, inputs=source) | |
| gr.Markdown( | |
| "**Privacy:** text is analyzed in memory and is not intentionally stored " | |
| "by this app. Hugging Face infrastructure remains subject to its platform " | |
| "policies." | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |