Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- Dockerfile +0 -5
- README.md +9 -6
- apis/chat_api.py +121 -92
- requirements.txt +1 -4
Dockerfile
CHANGED
@@ -12,10 +12,5 @@ ENV HOME=/home/user \
|
|
12 |
WORKDIR $HOME/app
|
13 |
|
14 |
COPY --chown=user . $HOME/app
|
15 |
-
RUN mkdir -p $HOME/app/models
|
16 |
-
RUN chmod 777 $HOME/app/models
|
17 |
-
ENV MODELS_PATH=$HOME/app/models
|
18 |
-
RUN mkdir -p $HOME/app/uploads
|
19 |
-
RUN chmod 777 $HOME/app/uploads
|
20 |
|
21 |
CMD ["python", "-m", "apis.chat_api"]
|
|
|
12 |
WORKDIR $HOME/app
|
13 |
|
14 |
COPY --chown=user . $HOME/app
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
CMD ["python", "-m", "apis.chat_api"]
|
README.md
CHANGED
@@ -15,8 +15,7 @@ Multilingual Translation and Language Detection API.
|
|
15 |
✅ Implemented:
|
16 |
|
17 |
- Language detection (`/detect`)
|
18 |
-
- Translation via Google Translate (`/translate`)
|
19 |
-
- Translation via local AI models (`/translate/ai`) using Hugging Face `transformers`
|
20 |
- Docker deployment
|
21 |
|
22 |
🔤 Supported languages (primary):
|
@@ -70,7 +69,7 @@ curl -X POST http://127.0.0.1:23333/detect \
|
|
70 |
-d '{"input_text": "Hello, how are you?"}'
|
71 |
```
|
72 |
|
73 |
-
- Translate
|
74 |
|
75 |
```bash
|
76 |
curl -X POST http://127.0.0.1:23333/translate \
|
@@ -78,10 +77,14 @@ curl -X POST http://127.0.0.1:23333/translate \
|
|
78 |
-d '{"to_language": "ar", "input_text": "Hello"}'
|
79 |
```
|
80 |
|
81 |
-
-
|
82 |
|
83 |
```bash
|
84 |
-
curl -X POST http://127.0.0.1:23333/translate/
|
85 |
-H "Content-Type: application/json" \
|
86 |
-
-d '{"
|
87 |
```
|
|
|
|
|
|
|
|
|
|
15 |
✅ Implemented:
|
16 |
|
17 |
- Language detection (`/detect`)
|
18 |
+
- Translation via Google Translate (`/translate`) using `deep_translator`
|
|
|
19 |
- Docker deployment
|
20 |
|
21 |
🔤 Supported languages (primary):
|
|
|
69 |
-d '{"input_text": "Hello, how are you?"}'
|
70 |
```
|
71 |
|
72 |
+
- Translate
|
73 |
|
74 |
```bash
|
75 |
curl -X POST http://127.0.0.1:23333/translate \
|
|
|
77 |
-d '{"to_language": "ar", "input_text": "Hello"}'
|
78 |
```
|
79 |
|
80 |
+
- Stream translate (OpenAI-compatible SSE)
|
81 |
|
82 |
```bash
|
83 |
+
curl -N -X POST http://127.0.0.1:23333/translate/stream \
|
84 |
-H "Content-Type: application/json" \
|
85 |
+
-d '{"to_language": "am", "input_text": "Hello, nice to meet you!"}'
|
86 |
```
|
87 |
+
|
88 |
+
Response is a stream of `data: {json}\n\n` chunks ending with `data: [DONE]`.
|
89 |
+
|
90 |
+
|
apis/chat_api.py
CHANGED
@@ -1,19 +1,17 @@
|
|
1 |
import argparse
|
2 |
import uvicorn
|
3 |
import sys
|
4 |
-
import os
|
5 |
-
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
6 |
-
import time
|
7 |
import json
|
8 |
-
import torch
|
9 |
-
import logging
|
10 |
|
11 |
|
12 |
from fastapi import FastAPI
|
13 |
from fastapi.encoders import jsonable_encoder
|
14 |
-
from fastapi.responses import JSONResponse
|
|
|
|
|
15 |
from pydantic import BaseModel, Field
|
16 |
-
from
|
|
|
17 |
from fastapi.middleware.cors import CORSMiddleware
|
18 |
|
19 |
class ChatAPIApp:
|
@@ -37,7 +35,7 @@ class ChatAPIApp:
|
|
37 |
description="(str) `Detect`",
|
38 |
)
|
39 |
to_language: str = Field(
|
40 |
-
default="
|
41 |
description="(str) `en`",
|
42 |
)
|
43 |
input_text: str = Field(
|
@@ -47,114 +45,60 @@ class ChatAPIApp:
|
|
47 |
|
48 |
|
49 |
def translate_completions(self, item: TranslateCompletionsPostItem):
|
50 |
-
translator = Translator()
|
51 |
f = open('apis/lang_name.json', "r")
|
52 |
available_langs = json.loads(f.read())
|
53 |
-
from_lang = 'en'
|
54 |
-
to_lang = 'en'
|
55 |
-
for lang_item in available_langs:
|
56 |
-
if item.to_language == lang_item['code']:
|
57 |
-
to_lang = item.to_language
|
58 |
-
break
|
59 |
-
|
60 |
-
|
61 |
-
translated = translator.translate(item.input_text, dest=to_lang)
|
62 |
-
item_response = {
|
63 |
-
"from_language": translated.src,
|
64 |
-
"to_language": translated.dest,
|
65 |
-
"text": item.input_text,
|
66 |
-
"translate": translated.text
|
67 |
-
}
|
68 |
-
json_compatible_item_data = jsonable_encoder(item_response)
|
69 |
-
return JSONResponse(content=json_compatible_item_data)
|
70 |
-
|
71 |
-
def translate_ai_completions(self, item: TranslateCompletionsPostItem):
|
72 |
-
translator = Translator()
|
73 |
-
f = open('apis/lang_name.json', "r")
|
74 |
-
available_langs = json.loads(f.read())
|
75 |
-
from_lang = 'en'
|
76 |
to_lang = 'en'
|
77 |
for lang_item in available_langs:
|
78 |
if item.to_language == lang_item['code']:
|
79 |
to_lang = item.to_language
|
80 |
-
|
81 |
-
from_lang = item.from_language
|
82 |
|
83 |
if to_lang == 'auto':
|
84 |
to_lang = 'en'
|
85 |
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
'en': 'eng_Latn',
|
92 |
-
'am': 'amh_Ethi',
|
93 |
-
'ar': 'arb_Arab',
|
94 |
-
'ti': 'tir_Ethi',
|
95 |
-
'om': 'orm_Latn',
|
96 |
-
'so': 'som_Latn',
|
97 |
-
'ko': 'kor_Hang',
|
98 |
-
'zh-CN': 'zho_Hans',
|
99 |
-
'zh-TW': 'zho_Hant',
|
100 |
-
'fr': 'fra_Latn',
|
101 |
-
'de': 'deu_Latn',
|
102 |
-
'it': 'ita_Latn',
|
103 |
-
'ja': 'jpn_Jpan',
|
104 |
-
}
|
105 |
|
106 |
-
|
107 |
-
nllb_tgt = nllb_code_map.get(to_lang, 'eng_Latn')
|
108 |
-
|
109 |
-
if torch.cuda.is_available():
|
110 |
-
device = torch.device("cuda:0")
|
111 |
-
else:
|
112 |
-
device = torch.device("cpu")
|
113 |
-
logging.warning("GPU not found, using CPU, translation will be very slow.")
|
114 |
-
|
115 |
-
time_start = time.time()
|
116 |
-
pretrained_model = "facebook/nllb-200-distilled-1.3B"
|
117 |
-
cache_dir = "models/"
|
118 |
-
tokenizer = AutoTokenizer.from_pretrained(pretrained_model, cache_dir=cache_dir)
|
119 |
-
model = AutoModelForSeq2SeqLM.from_pretrained(pretrained_model, cache_dir=cache_dir).to(device)
|
120 |
-
model.eval()
|
121 |
-
|
122 |
-
tokenizer.src_lang = nllb_src
|
123 |
-
with torch.no_grad():
|
124 |
-
encoded_input = tokenizer(item.input_text, return_tensors="pt").to(device)
|
125 |
-
generated_tokens = model.generate(
|
126 |
-
**encoded_input,
|
127 |
-
forced_bos_token_id=tokenizer.lang_code_to_id[nllb_tgt],
|
128 |
-
)
|
129 |
-
translated_text = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
|
130 |
-
|
131 |
-
time_end = time.time()
|
132 |
-
translated = translated_text
|
133 |
item_response = {
|
134 |
-
"from_language":
|
135 |
"to_language": to_lang,
|
136 |
"text": item.input_text,
|
137 |
-
"translate":
|
138 |
-
"start": str(time_start),
|
139 |
-
"end": str(time_end)
|
140 |
}
|
141 |
json_compatible_item_data = jsonable_encoder(item_response)
|
142 |
return JSONResponse(content=json_compatible_item_data)
|
143 |
|
144 |
|
|
|
|
|
145 |
class DetectLanguagePostItem(BaseModel):
|
146 |
input_text: str = Field(
|
147 |
default="Hello, how are you?",
|
148 |
description="(str) `Text for detection`",
|
149 |
)
|
150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
def detect_language(self, item: DetectLanguagePostItem):
|
152 |
-
|
153 |
-
|
|
|
|
|
154 |
|
155 |
item_response = {
|
156 |
-
"lang":
|
157 |
-
"confidence": detected.confidence,
|
158 |
}
|
159 |
json_compatible_item_data = jsonable_encoder(item_response)
|
160 |
return JSONResponse(content=json_compatible_item_data)
|
@@ -171,16 +115,101 @@ class ChatAPIApp:
|
|
171 |
summary="translate text",
|
172 |
)(self.translate_completions)
|
173 |
|
174 |
-
|
175 |
-
prefix + "/translate/ai",
|
176 |
-
summary="translate text with ai",
|
177 |
-
)(self.translate_ai_completions)
|
178 |
|
179 |
self.app.post(
|
180 |
prefix + "/detect",
|
181 |
summary="detect language",
|
182 |
)(self.detect_language)
|
183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
class ArgParser(argparse.ArgumentParser):
|
185 |
def __init__(self, *args, **kwargs):
|
186 |
super(ArgParser, self).__init__(*args, **kwargs)
|
|
|
1 |
import argparse
|
2 |
import uvicorn
|
3 |
import sys
|
|
|
|
|
|
|
4 |
import json
|
|
|
|
|
5 |
|
6 |
|
7 |
from fastapi import FastAPI
|
8 |
from fastapi.encoders import jsonable_encoder
|
9 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
10 |
+
import uuid
|
11 |
+
import time
|
12 |
from pydantic import BaseModel, Field
|
13 |
+
from deep_translator import GoogleTranslator
|
14 |
+
from deep_translator import single_detection
|
15 |
from fastapi.middleware.cors import CORSMiddleware
|
16 |
|
17 |
class ChatAPIApp:
|
|
|
35 |
description="(str) `Detect`",
|
36 |
)
|
37 |
to_language: str = Field(
|
38 |
+
default="am",
|
39 |
description="(str) `en`",
|
40 |
)
|
41 |
input_text: str = Field(
|
|
|
45 |
|
46 |
|
47 |
def translate_completions(self, item: TranslateCompletionsPostItem):
|
|
|
48 |
f = open('apis/lang_name.json', "r")
|
49 |
available_langs = json.loads(f.read())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
to_lang = 'en'
|
51 |
for lang_item in available_langs:
|
52 |
if item.to_language == lang_item['code']:
|
53 |
to_lang = item.to_language
|
54 |
+
break
|
|
|
55 |
|
56 |
if to_lang == 'auto':
|
57 |
to_lang = 'en'
|
58 |
|
59 |
+
detected_src = None
|
60 |
+
try:
|
61 |
+
detected_src = single_detection(item.input_text)
|
62 |
+
except Exception:
|
63 |
+
detected_src = 'auto'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
+
translated_text = GoogleTranslator(source='auto', target=to_lang).translate(item.input_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
item_response = {
|
67 |
+
"from_language": detected_src,
|
68 |
"to_language": to_lang,
|
69 |
"text": item.input_text,
|
70 |
+
"translate": translated_text,
|
|
|
|
|
71 |
}
|
72 |
json_compatible_item_data = jsonable_encoder(item_response)
|
73 |
return JSONResponse(content=json_compatible_item_data)
|
74 |
|
75 |
|
76 |
+
|
77 |
+
|
78 |
class DetectLanguagePostItem(BaseModel):
|
79 |
input_text: str = Field(
|
80 |
default="Hello, how are you?",
|
81 |
description="(str) `Text for detection`",
|
82 |
)
|
83 |
|
84 |
+
class ChatTranslateStreamItem(BaseModel):
|
85 |
+
# OpenAI-style payload compatibility
|
86 |
+
model: str | None = Field(default=None, description="(optional) ignored")
|
87 |
+
stream: bool | None = Field(default=True, description="(optional) ignored")
|
88 |
+
to_language: str = Field(default="am", description="Target language code")
|
89 |
+
messages: list[dict] = Field(
|
90 |
+
default_factory=list,
|
91 |
+
description="OpenAI-style messages; the last user message's content is translated",
|
92 |
+
)
|
93 |
+
|
94 |
def detect_language(self, item: DetectLanguagePostItem):
|
95 |
+
try:
|
96 |
+
detected_lang = single_detection(item.input_text)
|
97 |
+
except Exception:
|
98 |
+
detected_lang = None
|
99 |
|
100 |
item_response = {
|
101 |
+
"lang": detected_lang,
|
|
|
102 |
}
|
103 |
json_compatible_item_data = jsonable_encoder(item_response)
|
104 |
return JSONResponse(content=json_compatible_item_data)
|
|
|
115 |
summary="translate text",
|
116 |
)(self.translate_completions)
|
117 |
|
118 |
+
# Removed AI translation endpoint
|
|
|
|
|
|
|
119 |
|
120 |
self.app.post(
|
121 |
prefix + "/detect",
|
122 |
summary="detect language",
|
123 |
)(self.detect_language)
|
124 |
|
125 |
+
self.app.post(
|
126 |
+
prefix + "/translate/stream",
|
127 |
+
summary="stream translated text (OpenAI-compatible SSE)",
|
128 |
+
)(self.translate_stream)
|
129 |
+
|
130 |
+
self.app.post(
|
131 |
+
prefix + "/translate/chat/stream",
|
132 |
+
summary="stream translated text from OpenAI-style chat payload",
|
133 |
+
)(self.translate_chat_stream)
|
134 |
+
|
135 |
+
def translate_stream(self, item: TranslateCompletionsPostItem):
|
136 |
+
f = open('apis/lang_name.json', "r")
|
137 |
+
available_langs = json.loads(f.read())
|
138 |
+
to_lang = 'en'
|
139 |
+
for lang_item in available_langs:
|
140 |
+
if item.to_language == lang_item['code']:
|
141 |
+
to_lang = item.to_language
|
142 |
+
break
|
143 |
+
|
144 |
+
if to_lang == 'auto':
|
145 |
+
to_lang = 'en'
|
146 |
+
|
147 |
+
try:
|
148 |
+
translated_full = GoogleTranslator(source='auto', target=to_lang).translate(item.input_text)
|
149 |
+
except Exception as e:
|
150 |
+
error_event = {
|
151 |
+
"id": f"trans-{uuid.uuid4()}",
|
152 |
+
"object": "chat.completion.chunk",
|
153 |
+
"choices": [
|
154 |
+
{
|
155 |
+
"index": 0,
|
156 |
+
"delta": {"content": ""},
|
157 |
+
"finish_reason": "error",
|
158 |
+
}
|
159 |
+
],
|
160 |
+
"error": str(e),
|
161 |
+
}
|
162 |
+
def error_gen():
|
163 |
+
yield f"data: {json.dumps(error_event, ensure_ascii=False)}\n\n"
|
164 |
+
yield "data: [DONE]\n\n"
|
165 |
+
return StreamingResponse(error_gen(), media_type="text/event-stream")
|
166 |
+
|
167 |
+
# Character-based streaming for natural flow in languages without spaces
|
168 |
+
chars = list(translated_full) if translated_full else []
|
169 |
+
stream_id = f"trans-{uuid.uuid4()}"
|
170 |
+
|
171 |
+
def event_generator():
|
172 |
+
for ch in chars:
|
173 |
+
chunk = {
|
174 |
+
"id": stream_id,
|
175 |
+
"object": "chat.completion.chunk",
|
176 |
+
"choices": [
|
177 |
+
{
|
178 |
+
"index": 0,
|
179 |
+
"delta": {"content": ch},
|
180 |
+
"finish_reason": None,
|
181 |
+
}
|
182 |
+
],
|
183 |
+
}
|
184 |
+
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
|
185 |
+
time.sleep(0.005)
|
186 |
+
|
187 |
+
# Stream end
|
188 |
+
yield "data: [DONE]\n\n"
|
189 |
+
|
190 |
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
191 |
+
|
192 |
+
def translate_chat_stream(self, item: ChatTranslateStreamItem):
|
193 |
+
# Extract latest user content
|
194 |
+
input_text = None
|
195 |
+
for message in reversed(item.messages or []):
|
196 |
+
if message.get("role") == "user":
|
197 |
+
input_text = message.get("content", "")
|
198 |
+
break
|
199 |
+
|
200 |
+
if not input_text:
|
201 |
+
# Fallback to empty stream end
|
202 |
+
def empty_gen():
|
203 |
+
yield "data: [DONE]\n\n"
|
204 |
+
return StreamingResponse(empty_gen(), media_type="text/event-stream")
|
205 |
+
|
206 |
+
# Reuse the streaming translator
|
207 |
+
payload = self.TranslateCompletionsPostItem(
|
208 |
+
to_language=item.to_language,
|
209 |
+
input_text=input_text,
|
210 |
+
)
|
211 |
+
return self.translate_stream(payload)
|
212 |
+
|
213 |
class ArgParser(argparse.ArgumentParser):
|
214 |
def __init__(self, *args, **kwargs):
|
215 |
super(ArgParser, self).__init__(*args, **kwargs)
|
requirements.txt
CHANGED
@@ -1,9 +1,6 @@
|
|
1 |
fastapi
|
2 |
pydantic
|
3 |
uvicorn
|
4 |
-
|
5 |
-
torch
|
6 |
-
transformers
|
7 |
-
transformers[sentencepiece]
|
8 |
requests
|
9 |
termcolor
|
|
|
1 |
fastapi
|
2 |
pydantic
|
3 |
uvicorn
|
4 |
+
deep-translator
|
|
|
|
|
|
|
5 |
requests
|
6 |
termcolor
|