snackshell commited on
Commit
3ef8447
·
verified ·
1 Parent(s): c60dadc

Upload 7 files

Browse files
Files changed (4) hide show
  1. Dockerfile +0 -5
  2. README.md +9 -6
  3. apis/chat_api.py +121 -92
  4. 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 (Google)
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
- - Translate (AI model)
82
 
83
  ```bash
84
- curl -X POST http://127.0.0.1:23333/translate/ai \
85
  -H "Content-Type: application/json" \
86
- -d '{"model": "t5-base", "from_language": "en", "to_language": "fr", "input_text": "How are you?"}'
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 googletrans import Translator
 
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="fa",
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
- if item.from_language == lang_item['code']:
81
- from_lang = item.from_language
82
 
83
  if to_lang == 'auto':
84
  to_lang = 'en'
85
 
86
- if from_lang == 'auto':
87
- from_lang = translator.detect(item.input_text).lang
88
-
89
- # Map ISO/lang codes to NLLB-200 language codes
90
- nllb_code_map = {
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
- nllb_src = nllb_code_map.get(from_lang, 'eng_Latn')
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": from_lang,
135
  "to_language": to_lang,
136
  "text": item.input_text,
137
- "translate": translated,
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
- translator = Translator()
153
- detected = translator.detect(item.input_text)
 
 
154
 
155
  item_response = {
156
- "lang": detected.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
- self.app.post(
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
- googletrans==3.1.0a0
5
- torch
6
- transformers
7
- transformers[sentencepiece]
8
  requests
9
  termcolor
 
1
  fastapi
2
  pydantic
3
  uvicorn
4
+ deep-translator
 
 
 
5
  requests
6
  termcolor