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.
@@ -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"
@@ -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
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
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,3 @@
1
+ [console_scripts]
2
+ langid = langid.cli:main
3
+ langid-web = langid.server:main
@@ -0,0 +1,2 @@
1
+ scikit-learn>=1.2
2
+ numpy
@@ -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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+