langid-chat 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langid_chat-0.1.0/PKG-INFO +97 -0
- langid_chat-0.1.0/README.md +81 -0
- langid_chat-0.1.0/langid/__init__.py +12 -0
- langid_chat-0.1.0/langid/artifacts/languages.pkl +0 -0
- langid_chat-0.1.0/langid/artifacts/model.pkl +0 -0
- langid_chat-0.1.0/langid/artifacts/vectorizer.pkl +0 -0
- langid_chat-0.1.0/langid/cli.py +59 -0
- langid_chat-0.1.0/langid/language.py +71 -0
- langid_chat-0.1.0/langid/server.py +104 -0
- langid_chat-0.1.0/langid/static/app.js +213 -0
- langid_chat-0.1.0/langid/static/index.html +64 -0
- langid_chat-0.1.0/langid/static/style.css +343 -0
- langid_chat-0.1.0/langid_chat.egg-info/PKG-INFO +97 -0
- langid_chat-0.1.0/langid_chat.egg-info/SOURCES.txt +18 -0
- langid_chat-0.1.0/langid_chat.egg-info/dependency_links.txt +1 -0
- langid_chat-0.1.0/langid_chat.egg-info/entry_points.txt +3 -0
- langid_chat-0.1.0/langid_chat.egg-info/requires.txt +2 -0
- langid_chat-0.1.0/langid_chat.egg-info/top_level.txt +1 -0
- langid_chat-0.1.0/pyproject.toml +38 -0
- langid_chat-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langid-chat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A portable language detector (15 languages incl. code-mixed Indian) with a chat web UI
|
|
5
|
+
Author: orewamash
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/orewamash/langid
|
|
8
|
+
Keywords: language-detection,nlp,machine-learning,chatbot
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: scikit-learn>=1.2
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
|
|
17
|
+
# Language Detection AI
|
|
18
|
+
|
|
19
|
+
A classical machine-learning language detector: feed it a sentence, it tells
|
|
20
|
+
you which language it is written in. Built with Python + scikit-learn using
|
|
21
|
+
character n-grams + Naive Bayes / Linear SVC.
|
|
22
|
+
|
|
23
|
+
Includes detection of **Roman-script code-mixed Indian languages** (Tanglish,
|
|
24
|
+
Hinglish, Teluglish, Kanglish, Manglish).
|
|
25
|
+
|
|
26
|
+
## Install & run the chatbot UI
|
|
27
|
+
|
|
28
|
+
The UI ships as a pip-installable package (`langid-chat`) that bundles the
|
|
29
|
+
trained model, so anyone can run it with a single command — no repo clone
|
|
30
|
+
required.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install langid-chat
|
|
34
|
+
|
|
35
|
+
# Launch the chat UI (http://127.0.0.1:8000)
|
|
36
|
+
langid-web
|
|
37
|
+
|
|
38
|
+
# Or use the CLI
|
|
39
|
+
langid "Kya kar rahe ho bhai"
|
|
40
|
+
langid --interactive
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`langid-web` supports `--host` and `--port` (e.g. `langid-web --port 9000`).
|
|
44
|
+
|
|
45
|
+
To install from a source checkout instead:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install -e .
|
|
49
|
+
langid-web
|
|
50
|
+
# or without installing at all:
|
|
51
|
+
python web/app.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 0. (Optional) Re-fetch the code-mixed corpora into data/raw (cached by default)
|
|
58
|
+
python src/fetch_codemix.py
|
|
59
|
+
|
|
60
|
+
# 1. Prepare data -- the deployed model is trained on the FULL dataset with
|
|
61
|
+
# balanced class weights (keeps all code-mixed signal):
|
|
62
|
+
python src/preprocess.py --no-balance
|
|
63
|
+
python src/train.py --class-weight balanced
|
|
64
|
+
|
|
65
|
+
# 2. Test new sentences
|
|
66
|
+
python detect.py "Kya kar rahe ho bhai"
|
|
67
|
+
python detect.py --interactive
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> On Windows set `$env:PYTHONIOENCODING='utf-8'` first so multilingual text
|
|
71
|
+
> prints correctly.
|
|
72
|
+
|
|
73
|
+
`detect.py` also reports a **confidence score** per guess. Below a calibrated
|
|
74
|
+
threshold it says `LOW CONFIDENCE` instead of guessing — short or ambiguous
|
|
75
|
+
input (e.g. `ok`, `vanakkam`) gets flagged rather than answered wrongly.
|
|
76
|
+
Tune with `--min-confidence 0.5`, or force an always-guess with `0`.
|
|
77
|
+
|
|
78
|
+
## Languages supported (15)
|
|
79
|
+
|
|
80
|
+
Tamil, Russian, Arabic (distinct scripts) · Spanish, Portuguese, French,
|
|
81
|
+
Italian (linguistically close) · English, German, Dutch · plus code-mixed:
|
|
82
|
+
Tanglish, Hinglish, Teluglish, Kanglish, Manglish.
|
|
83
|
+
|
|
84
|
+
## Results
|
|
85
|
+
|
|
86
|
+
Best model (LinearSVC, full data + balanced class weights) reaches **~98% test
|
|
87
|
+
accuracy** across the 15 classes; script-distinct languages (Arabic, Russian,
|
|
88
|
+
Tamil) hit ~100%. High-confidence calls (>= 45%) are ~99% correct on the test
|
|
89
|
+
split; the only reliable misdetections left are 1–2 word Roman-script
|
|
90
|
+
fragments, which are exactly the ones the confidence gate flags.
|
|
91
|
+
|
|
92
|
+
**Limitations:** it only knows the 15 trained languages and still guesses on
|
|
93
|
+
anything fed to it without confidences if you lower the threshold. Very short
|
|
94
|
+
text is inherently unreliable and is flagged, not silently misanswered.
|
|
95
|
+
|
|
96
|
+
See `results/results.md` for the full write-up and `project.md`/`tasks.md` for
|
|
97
|
+
scope and tasks.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Language Detection AI
|
|
2
|
+
|
|
3
|
+
A classical machine-learning language detector: feed it a sentence, it tells
|
|
4
|
+
you which language it is written in. Built with Python + scikit-learn using
|
|
5
|
+
character n-grams + Naive Bayes / Linear SVC.
|
|
6
|
+
|
|
7
|
+
Includes detection of **Roman-script code-mixed Indian languages** (Tanglish,
|
|
8
|
+
Hinglish, Teluglish, Kanglish, Manglish).
|
|
9
|
+
|
|
10
|
+
## Install & run the chatbot UI
|
|
11
|
+
|
|
12
|
+
The UI ships as a pip-installable package (`langid-chat`) that bundles the
|
|
13
|
+
trained model, so anyone can run it with a single command — no repo clone
|
|
14
|
+
required.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install langid-chat
|
|
18
|
+
|
|
19
|
+
# Launch the chat UI (http://127.0.0.1:8000)
|
|
20
|
+
langid-web
|
|
21
|
+
|
|
22
|
+
# Or use the CLI
|
|
23
|
+
langid "Kya kar rahe ho bhai"
|
|
24
|
+
langid --interactive
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`langid-web` supports `--host` and `--port` (e.g. `langid-web --port 9000`).
|
|
28
|
+
|
|
29
|
+
To install from a source checkout instead:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e .
|
|
33
|
+
langid-web
|
|
34
|
+
# or without installing at all:
|
|
35
|
+
python web/app.py
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 0. (Optional) Re-fetch the code-mixed corpora into data/raw (cached by default)
|
|
42
|
+
python src/fetch_codemix.py
|
|
43
|
+
|
|
44
|
+
# 1. Prepare data -- the deployed model is trained on the FULL dataset with
|
|
45
|
+
# balanced class weights (keeps all code-mixed signal):
|
|
46
|
+
python src/preprocess.py --no-balance
|
|
47
|
+
python src/train.py --class-weight balanced
|
|
48
|
+
|
|
49
|
+
# 2. Test new sentences
|
|
50
|
+
python detect.py "Kya kar rahe ho bhai"
|
|
51
|
+
python detect.py --interactive
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> On Windows set `$env:PYTHONIOENCODING='utf-8'` first so multilingual text
|
|
55
|
+
> prints correctly.
|
|
56
|
+
|
|
57
|
+
`detect.py` also reports a **confidence score** per guess. Below a calibrated
|
|
58
|
+
threshold it says `LOW CONFIDENCE` instead of guessing — short or ambiguous
|
|
59
|
+
input (e.g. `ok`, `vanakkam`) gets flagged rather than answered wrongly.
|
|
60
|
+
Tune with `--min-confidence 0.5`, or force an always-guess with `0`.
|
|
61
|
+
|
|
62
|
+
## Languages supported (15)
|
|
63
|
+
|
|
64
|
+
Tamil, Russian, Arabic (distinct scripts) · Spanish, Portuguese, French,
|
|
65
|
+
Italian (linguistically close) · English, German, Dutch · plus code-mixed:
|
|
66
|
+
Tanglish, Hinglish, Teluglish, Kanglish, Manglish.
|
|
67
|
+
|
|
68
|
+
## Results
|
|
69
|
+
|
|
70
|
+
Best model (LinearSVC, full data + balanced class weights) reaches **~98% test
|
|
71
|
+
accuracy** across the 15 classes; script-distinct languages (Arabic, Russian,
|
|
72
|
+
Tamil) hit ~100%. High-confidence calls (>= 45%) are ~99% correct on the test
|
|
73
|
+
split; the only reliable misdetections left are 1–2 word Roman-script
|
|
74
|
+
fragments, which are exactly the ones the confidence gate flags.
|
|
75
|
+
|
|
76
|
+
**Limitations:** it only knows the 15 trained languages and still guesses on
|
|
77
|
+
anything fed to it without confidences if you lower the threshold. Very short
|
|
78
|
+
text is inherently unreliable and is flagged, not silently misanswered.
|
|
79
|
+
|
|
80
|
+
See `results/results.md` for the full write-up and `project.md`/`tasks.md` for
|
|
81
|
+
scope and tasks.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""langid — a portable, pip-installable language detector + chat web UI.
|
|
2
|
+
|
|
3
|
+
This package bundles the trained character n-gram model and a dependency-free
|
|
4
|
+
chat UI (stdlib http.server) so users can get a language detector with a single
|
|
5
|
+
``pip install``:
|
|
6
|
+
|
|
7
|
+
pip install langid
|
|
8
|
+
langid-web # http://127.0.0.1:8000 (chat UI)
|
|
9
|
+
langid "Kya kar rahe ho bhai" # CLI detection
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Command-line entry point (``langid``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .language import (
|
|
9
|
+
DEFAULT_MIN_CONFIDENCE,
|
|
10
|
+
load_pipeline,
|
|
11
|
+
predict_with_confidence,
|
|
12
|
+
print_predictions,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv=None) -> None:
|
|
17
|
+
parser = argparse.ArgumentParser(description="Detect language of input text.")
|
|
18
|
+
parser.add_argument("texts", nargs="*", help="one or more texts to classify")
|
|
19
|
+
parser.add_argument("--interactive", action="store_true",
|
|
20
|
+
help="prompt repeatedly for input")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--min-confidence", type=float, default=DEFAULT_MIN_CONFIDENCE,
|
|
23
|
+
help=f"flag guesses below this confidence (default {DEFAULT_MIN_CONFIDENCE})",
|
|
24
|
+
)
|
|
25
|
+
args = parser.parse_args(argv)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
vectorizer, model, languages = load_pipeline()
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
print("No trained artifacts found. Install a build with artifacts, "
|
|
31
|
+
"or run src/train.py in a source checkout.")
|
|
32
|
+
return 1
|
|
33
|
+
|
|
34
|
+
if args.interactive:
|
|
35
|
+
print("Type text and press Enter (empty line to quit).")
|
|
36
|
+
while True:
|
|
37
|
+
text = input("> ").strip()
|
|
38
|
+
if not text:
|
|
39
|
+
break
|
|
40
|
+
preds, confs = predict_with_confidence(
|
|
41
|
+
vectorizer, model, languages, [text]
|
|
42
|
+
)
|
|
43
|
+
print_predictions([text], preds, confs, args.min_confidence)
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
texts = [t for t in args.texts if t.strip()]
|
|
47
|
+
if not texts:
|
|
48
|
+
parser.print_help()
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
preds, confs = predict_with_confidence(
|
|
52
|
+
vectorizer, model, languages, texts
|
|
53
|
+
)
|
|
54
|
+
print_predictions(texts, preds, confs, args.min_confidence)
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
sys.exit(main())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Prediction engine: load the saved model/vectorizer and classify text.
|
|
2
|
+
|
|
3
|
+
Reuses the SAVED vectorizer (transform only -- never fit_transform on new
|
|
4
|
+
data) and the saved model so predictions exactly match training. Reports a
|
|
5
|
+
confidence score per guess; short/non-distinctive fragments are flagged rather
|
|
6
|
+
than answered with a wrong guess.
|
|
7
|
+
|
|
8
|
+
This module is importable both from a source checkout and from the installed
|
|
9
|
+
``langid`` package (artifacts ship inside the wheel).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
import pickle
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
ARTIFACT_DIR = Path(__file__).resolve().parent / "artifacts"
|
|
19
|
+
# Calibrated on the test split of the deployed LinearSVC: calls at/above 0.45
|
|
20
|
+
# average ~99% correct; everything below is short/non-distinctive.
|
|
21
|
+
DEFAULT_MIN_CONFIDENCE = 0.45
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_artifact(name: str):
|
|
25
|
+
with open(ARTIFACT_DIR / name, "rb") as f:
|
|
26
|
+
return pickle.load(f)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_pipeline():
|
|
30
|
+
return (
|
|
31
|
+
load_artifact("vectorizer.pkl"),
|
|
32
|
+
load_artifact("model.pkl"),
|
|
33
|
+
load_artifact("languages.pkl"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def softmax(scores):
|
|
38
|
+
m = max(scores)
|
|
39
|
+
exps = [math.exp(s - m) for s in scores]
|
|
40
|
+
total = sum(exps)
|
|
41
|
+
return [e / total for e in exps]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def predict_with_confidence(vectorizer, model, languages, texts):
|
|
45
|
+
"""Return (prediction, confidence) per text.
|
|
46
|
+
|
|
47
|
+
MultinomialNB has real probabilities; LinearSVC only has decision scores,
|
|
48
|
+
so we normalise them with a softmax as a confidence proxy.
|
|
49
|
+
"""
|
|
50
|
+
X_vec = vectorizer.transform(texts)
|
|
51
|
+
if hasattr(model, "predict_proba"):
|
|
52
|
+
proba = model.predict_proba(X_vec)
|
|
53
|
+
pred_idx = proba.argmax(axis=1)
|
|
54
|
+
confs = proba.max(axis=1)
|
|
55
|
+
return [languages[i] for i in pred_idx], confs
|
|
56
|
+
scores = model.decision_function(X_vec)
|
|
57
|
+
confs = []
|
|
58
|
+
for row in range(scores.shape[0]):
|
|
59
|
+
confs.append(max(softmax(scores[row].tolist())))
|
|
60
|
+
return list(model.predict(X_vec)), confs
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def print_predictions(texts, preds, confs, min_confidence):
|
|
64
|
+
for text, pred, conf in zip(texts, preds, confs):
|
|
65
|
+
if conf >= min_confidence:
|
|
66
|
+
print(f"{text!r} -> {pred} ({conf:.0%})")
|
|
67
|
+
else:
|
|
68
|
+
print(
|
|
69
|
+
f"{text!r} -> LOW CONFIDENCE ({conf:.0%}, best guess "
|
|
70
|
+
f"'{pred}') -- text too short/non-distinctive for a reliable call"
|
|
71
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Chat web UI server (``langid-web``).
|
|
2
|
+
|
|
3
|
+
Serves the chatbot-style interface and exposes one POST endpoint that runs the
|
|
4
|
+
bundled model (same pipeline as the CLI). Pure stdlib -- no framework.
|
|
5
|
+
|
|
6
|
+
langid-web # http://127.0.0.1:8000
|
|
7
|
+
langid-web --port 9000 # custom port
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .language import load_pipeline, predict_with_confidence
|
|
19
|
+
|
|
20
|
+
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
|
21
|
+
HOST = "127.0.0.1"
|
|
22
|
+
PORT = 8000
|
|
23
|
+
|
|
24
|
+
CONTENT_TYPES = {
|
|
25
|
+
".html": "text/html; charset=utf-8",
|
|
26
|
+
".css": "text/css; charset=utf-8",
|
|
27
|
+
".js": "application/javascript; charset=utf-8",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Handler(BaseHTTPRequestHandler):
|
|
32
|
+
server_version = "langid/1.0"
|
|
33
|
+
|
|
34
|
+
def _send(self, status, body, content_type):
|
|
35
|
+
data = body.encode("utf-8") if isinstance(body, str) else body
|
|
36
|
+
self.send_response(status)
|
|
37
|
+
self.send_header("Content-Type", content_type)
|
|
38
|
+
self.send_header("Content-Length", str(len(data)))
|
|
39
|
+
self.send_header("Cache-Control", "no-store")
|
|
40
|
+
self.end_headers()
|
|
41
|
+
self.wfile.write(data)
|
|
42
|
+
|
|
43
|
+
def do_GET(self):
|
|
44
|
+
path = self.path.split("?")[0]
|
|
45
|
+
if path == "/":
|
|
46
|
+
path = "/index.html"
|
|
47
|
+
rel = path.lstrip("/")
|
|
48
|
+
file = (STATIC_DIR / rel).resolve()
|
|
49
|
+
if not str(file).startswith(str(STATIC_DIR.resolve())) or not file.is_file():
|
|
50
|
+
self._send(404, "not found", "text/plain; charset=utf-8")
|
|
51
|
+
return
|
|
52
|
+
fmt = CONTENT_TYPES.get(file.suffix, "application/octet-stream")
|
|
53
|
+
self._send(200, file.read_bytes(), fmt)
|
|
54
|
+
|
|
55
|
+
def do_POST(self):
|
|
56
|
+
if self.path.split("?")[0] != "/api/detect":
|
|
57
|
+
self._send(404, "not found", "text/plain; charset=utf-8")
|
|
58
|
+
return
|
|
59
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
60
|
+
try:
|
|
61
|
+
payload = json.loads(self.rfile.read(length) or b"{}")
|
|
62
|
+
except json.JSONDecodeError:
|
|
63
|
+
payload = {}
|
|
64
|
+
text = (payload.get("text") or "").strip()
|
|
65
|
+
if not text:
|
|
66
|
+
self._send(400, json.dumps({"error": "empty text"}),
|
|
67
|
+
"application/json; charset=utf-8")
|
|
68
|
+
return
|
|
69
|
+
try:
|
|
70
|
+
vectorizer, model, languages = load_pipeline()
|
|
71
|
+
except FileNotFoundError as exc:
|
|
72
|
+
self._send(500, json.dumps({"error": f"model unavailable: {exc}"}),
|
|
73
|
+
"application/json; charset=utf-8")
|
|
74
|
+
return
|
|
75
|
+
(pred,), (conf,) = predict_with_confidence(
|
|
76
|
+
vectorizer, model, languages, [text]
|
|
77
|
+
)
|
|
78
|
+
self._send(200, json.dumps({
|
|
79
|
+
"text": text,
|
|
80
|
+
"language": pred,
|
|
81
|
+
"confidence": round(float(conf), 4),
|
|
82
|
+
}), "application/json; charset=utf-8")
|
|
83
|
+
|
|
84
|
+
def log_message(self, fmt, *args):
|
|
85
|
+
sys.stderr.write(f"[langid-web] {fmt % args}\n")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv=None) -> None:
|
|
89
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
90
|
+
parser.add_argument("--host", default=HOST)
|
|
91
|
+
parser.add_argument("--port", type=int, default=PORT)
|
|
92
|
+
args = parser.parse_args(argv)
|
|
93
|
+
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
|
94
|
+
url = f"http://{args.host}:{args.port}"
|
|
95
|
+
print(f"langid chat UI running at {url}")
|
|
96
|
+
print("Press Ctrl+C to stop.")
|
|
97
|
+
try:
|
|
98
|
+
server.serve_forever()
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
print("\nstopped.")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const chat = document.getElementById("chat");
|
|
3
|
+
const input = document.getElementById("input");
|
|
4
|
+
const sendBtn = document.getElementById("send");
|
|
5
|
+
const newChat = document.getElementById("newChat");
|
|
6
|
+
const burger = document.getElementById("burger");
|
|
7
|
+
const sidebar = document.getElementById("sidebar");
|
|
8
|
+
const brandSub = document.getElementById("brandSub");
|
|
9
|
+
|
|
10
|
+
const MIN_CONF = 0.45;
|
|
11
|
+
const SAMPLE_GROUPS = [
|
|
12
|
+
{
|
|
13
|
+
title: "Script-distinct",
|
|
14
|
+
tags: ["Tamil", "Russian", "Arabic"],
|
|
15
|
+
items: [
|
|
16
|
+
"வணக்கம் உலகம்",
|
|
17
|
+
"Привет, как дела?",
|
|
18
|
+
"السلام عليكم",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: "Romance & Germanic",
|
|
23
|
+
tags: ["Spanish", "French", "German"],
|
|
24
|
+
items: [
|
|
25
|
+
"Hola, ¿cómo estás?",
|
|
26
|
+
"Bonjour tout le monde",
|
|
27
|
+
"Das Wetter ist heute schön",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "Code-mixed Indian",
|
|
32
|
+
tags: ["Tanglish", "Hinglish", "Manglish"],
|
|
33
|
+
items: [
|
|
34
|
+
"Namma ooru santhe hogona",
|
|
35
|
+
"Kya kar rahe ho bhai?",
|
|
36
|
+
"Njan nalla thirakil aanu",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const FLAVOR = {
|
|
42
|
+
English: "straight-up English",
|
|
43
|
+
French: "Français",
|
|
44
|
+
German: "Deutsch",
|
|
45
|
+
Spanish: "Español",
|
|
46
|
+
Portuguese: "Português",
|
|
47
|
+
Italian: "Italiano",
|
|
48
|
+
Dutch: "Nederlands",
|
|
49
|
+
Russian: "Русский",
|
|
50
|
+
Arabic: "العربية",
|
|
51
|
+
Tamil: "தமிழ்",
|
|
52
|
+
Hinglish: "Hinglish (Hindi + English)",
|
|
53
|
+
Tanglish: "Tanglish (Tamil + English)",
|
|
54
|
+
Teluglish: "Teluglish (Telugu + English)",
|
|
55
|
+
Kanglish: "Kanglish (Kannada + English)",
|
|
56
|
+
Manglish: "Manglish (Malayalam + English)",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
let inner = null;
|
|
60
|
+
|
|
61
|
+
function ensureChat() {
|
|
62
|
+
if (!inner) {
|
|
63
|
+
inner = document.createElement("div");
|
|
64
|
+
inner.className = "chat-inner";
|
|
65
|
+
chat.appendChild(inner);
|
|
66
|
+
}
|
|
67
|
+
return inner;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scrollDown() {
|
|
71
|
+
chat.scrollTop = chat.scrollHeight;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function addMsg(role, avatarEmoji, html) {
|
|
75
|
+
const wrap = document.createElement("div");
|
|
76
|
+
wrap.className = `msg ${role} done`;
|
|
77
|
+
wrap.innerHTML = `
|
|
78
|
+
<div class="avatar">${avatarEmoji}</div>
|
|
79
|
+
<div class="bubble">${html}</div>`;
|
|
80
|
+
ensureChat().appendChild(wrap);
|
|
81
|
+
scrollDown();
|
|
82
|
+
return wrap;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function greet() {
|
|
86
|
+
const container = ensureChat();
|
|
87
|
+
container.innerHTML = "";
|
|
88
|
+
const wrap = document.createElement("div");
|
|
89
|
+
wrap.className = "msg greet done";
|
|
90
|
+
wrap.innerHTML = `
|
|
91
|
+
<div class="bubble">
|
|
92
|
+
<div class="greet-logo">🌐</div>
|
|
93
|
+
<h2>Hey, I'm LangID</h2>
|
|
94
|
+
<p>Type any sentence and I'll tell you which language it's written in —
|
|
95
|
+
including code-mixed ones like Tanglish or Hinglish. I'll show my
|
|
96
|
+
confidence, and if a fragment is too short to call, I'll say so
|
|
97
|
+
instead of guessing.</p>
|
|
98
|
+
<div class="prompt-groups">
|
|
99
|
+
${SAMPLE_GROUPS.map(g => `
|
|
100
|
+
<div class="prompt-group">
|
|
101
|
+
<div class="prompt-label">${g.title}</div>
|
|
102
|
+
<div class="chips">
|
|
103
|
+
${g.items.map(s => `<button class="chip" data-tags="${g.tags.join(",")}">${s}</button>`).join("")}
|
|
104
|
+
</div>
|
|
105
|
+
</div>`).join("")}
|
|
106
|
+
</div>
|
|
107
|
+
<div class="greet-foot">15 languages · LinearSVC · character n-grams</div>
|
|
108
|
+
</div>`;
|
|
109
|
+
container.appendChild(wrap);
|
|
110
|
+
wrap.querySelectorAll(".chip").forEach(c => {
|
|
111
|
+
c.addEventListener("click", () => sendMessage(c.textContent));
|
|
112
|
+
});
|
|
113
|
+
scrollDown();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function typingMsg() {
|
|
117
|
+
const wrap = document.createElement("div");
|
|
118
|
+
wrap.className = "msg typing";
|
|
119
|
+
wrap.innerHTML = `
|
|
120
|
+
<div class="avatar">🧠</div>
|
|
121
|
+
<div class="bubble"><i></i><i></i><i></i></div>`;
|
|
122
|
+
ensureChat().appendChild(wrap);
|
|
123
|
+
scrollDown();
|
|
124
|
+
return wrap;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function replyFor(language, conf) {
|
|
128
|
+
const pct = Math.round(conf * 100);
|
|
129
|
+
const flavor = FLAVOR[language] || language;
|
|
130
|
+
if (conf >= MIN_CONF) {
|
|
131
|
+
return `That's <span class="lang">${flavor}</span> — I'm ${pct}% sure${pct >= 90 ? " 🎯" : ""}.`;
|
|
132
|
+
}
|
|
133
|
+
return `Hmm, I'm only ${pct}% confident, so I'd rather not guess wrong. ` +
|
|
134
|
+
`That text is too short or ambiguous — want to give me a longer sentence?`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sendMessage(text) {
|
|
138
|
+
if (!text || !text.trim()) return;
|
|
139
|
+
addMsg("user", "🙂", escapeHtml(text.trim()));
|
|
140
|
+
const typing = typingMsg();
|
|
141
|
+
|
|
142
|
+
const delay = 450 + Math.random() * 350;
|
|
143
|
+
setTimeout(async () => {
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch("/api/detect", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: JSON.stringify({ text: text.trim() }),
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) throw new Error(await res.text());
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
const low = data.confidence < MIN_CONF;
|
|
153
|
+
typing.outerHTML = `
|
|
154
|
+
<div class="msg done">
|
|
155
|
+
<div class="avatar">🧠</div>
|
|
156
|
+
<div class="bubble">
|
|
157
|
+
${replyFor(data.language, data.confidence)}
|
|
158
|
+
<span class="conf-pill ${low ? "low" : ""}">${Math.round(data.confidence * 100)}%</span>
|
|
159
|
+
<span class="bubble-note">detected with the same pipeline as detect.py</span>
|
|
160
|
+
</div>
|
|
161
|
+
</div>`;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
typing.outerHTML = `
|
|
164
|
+
<div class="msg done">
|
|
165
|
+
<div class="avatar">🧠</div>
|
|
166
|
+
<div class="bubble">Something went wrong on my end (${escapeHtml(String(err))}). Try again?</div>
|
|
167
|
+
</div>`;
|
|
168
|
+
}
|
|
169
|
+
scrollDown();
|
|
170
|
+
}, delay);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function escapeHtml(s) {
|
|
174
|
+
return s.replace(/[&<>"']/g, c => ({
|
|
175
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
176
|
+
}[c]));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function submit() {
|
|
180
|
+
const text = input.value;
|
|
181
|
+
if (!text.trim()) return;
|
|
182
|
+
input.value = "";
|
|
183
|
+
autoResize();
|
|
184
|
+
sendBtn.disabled = true;
|
|
185
|
+
sendMessage(text);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function autoResize() {
|
|
189
|
+
input.style.height = "auto";
|
|
190
|
+
input.style.height = Math.min(input.scrollHeight, 140) + "px";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
input.addEventListener("input", () => {
|
|
194
|
+
autoResize();
|
|
195
|
+
sendBtn.disabled = !input.value.trim();
|
|
196
|
+
});
|
|
197
|
+
input.addEventListener("keydown", e => {
|
|
198
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
submit();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
sendBtn.addEventListener("click", submit);
|
|
204
|
+
newChat.addEventListener("click", () => {
|
|
205
|
+
greet();
|
|
206
|
+
input.focus();
|
|
207
|
+
brandSub.textContent = "new conversation";
|
|
208
|
+
});
|
|
209
|
+
burger.addEventListener("click", () => sidebar.classList.toggle("closed"));
|
|
210
|
+
|
|
211
|
+
greet();
|
|
212
|
+
input.focus();
|
|
213
|
+
})();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>LangID — Language Chat</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>">
|
|
8
|
+
<link rel="stylesheet" href="/style.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<aside class="sidebar" id="sidebar">
|
|
12
|
+
<div class="side-head">
|
|
13
|
+
<button class="new-chat" id="newChat">
|
|
14
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
15
|
+
New chat
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="side-label">Conversations</div>
|
|
19
|
+
<div class="convo-list">
|
|
20
|
+
<div class="convo active" data-title="Hello world, how are you?">
|
|
21
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
22
|
+
<span>Language — boot</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="convo" data-title="Vanakkam friends!">
|
|
25
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
26
|
+
<span>Code-mixed samples</span>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="side-foot">
|
|
30
|
+
<div class="chip-label">15 languages · LinearSVC · char n-grams</div>
|
|
31
|
+
</div>
|
|
32
|
+
</aside>
|
|
33
|
+
|
|
34
|
+
<main class="main">
|
|
35
|
+
<header class="topbar">
|
|
36
|
+
<button class="burger" id="burger" aria-label="Toggle sidebar">
|
|
37
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
|
38
|
+
</button>
|
|
39
|
+
<div class="brand">
|
|
40
|
+
<div class="logo">🌐</div>
|
|
41
|
+
<div>
|
|
42
|
+
<div class="brand-name">LangID</div>
|
|
43
|
+
<div class="brand-sub" id="brandSub">Powered by char n-grams</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="status"><span class="dot"></span> model ready</div>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<section class="chat" id="chat" aria-live="polite"></section>
|
|
50
|
+
|
|
51
|
+
<footer class="composer">
|
|
52
|
+
<div class="composer-inner">
|
|
53
|
+
<textarea id="input" rows="1" placeholder="Type a sentence and I'll tell you its language…"></textarea>
|
|
54
|
+
<button id="send" class="send-btn" aria-label="Send" disabled>
|
|
55
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2 11 13M22 2l-7 20-4-9-9-4z"/></svg>
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="composer-hint">LangID is trained on 15 languages — including Tanglish, Hinglish, Teluglish…</div>
|
|
59
|
+
</footer>
|
|
60
|
+
</main>
|
|
61
|
+
|
|
62
|
+
<script src="/app.js"></script>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0b0c0f;
|
|
3
|
+
--bg-2: #0f1013;
|
|
4
|
+
--panel: #15161a;
|
|
5
|
+
--panel-2: #1b1d23;
|
|
6
|
+
--panel-3: #23252c;
|
|
7
|
+
--border: #24262e;
|
|
8
|
+
--border-2: #2e313a;
|
|
9
|
+
--text: #ededf2;
|
|
10
|
+
--muted: #9a9daa;
|
|
11
|
+
--faint: #676a76;
|
|
12
|
+
--accent: #8b5cf6;
|
|
13
|
+
--accent-2: #6366f1;
|
|
14
|
+
--accent-soft: rgba(139, 92, 246, .14);
|
|
15
|
+
--user-bubble: linear-gradient(135deg, #6d5cf6, #a855f7);
|
|
16
|
+
--amber: #f5a623;
|
|
17
|
+
--ok: #34d399;
|
|
18
|
+
--radius: 18px;
|
|
19
|
+
--topbar-h: 60px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23
|
+
|
|
24
|
+
html, body { height: 100%; }
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
background:
|
|
28
|
+
radial-gradient(1200px 600px at 90% -10%, rgba(139, 92, 246, .10), transparent 60%),
|
|
29
|
+
radial-gradient(1000px 500px at -10% 110%, rgba(99, 102, 241, .08), transparent 55%),
|
|
30
|
+
var(--bg);
|
|
31
|
+
color: var(--text);
|
|
32
|
+
font: 15px/1.55 system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
33
|
+
display: flex;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
-webkit-font-smoothing: antialiased;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
::selection { background: rgba(139, 92, 246, .35); }
|
|
39
|
+
|
|
40
|
+
/* custom scrollbar (WebKit) */
|
|
41
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
42
|
+
::-webkit-scrollbar-thumb {
|
|
43
|
+
background: var(--border-2); border-radius: 99px;
|
|
44
|
+
border: 2px solid transparent; background-clip: padding-box;
|
|
45
|
+
}
|
|
46
|
+
::-webkit-scrollbar-thumb:hover { background: var(--faint); border-color: transparent; background-clip: padding-box; }
|
|
47
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
48
|
+
|
|
49
|
+
/* ---------- sidebar ---------- */
|
|
50
|
+
.sidebar {
|
|
51
|
+
width: 272px;
|
|
52
|
+
flex: 0 0 272px;
|
|
53
|
+
background: var(--bg-2);
|
|
54
|
+
border-right: 1px solid var(--border);
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
padding: 12px;
|
|
58
|
+
transition: margin-left .28s cubic-bezier(.4, 0, .2, 1);
|
|
59
|
+
}
|
|
60
|
+
.sidebar.closed { margin-left: -272px; }
|
|
61
|
+
|
|
62
|
+
.new-chat {
|
|
63
|
+
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
64
|
+
background: #fff;
|
|
65
|
+
color: #0b0c0f;
|
|
66
|
+
border: none; cursor: pointer;
|
|
67
|
+
padding: 11px 14px; border-radius: 12px;
|
|
68
|
+
font: 600 14px system-ui, sans-serif;
|
|
69
|
+
transition: transform .12s ease, background .15s ease, box-shadow .15s ease;
|
|
70
|
+
}
|
|
71
|
+
.new-chat:hover {
|
|
72
|
+
transform: translateY(-1px);
|
|
73
|
+
background: #f2f2f5;
|
|
74
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, .3);
|
|
75
|
+
}
|
|
76
|
+
.new-chat:active { transform: translateY(0); }
|
|
77
|
+
|
|
78
|
+
.side-label {
|
|
79
|
+
margin: 22px 4px 8px;
|
|
80
|
+
font-size: 11px; letter-spacing: .1em;
|
|
81
|
+
text-transform: uppercase; color: var(--faint);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.convo-list { display: flex; flex-direction: column; gap: 2px; overflow-y: auto; padding-right: 2px; }
|
|
85
|
+
|
|
86
|
+
.convo {
|
|
87
|
+
display: flex; align-items: center; gap: 10px;
|
|
88
|
+
padding: 9px 10px; border-radius: 9px;
|
|
89
|
+
color: var(--muted); cursor: pointer;
|
|
90
|
+
white-space: nowrap; overflow: hidden;
|
|
91
|
+
border: 1px solid transparent;
|
|
92
|
+
transition: background .14s, color .14s, border-color .14s;
|
|
93
|
+
}
|
|
94
|
+
.convo span { overflow: hidden; text-overflow: ellipsis; }
|
|
95
|
+
.convo svg { flex: 0 0 auto; color: var(--faint); }
|
|
96
|
+
.convo:hover { background: var(--panel); color: var(--text); }
|
|
97
|
+
.convo.active { background: var(--panel); color: var(--text); border-color: var(--border); }
|
|
98
|
+
|
|
99
|
+
.side-foot { margin-top: auto; padding-top: 14px; border-top: 1px solid var(--border); }
|
|
100
|
+
.chip-label {
|
|
101
|
+
font-size: 11px; color: var(--faint); text-align: center;
|
|
102
|
+
padding: 6px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ---------- main ---------- */
|
|
106
|
+
.main {
|
|
107
|
+
flex: 1;
|
|
108
|
+
display: flex; flex-direction: column;
|
|
109
|
+
min-width: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.topbar {
|
|
113
|
+
display: flex; align-items: center; gap: 14px;
|
|
114
|
+
height: var(--topbar-h);
|
|
115
|
+
padding: 0 20px;
|
|
116
|
+
border-bottom: 1px solid var(--border);
|
|
117
|
+
background: rgba(11, 12, 15, .72);
|
|
118
|
+
backdrop-filter: blur(12px) saturate(1.2);
|
|
119
|
+
-webkit-backdrop-filter: blur(12px) saturate(1.2);
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
position: relative; z-index: 5;
|
|
122
|
+
}
|
|
123
|
+
.burger {
|
|
124
|
+
background: transparent; border: none; cursor: pointer;
|
|
125
|
+
color: var(--muted); padding: 7px; border-radius: 8px;
|
|
126
|
+
transition: color .12s, background .12s;
|
|
127
|
+
}
|
|
128
|
+
.burger:hover { color: var(--text); background: var(--panel-2); }
|
|
129
|
+
|
|
130
|
+
.brand { display: flex; align-items: center; gap: 11px; }
|
|
131
|
+
.logo {
|
|
132
|
+
width: 34px; height: 34px; border-radius: 10px;
|
|
133
|
+
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
|
134
|
+
display: grid; place-items: center; font-size: 17px;
|
|
135
|
+
box-shadow: 0 4px 18px rgba(139, 92, 246, .45);
|
|
136
|
+
}
|
|
137
|
+
.brand-name { font-weight: 650; font-size: 15px; letter-spacing: -.01em; }
|
|
138
|
+
.brand-sub { font-size: 12px; color: var(--muted); }
|
|
139
|
+
|
|
140
|
+
.status {
|
|
141
|
+
margin-left: auto; display: flex; align-items: center; gap: 7px;
|
|
142
|
+
font-size: 12px; color: var(--muted);
|
|
143
|
+
}
|
|
144
|
+
.dot {
|
|
145
|
+
width: 8px; height: 8px; border-radius: 50%; background: var(--ok);
|
|
146
|
+
box-shadow: 0 0 0 0 rgba(52, 211, 153, .5);
|
|
147
|
+
animation: pulse 2.4s infinite;
|
|
148
|
+
}
|
|
149
|
+
@keyframes pulse {
|
|
150
|
+
0% { box-shadow: 0 0 0 0 rgba(52, 211, 153, .45); }
|
|
151
|
+
70% { box-shadow: 0 0 0 7px rgba(52, 211, 153, 0); }
|
|
152
|
+
100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ---------- chat ---------- */
|
|
156
|
+
.chat {
|
|
157
|
+
flex: 1;
|
|
158
|
+
overflow-y: auto;
|
|
159
|
+
padding: 28px 0 8px;
|
|
160
|
+
scroll-behavior: smooth;
|
|
161
|
+
}
|
|
162
|
+
.chat-inner {
|
|
163
|
+
max-width: 760px;
|
|
164
|
+
margin: 0 auto;
|
|
165
|
+
display: flex; flex-direction: column; gap: 22px;
|
|
166
|
+
padding: 0 20px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.msg { display: flex; gap: 12px; align-items: flex-start; }
|
|
170
|
+
.msg.user { flex-direction: row-reverse; }
|
|
171
|
+
|
|
172
|
+
.avatar {
|
|
173
|
+
flex: 0 0 auto;
|
|
174
|
+
width: 32px; height: 32px; border-radius: 10px;
|
|
175
|
+
display: grid; place-items: center; font-size: 16px;
|
|
176
|
+
background: var(--panel-2); border: 1px solid var(--border);
|
|
177
|
+
box-shadow: 0 2px 8px rgba(0,0,0,.25);
|
|
178
|
+
}
|
|
179
|
+
.msg.user .avatar { background: linear-gradient(135deg, var(--accent), var(--accent-2)); }
|
|
180
|
+
|
|
181
|
+
.bubble {
|
|
182
|
+
max-width: 76%;
|
|
183
|
+
padding: 12px 16px;
|
|
184
|
+
border-radius: var(--radius);
|
|
185
|
+
font-size: 15px;
|
|
186
|
+
background: var(--panel-2);
|
|
187
|
+
border: 1px solid var(--border);
|
|
188
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.25);
|
|
189
|
+
}
|
|
190
|
+
.msg.user .bubble {
|
|
191
|
+
color: #fff;
|
|
192
|
+
background: var(--user-bubble);
|
|
193
|
+
border: none;
|
|
194
|
+
border-bottom-right-radius: 6px;
|
|
195
|
+
box-shadow: 0 4px 18px rgba(139, 92, 246, .25);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.msg .bubble .lang {
|
|
199
|
+
font-weight: 750;
|
|
200
|
+
background: linear-gradient(90deg, #a5b4fc, #c4b5fd);
|
|
201
|
+
-webkit-background-clip: text;
|
|
202
|
+
background-clip: text;
|
|
203
|
+
color: transparent;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.conf-pill {
|
|
207
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
208
|
+
margin-left: 8px;
|
|
209
|
+
padding: 2px 10px; border-radius: 99px;
|
|
210
|
+
font-size: 12px; font-weight: 650;
|
|
211
|
+
background: rgba(52, 211, 153, .13);
|
|
212
|
+
color: var(--ok);
|
|
213
|
+
border: 1px solid rgba(52, 211, 153, .2);
|
|
214
|
+
vertical-align: 2px;
|
|
215
|
+
}
|
|
216
|
+
.conf-pill.low { background: rgba(245, 166, 35, .12); color: var(--amber); border-color: rgba(245, 166, 35, .2); }
|
|
217
|
+
|
|
218
|
+
.bubble-note { display: block; margin-top: 10px; font-size: 12.5px; color: var(--muted); }
|
|
219
|
+
.msg.user .bubble-note { color: rgba(255,255,255,.82); }
|
|
220
|
+
|
|
221
|
+
/* greeting / empty state */
|
|
222
|
+
.greet { width: 100%; }
|
|
223
|
+
.greet .bubble {
|
|
224
|
+
width: 100%; max-width: none;
|
|
225
|
+
text-align: center;
|
|
226
|
+
padding: 40px 28px 32px;
|
|
227
|
+
border-radius: 22px;
|
|
228
|
+
background:
|
|
229
|
+
radial-gradient(120% 150% at 50% -10%, rgba(139, 92, 246, .16), transparent 55%),
|
|
230
|
+
var(--panel);
|
|
231
|
+
border: 1px solid var(--border-2);
|
|
232
|
+
}
|
|
233
|
+
.greet-logo {
|
|
234
|
+
width: 56px; height: 56px; margin: 0 auto 18px;
|
|
235
|
+
border-radius: 16px; display: grid; place-items: center; font-size: 28px;
|
|
236
|
+
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
|
237
|
+
box-shadow: 0 8px 30px rgba(139, 92, 246, .5);
|
|
238
|
+
}
|
|
239
|
+
.greet .bubble h2 {
|
|
240
|
+
font-size: 22px; margin-bottom: 8px; letter-spacing: -.02em;
|
|
241
|
+
font-weight: 700;
|
|
242
|
+
}
|
|
243
|
+
.greet .bubble p {
|
|
244
|
+
color: var(--muted); max-width: 520px; margin: 0 auto 26px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.prompt-groups { display: flex; flex-direction: column; gap: 22px; text-align: left; }
|
|
248
|
+
.prompt-label {
|
|
249
|
+
font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
|
|
250
|
+
color: var(--faint); margin-bottom: 9px;
|
|
251
|
+
}
|
|
252
|
+
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
253
|
+
.chip {
|
|
254
|
+
background: var(--bg-2);
|
|
255
|
+
border: 1px solid var(--border-2);
|
|
256
|
+
color: var(--text);
|
|
257
|
+
font: 13.5px system-ui, sans-serif;
|
|
258
|
+
padding: 8px 14px; border-radius: 99px; cursor: pointer;
|
|
259
|
+
transition: border-color .15s, background .15s, color .15s, transform .12s, box-shadow .15s;
|
|
260
|
+
}
|
|
261
|
+
.chip:hover {
|
|
262
|
+
border-color: var(--accent);
|
|
263
|
+
background: var(--accent-soft);
|
|
264
|
+
color: #d6ccfb;
|
|
265
|
+
transform: translateY(-1px);
|
|
266
|
+
box-shadow: 0 4px 14px rgba(139, 92, 246, .2);
|
|
267
|
+
}
|
|
268
|
+
.chip:active { transform: translateY(0); }
|
|
269
|
+
|
|
270
|
+
.greet-foot {
|
|
271
|
+
margin-top: 26px; padding-top: 20px;
|
|
272
|
+
border-top: 1px solid var(--border);
|
|
273
|
+
font-size: 12px; color: var(--faint);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* typing indicator */
|
|
277
|
+
.typing .bubble { display: flex; gap: 5px; padding: 18px 20px; align-items: center; }
|
|
278
|
+
.typing .bubble i {
|
|
279
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
280
|
+
background: var(--muted);
|
|
281
|
+
animation: bounce 1.2s infinite ease-in-out;
|
|
282
|
+
}
|
|
283
|
+
.typing .bubble i:nth-child(2) { animation-delay: .15s; }
|
|
284
|
+
.typing .bubble i:nth-child(3) { animation-delay: .3s; }
|
|
285
|
+
@keyframes bounce { 0%, 60%, 100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-5px); opacity: 1; } }
|
|
286
|
+
|
|
287
|
+
.msg.done .bubble { animation: pop .22s cubic-bezier(.2, .8, .3, 1.1); }
|
|
288
|
+
@keyframes pop { from { transform: translateY(7px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
289
|
+
|
|
290
|
+
/* ---------- composer ---------- */
|
|
291
|
+
.composer {
|
|
292
|
+
flex-shrink: 0;
|
|
293
|
+
padding: 12px 20px 16px;
|
|
294
|
+
border-top: 1px solid var(--border);
|
|
295
|
+
background: linear-gradient(to top, var(--bg) 70%, transparent);
|
|
296
|
+
}
|
|
297
|
+
.composer-inner {
|
|
298
|
+
max-width: 760px; margin: 0 auto;
|
|
299
|
+
display: flex; align-items: flex-end; gap: 10px;
|
|
300
|
+
background: var(--panel-2);
|
|
301
|
+
border: 1px solid var(--border-2);
|
|
302
|
+
border-radius: 18px;
|
|
303
|
+
padding: 8px 8px 8px 16px;
|
|
304
|
+
box-shadow: 0 8px 30px rgba(0,0,0,.4);
|
|
305
|
+
transition: border-color .15s, box-shadow .15s;
|
|
306
|
+
}
|
|
307
|
+
.composer-inner:focus-within {
|
|
308
|
+
border-color: var(--accent);
|
|
309
|
+
box-shadow: 0 8px 34px rgba(139, 92, 246, .22);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.composer textarea {
|
|
313
|
+
flex: 1;
|
|
314
|
+
background: transparent; border: none; outline: none; resize: none;
|
|
315
|
+
color: var(--text); font: 15px/1.45 system-ui, sans-serif;
|
|
316
|
+
padding: 9px 0; max-height: 140px;
|
|
317
|
+
}
|
|
318
|
+
.composer textarea::placeholder { color: var(--faint); }
|
|
319
|
+
|
|
320
|
+
.send-btn {
|
|
321
|
+
flex: 0 0 auto;
|
|
322
|
+
width: 40px; height: 40px; border-radius: 12px;
|
|
323
|
+
border: none; cursor: pointer; color: #0b0c0f; background: #fff;
|
|
324
|
+
display: grid; place-items: center;
|
|
325
|
+
transition: filter .12s, transform .12s, opacity .15s, background .15s;
|
|
326
|
+
}
|
|
327
|
+
.send-btn:hover:not(:disabled) { filter: brightness(.95); transform: translateY(-1px); }
|
|
328
|
+
.send-btn:active:not(:disabled) { transform: translateY(0); }
|
|
329
|
+
.send-btn:disabled { opacity: .32; cursor: default; }
|
|
330
|
+
|
|
331
|
+
.composer-hint {
|
|
332
|
+
max-width: 760px; margin: 10px auto 0;
|
|
333
|
+
text-align: center; font-size: 12px; color: var(--faint);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* ---------- responsive ---------- */
|
|
337
|
+
@media (max-width: 760px) {
|
|
338
|
+
.sidebar { position: fixed; z-index: 20; height: 100%; }
|
|
339
|
+
.sidebar.closed { margin-left: -272px; }
|
|
340
|
+
.bubble { max-width: 86%; }
|
|
341
|
+
.status { display: none; }
|
|
342
|
+
.greet .bubble { padding: 28px 18px 24px; }
|
|
343
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langid-chat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A portable language detector (15 languages incl. code-mixed Indian) with a chat web UI
|
|
5
|
+
Author: orewamash
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/orewamash/langid
|
|
8
|
+
Keywords: language-detection,nlp,machine-learning,chatbot
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: scikit-learn>=1.2
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
|
|
17
|
+
# Language Detection AI
|
|
18
|
+
|
|
19
|
+
A classical machine-learning language detector: feed it a sentence, it tells
|
|
20
|
+
you which language it is written in. Built with Python + scikit-learn using
|
|
21
|
+
character n-grams + Naive Bayes / Linear SVC.
|
|
22
|
+
|
|
23
|
+
Includes detection of **Roman-script code-mixed Indian languages** (Tanglish,
|
|
24
|
+
Hinglish, Teluglish, Kanglish, Manglish).
|
|
25
|
+
|
|
26
|
+
## Install & run the chatbot UI
|
|
27
|
+
|
|
28
|
+
The UI ships as a pip-installable package (`langid-chat`) that bundles the
|
|
29
|
+
trained model, so anyone can run it with a single command — no repo clone
|
|
30
|
+
required.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install langid-chat
|
|
34
|
+
|
|
35
|
+
# Launch the chat UI (http://127.0.0.1:8000)
|
|
36
|
+
langid-web
|
|
37
|
+
|
|
38
|
+
# Or use the CLI
|
|
39
|
+
langid "Kya kar rahe ho bhai"
|
|
40
|
+
langid --interactive
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`langid-web` supports `--host` and `--port` (e.g. `langid-web --port 9000`).
|
|
44
|
+
|
|
45
|
+
To install from a source checkout instead:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install -e .
|
|
49
|
+
langid-web
|
|
50
|
+
# or without installing at all:
|
|
51
|
+
python web/app.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 0. (Optional) Re-fetch the code-mixed corpora into data/raw (cached by default)
|
|
58
|
+
python src/fetch_codemix.py
|
|
59
|
+
|
|
60
|
+
# 1. Prepare data -- the deployed model is trained on the FULL dataset with
|
|
61
|
+
# balanced class weights (keeps all code-mixed signal):
|
|
62
|
+
python src/preprocess.py --no-balance
|
|
63
|
+
python src/train.py --class-weight balanced
|
|
64
|
+
|
|
65
|
+
# 2. Test new sentences
|
|
66
|
+
python detect.py "Kya kar rahe ho bhai"
|
|
67
|
+
python detect.py --interactive
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> On Windows set `$env:PYTHONIOENCODING='utf-8'` first so multilingual text
|
|
71
|
+
> prints correctly.
|
|
72
|
+
|
|
73
|
+
`detect.py` also reports a **confidence score** per guess. Below a calibrated
|
|
74
|
+
threshold it says `LOW CONFIDENCE` instead of guessing — short or ambiguous
|
|
75
|
+
input (e.g. `ok`, `vanakkam`) gets flagged rather than answered wrongly.
|
|
76
|
+
Tune with `--min-confidence 0.5`, or force an always-guess with `0`.
|
|
77
|
+
|
|
78
|
+
## Languages supported (15)
|
|
79
|
+
|
|
80
|
+
Tamil, Russian, Arabic (distinct scripts) · Spanish, Portuguese, French,
|
|
81
|
+
Italian (linguistically close) · English, German, Dutch · plus code-mixed:
|
|
82
|
+
Tanglish, Hinglish, Teluglish, Kanglish, Manglish.
|
|
83
|
+
|
|
84
|
+
## Results
|
|
85
|
+
|
|
86
|
+
Best model (LinearSVC, full data + balanced class weights) reaches **~98% test
|
|
87
|
+
accuracy** across the 15 classes; script-distinct languages (Arabic, Russian,
|
|
88
|
+
Tamil) hit ~100%. High-confidence calls (>= 45%) are ~99% correct on the test
|
|
89
|
+
split; the only reliable misdetections left are 1–2 word Roman-script
|
|
90
|
+
fragments, which are exactly the ones the confidence gate flags.
|
|
91
|
+
|
|
92
|
+
**Limitations:** it only knows the 15 trained languages and still guesses on
|
|
93
|
+
anything fed to it without confidences if you lower the threshold. Very short
|
|
94
|
+
text is inherently unreliable and is flagged, not silently misanswered.
|
|
95
|
+
|
|
96
|
+
See `results/results.md` for the full write-up and `project.md`/`tasks.md` for
|
|
97
|
+
scope and tasks.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
langid/__init__.py
|
|
4
|
+
langid/cli.py
|
|
5
|
+
langid/language.py
|
|
6
|
+
langid/server.py
|
|
7
|
+
langid/artifacts/languages.pkl
|
|
8
|
+
langid/artifacts/model.pkl
|
|
9
|
+
langid/artifacts/vectorizer.pkl
|
|
10
|
+
langid/static/app.js
|
|
11
|
+
langid/static/index.html
|
|
12
|
+
langid/static/style.css
|
|
13
|
+
langid_chat.egg-info/PKG-INFO
|
|
14
|
+
langid_chat.egg-info/SOURCES.txt
|
|
15
|
+
langid_chat.egg-info/dependency_links.txt
|
|
16
|
+
langid_chat.egg-info/entry_points.txt
|
|
17
|
+
langid_chat.egg-info/requires.txt
|
|
18
|
+
langid_chat.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
langid
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langid-chat"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A portable language detector (15 languages incl. code-mixed Indian) with a chat web UI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "orewamash" }]
|
|
13
|
+
keywords = ["language-detection", "nlp", "machine-learning", "chatbot"]
|
|
14
|
+
urls = { Homepage = "https://github.com/orewamash/langid" }
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Natural Language :: English",
|
|
18
|
+
"Topic :: Text Processing :: Linguistic",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"scikit-learn>=1.2",
|
|
22
|
+
"numpy",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
langid = "langid.cli:main"
|
|
27
|
+
langid-web = "langid.server:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
packages = ["langid"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
langid = [
|
|
34
|
+
"static/*.html",
|
|
35
|
+
"static/*.css",
|
|
36
|
+
"static/*.js",
|
|
37
|
+
"artifacts/*.pkl",
|
|
38
|
+
]
|